Skip to content

Syndicate Hugo blog posts to dev.to and Hashnode#4872

Open
shai-almog wants to merge 5 commits intomasterfrom
blog-syndication
Open

Syndicate Hugo blog posts to dev.to and Hashnode#4872
shai-almog wants to merge 5 commits intomasterfrom
blog-syndication

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

Summary

  • Adds a daily GitHub Action (.github/workflows/blog-syndication.yml) that syndicates new blog posts to dev.to and Hashnode with a canonical_url / originalArticleURL back to www.codenameone.com.
  • New script scripts/website/syndicate_blog_posts.py (Python stdlib only) picks the oldest post dated after 2026-04-30, at least 7 days old, and not yet syndicated to a given platform; absolutizes relative links/images; and inserts a one-sentence "What is Codename One" blurb right after the fold.
  • New committed state file scripts/website/syndication-state.json tracks per-slug, per-platform results so partial failures retry only the failed side. Action commits state updates back to master.

Setup

Repo secrets required (already added):

  • DEVTO_API_KEY
  • HASHNODE_TOKEN
  • HASHNODE_PUBLICATION_ID

The workflow runs daily at 13:00 UTC and supports workflow_dispatch with a dry-run toggle. The first eligible post is liquid-glass-material-3-modern-native-themes (2026-05-01), which becomes a candidate from 2026-05-08 onward.

Test plan

  • python3 scripts/website/syndicate_blog_posts.py --dry-run on 2026-05-06 / 2026-05-07 reports no candidate.
  • --dry-run --today 2026-05-08 selects the May 1 post.
  • Floor correctly excludes the 2026-04-24 post.
  • State-based filter retries an unsyndicated platform when the other already succeeded.
  • Manually trigger the workflow with dry_run=true after merge to confirm secrets are wired.
  • Let one real run go through and confirm canonical link + blurb on both platforms.

🤖 Generated with Claude Code

Daily GitHub Action that picks the oldest blog post under
docs/website/content/blog dated after 2026-04-30, at least 7 days old, and
not yet syndicated to a given platform. The script absolutizes relative
links/images, inserts a one-sentence "What is Codename One" blurb after the
fold, and POSTs to each platform with canonical_url pointing back to the
original on www.codenameone.com. Per-platform state in
scripts/website/syndication-state.json so partial failures retry only the
failed side.

Requires repo secrets: DEVTO_API_KEY, HASHNODE_TOKEN, HASHNODE_PUBLICATION_ID.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

Cloudflare Preview

shai-almog and others added 4 commits May 6, 2026 12:20
Adds foojay.io as a third syndication target. Unlike dev.to and Hashnode
the foojay flow creates a WP draft via /wp-json/wp/v2/posts so the foojay
editors can review before publishing. The canonical link is surfaced as a
visible note at the top of the draft (rather than a meta field) so the
reviewer can wire it up using whichever SEO plugin foojay runs.

Side effects:
- platforms with missing credentials are now skipped at startup with a note
  instead of failing the whole run, so adding a new platform later does not
  strand the candidate selector
- requests now send a real User-Agent and Accept header (Cloudflare in
  front of foojay rejected the default Python-urllib UA with error 1010)
- foojay credentials (FOOJAY_USER / FOOJAY_PASSWORD) wired through the
  workflow as optional secrets; the script auto-skips foojay until both
  are configured

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
foojay runs Yoast SEO, so the canonical URL is now sent as
meta._yoast_wpseo_canonical on the WP draft. Yoast registers that key as
a REST-exposed post meta, so the standard /wp-json/wp/v2/posts payload
carries it through. The visible "originally published" line at the top
of the draft body is dropped — Yoast handles the SEO directive and the
"What is Codename One" blurb still provides reader-facing attribution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
foojay.io has Wordfence configured to disable WordPress Application
Passwords, so there is no usable Basic Auth path for the WP REST API
from the syndication script. Removing the foojay code path until / unless
foojay editorial offers an alternative auth method (JWT, per-user API
key, etc.). The User-Agent header and skip-when-unconfigured behaviour
introduced alongside the foojay work are kept — they are useful for the
remaining platforms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds scripts/website/syndicate_browser_posts.py — a Playwright-based
counterpart to the API syndicator. Each target site has its own adapter
(login + draft submission). State and post selection are shared with the
API script via syndication-state.json, so a post is "candidate" until all
configured platforms — API and browser — have a record.

Adapters:

- foojay: hybrid path. Playwright drives wp-login.php to obtain a real
  session (Wordfence has Application Passwords disabled, so token auth is
  out), then the script POSTs the draft via /wp-json/wp/v2/posts using
  the session cookies + X-WP-Nonce. Pure UI submission was attempted but
  Cloudflare in front of foojay challenges form POSTs and drops the
  payload, so drafts never landed. Yoast canonical isn't REST-writable
  on this Yoast install, so the canonical is surfaced as a visible note
  at the top of the draft body for the editor reviewer. Validated end-
  to-end against the live site (draft #123656).

- hackernoon, dzone, medium: standard browser flow. Selectors are
  best-effort and need a one-time validation pass against each live site
  via --validate-only --headed. medium has no password login, so it
  relies on a base64-encoded MEDIUM_STORAGE_STATE secret exported from a
  manually logged-in browser session.

Workflow additions:

- Detects whether any browser-syndication secret is configured; only
  installs Playwright + Chromium when something will actually run.
- Uploads the Playwright screenshot directory as a CI artifact on any
  outcome (kept for 14 days), so selector failures are debuggable.
- Screenshots dir is gitignored.

Per-platform secrets (all optional; missing = platform skipped):
  FOOJAY_USER, FOOJAY_PASSWORD
  HACKERNOON_USER, HACKERNOON_PASSWORD
  DZONE_USER, DZONE_PASSWORD
  MEDIUM_STORAGE_STATE

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
try:
field = _find_first(page, self.CANONICAL_FIELD_SELECTORS, timeout=3000)
field.fill(ctx.post.canonical_url)
except AdapterError:
try:
field = _find_first(page, self.CANONICAL_SELECTORS, timeout=3000)
field.fill(ctx.post.canonical_url)
except AdapterError:
field.fill(ctx.post.canonical_url)
# Close the settings panel
page.keyboard.press("Escape")
except AdapterError:
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