Skip to content
All posts
Engineeringdjango· 4 min read

From a static site to Next.js and Django: how we gave our writer a real blog

Our static React site was fast and cheap, but only engineers could update it. Here is how we moved to Next.js and Django admin so a writer can publish instantly, and what that cost us.

Muhammad ArslanCo-FounderPublished

Our website used to be a static React app: built on a laptop, uploaded to the server, served by nginx. Fast, cheap, and impossible for anyone but an engineer to update. When we decided to start a blog, that last part became the whole problem. A writer shouldn't need git, a terminal or a build step to publish a post.

This post walks through what we replaced it with, why, and what it cost us.

The requirement

We wrote it down as three lines before touching any code:

  • A non-technical writer publishes from a rich-text editor and the post is live without anyone deploying.
  • Every page, blog included, is server-rendered HTML with proper meta tags, so search engines and link previews see real content.
  • It runs on the single server we already have.

What we considered

Markdown files plus a git-based CMS. Posts live in the repo; a web editor commits them. No database, no backend. It fails our first line: every commit still needs a build and an upload before the post goes live, and our deploy was manual.

A hosted headless CMS. Excellent editors, but it adds a paid third-party dependency and a webhook pipeline for a blog that will publish a few posts a month.

Django admin plus a server-rendered React front end. We already write Django every day, the admin is a solid editing UI once you give it a proper editor, and Next.js renders React on the server. This is what we chose.

How it fits together

Two processes sit behind nginx. Django (under gunicorn) owns the admin, image uploads and a small read-only JSON API for posts. Next.js renders every public page, including the blog, and fetches posts from that API on the server.

nginx routes by path: /admin/, /api/ and /media/ go to Django, everything else goes to Next.js. The browser never talks to Django directly except for the contact form.

Publishing without a deploy

The interesting part is cache invalidation. Next.js caches each rendered page and tags it, so the blog index carries the tag posts and a single post carries post:<slug>. When the writer saves a post in the admin, a Django signal tells Next.js which tags to drop:

@receiver(post_save, sender=Post)
@receiver(post_delete, sender=Post)
def post_changed(sender, instance: Post, **kwargs) -> None:
    transaction.on_commit(
        lambda: revalidate(["posts", f"post:{instance.slug}"])
    )

transaction.on_commit matters: it waits until the database write is committed, so Next.js never re-fetches a post that isn't saved yet. The next visitor gets freshly rendered HTML; everyone after that gets the cached copy again.

If that call ever fails, for example while Next.js is restarting, pages still refresh on their own within an hour. The webhook makes publishing instant; the timer makes it reliable.

Drafts and previews

Drafts are invisible to the public API. Each draft gets a "Preview" link in the admin that renders the post uncached, with a banner and a noindex tag, so the writer sees exactly what will go live without search engines seeing it first.

SEO, handled once

Every post gets its own title, description, canonical URL, Open Graph tags and BlogPosting structured data, generated from the fields the writer already fills in. The sitemap and RSS feed are built from the same API, so a new post appears in both without anyone remembering to update them.

What it cost

This is not free, and pretending otherwise would be the wrong lesson to take from this post.

  • Two services instead of static files. Before, a broken server meant nginx was down. Now it can also mean Node or gunicorn is down, so both run under systemd with automatic restarts.
  • A database to back up. Posts live in PostgreSQL and uploaded images live on disk. Both need backups; the old site needed neither.
  • Rich text means HTML. The editor produces HTML that the site renders as-is, so the admin account is now something to protect, not just a convenience.
  • Scheduled posts are approximate. A post with a future publish time appears within the hourly refresh, not on the exact minute.

For us those were acceptable trades. A marketing site that only engineers can update stops being updated.

Would we do it again?

For a team that already lives in Django and React, yes. The Django admin turned out to be the cheapest good editing experience we could give a writer, and Next.js gave us server-rendered pages without hand-rolling a prerender step.

If your team doesn't run Django already, a git-based CMS with an automated deploy is simpler and has fewer moving parts. Pick the setup your team can keep running.

Building something similar, or stuck halfway through a migration like this one? Tell us what you're working on.

Written byMuhammad ArslanCo-Founder

Have a project in mind?

Tell us what you're building. A co-founder replies within one business day.