Skip to content

Migrate xam.dk from Jekyll to Quarkus Roq - #227

Merged
maxandersen merged 20 commits into
masterfrom
roq-migration
Oct 4, 2026
Merged

maxandersen merged 20 commits into
masterfrom
roq-migration

Conversation

@maxandersen

@maxandersen maxandersen commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Migrate xam.dk from Jekyll to Quarkus Roq

Converts the site from Jekyll 4.3 (clean-blog theme) to Quarkus Roq 2.1.9, preserving every URL and all content.

What changed

  • Scaffold: pom.xml, Maven wrapper, config/application.properties (Roq 2.1.9 / Quarkus 3.40.1 / Java 21+).
  • Templates: Liquid → Qute (alt-expr syntax). 4 layouts (default/home/page/post) + partials (head/navbar/footer/scripts/post-meta).
  • Content: 98 posts + pages moved into content/ via git mv (history preserved). AsciiDoc posts keep their original = Title/:page-* headers. _data/ reused in place (quarkus.roq.data.dir=_data); site props in _data/siteConfig.yml.
  • Assets: assets/ → public/assets/; SCSS compiled once to public/assets/main.css (build-css.sh). Static dirs/microsites moved to public/ (static) or content/ (AsciiDoc pages).
  • Features: blog pagination (/blog/, /blog/pageN/), RSS at the original /blog/feed.atom, post URLs /blog/<name>/ (match old Jekyll permalinks), site-wide default masthead background preserved via a root-layout frontmatter default.
  • CI: .github/workflows/deploy.yml now builds via jbang roq generate and rsyncs target/roq/ to the same ssh.xam.dk:/www target (unchanged excludes).
  • Removed Jekyll artifacts (Gemfile, _config*, _layouts, _includes, _plugins, Rakefile, theme submodule, …).

AsciiDoc: use the jruby plugin (AsciidoctorJ)

