Own your homepage completely
I want a totally custom homepage, not a template one.
Add your own route group and delete the framework home — a fully bespoke front page that still merges every future upstream release. This is exactly how Utah’s Events did it.
Typical effort: 1 hour
The recipe — paste this to your AI
# Recipe: own your homepage (the on-path way)
**Goal:** make `/` your own custom homepage without editing or deleting the framework's marketing
page, so future template upgrades still merge cleanly.
**The off switch is two slots.** You register your home in `lib/site-slots/index.tsx`, which is
your file and which the template never writes to again:
- `homeBody` replaces what the home page renders.
- `homeLayout` replaces the site header and footer on `/`, and only on `/`. Every other page
keeps them.
Register one or both. Nothing is deleted, `/` is still defined exactly once, and the page keeps
publishing the Organization + WebSite structured data that search engines read. You no longer have
to remember to re-add it.
**Why not just edit the framework home?** `app/(marketing)/page.tsx` is a `@genesis-managed:
FRAMEWORK` file. Editing it in place works once, then fights you on every merge.
## Steps
1. **Write your home as a component** in your own lane (`components/<your-site>/`):
```tsx
// components/acme/Home.tsx
export function AcmeHome() {
return (
<>
{/* your hero, your sections, your words */}
</>
);
}
```
2. **Want your own header and footer on `/` too?** Write a wrapper. It receives the page as
`children` and owns the landmarks, so give it a `<main>`:
```tsx
// components/acme/HomeChrome.tsx
export function AcmeHomeChrome({ children }: { children: React.ReactNode }) {
return (
<>
<AcmeNav />
<main>{children}</main>
<AcmeFooter />
</>
);
}
```
Keep our header and footer? Skip this step and register only `homeBody`.
3. **Register them** in `lib/site-slots/index.tsx`:
```tsx
import type { SiteSlots } from './types';
import { AcmeHome } from '@/components/acme/Home';
import { AcmeHomeChrome } from '@/components/acme/HomeChrome';
export const siteSlots: SiteSlots = {
homeBody: AcmeHome,
homeLayout: AcmeHomeChrome,
};
```
4. **Set the page's title and search description, if you want your own.** The page file stays
ours, so its metadata is ours unless you say otherwise. Add either key to `PAGE_COPY.home` in
`lib/content/marketing.ts`, your file:
```ts
home: {
fullTitle: 'Acme Events', // the WHOLE <title>, nothing appended (default: "Home · <name>")
seoDescription: 'What a search result says about your home page.', // default: your tenant description
// …your existing keys…
},
```
5. **Keep it inside the lanes.** Your components may import the kit (`components/ui/*`) and your
own components and content. Do NOT edit CORE files to make the home work. Words belong in
`lib/content/*`, brand tokens in `app/globals.css`.
6. **Custom styling?** Use a bundled stylesheet, never inline styles. See
`custom-css-the-csp-safe-way.md`. Inline `<style>` / `style="…"` is silently dropped in
production.
## Already own `/` the old way?
Before the slots existed, the recipe was to serve `/` from a route group of your own and delete
the framework home. That still works and is still supported. The upgrader resolves the deletion
itself whenever a release touches the framework home. You do not need to move. If you do move,
you stop carrying a deletion and a hand-copied structured-data block. The old steps, for
reference:
> 1. **Create your home in a new route group** (new files are always on-path — they aren't in the
> hash-lock):
> ```
> app/(home)/layout.tsx ← your own layout (no framework chrome unless you want it)
> app/(home)/page.tsx ← your homepage
> ```
> A route group in parentheses does not add a URL segment, so `app/(home)/page.tsx` serves `/`.
>
> **If you want to keep the site header and footer — do NOT copy the marketing layout.**
> Re-export it:
>
> ```tsx
> // app/(home)/layout.tsx
> export { default } from '../(marketing)/layout';
> ```
>
> Copying is the obvious move and it is a trap. The header and footer fork on day one, and
> every future improvement to the nav then lands on every page *except* your most important
> one — silently, because nothing fails. Importing a FRAMEWORK file is not editing it, so
> this stays fully on the upgrade path and you inherit chrome changes for free.
>
> Write a real layout only when you genuinely want different chrome on the home page (a
> transparent header over a full-bleed scene, say) — as a decision with a reason, not as a
> side effect of scaffolding. You can start with the re-export and replace it later.
>
> 2. **Delete the framework homepage so `/` isn't defined twice:**
> ```bash
> git rm "app/(marketing)/page.tsx"
> ```
> This is safe: removing a FRAMEWORK page is not a CORE edit, so `upgrade:check` stays green. (If a
> future template update re-adds a `(marketing)/page.tsx`, you'll get a simple add/delete conflict on
> the next merge — resolve it by keeping your delete.)
>
> 3. **Carry over the structured-data block.** The framework home you just removed publishes
> Organization + WebSite JSON-LD — the machine-readable "who this business is" that search
> engines read. Your custom home must keep publishing it, and nothing visual will remind you:
> the page renders perfectly and says nothing to a crawler. (This shipped on a real site —
> `check-served-assets` is what caught it.)
>
> ```tsx
> // in app/(home)/page.tsx
> import { JsonLd } from '@/components/json-ld';
> import { organizationJsonLd, websiteJsonLd } from '@/lib/jsonld';
>
> export default function Page() {
> return (
> <>
> <JsonLd data={[organizationJsonLd(), websiteJsonLd()]} />
> {/* …your home… */}
> </>
> );
> }
> ```
>
> Both helpers read `site.config.ts`, so the data stays correct as your config evolves.
> `node scripts/check-served-assets.mjs` (also in CI) fails the build if `/` stops
> publishing it.
>
> 4. **Keep it inside the lanes.** Your `(home)` files may import the kit (`components/ui/*`) and your
> own components/content, but do NOT edit CORE files to make the home work. Words belong in
> `lib/content/*`; brand tokens in `app/globals.css`.
>
> 5. **Custom styling?** Use a bundled stylesheet, never inline styles — see
> `custom-css-the-csp-safe-way.md`. Inline `<style>` / `style="…"` is silently dropped in production.
## Verify
```bash
npm run upgrade:check # must still read "0 edited" (you stayed on-path)
npm run build # / builds and renders as your new home
```
## Paste-ready prompt
> Replace the homepage with a custom one, staying on the upgrade path and deleting nothing: write
> my home as a component in `components/<my-site>/` and register it as `homeBody` in
> `lib/site-slots/index.tsx`. If I want my own header and footer on `/`, also write a wrapper that
> renders them around `<main>{children}</main>` and register it as `homeLayout`; otherwise leave
> `homeLayout` out and the site's header and footer stay. Do not edit or delete
> `app/(marketing)/page.tsx`, since it keeps publishing the structured data for me. Keep all edits in lanes
> (no CORE files). Put any custom CSS in a bundled `.css` file imported into the component — no
> inline styles (the CSP drops them). Then run `npm run upgrade:check` (must be "0 edited") and
> `npm run build`.
Source of truth
This recipe ships inside the template as docs/prompts/own-the-homepage.md — this page renders that exact file. Your own copy is always the newest version: take the template and you get every recipe locally, versioned with the release you are on, and each upgrade brings the newer ones with it. How to get it →