Crosspost Wasp blog articles (MDX) to DEV.to and Medium.
Converts a Wasp blog MDX file to clean markdown, HTML, and Medium-ready chunks. Output is saved to .claude/skills/crossposting/scripts/output/.
npx tsx .claude/skills/crossposting/scripts/convert-mdx.ts <path-to-mdx-file> [--publish-devto] [--update-devto <id>] [--upload-videos]
Output files (always written):
<slug>.md — clean markdown (for DEV.to)<slug>.html — HTML (for Medium preview/reference)<slug>-medium-chunks.json — { title: string, chunks: string[] } pre-split at <h2> boundaries, pre-escaped for JS template literalsFlags:
--publish-devto — POST as draft to DEV.to (requires DEVTO_API_KEY)--update-devto <id> — PUT to existing DEV.to article (requires DEVTO_API_KEY)--upload-videos{% youtube URL %}Env vars: DEVTO_API_KEY (from https://dev.to/settings/extensions), YOUTUBE_CLIENT_ID + YOUTUBE_CLIENT_SECRET (saved in 1password for the users).
Notes:
canonical_url is auto-generated from MDX filename → wasp.sh/blog/...--upload-videos, local .mp4 videos become plain linksYOUTUBE_CLIENT_ID and YOUTUBE_CLIENT_SECRET to ~/.zshrcnpx tsx .claude/skills/crossposting/scripts/upload-youtube.ts --auth → authorize in browser (select @wasplang channel if prompted)~/.youtube-upload-tokens.jsonnpx tsx .claude/skills/crossposting/scripts/upload-youtube.ts --whoami. Wrong channel? Delete token file and re-auth.Use --publish-devto or --update-devto <id> flags. Requires DEVTO_API_KEY.
Paste HTML into Medium's editor via Chrome DevTools MCP. The user must be logged in to Medium using email login (not google oauth) with [email protected].
Run the conversion script, if it hasn't been run yet (see usage above).
Images: Medium does not support webp. Convert webp images to jpg for later manual upload:
for f in static/img/<slug>/*.webp; do sips -s format jpeg "$f" --out ".claude/skills/crossposting/scripts/output/$(basename "${f%.webp}.jpg")"; done
https://medium.com/new-storyThe human user must login to Medium with the [email protected] email and get the OTP sent to your email.
Do NOT use the fill tool — it puts text into the body paragraph instead. Use evaluate_script with the setMediumTitle function from scripts/medium-helpers.js.
setMediumTitle from scripts/medium-helpers.js.TITLE_PLACEHOLDER with the actual title (escape backticks and ${ sequences).evaluate_script with the resulting function string.Then press Enter to move cursor to the body area.
IMPORTANT: Do NOT use document.execCommand — it bypasses Medium's internal state and causes save errors. Always use synthetic ClipboardEvent paste.
IMPORTANT: Chunks are already pre-escaped for JS template literals and pre-processed for Medium (YouTube URLs, image placeholders). Inline each chunk string directly into the pasteMediumChunk function body where HTML_CHUNK_PLACEHOLDER appears. Do NOT base64-encode, store chunks in page variables, or add any intermediate encoding steps.
output/<slug>-medium-chunks.json — chunks are ready to paste as-is.evaluate_script with the pasteMediumChunk function from scripts/medium-helpers.js. Replace HTML_CHUNK_PLACEHOLDER directly with the chunk string content.[IMAGE: filename.jpg] markers with actual image uploads in Medium's editor<p> tags)