Migrating from Jekyll

Poops is Jekyll-inspired, so most of the move is renaming directories and deleting a Gemfile. Four things are not a rename, and they are the reason a migration stalls halfway โ€” read these first, then the mapping tables.

The thing What happens What you do
permalink Not supported. A page's output path mirrors its source path, always: src/markup/blog/hello.md โ†’ dist/blog/hello.html Move the files to where the URLs should be. Keep old URLs alive with redirects on the host, or accept the change
Dates in _posts filenames Not parsed. 2024-01-05-hello.md becomes 2024-01-05-hello.html, and the post has no date Rename the files and put date: in front matter โ€” the build warns on any post without one and falls back to mtime, which a CI checkout resets
Liquid dialect Poops' Liquid is LiquidJS, not Jekyll's Ruby Liquid. {% include %}, {% highlight %} and Jekyll's filter set differ Port the tags in the table below, or switch the site to Nunjucks while you are in there
Gem plugins There is no plugin API The common ones are config keys โ€” see the plugin table. Anything else has to become a build step or go

Everything else is mechanical.

Directories

Jekyll's layout, and where each part lands:

Jekyll Poops Note
_config.yml poops.json One file, all pipelines. Not YAML โ€” JSON with $schema completion
_layouts/ _layouts/ under markup.in Underscore directories are never output. Add it to includePaths
_includes/ _partials/ (any name) under markup.in Also on includePaths; {% render "site-header.liquid" %}
_data/ markup.options.data An explicit list of files: ["_data/links.json", "_data/authors.yaml"]
_posts/ any direct subdirectory of markup.in It becomes a collection; the directory name is the collection name
_drafts/ published: false in front matter The page is skipped and kept out of the collection
_sass/ anywhere; styles[].in points at your entry Dart Sass, with includePaths for the load path
assets/ copy, or styles/scripts entries Static files get copied; sources get compiled
_site/ markup.out Whatever you name it โ€” dist in these docs

Config

_config.yml poops.json
title, description, author markup.options.site.title, .description, โ€ฆ โ€” anything under site reaches every template
url markup.options.site.url โ€” used by the canonical, og and jsonld filters and by sitemap.xml
baseurl markup.options.baseURL, or the --base-url flag in CI. Leave it unset and paths stay relative โ€” see deploying
collections: markup.options.collections, or collection: true in the directory's index front matter
paginate: 10 paginate: 10 on the collection
defaults: (front-matter defaults) no equivalent โ€” set the field per page, or read site in the layout as the fallback
exclude:/include: underscore-prefixed directories are excluded; there is no include list
markdown:/kramdown: fixed: marked with GFM, alerts, footnotes, emoji and build-time syntax highlighting

A minimal blog, whole:

{
  "markup": {
    "in": "src/markup",
    "out": "dist",
    "options": {
      "engine": "liquid",
      "site": { "title": "My Site", "url": "https://example.com" },
      "includePaths": ["_layouts", "_partials"],
      "data": ["_data/links.json"],
      "sitemap": "sitemap.xml",
      "feed": { "collection": "blog", "out": "feed.rss" }
    }
  },
  "styles": [{ "in": "src/scss/index.scss", "out": "dist/css/styles.css", "options": { "minify": true } }],
  "watch": ["src"],
  "livereload": true
}

Front matter

Jekyll Poops
layout: default same โ€” resolved from includePaths, or package/layout from node_modules
title, description same
date same, and now required on posts you care about ordering
published: false same
categories, tags any field becomes a taxonomy when the collection declares it: taxonomies: [tags]
permalink gone โ€” the file path is the URL
excerpt_separator gone โ€” page.excerpt is the first prose paragraph, capped at 160 characters
sitemap: false robots: noindex โ€” drops the page from the sitemap and llms.txt, keeps it in your on-site search index

Liquid, ported

Both engines are available; engine: "liquid" keeps your templates closest. The dialect still moves:

Jekyll Poops
{% include site-header.html %} {% render "site-header.liquid" %} โ€” LiquidJS names the file with quotes and an extension
{{ content }} in a layout {% block content %}{% endblock %} โ€” in both engines. Poops wraps the page body in a content block and the layout declares where it goes; a Liquid layout that still says {{ content }} renders empty
{% for post in site.posts %} {% for post in blog.items %} โ€” every collection is a global named after its directory
{{ site.baseurl }}/css/styles.css {{ relativePathPrefix }}css/styles.css โ€” correct at any depth and under any deploy path
{% highlight js %}โ€ฆ{% endhighlight %} same tag, quoted language: {% highlight 'javascript' %}โ€ฆ{% endhighlight %}. Markdown fences are highlighted with no tag at all
{{ post.date | date: "%b %-d, %Y" }} {{ post.date | date: "MMM D, YYYY" }} โ€” dayjs tokens, with a site-wide dateFormat default
{{ page.content | strip_html | truncatewords: 30 }} {{ page.excerpt }}
{% seo %} (jekyll-seo-tag) {{ page | og(site) }}, {{ page | canonical(site) }}, {{ page | jsonld(site) }}

Plugins

Gem Poops
jekyll-feed markup.options.feed โ€” RSS or Atom, one key
jekyll-sitemap markup.options.sitemap
jekyll-seo-tag the og, canonical and jsonld filters
jekyll-paginate paginate on the collection, plus the {% pagination %} tag
jekyll-archives taxonomies on the collection โ€” term pages, paginated, in the sitemap
jekyll-assets / a separate webpack styles, scripts, postcss โ€” the reason to move
jekyll-redirect-from nothing โ€” do redirects at the host
A theme gem one theme exists, poops-docs-theme, and it is a devDependency, not a gem

The order that works

  1. Copy the site into src/markup/. Rename _includes to _partials if you like; the name is yours as long as it starts with an underscore and is on includePaths.
  2. Rename _posts to the URL you want โ€” blog/ โ€” and strip the date prefixes from filenames.
  3. Add date: to every post's front matter. The build lists the ones you missed.
  4. Write poops.json from the config table above, with "engine": "liquid".
  5. Run poops -b and read the errors. Unknown tags are the Liquid dialect; empty output is usually a site.posts that is now blog.items.
  6. Delete Gemfile, Gemfile.lock, _config.yml, _site/. Add dist/ to .gitignore.
  7. Replace the GitHub Pages branch build with a workflow โ€” Pages no longer builds the site for you, and this is the step that catches people after the site already works locally.

Note

These pages are a mapping, not a script. Nothing here was run against an existing Jekyll site โ€” the Poops side of every row is documented behaviour, the Jekyll side is from its docs, and your site will have a case neither of us thought of.

When not to migrate

The site is Markdown and layouts with no assets to build, GitHub builds it for free on push, and the theme you use is a gem. That is Jekyll working as designed โ€” see the comparison for the rows it wins outright.