Open-source project
fxyadela/write-then-publish avatar
fxyadela/write-then-publish

write-then-publish: Markdown to Xiaohongshu cards and WeChat articles in one workspace

本地优先的中文内容排版工具:把 Markdown 转成小红书图文卡片、公众号长文和可下载图片,并支持 Obsidian 工作流。

555 stars62 forksJavaScriptNOASSERTION

At a glance

What is it?
A local-first Chinese content layout tool that turns one Markdown body into image cards, WeChat long-form articles and downloadable PNG/ZIP output, with an Obsidian round trip. Here is what it actually does, how to run it, and where it stops.
Who is it for?
Adopt write-then-publish if you already write in Markdown, publish to Xiaohongshu or WeChat, and want the layout step inside one page instead of three tools. Skip it if you need a permissive commercial licence, if you publish in English on Western platforms, or if you need Live Photo export on browsers without WebCodecs and refuse the cloud fallback path.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The last mile after writing, not the writing itself

The README is explicit that the tool does not write content for you. It targets the step after the draft exists: splitting one body into a paginated image card set, a long-form article that keeps Markdown structure, and a downloadable image bundle. That is a real gap for people who write in Markdown but publish on platforms that accept neither Markdown nor a single long image.

The audience is narrow and identifiable. Chinese-language creators posting to Xiaohongshu and WeChat, Obsidian users who keep images as `![[image.png]]` wiki references, and anyone who currently copies the same paragraph into a card maker, then into an editor, then exports images by hand. If you publish only to a platform that renders Markdown directly, this tool has nothing to add.

One body, two renderers, and a page-numbered export

The architecture is a single-page application in vanilla HTML, CSS and JavaScript. There is no framework and no build step in the listed stack; the rendering work is Canvas 2D, with html2canvas for long-image capture, JSZip for bundling, and mp4box.js plus mp4-muxer for the video path. Supabase handles accounts and cloud drafts.

The data flow described in the README is: paste or import Markdown, pick a mode, insert media, preview, export. Switching between image-card mode and article mode changes the output form only; the README states it does not rewrite the body. Card output is a 1728 x 2304 PNG with body text at size 34 by default. Batch export produces a flat, page-numbered ZIP rather than a nested tree, and the README shows the shape:

text
01-图片.png
02-图片.png
03-实况.pvt        # 包内含配对好的 JPG / MOV / metadata.plist
04-图片.png

Live Photo pages sit at the same level as ordinary images, with the JPG, MOV and metadata.plist paired inside the .pvt. That flat layout is a deliberate choice: it keeps the page order readable after unzipping, and it avoids a separate folder that would break the sequence.

Running it locally and exporting your first card set

The README gives two entry points. The quickest is opening index.html directly, which the capability matrix marks as sufficient for cards, long articles, cropping, single PNG, batch ZIP and long images. The second is the local server, which the README documents with these commands. After `npm start` the terminal window must stay open, and the page is served at 127.0.0.1:5173.

bash
git clone https://github.com/fxyadela/write-then-publish.git
cd write-then-publish
npm start

On macOS there is also a double-clickable `启动写了就发.command`. For a no-account session, `启动写了就发本地版.command` opens `http://127.0.0.1:5173/?mode=local`, which the README says does not load Supabase and keeps drafts only in that Mac's browser local storage.

For a first real run, paste a Markdown body into the left editor, select image-card mode, and insert an image by upload, paste, drag or batch import. In the right preview you can drag position, change alignment and resize, and the README states images are redrawn at full resolution rather than following the text canvas down in quality. Then export a single PNG or the batch ZIP. If you want the two-image collage, select an image in the preview and use the plus control on its right edge; the README gives the underlying syntax as `[[image:left|right|ratio]]`, and deleting `|right` splits it back into a single image.

Live Photo export is the sharpest edge in the project

The video path is where the constraints concentrate. Duration is offered in three fixed steps, 3, 5 and 8 seconds, and the README states these are not tied to any publishing platform. Speed is original, 1.5x, 2x or 3x. The interaction between the two is the part worth understanding: at higher speed the final duration does not change, the tool instead pulls a longer stretch of source footage, so the selection box widens as speed rises.

Several hard limits follow. Audio is muted on any sped-up export because resampling is needed to keep it aligned, and the sound toggle is greyed out in that case; original speed is unaffected. Options the source footage cannot fill are struck through, and clicking one reports how many seconds short you are without changing your current selection. When the source clip is shorter than the range being requested, the segment timeline hides itself.

Generation happens in the browser via WebCodecs. The README is candid that "the original video does not leave the device" applies only to that local path: signed cloud drafts back up project assets separately, and browsers without WebCodecs may fall back to a private cloud job on the hosted version. The documented handoff is a ZIP, unzip, AirDrop the whole .pvt to an iPhone, then long-press in Photos to confirm the motion. Splitting the JPG and MOV apart risks losing the Apple pairing. The README also notes that H.264 must be re-encoded because the frame is composited into the card, while the audio track is carried over unchanged where compatible.

