Zenn (Japanese) + dev.to (English) dual-publishing blog platform.
Write articles in either language, bidirectionally sync translations with Claude Code /sync skill, and publish to both platforms.
Write/edit article (JP or EN)
↓
/sync ← Claude Code translates and syncs bidirectionally
↓
articles/ + articles_en/ updated
↓
Preview both ← Zenn (JP) localhost:18000 + dev.to (EN) localhost:13000
↓
git push
├→ Zenn auto-publish (GitHub integration)
└→ GitHub Actions → validate → dev.to API publishes EN version
- Node.js 20+
- Claude Code (for translation sync)
- dev.to API key
npm installCreate a .env file in the project root:
DEV_TO_API_KEY=...
Add DEV_TO_API_KEY to your repository's GitHub Secrets for the GitHub Actions workflow.
Link this repository (asherish/blog, main branch) at Zenn deploy settings.
npm run new:articleEdit the generated file in articles/ (Japanese) or articles_en/ (English). Set published: true in the frontmatter when ready.
Use the Claude Code /sync skill:
/sync # Sync all changed articles
/sync my-article # Sync a specific slug
/sync --prefer ja # Resolve conflicts using JP as source
/sync --prefer en # Resolve conflicts using EN as source
The sync skill detects which side changed and translates accordingly:
| Scenario | Action |
|---|---|
| JP only exists | Full translate JP → EN |
| EN only exists | Full translate EN → JP |
| JP changed | Diff sync JP → EN |
| EN changed | Diff sync EN → JP |
| Both changed | Conflict — use --prefer ja or --prefer en to resolve |
| Neither changed | Skip |
You can also run detection only (without translation) via:
npm run sync # Detect changes (JSON output)
npm run sync -- my-article # Detect for a specific slugnpm run preview # Zenn preview at localhost:18000
npm run preview:devto # dev.to preview at localhost:13000Port 18000 and 13000 are used instead of the conventional 8000 / 3000 to avoid conflicts with other dev servers (Next.js, Express, Django, etc.). The rule is simple: original port + 10000.
git add -A && git commit -m "Add new article" && git push- Zenn publishes automatically via GitHub integration (Zenn does not provide a publishing API — it polls the linked repository and picks up articles directly)
- GitHub Actions validates published status, then publishes the English version to dev.to via the dev.to REST API
Add scheduled_publish_date to frontmatter of both JP and EN articles:
published: false
scheduled_publish_date: "2026-03-15"A GitHub Actions cron job runs daily at 00:05 JST. When the current date reaches the scheduled date, it sets published: true in both articles, removes scheduled_publish_date, publishes the English version to dev.to, commits, and pushes. Zenn picks up the change automatically via GitHub integration.
To check scheduled status locally:
npm run schedule:checkDesign note: Zenn natively supports scheduled publishing via published: true + published_at, but this requires setting published: true in the Zenn article upfront. Since dev.to has no equivalent — setting published: true would publish immediately — using Zenn's native scheduling would break the JP/EN published status consistency that our validation enforces. Instead, we use a unified scheduled_publish_date field (kept false on both sides until the cron flips them simultaneously).
blog/
├── .claude/
│ ├── settings.json # Permission auto-allow for sync scripts
│ └── skills/
│ ├── zenn-syntax.md # Zenn Markdown syntax skill
│ ├── devto-syntax.md # dev.to Markdown syntax skill
│ └── sync/SKILL.md # Bidirectional translation sync skill (/sync)
├── .github/workflows/
│ ├── publish-to-devto.yml # Validate + publish to dev.to on push
│ └── scheduled-publish.yml # Daily cron to publish scheduled articles
├── articles/ # Zenn articles (Japanese)
├── articles_en/ # Translated articles (English, for dev.to)
├── books/ # Zenn books
├── scripts/
│ ├── sync-detect.ts # Change detection script (JSON output)
│ ├── sync-apply.ts # Post-translation processing script
│ ├── sync/
│ │ ├── convert.ts # Zenn ↔ dev.to syntax conversion
│ │ └── state.ts # Sync state & mapping persistence
│ ├── publish-to-devto.ts # dev.to publishing script
│ ├── process-scheduled.ts # Scheduled publish processor
│ ├── validate-published.ts # Pre-publish validation
│ └── preview-devto.ts # dev.to preview server
├── .sync-state.json # Per-article hash tracking for sync
├── .devto-mapping.json # dev.to article ID tracking
├── package.json
└── tsconfig.json
| Script | Description |
|---|---|
npm run new:article |
Create a new Zenn article scaffold |
npm run preview |
Start Zenn preview server (localhost:18000) |
npm run preview:devto |
Start dev.to preview server (localhost:13000) |
npm run sync |
Detect translation changes (JSON output) |
npm run sync:apply |
Apply post-translation processing |
npm run schedule:check |
Check and process scheduled articles |
npm run validate |
Validate published status consistency |
npm run publish:devto |
Manually publish to dev.to |
This project includes custom Claude Code skills for platform-specific Markdown syntax and translation:
| Skill | Trigger | Description |
|---|---|---|
sync |
/sync command |
Bidirectional translation sync between JP ↔ EN |
zenn-syntax |
Editing articles/**/*.md |
Zenn Markdown syntax reference (message boxes, accordions, embeds, etc.) |
devto-syntax |
Editing articles_en/**/*.md |
dev.to Liquid tag syntax reference (details, katex, embeds, etc.) |
Skills are automatically activated when working with files in the corresponding directories. The sync skill is invoked manually via /sync.
- Scheduled publishing uses
scheduled_publish_datein frontmatter — a daily cron job auto-publishes when the date arrives - Articles with
published: falseare skipped during publishing (sync works regardless of published status) - Sync uses SHA-256 hashing for change detection — unchanged articles are not re-translated
- Diff sync only updates changed sections, preserving manual edits in the target language
canonical_urlis automatically set to the Zenn article URL- dev.to tags are limited to 4 (dev.to platform restriction)
- Images should use absolute URLs for cross-platform compatibility