stnd.buildSTANDARD MANUAL2026-09-20

Personal Blog Recipe

Architectural blueprint and file tree for creating a personal blog with typeset Markdown articles and reading time calculation.

Personal Blog Recipe

This recipe details the architecture of an editorial personal blog. In the Standard philosophy, an article should feel like a page in a finely printed book rather than an unstyled document. This template provides chronological archiving, tag categorization, estimated reading time, and baseline-grid typography.

Instant Scaffolding

To scaffold this project instantly with @stnd/cli, run:

npx @stnd/cli new my-blog --template blog
cd my-blog
pnpm install
pnpm dev

File Structure

The blog feature is packaged as a single vertical slice inside modules/blog/. Markdown content resides directly in modules/blog/content/posts/:

my-blog/
  astro.config.mjs               # Standard integration + Cloudflare
  package.json                   # Dependencies and scripts
  tsconfig.json                  # Path aliases
  modules/
    blog/
      index.module.js            # Module manifest mapping list and post routes
      helpers.js                 # Data fetching, sorting, and reading time helpers
      routes/
        index.astro              # Chronological index of published posts
        [...slug].astro          # Article reader with book typography
      content/
        posts/
          2026-09-01-welcome.md
          2026-09-08-the-art-of-typography.md
          2026-09-14-minimal-toolchains.md

Architectural Breakdown

1. Astro Configuration (astro.config.mjs)

import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import standard from "@stnd/core";

export default defineConfig({
  site: "https://myblog.example",
  output: "server",
  adapter: cloudflare(),
  integrations: [
    standard({
      title: "Personal Journal",
    }),
  ],
});

2. Module Manifest (modules/blog/index.module.js)

The manifest maps the archive view to / and individual article pages to /[...slug] (or /posts/[...slug]):

export default {
  id: "blog",
  name: "Blog",
  description: "Personal publishing vertical slice.",
  routes: [
    { path: "/", entrypoint: "./routes/index.astro" },
    { path: "/posts/[...slug]", entrypoint: "./routes/[...slug].astro" },
  ],
};

3. Data Helper (modules/blog/helpers.js)

Per Law 3 (“.astro holds no logic”), content loading, frontmatter parsing, reading time calculation, and chronological sorting are extracted to a pure JavaScript helper:

/**
 * Approximate reading time in minutes based on 200 words per minute.
 */
export function calculateReadingTime(text = "") {
  const words = text.trim().split(/\s+/).filter(Boolean).length;
  return Math.max(1, Math.ceil(words / 200));
}

/**
 * Load and sort all blog posts by date descending.
 */
export function getAllPosts(rawGlob) {
  return Object.entries(rawGlob)
    .map(([filepath, post]) => {
      const frontmatter = post.frontmatter || {};
      const body = typeof post.rawContent === "function" ? post.rawContent() : "";
      
      const filename = filepath.split("/").pop().replace(/\.md$/, "");
      const slug = frontmatter.slug || filename;
      const readingTime = calculateReadingTime(body);

      return {
        slug,
        title: frontmatter.title || "Untitled",
        date: frontmatter.date || "1970-01-01",
        description: frontmatter.description || "",
        tags: Array.isArray(frontmatter.tags) ? frontmatter.tags : [],
        readingTime,
        Content: post.Content,
      };
    })
    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}

4. Blog Index View (modules/blog/routes/index.astro)

The index displays the chronological post list, metadata tags, and reading estimates:

---
export const prerender = true;
import { getAllPosts } from "../helpers.js";

const rawPosts = import.meta.glob("../content/posts/*.md", { eager: true });
const posts = getAllPosts(rawPosts);
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Journal &mdash; Thoughts & Notes</title>
  </head>
  <body class="rhythm inky">
    <div class="box max-w-2xl py-16">
      <header class="mb-12 border-bottom pb-6">
        <h1 class="text-3xl font-bold mb-2">Journal</h1>
        <p class="opacity-70 text-sm spec">Personal essays, notes, and architectural studies.</p>
      </header>

      <main class="space-y-12">
        {posts.map((post) => (
          <article class="group">
            <div class="flex items-baseline justify-between text-xs spec opacity-60 mb-1">
              <time datetime={post.date}>{post.date}</time>
              <span>{post.readingTime} min read</span>
            </div>
            <h2 class="text-xl font-bold mb-2">
              <a href={`/posts/${post.slug}`} class="hover:underline">{post.title}</a>
            </h2>
            {post.description && <p class="opacity-80 text-sm mb-3">{post.description}</p>}
            {post.tags.length > 0 && (
              <div class="flex gap-2">
                {post.tags.map((tag) => (
                  <span class="spec text-xs px-2 py-0.5 border rounded-none opacity-60">#{tag}</span>
                ))}
              </div>
            )}
          </article>
        ))}
      </main>
    </div>
  </body>
</html>

5. Article Reader Route (modules/blog/routes/[...slug].astro)

The single post view wraps the rendered Markdown content with book typography:

---
export const prerender = true;
import { getAllPosts } from "../helpers.js";

export async function getStaticPaths() {
  const rawPosts = import.meta.glob("../content/posts/*.md", { eager: true });
  const posts = getAllPosts(rawPosts);

  return posts.map((post) => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = post;
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>{post.title} &mdash; Journal</title>
  </head>
  <body class="rhythm inky">
    <article class="box prose max-w-prose py-16">
      <nav class="mb-8">
        <a href="/" class="spec text-xs opacity-60 hover:opacity-100">&larr; Back to Journal</a>
      </nav>

      <header class="mb-10 border-bottom pb-6">
        <div class="flex items-center justify-between text-xs spec opacity-60 mb-2">
          <time datetime={post.date}>{post.date}</time>
          <span>{post.readingTime} min read</span>
        </div>
        <h1 class="text-3xl font-bold tracking-tight mb-4">{post.title}</h1>
        {post.description && <p class="text-lg opacity-80 leading-snug">{post.description}</p>}
      </header>

      <div class="content rhythm">
        <Content />
      </div>
    </article>
  </body>
</html>

Post Frontmatter Schema

Each article stored under modules/blog/content/posts/*.md follows this frontmatter specification:

---
title: The Art of Typographic Precision
date: 2026-09-08
description: Exploring how baseline grids and mathematical measures shape reader retention.
tags:
  - design
  - typography
---

Your markdown content begins here...