Obsidian sync, and what happens when the browser says no

The Obsidian integration reads `![[image.png]]`, `![[attachments/image.png]]` and standard Markdown image references. It writes Markdown into a `写了就发/` folder and new images into `写了就发/附件/`. The vault has to be authorised by the user, and the README states the online page does not upload the whole directory to the project server.

The failure mode is documented rather than hidden: when the browser cannot write to the directory, the tool degrades to a Markdown plus attachments ZIP. That is a sensible fallback, but it changes the workflow from sync to manual import, and the capability matrix shows direct write-back as permission-dependent in every column, including the macOS local build. If your whole reason for using this is hands-off Obsidian round trips, verify write permission in your actual browser before you build a routine around it.

Accounts, licensing and the cost of upgrading

Guest mode keeps data in the current tab session. Logged-in mode stores avatar, nickname, drafts and project assets in Supabase per account, with Row Level Security isolating rows by `auth.uid()`. Self-hosting the account layer means running `supabase/schema.sql` and filling in the Project URL and publishable key in `src/supabase-config.js`. The cloud Live Photo fallback needs more: the migration `supabase/migrations/20260731_cloud_live_photo.sql`, a deployed `live-photo-jobs` Edge Function, and repository-specific GitHub trigger credentials. The README warns against putting `service_role` or trigger credentials in the front end or the repository, which is the right instruction and also a sign of how much surface that fallback adds.

The licence is the constraint that matters most for adoption. The repository ships a LICENSE file, package.json declares `"license": "SEE LICENSE IN LICENSE"`, and the README badge reads Personal Non-Commercial. That is not an OSI-style permissive licence, and the badge alone does not tell you the terms. Anyone planning commercial use has to read the LICENSE file itself; this article cannot tell you what it permits.

Upgrade cost is low in one sense and undefined in another. The project has no retrieved releases, so there is no changelog to diff against and no versioned upgrade path to follow. The repository was last pushed on 2026-09-05, so it is recent. The front end is vanilla JavaScript with no build step, which means pulling changes is closer to replacing files than to running a migration, but the Supabase schema and Edge Function are stateful pieces you would have to reconcile yourself.

WeChat draft sync, and the alternative it replaces

On the hosted version the WeChat path is copy-as-rich-text with inline styles. The macOS local build can optionally sync to the WeChat draft box, and the README states this only creates or updates drafts, never mass-sends. The App Secret is read from the macOS keychain rather than written into the browser or the repository.

The honest alternative for this job is a Markdown editor with an image-card plugin, or a dedicated card maker plus a separate long-image exporter. The difference in approach is that those tools generally treat the card as the primary artifact and the article as a separate document, so you maintain two copies or re-paste after every edit. write-then-publish keeps one body and swaps the renderer, which is the whole argument for it. The trade is that you accept a fixed card geometry of 1728 x 2304, a fixed default body size, and a project that runs in the browser rather than in your editor's plugin system.

Editorial conclusion

Adopt write-then-publish if you already write in Markdown, publish to Xiaohongshu or WeChat, and want the layout step inside one page instead of three tools. Skip it if you need a permissive commercial licence, if you publish in English on Western platforms, or if you need Live Photo export on browsers without WebCodecs and refuse the cloud fallback path. Before committing, read LICENSE to see what the Personal Non-Commercial badge actually permits, open the site in your target browser and confirm WebCodecs is available, and test the Obsidian write-back once, because the README states it degrades to a ZIP when directory write permission is missing.

Frequently asked questions

How do I install write-then-publish and run it locally?

Clone the repository and run npm start, which executes python3 server.py, then open http://127.0.0.1:5173/ and keep the terminal window open. On macOS you can instead double-click 启动写了就发.command. For basic editing and image export the README says you can also open index.html directly.

Can write-then-publish be used commercially?

The README badge reads Personal Non-Commercial, package.json declares "license": "SEE LICENSE IN LICENSE", and the repository carries a LICENSE file. The terms themselves are in that file, so read it before planning commercial use.

Does write-then-publish upload my videos or images to a server?

The README states that browser-local Live Photo generation keeps the original video on the device, but that this applies only to that local path. Signed-in cloud draft sync backs up project assets separately, and the hosted version may use a private cloud job when WebCodecs is unavailable. Guest mode with a supported browser is the documented way to keep assets on the current device.

What does write-then-publish do when it cannot write to my Obsidian vault?

It degrades to a Markdown plus attachments ZIP, as stated in the README and reflected in the capability matrix, where direct write-back is listed as permission-dependent. The vault must be authorised by the user, and the online page does not upload the whole directory to the project server.

Official sources

  1. fxyadela/write-then-publish on GitHub
  2. Issues
  3. Project website
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/fxyadela-write-then-publish.svg)](https://hysenlabs.com/projects/fxyadela-write-then-publish)