AGENTS.md@layer/modules/blog · git:20261105.d42021f · 2026-11-05 · sha256 8ce0e4de313702ce

AGENTS.md@layer/modules/blog git:20261105.d42021fA

Immutable. This exact content never changes and is served at /api/v1/blob/8ce0e4de313702ce.

# Blog Module — AI Agent Instructions

Git-based blog with render-time scheduling, plus the `foundry-blog` writer (CLI + MCP).

## File Map

```
layer/modules/blog/
├── index.ts                         # Module: pages, endpoints, guard, routeRules (off unless foundry.blog.enabled)
├── README.md                        # User documentation (setup, bin, MCP, leaks)
├── core/
│   ├── post.mjs / post.d.mts        # PURE: postState, isLive, slugify, readMinutes, renderPost, parsePost, validatePostInput
│   ├── store.mjs                    # Node-only: files + git (list/get/upsert/schedule/unpublish)
│   └── mcp.mjs                      # Stdio MCP server over the store
├── runtime/
│   ├── types.ts
│   ├── composables/                 # useBlogPosts, useBlogPost, useBlogCopy (site.yml `blog:`), useBlogSeo
│   ├── utils/meta.ts                # PURE: postType, postLabels, cardMeta, longDate, readTimeLabel
│   ├── components/                  # BlogPostHeader, BlogByline, BlogPostCards, BlogTypeFilter (imported);
│   │                                #   BlogPostAfter, BlogIndexAfter (registered at priority -1: sites override by name)
│   └── pages/                       # BlogIndex.vue, BlogPost.vue (≤50 lines each)
└── server/
    ├── api/                         # posts.get.ts, post.get.ts → /api/_foundry/blog/*
    ├── routes/rss.xml.get.ts        # <prefix>/rss.xml (reuses rss/server/utils/rss-feed)
    ├── middleware/guard.ts          # 404 for raw-source routes of hidden posts
    ├── plugins/discovery.ts         # llms.txt / llms-full.txt / sitemap filters
    └── utils/                       # posts.ts (queries + liveness), guard.ts (pure path parser)
layer/bin/foundry-blog.mjs           # The bin (plain ESM — it runs from node_modules)
```

## Key Architecture

- **One liveness rule, in JS.** `isLive(row, new Date())` from `core/post.mjs` is the only
  place "live" is decided — the server utils, llms filter, sitemap and guard all call it.
  Never filter `publishAt` in SQL and never at build time.
- **`core/post.mjs` stays plain ESM with no Node-only imports** — Nuxt imports it for
  `isLive`; the bin imports it for everything. `store.mjs` and `mcp.mjs` are Node-only
  and must never be imported from `runtime/` or `server/`.
- **The bin has no TypeScript** (Node does not strip types under `node_modules`).
- Pages read through `/api/_foundry/blog/*` (same reason as `useContentPage`), not
  `queryCollection` in the page.
- The `article` layout is NOT reused: it is bound to the route→collection map, a string
  `image`, a `date` field, and `surround` (which would name unpublished posts).

## How to Modify

- **New page copy or label:** add it to the `blog` group of `baseConfigSchema`
  (`layer/content.collections.ts`) and `BlogCopy` (`runtime/types.ts`); unset must render
  as before. Switches (not words) go in `BlogOptions` + `DEFAULTS` (`index.ts`).
- **New frontmatter field:** add to `baseBlogSchema` (`layer/content.collections.ts`),
  `postInputSchema` + `KEY_ORDER` + `post.d.mts` (`core/post.mjs`), then use it in the pages.
- **New leak surface:** add the request shape to `server/utils/guard.ts` (with a test in
  `playground/test/unit/blog-post.test.ts`) or a hook to `plugins/discovery.ts`.
- **New CLI command:** add it to `core/store.mjs`, the switch in `bin/foundry-blog.mjs`,
  and a tool in `core/mcp.mjs`; extend `playground/test/unit/blog-{cli,mcp}.test.ts`.

## Design Principles
- Fail closed — a bad `publishAt` is a draft, an unreadable post is hidden.
- The file is the record — every write is one pathspec commit; never touch other paths.
- Scheduling is visibility, not secrecy (see the README's residual leak).