Skip to content

Repository files navigation

jekyll-starter

A working Indiekit + Jekyll site. Click Use this template, and you have a site that publishes posts and serves them at the URLs Indiekit reports.

Quick start

bundle install
npm install
cp .env.example .env      # set SECRET to a long random string
npm start                 # Indiekit on :3000

Visit http://localhost:3000/auth/new-password, set a password, copy the generated value into PASSWORD_SECRET, and restart. Then in a second terminal:

npm run build && npm run serve   # your site on :8080

Publish a post in Indiekit, then re-run npm run build and open the URL it gave you. Nothing watches for you: npm run build is one-shot, so a freshly published post 404s until you rebuild.

When you deploy, point indiekit: in _config.yml at where Indiekit actually runs. The authorization_endpoint, token_endpoint and micropub <link rel> tags are built from it, and the default is http://localhost:3000 — which on a public site points every visitor at their own machine, so IndieAuth works for nobody. Jekyll has no environment-variable interpolation in _config.yml, so there are two ways to set it: edit _config.yml directly (simplest for one deployment), or layer a second config file at build time:

# _config.production.yml
indiekit: https://indiekit.example.com
bundle exec jekyll build --config _config.yml,_config.production.yml

URLs, and the two lines that matter

Default — dateless. indiekit.config.js sets path and url to the same shape for every post type, so _notes/hello.md is reported and served as /notes/hello. Change the shape there and both halves stay in step.

Every collection in _config.yml needs both:

notes: { output: true, permalink: /notes/:name/ }

output: true makes Jekyll render the collection at all — without it your posts are written to disk and never become pages. The trailing-slash permalink makes Jekyll write notes/hello/index.html instead of notes/hello.html; without it the URL Indiekit reports works on GitHub Pages and Netlify, which fall back to .html, and 404s on a plain static host. If you add a post type, add its collection here too.

Dated URLs — opt-in. If you want /notes/2026/08/22/hello, leave the file paths alone and change two things. First the advertised URL in indiekit.config.js:

url: `${collection}/{yyyy}/{MM}/{dd}/{slug}`,   // path stays _${collection}/{slug}.md

then the collection's permalink in _config.yml:

notes: { output: true, permalink: /notes/:year/:month/:day/:title/ }

One entry per collection. Jekyll takes the date from each post's front matter, which preset-jekyll already writes as date, so the filenames stay dateless and nothing needs renaming.

Verified against Jekyll 4.4.1: _notes/hello.md with a date builds to _site/notes/2026/08/24/hello/index.html.

article needs one extra line. Unlike the other five it can also live in Jekyll's built-in _posts collection, and stock Jekyll renders that to a flat /2026/08/23/hello.html — an extension the preset does not advertise. If you move articles there, set the site-wide permalink too:

permalink: /:year/:month/:day/:title/

See indiekit#902 for why the preset's own defaults do not resolve on their own.

How the Jekyll starter differs

The starters are the same idea per generator, but Jekyll is not Eleventy or Hugo and this one does not pretend otherwise. What is genuinely different here:

Every collection needs both output: true and a trailing-slash permalink. Hugo and Eleventy write directory-style output (slug/index.html) natively; Jekyll defaults to a flat slug.html per collection entry unless the permalink says otherwise.

The store is the repository root, with _-prefixed collection directories (_notes/, _articles/, …) and media/ beside them — Jekyll's own convention for collections. The Eleventy sibling points its store at content/; the Hugo sibling uses the root too but splits content/ and static/.

Front matter arrives snake_cased. preset-jekyll runs snakecase-keys deeply, so Micropub's bookmark-of is page.bookmark_of in Liquid, and in-reply-to is page.in_reply_to; name becomes title and published becomes date. The Hugo sibling gets camelCase (.Params.bookmarkOf).

photo is a sequence of mappings, not strings. It is written as photo: [{ url: /media/photos/pixel.gif }], so {{ image }} alone renders a Ruby hash dump ({"url"=>...}) rather than a URL — the layout has to read image.url.

jekyll serve hides the permalink requirement above. WEBrick resolves extension-less paths, and so does http-server (which this repo's npm run serve uses) — its .html fallback is on by default and cannot be turned off. That is why test/all-types.mjs checks the built output on disk rather than trusting an HTTP status code to catch a missing permalink.

Ruby, not Node, builds the site. bundle install is a prerequisite the Node-only Eleventy and Hugo siblings do not have, and a build must run jekyll clean first or deleted posts keep being served — Jekyll does not prune its own destination on a plain build.

Stock Jekyll generates nothing extra. With only gem "jekyll" and no theme there is no feed and no tag or category pages — those come from jekyll-feed and from minima. The Hugo sibling emits index.xml, tags/ and categories/ natively and keeps them.

Dated URLs need a permalink per collection, and one more for article. All three starters can do dated URLs; Jekyll is the only one where a post type (article) can also land in a built-in collection (_posts) that needs its own site-wide permalink. See the URLs section.

What this is not

No feeds, pagination, category or tag pages, 404 page or theming, and deliberately no Jekyll theme — the layout in _layouts/ is the point. It is a floor to build on. Add what you need.

Adding a post type

Adding an entry to publication.postTypes in indiekit.config.js is not enough on its own. Without the matching post-type plugin also listed in plugins, Indiekit silently drops the type — no error, it just does not exist (lib/post-types.js only keeps types that have a properties object, which only a post-type plugin supplies).

For Jekyll, add the second half too: the collection in _config.yml, with both output: true and a trailing-slash permalink — without it, the new type's posts are written to disk and never rendered.

Tests

npm run smoke        # publish → resolve → marker → index → delete → 404
npm run test:types   # all six default post types, plus media upload

Both run against built output, not jekyll serve — WEBrick resolves extension-less paths, so the dev server hides a missing permalink that breaks on a real static host.

Do not assume the static server is what catches that, though: http-server, which npm run serve uses, has its own .html fallback on by default, so it answers /notes/hello with 200 whether Jekyll wrote notes/hello/index.html or a flat notes/hello.html. No HTTP assertion here can tell those apart. What actually catches a missing permalink is test/all-types.mjs, which inspects the built files on disk and fails if any collection rendered flat.

MongoDB is optional, with one exception

Everything above works with no database at all. Without MongoDB, npm run smoke prints skip deleted post returns 404 — requires MongoDB and reports 5/5 assertions passed, 1 skipped — that skip is expected, not a broken template. Deleting a published post through Indiekit's Micropub delete action needs a database to track it (indiekit#904).

To run it, point Indiekit at a database and set the same MONGO_URL for the tests — that variable is what test/smoke.mjs branches on. With it set, npm run smoke reports 6/6 assertions passed.

If you need to remove a post, delete its Markdown file under the _-prefixed collection directory and rebuild — npm run build runs jekyll clean first, so the stale page is always removed, database or not.

About

Indiekit + Jekyll starter: publish a post and it resolves at the URL Indiekit reports

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages