From 3e8b2735bb860d0e56291a24bee8a3ecc61f9f8b Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Sun, 30 Aug 2026 20:17:04 -0700 Subject: [PATCH] Cap /app/ meta descriptions at 150 characters (#3698) The app shell fed the row's full description into , og:description and twitter:description. Long descriptions now collapse whitespace and cut at a word boundary with an ellipsis, never exceeding 150 characters. Co-authored-by: Claude Fable 5 --- .../homepage/HomepageController.test.ts | 41 +++++++++++++++++++ .../homepage/HomepageController.ts | 26 +++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/backend/controllers/homepage/HomepageController.test.ts b/src/backend/controllers/homepage/HomepageController.test.ts index 5e076647f..6d5a8157d 100644 --- a/src/backend/controllers/homepage/HomepageController.test.ts +++ b/src/backend/controllers/homepage/HomepageController.test.ts @@ -278,6 +278,47 @@ describe('HomepageController GET /app/:name', () => { expect(html).toContain('Cool App'); }); + it('caps description, og:description and twitter:description at 150 chars', async () => { + const { userId } = await makeUser(); + const name = `long-${Math.random().toString(36).slice(2, 10)}`; + const longDescription = + 'This drawing app has brushes, layers, filters and more. '.repeat( + 8, + ); + await server.stores.app.create( + { + name, + title: 'Wordy App', + description: longDescription, + index_url: `https://example.com/${name}/`, + }, + { ownerUserId: userId }, + ); + + const { res, captured } = makeRes(); + await callRoute( + 'get', + '/app/:name', + makeReq({ params: { name }, path: `/app/${name}` }), + res, + ); + + const html = String(captured.body); + for (const tag of [ + 'name="description"', + 'property="og:description"', + 'name="twitter:description"', + ]) { + const match = new RegExp(`${tag} content="([^"]*)"`).exec(html); + expect(match, `no ${tag} tag`).toBeTruthy(); + const content = match![1]; + expect(content.length).toBeLessThanOrEqual(150); + expect(content.endsWith('…')).toBe(true); + // Cut at a word boundary, not mid-word. + expect(longDescription).toContain(content.slice(0, -1)); + } + }); + it('does not leak a private app index_url or owner id to an anonymous visitor', async () => { const { userId } = await makeUser(); const name = `priv-${Math.random().toString(36).slice(2, 10)}`; diff --git a/src/backend/controllers/homepage/HomepageController.ts b/src/backend/controllers/homepage/HomepageController.ts index e9e3a3b87..de3fa96b2 100644 --- a/src/backend/controllers/homepage/HomepageController.ts +++ b/src/backend/controllers/homepage/HomepageController.ts @@ -28,6 +28,23 @@ import type { LaunchOptions, } from '../../services/homepage/PuterHomepageService'; +/** Longest description/og:description/twitter:description on `/app/`. */ +export const APP_META_DESCRIPTION_MAX = 150; + +/** + * App descriptions can run long; search snippets and social cards want a + * short blurb. Collapses whitespace, then cuts at a word boundary with an + * ellipsis so the result never exceeds `APP_META_DESCRIPTION_MAX`. + */ +export function appMetaDescription(text: string): string { + const clean = text.replace(/\s+/g, ' ').trim(); + if (clean.length <= APP_META_DESCRIPTION_MAX) return clean; + const head = clean.slice(0, APP_META_DESCRIPTION_MAX - 1); + const wordEnd = head.lastIndexOf(' '); + const cut = wordEnd > 0 ? head.slice(0, wordEnd) : head; + return `${cut.replace(/[\s,;:.!?…—-]+$/, '')}…`; +} + /** * Routes that render the Puter GUI shell, plus a catch-all static fallback * under `/src` for non-dist/src paths (images, fonts, lib @@ -114,10 +131,15 @@ export class HomepageController extends PuterController { // through the apps driver before launching, and that read is // where the entitlement gate and hosted-backing guard run. const clientApp = toAppShellView(app); + // Feeds , og:description and + // twitter:description; scrapers truncate long blurbs anyway. + const description = appMetaDescription( + String(app.description ?? ''), + ); await sendShell(req, res, { title: String(app.title ?? name), - description: String(app.description ?? ''), - short_description: String(app.description ?? ''), + description, + short_description: description, icon: typeof app.icon === 'string' ? app.icon : undefined, social_media_image: typeof metadata.social_image === 'string'