---
status: "published"
title: "cmless: Seamless Blogging with GitHub Issues"
date: "2026-07-23"
description: "I've tried to maintain a blog for years, but every tool had too much friction. So I made cmless: turn a GitHub repo into a blog with Issues as the CMS and GitHub Pages for hosting."
image: "https://github.com/user-attachments/assets/81a6f404-3e6d-4df4-b19d-390c87658744"
imageAlt: "cmless - seamless blogging with GitHub issues"
author: "Vlad Sabev"
authorUrl: "https://github.com/vdsabev"
authorAvatar: "https://github.com/vdsabev.png"
tags: ["blogging","software development"]
navigationIndex: 0
---

## 🤔 Too Much Friction

I've been trying to maintain a blog for years but anything I used had too much friction.

WordPress is overkill. Medium is not your own. Ghost needs hosting. Markdown-in-a-repo means you have to make a commit just to fix a typo.

I wanted something simpler - write where I already work, keep assets out of the code tree, and publish without managing a server.

So I made [cmless](https://github.com/vdsabev/cmless) - a tool to seamlessly turn a GitHub repo into a blog.

## ℹ️ What It Does

cmless takes your repo's GitHub issues, turns them into blog posts, and hosts them on GitHub Pages.

- Open issues to write posts
- Drag-and-drop or paste images (GitHub hosts them on its CDN)
- Embed YouTube, X posts, Instagram, CodePen, and GitHub gists with a simple `![](...)` image-style link
- Optional Frontmatter for date, description, tags, and more
- Publish with labels: `status: draft`, `status: unlisted`, `status: published`
- Updating an issue rebuilds the blog

This post is part of that workflow. If you're reading it on my website - it started as [issue #24](https://github.com/vdsabev/vdsabev.github.io/issues/24).

**Code stays code. Posts stay posts.** Since content lives in Issues, the repo doesn't bloat with every draft and image, and you can write from any device with a browser and a GitHub login.

## 🛠️ How It Works

### 1. Write the post as a GitHub issue

Title = post title. Body = Markdown. Drag and drop images into the issue.

Embeds use the same syntax as images - put the URL in `![](...)`:

<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%"><iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" title="" style="position:absolute;top:0;left:0;width:100%;height:100%" frameborder="0" allowfullscreen></iframe></div>

### 2. Add Frontmatter at the top

```yaml
---
date: "2026-07-10"
description: "Short summary for SEO."
tags: tech, dev
---
```

Author, avatar, and profile link default to the GitHub user who opened the issue. You can override those, plus title, slug, OG image, or place a link to the page in the header or footer if you need to.

### 3. Apply a status label

| Label | Listed on homepage? | Reachable by URL? | Best for |
| --- | --- | --- | --- |
| `status: draft` | No | No | Work in progress, only on GitHub |
| `status: unlisted` | No | Yes | Previews, private links, pages like About me |
| `status: published` | Yes | Yes | Public posts |

`status: published` adds the post to your list. `status: unlisted` publishes it without listing it - handy for custom pages like About me.

When you're done editing you can close the issue - or keep it open - the published status of the post is only based on the label.

### 4. Update the issue to rebuild

Create or edit an issue (or change a status label) and the site rebuilds. Contributors control the labels so open issues by random people don't get put on your homepage. But if you want to make sure there's no noise you can limit issue creation to collaborators.

## ⚙️ Under the Hood

A GitHub Actions workflow runs on issue events and pushes, using the `gh` GitHub CLI tool and a small generator (`scripts/generate.ts`) to turn labeled issues into Markdown for the build. Astro produces a static site, and GitHub Pages serves it.

The repo holds the theme, pipeline, and config - the issues hold the writing. If you use the [cmless template](https://github.com/vdsabev/cmless) to make your own repo you can still sync engine updates later by adding an upstream remote and merging it into your repo.

## 🚀 Getting Started

Click **Use this template** on [vdsabev/cmless](https://github.com/vdsabev/cmless) and start creating issues - it's that simple.

1. Create a repo from the template
   - `<username>.github.io` → `https://<username>.github.io`
   - any other name → `https://<username>.github.io/<repo>`
2. In **Settings → Pages**, set **Source** to **GitHub Actions**
3. Create or edit an issue to trigger the first build

Labels come from the Blog post issue template, or create them with:

```sh
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"
```

Site title comes from the repository description. And if you want a custom domain you can do that just like with any other static website on GitHub Pages - drop a `CNAME` in `public/` and point DNS to it.

For local preview: clone your repo, install, `npm run dev` (or Bun, etc.). Point `GH_REPO` at any cmless-based repo if you want to develop against live issues without publishing from your laptop.

You can also have an AI agent use the `gh` GitHub CLI to manage posts (except uploading assets - paste those yourself in the web UI, or link to external hosting).

## 🏁 In Conclusion

I don't need another platform optimized for engagement metrics. Just a place to publish my thoughts with tools I already know and understand.

cmless is small on purpose: Issues for writing, labels for status, Actions for rebuilds, Pages for hosting. Simple to reason about, very little maintenance.

Try the template from [github.com/vdsabev/cmless](https://github.com/vdsabev/cmless)

Or see it in action on my blog: [vdsabev.github.io](https://vdsabev.github.io)
