- Content as code: content is code, and posts are Markdown files in your repository.
- Publishing as push: publishing is pushing, directly through your existing Git/CI/CD workflow.
- Cloneable and forkable: cloneable and forkable, the whole blogging system is portable and reproducible.
- No platform lock-in: no platform lock-in and no dependency on third-party image hosting.
- SEO optimized by default: powered by SSG static generation, with built-in
metadata,sitemap.xml, androbots.txtfor search-engine friendliness. - Visual admin panel: includes an admin system with Markdown block-level editing, so non-technical users can also write and manage content easily.
- Admin preview: https://blogit-admin.pages.dev
- Admin password:
blogit123456 - Blog preview: https://blogit-blog.2437951611.workers.dev
Click Use this template to create your own repository (Public) from the Blogit template, then clone it locally and install dependencies.
2. Update repo config in config.ts
Change it to the owner and repo name of the repository created in step 1.
Create a GitHub fine-grained personal access token with Contents: Read and write permission for the current repository only, then copy .dev.vars.example to .dev.vars:
Then update values in .dev.vars:
ADMIN_PAT=<your_github_pat>ADMIN_PASSWORD_HASH=<sha256_of_password>(you can generate it at https://emn178.github.io/online-tools/sha256.html)
Giscus env vars are optional. If not configured, the comment area will not be shown.
Enable Discussions in your repo and install Giscus App, then visit giscus.app to generate parameters and write them into .env:
NEXT_PUBLIC_GISCUS_REPONEXT_PUBLIC_GISCUS_REPO_IDNEXT_PUBLIC_GISCUS_CATEGORYNEXT_PUBLIC_GISCUS_CATEGORY_ID
Log in to Cloudflare, create an Account API Token (the Edit Cloudflare Workers template is enough with proper permissions), and get your Account ID.
In your repository at Settings -> Secrets and variables -> Actions -> Repository secrets, configure:
CLOUDFLARE_API_TOKENCLOUDFLARE_ACCOUNT_IDADMIN_PATADMIN_PASSWORD_HASH
- Commit all changes to GitHub.
- After GitHub workflows finish, you can find deployment URLs for
blogit-adminandblogit-blogunderCloudflare Workers and Pages. - Get the deployment URL of
blogit-blogand updateNEXT_PUBLIC_SITE_URLin .env.
Blogit is a pnpm monorepo Markdown blog system with two applications:
apps/blog: reader-facing blog site, built with Next.js 16 (App Router), deployed to Cloudflare via OpenNext.apps/admin: admin application for visual blog CRUD operations. Built with React + Vite + Cloudflare Pages Functions, and writes content directly via GitHub Git Data API.
- Post source of truth:
apps/blog/posts/<slug>/index.md - Post assets:
apps/blog/posts/<slug>/assets/* - Post index cache: apps/blog/posts/_index.json
- Blog capabilities:
- SSG static pages
- SEO (
sitemap.xml,robots.txt, metadata) - Giscus comments
- KaTeX math, Shiki code highlighting, code copy, image preview enhancement
- Tag system
- Admin capabilities:
- Password login
- Post list / create / edit / delete
- Atomic commit (content + images +
_index.jsonin one commit)
├── apps/
│ ├── blog/ # Public blog
│ │ ├── app/
│ │ ├── posts/ # Markdown source of truth
│ │ ├── public/
│ │ └── scripts/
│ └── admin/ # Admin
├── package.json
└── pnpm-workspace.yaml
pnpm installpnpm devpnpm dev:blog
pnpm dev:adminCommon local URLs:
- Blog:
http://localhost:3000 - Admin: printed by Wrangler Pages dev (
http://localhost:8788)
apps/blog/posts/<slug>/
├── index.md
└── assets/
Frontmatter example:
---
title: "Post title"
date: "2026-03-05"
pinned: true # optional, pinned posts are sorted before regular posts
cover: "assets/cover.webp"
tags:
- nextjs
- cloudflare
source: "https://example.com/original-link" # optional external link post
---- title (required): post title
- date (required): publish date, format: YYYY-MM-DD
- pinned (optional): whether the post is pinned. Pinned posts are sorted before non-pinned posts; when multiple posts are pinned, they are still ordered by date descending.
- cover (optional): cover image path; supports relative paths (for example,
assets/cover.jpg) or external URLs (for example,https://example.com/image.jpg). If missing, the system tries the first image in markdown; if none exists, it falls back to/default-cover.png. - source (optional): external article URL. If set, clicking the card in the list redirects directly to this external URL (opens in a new tab), instead of the internal post detail page. The source link is also shown at the bottom of the post detail page. Useful for third-party content references.
- tags (optional): post tags. Supports YAML array format, for example
tags: ["nextjs", "react"]or multiline list format.
Important: The site does not fetch content from external links automatically. The blog list still needs summary/description text, so you must provide content in the
index.mdbody manually. The system extracts text from markdown and generates the card description.
pnpm --filter blog run generate-index: rebuildposts/_index.json(automatically handled by workflow, no manual local run needed)pnpm --filter blog run sync-assets: syncposts/*/assetstopublic/blog-assets(automatically handled by workflow, no manual local run needed)
MIT License, see LICENSE.