diff --git a/COPY.md b/COPY.md index f3b3d662..93e153c3 100644 --- a/COPY.md +++ b/COPY.md @@ -22,7 +22,7 @@ These exact strings. Do not paraphrase them per page. | Tagline | Curated agent skills by humans. | guidance only: the five-word product summary. The homepage and its OG card use the Home H1 instead | | Site description | Curated agent skills by humans, written by real maintainers in their own GitHub repos | `nuxt.config.ts` site description (feeds every page's meta description) | | Elevator pitch | Skilld is a curated registry of agent skills written by real people in the GitHub repos you already depend on. One install command, every agent; watch your stack and get a digest when it changes. | guidance only: no page ships it yet (use it where two sentences fit) | -| Home H1 | Agent skills for you and your agent | guidance only: the homepage H1, and the title of the homepage OG card. The page draws the rose brand dot as its full stop, so never type a period. The card types no full stop either, because its lockup dot is its one rose element. It breaks the line after "for you", as the page does. Its alt text is "skilld, agent skills for you and your agent" | +| Home H1 | Agent skills for you and your agent | index H1, index OG image alt | | Home verb line | search · run · install · keep current | guidance only: the homepage line under the H1. Each verb links to its `/cli` section: `#search`, `#run`, `#install`, `#update` | | Home CLI link | The skilld CLI → | guidance only: the homepage link to `/cli` at the end of the verb line. The arrow is an icon | | Home caption | Try any skill before you install it. Your agent can search for its own. | guidance only: the homepage caption under the verb line. It assumes the reader knows what a Skill is, so it names only what sets skilld apart. "Search for its own" is the skilld Skill behind the promo label. The homepage OG card uses it as its description | @@ -57,6 +57,8 @@ These exact strings. Do not paraphrase them per page. | Claim: privacy | analytics without cookies or IPs | guidance only: analytics and privacy copy. Never the bare "privacy-friendly" | | Claim: comparison | skills.sh alternative | guidance only: always a link to `/vs/skills-sh` | +The homepage H1 is also the title of the homepage OG card. The page draws the rose brand dot as its full stop, so never type a period. The card types no full stop either, because its lockup dot is its one rose element. Both break the line after "for you". + The homepage deck has no collection section. Its old heading, "Install a collection in one command.", is retired. ### The product in four lengths diff --git a/DESIGN.md b/DESIGN.md index 102999c1..68c2326b 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -339,7 +339,7 @@ Rules for every texture: 5. **One per region.** No other motion runs in the same viewport region as a moving texture. 6. **Converge is the loader.** At small sizes the file minimap looks like a skeleton loader. Do not use it as one. -**On OG cards.** An OG card is the homepage hero as one still frame, drawn server side. The OG renderer has only Plus Jakarta Sans and IBM Plex Mono, and neither has braille glyphs, so the texture is SVG dots, never a glyph or a canvas. `OgLayout` draws the frame for every card: the lockup centred at the top, the content centred under it, and Braille names in the margins. The field resolves toward the lockup's dot, which is the card's one rose element. The title takes the hero H1's face, Plus Jakarta Sans 600 at −0.045em. A Skill name has no space to wrap at, so it shrinks to fit one line. Text that needs two lines breaks into two of even length, since the renderer's `text-wrap: balance` pushes the lines off centre. `app/utils/og-texture.ts` draws the dots, `app/utils/og-style.ts` holds the type, and `OgLines` draws the even lines. +**On OG cards.** An OG card is the homepage hero as one still frame, drawn server side. The OG renderer has only Plus Jakarta Sans and IBM Plex Mono, and neither has braille glyphs, so the texture is SVG dots, never a glyph or a canvas. `OgLayout` draws the frame for every card: the lockup centred at the top, the content centred under it, and a sparse Braille names field in the margins. The content column stays clear from top to bottom, so the field reads as a quiet dusting and never resolves into letters. The lockup's dot is the card's one rose element. The title takes the hero H1's face, Plus Jakarta Sans 600 at −0.045em. A Skill name has no space to wrap at, so it shrinks to fit one line. Text that needs two lines breaks into two of even length, since the renderer's `text-wrap: balance` pushes the lines off centre. `app/utils/og-texture.ts` draws the dots, `app/utils/og-style.ts` holds the type, and `OgLines` draws the even lines. | Card | Content | | --- | --- | diff --git a/app/components/OgLayout.vue b/app/components/OgLayout.vue index 8fa88dd9..8e2dd96d 100644 --- a/app/components/OgLayout.vue +++ b/app/components/OgLayout.vue @@ -4,9 +4,9 @@ import { OG_COLUMN_WIDTH } from '../utils/og-style' import { brailleNamesField, textureSvg } from '../utils/og-texture' // Every OG card is the homepage hero as one still frame: the lockup centred at -// the top, the card's content centred under it, and the braille names field -// in the margins. The field resolves toward the lockup's dot, which stays the -// card's one rose element. +// the top, the card's content centred under it, and a sparse braille names +// field in the margins. The clear column covers the lockup, so the field stays +// a quiet dusting there and the lockup's dot stays the card's one rose element. const WIDTH = 1200 const HEIGHT = 600 const LOCKUP_SIZE = 34 diff --git a/app/utils/og-style.ts b/app/utils/og-style.ts index 3361e900..039039f1 100644 --- a/app/utils/og-style.ts +++ b/app/utils/og-style.ts @@ -35,18 +35,21 @@ export function ogTitleSize(text: string, options: { max: number, min: number, l * Splits text that needs two lines at the space that makes them closest in * length, so centred text never leaves one word alone. The renderer's own * `text-wrap: balance` shifts balanced lines off centre, so the card breaks - * them itself. A line break in the text wins, for a title that should break - * where its page breaks it. Text that fits one line, or needs more than - * `lines`, comes back whole to wrap and clamp on its own. + * them itself. A line break in a title wins, for a title that should break + * where its page breaks it. Body text comes from bios, notes and textareas + * with line breaks of its own, so it collapses to one paragraph first. Text + * that fits one line, or needs more than `lines`, comes back whole to wrap + * and clamp on its own. */ export function ogEvenLines(text: string, options: { fontSize: number, face: OgFace, lines: number }): string[] { const { fontSize, face, lines } = options - if (text.includes('\n')) - return text.split('\n').slice(0, lines) - const words = text.split(' ') - const needed = Math.ceil((text.length * FACE_EM[face] * fontSize) / OG_COLUMN_WIDTH) + if (face === 'title' && text.includes('\n')) + return text.split('\n').map(line => line.trim()).filter(Boolean).slice(0, lines) + const flat = text.replace(/\s+/g, ' ').trim() + const words = flat.split(' ') + const needed = Math.ceil((flat.length * FACE_EM[face] * fontSize) / OG_COLUMN_WIDTH) if (lines < 2 || needed !== 2 || words.length < 2) - return [text] + return [flat] let split = 1 let longest = Number.POSITIVE_INFINITY for (let k = 1; k < words.length; k++) { diff --git a/test/unit/og-style.test.ts b/test/unit/og-style.test.ts index a2023131..21df430f 100644 --- a/test/unit/og-style.test.ts +++ b/test/unit/og-style.test.ts @@ -15,6 +15,11 @@ describe('ogEvenLines', () => { .toEqual(['Agent skills for you', 'and your agent']) }) + it('collapses the line breaks body text brings with it', () => { + expect(ogEvenLines('Para one about the collection.\r\n\r\nPara two goes here.', { fontSize: 28, face: 'body', lines: 2 })) + .toEqual(['Para one about the collection. Para two goes here.']) + }) + it('leaves text whole when it fits one line', () => { expect(ogEvenLines('The skilld CLI', { fontSize: 84, face: 'title', lines: 2 })).toEqual(['The skilld CLI']) })