Add or remove a page
There are two kinds of new page: one you can make from content alone, in the editor, and one that needs a new route in the code because it has a layout of its own.
Pages from content alone
Section titled “Pages from content alone”Some pages exist for every entry of a collection, so adding an entry adds a page:
| In Handy | Adds a page at | Edit it in |
|---|---|---|
| A service | /services/<slug> |
Content → Services |
| A job | /jobs/<slug> |
Content → Jobs |
| A tip | /journal/<slug> |
Content → Tips |
| A legal page | /legal/<slug> |
Pages → Legal |
The slug (the end of the address) comes from the entry’s file name, which Keystatic makes from its name or title.
Legal is the general-purpose one: a title, a “last updated” date and a Markdown body (headings, lists, links), shown as a plain readable article. Every template has it, and it’s fine for any simple text page, such as an accessibility statement or a cookie policy. Add the entry, then link it: Site settings → Footer links or Navigation.
These pages appear by themselves in /sitemap.xml and /llms.txt, in every language. Services,
jobs and tips are also in the site search and the AI chat’s knowledge.
Other templates have their own collections with pages (bouquets, walks, classes, team…): your
template’s page lists them. Aurora also has Landing pages, built from sections like the home
page and published at /<slug>.
A new page with its own layout
Section titled “A new page with its own layout”For a page that isn’t an entry of a collection, for example an About page with a heading, a photo and a few paragraphs, you add its content, a view and two small route files. The example uses Handy; in other templates the files have the same names and roles.
1. Its fields in the content model
Section titled “1. Its fields in the content model”In src/content/model.ts, add the page next to the other single pages (servicesPage,
jobsPage…). heading (eyebrow, title, lead) and seo (SEO title and description) are
helpers defined at the top of the file:
/** About page: heading, SEO, a photo and the story. */aboutPage: z .object({ ...heading, ...seo, story: z.string().meta({ title: 'Story', multiline: true }), photo: image().meta({ title: 'Photo', description: 'A 3:2 photo.', image: { directory: 'src/assets/photos/about' } }), photoAlt: z.string().meta({ title: 'Photo description (alt text)' }), }) .meta({ title: 'About page' }),2. Register it
Section titled “2. Register it”Three files list which content exists; add the page to each.
src/content/cms.ts: add 'aboutPage' to the key type of ContentEntry, and a line to
CONTENT (this is what puts it in the editor’s sidebar):
{ key: 'aboutPage', kind: 'singleton', label: 'About page', group: 'Pages' },src/content.config.ts: add it to collections:
aboutPage: defineCollection({ loader: loaderFor('aboutPage'), schema: ({ image }) => m(image).aboutPage }),src/lib/content/source.ts: add 'aboutPage' to the Single type, load it in load() next to
the others and return it in pages:
const [site, ui, home, services, jobs, prices, journal, contact, notFound, about] = await Promise.all([ // …the other pages single('notFoundPage', locale, fallbacks), single('aboutPage', locale, fallbacks),]);// …return { locale, langOf, site, ui, home, pages: { services, jobs, prices, journal, contact, notFound, about } };Now every page can read it as Astro.locals.content.pages.about. In the EmDash variant,
source.ts reads from EmDash instead of files but has the same Single type and load():
make the same change there.
3. The content
Section titled “3. The content”Create src/content/singletons/en/aboutPage.json with every required field, or restart
npm run dev and fill it in under Pages → About page in Keystatic. The build stops if the
file is missing a field, and says which. With more languages, add the file in each language’s
folder; until you do, the page shows the default language’s text.
4. The view
Section titled “4. The view”Views in src/views/ are the pages themselves. Create src/views/About.astro:
---import PageHeader from '../components/PageHeader.astro';import Photo from '../components/Photo.astro';import Base from '../layouts/Base.astro';
/** About: who we are, with a photo. */const { pages, langOf } = Astro.locals.content;const page = pages.about;---
<Base title={page.seoTitle} description={page.seoDescription}> <div lang={langOf('aboutPage')}> <PageHeader eyebrow={page.eyebrow} title={page.title} lead={page.lead} /> <div class="mt-14 grid items-start gap-10 lg:grid-cols-2"> <Photo src={page.photo} alt={page.photoAlt} widths={[480, 720, 960]} sizes="(min-width: 64rem) 45vw, calc(100vw - 2rem)" loading="eager" fetchpriority="high" class="rounded-brand" /> <p class="max-w-[60ch] text-lg leading-relaxed whitespace-pre-line">{page.story}</p> </div> </div></Base><Base> gives the page its head (title, description, canonical link, social preview, language
links), header, footer, phone tab bar, chat and search. PageHeader renders the eyebrow, the
h1 (with *stars* marking words, as on the other pages) and the lead. langOf marks the text
with the default language when a translation is missing.
5. The routes
Section titled “5. The routes”Each page has two route files of a few lines. The first serves the default language at the
root, src/pages/about.astro → /about:
---import About from '../views/About.astro';---
<About />The second serves the other languages, src/pages/[locale]/about.astro → /de/about:
---import { exists, otherLocales } from '../../lib/routes';import About from '../../views/About.astro';
export const getStaticPaths = otherLocales;
if (!(await exists(Astro.params))) return Astro.rewrite('/404');---
<About />With one language the second file builds nothing, but keep it: adding a language later then just works.
6. Link it
Section titled “6. Link it”- Site settings → Navigation: add
About→/about. It shows in the header, the footer and the phone menu. Write links without a language prefix (/about, not/de/about): they’re turned into the visitor’s language by themselves. - On phones the tab bar shows four tabs (Interface texts → Navigation → Phone tab bar); every navigation link that isn’t a tab is in its Menu. Swap a tab only if the page matters that much.
7. Sitemap, llms.txt and search
Section titled “7. Sitemap, llms.txt and search”These are built from lists in the code, so a new route needs a line in each:
-
src/pages/sitemap.xml.ts: add'/about'topaths. It’s then listed in every language, with its translations linked. -
src/pages/llms.txt.ts: addlink(pages.about.seoTitle, '/about', pages.about.seoDescription),next to the other pages. -
src/lib/search-pages.ts(optional): add a record so the site search finds it:{ url: '/about', title: pages.about.seoTitle, section: pages.about.seoTitle, markdown: pages.about.story }, -
src/lib/chat-knowledge.ts(optional) holds what the AI chat knows about the site; add the page there if visitors might ask about it.
8. Your CMS
Section titled “8. Your CMS”- Keystatic: nothing more; the page is in the sidebar from step 2.
- Sanity:
npm run cms -- sanity schema, deploy the Studio (cd studio && npm run deploy), then create and publish the About page there. - EmDash:
npm run emdash:schema(see EmDash), then fill the page in the admin.
Then npm run build to check.
Remove a page
Section titled “Remove a page”Say you don’t show finished jobs. The simplest removal keeps the content model as it is and removes the page and every way to reach it:
- Delete the route files:
src/pages/jobs/andsrc/pages/[locale]/jobs/. - Delete the views they used,
src/views/Jobs.astroandsrc/views/Job.astro. - Remove the links in the content: Site settings → Navigation, the phone tab bar in
Interface texts (replace the tab with another page: it has exactly four), and any
button that points at
/jobs. - Remove the home page sections that show it (the
jobssection; see Home page sections). - Remove its lines from
src/pages/sitemap.xml.ts,src/pages/llms.txt.ts,src/lib/search-pages.tsandsrc/lib/chat-knowledge.ts. - Search the project for the path (
/jobs) to catch the rest. In Handy, job cards (JobCard.astro) link to a job’s page, and they also appear in the “Recent jobs” part of each service’s page (src/views/Service.astro): remove that part too. npm run build, then click through the site. The build doesn’t check links, so a forgotten one leads to the 404 page.
Removing the content type itself as well (the Jobs collection in the editor) means undoing
step 2 of “A new page” for it: its entry in cms.ts, content.config.ts and model.ts, and
everything that reads it. Leave that for last, and only if the empty entry in the editor bothers
you.