Build the dev.to syndication upsert #103

Open
opened 2026-08-10 20:09:17 +00:00 by coilyco-ops · 1 comment
Collaborator

Context

The goal is programmatic sync of promoted posts to dev.to that can be re-run at will, so an edit on coilysiren.me propagates rather than creating a duplicate.

dev.to runs Forem, whose v1 API supports both create and update, which is the property that eliminates most alternative platforms. Medium stopped issuing integration tokens in 2025 and cannot be scripted. Substack has no public write API.

  • API reference: https://developers.forem.com/api/v1
  • POST /api/articles creates, PUT /api/articles/{id} updates, GET /api/articles/me lists.
  • Auth is an api-key header.
  • The article payload accepts canonical_url, which keeps coilysiren.me the SEO original.

Required outcome

A ward-invocable script that pushes the promoted set to dev.to idempotently. Running it twice in a row produces no second copy and no spurious edit.

Acceptance criteria

  • A new script, invoked through a new .ward/ward.yaml verb declared before first use.
  • The script reads the promotion flag as its allowlist. It never publishes an unflagged post.
  • Every published article sets canonical_url to the https://coilysiren.me/posts/<slug>/ original.
  • Idempotency is keyed on canonical_url matched against GET /api/articles/me, not on title. Retitling a post must update rather than duplicate.
  • A dry-run mode prints the create and update plan without calling the write endpoints. This is the default.
  • The API key is read from SSM, never from a tracked file, and never appears in argv or logs.
  • Relative links and image paths in post bodies are rewritten to absolute https://coilysiren.me URLs. The Terraform post's HTML author-note comment must not break the dev.to renderer.
  • Markdown that renders correctly on the site also renders correctly on dev.to, verified against at least the Terraform post's fenced HCL blocks.
  • Forgejo Actions wiring is deferred until at least one successful manual run.

Sequencing

Blocked on the promotion-flag issue, which supplies both the allowlist and the indexable canonical target. Publishing while the canonical target is noindex would hand dev.to the indexed copy.

Human checkpoint

First live publish is externally visible and creates public artifacts under Kai's name. Kai approves the dry-run plan before the first write.

Non-goals

  • Hashnode and Bluesky. Tracked separately.
  • Syncing dev.to comments or reactions back to the site.
  • Deleting remote articles when a post is demoted. Unflagging stops updates and leaves the remote copy alone.
## Context The goal is programmatic sync of promoted posts to dev.to that can be re-run at will, so an edit on coilysiren.me propagates rather than creating a duplicate. dev.to runs Forem, whose v1 API supports both create and update, which is the property that eliminates most alternative platforms. Medium stopped issuing integration tokens in 2025 and cannot be scripted. Substack has no public write API. * API reference: https://developers.forem.com/api/v1 * `POST /api/articles` creates, `PUT /api/articles/{id}` updates, `GET /api/articles/me` lists. * Auth is an `api-key` header. * The article payload accepts `canonical_url`, which keeps coilysiren.me the SEO original. ## Required outcome A ward-invocable script that pushes the promoted set to dev.to idempotently. Running it twice in a row produces no second copy and no spurious edit. ## Acceptance criteria * A new script, invoked through a new `.ward/ward.yaml` verb declared before first use. * The script reads the promotion flag as its allowlist. It never publishes an unflagged post. * Every published article sets `canonical_url` to the `https://coilysiren.me/posts/<slug>/` original. * Idempotency is keyed on `canonical_url` matched against `GET /api/articles/me`, not on title. Retitling a post must update rather than duplicate. * A dry-run mode prints the create and update plan without calling the write endpoints. This is the default. * The API key is read from SSM, never from a tracked file, and never appears in argv or logs. * Relative links and image paths in post bodies are rewritten to absolute `https://coilysiren.me` URLs. The Terraform post's HTML author-note comment must not break the dev.to renderer. * Markdown that renders correctly on the site also renders correctly on dev.to, verified against at least the Terraform post's fenced HCL blocks. * Forgejo Actions wiring is deferred until at least one successful manual run. ## Sequencing Blocked on the promotion-flag issue, which supplies both the allowlist and the indexable canonical target. Publishing while the canonical target is `noindex` would hand dev.to the indexed copy. ## Human checkpoint First live publish is externally visible and creates public artifacts under Kai's name. Kai approves the dry-run plan before the first write. ## Non-goals * Hashnode and Bluesky. Tracked separately. * Syncing dev.to comments or reactions back to the site. * Deleting remote articles when a post is demoted. Unflagging stops updates and leaves the remote copy alone.
Author
Collaborator

Frontend seat. This is unblocked now, and it is not mine to build. Handing it over with the corrections it needs, and with my half already delivered.

Unblocked

The dependency is cleared. #97 landed the promotion flag, so the allowlist exists, and #99 landed the feed. The three promoted posts are indexable, so the sequencing risk this issue names, "publishing while the canonical target is noindex would hand dev.to the indexed copy", no longer applies.

Why I am handing it over rather than building it

The script is an authenticated API client with SSM secret handling and idempotency state. That is foundational software, which the frontend seat defers to the platform seat. Publishing Kai's writing to dev.to is communication addressed outward, which is the Developer Advocate's. Neither is mine, and Kai confirmed the split.

What was mine is done: the promoted flag this reads as its allowlist, and the canonical target it must match.

Three corrections before anyone builds it

1. The canonical host is wrong, and it matters most here. The acceptance says https://coilysiren.me/posts/<slug>/, twice. The site declares:

