diff --git a/layers/identity/server/utils/weekly-template.ts b/layers/identity/server/utils/weekly-template.ts index 778d1ff7..985fdc16 100644 --- a/layers/identity/server/utils/weekly-template.ts +++ b/layers/identity/server/utils/weekly-template.ts @@ -437,18 +437,20 @@ function row(t: Tokens, options: { /** * A dense trending row. * - * Seven full editorial rows turned the email into a feed. The Skill name, - * exact source, ranking reason, and evidence account stay. Descriptions, - * quotes, and the duplicate source action remain in the plain-text version. + * Show a short description below the Skill name. + * Keep the source, ranking reason, and evidence account compact. + * Quotes and the duplicate source action remain in the plain-text version. */ function trendingRow(t: Tokens, options: { siteUrl: string owner: string title: string href: string + description: string | null meta: string evidence: WeeklyEvidence | null }): string { + const description = trimDescription(options.description) const evidence = options.evidence ? ` ยท @${esc(options.evidence.authorHandle)} ${options.evidence.platform === 'x' ? 'on X' : 'on Bluesky'}` : '' @@ -462,6 +464,7 @@ function trendingRow(t: Tokens, options: { ${esc(options.title)} + ${description ? `
${esc(description)}
` : ''}
${esc(options.meta)}${evidence}
@@ -709,6 +712,7 @@ export function renderWeekly(input: WeeklyRenderInput): WeeklyRender { owner: skill.owner, title: skill.canonicalName, href: sourceUrl, + description: skill.description, meta: trendingMeta(skill, now), evidence: skill.evidence, }) diff --git a/test/unit/weekly-template.test.ts b/test/unit/weekly-template.test.ts index 813870ad..45501e6e 100644 --- a/test/unit/weekly-template.test.ts +++ b/test/unit/weekly-template.test.ts @@ -217,11 +217,12 @@ describe('weekly template', () => { expect(text).not.toContain('t.co/abc') }) - it('keeps descriptions out of compact HTML rows but retains them in text', () => { + it('shortens long descriptions in HTML and text on a word boundary', () => { const long = 'Forces the laziest solution that actually works, simplest, shortest, most minimal. Channels a senior dev who has seen everything: question whether the task needs to exist at all.' const { html, text } = renderWeekly(input({ trending: [trending({ description: long })] })) - expect(html).not.toContain('Forces the laziest solution') + expect(html).toContain('Forces the laziest solution') + expect(html).not.toContain('needs to exist at all') expect(text).toContain('Forces the laziest solution') expect(text).not.toContain('needs to exist at all') // Cut on a word boundary: the last word before the ellipsis is a whole @@ -230,13 +231,30 @@ describe('weekly template', () => { expect(long).toContain(` ${tail} `) }) - it('leaves a short description in the plain-text alternative', () => { + it('shows a short description in HTML and text', () => { const { html, text } = renderWeekly(input({ trending: [trending({ description: 'Testing conventions.' })] })) - expect(html).not.toContain('Testing conventions.') + expect(html).toContain('Testing conventions.') expect(text).toContain('Testing conventions.') }) + it('escapes description markup in HTML and preserves it in text', () => { + const description = 'Use & test.' + const { html, text } = renderWeekly(input({ trending: [trending({ description })] })) + + expect(html).toContain('Use <script>alert("skill")</script> & test.') + expect(html).not.toContain('