Automated blog post generator for constructpermits.com/permit-blogs.
-
generate.ts(interval/cron) — Fires daily, randomly skips ~50% of days for an organic every-other-day cadence. Uses Claude to generate a blog post in Chase Morrow's voice, auto-selects relevant images (hero + 2 inline), and stores everything in blob storage. -
api-list.ts(HTTP) — Returns paginated blog index with hero images. Supports?limit=N&offset=N. Posts includecategoryfor tab filtering. -
api-post.ts(HTTP) — Returns a single full blog post by slug (/slugor?slug=slug). Includes hero image, inline images embedded in markdown, and full metadata. -
shared/config.ts— System prompt, rotating category prompts (7-week cycle), TypeScript types. -
shared/images.ts— Curated Unsplash image library with keyword-based matching. Auto-selects contextually relevant hero and inline images per post.
| Category | Description |
|---|---|
| Statutory Enforcement | Permit dispute war stories with statute citations |
| Building Department | Behavioral patterns across FL jurisdictions |
| Statute Deep-Dive | Plain-English breakdowns of specific statutes |
| Common Mistakes | Costly errors expediters/contractors make |
| Legislative Update | HB 683 and other recent FL legislation |
| Private Provider | Tactical guidance on private provider workflows |
| Contractor Advice | What your permit expediter wishes you knew |
Every post includes:
- Hero image — wide banner at top of post, returned in both list and post APIs as
heroImage: { url, alt } - 2 inline images — embedded in the markdown content at logical section breaks, also returned as
inlineImages[]
Images are auto-selected from a curated Unsplash library based on the post's category, tags, and Claude-provided imageHints.
- List:
GET /→{ posts[], total, limit, offset, hasMore } - Post:
GET /slug→ fullBlogPostobject
| Key | Description |
|---|---|
ANTHROPIC_API_KEY | Claude API key for blog generation |
To trigger a post outside the normal schedule:
import { blob } from "https://esm.town/v/std/blob";
await blob.set("blog-force-post", "1");
Then wait for the next cron tick, or manually run generate.ts.