Static site generator written in Swift. Started as a personal blog tool; v0.3 generalizes it into a publishing tool that can also drive portfolios — multiple content collections, standalone pages, static data files, configurable home page, and themes.
- Content collections. Configure any number of content types in
blog.config.json(posts, projects, notes, …); each gets its own list, detail, and taxonomy routes. - Standalone pages. Drop markdown into
content/pages/for one-off pages like/about/or/now/. - Static data files. YAML/JSON in
data/is exposed to every template — drives data-driven pages like a résumé. - Configurable home. Pull featured + recent items from any collection into the landing page via a
homeconfig block. - Themes. Two themes bundled (
defaultfor blogs,quietfor portfolios + blogs). Stencil templates, project-side files override bundled ones per file. - Multi-language (v0.5). Opt-in i18n with default-at-root URLs (
/posts/foo/) and prefixed translations (/ja/posts/foo/). File-suffix authoring —foo.mdis the default,foo.ja.mdis the translation. Browser-language detection on first visit, language switcher in the top bar,<link rel="alternate" hreflang>for SEO, and graceful fallback so partial translation never hides content. - Authoring CLI.
init,post new,content new,build,serve --watch,check,theme,deploy. - Markdown. GFM (tables, task lists, strikethrough, alerts, fenced code) plus Mermaid blocks; build-time syntax highlighting with Shiki.
- SEO + feeds. Canonical URLs, Open Graph, Twitter cards, sitemap.xml, robots.txt, RSS, search index.
- Static deploy. GitHub Pages workflow generator built in; output is a plain directory you can deploy anywhere.
Homebrew (recommended):
brew tap KristopherGBaker/tap && brew install inkwellMint:
brew install mint
mint install KristopherGBaker/inkwellOr run without installing: mint run KristopherGBaker/inkwell inkwell <subcommand>.
inkwell init
inkwell post new "Hello World"
inkwell serve --watch # rebuilds + live-reloads on save
inkwell build # writes to docs/
inkwell check # validates content + links + assetsserve --watch rebuilds when you edit posts, theme files, blog.config.json, or anything in public/ and static/. The home page links to /archive/; both paginate published posts newest first.
inkwell init
# edit blog.config.json: set theme, add author/nav/home/collections
inkwell content new projects "Wolt Membership"
# add data/experience.yml, data/competencies.yml, data/education.yml
inkwell build
inkwell checkExample blog.config.json for a portfolio:
{
"title": "Kristopher Baker",
"baseURL": "https://krisbaker.com/",
"theme": "quiet",
"outputDir": "docs",
"tagline": "Tokyo · Available for new conversations",
"author": {
"name": "Kristopher Baker",
"role": "Senior Software Engineer",
"location": "Tokyo, Japan",
"social": [{ "label": "GitHub", "url": "https://github.com/KristopherGBaker" }]
},
"nav": [
{ "label": "Work", "route": "/work/" },
{ "label": "Writing", "route": "/posts/" },
{ "label": "Résumé", "route": "/resume/" }
],
"home": {
"template": "landing",
"featuredCollection": "projects",
"featuredCount": 4,
"recentCollection": "posts",
"recentCount": 2
},
"collections": [
{ "id": "posts", "dir": "content/posts", "route": "/posts" },
{
"id": "projects",
"dir": "content/projects",
"route": "/work",
"sortBy": "year",
"taxonomies": ["tags"],
"detailTemplate": "layouts/case-study"
}
]
}For the résumé page, drop a one-liner shell into content/pages/resume.md:
---
title: Résumé
layout: resume
---The resume layout reads from data/experience.yml, data/competencies.yml, and data/education.yml. The portfolio-data agent skill walks Claude Code (or Codex) through importing your existing résumé into those files.
- Collections are content types. Each declared collection has a
dir(where markdown lives), aroute(URL prefix), asortBy(defaultdate; useyearfor projects, etc.), and optionaltaxonomies(tag/category-style facets, scoped to the collection —/work/tags/iOS/, not top-level). - Pages are markdown files in
content/pages/whose route comes from their path (about.md → /about/,now/index.md → /now/). Front-matterlayout: <name>selects the theme template. - Data files are YAML/JSON in
data/, loaded asdata.<basename>in every template's context. Use them for résumé content, link rolls, structured profile data — anything where keeping the content out of markdown is cleaner. - Themes ship with the binary; project-side
themes/<name>/templates/andthemes/<name>/assets/override on a per-file basis. Thedefaulttheme keeps the v0.2 blog look (Tailwind, amber/stone). Thequiettheme is portfolio-friendly (Fraunces / Manrope / JetBrains Mono, generous whitespace, print-friendly résumé). - Backward compatibility. A v0.2
blog.config.jsonwith nocollections/home/author/navkeeps today's URL structure verbatim —/posts/<slug>/,/archive/, top-level/tags/<slug>/, paginated/. Sites without ani18nblock stay monolingual.
Set up languages in blog.config.json:
{
"i18n": { "defaultLanguage": "en", "languages": ["en", "ja"] },
"heroHeadline": "I build *millions* of...",
"footerCta": { "headline": "Quietly open to good work." },
"translations": {
"ja": {
"heroHeadline": "*数百万人*のための...",
"footerCta": { "headline": "良い仕事に静かに開いています。" },
"themeCopy": { "workCardCta": "ケーススタディを読む" },
"home": { "featuredLabel": "選ばれた仕事" },
"nav": [{ "label": "仕事", "route": "/work/" }],
"collections": [{ "id": "posts", "headline": "ある現場のメモ。" }]
}
}
}Authoring conventions:
- Translate a post by adding
foo.ja.mdnext tofoo.md. Sameslugin front matter pairs them. - Translate a page by adding
about.ja.mdnext toabout.md. - Translate a data file by adding
resume.ja.ymlnext toresume.yml. - Co-located static assets (
static/posts/<slug>/cover.mp4) are referenced once by their canonical absolute URL — the renderer rewrites relative paths automatically, so the same asset works from/posts/foo/and/ja/posts/foo/.
Renderer behavior:
- Default language renders at canonical root URLs; non-default languages at
/<lang>/.... /<defaultLang>/<route>/(e.g.,/en/posts/foo/) emits a meta-refresh redirect to the canonical root path so explicit-prefix URLs work as aliases.- Listing pages, the home featured/recent strips, and detail pages always include every item — translated where available, falling back to the default language otherwise.
- The quiet theme renders a language switcher in the top bar (only on pages that have alternate translations) and an inline browser-language redirect script that runs once per visitor (respects
localStorage.langso manual switches stick). <html lang="...">and<link rel="alternate" hreflang="...">are emitted automatically.
| Command | What it does |
|---|---|
inkwell init |
Scaffold a new project in the current directory |
inkwell post new "<title>" |
Create a new draft post in content/posts/ |
inkwell post list |
List posts and their state |
inkwell post publish <slug> |
Flip a post from draft: true to false |
inkwell content new <collection> "<title>" |
Scaffold a new item in any declared collection |
inkwell build |
Build the site to outputDir (default docs/) |
inkwell serve [--watch] |
Local dev server with optional rebuild + live reload |
inkwell check |
Validate front matter, asset paths, links, taxonomy collisions |
inkwell theme use <name> |
Switch the active theme in blog.config.json |
inkwell deploy setup github-pages |
Generate the Pages workflow |
my-site/
├── blog.config.json
├── content/
│ ├── posts/ # blog posts (the legacy collection)
│ ├── projects/ # any other declared collection
│ └── pages/
│ └── about.md # → /about/
├── data/
│ ├── experience.yml # → data.experience in templates
│ └── education.yml
├── public/ # copied verbatim into the output root
├── static/ # alternate copy-verbatim location; static/assets/ is canonical for /assets/...
├── themes/
│ └── quiet/ # only present if you're overriding bundled templates/assets
└── docs/ # build output (gitignore'd or committed for Pages)
Asset references in front matter (coverImage, shots, featuredImage, ogImage, thumbnail) should be /assets/... (resolved from static/assets/ or public/assets/) or fully-qualified https://... URLs. Relative paths like assets/foo.png are rejected by inkwell check.
inkwell init
inkwell deploy setup github-pagesReview baseURL in blog.config.json for your Pages URL before publishing. The setup is optional and does not rewrite existing config.
git clone https://github.com/KristopherGBaker/inkwell.git
cd inkwell
swift run inkwell init /path/to/site
swift run inkwell buildmake brew-strap # install local tooling via Brewfile
make bootstrap-mint # install Mint-managed tools (SwiftLint, etc.)
npm ci # install shiki for syntax highlighting in tests
make verify # lint + testsmake verify defaults to a Mint-managed SwiftLint pin. Override with SWIFTLINT=swiftlint make verify if you have it on PATH.
docs/getting-started.md— extended walkthrough, including v0.3 featuresdocs/roadmap.md— what's shipped vs. deferreddocs/rfcs/— design decisions; v0.3's source of truth isdocs/rfcs/2026-04-30-content-collections-and-templating.mddocs/plans/— TDD-driven implementation plansCLAUDE.md— agent guide for working in this repo