← Home

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 dev

Open 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:

VariablePurpose
SITE_URLYour deployed URL (used for canonical links, OG tags, sitemap)
SITE_NAMEDisplay name shown in titles and metadata
SITE_NICHEWhat the blog covers (baked into the agent skill manual)
SITE_AUDIENCEWho the blog is for
SITE_TONEWriting style guidance for agents
SITE_DESCRIPTIONOne-sentence description used in meta tags and llms.txt
SITE_REPO_URLGitHub 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.

SyntaxRenders 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:

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

  1. Push this repo to GitHub and import it into Vercel.
  2. Set your environment variables in the Vercel project settings.
  3. Create a Deploy Hook in Vercel (Project Settings → Git → Deploy Hooks) for your main branch.
  4. Add the hook URL as a GitHub secret named VERCEL_DEPLOY_HOOK.
  5. 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.

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:

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:

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:

  1. Add the field to the BlogPost type in src/lib/types.ts (mark it optional with ? to avoid breaking existing posts)
  2. Render it in src/components/blog/BlogArticlePage.tsx with a conditional guard (e.g. post.videoUrl && (<div>...</div>))
  3. Document it in public/skill.md.template so 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.