The old site was Jekyll + jekyll-asciidoc = AsciidoctorJ. The default Roq plugin uses Yupiik asciidoc-java (pure Java), which has rendering gaps vs AsciidoctorJ (single-line ifdef drops the whole body — quarkus-roq#1287; video:: drops width/height; admonition markup differs). To match the old site by construction and avoid per-gap hacks, this uses quarkus-roq-plugin-asciidoc-jruby:

  • single-line ifdef renders correctly (content unchanged);
  • video::[... width=640, height=480] emits <iframe width height> as before;
  • quarkus.asciidoc.attributes.notitle=true (title once, in the masthead) and icons=font (admonition <i class="fa icon-note">), matching old behaviour;
  • dropped redundant site.escaped-pages.

Trade-off: bundles a JRuby runtime (heavier than pure Java), acceptable for a personal blog.

Historical comments + webmentions (read-only)

New comments/webmentions can't be submitted (staticman + jekyll-webmention_io are gone), but the old ones are preserved: _data/comments/ and _data/webmentions/received.yml were consolidated into _data/comments.json / _data/mentions.json (keyed by post URL) and rendered via templates/partials/post-reactions.html (213 comments, 147 webmentions). Likes/reposts/mentions show as a compact facepile with counts; replies and comments show full text. Author avatars are cached locally in public/avatars/ (96px) by the one-off jbang script FetchAvatars.java (unavatar.io, token-aware, idempotent) — no runtime dependency; dead accounts fall back to initials circles. The webmention receiving <link rel="webmention"> endpoint is kept in the head.

Cleanup of unused code/assets

  • Removed dead staticman/recaptcha config and the unused contact-form JS (no contact page).
  • Dropped Jekyll-theme dev/source artifacts that were never served: bootstrap/font-awesome/jquery non-min + less//scss/ sources, jqBootstrapValidation.js, public/assets/main.scss, and clean-blog theme sample images. Only referenced minified assets + webfonts remain (public/assets ~1.3M).

Testing

  • @RoqAndRoll generation test (src/test/java/dk/xam/SiteGenerationTest.java) — 8/8 pass; guards body rendering, no duplicate title, default background, pagination, RSS, microsites.
  • Content-parity diff vs live xam.dk: 0 URL drift (all 98 post URLs return 200); per-page prose identical (the only flagged, code-heavy posts differ solely in code-highlighter markup — prose byte-identical).
  • roq generate: BUILD SUCCESS, 119 pages.

See .specs/roq-migration.md for the full plan, testing method, known corner-cuts, and follow-ups (e.g. Font Awesome icon-note glyph CSS).

…ering

- pom.xml/mvnw/.mvn + config/application.properties (Roq 2.1.9)
- Qute layouts (default/home/page/post) + partials (head/navbar/footer/scripts/post-meta)
- git mv _posts->content/posts, about/404/index into content/
- reuse _data via quarkus.roq.data.dir; site props in _data/siteConfig.yml
- fix dup post (besog) and mixed asciidoc callouts
- install Roq skill refs; plan in .specs

roq generate: BUILD SUCCESS, 101 pages
- assets/ -> public/assets; compile _sass -> public/assets/main.css (build-css.sh)
- static dirs (img images js qrcode userscripts) + files (avatar favicon etc) -> public/
- self-contained microsites (anabelle benjamin asylum tree) -> public/ as-is
- asciidoc microsites (cape5 hibern8ide house surprise maxandersen) -> content/ directory pages
- blog/wp-content -> public/blog/wp-content; split maxandersen .well-known -> public
- drop obsolete pygments css link

roq generate: BUILD SUCCESS, 109 pages, css+assets+microsites verified
- content/blog.html: paginated blog index (/blog/, /blog/pageN/)
- content/rss.xml -> /blog/feed.atom (full content)
- posts link /blog/:name/ (filename-based, matches old Jekyll :title URLs)
- drop unused sitemap/tagging/aliases plugins (not in original site)
- rewrite CI: jbang 'roq generate' + rsync to ssh.xam.dk:/www
- move .htaccess -> public/; migrate draft with draft:true
- remove Jekyll artifacts (Gemfile, _config*, _layouts, _includes, _plugins,
  Rakefile, _rake*, themes submodule, staticman.yml, etc.)

roq generate: BUILD SUCCESS, 119 pages, all URLs verified
@maxandersen
maxandersen force-pushed the roq-migration branch 2 times, most recently from 7e6e20e to f8eb19d Compare October 3, 2026 21:47
…ault + tests

The old site was Jekyll + jekyll-asciidoc (AsciidoctorJ). Use
'quarkus-roq-plugin-asciidoc-jruby' so rendering matches by construction and we
avoid per-gap workarounds for the default pure-Java engine (asciidoc-java):
- single-line 'ifdef::env-github,...[:imagesdir: ..]' renders correctly
  (asciidoc-java silently dropped the whole body -> quarkiverse/quarkus-roq#1287);
- 'video::[... width=640, height=480]' emits '<iframe width height>' like old;
- content keeps original '= Title'/':page-*' headers (no block-form/YAML rewrites).

AsciiDoc config (matches old behaviour):
- quarkus.asciidoc.attributes.notitle=true  (title once, in the masthead)
- quarkus.asciidoc.attributes.icons=font    (admonition <i class=fa icon-note>)
- dropped redundant site.escaped-pages

Also restored Jekyll's site-wide default masthead background as a root-layout
frontmatter default (per-page backgrounds override).

Testing: @RoqAndRoll generation test (8/8). Parity vs live: 0 URL drift (98/98
posts 200); per-page prose identical (the 2 flagged code-heavy posts differ only
in code-highlighter markup, prose byte-identical). roq generate: BUILD SUCCESS.
New comments/webmentions can't be submitted anymore (staticman + jekyll-webmention_io
are gone), but the old ones are preserved and displayed:
- Consolidated _data/comments/<slug>/*.yml -> _data/comments.json and
  _data/webmentions/received.yml -> _data/mentions.json, both keyed by post URL
  path (the nested dirs registered as slash-named beans Qute can't navigate).
  213 comments across 28 posts; 147 webmentions across 10 posts.
- templates/partials/post-reactions.html renders them on each post (included in
  the post layout), with avatars/author/date and h-cite/h-card microformats.
- Restored the webmention receiving endpoint (<link rel=webmention/pingback> ->
  webmention.io) in the head (was dropped in Phase A).
- Styling in user.css; 3 new @RoqAndRoll tests (11/11 pass).

e.g. /blog/the-black-swan-of-java/ shows 75 webmentions, /blog/new-job-at-jboss/
shows its comments.
Likes/reposts/mentions now render as a small avatar facepile with a one-line
count ('52 likes · 19 reposts · 1 mention') instead of a verbose row each, so
actual replies and comments stand out. Replies and comments keep full content.

- Regrouped _data/mentions.json by likes/reposts/links/replies (all keys always
  present so Qute .size/iteration is null-safe).
- Rewrote post-reactions.html facepile + counts; facepile CSS in user.css.

Tests 11/11.
The webmention photos were all dead 'webmention.io/avatar/...' links (404; the
proxy is gone), so broken <img>s were falling back to showing the author names.
Replaced them with compact colored initials circles (computed into mentions.json,
no external deps, always render). Comment avatars use the still-working disqus
CDN and are kept as images.

Tests 11/11.
Webmention photos now come from unavatar.io derived from each author's twitter
handle (real current avatars; unavatar returns a default for dead accounts, never
a broken link). Falls back to the colored initials circle via img onerror, and
for the few non-twitter/no-url authors. New templates/partials/avatar.html.

Tests 11/11.
Avatars were rendering at full source size. Fetch small images via unavatar
'?size=96' and enforce explicit width/height on reaction avatars (facepile 32px,
replies/comments 40px, object-fit: cover) so size is unambiguous even if an
avatar source is large or CSS is stale.

Tests 11/11.
- FetchAvatars.java (jbang, token-aware, idempotent): downloads unavatar.io photos
  once, resizes to 96px, caches in public/avatars/, rewrites _data/mentions.json to
  local /avatars/*.jpg (dead accounts -> initials). No runtime dependency / rate
  limits. 86 avatars cached (348K); 10 dead -> initials.
- avatar.html partial: <img> local photo with initials-circle onerror fallback.
- CSS: consolidated; classic circular avatars, smaller overlapping facepile
  (24px) with white ring, 36px comment/reply avatars.

Tests 11/11.
Staticman comment submission is gone (staticman.yml removed in Phase C), so:
- drop the dangling staticman endpoint + unused recaptcha keys from _data/config.yml
- remove the dead contact-form block + unconditional recaptcha api.js from
  scripts.html (no contact page exists; form never rendered)

Tests 11/11.
Only the referenced minified assets + webfonts are kept; dropped dev/source
artifacts that were copied in from the Jekyll theme and never served:
- bootstrap non-min css/js, bootstrap-grid/-reboot, bootstrap.js/bundle.js
- font-awesome non-min css, FontAwesome.otf, less/ and scss/ sources
- jquery.js (non-min), jqBootstrapValidation.js (contact form gone)
- clean-blog scss/ sources, public/assets/main.scss (we compile _sass->main.css)
- img/ theme samples (about-bg2, contact-bg, post-sample-image)

public/assets 1.3M. Tests 11/11.
Jekyll relativize ensured links worked under any base path. Roq equivalent:
site.url(path)/page.url emit host-relative paths that honor site.path-prefix.
Converted all hardcoded /... links+assets (head, navbar, footer, scripts,
home/page/post layouts, avatar partial, social links) to site.url(...).
External/mailto URLs pass through unchanged. At root output is identical;
setting site.path-prefix=/sub now relocates everything (verified -> /sub/...).

Tests 11/11.
Restores Jekyll's relativize_url behavior so the site is portable under any base
path (and file://) with no config. UrlExtensions adds page.rel(target) (Qute
template extension) computing a path relative to the current page via
java.nio.file.Path.relativize; external/mailto/anchor pass through. All templates
now use {=page.rel(...)} for internal links/assets/backgrounds/avatars/social;
post/prev/next links relativized too; u-url uses page.url.absolute. Includes a
self-test (main) verifying relativize round-trips via URI.resolve.

Verified: 2868 generated local links resolve to existing files. Also fixed a
stray 'kk' typo in home.html masthead. Tests 11/11.
… .adoc link

- image::images/… -> image::/images/… so they resolve (matches the /images/
  convention used by all other posts); affected flamegraph.png and quoting-ios.png
- jbang-everywhere: link:…unleashing….adoc[] -> link:/blog/unleasing-the-scripting-powers-of-java/[]

Remaining unresolved links are all in the archived asylum/ microsite (dead
index.php + Scripts/), left as-is. Tests 11/11.
Pure AsciiDoc (native = Title / :page-*: header, no YAML frontmatter) telling the
migration story and calling out: no frontmatter needed, relativize via a Qute
template extension (with the in-content caveat), restored read-only comments/
webmentions, and the cleanup.
The compiler-plugin runs now that there's a Java source (UrlExtensions);
CI was on JDK 21 -> 'release version 25 not supported'. Bump setup-java to 25.
@maxandersen
maxandersen merged commit b44406d into master Oct 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant