Labels.io

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

WhatFile
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" cardsapp/routes/about.tsx (reads the first 3 posts)
SEO helper (meta + Open Graph + canonical + JSON-LD)app/lib/seo.ts
Sitemap generatorscripts/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)

  1. Open app/data/blog-posts.ts.
  2. Copy an existing entry in the blogPosts array (the whole { ... } block).
  3. Paste it at the top of the array (newest first — the listing renders in array order).
  4. Change every field: new slug, title, excerpt, date, author, tags, image, and content.
  5. 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:

  • ## Heading and ### Subheading
  • Paragraphs separated by a blank line
  • - bullet lists
  • Inline `code` with backticks

The post <h1> comes from titledon'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 as image: '/my-post.jpg'.
  • The starter posts use https://picsum.photos/... placeholders — replace them with real assets before you publish.
  • The same image is 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 (from title + excerpt)
  • Open Graph + Twitter card tags (title, description, image, url, type=article)
  • A self-referencing <link rel="canonical">
  • keywords (from tags), author, article:published_time / article:modified_time
  • JSON-LD Article structured 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 sitemap

The 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. Bump updated to 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 + compelling excerpt (the excerpt is your meta description)
  • author and at least one tag
  • A real cover image in public/ (wide, ≥1200×630)
  • VITE_SITE_URL set + public/robots.txt host updated (once, before first deploy)
  • pnpm build (regenerates the sitemap) before you ship

On this page