The Lossless Group's pattern for repo-level splash pages — small Astro sites at <repo>/splash/ that ship to GitHub Pages on push to main, render the repo's changelog/ + context-v/ alongside curated marketing copy, and stay isolated from any package the repo also publishes. Use proactively whenever scaffolding a noteworthy new repo (every "important" repo wants one), when shipping a coherent chunk of work that an external reader would land on, when adding a feature (search, sort, tags row, theme mode) to an existing splash, when converting a legacy apps/<name>/ site to splash/, when troubleshooting a Pages deploy, or when the user mentions "splash", "GitHub Pages", "lossless-group.github.io", "Pagefind on our site", or working under a splash/ directory. Codifies the proven shape across three reference implementations (memopop-site, content-farm/splash, lfm/splash) and the package-isolation discipline that keeps splashes safe to add to repos that also publish to JSR/npm.
A splash is the source repo's own GitHub Pages presence — distinct from any future custom-domain marketing site. The directory is named splash/ precisely to keep that linguistic space open.
Status: Draft. This skill codifies the pattern after three confirmed instances. It pairs with a sibling habit titled "Maintain a Github Splash Page for each Repo" that explains the why; this skill is canonical for the how. The habit currently lives under lossless-monorepo/context-v/habits/ but may move — search for it by title before assuming a path. Always cross-reference the most recent reference implementation (currently lfm/splash/) before scaffolding fresh.
Reasoning step before implementation: does this repo deserve a splash?
The convention is "every important repo." That word is doing work — not every repo earns one. Use this rough cut:
Yes, scaffold a splash:
The repo will be shared with people outside the team (clients, collaborators, hiring loops, conference talks)
The repo has — or will have — a non-trivial changelog/ worth surfacing
The repo publishes a package (npm/JSR) and you'd like an installable-package landing page that isn't just the registry's auto-generated one
The repo is an Astro Knots site or feature
The repo is a study, blueprint, or reference that other repos will link to
Someone has asked "where's the README rendered?" or "is there a page for this?"
Probably not (yet):
The repo is purely internal scratch — experiments, sandboxes, throwaway studies that aren't meant to outlive the week
The repo is a fork where we're contributing upstream
The repo is a private tool with no external audience
When in doubt, ask the user. The cost of not having a splash is "no public face" — fixable later. The cost of having a stale, unmaintained one is worse than nothing.
Surface the question proactively. When working in a repo that meets the yes criteria but doesn't have a splash, mention it. ("This repo doesn't have a splash yet; want me to scaffold one?") That's how the convention spreads.
Second reasoning step: visual posture — codified or creative?
Splash pages are one of the few places in the Lossless ecosystem where aesthetic experimentation is explicitly encouraged, not just tolerated. Astro Knots sites have their own marketing identities. Packages have design languages tied to their function. Splashes are deliberately the place where individual repos can have distinct feels — and where agent creativity in visual design and layout is wanted, not feared.
Before laying out a single component, ask the user this question:
"Are we following a design pattern from a reference splash, or are we getting creative on visual design?"
The two answers warrant different work, and the answer should be explicit:
"Follow the reference": Lift the closest sibling splash structurally and adapt only what the new repo's content requires. Same hero composition, same type stack, same card chrome — fresh content, recognizable surface. Faster, lower-risk.
"Get creative": Treat this as the experimentation surface it's meant to be. Diverge in shape, not just in color. Reach for typographic moves, layout moves, ornament moves — not just remap the brand-spine tokens to different hues.
The trap to watch for
The recurring failure mode across the first three splashes (before this one) was visible in retrospect: each new splash inherited the previous one's hero composition, card geometry, and ornament strategy, then renamed the brand-spine variables to "make it different." The output: the same aesthetic three times in three palettes.
Recognizable signs you're sliding into the trap:
The hero is centered with stacked headline + tagline + CTA — because the last splash was
The cards have rounded glass with a glow shadow — because the last splash had them
The background is a radial mesh with a faint grid — because the last splash used one
The display + sans pairing is the obvious modern default — because that's what gets reached for
You're picking colors but not picking moves
When you notice this happening, stop, surface, and propose.
Surface a posture proposal before laying out components
When the user says "get creative" (or doesn't specify and the splash matters enough to diverge), send a one-paragraph posture proposal before writing any layout. Name the axes you're considering:
"For this splash I'm thinking light-mode default + serif display (Newsreader) + asymmetric hero with the diagram dominant on the right + paper-grain background with a manuscript margin rule + literary voice in the copy. That makes it visibly distinct from memopop (dark, sans, centered, radial mesh, matter-of-fact) and content-farm/splash. Sound right, or want to push in a different direction?"
Cheap to send, gets the user a chance to nudge before code lands, and reinforces that aesthetic posture is an explicit decision, not a default.
The detailed what's locked vs. what's free table lives further down under Visual identity — divergent by design. Read it before drafting the proposal.
When to use this skill
The user asks to scaffold a splash for a new repo
The user wants to add search, sort, tags, mode toggle, or any other feature to an existing splash
Converting a legacy apps/<name>/ site to the canonical splash/ location
Troubleshooting a GitHub Pages deploy of a splash
The user mentions "splash", "GitHub Pages", "lossless-group.github.io//", "Pagefind on our site"
You're working anywhere under a splash/ directory in any Lossless repo
The user wants to verify a splash meets the convention ("is this splash done?")
Composes with other skills
Skills this one always pulls along:
astro-knots — framework rules, hard prohibitions (no React, no JSX). The splash is built in Astro.
theme-system — two-tier tokens, three-mode contract. The splash uses these.
changelog-conventions — changelog/ structure. The splash renders changelog/.
pseudomonorepos — search-first behavior, parent-vs-child level discipline. Especially for the rollup variant.
Reference implementations
Three confirmed splashes as of writing. Read at least one end-to-end before scaffolding a new one:
Repo
Variant
Path
Notable
ai-labs/memopop-ai/apps/memopop-site/
Single-project (legacy location, predates the habit)
apps/<name>/
First instance. Has Pagefind search.
content-farm/splash/
Pseudomonorepo (rollup)
splash/
Canonical reference. Submodule rollup via GitHub Content API.
lfm/splash/
Single-project, package-publishing repo
splash/
Second Pagefind instance. Asymmetric hero. Package-isolation discipline.
The habit names content-farm/splash/ as the reference implementation — read its README.md end-to-end.
Variants
Pick once, stick with it:
Single-project repo — no submodules. content.config.ts reads ../changelog and ../context-v directly. No src/rollup/, no scripts/rollup-sync.ts, no splash/.env. Examples: lfm/splash/.
Pseudomonorepo — children as submodules. Splash aggregates each child's changelog/ and context-v/ via the GitHub Content API. Sync-on-demand (pnpm rollup:sync), never sync-on-build. Examples: content-farm/splash/.
Package-publishing repo — repo also publishes to JSR/npm. Splash must be excluded from publish allowlists. See Package isolation below. Examples: lfm/splash/.
The variants compose: lfm is single-project + package-publishing. content-farm is pseudomonorepo + non-publishing.
Locked conventions
These are deliberate. Don't drift without a reason.
Tech & structure
Astro. No React, no JSX. (See astro-knots.)
Directory name:splash/ at repo root. Notsite/. Notapps/<name> (legacy memopop only). The name preserves linguistic space for a future custom-domain marketing site.
Package name:<repo>-splash. private: true. Astro is the only required dep at minimum; astro-pagefind + pagefind if search is enabled.
TypeScript path aliases in tsconfig.json: @components/*, @layouts/*, @loaders/*, @lib/*, @styles/*, @content/*, @pages/*, @/*. Loader code never needs ../../../ guesswork.
Build & host
Host: GitHub Pages, project-page form. Live URL pattern: https://lossless-group.github.io/<repo>/.
astro.config.mjs:site: 'https://lossless-group.github.io', base: '/<repo>/', trailingSlash: 'ignore', build.format: 'directory'. Custom domain later? Set site to the domain and base to '/'.
Build trigger: push to main. Aligns with the development → main → master tier model.
Deploy action:actions/deploy-pages@v4 with actions/configure-pages@v5 using enablement: true so the workflow bootstraps Pages on first run.
No submodule fetching in CI even for pseudomonorepos — rolled-up content is pre-synced and committed; CI does pure file IO.
Local dev
pnpm install --ignore-workspace — required because the parent monorepo's pnpm-workspace.yaml does not include splash sites; the splash installs deps independently.
pnpm dev respects the configured base; visit http://localhost:4321/<repo>/.
For Pagefind: search index is built at deploy time only — pnpm dev shows the search box mounted with a missing-state message. Run pnpm build && pnpm preview to exercise search locally.
Package isolation (for repos that publish to JSR / npm)
This was the design constraint that determined whether the splash could be added to LFM at all. The boundary is enforced by explicit allowlists, not by what exists in the working tree:
splash/package.json carries "private": true as defense-in-depth — the boundary is the allowlists, the private flag is the suspenders to the allowlist's belt. Both belong.
When scaffolding a splash for a publishing repo: verify all four boundaries before declaring the work done. It's cheap to check; the failure mode (publishing splash files to npm) is hard to undo.
Content schemas — lenient, never throw
Every field in content.config.ts uses z.preprocess to coerce empty strings, nulls, and unexpected types gracefully. Schemas never throw on legacy entries — they safeParse and store raw frontmatter as a fallback. Pages stay defensive too: any code that calls .getTime() on a date goes through a toDate(unknown) helper because the loader's "store raw" fallback path means strings can arrive where Dates are expected.
The lenient preprocessor stack to lift verbatim:
lenientString — coerces empty strings and nulls to undefined
lenientStringArray — coerces a single string to a one-item array, accepts arrays as-is. Important: the changelog-conventions skill specifies augmented_with is a list field. Use lenientStringArray for it, not lenientString. Older legacy entries used a bare string; the lenient array preprocessor accepts both forms.
lenientDate — accepts Date | number | string; tolerates "[]", "~", "TBD", "tbd" as undefined; never throws
lenientNumber, lenientBoolean — same pattern
Schemas use .passthrough() so unknown fields (site_uuid, slug, image_prompt, usageCount, usage_count, etc.) ride along without validation noise.
The defensive-rendering pair
Lenient schemas alone aren't enough. When schema validation fails for any reason, the loader's safeParse falls back to raw frontmatter — which means dates arrive as strings, not Date objects. Every render-side call site that touches a date must go through a toDate(unknown): Date | undefined helper in @lib/date.ts. Sort code that calls .getTime() directly on a possibly-string field will crash the page; defensive code never does.
// @lib/date.ts
export function toDate(v: unknown): Date | undefined {
if (v instanceof Date) return Number.isNaN(v.getTime()) ? undefined : v;
if (typeof v === 'number') {
const d = new Date(v);
return Number.isNaN(d.getTime()) ? undefined : d;
}
if (typeof v === 'string') {
const t = v.trim();
if (t === '' || t === '[]' || t === '~' || t === 'TBD' || t === 'tbd') return undefined;
const d = new Date(t);
return Number.isNaN(d.getTime()) ? undefined : d;
}
return undefined;
}
The lenient schemas + defensive renderers are a pair. Skipping either one creates page-crash classes that only surface when a particular legacy entry happens to violate the schema.
The full pattern is in lfm/splash/src/content.config.ts and lfm/splash/src/lib/date.ts — copy and adapt, don't re-invent.
Search — Pagefind by default
The convention is set: every splash ships search-by-default. The exploration astro-knots/context-v/explorations/Implementing-Full-Text-Search-by-Default.md named Pagefind as the strong default; we've now confirmed it twice (memopop-site, lfm/splash).
SearchBox.astro component: lift verbatim from lfm/splash/src/components/SearchBox.astro or memopop-site/src/components/SearchBox.astro. Two variants — compact (header popover) and full (search page). <details> for the popover (no JS state needed). Global / keyboard shortcut.
/search page: full-panel SearchBox with autoFocus.
Header: mount <SearchBox compact /> in the actions area; add a Search nav link to /search/.
data-pagefind-* placement
Critical Pagefind behavior to know: once any page on the site uses data-pagefind-body, only pages with that marker are indexed. Pages without it are silently skipped. This is desirable here — we want results pointing at content, not list/home/search/navigation surfaces. But if you're debugging "why is page X not in the index," check whether some other page on the site has data-pagefind-body set, which made all unmarked pages opt-out.
data-pagefind-body on <main>, data-pagefind-meta="title:...", hidden <span data-pagefind-filter="kind:Changelog|Context">, hidden <span data-pagefind-filter="tag:..." per tag, data-pagefind-ignore on chrome (back link, meta line, status pill row)
List pages (/changelog/, /context-v/)
data-pagefind-ignore="all" on the <ul>
Home / search / index pages
Nothing — they're not indexable since none have data-pagefind-body
Filter taxonomy
The convention has two facet keys:
kind: — Changelog | Context (and Notes in legacy memopop). One value per page.
tag: — emits one hidden filter span per entry tag. Train-Case values per the changelog/context-v conventions.
Pseudomonorepos add a third facet: from:<peer> — peer-app or submodule slug. Single-project splashes drop it; conventions still hold.
UI overrides — token-driven
Pagefind's default UI exposes CSS variables. Map them to your semantic tokens so the search UI pivots through light/dark/vibrant modes with the rest of the site:
Don't fork the UI. Variable overrides have been sufficient through three implementations.
Sort controls on list pages
Both /changelog/ and /context-v/ carry a <SortControls> component above the list. Default sort: date_modified descending (newest first). Options: Modified | Created | Published | Title.
Wire-up:
Server pre-sorts by the default key/direction so the static HTML matches the UI default — page reads correctly even before JS hydrates or with JS off.
Each <li> carries data-sort-modified, data-sort-created, data-sort-published, data-sort-title attrs. ISO date strings sort lexicographically (correct as time-order); titles are lowercased for stable alpha sort. Empty values always sink to the bottom regardless of direction.
Each <ul> carries data-sort-target="<page-name>" so the SortControls JS can find all sortable lists on the page (multiple <ul>s on grouped views like context-v).
Persistence: per-page in localStorage under lfm-splash-sort:<name> (or analogous). <page-name> is the namespace — changelog and context-v get independent prefs.
Direction label adapts to value type: dates show "Newest first / Oldest first", titles show "A → Z / Z → A".
Lift lfm/splash/src/components/SortControls.astro directly. The component is generic — takes a name, options[], defaultKey, defaultDirection.
Tags row in list previews
Each <li> in list views renders the entry's tags below the lede, wrapping to multiple lines as needed. No slice limit by default — show every tag. (Memopop-site sliced to the first 5; the newer convention is "let it wrap" — entries with many tags shouldn't have most hidden, since tags are a primary discoverability surface.) Train-Case values come straight from frontmatter tags: arrays. Visual: small mono-font chips with hairline borders, flex-wrap: wrap.
Each splash should look distinct from its siblings while keeping the same architectural shape. The structural shape is locked because affordance consistency matters; the aesthetic surface is free because individual-project feel matters. The discipline is to know which is which.
What's locked (don't break) vs. what's free (encouraged to diverge)
A reader navigating between two Lossless splashes back-to-back should feel the family resemblance — same affordances, same conventions, search in the same place, mode toggle in the same place — but each splash should also feel like its own thing, not a recolor of the last one.
The first two splashes shipped before the divergence discipline was named — they came out feeling like the same site twice. The lfm splash was the first to break the mold deliberately. Treat that as the bar going forward, not the ceiling — the next splash should feel different from all three.
Concrete divergence axes worth pushing on
Don't just pick colors. Pick moves:
Mode default: light, dark, vibrant. Each has a different voice.
Mix freely across axes; pulling from the same column on every axis is the trap. If the new splash's row in the table above looks like a copy of an existing splash's row, push harder.
Acceptance — "this repo has a splash"
Verify before declaring the habit met (lifted from the habit doc, slightly tightened):
splash/ directory exists at repo root
splash/astro.config.mjs has correct site and base: '/<repo>/'
splash/package.json has "private": true and the <repo>-splash name
splash/tsconfig.json declares the path aliases
splash/README.md documents local dev, deploy, package isolation (if applicable), and where content lives
pnpm install --ignore-workspace && pnpm build succeeds from a clean clone
splash/dist/ includes routes for /, /search/, /changelog/, /context-v/, plus per-entry detail routes when entries exist
splash/dist/pagefind/pagefind-entry.json exists with page_count matching the number of detail pages
.github/workflows/pages.yml exists, builds splash/, deploys via actions/deploy-pages@v4, uses actions/configure-pages@v5 with enablement: true
GitHub Pages source is set to "GitHub Actions" in repo settings
First deploy reaches https://lossless-group.github.io/<repo>/ and loads cleanly
Mode toggle works: light/dark/vibrant all render without FOUC, persist across reloads
Search returns results when typing into the header popover or /search page
Sort controls reorder correctly on /changelog/ and /context-v/; default is date_modified descending
(Pseudomonorepos only) pnpm rollup:sync runs locally; splash/src/rollup/ is committed; rolled-up content appears with provenance
(Publishing repos only) Verify all four package-isolation boundaries — JSR publish.include, npm files, build entry list, parent workspace exclusion
Maintenance cadence
On every shipped change — author a changelog/ entry; surfaces on the splash on next deploy
(Pseudomonorepos) when a child ships — run pnpm rollup:sync, commit src/rollup/, push
When the curated gallery drifts — edit src/content/<thing>-highlights/. New item? Add a file. Retired? Delete it.
Periodically (e.g. weekly) — sync rollup to catch upstream drift even without a triggering event
Typical flow when scaffolding a new splash
Walk the tree. Per pseudomonorepos, search for prior splash work in the repo and parent levels. Note any context-v entries about search, theming, or splash-specific design decisions.
Pick the variant — single-project, pseudomonorepo, or package-publishing (or combination).
Read at least one reference splash end-to-end — the habit doc names content-farm/splash/. For single-project + publishing, lfm/splash/ is closest.
Scaffold the directory matching the reference layout. Lift astro.config.mjs, package.json, tsconfig.json, BaseLayout.astro, Header.astro, MetaTags.astro, ModeToggle.astro, SearchBox.astro, SortControls.astro, frontmatter.ts directly.
Resolve the visual posture with the user — ask: "following a reference or getting creative?" If creative, propose the divergence axes (mode default, type pairing, hero composition, card chrome, ornament, voice) before laying out components. Stay distinct from sibling splashes; reuse the semantic-token contract.
Wire the content collections with lenient schemas pointing at ../changelog and ../context-v. Use the proven preprocessor stack.
Add Pagefind — data-pagefind-body on detail pages, data-pagefind-ignore="all" on list teasers, kind: + tag: filter spans.
Add the GitHub Pages workflow — .github/workflows/pages.yml with actions/deploy-pages@v4.
Write splash/README.md — local dev, deploy, package isolation table (if publishing), where to edit content.
Verify acceptance criteria — run through the checklist above before declaring done.
Author a changelog entry at the repo root for the splash addition. Per changelog-conventions, lead with ## Why Care? then ## What's New?.
For publishing repos — run the parent's build (pnpm build, tsup, etc.) once more to confirm the splash hasn't broken the package.
Lessons learned (from the lfm/splash session)
These are the gotchas worth flagging in advance so the next implementation doesn't rediscover them.
augmented_with is a list field. The changelog-conventions skill says "same ul-list preference as authors." Schemas that declare it as a lenientString will trip safeParse on convention-correct entries, kicking the loader into raw-frontmatter fallback. Always lenientStringArray.
Schema validation failure → date strings, not Date objects. When a schema falls back to raw frontmatter, every date field arrives as a string. Render-side sort and format code must go through toDate(unknown). The lenient schema and the defensive renderer are a pair — skipping either is a deferred crash.
Pagefind's "once one page has data-pagefind-body, only marked pages are indexed." Add the marker to the routes you actually want indexed (changelog/[...slug], context-v/[...slug]). The home, list, and search pages will silently drop out of the index — which is what you want, but be ready to explain it.
Tags row: no slice limit. Wrap, don't truncate.
Default sort by date_modified descending — server-pre-sort to match the UI default. Static HTML must read correctly even with JS off; the SortControls component reorders client-side once mounted, but the initial render needs to already be in default order.
Sort UI persists per-page in localStorage under namespaced keys (lfm-splash-sort:changelog, lfm-splash-sort:context-v). Different pages have different prefs.
Walk the tree before re-deciding architecture questions. Per pseudomonorepos. The exploration astro-knots/context-v/explorations/Implementing-Full-Text-Search-by-Default.md exists for a reason — read it before relitigating the search-engine choice.
Second instance crystallizes a convention. Memopop-site shipped Pagefind first as an exploration; lfm/splash shipped it as a confirmed pattern. When you find yourself implementing something "just like the last splash," that's the moment to update the skill (and any companion blueprint) so the third instance doesn't have to rediscover the choices.
Light is a legitimate default mode. Memopop defaults dark; lfm defaults light. The three-mode contract is symmetric — pick the default that fits the splash's voice.
Single <details> element is enough for the search popover. No JS state machine needed for "open on click, close on outside-click." The <details data-search-compact> pattern from SearchBox.astro is the cleanest version.
Render the existing changelog conventions field-for-field. When wiring a changelog/ list, remember the conventions skill's mandatory frontmatter (8 fields including lede). The splash should display all of them gracefully — but never crash if a legacy entry is missing several.
Convention fixes belong in the schema, not in per-page code. When you find a frontmatter-shape bug (e.g. augmented_with mismatch), fix it in content.config.ts so every page benefits. Don't paper over it at the call site.
Aesthetic divergence is the goal, not a risk to manage. The first three splashes (before lfm) ended up looking like each other — same hero shape, same card geometry, same ornament strategy, with brand-spine recolors layered on top. The path out: ask the user upfront whether to follow a reference or get creative, propose the divergence axes explicitly, and diverge in shape, not just in hue. When agent creativity in visual design produces something genuinely distinct, that's the right outcome — splashes are the experimentation surface where this kind of move belongs.
What this skill deliberately is not
Not the design system. The component shapes here are minimum viable; nothing prevents richer per-splash UI (search hotkeys, animated mode transitions, custom filters). The skill defines the floor of consistency.
Not a runtime dependency. Every component is meant to be lifted/copied. We don't ship a @lossless-group/splash-kit package — that would couple splashes through versioning. Per the Astro Knots philosophy: copy-pattern, not runtime dep.
Not the eventual marketing site. When a project deserves a custom-domain marketing site, build that separately. The splash stays put as the source repo's own Pages presence.
See also
Habit:"Maintain a Github Splash Page for each Repo" — currently at lossless-monorepo/context-v/habits/. The why. Search by title; the path may evolve.
Spec:content-farm/context-v/specs/Github-Splash-Page-for-Content-Farm.md — the spec the canonical reference implements.
Search exploration:astro-knots/context-v/explorations/Implementing-Full-Text-Search-by-Default.md — establishes Pagefind as the default for Astro Knots. The lfm/splash work is the second confirmed instance; promoting to a blueprint is the natural next step.
1---2name: maintain-splash-pages3description: The Lossless Group's pattern for repo-level splash pages — small Astro sites at <repo>/splash/ that ship to GitHub Pages on push to main, render the repo's changelog/ + context-v/ alongside curated marketing copy, and stay isolated from any package the repo also publishes. Use proactively whenever scaffolding a noteworthy new repo (every "important" repo wants one), when shipping a coherent chunk of work that an external reader would land on, when adding a feature (search, sort, tags row, theme mode) to an existing splash, when converting a legacy apps/<name>/ site to splash/, when troubleshooting a Pages deploy, or when the user mentions "splash", "GitHub Pages", "lossless-group.github.io", "Pagefind on our site", or working under a splash/ directory. Codifies the proven shape across three reference implementations (memopop-site, content-farm/splash, lfm/splash) and the package-isolation discipline that keeps splashes safe to add to repos that also publish to JSR/npm.4---56# Maintain Splash Pages78> A splash is the source repo's own GitHub Pages presence — distinct from any future custom-domain marketing site. The directory is named `splash/` precisely to keep that linguistic space open.910**Status: Draft.** This skill codifies the pattern after three confirmed instances. It pairs with a sibling **habit** titled *"Maintain a Github Splash Page for each Repo"* that explains the *why*; this skill is canonical for the *how*. The habit currently lives under `lossless-monorepo/context-v/habits/` but may move — search for it by title before assuming a path. Always cross-reference the most recent reference implementation (currently `lfm/splash/`) before scaffolding fresh.1112## Reasoning step before implementation: does this repo deserve a splash?1314The convention is "every important repo." That word is doing work — not every repo earns one. Use this rough cut:1516**Yes, scaffold a splash:**17- The repo will be shared with people outside the team (clients, collaborators, hiring loops, conference talks)18- The repo has — or will have — a non-trivial `changelog/` worth surfacing19- The repo publishes a package (npm/JSR) and you'd like an installable-package landing page that isn't just the registry's auto-generated one20- The repo is an Astro Knots site or feature21- The repo is a study, blueprint, or reference that other repos will link to22- Someone has asked "where's the README rendered?" or "is there a page for this?"2324**Probably not (yet):**25- The repo is purely internal scratch — experiments, sandboxes, throwaway studies that aren't meant to outlive the week26- The repo is a fork where we're contributing upstream27- The repo is a private tool with no external audience2829**When in doubt, ask the user.** The cost of *not* having a splash is "no public face" — fixable later. The cost of having a stale, unmaintained one is worse than nothing.3031**Surface the question proactively.** When working in a repo that meets the *yes* criteria but doesn't have a splash, mention it. ("This repo doesn't have a splash yet; want me to scaffold one?") That's how the convention spreads.3233## Second reasoning step: visual posture — codified or creative?3435Splash pages are one of the few places in the Lossless ecosystem where **aesthetic experimentation is explicitly encouraged, not just tolerated.** Astro Knots sites have their own marketing identities. Packages have design languages tied to their function. Splashes are deliberately the place where individual repos can have distinct *feels* — and where agent creativity in visual design and layout is wanted, not feared.3637Before laying out a single component, **ask the user this question**:3839> *"Are we following a design pattern from a reference splash, or are we getting creative on visual design?"*4041The two answers warrant different work, and the answer should be explicit:4243- **"Follow the reference":** Lift the closest sibling splash structurally and adapt only what the new repo's content requires. Same hero composition, same type stack, same card chrome — fresh content, recognizable surface. Faster, lower-risk.44- **"Get creative":** Treat this as the experimentation surface it's meant to be. **Diverge in *shape*, not just in *color*.** Reach for typographic moves, layout moves, ornament moves — not just remap the brand-spine tokens to different hues.4546### The trap to watch for4748The recurring failure mode across the first three splashes (before this one) was visible in retrospect: each new splash inherited the previous one's hero composition, card geometry, and ornament strategy, then renamed the brand-spine variables to "make it different." The output: the same aesthetic three times in three palettes.4950Recognizable signs you're sliding into the trap:5152- The hero is centered with stacked headline + tagline + CTA — *because the last splash was*53- The cards have rounded glass with a glow shadow — *because the last splash had them*54- The background is a radial mesh with a faint grid — *because the last splash used one*55- The display + sans pairing is the obvious modern default — *because that's what gets reached for*56- You're picking colors but not picking **moves**5758When you notice this happening, **stop, surface, and propose**.5960### Surface a posture proposal before laying out components6162When the user says "get creative" (or doesn't specify and the splash matters enough to diverge), send a one-paragraph posture proposal *before* writing any layout. Name the axes you're considering:6364> *"For this splash I'm thinking light-mode default + serif display (Newsreader) + asymmetric hero with the diagram dominant on the right + paper-grain background with a manuscript margin rule + literary voice in the copy. That makes it visibly distinct from memopop (dark, sans, centered, radial mesh, matter-of-fact) and content-farm/splash. Sound right, or want to push in a different direction?"*6566Cheap to send, gets the user a chance to nudge before code lands, and reinforces that **aesthetic posture is an explicit decision, not a default.**6768The detailed *what's locked vs. what's free* table lives further down under [Visual identity — divergent by design](#visual-identity--divergent-by-design). Read it before drafting the proposal.6970## When to use this skill7172- The user asks to scaffold a splash for a new repo73- The user wants to add search, sort, tags, mode toggle, or any other feature to an existing splash74- Converting a legacy `apps/<name>/` site to the canonical `splash/` location75- Troubleshooting a GitHub Pages deploy of a splash76- The user mentions "splash", "GitHub Pages", "lossless-group.github.io/<repo>/", "Pagefind on our site"77- You're working anywhere under a `splash/` directory in any Lossless repo78- The user wants to verify a splash meets the convention ("is this splash done?")7980## Composes with other skills8182Skills this one always pulls along:8384- **`astro-knots`** — framework rules, hard prohibitions (no React, no JSX). The splash is built in Astro.85- **`theme-system`** — two-tier tokens, three-mode contract. The splash uses these.86- **`context-vigilance`** — `context-v/` schemas, frontmatter, lenient parsing. The splash renders `context-v/`.87- **`changelog-conventions`** — `changelog/` structure. The splash renders `changelog/`.88- **`pseudomonorepos`** — search-first behavior, parent-vs-child level discipline. Especially for the rollup variant.8990## Reference implementations9192Three confirmed splashes as of writing. Read at least one end-to-end before scaffolding a new one:9394| Repo | Variant | Path | Notable |95|---|---|---|---|96| `ai-labs/memopop-ai/apps/memopop-site/` | Single-project (legacy location, predates the habit) | `apps/<name>/` | First instance. Has Pagefind search. |97| `content-farm/splash/` | Pseudomonorepo (rollup) | `splash/` | Canonical reference. Submodule rollup via GitHub Content API. |98| `lfm/splash/` | Single-project, package-publishing repo | `splash/` | Second Pagefind instance. Asymmetric hero. Package-isolation discipline. |99100The habit names `content-farm/splash/` as the reference implementation — read its `README.md` end-to-end.101102## Variants103104Pick once, stick with it:105106- **Single-project repo** — no submodules. `content.config.ts` reads `../changelog` and `../context-v` directly. No `src/rollup/`, no `scripts/rollup-sync.ts`, no `splash/.env`. Examples: `lfm/splash/`.107- **Pseudomonorepo** — children as submodules. Splash aggregates each child's `changelog/` and `context-v/` via the GitHub Content API. Sync-on-demand (`pnpm rollup:sync`), never sync-on-build. Examples: `content-farm/splash/`.108- **Package-publishing repo** — repo also publishes to JSR/npm. Splash must be excluded from publish allowlists. See *Package isolation* below. Examples: `lfm/splash/`.109110The variants compose: lfm is single-project + package-publishing. content-farm is pseudomonorepo + non-publishing.111112## Locked conventions113114These are deliberate. Don't drift without a reason.115116### Tech & structure117118- **Astro.** No React, no JSX. (See `astro-knots`.)119- **Directory name:** `splash/` at repo root. *Not* `site/`. *Not* `apps/<name>` (legacy memopop only). The name preserves linguistic space for a future custom-domain marketing site.120- **Package name:** `<repo>-splash`. `private: true`. Astro is the only required dep at minimum; `astro-pagefind` + `pagefind` if search is enabled.121- **TypeScript path aliases** in `tsconfig.json`: `@components/*`, `@layouts/*`, `@loaders/*`, `@lib/*`, `@styles/*`, `@content/*`, `@pages/*`, `@/*`. Loader code never needs `../../../` guesswork.122123### Build & host124125- **Host:** GitHub Pages, project-page form. Live URL pattern: `https://lossless-group.github.io/<repo>/`.126- **`astro.config.mjs`:** `site: 'https://lossless-group.github.io'`, `base: '/<repo>/'`, `trailingSlash: 'ignore'`, `build.format: 'directory'`. Custom domain later? Set `site` to the domain and `base` to `'/'`.127- **Build trigger:** push to `main`. Aligns with the `development` → `main` → `master` tier model.128- **Deploy action:** `actions/deploy-pages@v4` with `actions/configure-pages@v5` using `enablement: true` so the workflow bootstraps Pages on first run.129- **No submodule fetching in CI** even for pseudomonorepos — rolled-up content is pre-synced and committed; CI does pure file IO.130131### Local dev132133- `pnpm install --ignore-workspace` — required because the parent monorepo's `pnpm-workspace.yaml` does not include splash sites; the splash installs deps independently.134- `pnpm dev` respects the configured `base`; visit `http://localhost:4321/<repo>/`.135- For Pagefind: search index is built at deploy time only — `pnpm dev` shows the search box mounted with a missing-state message. Run `pnpm build && pnpm preview` to exercise search locally.136137## Reference file layout138139```140<repo>/141├── splash/142│ ├── astro.config.mjs # base: '/<repo>/', trailingSlash: 'ignore'143│ ├── package.json # private; "<repo>-splash"; astro (+ astro-pagefind, pagefind)144│ ├── tsconfig.json # path aliases145│ ├── .gitignore # node_modules/, dist/, .astro/, .env146│ ├── .env.example # GITHUB_API_TOKEN= (pseudomonorepos only)147│ ├── README.md # local dev, deploy, where content lives, how to update148│ ├── public/ # favicon.svg, og image, brand marks149│ ├── scripts/150│ │ └── rollup-sync.ts # pseudomonorepos only151│ └── src/152│ ├── content.config.ts # lenient schemas + (pseudomono) unionLoader153│ ├── content/<thing>-highlights/ # curated gallery cards (one .md per item)154│ ├── rollup/ # pseudomono only — synced submodule content; committed155│ ├── loaders/ # frontmatter, githubContentApi, parseGitmodules, rollupFetch156│ ├── lib/seo.ts # static SEO copy157│ ├── lib/date.ts # toDate / formatDate / isoDate helpers158│ ├── layouts/BaseLayout.astro # tokens, fonts, head, body shell, mode pre-paint159│ ├── components/ # Header, MetaTags, ModeToggle, FeatureCard, SearchBox, SortControls, MetaTags160│ ├── styles/ # theme.css (two-tier tokens), prose.css161│ └── pages/162│ ├── index.astro163│ ├── search.astro164│ ├── changelog/index.astro165│ ├── changelog/[...slug].astro166│ ├── context-v/index.astro167│ └── context-v/[...slug].astro168└── .github/workflows/pages.yml # deploy splash/ on push to main169```170171## Package isolation (for repos that publish to JSR / npm)172173This was the design constraint that determined whether the splash could be added to LFM at all. The boundary is enforced by **explicit allowlists**, not by what exists in the working tree:174175| Channel | Defined in | Allowlist | Splash files |176|---|---|---|---|177| JSR (canonical) | `deno.json` → `publish.include` | `src/**/*.ts`, `src/**/*.md`, `deno.json`, `LICENSE`, `README.md` | excluded |178| npm | `package.json` → `"files"` | `src`, `dist`, `README.md`, `LICENSE` | excluded |179| Build (`tsup`) | `tsup.config.ts` `entry` | hard-coded `src/...ts` paths | excluded |180| Workspace install | parent `pnpm-workspace.yaml` | `packages: [...]` omits `splash/` | excluded |181182`splash/package.json` carries `"private": true` as defense-in-depth — the boundary is the allowlists, the `private` flag is the suspenders to the allowlist's belt. Both belong.183184When scaffolding a splash for a publishing repo: **verify all four boundaries before declaring the work done.** It's cheap to check; the failure mode (publishing splash files to npm) is hard to undo.185186## Content schemas — lenient, never throw187188Every field in `content.config.ts` uses `z.preprocess` to coerce empty strings, nulls, and unexpected types gracefully. Schemas **never throw** on legacy entries — they `safeParse` and store raw frontmatter as a fallback. Pages stay defensive too: any code that calls `.getTime()` on a date goes through a `toDate(unknown)` helper because the loader's "store raw" fallback path means strings can arrive where Dates are expected.189190The lenient preprocessor stack to lift verbatim:191192- `lenientString` — coerces empty strings and nulls to `undefined`193- `lenientStringArray` — coerces a single string to a one-item array, accepts arrays as-is. **Important:** the changelog-conventions skill specifies `augmented_with` is a *list* field. Use `lenientStringArray` for it, not `lenientString`. Older legacy entries used a bare string; the lenient array preprocessor accepts both forms.194- `lenientDate` — accepts Date | number | string; tolerates `"[]"`, `"~"`, `"TBD"`, `"tbd"` as undefined; never throws195- `lenientNumber`, `lenientBoolean` — same pattern196197Schemas use `.passthrough()` so unknown fields (`site_uuid`, `slug`, `image_prompt`, `usageCount`, `usage_count`, etc.) ride along without validation noise.198199### The defensive-rendering pair200201Lenient schemas alone aren't enough. When schema validation fails for *any* reason, the loader's `safeParse` falls back to raw frontmatter — which means dates arrive as strings, not Date objects. **Every render-side call site that touches a date must go through a `toDate(unknown): Date | undefined` helper** in `@lib/date.ts`. Sort code that calls `.getTime()` directly on a possibly-string field will crash the page; defensive code never does.202203```ts204// @lib/date.ts205export function toDate(v: unknown): Date | undefined {206 if (v instanceof Date) return Number.isNaN(v.getTime()) ? undefined : v;207 if (typeof v === 'number') {208 const d = new Date(v);209 return Number.isNaN(d.getTime()) ? undefined : d;210 }211 if (typeof v === 'string') {212 const t = v.trim();213 if (t === '' || t === '[]' || t === '~' || t === 'TBD' || t === 'tbd') return undefined;214 const d = new Date(t);215 return Number.isNaN(d.getTime()) ? undefined : d;216 }217 return undefined;218}219```220221The lenient schemas + defensive renderers are a *pair*. Skipping either one creates page-crash classes that only surface when a particular legacy entry happens to violate the schema.222223The full pattern is in `lfm/splash/src/content.config.ts` and `lfm/splash/src/lib/date.ts` — copy and adapt, don't re-invent.224225## Search — Pagefind by default226227The convention is set: every splash ships search-by-default. The exploration `astro-knots/context-v/explorations/Implementing-Full-Text-Search-by-Default.md` named Pagefind as the strong default; we've now confirmed it twice (memopop-site, lfm/splash).228229### Wire-up2302311. **Deps:** `pnpm add astro-pagefind pagefind --ignore-workspace`2322. **Integration in `astro.config.mjs`:** `import pagefind from 'astro-pagefind'; integrations: [pagefind()]`2333. **Build format:** `build.format: 'directory'` — Pagefind needs stable per-page URLs.2344. **`SearchBox.astro` component:** lift verbatim from `lfm/splash/src/components/SearchBox.astro` or `memopop-site/src/components/SearchBox.astro`. Two variants — `compact` (header popover) and `full` (search page). `<details>` for the popover (no JS state needed). Global `/` keyboard shortcut.2355. **`/search` page:** full-panel SearchBox with `autoFocus`.2366. **Header:** mount `<SearchBox compact />` in the actions area; add a `Search` nav link to `/search/`.237238### `data-pagefind-*` placement239240**Critical Pagefind behavior to know:** once *any* page on the site uses `data-pagefind-body`, only pages with that marker are indexed. Pages without it are silently skipped. This is desirable here — we want results pointing at content, not list/home/search/navigation surfaces. But if you're debugging "why is page X not in the index," check whether *some other* page on the site has `data-pagefind-body` set, which made all unmarked pages opt-out.241242| Page | Markers |243|---|---|244| Detail pages (`changelog/[...slug]`, `context-v/[...slug]`) | `data-pagefind-body` on `<main>`, `data-pagefind-meta="title:..."`, hidden `<span data-pagefind-filter="kind:Changelog\|Context">`, hidden `<span data-pagefind-filter="tag:..."` per tag, `data-pagefind-ignore` on chrome (back link, meta line, status pill row) |245| List pages (`/changelog/`, `/context-v/`) | `data-pagefind-ignore="all"` on the `<ul>` |246| Home / search / index pages | Nothing — they're not indexable since none have `data-pagefind-body` |247248### Filter taxonomy249250The convention has two facet keys:251252- `kind:` — `Changelog` | `Context` (and `Notes` in legacy memopop). One value per page.253- `tag:` — emits one hidden filter span per entry tag. Train-Case values per the changelog/context-v conventions.254255Pseudomonorepos add a third facet: `from:<peer>` — peer-app or submodule slug. Single-project splashes drop it; conventions still hold.256257### UI overrides — token-driven258259Pagefind's default UI exposes CSS variables. Map them to your semantic tokens so the search UI pivots through light/dark/vibrant modes with the rest of the site:260261```css262:global(.pagefind-ui) {263 --pagefind-ui-primary: var(--color-accent);264 --pagefind-ui-text: var(--color-text);265 --pagefind-ui-background: var(--color-bg);266 --pagefind-ui-border: var(--color-border-strong);267 --pagefind-ui-tag: var(--color-bg-soft);268 --pagefind-ui-font: var(--font__sans);269 /* ... */270}271```272273Don't fork the UI. Variable overrides have been sufficient through three implementations.274275## Sort controls on list pages276277Both `/changelog/` and `/context-v/` carry a `<SortControls>` component above the list. Default sort: `date_modified` descending (newest first). Options: `Modified` | `Created` | `Published` | `Title`.278279Wire-up:280281- **Server pre-sorts** by the default key/direction so the static HTML matches the UI default — page reads correctly even before JS hydrates or with JS off.282- **Each `<li>`** carries `data-sort-modified`, `data-sort-created`, `data-sort-published`, `data-sort-title` attrs. ISO date strings sort lexicographically (correct as time-order); titles are lowercased for stable alpha sort. Empty values always sink to the bottom regardless of direction.283- **Each `<ul>`** carries `data-sort-target="<page-name>"` so the SortControls JS can find all sortable lists on the page (multiple `<ul>`s on grouped views like context-v).284- **Persistence:** per-page in `localStorage` under `lfm-splash-sort:<name>` (or analogous). `<page-name>` is the namespace — `changelog` and `context-v` get independent prefs.285- Direction label adapts to value type: dates show "Newest first / Oldest first", titles show "A → Z / Z → A".286287Lift `lfm/splash/src/components/SortControls.astro` directly. The component is generic — takes a `name`, `options[]`, `defaultKey`, `defaultDirection`.288289## Tags row in list previews290291Each `<li>` in list views renders the entry's tags below the lede, wrapping to multiple lines as needed. **No slice limit by default — show every tag.** (Memopop-site sliced to the first 5; the newer convention is "let it wrap" — entries with many tags shouldn't have most hidden, since tags are a primary discoverability surface.) Train-Case values come straight from frontmatter `tags:` arrays. Visual: small mono-font chips with hairline borders, `flex-wrap: wrap`.292293```astro294{entry.data.tags && entry.data.tags.length > 0 && (295 <ul class="entry-list__tags" aria-label="Tags">296 {entry.data.tags.map((t) => (297 <li class="entry-list__tag">{t}</li>298 ))}299 </ul>300)}301```302303## Visual identity — divergent by design304305Each splash should *look distinct from its siblings* while keeping the same architectural shape. The structural shape is locked because affordance consistency matters; the aesthetic surface is free because individual-project feel matters. The discipline is to know which is which.306307### What's locked (don't break) vs. what's free (encouraged to diverge)308309| Locked — architectural | Free — aesthetic |310|---|---|311| `data-mode` on `<html>` + the three-mode contract | Which mode is the **default** |312| Semantic token names (`--color-bg`, `--color-text`, `--color-accent`, `--color-thread`, ...) | The Tier-1 **values** behind them |313| The pre-paint mode-resolution script in `BaseLayout` (no FOUC) | The **brand spine** (cyan vs. ink-violet vs. ...) |314| Accessibility primitives (focus rings, `prefers-reduced-motion`) | The **typeface stack** (display + sans; mono usually JetBrains) |315| Component primitive shapes (`.pill`, `.eyebrow`, `.gradient-text`, `.from-tag`, `.folio`, `.chip`) | The **hero composition** (centered, asymmetric, manuscript-style, terminal-style, …) |316| Lenient schema + defensive rendering pair | The **card chrome** (rounded glass, hairline borders, printer's-mark corners, brutalist no-border, …) |317| `data-pagefind-*` placement convention | The **background ornament** (radial mesh, paper grain, ASCII diagram lines, riso noise, …) |318| `BaseLayout` props surface | The **density** (airy vs. dense), **voice** (matter-of-fact vs. literary vs. terminal vs. academic) |319| Content collection structure (`feature-highlights`, `sct-examples`, etc.) | The component *visuals* layered on top |320321A reader navigating between two Lossless splashes back-to-back should feel the **family resemblance** — same affordances, same conventions, search in the same place, mode toggle in the same place — but each splash should also feel like **its own thing**, not a recolor of the last one.322323### Examples of divergence in the existing splashes324325| Axis | memopop-site | content-farm/splash | lfm/splash |326|---|---|---|---|327| Default mode | dark (operator) | dark | **light** (writer's mode) |328| Display + sans | Fraunces + Inter | (similar to memopop) | **Newsreader + Manrope** |329| Hero composition | centered, stacked CTAs | (similar) | **asymmetric, diagram-dominant** |330| Card chrome | rounded glass, glow shadows | (similar) | **squarer corners, hairline borders, printer's-mark corner ticks** |331| Background ornament | radial mesh + faint grid | (similar) | **paper grain + manuscript margin rule** |332| Brand spine | cyan + aquamarine + plum | (similar palette family) | **ink-violet + sienna + moss** |333| Voice | matter-of-fact | matter-of-fact | **literary, manuscript-flavored** |334335The first two splashes shipped before the divergence discipline was named — they came out feeling like the same site twice. The lfm splash was the first to break the mold deliberately. **Treat that as the bar going forward**, not the ceiling — the next splash should feel different from all three.336337### Concrete divergence axes worth pushing on338339Don't just pick colors. Pick **moves**:340341- **Mode default**: light, dark, vibrant. Each has a different voice.342- **Type pairing**: serif display + geometric sans, monospace-forward, all-mono, variable-axis serif, slab + grotesque.343- **Hero composition**: centered stacked, asymmetric two-column, full-bleed with ornament, terminal-prompt frame, manuscript-page header, magazine-style pull-quote.344- **Card chrome**: glassmorphism, hairline + corner ticks, brutalist no-border, notebook-tab tabbed, ledger-row dense, polaroid-frame.345- **Background ornament**: radial mesh, paper grain, ASCII diagram strokes, risograph noise, gradient bands, dot grid, blueprint grid.346- **Voice**: matter-of-fact, literary, terminal/CLI, academic, hand-written zine, datasheet.347348Mix freely *across* axes; pulling from the same column on every axis is the trap. If the new splash's row in the table above looks like a copy of an existing splash's row, push harder.349350## Acceptance — "this repo has a splash"351352Verify before declaring the habit met (lifted from the habit doc, slightly tightened):353354- [ ] `splash/` directory exists at repo root355- [ ] `splash/astro.config.mjs` has correct `site` and `base: '/<repo>/'`356- [ ] `splash/package.json` has `"private": true` and the `<repo>-splash` name357- [ ] `splash/tsconfig.json` declares the path aliases358- [ ] `splash/README.md` documents local dev, deploy, package isolation (if applicable), and where content lives359- [ ] `pnpm install --ignore-workspace && pnpm build` succeeds from a clean clone360- [ ] `splash/dist/` includes routes for `/`, `/search/`, `/changelog/`, `/context-v/`, plus per-entry detail routes when entries exist361- [ ] `splash/dist/pagefind/pagefind-entry.json` exists with `page_count` matching the number of detail pages362- [ ] `.github/workflows/pages.yml` exists, builds `splash/`, deploys via `actions/deploy-pages@v4`, uses `actions/configure-pages@v5` with `enablement: true`363- [ ] GitHub Pages source is set to **"GitHub Actions"** in repo settings364- [ ] First deploy reaches `https://lossless-group.github.io/<repo>/` and loads cleanly365- [ ] Mode toggle works: light/dark/vibrant all render without FOUC, persist across reloads366- [ ] Search returns results when typing into the header popover or `/search` page367- [ ] Sort controls reorder correctly on `/changelog/` and `/context-v/`; default is `date_modified` descending368- [ ] (Pseudomonorepos only) `pnpm rollup:sync` runs locally; `splash/src/rollup/` is committed; rolled-up content appears with provenance369- [ ] (Publishing repos only) Verify all four package-isolation boundaries — JSR `publish.include`, npm `files`, build entry list, parent workspace exclusion370371## Maintenance cadence372373- **On every shipped change** — author a `changelog/` entry; surfaces on the splash on next deploy374- **(Pseudomonorepos) when a child ships** — run `pnpm rollup:sync`, commit `src/rollup/`, push375- **When the curated gallery drifts** — edit `src/content/<thing>-highlights/`. New item? Add a file. Retired? Delete it.376- **Periodically (e.g. weekly)** — sync rollup to catch upstream drift even without a triggering event377378## Typical flow when scaffolding a new splash3793801. **Walk the tree.** Per `pseudomonorepos`, search for prior splash work in the repo and parent levels. Note any context-v entries about search, theming, or splash-specific design decisions.3812. **Pick the variant** — single-project, pseudomonorepo, or package-publishing (or combination).3823. **Read at least one reference splash end-to-end** — the habit doc names `content-farm/splash/`. For single-project + publishing, `lfm/splash/` is closest.3834. **Scaffold the directory** matching the reference layout. Lift `astro.config.mjs`, `package.json`, `tsconfig.json`, `BaseLayout.astro`, `Header.astro`, `MetaTags.astro`, `ModeToggle.astro`, `SearchBox.astro`, `SortControls.astro`, `frontmatter.ts` directly.3845. **Resolve the visual posture with the user** — ask: *"following a reference or getting creative?"* If creative, propose the divergence axes (mode default, type pairing, hero composition, card chrome, ornament, voice) before laying out components. Stay distinct from sibling splashes; reuse the semantic-token contract.3856. **Wire the content collections** with lenient schemas pointing at `../changelog` and `../context-v`. Use the proven preprocessor stack.3867. **Add Pagefind** — `data-pagefind-body` on detail pages, `data-pagefind-ignore="all"` on list teasers, `kind:` + `tag:` filter spans.3878. **Add the GitHub Pages workflow** — `.github/workflows/pages.yml` with `actions/deploy-pages@v4`.3889. **Write `splash/README.md`** — local dev, deploy, package isolation table (if publishing), where to edit content.38910. **Verify acceptance criteria** — run through the checklist above before declaring done.39011. **Author a changelog entry** at the repo root for the splash addition. Per `changelog-conventions`, lead with `## Why Care?` then `## What's New?`.39112. **For publishing repos** — run the parent's build (`pnpm build`, `tsup`, etc.) once more to confirm the splash hasn't broken the package.392393## Lessons learned (from the lfm/splash session)394395These are the gotchas worth flagging in advance so the next implementation doesn't rediscover them.3963971. **`augmented_with` is a list field.** The changelog-conventions skill says "same ul-list preference as authors." Schemas that declare it as a `lenientString` will trip `safeParse` on convention-correct entries, kicking the loader into raw-frontmatter fallback. Always `lenientStringArray`.3982. **Schema validation failure → date strings, not Date objects.** When a schema falls back to raw frontmatter, every date field arrives as a string. Render-side sort and format code must go through `toDate(unknown)`. The lenient schema and the defensive renderer are a *pair* — skipping either is a deferred crash.3993. **Pagefind's "once one page has `data-pagefind-body`, only marked pages are indexed."** Add the marker to the routes you actually want indexed (`changelog/[...slug]`, `context-v/[...slug]`). The home, list, and search pages will silently drop out of the index — which is what you want, but be ready to explain it.4004. **Tags row: no slice limit.** Wrap, don't truncate.4015. **Default sort by `date_modified` descending — server-pre-sort to match the UI default.** Static HTML must read correctly even with JS off; the SortControls component reorders client-side once mounted, but the initial render needs to already be in default order.4026. **Sort UI persists per-page** in `localStorage` under namespaced keys (`lfm-splash-sort:changelog`, `lfm-splash-sort:context-v`). Different pages have different prefs.4037. **Walk the tree before re-deciding architecture questions.** Per `pseudomonorepos`. The exploration `astro-knots/context-v/explorations/Implementing-Full-Text-Search-by-Default.md` exists for a reason — read it before relitigating the search-engine choice.4048. **Second instance crystallizes a convention.** Memopop-site shipped Pagefind first as an exploration; lfm/splash shipped it as a confirmed pattern. When you find yourself implementing something "just like the last splash," that's the moment to update the skill (and any companion blueprint) so the third instance doesn't have to rediscover the choices.4059. **Light is a legitimate default mode.** Memopop defaults dark; lfm defaults light. The three-mode contract is symmetric — pick the default that fits the splash's voice.40610. **Single `<details>` element is enough for the search popover.** No JS state machine needed for "open on click, close on outside-click." The `<details data-search-compact>` pattern from `SearchBox.astro` is the cleanest version.40711. **Render the existing changelog conventions field-for-field.** When wiring a changelog/ list, remember the conventions skill's mandatory frontmatter (8 fields including `lede`). The splash should display all of them gracefully — but never crash if a legacy entry is missing several.40812. **Convention fixes belong in the schema, not in per-page code.** When you find a frontmatter-shape bug (e.g. `augmented_with` mismatch), fix it in `content.config.ts` so every page benefits. Don't paper over it at the call site.40913. **Aesthetic divergence is the goal, not a risk to manage.** The first three splashes (before lfm) ended up looking like each other — same hero shape, same card geometry, same ornament strategy, with brand-spine recolors layered on top. The path out: ask the user upfront whether to follow a reference or get creative, propose the divergence axes explicitly, and **diverge in shape, not just in hue**. When agent creativity in visual design produces something genuinely distinct, that's the right outcome — splashes are the experimentation surface where this kind of move belongs.410411## What this skill deliberately is not412413- **Not the design system.** The component shapes here are minimum viable; nothing prevents richer per-splash UI (search hotkeys, animated mode transitions, custom filters). The skill defines the *floor* of consistency.414- **Not a runtime dependency.** Every component is meant to be lifted/copied. We don't ship a `@lossless-group/splash-kit` package — that would couple splashes through versioning. Per the Astro Knots philosophy: copy-pattern, not runtime dep.415- **Not the eventual marketing site.** When a project deserves a custom-domain marketing site, build that separately. The splash stays put as the source repo's own Pages presence.416417## See also418419- **Habit:** *"Maintain a Github Splash Page for each Repo"* — currently at `lossless-monorepo/context-v/habits/`. The *why*. Search by title; the path may evolve.420- **Reference splashes:** `lfm/splash/` (most recent), `content-farm/splash/` (canonical reference, pseudomonorepo variant), `ai-labs/memopop-ai/apps/memopop-site/` (first instance, legacy `apps/` location).421- **Spec:** `content-farm/context-v/specs/Github-Splash-Page-for-Content-Farm.md` — the spec the canonical reference implements.422- **Search exploration:** `astro-knots/context-v/explorations/Implementing-Full-Text-Search-by-Default.md` — establishes Pagefind as the default for Astro Knots. The lfm/splash work is the second confirmed instance; promoting to a blueprint is the natural next step.423- **Sibling skills:**424 - `astro-knots` — framework rules and prohibitions425 - `theme-system` — two-tier tokens, three-mode contract426 - `context-vigilance` — context-v frontmatter and rendering427 - `changelog-conventions` — changelog frontmatter and ship-note structure428 - `pseudomonorepos` — parent-repo patterns, search-first behavior, roll-up convention
Run npx skillmds@latest add lossless-group/maintain-splash-pages in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
The Lossless Group's pattern for repo-level splash pages — small Astro sites at <repo>/splash/ that ship to GitHub Pages on push to main, render the repo's changelog/ + context-v/ alongside curated marketing copy, and stay isolated from any package the repo also publishes. Use proactively whenever scaffolding a noteworthy new repo (every "important" repo wants one), when shipping a coherent chunk of work that an external reader would land on, when adding a feature (search, sort, tags row, theme mode) to an existing splash, when converting a legacy apps/<name>/ site to splash/, when troubleshooting a Pages deploy, or when the user mentions "splash", "GitHub Pages", "lossless-group.github.io", "Pagefind on our site", or working under a splash/ directory. Codifies the proven shape across three reference implementations (memopop-site, content-farm/splash, lfm/splash) and the package-isolation discipline that keeps splashes safe to add to repos that also publish to JSR/npm. It is listed under Marketing & Growth on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
lossless-group (@lossless-group) published this skill. Their other Agent Skills are listed on their SkillMD profile.