|
| 1 | +--- |
| 2 | +name: crossposting |
| 3 | +description: Crosspost Wasp blog articles (MDX) to DEV.to and Medium. |
| 4 | +--- |
| 5 | + |
| 6 | +# MDX Conversion Script |
| 7 | + |
| 8 | +Converts a Wasp blog MDX file to clean markdown, HTML, and Medium-ready chunks. Output is saved to `.claude/skills/crossposting/scripts/output/`. |
| 9 | + |
| 10 | +```bash |
| 11 | +npx tsx .claude/skills/crossposting/scripts/convert-mdx.ts <path-to-mdx-file> [--publish-devto] [--update-devto <id>] [--upload-videos] |
| 12 | +``` |
| 13 | + |
| 14 | +**Output files (always written):** |
| 15 | +- `<slug>.md` — clean markdown (for DEV.to) |
| 16 | +- `<slug>.html` — HTML (for Medium preview/reference) |
| 17 | +- `<slug>-medium-chunks.json` — `{ title: string, chunks: string[] }` pre-split at `<h2>` boundaries, pre-escaped for JS template literals |
| 18 | + |
| 19 | +**Flags:** |
| 20 | +- `--publish-devto` — POST as draft to DEV.to (requires `DEVTO_API_KEY`) |
| 21 | +- `--update-devto <id>` — PUT to existing DEV.to article (requires `DEVTO_API_KEY`) |
| 22 | +- `--upload-videos` — upload local .mp4s to YouTube (unlisted), embed as `{% youtube URL %}` liquid tags |
| 23 | + |
| 24 | +**Env vars:** `DEVTO_API_KEY` (from https://dev.to/settings/extensions), `YOUTUBE_CLIENT_ID` + `YOUTUBE_CLIENT_SECRET` (saved in 1password for the users). |
| 25 | + |
| 26 | +**Notes:** |
| 27 | +- `canonical_url` is auto-generated from MDX filename → `wasp.sh/blog/...` |
| 28 | +- Without `--upload-videos`, local .mp4 videos become plain links |
| 29 | + |
| 30 | +### YouTube Setup (one-time, human steps) |
| 31 | + |
| 32 | +1. Create Google Cloud project → enable YouTube Data API v3 → create OAuth 2.0 Desktop credentials |
| 33 | +2. Add `YOUTUBE_CLIENT_ID` and `YOUTUBE_CLIENT_SECRET` to `~/.zshrc` |
| 34 | +3. Run `npx tsx .claude/skills/crossposting/scripts/upload-youtube.ts --auth` → authorize in browser (select @wasplang channel if prompted) |
| 35 | +4. Refresh token stored in `~/.youtube-upload-tokens.json` |
| 36 | +5. Check channel: `npx tsx .claude/skills/crossposting/scripts/upload-youtube.ts --whoami`. Wrong channel? Delete token file and re-auth. |
| 37 | + |
| 38 | +--- |
| 39 | + |
| 40 | +# Crossposting to Dev.to |
| 41 | + |
| 42 | +Use `--publish-devto` or `--update-devto <id>` flags. Requires `DEVTO_API_KEY`. |
| 43 | + |
| 44 | +--- |
| 45 | + |
| 46 | +# Crossposting to Medium |
| 47 | + |
| 48 | +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 `info@wasp-lang.dev`. |
| 49 | + |
| 50 | +## Steps |
| 51 | + |
| 52 | +### 1. Convert the MDX article |
| 53 | + |
| 54 | +Run the conversion script, if it hasn't been run yet (see usage above). |
| 55 | + |
| 56 | +**Images:** Medium does not support webp. Convert webp images to jpg for later manual upload: |
| 57 | +```bash |
| 58 | +for f in static/img/<slug>/*.webp; do sips -s format jpeg "$f" --out ".claude/skills/crossposting/scripts/output/$(basename "${f%.webp}.jpg")"; done |
| 59 | +``` |
| 60 | + |
| 61 | +### 2. Navigate to `https://medium.com/new-story` |
| 62 | + |
| 63 | +The human user must login to Medium with the `info@wasp-lang.dev` email and get the OTP sent to your email. |
| 64 | + |
| 65 | +### 3. Set the article title |
| 66 | + |
| 67 | +**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`. |
| 68 | + |
| 69 | +1. Read `setMediumTitle` from `scripts/medium-helpers.js`. |
| 70 | +2. Replace `TITLE_PLACEHOLDER` with the actual title (escape backticks and `${` sequences). |
| 71 | +3. Call `evaluate_script` with the resulting function string. |
| 72 | + |
| 73 | +Then press `Enter` to move cursor to the body area. |
| 74 | + |
| 75 | +### 4. Paste the article body |
| 76 | + |
| 77 | +**IMPORTANT:** Do NOT use `document.execCommand` — it bypasses Medium's internal state and causes save errors. Always use synthetic `ClipboardEvent` paste. |
| 78 | + |
| 79 | +**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. |
| 80 | + |
| 81 | +1. Read `output/<slug>-medium-chunks.json` — chunks are ready to paste as-is. |
| 82 | +2. Click the body paragraph to focus it. |
| 83 | +3. For each chunk, use `evaluate_script` with the `pasteMediumChunk` function from `scripts/medium-helpers.js`. Replace `HTML_CHUNK_PLACEHOLDER` directly with the chunk string content. |
| 84 | +4. Wait for "Saved" status after all chunks are pasted. |
| 85 | + |
| 86 | +### 5. Verify |
| 87 | + |
| 88 | +- Snapshot the article to confirm it looks correct and shows "Draft" / "Saved" |
| 89 | +- Scroll through to confirm section order matches the original and make any necessary adjustments. |
| 90 | + |
| 91 | +### 6. Before Publishing (human steps) |
| 92 | + |
| 93 | +- Replace `[IMAGE: filename.jpg]` markers with actual image uploads in Medium's editor |
| 94 | +- Add any YouTube embeds that didn't auto-embed (Medium auto-embeds standalone YouTube URLs in `<p>` tags) |
| 95 | +- Add the banner image and canonical URL |
| 96 | +- Review and publish from Medium's UI |
0 commit comments