ainame/tuzuru
Tuzuru (綴る) is a dead simple blogging tool. Write a plain md and store it in Git. That's it.
Installation
npm (Cross-platform)
npm install -g @ainame/tuzuruThis will download and install the appropriate prebuilt binary for your platform (macOS or Linux).
Homebrew (macOS)
brew tap ainame/tuzuru https://github.com/ainame/Tuzuru
brew install tuzuruManual Build
swift build -c release
cp .build/release/tuzuru /path/to/binGetting started
You first need to set up a Git repo locally.
mkdir new-blog
cd new-blog
git init
# Initialize a blog project
# This adds `assets`, `contents`, `templates` directories and `tuzuru.json`
tuzuru init
git add .
git commit -m "init commit"Then create a markdown file under the contents directory and do git commit.
emacs contents/first-blog-post.md
git add contents/first-blog-post.md
git commit -m "First post"When you make git commit becomes your post's published date. Specifically, the first commit's Author Date for a markdown file under contents is the published date and also, author name will be taken from Git's config.
Now it's time to build your blog.
tuzuru generateYou can now see the blog directory that can be deployed to GitHub Pages or your favorite HTTP server.
For local development, use the built-in preview command:
tuzuru previewThis starts a local HTTP server at http://localhost:8000 with auto-regeneration enabled. When you modify source files, the blog will be automatically rebuilt on the next request.
Deployment
This repo has two GitHub Actions prepared for Tuzuru blogs to set up deployment easily.
* Install tuzuru via npm, generate blog, upload the artefact, and deploy to GitHub page
Only install tuzuru via npm and generate blog You can deploy to anywhere you like
Their versions should match the CLI’s version. When you update the CLI version, you should also update the action’s version. It is recommended to use Renovate or Dependabot to keep it up to date.
Note that sicne Tuzuru relies on Git history, you have to checkout git repo with the entire history. Specify fetch-deploy: 0 in actions/checkout
This is an exmaple .github/workflows/deploy.yml.
<details>
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: ainame/Tuzuru/.github/actions/tuzuru-deploy@0.1.2</details>
Static Files (public directory)
If you need to include static files that should be copied as-is to your deployed site (e.g., Google Search Console verification HTML, robots.txt, or other verification files), create a public/ directory in your project root:
my-blog/
├── public/
│ ├── google1234567890abcdef.html # → /google1234567890abcdef.html
│ └── robots.txt # → /robots.txt
├── contents/
├── templates/
└── ...The tuzuru-deploy action will automatically copy all files from public/ to the output directory before deployment. This directory is optional—if it doesn't exist, the step is skipped.
Built-in layout
The built-in layout is a great starting point and is easy to customize. It already includes github-markdown-css and highlight.js to make writing tech blog posts a breeze.
[screenshot]
Demo
You can see Tuzuru in action with this demo blog hosted on GitHub Pages:
- Live Demo: https://ainame.tokyo/tuzuru-demo/
- Source Repository: https://github.com/ainame/tuzuru-demo
This demo showcases the built-in layout.
How it works
This is how a tuzuru project look like.
my-blog/
├── contents/
│ ├── hello-world.md # → /hello-world
│ ├── tech/
│ │ └── swift-tips.md # → /tech/swift-tips (listed on /tech)
│ └── unlisted/
│ └── about.md # → /about (not listed anywhere. You can link to /about from layout.mustache manually)
├── templates/
│ ├── layout.mustache
│ ├── post.mustache
│ └── list.mustache
├── assets/
│ └── main.css
└── tuzuru.jsoncontents/- where you put markdown filestemplates/- layout filesassets/- place to locate your assets files, like css or imagestuzuru.json- configuration
Layout and customization
Tuzuru supports two types of pages.
- Post - a blog article
- List - a listing page generated automatically
You can customize these layouts using three Mustache files:
- templates/layout.mustache - Base layout
- templates/post.mustache - main part of post page
- templates/list.mustache - main part of list page
For more on syntax, see the documentation. https://docs.hummingbird.codes/2.0/documentation/hummingbird/mustachesyntax/
Listing and Unlisted Pages
By default, any Markdown file in the contents directory is automatically listed on:
- The home page (
/) based on your configuration. - Yearly archive pages (e.g.,
/2025,/2024). - Category pages for files in subdirectories (e.g.,
contents/tech/swift.mdis listed on/tech).
You can add an unlisted page by placing it in contents/unlisted/. These pages won't be listed anywhere automatically, but you can link to them manually from your templates.
Assets
The tuzuru init command creates an assets directory containing main.css. The tuzuru generate command copies all files from this directory to blog/assets.
To prevent browser cache issues, use the {{buildVersion}} variable in your templates.
<link rel="stylesheet" href="{{assetsUrl}}main.css?{{buildVersion}}">tuzuru.json
tuzuru.json is the main configuration file. By default, you get only metadata section by tuzuru init but here's the rest of customization you can do.
{
// `metadata` is the only mandatory section.
"metadata" : {
"blogName" : "My Blog",
"copyright" : "My Blog",
"description" : "My personal blog", // Meta description for SEO
"baseUrl" : "https://example.com/", // Production URL
"locale" : "en_GB" // Affects the published date format
},
// `output` for configuring output options
"output" : {
"directory" : "blog", // The output directory
"homePageStyle" : "all", // "all", "pastYear", or a number (last X posts)
"routingStyle" : "subdirectory" // "subdirectory" (e.g., /hello-world) or "direct" (e.g., /hello-world.html)
},
// `sourceLayout` to customize the default directory structure (typically not needed)
"sourceLayout" : {
"assets" : "assets",
"contents" : "contents",
"imported" : "contents/imported",
"templates" : {
"layout" : "templates/layout.mustache",
"list" : "templates/list.mustache",
"post" : "templates/post.mustache"
},
"unlisted" : "contents/unlisted"
}
}Import posts from Hugo project
You can import Markdown files from a Hugo project. Tuzuru will parse the YAML front matter to get the title, author, and date, then remove it. Each imported Markdown file will be added as an individual Git commit.
tuzuru import /path/to/import-target-dir # import them to ./contents/imported by default
tuzuru import /path/to/import-target-dir --destination /path/to/importLocal Development Server
Tuzuru includes a built-in HTTP server for local development:
# Basic usage (serves on port 8000)
tuzuru preview
# Custom port
tuzuru preview --port 3000
# Custom directory (default is 'blog')
tuzuru preview --directory my-output
# Custom config file
tuzuru preview --config my-config.jsonAuto-regeneration
The preview command automatically watches for changes in your source files and regenerates the blog when needed:
- Content files: Watches
contents/andcontents/unlisted/directories - Asset files: Watches the
assets/directory - Templates: Watches template files for changes
When files are modified, the blog is regenerated on the next HTTP request, providing a seamless development experience without manual rebuilds.
Build Requirements
- Swift 6.1+
- macOS v15+
Package Metadata
Repository: ainame/tuzuru
Stars: 13
Forks: 0
Open issues: 12
Default branch: main
Primary language: swift
License: MIT
Topics: blog, homebrew, static-site-generator, swift
README: README.md