Seamless blogging with GitHub issues.
- Open GitHub issues in your repo to write posts
- Drag-and-drop or paste images in the issue to upload them to GitHub
- Blog post automatically rebuilds when its issue is created or updated
- Manually manage post publishing by applying labels:
state: draft,state: unlisted,state: published - Set metadata via Frontmatter
- Add a link to a page in the header or footer
- Full-text client-side search (Pagefind) — titles, content, tags, and descriptions
- Click "Use this template" → create a new repository
- Naming the repo
<your-username>.github.iohosts it athttps://<your-username>.github.io, naming it<your-repo>hosts it athttps://<your-username>.github.io/<your-repo> - In repo Settings → Pages set Source to GitHub Actions.
- Create or edit an issue to trigger the first build.
The labels are provided by the "Blog post" issue template. Or create them with the gh CLI:
gh label create "status: draft" --color "d73a4a" --description "Not published"
gh label create "status: unlisted" --color "5319e7" --description "Published but hidden from list"
gh label create "status: published" --color "0e8a16" --description "Published and listed"| Label | Listed on homepage? | Reachable by direct URL? | Best for |
|---|---|---|---|
status: draft |
No | No | Work in progress, only visible on GitHub |
status: unlisted |
No | Yes | Private links / previews / pages |
status: published |
Yes | Yes | Public posts |
Note
Publishing is only controlled by the status labels which are limited to contributors by default, but if you want to prevent random people from opening issues in your blog repo - limit issue creation to collaborators only in Settings → General.
Tip
When you're done writing and editing a post you can close its respective issue.
- Issues → New issue.
- Choose the "Blog post" template or start blank.
- Title = post title. Body = Markdown (drag and drop images straight in, or link external images by URL).
- Apply
status: published(orunlisted). - Site updates in ~1 minute.
The template documents Frontmatter options.
# Draft
gh issue create --title "Hello" --body '---
title: Hello
date: 2026-07-10
description: Short blurb.
author: Name
tags: tech
---
Content here.'
# Publish it
gh issue edit 42 --add-label "status: published"
# Update content
gh issue edit 42 --body 'New markdown...'Re-applying the status label (or just editing a published/unlisted issue) triggers a rebuild.
Warning
You cannot upload images through the gh CLI.
git clone https://.../your-repo.git
cd your-repo
npm install # or bun, etc.
npm run dev # run the current folder's repo as a blog
GH_REPO=<username>/<repo> npm run dev # run another cmless-based repo as a blogPagefind indexes published posts and navigation pages (title, description, tags, body). Build writes the index into dist/pagefind/ and copies it to gitignored public/pagefind/ so dev can serve it; re-run build (or pagefind if dist/ is current) after content changes. Assets and result links follow Astro’s base for project-site subpaths.
Site title is taken from the GitHub repository description.
Author / avatar / profile: auto from the GitHub issue author (or override with author, authorUrl, authorAvatar).
Series: from series: My Series Name Frontmatter. Posts that share the same series name get previous/next navigation and a list of posts in the series, ordered by date (oldest first).
Tags: from tags: a, b Frontmatter.
Navigation pages: useful for custom pages like "About me". Add navigation: Label and navigationIndex: N to an issue's Frontmatter. Negative indices place the link before "Posts" in the header, non-negative after. Footer links are sorted by index and appear before "powered by". These pages render as plain prose without blog metadata.
Custom domain: Place a CNAME file in public/ and set up DNS.
A GitHub Actions workflow runs scripts/generate.ts (via gh) on issue events and pushes. It turns qualifying issues into Markdown files in src/content/blog/. Astro builds a static site that GitHub Pages serves. Nothing about individual posts is stored in the repository.
Template clones have their own history. To sync:
# first time
git remote add cmless https://github.com/vdsabev/cmless.git
git fetch cmless && git merge cmless/master --allow-unrelated-histories
# thereafter
git fetch cmless && git merge cmless/masterFor conflicts on engine files: git checkout --theirs <file>.
You can replace cmless's template-oriented README in your own blog repo. To protect it from merges, add this to .gitattributes:
README.md merge=ours
MIT — use it for anything.
Made for people who want to write in GitHub Issues and have a fast, reliable blog.