
Rebuilding a Pure Astro Theme with Next.js
Notes on porting the Astro Theme Pure design to Next.js, adding a database-backed editor, and keeping the interface quiet.
The original Astro Theme Pure is a study in restraint: a narrow column, a sticky header that politely gets out of the way, and typography that trusts the reader. I wanted to keep all of that while moving the site to a stack that could also host an editor.
This post is a field note on the port: what transferred cleanly, what had to be rebuilt, and where Next.js made life easier.
The shape of the port
The design is not coupled to Astro. Almost everything that makes the theme feel the way it does is ordinary CSS: a small set of design tokens, a 70rem container, a muted palette, and careful spacing.
| Layer | Astro original | Next.js port |
|---|---|---|
| Pages | file-based routes | App Router routes |
| Content | content collections | libSQL + Drizzle |
| MDX | @astrojs/mdx | next-mdx-remote |
| Highlighting | Shiki transformers | same transformers, rehosted |
| Search | Pagefind | generated JSON + Fuse.js |
| Editing | Markdown files | /admin editor |
The table is deliberately boring. A port is successful when the technology disappears and the reading experience remains.
Reusing the Shiki setup
The theme's code blocks have a very particular shape: a title chip, a language label, a copy button, line numbers, optional collapse, and diff/highlight notation. I ported the transformers almost verbatim.
const transformer = (): ShikiTransformer => ({
name: 'pure:add-copy-button',
pre(node) {
node.children.push(
h('button', {
type: 'button',
class: 'copy',
'data-code': this.source
})
)
}
})tsHere is the same idea with notation enabled. The highlighted line is marked in the source with a comment; the added and removed lines use the diff notation.
import { requireAdmin } from '@/lib/auth-guards'
import { createAdminPost } from '@/lib/admin-posts'
import { postInputSchema } from '@/lib/validation'
export async function POST(request: NextRequest) {
const guard = await requireAdmin()
if ('error' in guard) return guard.error
const parsed = postInputSchema.safeParse(await request.json())
// const post = await createAdminPost(body)
if (!parsed.success) {
return Response.json({ error: 'validation_error' }, { status: 422 })
}
const post = await createAdminPost(parsed.data)
revalidatePath('/', 'layout')
return Response.json({ data: post }, { status: 201 })
}tsLong blocks collapse automatically after fifteen lines, which keeps a long post from becoming a wall of code.
The database-backed editor
This was the real change. The public site still wants a simple Post object, so the database schema is shaped to feed it directly: title, description, body, dates, tags, cover image and status.
The editor is intentionally modest:
- Write in Markdown or MDX with a small formatting toolbar.
- Upload a cover image (stored in the database, served from
/api/media/...). - Save as a draft, preview it on the real site, then publish.
- Every save revalidates the public routes.
Two interfaces, one design language
The admin area is not a separate product. It borrows the same tokens, the same radius, the same muted palette. It is a tool, but it should not feel like a different website.
Server components rendered on demand, and a small amount of client-side enhancement for the header, table of contents and code controls.
A client component with autosave-friendly fields, image uploads, and a preview link that opens the real post layout in a new tab.
Route handlers under /api/admin for create, update, delete and upload, plus public read endpoints under /api/posts.
What I would do differently
A few things I would reach for earlier next time:
- Treat the date as data, not a string. Time-zone bugs are quiet and embarrassing.
- Keep the theme's CSS tokens as the single source of truth. Every time I hard-coded a colour, I regretted it.
- Build the preview route before the editor. It changes how the editing flow feels.
- Resist the urge to add a setting for everything. A site like this is a small machine.
The site now has the two things it needed: a reading experience worth keeping, and a way to add the next post without opening a terminal.

The cover art is generated from each post's accent colour.
That is enough architecture for one post. The next one will be about words.