<link rel="canonical" href="https://www.coilysiren.me/posts/stochastic-design-iteration/">

The apex 301s to www. If dev.to's canonical_url names the apex while the site names www, the two disagree and every syndicated article points through a redirect. That is exactly the defect #123 item 2 existed to fix, and this issue's whole SEO argument rests on the canonical being right.

Use https://www.coilysiren.me/posts/<slug>/. Better still, read it from the post's own computed canonical rather than rebuilding the string, so it cannot drift again.

2. .ward/ward.yaml verbs are retired. The acceptance says to declare a new verb there before first use. That file carries catalog metadata only now, and dev verbs live in the justfile. The verb goes there.

3. One acceptance criterion has nothing to act on. "Relative links and image paths in post bodies are rewritten to absolute URLs" is worth keeping as a guard, but all three promoted posts currently contain zero relative links or images. Do not build an elaborate rewriter for a case that does not exist yet.

The other half of that criterion is real. The Terraform post does carry an HTML comment at line 55:

<!-- author note: my queendom for an HCL syntax highlighter... -->

That one needs handling against the dev.to renderer.

What stays correct, and is worth keeping

Two design choices here are better than the obvious version and should survive the rewrite:

  • Idempotency keyed on canonical_url, matched against GET /api/articles/me, not on title. Title is the obvious key and the wrong one, because retitling a post would duplicate rather than update.
  • Dry-run as the default, with the first live publish gated on Kai. It creates public artifacts under her name.

The platform survey is also worth not re-running: Forem's v1 API supports create and update, Medium stopped issuing integration tokens in 2025, and Substack has no public write API. That is why this is dev.to and not somewhere else.

Worth a moment before building

Kai closed #98 today on the grounds that a three-post archive, two of them years old, was not worth advertising. The same arithmetic applies to what this syndicates:

2026-04-19   stochastic-design-iteration       4 months
2023-10-11   azure-openai-terraform            ~3 years
2020-01-12   on-permissions-models             ~6 years

She chose to keep this one moving anyway, and nothing else is blocked on it either way. Flagging it so the implementer knows the reach is thin today and the value grows with new writing rather than with this batch.

Frontend seat. **This is unblocked now, and it is not mine to build.** Handing it over with the corrections it needs, and with my half already delivered. ## Unblocked The dependency is cleared. `#97` landed the promotion flag, so the allowlist exists, and `#99` landed the feed. The three promoted posts are indexable, so the sequencing risk this issue names, "publishing while the canonical target is `noindex` would hand dev.to the indexed copy", no longer applies. ## Why I am handing it over rather than building it The script is an authenticated API client with SSM secret handling and idempotency state. That is foundational software, which the frontend seat defers to the platform seat. Publishing Kai's writing to dev.to is communication addressed outward, which is the Developer Advocate's. Neither is mine, and Kai confirmed the split. What was mine is done: the `promoted` flag this reads as its allowlist, and the canonical target it must match. ## Three corrections before anyone builds it **1. The canonical host is wrong, and it matters most here.** The acceptance says `https://coilysiren.me/posts/<slug>/`, twice. The site declares: ``` <link rel="canonical" href="https://www.coilysiren.me/posts/stochastic-design-iteration/"> ``` The apex 301s to `www`. If dev.to's `canonical_url` names the apex while the site names `www`, the two disagree and every syndicated article points through a redirect. That is exactly the defect `#123` item 2 existed to fix, and this issue's whole SEO argument rests on the canonical being right. **Use `https://www.coilysiren.me/posts/<slug>/`.** Better still, read it from the post's own computed `canonical` rather than rebuilding the string, so it cannot drift again. **2. `.ward/ward.yaml` verbs are retired.** The acceptance says to declare a new verb there before first use. That file carries catalog metadata only now, and dev verbs live in the [`justfile`](../justfile). The verb goes there. **3. One acceptance criterion has nothing to act on.** "Relative links and image paths in post bodies are rewritten to absolute URLs" is worth keeping as a guard, but all three promoted posts currently contain **zero** relative links or images. Do not build an elaborate rewriter for a case that does not exist yet. The other half of that criterion is real. The Terraform post does carry an HTML comment at line 55: ``` <!-- author note: my queendom for an HCL syntax highlighter... --> ``` That one needs handling against the dev.to renderer. ## What stays correct, and is worth keeping Two design choices here are better than the obvious version and should survive the rewrite: * **Idempotency keyed on `canonical_url`, matched against `GET /api/articles/me`, not on title.** Title is the obvious key and the wrong one, because retitling a post would duplicate rather than update. * **Dry-run as the default**, with the first live publish gated on Kai. It creates public artifacts under her name. The platform survey is also worth not re-running: Forem's v1 API supports create and update, Medium stopped issuing integration tokens in 2025, and Substack has no public write API. That is why this is dev.to and not somewhere else. ## Worth a moment before building Kai closed `#98` today on the grounds that a three-post archive, two of them years old, was not worth advertising. The same arithmetic applies to what this syndicates: ``` 2026-04-19 stochastic-design-iteration 4 months 2023-10-11 azure-openai-terraform ~3 years 2020-01-12 on-permissions-models ~6 years ``` She chose to keep this one moving anyway, and nothing else is blocked on it either way. Flagging it so the implementer knows the reach is thin today and the value grows with new writing rather than with this batch.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilysiren/website#103
No description provided.