glokki

BlogGuides

How we added a blog to our Next.js site with glokki collections

One collection, two blocks and an RSS route. How the glokki.com blog is built, what we reused from the docs section, and the two limits we hit.

glokki team3 min read

The blog you are reading was added to glokki.com in an evening. It is not a separate app or a Markdown folder in the repo: posts are entries of a glokki collection, rendered by two blocks, edited in the same visual editor as the rest of the site. This post walks through how it fits together.

A collection is the data model

A collection in glokki is a set of pages that share fields and a URL prefix. The docs on this site are one (/docs), the blog is another (/blog). We created it over MCP with one call:

FieldTypeUsed for
contenttextareaMarkdown body
summarytextarealead under the title, card text, RSS description
categoryselectEngineering, AI & agents, Product, Guides, Comparisons
coverimagecard and header image
authortextbyline
featuredbooleanthe large card at the top of /blog
updateddate"Updated" line on revised posts

The post date and topics are not fields: every entry already has publishedAt and tags.

Because every post is a page, the things the site already does for pages apply to posts with no extra routes: ISR, preview links, the visual editor, /sitemap.xml, and a Markdown version of every page for agents (/blog/<slug>.md).

Block one: the post

blog-post renders a single entry. Its props are not typed in by an editor; they are bound to the page and to the collection fields:

const blogPostSchema = z.object({
  title: pageField("title"),
  slug: pageField("slug"),
  publishedAt: pageField("publishedAt"),
  summary: collectionField("summary"),
  content: collectionField("content"),
  category: collectionField("category"),
  cover: collectionField("cover"),
  author: collectionField("author"),
  showCover: z.boolean().default(true),
})

The component draws the byline (author, date, reading time computed from the body), the cover, the Markdown body with the same renderer and outline rail as the docs, and a BlogPosting JSON-LD script. The block also declares how it renders to Markdown, so the agent-facing version of a post is the title, byline, summary and body, without the page chrome.

Block two: the list

blog-list shows posts newest first, in two variants:

  • grid for /blog: a featured card, then a card grid, with category filters.
  • compact for under a post ("More from the blog") or the home page: date and title, three rows.

Lists are the part that is easy to get slow. A naive block fetches entries in the browser after hydration, and fetches every field, including every post's full body. We had already solved this for the docs sidebar, so the blog reuses the pattern:

  1. Before rendering, the page walks its block tree and collects one query per blog-list it finds.
  2. The server fetches those entries (sortBy: "published_at", newest first) and trims them to the fields a card shows — never content.
  3. A context provider hands them to the blocks, so the list is in the HTML.

Inside the visual editor the page renders client-side, so the block falls back to fetching through a small same-origin proxy, with the same trimming.

RSS in one route

/blog/rss.xml is a plain Next.js route handler: list the latest 30 entries, map them to <item>s, return application/rss+xml. The only detail worth knowing is routing. The site serves every CMS page from one catch-all route, and a static segment under app/blog/ only claims /blog/rss.xml, so /blog and the posts still go through the catch-all.

Blog pages advertise the feed with <link rel="alternate">, and only pages that exist: our first version also put it on the 404 page under /blog.

Publishing a post refreshes the index

Pages are cached with ISR and purged by a signed webhook when something is published. The webhook purges the path that changed — which is the post, not the /blog index that lists it. One line fixes that: when the published slug starts with /blog/, purge /blog too.

Writing posts

Posts can be written in the editor, but ours start as Markdown files in the repo, with front matter for the fields. A script imports them over the MCP server: it matches entries by slug, creates missing ones as drafts, fills in the fields and seeds the three blocks a post uses. Nothing is published until someone publishes it.

Two limits we hit

  • Tags can't be set over MCP yet. create_page takes collection fields but not tags, so the import script prints them and we add them in the editor.
  • A post page doesn't know its own tags. Tags come back on list entries, not on the delivered page, so the post shows its category but not its topics.

Both are on our list for the CMS. Neither stopped us from shipping.

Build your own

Everything here uses the public SDK: collections, defineBlock() with pageField() / collectionField() bindings, and the delivery API. The block development guide covers bindings, and the integration guide covers the catch-all route, ISR and the revalidation webhook.

Create a free account and add a blog to your own site.

More from the blog

Add a blog to your own site

Free during beta · No credit card required