Getting started with ForgeBlog
ForgeBlog is a Next.js blog template with scheduled publishing and a machine-readable agent skill layer. Posts live in a single TypeScript file, are filtered by date at build time, and go live automatically via a daily cron rebuild.
Quick start
git clone https://github.com/coenhewes/forgeblog.git
cd forgeblog
npm install
cp .env.example .env.local # edit with your site details
npm run devOpen http://localhost:3000/blog to see the seed posts.
Configure your site
Edit .env.local with your details. These values are baked into the skill files at build time:
| Variable | Purpose |
|---|---|
SITE_URL | Your deployed URL (used for canonical links, OG tags, sitemap) |
SITE_NAME | Display name shown in titles and metadata |
SITE_NICHE | What the blog covers (baked into the agent skill manual) |
SITE_AUDIENCE | Who the blog is for |
SITE_TONE | Writing style guidance for agents |
SITE_DESCRIPTION | One-sentence description used in meta tags and llms.txt |
SITE_REPO_URL | GitHub repo URL (used in landing page and install scripts) |
How scheduled publishing works
Every post in src/content/blogPosts.ts has a publishedAtdate. At build time, posts whose date is in the future are filtered out. They exist in the code but don't appear on the site.
A GitHub Actions workflow runs daily (06:00 UTC) and hits a Vercel Deploy Hook, triggering a fresh build. When the build runs, the date filter re-evaluates, and any post whose date has now passed goes live.
Write ten posts today with dates spread over the next ten weeks, commit them all at once, and they appear one by one. No manual publishing step.
Adding a post
Open src/content/blogPosts.ts and add an object to the array. Every post must conform to the BlogPost type defined in src/lib/types.ts.
Minimal example:
{
slug: "my-new-post",
title: "My New Post",
description: "A short meta description.",
excerpt: "A one-liner for the blog list.",
heroImage: "https://images.unsplash.com/photo-example?w=1200&h=630&fit=crop",
heroImageAlt: "Description of the image",
publishedAt: "2025-06-01T06:00:00Z",
updatedAt: "2025-06-01T06:00:00Z",
readingTime: "5 min read",
tags: ["topic"],
keywords: ["keyword one", "keyword two"],
primaryKeyword: "keyword one",
author: { name: "Your Name" },
sections: [
{
heading: "Section title",
paragraphs: [
"Paragraphs support **bold**, *italic*, `inline code`, and [links](https://example.com).",
"Each string in the array is rendered as a separate paragraph."
],
},
],
}Optional fields like stats, checklist, takeaways, and faqs add extra sections to the article. FAQs also generate FAQPage structured data for Google rich results.
Inline formatting
All text fields in the article body support inline markdown formatting. This includes paragraphs, highlights, bullets, checklist items, takeaways, and FAQ answers.
| Syntax | Renders as |
|---|---|
**bold text** | bold text |
*italic text* | italic text |
`code` | code |
[text](url) | External link (opens in new tab) |
Do not use inline formatting in title, description, excerpt, heading, or heroImageAlt — these are used in meta tags and heading elements where markdown syntax would render as literal characters. Internal links are handled automatically by the linking engine; use [text](url) only for external URLs.
The agent skill layer
ForgeBlog ships three machine-readable files that let any LLM agent understand and contribute to the blog:
- /skill.md — The full instruction manual. Covers the post schema, quality rules, SEO requirements, and a step-by-step workflow.
- /skill.json — Live blog state: every published and scheduled post, tag frequencies, internal linking rules. Regenerated on every build.
- /install.md — Install instructions for agents setting up ForgeBlog in a new or existing project.
To use it, paste this into any agent:
Read https://forgeblog-mauve.vercel.app/skill.md and write me a blog post about [your topic]The agent fetches the manual, fetches skill.json to check existing content, drafts a post matching the schema, and appends it to blogPosts.ts.
Deploy to Vercel
- Push this repo to GitHub and import it into Vercel.
- Set your environment variables in the Vercel project settings.
- Create a Deploy Hook in Vercel (Project Settings → Git → Deploy Hooks) for your
mainbranch. - Add the hook URL as a GitHub secret named
VERCEL_DEPLOY_HOOK. - The included GitHub Actions workflow triggers a rebuild daily at 06:00 UTC.
Customising
Internal links
Edit src/content/internalLinks.ts to add regex-based rules that automatically convert matching text in your posts into internal links. The file includes documentation and examples.
Related callouts
Edit src/content/relatedLinks.ts to map post slugs to callout cards that appear at the bottom of articles.
Styling
Tailwind CSS with Geist fonts and a neutral palette. Dark mode is automatic via prefers-color-scheme.
- Theme colours:
src/app/globals.css - Custom fonts: import in
layout.tsxvianext/font/google - Prose styling:
@tailwindcss/typography
Skill manual
Edit public/skill.md.template to change agent instructions. The {{TOKEN}} placeholders are resolved at build time from your env vars. Add site-specific rules, prohibited topics, or house style notes.
Post format
The renderer (src/components/blog/BlogArticlePage.tsx) only requires sections for the article body. Every other block — stats, checklist, takeaways, and faqs — is conditional and renders nothing when omitted. A minimal essay-style post needs only the required fields plus a sections array:
{
slug: "my-essay",
title: "An Essay-Style Post",
description: "A short meta description.",
excerpt: "Preview text for the blog list.",
heroImage: "https://images.unsplash.com/photo-example?w=1200&h=630&fit=crop",
heroImageAlt: "Description of the image",
publishedAt: "2026-06-01T06:00:00Z",
updatedAt: "2026-06-01T06:00:00Z",
readingTime: "4 min read",
tags: ["essays"],
keywords: ["essay writing"],
primaryKeyword: "essay writing",
author: { name: "Your Name" },
sections: [
{
heading: "Opening",
paragraphs: [
"Use **bold** for emphasis and `code` for technical terms.",
"Second paragraph with *italic* and a [link](https://example.com)."
],
},
{ heading: "The argument", paragraphs: ["..."] },
],
// no stats, no checklist, no takeaways, no faqs — the renderer skips them
}To get the full structured layout (stats cards, action checklist, key takeaways, FAQ accordion), include those optional fields. You can mix and match — for example, include faqs but skip stats. Each block is independent. All text in the article body supports inline formatting (**bold**, *italic*, `code`, [text](url)).
Agent quality rules
The skill manual at public/skill.md.templatetells agents to write 5–8 sections, exactly 3 stats cards, 3–6 checklist items, and 4–8 FAQs. These ranges are best-practice guidance for rich, structured posts — the renderer does not enforce them. If you want agents to write looser essay-style or interview posts, edit the “Quality rules” section of the template:
- Change
5 to 8 sectionsto1 to 8 sectionsto allow shorter posts - Remove or soften the
3 stats cards (not 2, not 4)line to make stats truly optional - Update step 5 in the workflow section — it currently says “do not skip optional sections unless there's a reason”, which nudges agents toward the full structure
Renderer layout
The article renderer at src/components/blog/BlogArticlePage.tsx renders sections in this fixed order: header, hero image, share buttons, stats cards, body sections, checklist, takeaways, related callout, FAQ. To reorder, remove, or add sections, edit that component directly. Each block is a self-contained JSX chunk guarded by a conditional check.
Section separation
Body sections are separated by a subtle border and padding (border-t border-zinc-200 pt-6). To customise:
- Change
pt-6topt-4orpt-10for tighter/looser spacing - Swap
border-zinc-200for a different border colour - Remove
border-tentirely for whitespace-only separation
The classes are on the <section> element inside BlogArticlePage.tsx (look for the sectionIdx > 0 conditional).
Extending the post type
To add a new field (e.g. a videoUrl for embedded video), update three files:
- Add the field to the
BlogPosttype insrc/lib/types.ts(mark it optional with?to avoid breaking existing posts) - Render it in
src/components/blog/BlogArticlePage.tsxwith a conditional guard (e.g.post.videoUrl && (<div>...</div>)) - Document it in
public/skill.md.templateso agents know the field exists and when to use it
Pagination
The blog list shows 9 posts per page. To change this, edit the POSTS_PER_PAGE constant at the top of src/components/blog/BlogListPage.tsx.
Structured data
JSON-LD schemas are built in src/components/StructuredData.tsx. It exports helpers for BlogPosting, FAQPage, BreadcrumbList, Organization, and CollectionPage schemas. FAQPage is only generated when a post includes faqs. To modify the schemas (e.g. add a VideoObject for a new video field), edit the relevant builder function in that file.
Project structure
src/
├── app/
│ ├── layout.tsx # root layout + Organization JSON-LD
│ ├── page.tsx # landing page
│ ├── blog/
│ │ ├── page.tsx # paginated blog list
│ │ └── [slug]/
│ │ └── page.tsx # individual post page
│ ├── docs/
│ │ └── page.tsx # this page
│ ├── sitemap.ts # dynamic sitemap
│ └── robots.ts # robots.txt
├── components/
│ ├── blog/ # article renderer, list, FAQ, linking
│ ├── StructuredData.tsx # JSON-LD schema builders
│ └── CopyCommand.tsx # clipboard UI
├── content/
│ ├── blogPosts.ts # all posts (the single source of truth)
│ ├── internalLinks.ts # regex → URL linking rules
│ └── relatedLinks.ts # slug → callout mapping
├── lib/
│ ├── blog.ts # query helpers (published, by slug, related)
│ ├── types.ts # BlogPost type definition
│ └── site.ts # site config from env vars
scripts/
└── generate-skill-json.ts # prebuild: skill.json + template resolution
public/
├── skill.md.template # agent manual (source)
├── llms.txt.template # agent pointer (source)
├── install.md.template # install manual (source)
└── install.sh.template # curl install script (source)Full source and contribution guide on GitHub.