Show navigationHide navigation
Recipes
Short task guides for editors and integrators.
Create or edit a content entry#
- Sign in at
/cmswith GitHub. - Open a collection from the navigation (for example Posts).
- Create a new entry or select an existing one.
- Edit fields; use Save when done. If the schema marks fields
required, empty values show errors and Save stays disabled until they are fixed.
Entry title in the list usually comes from the field marked entryTitle in config.
Add a featured or inline image#
- In an image field, open the picker or upload flow.
- Upload creates a media entry and sets the field to its UUID; or pick an existing image from the library.
- For bulk management, use
/cms/media.
Link to other entries (references)#
- In a reference field, use Add existing to search and attach entries, or Create new to add an entry and link it in one flow.
- Drag handles reorder references when
manycardinality is used.
Read content from server code#
Use query() in a Server Component, route handler, or server action. Wrap it in a cached helper with "use cache" + cacheTag() for automatic invalidation on save (see Tutorial). There is no separate /api/content/... route; expose your own API if a client needs JSON over HTTP.
Search entries (admin)#
Press ⌘K (or Ctrl K) on any admin page to open the command palette. Type to search across all content in real-time (matches work with typos and partial words).
Results are grouped by collection type, with extra Branches and Actions sections. Click any entry result to jump to the entry editor.
Search includes all searchable fields (configurable via searchable: false flag in field definitions).
Add search to a public page#
-
Decide which collections should be searchable. In
cms/octocms.config.ts, add them tosearch.publicCollectionswith URL patterns:typescriptsearch: { publicCollections: { post: { urlPattern: '/blog/:slug' }, // posts with slug field map to /blog/my-post }, } -
Import and use the
<SearchBox />component on your page:tsximport SearchBox from 'octocms/components/public/SearchBox'; export default function Header() { return <SearchBox placeholder="Search posts..." className="max-w-sm" />; } -
Save an entry in the CMS to rebuild the search index. The component will now find results.
See Search features for detailed configuration and customization.
Exclude a field from search#
By default, text-like fields are indexed. To exclude:
typescriptfields: { myField: { label: 'My Field', format: 'text', searchable: false, // ← Won't appear in search results }, }
Find the schema for this repo#
Open cms/octocms.config.ts — it defines all collections and fields. Run npx octocms types:gen after any schema change to regenerate TypeScript types.
Add a sitemap#
Create src/app/sitemap.ts to auto-generate an XML sitemap from CMS content:
ts// src/app/sitemap.ts import type { MetadataRoute } from 'next'; import { query } from 'octocms/query'; const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://example.com'; export default async function sitemap(): Promise<MetadataRoute.Sitemap> { const posts = await query('post') .filter((p) => p.fields.publishedAt != null) .toArray(); return [ { url: BASE_URL, lastModified: new Date(), changeFrequency: 'daily', priority: 1.0 }, { url: `${BASE_URL}/blog`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.8 }, ...posts.map((p) => ({ url: `${BASE_URL}/blog/${p.fields.slug}`, lastModified: new Date(p.fields.publishedAt!), changeFrequency: 'weekly' as const, priority: 0.7, })), ]; }
Add an RSS feed#
Create an App Router Route Handler that returns XML:
ts// src/app/feed.xml/route.ts import { query } from 'octocms/query'; const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://example.com'; export async function GET() { const posts = await query('post') .filter((p) => p.fields.publishedAt != null) .sort('publishedAt', 'desc') .limit(20) .toArray(); const items = posts .map( (p) => ` <item> <title><![CDATA[${p.fields.title}]]></title> <link>${BASE_URL}/blog/${p.fields.slug}</link> <pubDate>${new Date(p.fields.publishedAt!).toUTCString()}</pubDate> <guid>${BASE_URL}/blog/${p.fields.slug}</guid> </item>`, ) .join(''); const xml = `<?xml version="1.0" encoding="UTF-8"?> <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> <channel> <title>My Blog</title> <link>${BASE_URL}</link> <description>Latest posts</description> <atom:link href="${BASE_URL}/feed.xml" rel="self" type="application/rss+xml"/> ${items} </channel> </rss>`; return new Response(xml, { headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' }, }); }
Enable Draft Mode preview#
Preview unpublished content (entries with sys.status of draft) before publishing.
1. Create enable/disable routes:
ts// src/app/api/draft/enable/route.ts import { draftMode } from 'next/headers'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); if (searchParams.get('secret') !== process.env.DRAFT_MODE_SECRET) { return new Response('Invalid secret', { status: 401 }); } const dm = await draftMode(); dm.enable(); const redirectTo = searchParams.get('redirect') ?? '/'; return Response.redirect(new URL(redirectTo, request.url)); }
ts// src/app/api/draft/disable/route.ts import { draftMode } from 'next/headers'; export async function GET() { const dm = await draftMode(); dm.disable(); return Response.redirect('/'); }
2. Use .includeDrafts() in your page when Draft Mode is active:
ts// src/app/blog/page.tsx import { draftMode } from 'next/headers'; import { query } from 'octocms/query'; export default async function BlogPage() { const { isEnabled } = await draftMode(); const q = query('post').sort('publishedAt', 'desc'); const posts = isEnabled ? await q.includeDrafts().toArray() : await q.filter((p) => p.fields.publishedAt != null).toArray(); return ( <> {isEnabled && <div>Draft Mode ON</div>} <ul> {posts.map((p) => ( <li key={p.sys.id}> {p.fields.title} {p.sys.status === 'draft' && '(draft)'} </li> ))} </ul> </> ); }
3. Enable Draft Mode by visiting /api/draft/enable?secret=YOUR_SECRET&redirect=/blog. Set DRAFT_MODE_SECRET in your .env.local.
Fetch content from a client component#
Client Components cannot call query() directly. Create an API route and fetch from it:
ts// src/app/api/posts/route.ts import { query } from 'octocms/query'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const page = Number(searchParams.get('page') ?? '1'); const result = await query('post') .sort('publishedAt', 'desc') .skip((page - 1) * 10) .limit(10) .paginate(); return Response.json(result); }
Then fetch from your client component:
tsx'use client'; import { useState, useEffect } from 'react'; export function LatestPosts() { const [posts, setPosts] = useState([]); useEffect(() => { fetch('/api/posts') .then((r) => r.json()) .then((data) => setPosts(data.items)); }, []); return <ul>{posts.map((p: any) => <li key={p.sys.id}>{p.fields.title}</li>)}</ul>; }
Add on-demand revalidation from a webhook#
The built-in endpoint GET /api/revalidate/<tag> expires cache tags immediately. Call it from a GitHub webhook, CI pipeline, or external service:
bash# Invalidate all OctoCMS-backed public "use cache" readers after deployment curl https://your-site.com/api/revalidate/octocms:content
For more control, create a secured webhook handler:
ts// src/app/api/webhook/route.ts import { revalidateTag } from 'next/cache'; export async function POST(request: Request) { const secret = request.headers.get('x-webhook-secret'); if (secret !== process.env.WEBHOOK_SECRET) { return new Response('Unauthorized', { status: 401 }); } revalidateTag('octocms:content', { expire: 0 }); return Response.json({ revalidated: true }); }