mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-12 08:15:58 +00:00
Cap /app/<name> meta descriptions at 150 characters (#3698)
The app shell fed the row's full description into <meta name= "description">, 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5065bcbf0a
commit
3e8b2735bb
@@ -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)}`;
|
||||
|
||||
@@ -28,6 +28,23 @@ import type {
|
||||
LaunchOptions,
|
||||
} from '../../services/homepage/PuterHomepageService';
|
||||
|
||||
/** Longest description/og:description/twitter:description on `/app/<name>`. */
|
||||
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 `<gui_assets_root>/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 <meta name="description">, 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'
|
||||
|
||||
Reference in New Issue
Block a user