
jekyll — go-ruby-jekyll

A pure-Go (no cgo) jekyll command-line tool whose command and option
surface is a drop-in match for Ruby Jekyll 4.4.1, and
whose build pipeline reproduces a real site's _site/ output — verified
byte-for-byte against the reference implementation.
It builds on the pure-Go Jekyll pillars:
No Ruby runtime is required to build or run it. The resulting binary is static
and CGO-free, and cross-compiles to all six supported 64-bit targets
(amd64/arm64/riscv64/loong64/ppc64le/s390x).
Install
go install github.com/go-ruby-jekyll/jekyll/cmd/jekyll@latest
Usage
jekyll new my-site # scaffold a self-contained site
jekyll build # build ./_site
jekyll serve # build and serve on http://127.0.0.1:4000
jekyll clean # remove the generated site
jekyll doctor # sanity-check configuration
The full command tree and every option/flag — with exact long/short forms,
defaults and descriptions — is captured in CLI_COMPAT.md, which
is transcribed verbatim from jekyll help and used as the differential oracle.
What is implemented
The core build pipeline matches Jekyll's behaviour:
_config.yml load + deep-merge + CLI overrides
- YAML front matter (
--- blocks), --strict_front_matter
_posts, _drafts (--drafts), --future, --unpublished, --limit_posts
- Collections (
_config.yml collections:), output, per-collection permalink
_layouts with layout chaining, _includes (incl. {% include x.html k=v %})
- Liquid rendering with Jekyll's filters (
relative_url, absolute_url,
date_to_string/_xmlschema/_rfc822, slugify, xml_escape, jsonify,
markdownify, number_of_words, array_to_sentence_string, …) and tags
(include, highlight, link, post_url)
- SCSS/Sass (
.scss/.sass) compiled through
go-ruby-sass (pure-Go, CGO=0,
dart-sass-compatible over go-scss), matching
jekyll-sass-converter: the _sass load path (sass.sass_dir), sass.style,
sass.load_paths, and front-matter-triggered conversion to .css
- Kramdown Markdown → HTML with Jekyll's Rouge code-block/inline wrappers
- Permalinks (named styles +
:placeholder templates), site/page variables,
_data, static files, exclude/underscore/dotfile rules
serve (WEBrick-equivalent HTTP with baseurl prefix), new, new-theme,
clean, doctor
- Post/collection excerpts (
page.excerpt): the content up to
excerpt_separator (default "\n\n", overridable per-document), with trailing
Markdown link-reference definitions carried over, rendered through the same
Liquid + Markdown pipeline as the body — matching Jekyll::Excerpt
- jekyll-feed (enabled via
plugins: or a theme's gem dependency): generates
an Atom /feed.xml byte-for-byte with the plugin's template — feed
title/subtitle/author, and per-post title/link/dates/id/content/author
(resolved through site.data.authors)/categories/tags/summary/image, honouring
feed.path, feed.posts_limit and feed.excerpt_only. The {% feed_meta %}
autodiscovery tag is supported. (Per-collection / per-category / per-tag feed
files are a named gap.)
- jekyll-seo-tag (
{% seo %}): emits the SEO / OpenGraph / Twitter-card /
JSON-LD <head> block byte-for-byte with the plugin — title/description/
canonical, og:*, twitter:*, author/image/site verifications, and a
key-sorted JSON-LD document (WebSite / WebPage / BlogPosting), honouring
{% seo title=false %} / canonical=false. Liquid pagination fields
(paginator.*) are the only omitted inputs (a named gap).
- Theme gems (
theme: in _config.yml): a gem laid out in the standard
structure (_layouts, _includes, _sass, assets) is resolved from the
Ruby gem path and layered under the site's own files, so a site file always
overrides its theme equivalent — exactly like Jekyll::Theme. The gem is
located by scanning GEM_HOME/GEM_PATH and gem environment gempath.
Substrate gaps found (reported upstream)
- go-ruby-liquid had no filter/tag registration API. Jekyll's filters must be
evaluable inside
{% for %} loops, so they cannot be pre-expanded by a
wrapper. This is fixed additively by
go-ruby-liquid#6
(liquid.WithFilter / WithFilters), which this module depends on. Custom
tags still have no engine hook, so include/highlight/link/post_url
are expanded by a preprocessing layer here (works at page/layout scope; an
{% include %} that references a loop variable is a residual — see below).
- go-ruby-kramdown collapses the blank lines around a raw HTML block. Ruby
kramdown keeps a leading newline and a trailing blank line; go-ruby-kramdown
does not. The only observed effect is the surrounding whitespace of a
{% highlight %} block placed inside a Markdown document — the highlighted
figure content itself is byte-identical. ({% highlight %} in an .html page
is byte-identical.)
Honestly-deferred surface (parsed, not yet behavioural)
| Area |
Status |
Sass source maps (sass.sourcemap) |
not emitted — go-scss does not yet produce source maps, so the .css.map file and the /*# sourceMappingURL */ comment that jekyll-sass-converter writes under its default sourcemap: always are omitted. CSS output byte-matches the gem on the common surface; set sass.sourcemap: never for exact parity. Other go-scss residuals (advanced @extend unification, media-query conflict pruning, exotic sass:meta/sass:selector) are inherited. |
--lsi related posts |
flag parsed, ignored (needs a latent-semantic-indexing model). |
--incremental |
flag parsed, ignored (a full rebuild is always performed). |
--livereload websocket |
flag parsed; server serves without the live-reload injection. |
--watch / --profile |
parsed; watcher and Liquid profiler not implemented. |
Ruby _plugins/*.rb |
not executed (pure-Go, CGO=0). --plugins dir is still recorded. |
{% include %} referencing a loop variable |
residual of the missing tag-registration hook (page/layout-scope includes work). |
Array/hash Jekyll filters (where_exp, group_by) |
best-effort; string-valued filters are exact. |
| Theme gems with a non-standard layout |
only the standard _layouts/_includes/_sass/assets structure is resolved; a theme that ships its files elsewhere (or via a custom Jekyll::Theme subclass) is not. |
| Empty page body newline |
go-ruby-kramdown renders an empty Markdown body as "" where Ruby kramdown yields "\n". On a jekyll new + minima build the only observed effect is a single insignificant newline inside home.html's {{ content }} slot on the index page — about and post pages are byte-identical. (go-ruby-liquid's {%- -%} / {{- -}} whitespace control is byte-exact against the liquid gem, including minima's skipped-conditional layout shape.) |
Tests & coverage
testdata/site/ is a controlled Jekyll site; testdata/expected/ is its
_site/ as produced by Ruby Jekyll 4.4.1. The test suite builds the source
with this tool and asserts the output is byte-for-byte identical, plus unit
tests for the CLI surface, config, front matter, permalinks, filters and
converters. Coverage target: 100% including error branches.
An end-to-end test (integration_test.go) additionally builds a real
jekyll new-style site on the minima theme (vendored under
testdata/gemroot/) and compares every page to Ruby Jekyll 4.4.1 with minima
2.5.2, jekyll-feed 0.17.0 and jekyll-seo-tag 2.9.0: feed.xml and assets/main.css
are byte-identical (feed modulo its site-time <updated>; CSS modulo the
source-map trailer), the about and post pages are byte-identical, and the
index page is content-identical up to the single empty-body newline residual
noted above.
License
BSD-3-Clause. Copyright (c) the go-ruby-jekyll/jekyll authors.