Public Pages
Public pages are standard Astro pages that fetch content from the local API.
The scaffold
Section titled “The scaffold”New projects ship a minimal route structure:
src/pages/ index.astro # home page [slug].astro # pages by slug[slug].astro fetches a document from the pages collection and renders its content-type body field with <ContentRenderer>. Here is the full pattern — preview-aware fetch, cache tagging, and live-preview attributes in one place:
---import PublicLayout from "@/layouts/PublicLayout.astro";import ContentRenderer from "@/components/ContentRenderer.astro";import { cms } from "@/cms/.generated/api";import { cacheTags } from "@kidecms/core";
const isPreview = Astro.url.searchParams.has("preview");const doc = await cms.pages.findOne({ slug: Astro.params.slug!, status: isPreview ? "any" : "published" });if (!doc) return Astro.redirect("/404");if (isPreview) Astro.cache.set(false); // never cache draft responseselse Astro.cache.set({ tags: cacheTags("pages", doc._id) });---
<PublicLayout title={doc.title}> <h1 data-cms="title">{doc.title}</h1> <div data-cms="body"> <ContentRenderer content={doc.body} /> </div></PublicLayout>As you add collections, give each content type its own route file (blog/[slug].astro for a posts collection, and so on) following the same pattern.
Live preview
Section titled “Live preview”The admin’s live preview updates the public page as the editor types. Pages opt in with two things, both visible in the snippet above:
- Fetch with
status: isPreview ? "any" : "published"so draft content renders when?previewis in the URL. - Add
data-cms="{fieldName}"attributes to the elements that render CMS fields. The preview client is auto-injected on every page but activates only when?previewis present; it updates matching elements over a BroadcastChannel without a reload.
Preview requests also skip caching (the if (!isPreview) guard), so editors always see the latest saved content.
Querying the local API
Section titled “Querying the local API”All content queries go through the typed local API. This gives you full type safety and access to all query options.
---import { cms } from "@/cms/.generated/api";
// List published postsconst posts = await cms.posts.find({ status: "published", sort: { field: "_createdAt", direction: "desc" }, limit: 10,});
// Find by slugconst post = await cms.posts.findOne({ slug: "hello-world" });
// Find by IDconst post = await cms.posts.findById("abc123");---See Local API for the full query API.
Caching
Section titled “Caching”Content pages use Astro’s route caching with tag-based invalidation. Three parts: the Astro config that enables caching, the tags a page sets, and the hooks that invalidate them.
Astro config
Section titled “Astro config”Route caching needs a cache provider and route rules in astro.config.mjs — without them, Astro.cache.set() is a no-op:
import { defineConfig, memoryCache } from "astro/config";
export default defineConfig({ // ... cache: { provider: memoryCache(), }, routeRules: { "/": { maxAge: 86400, swr: 3600 }, "/blog/**": { maxAge: 86400, swr: 3600 }, },});Tagging pages
Section titled “Tagging pages”---if (isPreview) { // Skipping set() is not enough: route rules alone activate caching, so a // cached draft response could be served to anonymous visitors. Opt out. Astro.cache.set(false);} else { Astro.cache.set({ tags: cacheTags("posts", doc._id) });}---cacheTags generates the tag array — cacheTags("posts", doc._id) returns ["posts", "post:abc123"], equivalent to writing the raw array by hand.
Invalidating on content changes
Section titled “Invalidating on content changes”Invalidation is code you write: nothing invalidates per-document tags automatically. Add after* hooks to the collection and call context.cache?.invalidate():
import { cacheTags } from "@kidecms/core";
hooks: { afterPublish(doc, context) { context.cache?.invalidate({ tags: cacheTags("posts", String(doc._id)) }); },},The one built-in case: deleteMany invalidates the collection tag (posts) on its own. Everything else — publish, update, unpublish — needs a hook. See Hooks for the full pattern.
Block-driven pages (optional pattern)
Section titled “Block-driven pages (optional pattern)”For landing-page-style content, define a collection with a blocks field and render it with <BlockRenderer>:
---// e.g. src/pages/[...slug].astroimport PublicLayout from "@/layouts/PublicLayout.astro";import BlockRenderer from "@/components/BlockRenderer.astro";import { cms } from "@/cms/.generated/api";import { cacheTags, parseBlocks } from "@kidecms/core";
const isPreview = Astro.url.searchParams.has("preview");const doc = await cms.landing.findOne({ slug: Astro.params.slug!, status: isPreview ? "any" : "published" });if (!doc) return Astro.redirect("/404");if (isPreview) Astro.cache.set(false);else Astro.cache.set({ tags: cacheTags("landing", doc._id) });
const blocks = parseBlocks(doc.blocks);---
<PublicLayout title={doc.title}> <h1>{doc.title}</h1> <BlockRenderer blocks={blocks} /></PublicLayout><BlockRenderer> maps each block type to an Astro component in src/components/blocks/ (PascalCase filename → camelCase block type — Hero.astro renders hero blocks). Block fields are passed as props:
---const { eyebrow, heading, body, ctaLabel, ctaHref } = Astro.props;---
<section> {eyebrow && <p>{eyebrow}</p>} <h2>{heading}</h2> {body && <p>{body}</p>} {ctaLabel && ctaHref && <a href={ctaHref}>{ctaLabel}</a>}</section>For fields that store JSON arrays (repeaters, image lists), use the parseList helper:
---import { parseList } from "@kidecms/core";
const { heading, items: rawItems } = Astro.props;const items = parseList<{ title?: string; description?: string }>(rawItems);---
<h2>{heading}</h2>{ items.map((item) => ( <div> <p>{item.title}</p> <p>{item.description}</p> </div> ))}Block types without a matching component render generically. No code is needed for basic blocks.
Images
Section titled “Images”Use cmsImage and cmsSrcset for optimized images:
---import { cmsImage, cmsSrcset } from "@kidecms/core";---
<!-- Single optimized image --><img src={cmsImage(doc.image, 800)} alt={doc.title} />
<!-- Responsive with srcset --><img src={cmsImage(doc.image, 1024)} srcset={cmsSrcset(doc.image)} sizes="(max-width: 640px) 100vw, 640px" alt={doc.title}/>For cropped, art-directed images (e.g. a wide hero on desktop recomposed for mobile), use <CmsPicture>. It resolves the asset’s focal point, crops server-side, emits AVIF + WebP sources, and sets width/height to prevent layout shift:
---import CmsPicture from "@/components/CmsPicture.astro";---
<CmsPicture src={doc.image} alt={doc.title} preset="banner" art={[{ media: "(max-width: 640px)", preset: "heroMobile" }]} loading="eager" fetchpriority="high"/>Images are transformed on-demand (Sharp) and cached to disk. See Assets for cropping, focal points, presets, and the full <CmsPicture> reference.