Skip to content

Honor --canonical-url on every network that has one - #29

Merged
ralyodio merged 1 commit into
mainfrom
feat/canonical-url-everywhere
Sep 6, 2026
Merged

Honor --canonical-url on every network that has one#29
ralyodio merged 1 commit into
mainfrom
feat/canonical-url-everywhere

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

--canonical-url reached dev.to and nowhere else. So --to htmlblog,devto
published the same article twice with no statement about which was the
original, and a Hashnode or Ghost copy had no way to make one at all.

What each network actually calls it

This failed quietly rather than loudly because the field name is different
every time:

network field
dev.to canonical_url (already worked)
Hashnode originalArticleURL
Ghost canonical_url
Tumblr source_url — the attribution link it has instead of a canonical
gitblog canonical: frontmatter key, for the site template to render
htmlblog a real <link rel="canonical"> in the head

WordPress and Micro.blog are deliberately excluded. WordPress core has no
canonical field — it belongs to an SEO plugin's post meta — and Micropub
defines no canonical property. Wiring something plausible there would ship a
flag that looks configured and does nothing.

htmlblog points at itself

The original should confirm what the copies claim, so a page gets a
self-referential canonical from the siteUrl the account logged in with.
--canonical-url overrides it for an article first published elsewhere.

A canonical that does not byte-match the real address points at a different
page, so postUrl() is now the single place that joins siteUrl to a file
name, shared by the canonical, the URL myna reports and the timeline. It trims
trailing slashes, which the old inline concatenation did not — that was a
latent double-slash bug in the reported URL.

The blog-post seam

myna post --to htmlblog does not write the page itself when
profullstack/cli-tools' blog-post is on PATH, and on the dev box it is. So a
tag added only to myna's native writer would never have reached the real blog.
blog-post applies its own siteUrl (profullstack/cli-tools#55) and myna
forwards --canonical only when one was explicitly given. That flag needs
cli-tools 0.28.0.

Verified

  • 242 tests pass, 12 skipped, 11 new
  • The API adapters are driven against a stubbed fetch and the request body read
    back, since the assertion worth making is the field name on the wire. One
    test loops every network claiming support and fails if the URL is missing
    from the request, so adding an adapter and forgetting to wire it breaks.
  • Also covered: HTML escaping of the URL, the no-siteUrl case emitting no tag,
    frontmatter quoting, and trailing-slash joining
  • tsc --noEmit clean

Version bumped to 0.12.0 across the packages that track core. The plugin
packages sit at 0.8.4 on main already; that drift is pre-existing and untouched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GNpSGiTnfodDyCQ3MU6Nsk

`--canonical-url` reached dev.to and nowhere else. Publishing the same article
to `--to htmlblog,devto` therefore left two copies with no statement about
which was the original, and a Hashnode or Ghost copy had no way to say at all.

Now every long-form adapter that has a field for it sends one, and the field
name differs every time, which is exactly why this failed quietly rather than
loudly: dev.to `canonical_url`, Hashnode `originalArticleURL`, Ghost
`canonical_url`, Tumblr `source_url` (the attribution link it has instead of a
canonical), gitblog a `canonical:` frontmatter key, htmlblog a real
`<link rel="canonical">`.

WordPress and Micro.blog are deliberately not in that list. WordPress core has
no canonical field, only an SEO plugin's post meta, and Micropub defines no
canonical property. Faking either would post something that looks configured
and does nothing.

htmlblog points a page at itself from the siteUrl it was logged in with, since
the original should confirm what the copies claim; an explicit --canonical-url
overrides it for an article first published elsewhere. That URL has to byte
match the address the post is actually served at, so postUrl() is now the one
place that joins siteUrl to a file name, used by the canonical, the link myna
reports and the timeline alike. It trims trailing slashes, which the old
inline concatenation did not.

When cli-tools' blog-post writes the page instead, it applies its own siteUrl
and myna forwards the flag only when one was given. That flag needs cli-tools
0.28.0.

242 tests pass, 11 new. The API adapters are driven against a stubbed fetch and
the request body read back, because the assertion worth making is the field
name on the wire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNpSGiTnfodDyCQ3MU6Nsk
@ralyodio
ralyodio merged commit 671bd62 into main Sep 6, 2026
2 checks 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