Conversation
Replaces the flat-file docs under docs/ with an Astro + Starlight site that will publish to markbridge.dev via GitHub Pages. Site content covers the same ground as the legacy docs but restructured into a Getting Started page, per-format guides (BBCode, HTML, MediaWiki, TextFormatter), an Extending guide, a Configuration guide, and Concept pages for architecture, the AST, parsers, renderers, and performance. Tooling: - bin/docs wrapper runs pnpm scripts against docs/ (defaults to dev). - .silo.yml installs nodejs+pnpm from dnf, runs pnpm install on setup and sync, exposes port 4321, and autostarts a docs daemon. - .github/workflows/docs.yml deploys to GitHub Pages on pushes to main that touch docs/ or the workflow itself. Branding assets (icon and social card masters) live under docs/branding/; the site-served copies are in docs/public/ and docs/src/assets/.
Use the standard foreground color for the Starlight site title so the brand-orange accent stays on links and highlights. Mention TruffleRuby and JRuby alongside CRuby in the requirements, landing page, and performance guide.
Running the docs locally creates a top-level /.pnpm-store/ cache; keep it out of git.
Tighten the hero tagline (formats are listed in the first card now, not enumerated twice), drop the Seti file-tree icons in favour of Starlight's built-in line-art set for visual consistency, and give each feature card the rotation accent on its border plus a 2px hover lift. The lift composes with the CardGrid stagger offset so right-column cards no longer leap when hovered.
Drop sections that duplicate the docs site or are derivable from the code (architecture overview, AST tree, module structure, constants table, file/extension-point tables, generic RSpec advice). Keep the project-specific rules an agent can't infer: workflow scripts, style conventions RuboCop doesn't enforce, mutation-testing guardrails, and the MarkdownEscaper hot-path warning.
The mutation-coverage refactor (8ddfa6b) changed elements_from_current(MAX_AUTO_CLOSE_DEPTH) to MAX_AUTO_CLOSE_DEPTH - 1, lowering the actual peek-ahead bound by one. The docs still advertised "looks ahead up to 5 tokens" and listed a separate MAX_PEEK_AHEAD constant that doesn't exist. Reframe in terms of the real bound (sequences of up to 5 mismatched closing tags) and fix the constant location in the performance table from ClosingStrategies::Base to ClosingStrategies::TagReconciler.
Each format guide now demonstrates the umbrella loader for the format it covers, and the getting-started "lighter alternative" hint enumerates them so readers see HTML/TextFormatter pull Nokogiri while BBCode/MediaWiki don't.
Match the HTML guide's wording so readers see the gemfile snippet and the "only for this parser" clarification, instead of the one-line note that easily reads past.
Drop the misleading "only" from the Nokogiri requirement note in the
HTML and TextFormatter guides — Nokogiri is needed by both, so neither
guide owns it exclusively. On the landing page, restate the same dep
generically ("for some parsers") and replace the BBCode-specific
"single-pass scanner" wording with "single-pass parsing" so the
Performance-conscious card describes all four parsers, not just BBCode.
Switch single-format snippets to per-format umbrella requires and drop the `-rmarkbridge/all` preload from the YJIT CLI example. The landing page's "See it in action" snippet now requires `bbcode` and `html` directly to match the two methods it calls; getting-started's first conversion uses `markbridge/bbcode`, with the explanatory paragraph re-led to introduce the format-specific path first and mention `markbridge/all` as the "all four" shortcut. Only one `markbridge/all` reference remains, which is the intentional one in that paragraph.
Add docs/scripts/generate-changelog.mjs, a small Node script that hits the GitHub Releases API and writes docs/src/content/docs/changelog.md. Hook it into pnpm dev/start/build so the page is always populated, and expose it as `pnpm changelog` for ad-hoc runs. The generated file is gitignored — the GitHub release page is the single source of truth. Each release is rendered compactly: H3 heading with version and date, H4 category headings (Features / Bug Fixes), with the redundant "What's Changed" wrapper and category emojis stripped. The wrapper stays on the GitHub release page itself, where the body has no H1 to anchor under. Replace the external "Releases" sidebar entry with an internal Changelog page, and extend .github/workflows/docs.yml to fire on release: published / edited / deleted so editing a release on GitHub republishes the site within a minute or two.
Add spec/docs/ — a new spec tier for cross-checks between code and the docs site — and put the first guard in it: a bidirectional check that every Markbridge::AST::* class appears in the AST concepts page, and that the page doesn't reference classes that no longer exist. The check caught seven classes the page had silently lost track of (Email, Event, MarkdownText, Mention, Paragraph, Poll, Upload), so expand the node-hierarchy tree to cover them. Split the existing "Content" group into "Block-level" and "Content" so paragraph- and heading-style nodes have a natural home alongside the new Email and Discourse-specific leaves. Note the new tier in AGENTS.md and add a matching exclusion to .rubocop.yml so RSpec/DescribeClass doesn't fire on tier-meta specs.
Walks docs/src/content/docs/**/*.{md,mdx}, extracts every ```ruby
fenced block, and runs each in a subprocess so blocks don't pollute
each other. Setup injected once per block:
- If the block already has a require "markbridge/..." line, leave
it alone — only `include Markbridge` is prepended.
- Otherwise prepend `require "markbridge/all"; include Markbridge`,
so the example pulls in the full library and short-form refs
like `AST::Foo` resolve without the `Markbridge::` prefix that
docs typically omit for readability.
Failures surface stderr, stdout, and the wrapped source so the
specific docs file:line is easy to find. No skip mechanism yet —
add one if it turns out we need an escape hatch for snippets that
genuinely can't run.
Two HTML/MDX comment directives now drive how each ```ruby block runs:
<!-- spec:continue --> prepend every preceding ```ruby block in
this file. Use for narrative docs that
build up an example across snippets.
<!-- spec:before
ruby = "code" invisible setup for the next block. Use
--> when readers need to see a placeholder
(e.g. an `input` variable) but the spec
needs it filled in.
Multiple `spec:before` comments accumulate; both directives flush at
the next ```ruby block. There is intentionally no skip directive — the
goal is for every Ruby fenced block in the docs to run.
Fix: extract_blocks looped indefinitely on single-line HTML comments
because read_comment returned `chunk.length - 1` (= 0) when `<!-- ... -->`
fit on one line, so the parser cursor never advanced past it. Returning
`chunk.length` resolves the hang.
The spec now runs each subprocess with `RUBYOPT="-W0 -rbundler/setup"`
so snippets can `require` Gemfile gems. Add `csv` and `benchmark` to
the Gemfile — they're stdlib gems on older Rubies and bundled gems on
4.0+, and several docs examples idiomatically use them.
Annotate every previously-failing ```ruby block with the right
directive so the docs code-example spec runs all 41 of them green
(and finds none of the 13 that used to fail at first run):
- spec:continue on the second-and-later blocks of narrative chains
in extending.md, bbcode.md, and textformatter.md, so each block
sees the AST/handler/tag classes defined in earlier blocks.
- spec:before for invisible setup before snippets that show
placeholder identifiers (`input`, `my_registry`, `MyBoldTag`,
`MyCalloutHandler`) or stub I/O (`CSV.foreach`, `File.readlines`).
- The `...` ellipsis in parsers.md's lambda example was a literal
syntax error; replace with a minimal real body (`{ parent }`).
Fix one real doc bug uncovered by the spec: bbcode.md showed
`ClosingStrategies::Strict.new` (zero args), but the strategy now
requires a TagReconciler. Replace with the correct construction
through `HandlerRegistry.build_from_default`.
Captures the user journey we keep seeing in real importers (resolve IDs, emit placeholders, collect side data, surface unknown tags) plus the concrete pain points the current API doesn't smooth over: default-handler fallback boilerplate, mutable trackers threaded into Tag constructors, the three-file placeholder triad, and the lack of a high-level unknown-tags surface. Sketches four API additions (result object, side-data emission, overlay registration, optional placeholder DSL) and the docs reorganization that follows once the API stabilizes — explicitly NOT a commitment, just a parking place so we don't lose context while the actual work happens on a fresh branch off main.
Pivot the docs site to the task-oriented "I'm migrating a forum to Discourse" perspective and bring every page in sync with the migration- API redesign coming in PR #30. New top-level "Migrating to Discourse" section with overview, placeholders, and a full walkthrough that ports examples/forum_migration.rb end-to-end. The placeholders page makes explicit what was implicit: Tag return strings are spliced verbatim — only AST::Text goes through the escaper — so placeholder sigils reach the output untouched. The old guides/configuration.md is gone; renderer-side knobs move to a new customization/customizing-renderer.md describing the Markbridge.discourse_renderer(...) factory. PR30's UPGRADING.md is ported under reference/upgrading.md. Existing pages updated for: Conversion return type, removed Markbridge.configure global, MediaWiki handlers: kwarg rename, TextFormatter handler processor: arg, interface.emit and html_mode? on the rendering interface table, the build-once / reuse-many renderer pattern. MIGRATION_API_PLAN.md updated to reflect §1-3 implemented in PR30, §4 (DSL) considered and rejected, and the resolved open questions. Every code sample in the migration section was verified against PR30's branch via a sibling worktree before commit.
Add a third dependabot ecosystem entry covering docs/'s npm/pnpm package tree (Astro, Starlight, sharp). Grouped weekly so the docs deps don't leak into the gem's normal DEPS PRs.
Three pipeline diagrams ship as native Excalidraw SVG exports with
light and dark variants, embedded via <figure class="diagram">
markup that toggles between the two via Starlight's [data-theme]:
- concepts/architecture: three-box parse → AST → render pipeline
- migrating/overview: five-stage pipeline with the
interface.emit side channel
- migrating/placeholders: the placeholder triad showing how a
source tag splits into a placeholder string and a side-data
emission
Each diagram has three files in docs/public/diagrams/:
- <name>.excalidraw — editable JSON source (drop into
excalidraw.com to tweak)
- <name>.svg — light-mode export
- <name>-dark.svg — dark-mode export (toggle Excalidraw's
"Dark mode" checkbox in the export dialog)
Visual language: amber for boundaries (input, output), blue for
processes (Parse, Render), purple for data (AST::Document, custom
AST nodes), green for output, red dashed for side channels
(interface.emit, emissions).
Card surface in custom.css uses a soft tint that adapts to the
page theme — #f8fafc in light mode, #1e2230 in dark mode — so the
diagram has a frame without becoming a bright white island in dark
mode.
Re-introduce the dialog-based click-to-zoom that worked for the mermaid era, scoped to figure.diagram > img. Native <dialog> opened with showModal() lands in the top layer, immune to ancestor CSS transforms — Starlight's view-transition wrapper otherwise breaks position: fixed and the zoom never lifts off the article column. Clicks delegate from document; closes on backdrop click or Escape. The clone of the visible <img> (light or dark per current theme) is appended to the dialog content, so the inline copy stays in place when the dialog closes.
Two Vite tweaks that together fix the "edit an SVG, hard-refresh,
still see the old one" loop:
- `server.headers: { 'Cache-Control': 'no-store' }` — Vite's
default static-file middleware lets the browser cache `public/`
assets aggressively. With no-store the browser must revalidate
every fetch, so changed files actually appear on refresh.
- `server.watch.usePolling: true` — `inotify` doesn't fire on
every filesystem (overlayfs, networked mounts, some containers).
Polling guarantees file changes are picked up regardless of the
underlying FS. Interval: 200ms.
Build mode is unaffected (Astro emits hashed asset URLs there, so
caching is fine).
Swap the made-up `[upload=42]filename.jpg[/upload]` for phpBB3's real attachment syntax, `[attachment=0]filename.jpg[/attachment]`. The `0` is the *position* of the attachment within the post (zero- indexed, the same shape vBulletin and IPB use under different tag names) — not an upload ID, since the file metadata lives in the source forum's attachments table joined to the post. That position- to-row indirection is exactly the kind of resolution `interface.emit` is built for: capture the position at render time, let the importer match it against the source database after the fact. Renames track the new shape: - UploadPlaceholder → AttachmentPlaceholder - source_id → position - emissions key :uploads → :attachments - placeholder string [upload|N] → [attachment|N] Handler now overrides Markbridge's built-in [attachment]/[attach] defaults rather than registering an unused [upload] alias. Wired with a single shared handler instance per the multi-alias caveat. Verified end-to-end against PR30's gem branch including the [attach] alias path. Diagram source updated to match; the exported SVGs need a re-export from Excalidraw to pick up the new labels.
Output the Discourse-native upload-marker shape `[upload|HASH]`
instead of echoing the source position back as `[attachment|N]`.
That matches what Discourse's import infrastructure recognizes —
the placeholder is what an importer feeds the upload-resolution
pass, not a thing the importer rewrites further.
To make that work the Tag needs the source post's attachment
table at render time, so it can look up SHA-1 by position.
Construct the renderer per post with that post's attachments
passed to the Tag constructor; the handler stays shared. The
extra notes call out the build-once-vs-per-post tradeoff:
post-independent state (handlers, escaper, postprocessor) lives
outside the loop, post-dependent state (the attachment table)
goes inside.
Emission now carries `{position, sha1, filename}` so the importer
gets both source-side identification and the resolved hash for
its own bookkeeping.
Diagram source updated to match (`[upload|HASH]` placeholder text,
emission shape includes the sha1). Re-export from Excalidraw to
refresh the rendered SVGs.
The hash in `[upload|HASH]` isn't a content hash — it's the stable
identifier the importer's converter framework derives from the
source filename/path so each placeholder maps unambiguously to one
source-side row.
Renames everywhere:
- sha1 attribute → upload_id
- emission key :attachments[:sha1] → :attachments[:upload_id]
- sample value abc123def456 → u_screenshot_png_3a7c2 (suggestive
of a path-derived ID, not a content hash)
The narrative now explicitly names this as a path-derived ID,
not a SHA-1, so future readers don't reach for `Digest::SHA1` and
get the wrong identifier. Diagram source updated to match; SVGs
need a re-export from Excalidraw to refresh.
Two corrections to the placeholder example:
1. The AST node now carries `filename` directly. Following
Markbridge's own AttachmentHandler, the parser uses
RawContentCollector to capture the body text between
`[attachment=0]` and `[/attachment]` at parse time and pins
it on the node. AttachmentPlaceholder is now a `Node` (leaf)
instead of `Element`, since there are no children to render.
The Tag drops `interface.render_children` and reads
`element.filename` directly.
2. The emission key is `:uploads`, not `:attachments`. The
downstream consumer is Discourse uploads, so the key matches
what the importer is actually building. `result.emitted(:uploads)`
now returns the placeholder records.
Diagram source updated to match: AST box says `(position: 0,
filename: ...)`, emissions box says `Conversion#emissions[:uploads]`.
SVGs need a re-export from Excalidraw to refresh.
"Worked example for **phpBB3**, which emits..." reads like a journal heading. Switch to "Concrete example: phpBB3 emits...", which signals "I just described the general shape — here's a specific one" more directly and drops jargon-y bolding.
Add §5 to MIGRATION_API_PLAN.md capturing the design conclusion that
placeholder resolution belongs in custom handler subclasses at parse
time, not in renderer Tags at render time.
Implications, all written out:
- The converter framework (a layer above Markbridge) owns the
resolution-aware base handler classes, the placeholder AST
classes, and the trivial render Tags. Markbridge keeps the parser
primitives. Per-format converters (phpBB / vBulletin / SMF /
IPB / XenForo) subclass the bases via template method, only
overriding source-ref extraction.
- Resolution is a *write* (store the upload, get the id), not a
pure lookup. Parse-time resolution gives that side effect a
natural once-per-attachment home; render-time would re-store on
every render or need an in-Tag cache.
- AST nodes carry resolved data; render Tags become trivial.
`interface.emit` and `Conversion#emissions` (§2) are no longer
needed and should be removed from PR #30 before merge.
- Section ends with a concrete file-by-file change list for PR #30
plus the docs-branch follow-up needed once §2 is removed.
§2 above is now marked obsoleted-by-§5. Working branch section
unchanged.
Replace the manual rv/ruby-install/cargo bootstrap with silo's `use:` directive for Ruby (3.3/3.4/4.0/jruby, default 4.0) and Node, and bump the base image to fedora/44. Ports now live under each daemon instead of the top-level `ports:` list. Add docs/pnpm-workspace.yaml approving the esbuild/sharp build scripts so the new `CI=true pnpm install` runs non-interactively in setup/sync.
Re-export the placeholders diagrams from Excalidraw, updating the viewBox/dimensions and embedded font data.
Pin pnpm@11.9.0 so the pnpm version is deterministic across local dev, CI, and Dependabot. Matches the existing lockfile (v9.0); no lockfile change needed.
Astro's sitemap integration emits the sitemap but no robots.txt to advertise it. Add one (allow all + Sitemap: line).
Add a pull_request trigger (scoped to docs/**) so opening a PR validates the Astro build on CI. Guard the deploy job with github.event_name != 'pull_request' so Pages only publishes on push to main / release. Scope concurrency per-ref so a PR build never blocks a main deploy, and cancel superseded PR builds.
withastro/action's package-manager: pnpm input fights the packageManager pin in docs/package.json — pnpm/action-setup errors with 'Multiple versions of pnpm specified'. Let the packageManager field be the single source; the action still auto-detects pnpm from the lockfile.
pnpm 11.9.0 requires Node >= 22.13; the workflow was on Node 20, which failed with ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite. Node 22 matches local dev.
Bump withastro/action v3 → v6 (the v3 sub-actions target the now-deprecated Node 20 and were being forced onto Node 24), checkout v6 → v7 to match main.yml, and deploy-pages v4 → v5.
Each snippet runs in its own ruby subprocess for isolation. On JRuby and TruffleRuby that's a JVM/Truffle cold start per block (dozens of them), which dominated those CI jobs. The snippets test the documented public API, which is engine-agnostic, so run them on CRuby only — the unit, integration, and system specs still cover JRuby/TruffleRuby.
Discourse quotes render as [quote="alice, post:3, topic:7"], so they need the importer to resolve the post/topic refs — they don't translate straight across. Use headings as the example instead.
Astro's content-collection watcher is a separate chokidar instance that doesn't inherit Vite's server.watch.usePolling, so on container/mounted filesystems (where inotify is unreliable) Markdown edits weren't picked up — the page kept serving stale content even after a hard refresh. Set CHOKIDAR_USEPOLLING for the dev server, which every chokidar watcher in the process reads.
I added CHOKIDAR_USEPOLLING on a wrong diagnosis. Tested it properly: inotify works fine in the container (limits are huge and barely used), and a fresh dev server picks up Markdown edits via inotify in ~1s with no polling. The stale page came from a long-running Astro persistent dev-server process whose watcher had desynced — 'astro dev stop' + restart fixes it, no polling needed.
Same wrong assumption as the reverted CHOKIDAR_USEPOLLING: inotify works fine in the Incus container. Verified after removal that both Markdown (Astro content watcher) and custom.css (Vite watcher) hot-reload via inotify. Keeps the Cache-Control: no-store header, which is about browser caching of public/ assets in dev, not file watching.
'reads side data from conversion.ast' used a term not yet defined at the top of the page and an implementation path. Relabel to 'importer reads back from the AST' — names the actor and points at the AST::Document box. Align the alt text. (overview SVGs need re-exporting from the source.)
The 'Four stages' list covered only the pipeline boxes, leaving the 'importer reads back from the AST' element in the diagram unexplained. Spell out that read-back step in the closing paragraph and match the diagram's wording so the box has a clear counterpart in the text.
The 'importer reads back from the AST' element was a placeholder detail that confused more than it helped in a top-level pipeline diagram. Remove it (box, label, and arrow) so the diagram is exactly the four stages, and keep the read-back where it reads more naturally — in the prose, pointing at the Placeholders page. (overview SVGs need re-exporting.)
'tokenizes the input' is only true for BBCode; HTML and TextFormatter walk a Nokogiri DOM and MediaWiki is line-based. Say 'reads the input' so the sentence holds for all four. (The unknown-tags-never-raise claim was already accurate for every parser — verified.)
'Migrating to Discourse' sat second in the sidebar, which made the whole site feel like it's only about migration. Move it below the general sections (Format guides, Customization, Concepts) so migration reads as one substantial use case, not the headline. Also drop a stray 'your importer' from the overview intro.
The page leaned on a personified 'importer' as the subject of nearly every sentence. It's already written in second person, so use 'you' / 'your own code' and drop 'importer' entirely (down from ~7 uses to 0). The page still covers migration — that's its job — it just reads less like jargon.
The restructure left no gem-level overview in the nav — Getting Started was doing double duty as overview + quickstart. Add a short Introduction (first nav item): what Markbridge is, the parse → AST → render pipeline with the diagram, the four formats, when to use it, and a map of the docs. Trim Getting Started back to a real quickstart — drop the 'What just happened' pipeline section (now in Introduction + Architecture) and slim 'Where to next' to concrete next steps.
Adding the Introduction page left the migration overview repeating the parse → AST → render pipeline (its own diagram + bullets). Replace the 'Four stages' section with a short 'how a migration uses the pipeline' that links to the Introduction and focuses on the migration-specific plug-in points (handlers in, Tags out, read-back after). Remove the now unused overview diagram (svg + dark + excalidraw).
Lead with the hands-on quickstart; the Introduction (big picture + doc map) sits right after for readers who want orientation.
Markbridge installs fine with 'gem install markbridge' — Bundler is only for the 'bundle add' / Gemfile workflow, so it's optional, not a requirement. The only requirement is Ruby 3.3+.
Format guides, Customization, and Concepts had no page to link to — prose faked it (e.g. 'format guides' pointed at the BBCode guide). Add a short Overview page for each as the group's first sidebar item, and point the Getting Started and Introduction links at the real landings.
The nav, links, and section landing already use the bare format name; the page titles were the last holdout. The '→ Markdown' was repetitive (the whole gem converts to Markdown) and only added marginal SEO. Titles now match the nav: BBCode / HTML / MediaWiki / TextFormatter.
The full Conversion write-up lived only on the migration overview, so general pages (format guides, Getting Started) linked a general type to a migration page. Add a 'Result objects' page under Concepts covering Conversion and Parse, move the explanation there, and repoint the links. The migration overview keeps a one-line bridge to it.
Every 'Supported tags' sub-table now uses Tags | Renders as | AST node (the old Formatting-only 'Output'+'AST node' and the six 'Notes' tables disagreed). Wrap the group tables in a .format-tags div and pin equal column widths via CSS so the stacked tables line up. AST nodes verified against the handler registry.
Same schema and aligned columns across BBCode, HTML, MediaWiki, and TextFormatter: wrap each guide's tag tables in .format-tags for shared column widths, merge the old Output/Notes/Attributes into a clean 'Renders as' (output only — no behavior notes), and add the AST-node column where it was missing (MediaWiki nodes verified against the parser).
The 'Renders as' cleanup dropped a few genuinely useful bits. Add them back as short notes below the tables (not in the cells): BBCode's code language, ordered-list, quote-attribution, and link-form variants; and the attributes each TextFormatter element carries.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This adds a real documentation site for Markbridge, built with Astro Starlight
and deployed to markbridge.dev. So far the only docs were the README and a few
loose Markdown files — this gives the gem a proper home with guides, format
references, and the architecture background.
I built it around the main use case (migrating a forum into Discourse), but kept
the plain "convert markup to Markdown" path as the first thing people see, so it
doesn't come across as a migration-only tool.
What's in it:
TextFormatter).
node + parser handler + renderer Tag, then reading the nodes back off
conversion.astafterwards.handlers.
UPGRADING.mdjust points here), the changelog (generated from GitHubReleases, with
#NNNturned into links), and a link to the API docs onrubydoc.info.
A few things worth pointing out:
spec/docsruns every```rubyblock inthe site, so the examples can't drift from the API. Snippets that can't run
(old-API "before" examples, bare method signatures) are fenced as
```rb—still highlighted, just not executed.
examples/directory. The tested docs snippets replace it,and the standalone files had already drifted from the current API.
on merge to main. Pages is set up for Actions deploy with the markbridge.dev
custom domain.
I also folded in one unrelated fix: the JRuby CI job was failing on
bundle installbecause a Dependabot bump pulledrbs(a native extension)onto JRuby through mutant's dependency chain. I guarded mutant to MRI only, the
same way
commonmarkeralready is.