Skip to content

Public Pages

Public pages are standard Astro pages that fetch content from the local API. The CMS provides helpers to keep them simple.

---
import { findContent, cacheTags } from "@/cms/core/content";
export const prerender = false;
const { doc, isPreview } = await findContent("posts", Astro.params.slug, Astro.url);
if (!doc) return Astro.redirect("/404");
if (!isPreview) Astro.cache.set({ tags: cacheTags("posts", doc._id) });
---
<h1>{doc.title}</h1>
<p>{doc.excerpt}</p>
  • Detects ?preview=true in the URL
  • Queries with status: "any" in preview mode, "published" otherwise
  • Parses block fields from JSON
  • Returns { doc, isPreview, blocks }

Generates cache tag arrays for Astro’s route caching. When content changes, hooks invalidate these tags automatically.

cacheTags("posts", doc._id);
// → ["posts", "post:abc123"]

The recommended structure separates each content type into its own route file:

src/pages/
index.astro # home page
blog/[slug].astro # posts
[...slug].astro # pages (catch-all)
src/pages/blog/[slug].astro
---
import PublicLayout from "@/layouts/PublicLayout.astro";
import RichTextContent from "@/components/RichTextContent.astro";
import { findContent, cacheTags } from "@/cms/core/content";
import { cmsImage, cmsSrcset } from "@/cms/core/image";
export const prerender = false;
const { doc, isPreview } = await findContent("posts", Astro.params.slug, Astro.url);
if (!doc) return Astro.redirect("/");
if (!isPreview) Astro.cache.set({ tags: cacheTags("posts", doc._id) });
---
<PublicLayout title={doc.title}>
<h1>{doc.title}</h1>
{
doc.image && (
<img
src={cmsImage(doc.image, 1024)}
srcset={cmsSrcset(doc.image)}
sizes="(max-width: 640px) 100vw, 640px"
alt={doc.title}
/>
)
}
{doc.excerpt && <p>{doc.excerpt}</p>}
<RichTextContent content={doc.body} />
</PublicLayout>
src/pages/[...slug].astro
---
import PublicLayout from "@/layouts/PublicLayout.astro";
import BlockRenderer from "@/components/BlockRenderer.astro";
import { findContent, cacheTags } from "@/cms/core/content";
export const prerender = false;
const slug = (Astro.params.slug ?? "").split("/").filter(Boolean).join("/");
if (!slug) return Astro.redirect("/");
const { doc, isPreview, blocks } = await findContent("pages", slug, Astro.url);
if (!doc) return Astro.redirect("/");
if (!isPreview) Astro.cache.set({ tags: cacheTags("pages", doc._id) });
---
<PublicLayout title={doc.title}>
<h1>{doc.title}</h1>
{doc.summary && <p>{doc.summary}</p>}
<BlockRenderer blocks={blocks} />
</PublicLayout>

<BlockRenderer> renders blocks automatically. Each block type maps to an Astro component in src/components/blocks/:

src/components/blocks/
Hero.astro ← renders "hero" blocks
Text.astro ← renders "text" blocks
Faq.astro ← renders "faq" blocks
Image.astro ← renders "image" blocks

The component name maps to the block type (PascalCase → camelCase). Block fields are passed as props.

src/components/blocks/Hero.astro
---
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 "@/cms/core/content";
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 are rendered generically — no code needed for basic blocks.

Use cmsImage and cmsSrcset for optimized images:

---
import { cmsImage, cmsSrcset } from "@/cms/core/image";
---
<!-- 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}
/>

Images are transformed on-demand (Sharp) and cached to disk. See Assets for details.

Content pages use Astro’s route caching with tag-based invalidation. When you save or publish content in the admin, lifecycle hooks automatically invalidate the relevant cache tags.

---
// Cache this page, tagged with the collection and document ID
if (!isPreview) {
Astro.cache.set({ tags: cacheTags("posts", doc._id) });
}
---

Preview requests (?preview=true) skip caching so editors always see the latest saved content.

You can also query the local API directly without findContent:

---
import { cms } from "@/cms/.generated/api";
// List published posts
const posts = await cms.posts.find({
status: "published",
sort: { field: "_createdAt", direction: "desc" },
limit: 10,
});
// Find by slug
const post = await cms.posts.findOne({ slug: "hello-world" });
// Find by ID
const post = await cms.posts.findById("abc123");
---

See Local API for the full query API.