Building a GitHub-Backed Blog System
For a long time, I have wanted to maintain a blog consistently. I tried WordPress, hosted my own VPS, and even built my own platform from scratch. Each time, I made the same mistake: I over-engineered the system before I had built the habit of writing.
Instead of focusing on content, I ended up designing databases, thinking through user management, building tagging and category structures, and configuring hosting. I spent more time building the blog than actually writing for it.
As an engineer, that is an easy trap to fall into. Building the system feels like progress. But the outcome I really wanted was simple: a place to write, share ideas, and build a body of work over time.
Design was another challenge. I am not a graphic designer, so every version of my site ended up looking generic. It either looked like a standard WordPress template or a modern but forgettable developer portfolio. It worked, but it never felt distinctive enough.
Over the last 12 months, I have thought more deeply about what I actually want from a blog. I came back to one simple conclusion: the content matters more than the platform. I still needed a site, but I wanted something simple, durable, easy to update, and opinionated enough to stop me from spending weeks polishing infrastructure instead of publishing.
I experimented with AI tools like Lovable, Vercel, GitHub Copilot, Claude Code, and others. They were impressive, but many of the outputs still felt familiar: the same layouts, gradients, and AI-generated design patterns.
Recently, while learning OpenAI Codex, I decided to try again. This time, the goal was not to build the most feature-rich blog platform. The goal was to build a system that would help me write more, publish faster, and maintain the site for the long term.
In this post, I will share that two-hour journey with OpenAI Codex: the architecture I chose, the prompts I used, and how you can build something similar using Codex, GitHub, and a simpler content-first approach.
Why GitHub works well
GitHub is a strong fit for the approach I had in mind because writing can benefit from the same discipline we already use for software.
If each article is written as a Markdown file, Git and GitHub give every post a clear history. I can see what changed, when it changed, and why it changed. With GitHub, I can use branches, pull requests, and a mix of AI and human review to create a simple publishing workflow. GitHub Actions can then provide a validation gate before anything is published to production.
More importantly, GitHub gives me a clean way to separate the content from the web application. That keeps the system simple, maintainable, and much easier to reason about over time.
My goals were straightforward:
- Keep both the content and the application versioned in private repositories.
- Keep the web application mostly static, fast, and simple to deploy.
- Avoid the need for a database.
- Allow draft blog posts to live safely in the content repository without appearing on the public site.
- Make publishing deliberate, but not heavy.
The idea is not to create a full CMS. The goal is to create a lightweight blog website that I can easily update, manage on GitHub, and deploy on free or low-cost hosting like Vercel.
That means I can quickly move away from building the site and focus on what actually matters: writing more blogs.
Some of you may be thinking: "Why not just use Medium, Substack, or one of the many publishing platforms already available?" That is a fair question. Those platforms are feature-rich, polished, and ready to go. I did look through their capabilities, and they are great options for many people.
But where is the fun in that?
And more importantly, how would I explain why GitHub works so well for this use case? 😀
Reason through the architecture with ChatGPT
I used the following simple prompt with ChatGPT, running on GPT-5.5, to reason through the architecture and overall approach.
How to build a blog system that uses markdown for the articles and post and
a github repository for the markdown versioning for the post/articles
And off we go.
You will see that I started with a single-repository design, then asked ChatGPT to refine the architecture into two separate repositories: one for the application and one for the content.
Why Two Repositories
I deliberately chose a two-repository model to separate the content from the blog application.
This gives each part of the system its own lifecycle. The content can evolve independently as I write, edit, and publish articles, while the website can evolve separately as I improve the design, performance, and overall experience.
example-content | example.com |
|---|---|
| Contains Markdown articles, frontmatter metadata, author profiles, images/assets, validation scripts, and the editorial workflow. | Contains the Next.js application, rendering engine, SEO, RSS, sitemap, contact form, Resend integration, deployment workflows, UI, and design system. |
The Publishing Workflow
The publishing pipeline looks like this:
Write Markdown
↓
Open Pull Request
↓
GitHub Actions validate content
↓
Merge to main
↓
Trigger rebuild of website
↓
Next.js statically renders pages
↓
Vercel deploys automatically
While we need to redeploy the site each time we publish a blog post or make changes, the blog is small enough that this takes less than 1 minute on Vercel and can be automated quickly while keeping it lightweight. With this, I get:
- Version history
- Rollback support
- Pull request reviews
- Content validation
- Automated publishing
- Auditability
- Repeatability
- Fast and Free Deployment
The Tech Stack
I kept the stack intentionally simple:
- Next.js App Router
- TypeScript
- Tailwind CSS
- Markdown
- gray-matter
- GitHub Actions
- Vercel for Hosting
- Resend for the contact form
No database. No authentication. No CMS. No GitHub API calls.
Everything is statically rendered at build time, which keeps the architecture simple and the pages fast. It also means there are fewer moving parts, and therefore fewer things to secure, maintain, debug, or pay for.
Markdown as the Content Layer
Each article is just a Markdown file with frontmatter:
title: "Building a GitHub-Backed Blog System"
slug: "building-a-github-backed-blog-system"
description: "How to build a Markdown-based blog with GitHub."
date: "2026-05-26"
author: "example-author"
status: "published"
tags:
- markdown
- github
- nextjs
coverImage: "/content-assets/images/blog/example.png"
The frontmatter does the heavy lifting. It powers SEO, RSS feeds, sitemaps, tags, article pages, metadata, filtering, and draft handling.
Markdown, on the other hand, is boring in the best possible way. It is portable, durable, human-readable, and not tied to any specific vendor or platform. If I ever need to move the blog elsewhere, the content can come with me.
Content Validation with GitHub Actions
One of the most important parts of the system is validation.
Before a post can be merged, GitHub Actions checks that the content is structurally sound. It validates the required frontmatter fields, unique slugs, valid dates, author references, image paths, and Markdown parsing.
I used Zod for schema validation, supported by a lightweight TypeScript validation script. That gives the content repository a simple but effective quality gate.
This makes publishing much safer. Instead of discovering broken metadata, missing images, duplicate slugs, or malformed Markdown after deployment, the pipeline catches those issues before they reach production.
Build-Time Content Loading
The website repository checks out the content repository during a build.
At build time, the structure becomes:
example.com/
content/
posts/
authors/
assets/
The Next.js application reads directly from the local filesystem:
const CONTENT_ROOT = path.join(process.cwd(), "content");
const posts = await getPublishedPosts(CONTENT_ROOT);
This approach avoids a few problems I did not want to introduce.
There are no runtime GitHub API dependencies, no API rate limits to worry about, no slower page loads caused by dynamic content fetching, and no added rendering complexity. The public site also does not depend on GitHub being available at the exact moment someone visits a page.
Everything is built in advance, so the site remains static, simple, and fast.
That creates a better model for everyone. The reader gets a fast website. The author gets a clean publishing workflow. And the system has fewer runtime dependencies to secure, monitor, and maintain.
Full Specification For OpenAI Codex
After refining the requirements and making sure the details matched what I wanted, I asked ChatGPT to turn the architecture into a complete specification that OpenAI Codex could use to build the system.
Write the whole specification for Codex.
For completeness, and because I always find it useful to see the full prompt used to generate the system, here is the full specification I used in OpenAI Codex to generate the blog system. I have changed some of the names and repository references to use examples, but the overall structure, architecture, and level of detail are the same. It is a long specification, but that is the point. I wanted ChatGPT to turn the idea, architecture, constraints, design direction, and operating model into something sufficiently detailed for OpenAI Codex to build on.
⚠️Lengthy Specification Warning⚠️
# Codex Specification: GitHub-Backed Markdown Blog System for example.com
You are building a production-ready, GitHub-backed Markdown blog system for example.com.
The system must use two separate private GitHub repositories under the GitHub account:
https://github.com/example-github-account
Repositories:
1. example-github-account/example-content
2. example-github-account/example.com
Both repositories must be private.
The example-content repository owns all Markdown content, article metadata, author profiles, content images, content validation, and editorial workflow.
The example.com repository owns the Next.js application, routing, rendering, visual design, SEO, RSS, sitemap, robots, contact form, Resend email integration, and deployment.
Important:
Save this entire specification in the example.com repository as:
docs/specification.md
If the docs folder does not exist, create it.
The saved specification must be complete enough for future contributors to understand the architecture, implementation decisions, repository responsibilities, environment variables, build workflow, and acceptance criteria.
Do not create a database.
Do not create authentication.
Do not create a CMS.
Do not create comments.
Do not create newsletter automation.
Do not fetch content from GitHub at runtime for public pages.
The MVP must be static-first, build-time rendered, and production-ready.
============================================================
1. HIGH-LEVEL OBJECTIVE
============================================================
Build a polished personal/professional publishing site for:
https://example.com
The site should allow the author to publish articles by committing Markdown files to the private example-content repository.
The content publishing flow should be:
Author writes Markdown in example-github-account/example-content
↓
Pull request validates content
↓
Merge to main
↓
example-github-account/example-content triggers example-github-account/example.com rebuild
↓
example-github-account/example.com checks out latest content during build
↓
Next.js statically renders pages
↓
Vercel deploys example.com
The system must make GitHub the source of truth for content versioning.
The design must feel premium, editorial, technical, distinctive, and polished.
It must not look like a generic AI-generated Next.js template.
============================================================
2. GITHUB ACCOUNT AND REPOSITORY REQUIREMENTS
============================================================
GitHub owner:
example-github-account
GitHub profile:
https://github.com/example-github-account
Create and use two private GitHub repositories under the example-github-account account:
1. example-github-account/example-content
2. example-github-account/example.com
Both repositories must be private.
Repository visibility requirement:
- example-github-account/example-content must be private
- example-github-account/example.com must be private
- Do not create public repositories
- Do not publish source code or content publicly through GitHub repository visibility
- The website itself will be publicly deployed at https://example.com
- The GitHub repositories must remain private
Repository settings:
- Visibility: private
- Default branch: main
- Enable GitHub Actions
- Add README.md
- Add .gitignore appropriate for Node/Next.js
- Do not commit .env files
- Do not commit secrets
- Do not expose Resend API keys
- Do not expose GitHub tokens
Where practical, configure branch protections:
- Require pull request before merging
- Require status checks before merging
- Require successful validation/build workflows before merging
============================================================
3. REPOSITORY ARCHITECTURE
============================================================
------------------------------------------------------------
3.1 Repository 1: example-github-account/example-content
------------------------------------------------------------
Visibility:
Private
Purpose:
- Markdown articles
- Article metadata
- Author profiles
- Content images
- Content validation
- Editorial workflow
- Publishing source of truth
Expected structure:
example-content/
posts/
2026/
05/
building-a-github-backed-blog-system.md
authors/
joe.md
assets/
images/
blog/
github-backed-blog-cover.png
authors/
joe.png
schemas/
post.schema.ts
author.schema.ts
scripts/
validate-content.ts
.github/
workflows/
validate-content.yml
trigger-example-com.yml
package.json
tsconfig.json
README.md
.gitignore
------------------------------------------------------------
3.2 Repository 2: example-github-account/example.com
------------------------------------------------------------
Visibility:
Private
Purpose:
- Next.js application
- Static site rendering
- Markdown parsing and rendering
- Homepage
- Blog index
- Article pages
- Tag pages
- About page
- Contact page
- Contact form API route
- Resend email integration
- RSS feed
- Sitemap
- Robots
- SEO
- Visual design system
- Deployment to example.com
Expected structure:
example.com/
app/
layout.tsx
page.tsx
globals.css
about/
page.tsx
blog/
page.tsx
[slug]/
page.tsx
tags/
[tag]/
page.tsx
contact/
page.tsx
api/
contact/
route.ts
rss.xml/
route.ts
sitemap.ts
robots.ts
not-found.tsx
components/
site/
Header.tsx
Footer.tsx
MobileNav.tsx
ThemeToggle.tsx
blog/
BlogCard.tsx
FeaturedPost.tsx
ArticleHeader.tsx
ArticleLayout.tsx
ArticleMeta.tsx
TagPill.tsx
contact/
ContactForm.tsx
ContactEmailTemplate.tsx
ContactSuccess.tsx
ContactField.tsx
ui/
Container.tsx
SectionHeading.tsx
GradientOrb.tsx
DecorativeGrid.tsx
SkipLink.tsx
content/
.gitkeep
lib/
content/
posts.ts
authors.ts
markdown.ts
reading-time.ts
content-paths.ts
contact/
contact-schema.ts
rate-limit.ts
resend.ts
seo/
metadata.ts
structured-data.ts
rss/
rss.ts
utils.ts
public/
content-assets/
scripts/
sync-content-assets.ts
docs/
specification.md
.github/
workflows/
build.yml
package.json
tsconfig.json
next.config.ts
tailwind.config.ts
postcss.config.js
README.md
.env.example
.gitignore
============================================================
4. TECHNICAL STACK
============================================================
Use this stack unless there is a strong implementation reason not to:
Framework:
- Next.js latest stable App Router
Language:
- TypeScript
Styling:
- Tailwind CSS
Content:
- Markdown files with frontmatter
Markdown parsing and rendering:
- gray-matter
- remark
- rehype
- react-markdown or unified pipeline
Code highlighting:
- shiki, if practical
Validation:
- zod
Icons:
- lucide-react
Email:
- Resend
- resend Node SDK
Fonts:
- next/font
Deployment target:
- Vercel
Package manager:
- npm
Do not introduce:
- Database
- Authentication
- Headless CMS
- Runtime GitHub API content fetching
- Comments
- Newsletter automation
- Admin dashboard
============================================================
5. CONTENT MODEL
============================================================
All blog posts live in the private repository:
example-github-account/example-content
Posts must be Markdown files with frontmatter.
Example file:
posts/2026/05/building-a-github-backed-blog-system.md
Example content:
---
title: "Building a GitHub-Backed Blog System"
slug: "building-a-github-backed-blog-system"
description: "A practical approach to building a Markdown-based blog using GitHub for versioning."
date: "2026-05-26"
updated: "2026-05-26"
author: "joe"
status: "published"
category: "engineering"
tags:
- markdown
- github
- blogging
- nextjs
coverImage: "/content-assets/images/blog/github-backed-blog-cover.png"
featured: true
seoTitle: "Building a GitHub-Backed Blog System"
seoDescription: "Learn how to build a fast, version-controlled blog using Markdown, GitHub, and Next.js."
canonicalUrl: ""
---
# Building a GitHub-Backed Blog System
Article content goes here.
Required post frontmatter:
- title
- slug
- description
- date
- author
- status
- category
- tags
Optional post frontmatter:
- updated
- coverImage
- featured
- seoTitle
- seoDescription
- canonicalUrl
Allowed statuses:
- draft
- published
- archived
Only posts with:
status: "published"
should render publicly.
Draft and archived posts must not appear in:
- Homepage latest posts
- Blog index
- Article pages
- Tag pages
- RSS feed
- Sitemap
============================================================
6. AUTHOR MODEL
============================================================
Authors live in:
example-github-account/example-content/authors/
Example:
authors/joe.md
Example content:
---
id: "Joe"
name: "Joe Smith"
role: "Artist"
bio: "Writing about Arts."
avatar: "/content-assets/images/authors/joe.png"
linkedin: "https://www.linkedin.com/"
github: "https://github.com/example-github-account"
---
Joe writes about Art.
Author files must be associated with posts through the author ID.
If a post references an author that does not exist, validation must fail.
============================================================
7. example-content VALIDATION
============================================================
In example-github-account/example-content, create:
scripts/validate-content.ts
Use zod for schema validation.
The validation script must check:
- All Markdown files parse correctly
- All posts have required frontmatter
- Post slugs are unique
- Dates are valid ISO date strings in YYYY-MM-DD format
- Status is one of draft, published, archived
- Published posts have at least one tag
- Published posts have a description
- coverImage paths are valid when provided
- Referenced author exists
- Author files parse correctly
- Author IDs are unique
- No duplicate post filenames
- Markdown content exists below frontmatter
- No obviously broken internal links where practical
Create schema files:
schemas/post.schema.ts
schemas/author.schema.ts
Add package scripts in example-github-account/example-content:
{
"scripts": {
"validate:content": "tsx scripts/validate-content.ts",
"typecheck": "tsc --noEmit"
}
}
Add required dependencies:
- zod
- gray-matter
- fast-glob
- tsx
- typescript
============================================================
8. example-content GITHUB ACTIONS
============================================================
Create:
.github/workflows/validate-content.yml
It must run on:
- pull_request
- push to main
It should:
- Checkout repo
- Setup Node
- Run npm ci
- Run npm run typecheck
- Run npm run validate:content
Create:
.github/workflows/trigger-joe-com.yml
It must run on push to main.
It must trigger a repository_dispatch event against:
example-github-account/example.com
Use event type:
content-updated
Use secret:
EXAMPLE_COM_REPO_TOKEN
Example workflow:
name: Trigger example.com Rebuild
on:
push:
branches:
- main
jobs:
trigger:
runs-on: ubuntu-latest
steps:
- name: Trigger example.com deploy
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.EXAMPLE_COM_REPO_TOKEN }}
repository: example-github-account/example.com
event-type: content-updated
Note:
The default GITHUB_TOKEN is not sufficient for dispatching to a different repository. Document that EXAMPLE_COM_REPO_TOKEN must be a Personal Access Token or GitHub App token with appropriate access to trigger repository dispatch in the private example-github-account/example.com repository.
============================================================
9. example.com CONTENT LOADING
============================================================
The example-github-account/example.com repo must expect example-github-account/example-content to be checked out locally during CI/build into:
./content
During build, the local structure should be:
example.com/
content/
posts/
authors/
assets/
Use this root:
const CONTENT_ROOT = path.join(process.cwd(), "content");
Create these files:
lib/content/content-paths.ts
lib/content/posts.ts
lib/content/authors.ts
lib/content/markdown.ts
lib/content/reading-time.ts
The app must:
- Read posts from ./content/posts
- Read authors from ./content/authors
- Parse frontmatter with gray-matter
- Validate content shape with zod or shared compatible schemas
- Convert Markdown into renderable content
- Calculate reading time
- Sort posts by date descending
- Filter public posts to status === "published"
- Exclude drafts and archived posts from public pages
============================================================
10. ASSET SYNC
============================================================
Content assets live in:
example-github-account/example-content/assets/
During the example-github-account/example.com build, copy assets from:
content/assets/
to:
public/content-assets/
Create:
scripts/sync-content-assets.ts
It must:
- Remove existing public/content-assets before copying
- Copy content/assets into public/content-assets
- Gracefully handle missing assets folder in local development
- Log what it copied
Markdown files should reference assets as public URLs:
/content-assets/images/blog/example.png
Add this to example.com package scripts:
{
"scripts": {
"dev": "next dev",
"prebuild": "tsx scripts/sync-content-assets.ts",
"build": "next build",
"lint": "next lint",
"typecheck": "tsc --noEmit"
}
}
If the latest stable Next.js setup no longer supports next lint as expected, configure linting appropriately and make npm run lint work.
============================================================
11. example.com GITHUB ACTIONS
============================================================
Create:
.github/workflows/build.yml
It must run on:
- push to main
- pull_request
- repository_dispatch with type content-updated
It should:
- Checkout example-github-account/example.com into site
- Checkout example-github-account/example-content into site/content
- Use secret CONTENT_REPO_TOKEN
- Setup Node
- Run npm ci
- Run npm run lint
- Run npm run typecheck
- Run npm run build
Example:
name: Build
on:
push:
branches:
- main
pull_request:
repository_dispatch:
types:
- content-updated
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout example.com
uses: actions/checkout@v4
with:
path: site
- name: Checkout example-content
uses: actions/checkout@v4
with:
repository: example-github-account/example-content
path: site/content
token: ${{ secrets.CONTENT_REPO_TOKEN }}
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: site/package-lock.json
- name: Install dependencies
working-directory: site
run: npm ci
- name: Lint
working-directory: site
run: npm run lint
- name: Typecheck
working-directory: site
run: npm run typecheck
- name: Build
working-directory: site
run: npm run build
Document the required GitHub secrets:
- CONTENT_REPO_TOKEN in example-github-account/example.com
- EXAMPLE_COM_REPO_TOKEN in example-github-account/example-content
Token requirements:
- CONTENT_REPO_TOKEN must allow example-github-account/example.com to read the private example-github-account/example-content repo
- EXAMPLE_COM_REPO_TOKEN must allow example-github-account/example-content to trigger repository_dispatch on the private example-github-account/example.com repo
- Use least-privilege access where possible
- Do not commit tokens to either repository
- Store tokens only as GitHub Actions secrets
============================================================
12. SITE ROUTES
============================================================
Implement these routes in example-github-account/example.com.
------------------------------------------------------------
12.1 Homepage
------------------------------------------------------------
Route:
/
Purpose:
- Personal/professional landing page
- Featured writing
- Topic clusters
- Latest posts
- Professional positioning
- Contact pathway
Positioning copy:
Joe Smith
Writing about Arts.
Homepage sections:
- Hero
- Featured article
- Latest writing
- Topic clusters
- Professional note
- Contact call-to-action
The homepage must feel editorial and strategic, not like a generic portfolio template.
------------------------------------------------------------
12.2 Blog Index
------------------------------------------------------------
Route:
/blog
Requirements:
- List all published posts
- Sort newest first
- Show title
- Show description
- Show date
- Show category
- Show tags
- Show reading time
- Support featured post treatment
- Exclude draft and archived posts
------------------------------------------------------------
12.3 Blog Article Page
------------------------------------------------------------
Route:
/blog/[slug]
Requirements:
- Render Markdown content
- Show article title
- Show description
- Show author
- Show published date
- Show updated date when available
- Show reading time
- Show tags
- Show cover image when available
- Generate SEO metadata
- Generate article JSON-LD structured data
- Use polished typography
- Support code blocks
- Support blockquotes
- Support tables
- Support images
- Support links
- Support headings
- Support inline code
- Support ordered and unordered lists
- Return 404 for unknown slugs
- Return 404 for draft or archived post slugs
Article layout:
- On desktop, use a distinctive article metadata rail on the left when appropriate
- Keep the main reading column highly readable
- Use premium editorial spacing
- Make code blocks distinctive and readable
------------------------------------------------------------
12.4 Tag Page
------------------------------------------------------------
Route:
/tags/[tag]
Requirements:
- List published posts for selected tag
- Sort newest first
- Generate tag-specific metadata
- Return 404 if tag does not exist
- Exclude draft and archived posts
------------------------------------------------------------
12.5 About Page
------------------------------------------------------------
Route:
/about
Purpose:
- Professional biography
- Writing themes
- Areas of interest
- Links to GitHub and LinkedIn
- Link to contact page
Keep this elegant, personal, and credible.
Do not make it overly corporate.
------------------------------------------------------------
12.6 Contact Page
------------------------------------------------------------
Route:
/contact
Purpose:
Allow visitors to contact Joe through a polished form.
Page copy direction:
Contact
For thoughtful conversations about Arts, send me a note.
Page requirements:
- Match the premium editorial Arts design system
- Include a short introduction
- Include the contact form
- Include links to LinkedIn and GitHub as secondary contact paths
- Include clear expectations on what the form is for
- Be accessible and keyboard friendly
Form fields:
- name, required
- email, required
- company, optional
- subject, required
- message, required
- honeypot field, hidden from users
Validation rules:
- name: required, 2 to 120 characters
- email: required, valid email format
- company: optional, maximum 160 characters
- subject: required, 4 to 160 characters
- message: required, 20 to 5000 characters
- honeypot: must remain empty
UX requirements:
- Inline validation errors
- Disabled submit button while sending
- Success state after message is sent
- Generic error state if sending fails
- Do not leak internal Resend errors to the user
- Visible focus states
- Keyboard accessible
- Works on mobile and desktop
------------------------------------------------------------
12.7 Contact API Route
------------------------------------------------------------
Route:
POST /api/contact
Create:
app/api/contact/route.ts
The route must:
- Accept JSON form submissions
- Validate request body with zod
- Reject invalid submissions with 400
- Reject honeypot submissions without sending email
- Return generic success for honeypot submissions to avoid signalling bot detection
- Send valid emails using Resend
- Return success when Resend accepts the email
- Return a generic 500 response on send failure
- Never expose RESEND_API_KEY
- Never expose raw provider errors to the client
Email destination:
to: process.env.CONTACT_EMAIL_TO
Email sender:
from: process.env.CONTACT_EMAIL_FROM
Reply-to:
replyTo: submitted email address
Subject format:
[example.com] {submitted subject}
Email body should include:
- Name
- Email
- Company, if provided
- Subject
- Message
- Submitted timestamp
- Source: example.com contact form
Create:
- lib/contact/contact-schema.ts
- lib/contact/resend.ts
- lib/contact/rate-limit.ts if practical
- components/contact/ContactForm.tsx
- components/contact/ContactEmailTemplate.tsx
Use Resend server-side only.
Required environment variables:
- RESEND_API_KEY
- CONTACT_EMAIL_TO
- CONTACT_EMAIL_FROM
- NEXT_PUBLIC_SITE_URL
Add .env.example:
RESEND_API_KEY=
CONTACT_EMAIL_TO=joe.smith@example.com
CONTACT_EMAIL_FROM=example.com <contact@example.com>
NEXT_PUBLIC_SITE_URL=https://example.com
Important:
CONTACT_EMAIL_FROM must use a verified sending domain in Resend.
Document this in README.md.
Spam and abuse protection:
- Honeypot field
- Message length validation
- Maximum message length
- Basic in-memory IP rate limiting if practical without external infrastructure
- No Redis
- No database
- Generic success response for honeypot submissions
Optional future enhancement, not MVP:
- Cloudflare Turnstile
- hCaptcha
- Persistent rate limiting
------------------------------------------------------------
12.8 RSS Feed
------------------------------------------------------------
Route:
/rss.xml
Create:
app/rss.xml/route.ts
Requirements:
- Include all published posts
- Exclude draft and archived posts
- Include title
- Include description
- Include link
- Include pubDate
- Include author if available
- Use site URL https://example.com
------------------------------------------------------------
12.9 Sitemap
------------------------------------------------------------
Route:
/sitemap.xml
Create:
app/sitemap.ts
Requirements:
- Include homepage
- Include about page
- Include contact page
- Include blog page
- Include all published articles
- Include all tag pages
- Exclude draft and archived posts
------------------------------------------------------------
12.10 Robots
------------------------------------------------------------
Route:
/robots.txt
Create:
app/robots.ts
Requirements:
- Allow crawling
- Include sitemap reference:
https://example.com/sitemap.xml
============================================================
13. SEO REQUIREMENTS
============================================================
Use Next.js App Router metadata conventions.
Default site metadata:
- Site name: example.com
- Domain: https://example.com
- Default title: Joe Smith
- Default description: Writing about Arts.
Homepage metadata:
- Title: Joe Smith
- Description: Writing about the Arts.
- Canonical: https://example.com
Blog metadata:
- Title: Writing | Joe Smith
- Description: Articles on Arts.
- Canonical: https://example.com/blog
Article metadata:
- title
- description
- canonical URL
- Open Graph title
- Open Graph description
- Open Graph image
- Twitter card metadata
- article published date
- article modified date
- author
- tags
- JSON-LD Article structured data
About metadata:
- Title: About | Joe Smith
- Description: Learn more about Joe Smith and his writing on the Arts.
- Canonical: https://example.com/about
Contact metadata:
- Title: Contact | Joe Smith
- Description: Contact Joe Smith for conversations about the Arts.
- Canonical: https://example.com/contact
Tag metadata:
- Title: {Tag} | Joe Smith
- Description: Articles tagged with {tag} by Joe Smith.
- Canonical: https://example.com/tags/{tag}
============================================================
14. DESIGN REQUIREMENTS
============================================================
The visual design must be modern, polished, distinctive, and production-ready.
Avoid the common generic AI-generated Next.js look.
Do not use:
- Plain black-and-white SaaS template styling
- Generic blue-purple gradients as the primary identity
- Identical rounded cards everywhere
- Excessive glassmorphism
- Sterile dashboard styling
- shadcn demo aesthetics
- Overly symmetrical template layouts
Desired visual identity:
- Strategic
- Calm
- Modern
- Editorial
- Technical
- Premium
- Personal
- Distinctive
Use dark mode as the primary visual anchor.
Light mode must also be polished if implemented.
Use:
- Asymmetric accents
- Editorial spacing
- Layered backgrounds
- Subtle grid or topographic textures
- Distinctive article cards
- Irregular topic tiles
- Copper and apricot rules
- Strong but restrained colour
- Premium typography
Do not make the site neon.
Use strong colours sparingly.
============================================================
15. TYPOGRAPHY
============================================================
Use next/font.
Recommended direction:
- Headings: Fraunces, Newsreader, or Lora
- Body: Inter, Geist Sans, or Source Sans 3
- Code: Geist Mono or JetBrains Mono
Typography should feel editorial and premium, not startup-generic.
Requirements:
- Strong heading hierarchy
- Comfortable body line height
- Excellent article readability
- Distinctive pull quotes
- Beautiful code styling
- Readable tables
- Good mobile typography
============================================================
16. COMPONENT REQUIREMENTS
============================================================
Create reusable components.
Site components:
- Header
- Footer
- MobileNav
- ThemeToggle if straightforward
- SkipLink
Blog components:
- BlogCard
- FeaturedPost
- ArticleHeader
- ArticleLayout
- ArticleMeta
- TagPill
Contact components:
- ContactForm
- ContactEmailTemplate
- ContactSuccess if useful
- ContactField if useful
UI components:
- Container
- SectionHeading
- DecorativeGrid
- GradientOrb
Component style:
- Cards should not all look identical
- Featured article should feel materially different from standard cards
- Topic clusters should look editorial and slightly irregular
- Article page should be highly readable and premium
- Contact page should feel integrated, not bolted on
============================================================
17. MARKDOWN RENDERING REQUIREMENTS
============================================================
Support:
- Headings
- Paragraphs
- Links
- Images
- Blockquotes
- Ordered lists
- Unordered lists
- Inline code
- Code blocks
- Tables
- Horizontal rules
- Bold
- Italic
Enhance:
- Heading anchors if practical
- Syntax-highlighted code blocks
- External link handling
- Responsive images
- Readable prose styling
Code blocks should include:
- Language label when available
- Copy button if easy to implement cleanly
- Distinctive visual treatment
Do not compromise build quality if copy button becomes too complex.
============================================================
18. ACCESSIBILITY REQUIREMENTS
============================================================
Implement:
- Semantic HTML
- Keyboard-accessible navigation
- Visible focus states
- Skip-to-content link
- Proper heading hierarchy
- Sufficient colour contrast
- Descriptive alt text support
- ARIA only where needed
- Reduced motion support
- Accessible form labels
- Inline form errors associated with fields
- Error and success states readable by assistive technologies
============================================================
19. PERFORMANCE REQUIREMENTS
============================================================
Targets:
- Static generation wherever possible
- Server components by default
- Minimal client components
- Minimal JavaScript
- Optimised images
- No database calls
- No runtime GitHub API calls for public pages
- Good Lighthouse score
- Fast article pages
- Fast homepage
Use client components only where required:
- Contact form interactivity
- Mobile navigation
- Theme toggle if implemented
============================================================
20. ERROR HANDLING
============================================================
Implement:
- 404 for unknown blog slug
- 404 for draft or archived blog slug
- 404 for unknown tag
- Helpful empty state if no posts exist
- Build-time validation for malformed content
- Graceful fallback if author profile is missing, though validation should prevent this
- Generic contact form error if email send fails
- No leaking of server secrets or provider errors
============================================================
21. NAVIGATION REQUIREMENTS
============================================================
Header navigation:
- Writing -> /blog
- About -> /about
- Contact -> /contact
Footer navigation:
- Blog -> /blog
- About -> /about
- Contact -> /contact
- GitHub -> https://github.com/example-github-account
- LinkedIn
- RSS -> /rss.xml
The header should be elegant, minimal, and distinctive.
The footer should feel editorial and complete.
============================================================
22. README REQUIREMENTS
============================================================
Create or update README.md in example-github-account/example.com with:
- Project purpose
- Architecture summary
- Two-repo explanation
- Private repository requirement
- Local development setup
- How to provide local content in ./content
- Build process
- Environment variables
- Resend setup
- Vercel deployment notes
- GitHub secrets required
- How content publishing works
- How to add a blog post
- How to run lint, typecheck, and build
- Confirmation that source repos are private but website is public
Create or update README.md in example-github-account/example-content with:
- Content repo purpose
- Private repository requirement
- Folder structure
- How to add a post
- Required frontmatter
- Author model
- Asset path conventions
- Validation rules
- GitHub Actions
- Publishing workflow
- How example.com rebuild is triggered
============================================================
23. ENVIRONMENT VARIABLES
============================================================
In example-github-account/example.com, create .env.example:
RESEND_API_KEY=
CONTACT_EMAIL_TO=joe.smith@example.com
CONTACT_EMAIL_FROM=example.com <contact@example.com>
NEXT_PUBLIC_SITE_URL=https://example.com
Document:
- RESEND_API_KEY must be created in Resend
- CONTACT_EMAIL_FROM must use a verified Resend sender/domain
- These values must be configured in Vercel
- RESEND_API_KEY must never be exposed to client-side code
GitHub secrets:
In example-github-account/example.com:
- CONTENT_REPO_TOKEN
In example-github-account/example-content:
- EXAMPLE_COM_REPO_TOKEN
Token requirements:
- CONTENT_REPO_TOKEN must allow example-github-account/example.com to read example-github-account/example-content
- EXAMPLE_COM_REPO_TOKEN must allow example-github-account/example-content to trigger repository_dispatch on example-github-account/example.com
- Use least-privilege access where possible
- Do not commit tokens to either repository
- Store tokens only as GitHub Actions secrets
============================================================
24. SAMPLE CONTENT
============================================================
Add enough sample content to demonstrate the system.
In example-github-account/example-content, create at least three posts:
1. Building a GitHub-Backed Blog System
2. How I Use AI as a Leadership Multiplier
3. Why Developer Experience Is Now a Board-Level Topic
At least one post should be featured.
At least one post should include:
- Headings
- Lists
- Blockquote
- Code block
- Tags
- Cover image reference
Create one draft post to validate that drafts do not appear publicly.
If real images are not available, use a placeholder SVG or generated local placeholder asset in the assets folder.
============================================================
25. ACCEPTANCE CRITERIA
============================================================
The project is complete when:
Repository architecture:
- example-github-account/example-content and example-github-account/example.com are separate repositories
- Both repositories are private
- Markdown articles live only in example-github-account/example-content
- Application code lives only in example-github-account/example.com
- The public website deploys to https://example.com
- The full specification is saved in example-github-account/example.com at docs/specification.md
Content:
- example-github-account/example-content contains sample Markdown posts
- example-github-account/example-content contains author profile files
- example-github-account/example-content validates content through GitHub Actions
- Invalid content fails validation
- Draft and archived posts are excluded from public output
Build integration:
- example-github-account/example.com expects example-github-account/example-content to be checked out into ./content
- example-github-account/example.com reads posts and authors from ./content
- Assets from example-github-account/example-content are copied into public/content-assets
- example-github-account/example-content can trigger example-github-account/example.com rebuild via repository_dispatch
- example-github-account/example.com build workflow responds to content-updated dispatch
Routes:
- / works
- /blog works
- /blog/[slug] works
- /tags/[tag] works
- /about works
- /contact works
- /api/contact works
- /rss.xml works
- /sitemap.xml works
- /robots.txt works
Contact:
- Contact form validates name, email, subject, message, company, and honeypot field
- Valid submissions send email through Resend
- Successful submissions show a success state
- Failed submissions show a generic error state
- RESEND_API_KEY is server-side only
- CONTACT_EMAIL_TO and CONTACT_EMAIL_FROM are configurable
- Resend sender/domain verification is documented
SEO:
- Homepage metadata works
- Blog metadata works
- Article metadata works
- Tag metadata works
- About metadata works
- Contact metadata works
- Article JSON-LD is included
- Sitemap excludes drafts and archived posts
- RSS excludes drafts and archived posts
Design:
- UI is distinctive, premium, responsive, and polished
- Design does not look like a generic AI-generated Next.js app
- Homepage feels editorial and strategic
- Article pages are highly readable
- Contact page is visually integrated with the site
- Dark mode is polished
- Light mode is polished if implemented
Quality:
- npm run lint passes in example-github-account/example.com
- npm run typecheck passes in example-github-account/example.com
- npm run build passes in example-github-account/example.com
- npm run typecheck passes in example-github-account/example-content
- npm run validate:content passes in example-github-account/example-content
- No database is introduced
- No authentication is introduced
- No CMS is introduced
- No runtime GitHub content fetching is introduced for public pages
============================================================
26. FINAL CODEX INSTRUCTIONS
============================================================
Before finishing:
1. Confirm that both GitHub repositories are private:
- example-github-account/example-content
- example-github-account/example.com
2. Save this specification in example-github-account/example.com as:
docs/specification.md
3. Ensure both README files explain the two-repo architecture.
4. Ensure .env.example exists in example-github-account/example.com.
5. Ensure the GitHub owner references are consistently:
example-github-account
6. Ensure GitHub repository references are consistently:
example-github-account/example-content
example-github-account/example.com
7. Run lint, typecheck, and build for example-github-account/example.com.
8. Run typecheck and validate:content for example-github-account/example-content.
9. Review the UI and refactor anything that looks like a generic AI-generated Next.js template.
10. Prioritise strong visual identity, spacing, typography, and editorial polish.
11. Ensure the final implementation is ready to deploy to Vercel.
12. Do not commit secrets, .env files, Resend API keys, or GitHub tokens.
What I Learned
ChatGPT can generate highly detailed specifications for an application, but the output's quality depends heavily on the input.
You should spend time refining the specification before asking Codex to build anything. Start with the architecture first. Be clear on the repositories, responsibilities, data flow, validation rules, deployment model, and what should not be built.
Once the architecture was clear, OpenAI Codex became much more effective.
The same applies to design. You need to be explicit about the visual direction, layout, tone, colors, and overall brand experience. Otherwise, AI-generated applications tend to converge toward the same generic templates: clean, modern, but familiar.
My recommendation is to define the brand and design direction early. The clearer the specification, the better the application you get back.
Final Thoughts
AI-assisted development is moving beyond “generate code” and into “generate systems”. The code still matters. Of course it does. But the specification matters more than ever.
In many ways, we are coming back to the discipline of writing clear specifications up front. Not in a slow, heavyweight waterfall way, but with more intent than simply “let’s go agile and figure it out as we go.”
When AI can generate large parts of an application in minutes, the hard part changes. It is no longer just about writing the code. It is about being clear on what should be built, why it should work that way, what should be excluded, and how the system should operate over time. That means the architecture matters. The workflow matters. The constraints matter. The validation gates matter. The operational model matters. AI can move incredibly fast. But without clear direction, it can also generate complexity incredibly fast.
The better the specification, the better the system.
In this example, OpenAI Codex was able to generate a full system in 43 minutes using the specification refined by me and generated by ChatGPT. And here we are on the new blog system! You are reading it here.
Resources
Core technologies used:
- Next.js
- GitHub
- GitHub Actions
- Markdown
- Tailwind CSS
- Vercel
- Resend
- OpenAI Codex