Publishing Without Rebuilds | Making My Website Easier to Write For

I like writing in my own editor. I do not like turning every article into a software release.

That was the small problem with this website. Articles lived inside the Next.js app as MDX files under src/app/writings/<slug>/page.mdx. Publishing meant creating a folder, adding the article, committing, pushing, waiting for Amplify to rebuild, then checking the live site.

That workflow is fine for code. It is annoying for prose.

I wanted a simpler rule: writing should feel like publishing, not deploying.

What Changed

The old setup was not broken. Static MDX in a Next.js app is clean, understandable, and cheap. The problem was that content and application code shared the same release path.

So I split them.

Articles are still plain MDX files, but they no longer live in the app deployment. A publish script uploads the article folder, writes a small metadata record, and asks the site to refresh its cache.

The shape is:

  1. page.mdx holds the article body.
  2. Images sit beside the article.
  3. A metadata index stores slug, title, description, author, date, and status.
  4. The site reads the article at request time.
  5. A cache keeps repeat reads cheap.

Publishing now looks like this:

mkdir content/articles/my-new-thing
$EDITOR content/articles/my-new-thing/page.mdx
./scripts/publish-article.sh content/articles/my-new-thing

That is the level of ceremony I wanted. One command. No repo commit. No rebuild.

Why Keep Plain Files?

Because plain files are boring in exactly the right way.

An article folder with page.mdx and a few images is easy to inspect, back up, move, and understand. I am not locked into a hosted editor. I am not dependent on a CMS export path. I am not storing prose in a shape that only makes sense to one product.

The current implementation uses S3 because the site already runs on AWS and S3 is cheap, durable object storage. But the important decision is not "use AWS." The important decision is "keep writing portable."

Why Add a Metadata Index?

I considered using file storage alone: list article folders, read the frontmatter, sort by date, and call it done.

That works until it does not. Drafts, tags, publish status, ordering, and article previews all want a real index. Once you need that, scanning files starts feeling clever in the wrong direction.

So the article body lives as a file, and the article listing lives in DynamoDB. The table is not a CMS. It is just a catalog. Ask for published articles, newest first, and render the result.

At personal-site scale, that is still basically free. More importantly, it keeps the system honest: files store content, the index stores metadata, the app renders both.

Why Not a CMS?

A CMS would work, but it would solve a bigger problem than I had.

I did not need approvals, collaboration, content modeling, preview workflows, or a hosted editor. I needed to write locally and publish without redeploying the app.

Adding a CMS would have changed the writing workflow to fix the publishing workflow. That was backwards. The better fit was a small content pipeline built around files I already like writing.

Images Needed a Real Answer

Articles are not always just text. The old setup could import local images directly because the images were in the repo. Once articles moved out of the app, image paths needed to survive too.

Now an article can use ordinary Markdown:

![Penny trickle illustration](./penny_trickle.png)

At render time, the site rewrites that relative path to a CDN URL for the same article folder. The article stays readable as Markdown, and the browser gets a fast image URL.

The content bucket is not public. Browser-facing assets go through the CDN; server-side article reads use runtime permissions. I still want to tighten this by separating public assets and private MDX into different prefixes, but the model is clear enough: browsers get images, the app gets content.

The Part That Bit Me

The trickiest part was not storage. It was runtime configuration.

Build-time configuration and server-side runtime configuration are not the same thing. I knew that in theory. The migration reminded me in practice.

Non-secret values like the content location, metadata table, and image CDN URL can be inlined during build. Secrets cannot. The revalidation endpoint needs a shared secret, but I did not want that secret baked into deployed JavaScript. So the server-side runtime fetches it from a managed parameter store using its own permissions.

That distinction is dry, but important: configuration can be bundled; secrets should be fetched.

Cache Makes It Practical

The site does not read from storage on every request. Article lists, metadata, and MDX bodies are cached through Next.js with a short TTL.

Publishing clears the relevant cache entries:

  1. Upload files.
  2. Write metadata.
  3. Revalidate the listing and article.
  4. Refresh the page.

That gives me the part I care about: the site feels current without making every request expensive.

Cost Matters

This was a cost-aware decision, because small sites should not grow expensive systems just to feel sophisticated.

The new pieces are tiny line items:

ServiceExpected monthly cost
File storagepennies
Metadata indexpennies
Image deliverylikely free-tier
Secret storagefree or pennies

The main bill is still website hosting, which I was already paying for. The article layer adds almost nothing.

That matters. A personal site should be easy to maintain emotionally and financially. If the architecture creates a monthly bill that makes me think twice before writing, it has failed.

Loose Ends

The system works, but it is not done.

The infrastructure needs to be fully captured as code. Some permissions and hosting settings were done manually while debugging, which is fine for exploration and bad for recovery.

The publish script should also get stricter. It needs better slug validation, status validation, and partial-failure handling. Uploading files and writing metadata are separate operations; if one succeeds and the other fails, the system can land half-published.

And I still want the cleaner public/private split for article assets and MDX bodies.

None of these change the direction. They are just the chores that turn a working system into a dependable one.

Was It Worth It?

Yes.

The old workflow treated writing like code deployment. The new workflow treats writing like publishing.

Now I can write locally, keep the article as plain MDX, keep images beside it, run one command, and let the website pick it up without a rebuild.

Nothing fancy. Just less friction between having a thought and putting it on the internet.