Managing the blog
The blog ships ready to publish. Posts live in one file, every post gets SEO tags and structured data automatically, and the sitemap regenerates on every build. This doc is how you add, edit, and remove posts — by hand or by asking your AI.
Where everything lives
| What | File |
|---|---|
| All posts (the source of truth) | app/data/blog-posts.ts |
Listing page (/blog) | app/routes/blog.index.tsx |
Post page (/blog/<slug>) | app/routes/blog.$slug.tsx |
| Home page "Working journal" cards | app/routes/about.tsx (reads the first 3 posts) |
| SEO helper (meta + Open Graph + canonical + JSON-LD) | app/lib/seo.ts |
| Sitemap generator | scripts/generate-sitemap.mjs → writes public/sitemap.xml |
You only edit app/data/blog-posts.ts for normal publishing. The pages,
SEO, and sitemap all read from it.
The shape of a post
Every entry in the blogPosts array has these fields:
{
slug: 'my-post', // URL: /blog/my-post — lowercase, hyphens, unique
title: 'My post title', // <h1> + <title> + Open Graph title
excerpt: 'One or two sentences', // listing card + meta description + OG/Twitter + JSON-LD
date: '2026-05-01', // ISO date — shown on the post + sitemap
updated: '2026-05-10', // OPTIONAL — last-edited date (sitemap lastmod + article:modified_time)
readTime: '4 min read', // shown in the byline
author: 'Your Name', // byline + JSON-LD Article author + author meta
tags: ['Product', 'Updates'], // tag pills on the post + keywords meta
image: '/my-post.jpg', // cover image — see "Images" below
content: `...markdown...`, // the body — see "Writing the body" below
}slug, title, excerpt, date, readTime, author, tags, image, and
content are required. updated is optional.
Add a new post (by hand)
- Open
app/data/blog-posts.ts. - Copy an existing entry in the
blogPostsarray (the whole{ ... }block). - Paste it at the top of the array (newest first — the listing renders in array order).
- Change every field: new
slug,title,excerpt,date,author,tags,image, andcontent. - Save. The post appears at
/blog, at/blog/<your-slug>, and (if it's one of the three newest) on the home "Working journal" strip — no other edits.
That's it. SEO tags, canonical URL, JSON-LD, and the sitemap entry are all generated from the fields above.
Writing the body
content is a markdown-flavored string. The starter renderer supports:
## Headingand### Subheading- Paragraphs separated by a blank line
-bullet lists- Inline
`code`with backticks
The post <h1> comes from title — don't put a # H1 inside content.
Want full markdown (images in body, tables, embedded components, frontmatter)? That's the file-based MDX upgrade — see Add Blog, or just ask your AI "convert the blog to MDX with file-based posts".
Images
- Drop a file in
public/(e.g.public/my-post.jpg) and reference it asimage: '/my-post.jpg'. - The starter posts use
https://picsum.photos/...placeholders — replace them with real assets before you publish. - The same
imageis used for the cover and for the Open Graph / Twitter share card, so use a wide (≈16:9) image at least 1200×630 for clean social previews.
SEO — what's automatic, what you control
Every post page automatically emits (via app/lib/seo.ts):
<title>and meta description (fromtitle+excerpt)- Open Graph + Twitter card tags (title, description, image, url, type=article)
- A self-referencing
<link rel="canonical"> keywords(fromtags),author,article:published_time/article:modified_time- JSON-LD
Articlestructured data (headline, author, dates, publisher)
You don't touch the meta directly — you control it through the post fields.
Good excerpt, title, tags, and image = good SEO.
One thing to set before deploying: the production domain. Set
VITE_SITE_URL in your environment (e.g. VITE_SITE_URL=https://yourdomain.com)
so canonical URLs, OG urls, and the sitemap point at the real host instead of
the localhost placeholder. Also update the host in public/robots.txt.
Sitemap
public/sitemap.xml lists every page plus every blog post. It is regenerated
automatically on pnpm build (via the prebuild script), so new posts show
up without any manual step.
To regenerate it on demand:
pnpm sitemapThe generator (scripts/generate-sitemap.mjs) reads the slugs and dates
straight from app/data/blog-posts.ts, so it always matches your posts.
Edit or remove a post
- Edit: change the fields in
app/data/blog-posts.ts. Bumpupdatedto the new date so search engines and the sitemap see the change. - Remove: delete the entry's
{ ... }block from the array. The route, listing, home cards, and the next sitemap build all drop it automatically. - Reorder: the listing follows array order. Move blocks to reorder; keep newest at the top.
Ask your AI
You rarely need to do the steps by hand. Plain-language prompts work:
Add a new blog post titled "Shipping faster with reinforced workflows". Author "Mia Chen", tags Product and Engineering, dated today, with a 5-minute read time. Write ~600 words of body with two
##sections and a short bullet list. Use a placeholder cover image for now.
Update the "Hello, world" post: rewrite the body to introduce our product, set the author to me, and bump the updated date to today.
Regenerate the sitemap and remind me to set VITE_SITE_URL before I deploy.
Convert the blog to file-based MDX so I can write posts as separate files.
Quick checklist before publishing
- Unique, lowercase, hyphenated
slug - Real
title+ compellingexcerpt(the excerpt is your meta description) -
authorand at least onetag - A real cover
imageinpublic/(wide, ≥1200×630) -
VITE_SITE_URLset +public/robots.txthost updated (once, before first deploy) -
pnpm build(regenerates the sitemap) before you ship