Simple blog generator using your Git index.
Inkcairn turns a Git repository of Markdown files into a complete static blog. It provides a built-in layout, pages, categories, syntax highlighting, search, feeds, and live preview, then writes plain static files that can be hosted anywhere. Publication and update dates come from file history by default, so authors normally write no date metadata.
Checkout https://inkcairn.so1ve.dev.
Create and preview a site:
inkcairn init my-blog
inkcairn dev my-blogFor a new site, init also creates the Git repository and its first commit.
Open the address printed by Inkcairn and start editing files in my-blog/. The
preview updates when the site changes.
When the site is ready to publish:
inkcairn build my-blogUpload the contents of my-blog/dist/ to any static hosting service.
Edit inkcairn.md:
---
url: https://example.com
language: en
---
# My Blog
Notes on software and writing.The first # heading is the site title, and the introductory text is its
description. Only the title is required. language defaults to en. Set url
when publishing the site so links, the RSS feed, and the sitemap use the public
address.
Inkcairn can render giscus discussions into posts and pages at build time, so
readers can see existing comments without downloading the giscus client.
Configure the repository and category in inkcairn.md using the values
generated by https://giscus.app:
---
comments:
repo: owner/repository
repo_id: R_kg...
category: Announcements
category_id: DIC_kw...
---The repository must be public, have GitHub Discussions enabled, and have the
giscus App installed. Copy all four values from the giscus configuration
script. Inkcairn uses each post URL, such as posts/hello.html, as the giscus
specific term. Pages use their output URL, such as about.html, in the same way.
Set GITHUB_TOKEN while building to include the static comment snapshot. The
token needs read access to Discussions. Without a token, the site still provides a
button that loads the giscus client on demand.
For an automatically refreshed snapshot, rebuild the site when a discussion or discussion comment changes:
on:
discussion:
types: [created, edited, deleted]
discussion_comment:
types: [created, edited, deleted]
permissions:
contents: read
discussions: read
jobs:
deploy:
steps:
- run: inkcairn build .
env:
GITHUB_TOKEN: ${{ github.token }}Add Markdown files to posts/:
# Hello
A short introduction to the post.
## First section
Write the rest of the post here.The first # heading is the title. The introductory content is shown in post
lists.
Do not normally add a date to a post. For a committed file, Inkcairn uses its
first Git commit as the publication date and its latest commit as the update
date. If the file is dirty, its filesystem modification date becomes the update
date. Untracked and non-Git files use filesystem creation and modification
dates during preview or an --allow-dirty build.
Use published only when importing an older post whose original publication
date is not represented by its Git history:
---
published: 2020-04-12
---
# An older postThe value accepts YYYY-MM-DD or RFC 3339. A date without a time means
midnight UTC. The update time still follows the latest Git or filesystem change.
Keep the reposted Markdown under posts/ and identify its original source in
frontmatter:
---
repost:
url: https://example.com/original-post
title: The original article
author: Alice
published: 2024-05-10T14:20:00+08:00 # you can omit `T..`
---
# Reposted articleauthor is required. title, url, and the original published date are
optional. Omit the URL when the original article has no online location. The
post's top-level published date remains the date it was published on this site
and controls post ordering.
Reposts display a visible source notice and remain available in post lists,
search, and RSS. They use noindex and are omitted from the sitemap. When a
source URL is present, it becomes the canonical URL. Without one, the local
page remains canonical but is still excluded from search indexing.
Source attribution does not replace any permission or license required to
republish the article.
The filename becomes the URL and can also control the listing order:
posts/hello.md -> /posts/hello.html
posts/00-welcome.md -> /posts/welcome.html
posts/2026-08-27-notes.md -> /posts/notes.html
posts/2026-08-27-01-follow-up.md -> /posts/follow-up.html
posts/notes/hello.md -> /posts/notes/hello.html
Use 00- through 09- to pin posts; pinned posts appear first in numeric
order. Other posts appear by publication date, newest first. A date prefix is
optional and must match the post's publication date. For posts published on the
same day, add 00- through 99- after the date to order them; numbered posts
appear before unnumbered posts from that day. These ordering prefixes are
omitted from the URL and do not replace the Git, filesystem, or published
date.
Nested directories create post categories. Use __ in a category directory
name where its displayed name should contain a space.
Append .draft to keep a post out of normal builds:
posts/next-post.draft.md
Add Markdown files to pages/. Pages use the same format as posts and are
generated at the site root:
pages/01-about.md -> /about.html
pages/02-projects.md -> /projects.html
The numeric prefixes also set the navigation order.
Fenced code blocks support syntax highlighting:
```rust
fn main() {
println!("Hello");
}
```Add diff after the language to highlight changed lines:
```rust,diff
-let published = false;
+let published = true;
```Footnotes and callouts are also available:
This sentence has a note.[^note]
[^note]: Footnote text.
> [!TIP]
> Callouts can be Note, Tip, Important, Warning, or Caution.Render a responsive list of friend links with a friends block:
```friends
- name: John Doe
url: https://john.example.com
description: A short description
avatar: /assets/john.png
- name: Jane Smith
url: https://jane.example.com
```name and url are required. description and avatar are optional. Without
an avatar, the card displays the first character of its name. Entries retain
their order in the Markdown file.
Render device cards with a devices block:
```devices
- name: OnePlus 13
description: A short description
image: /assets/oneplus-13.png
specs:
- 24GB RAM
- 1TB storage
- name: OnePlus Pad Pro
specs:
- 8GB RAM
- 256GB storage
```name is required. description, image, and specs are optional. Cards are
shown two per row and collapse to one per row on narrow screens.
Place images and other static files in assets/. Reference them from Markdown
with paths such as /assets/photo.jpg.
Optional snippets can add shared content:
snippets/head.html content added to every page's <head>
snippets/home.md content shown above the home-page post list
snippets/after-content.md content shown after every post and page
Each snippet may use either .md or .html, but not both.
Preview on an automatically selected available port:
inkcairn devUse a specific port:
inkcairn dev --port 8080Create a production build in dist/:
inkcairn buildA normal build requires a clean Git worktree. To build uncommitted, untracked, or non-Git content:
inkcairn build --allow-dirtyTo include files ending in .draft.md:
inkcairn build --include-draftsTheme is based on Cactus
MIT. Made with ❤️ by Ray