mark2note converts Markdown into presentation assets through the flow: Markdown -> AI deck JSON -> HTML / PNG.
It calls the AI CLI configured in your config file to generate deck JSON, renders HTML, and captures PNG images from the rendered pages. The default primary output remains stable HTML + PNG. When --animated or render.animated.enabled is enabled, it additionally tries to export one Animated WebP or MP4 per page as an enhancement output. For Xiaohongshu workflows, MP4 is currently the more practical intermediate format before converting to Live Photo. The experimental --live / render.live.enabled mode additionally tries to build one Live package directory per page and is mainly intended for macOS / iPhone import workflows.
The capture-html command is also a public CLI subcommand that converts existing HTML files, or HTML files inside a directory, into sibling PNG files. In directory mode, it only scans the current directory, does not recurse into subdirectories, only processes lowercase .html files, and writes PNG output next to the source HTML files. It does not export Animated WebP, MP4, or Live packages.
- Markdown -> AI deck JSON -> HTML / PNG
- Optional Animated WebP / MP4 export
- Optional experimental Live package export and Apple Live Photo assembly
- Convert existing HTML files into sibling PNG files
- Publish standard image posts or Live-photo assets to Xiaohongshu through
publish-xhs - Prepare Xiaohongshu publish metadata after the main PNG render with
--prepare-xhs, without publishing - Publish to Xiaohongshu automatically after the main PNG render with
--publish-xhs - Add work poster covers to list-style decks with
posters.yamlor--auto-posters
- Go 1.25+
- An authenticated Codex CLI; the default integration uses
codex execand sends prompts through stdin - Google Chrome, or a compatible browser binary that supports
--headless=new img2webp(required only when Animated WebP export is enabled)ffmpeg(required when MP4 or Live export is enabled)exiftool(required when Live package export is enabled)makelive(required only when final Apple Live Photo assembly is enabled)
Prefer downloading the archive for your platform from GitHub Releases, for example:
tar -xzf mark2note_<tag>_<os>_<arch>.tar.gz
./mark2note --helpYou can also build from source when developing locally:
bash ./build.sh
./mark2note --helpcp configs/config.example.yaml configs/config.yamlThe default config path is configs/config.yaml. Update it as needed for your AI CLI, output directory, theme, author, and watermark.
go build -o ./mark2note ./cmd/mark2notePrepare a Markdown file first, for example ./article.md, then run:
./mark2note --input ./article.mdNotes:
- The repository root
./config.yamlis not the default path; the default path is alwaysconfigs/config.yaml - If you still need the legacy root-level config behavior, pass it explicitly with
--config ./config.yaml - You can also use
--configto point to any other config file
Successful renders write deck.json and render-meta.json to the output directory. Use --from-deck to regenerate HTML/PNG from that saved layout without rereading Markdown or rerunning AI layout:
./mark2note --from-deck ./output/preview/deck.jsonTo keep the saved layout and only change the theme, rerender from deck.json with --theme:
./mark2note \
--from-deck ./output/preview/deck.json \
--theme plum-ink \
--out ./output/preview-plum-inkThe rerender path reuses the same post-render flows:
./mark2note --from-deck ./output/preview/deck.json --import-photos --import-album "mark2note"
./mark2note --from-deck ./output/preview/deck.json --live --live-assemble --live-import-photosWithout --out, mark2note creates a new timestamped directory from the original deck directory name. If a sibling render-meta.json exists, it restores theme, viewport, author, and watermark. --theme can override the restored theme for one run. --prompt-extra is only valid with --input, not --from-deck.
List-style articles, book lists, manga lists, and drama recommendations can render work covers on content pages. The shortest workflow is to search covers during render and prepare Xiaohongshu metadata at the same time:
./mark2note \
--input ./article.md \
--auto-posters \
--prepare-xhs \
--prompt-extra "make it suitable for Xiaohongshu: restrained, clean, and non-graphic"To generate scheduled publish metadata for a single run, override the Xiaohongshu publish mode and schedule time on the main command:
./mark2note \
--input ./article.md \
--auto-posters \
--prepare-xhs \
--xhs-mode schedule \
--xhs-schedule-at "2026-05-07 07:00:00"This writes HTML/PNG plus <out>/xhs-publish-meta.json, but does not publish. After reviewing the PNG files, replay the metadata:
./mark2note publish-xhs --meta ./output/preview/xhs-publish-meta.jsonIf important covers need human review, generate and inspect a posters.yaml manifest first, then render:
./mark2note enrich-posters --input ./article.md --out ./posters.yaml
./mark2note --input ./article.md --asset-manifest ./posters.yaml --prepare-xhsposters.yaml format:
posters:
Usogui:
src: https://example.com/usogui.jpg
source: manual
Death Note: ./assets/death-note.jpgsrc accepts http://, https://, data:image/..., and local image paths relative to the manifest file. Local files are converted to data URIs before rendering so screenshots can read them consistently. Use manifests only from trusted sources to avoid embedding unintended local images into the output.
The built-in providers are currently bilibili, bangumi, anilist, and mydramalist; use --poster-sources bilibili,bangumi,anilist,mydramalist to restrict sources. When --asset-manifest and --auto-posters are used together, the manual manifest overrides automatic candidates. This is useful when you want to pin important covers and let automatic search fill gaps. --auto-posters is supported only with --input, not --from-deck.
- Stable themes:
default,warm-paper,editorial-cool,tech-noir,plum-ink,red-editorial,sage-mist,fresh-green deck.theme_mode: weeklychooses a fixed theme fromdeck.weekly_themesby local weekday;deck.themeremains the fallback--themeoverrides fixed or weekly selection for one run- Unknown theme names fall back to
default
Default config file: configs/config.yaml
Key fields:
output.dir: default output root directoryai.command/ai.args: AI CLI command and arguments used to generate deck JSONdeck.theme_mode: theme selection mode, supportingfixedandweekly;weeklyreadsdeck.weekly_themesby local weekdaydeck.theme: default / fallback theme, supportingdefault,warm-paper,editorial-cool,tech-noir,plum-ink,red-editorial,sage-mist, andfresh-green; unknown names fall back todefaultdeck.weekly_themes: weekday-to-theme map for weekly mode, such asmonandtuedeck.author: default cover author valuedeck.watermark.enabled: enables the page watermark, on by defaultdeck.watermark.text: watermark text, defaulting towalker1211/mark2notedeck.watermark.position: watermark position, supportingbottom-rightandbottom-leftrender.viewport.width: export width, default1242render.viewport.height: export height, default1656render.animated.enabled: whether to additionally export one animated asset per page, off by defaultrender.animated.format: animated format, supportingwebpandmp4render.animated.duration_ms: total page animation timeline duration, default2400; when Live export is enabled it also affectsmotion.movrender.animated.fps: animation capture density, default8; it affects Animated WebP / MP4 output and also Live frame sampling densityrender.import_photos: whether to import generated PNG files into Apple Photos, off by defaultrender.import_album: Apple Photos album name for PNG import; when empty,mark2note-photos-<timestamp>is generatedrender.import_timeout: PNG import timeout, default2m; supports Go duration strings such as45sand2mrender.live.enabled: whether to additionally export one experimental Live package per page, off by defaultrender.live.photo_format: Live still photo format, currently emitted asjpegrender.live.cover_frame: Live cover frame strategy, supportingfirst,middle, andlastrender.live.assemble: whether to callmakeliveafter.live/packages are generated, off by defaultrender.live.output_dir: final Apple Live Photo output directory; when empty it defaults to<out>/apple-live/render.live.import_photos: whether to import assembled Live Photos into Apple Photos, off by default; requiresrender.live.assemblerender.live.import_album: Apple Photos album name for Live import; when empty,mark2note-live-<timestamp>is generatedrender.live.import_timeout: Live import timeout, default2m; supports Go duration strings such as45sand2mxhs.publish.account: default account forpublish-xhs,--publish-xhs, and--prepare-xhsxhs.publish.headless: default browser headless mode forpublish-xhs,--publish-xhs, and--prepare-xhsxhs.publish.browser_path: default browser executable path forpublish-xhs,--publish-xhs, and--prepare-xhs; CLI--chromecan override it for one runxhs.publish.profile_dir: default browser profile directory forpublish-xhs,--publish-xhs, and--prepare-xhsxhs.publish.mode: publish timing/flow, not page visibility; supportsimmediate(normal immediate publish flow) andschedule(scheduled publish flow); main render commands can override it for one run with--xhs-modexhs.publish.visibility: Xiaohongshu page visibility, not publish timing; supportspublicandonly-selfxhs.publish.schedule_at: default scheduled publish time formode: schedule, usingYYYY-MM-DD HH:MM:SSand parsed in Asia/Shanghai; main render commands can override it for one run with--xhs-schedule-atxhs.publish.topic_generation.enabled: whether--publish-xhs/--prepare-xhscalls AI to generate 3-6 Xiaohongshu topics when--xhs-tagsis omitted, on by defaultxhs.publish.title_generation.enabled: whether--publish-xhs/--prepare-xhscalls AI to rewrite titles that exceedmax_runes, on by defaultxhs.publish.title_generation.max_runes: auto-publish title limit, default20; counted with an approximate Xiaohongshu-style length, where half-width ASCII letters, digits, and punctuation count as about 0.5 characters, and non-ASCII characters count as about 1xhs.publish.chrome_args: extra Chrome launch arguments used only by Xiaohongshu publishing
Additional notes:
- The example value for
deck.authorcan be changed; leave it blank if you do not want the author to be shown - The example author value is not a built-in program default
- Watermark is enabled by default and rendered directly in HTML, so PNG files captured from HTML include the same watermark
deck.watermark.positiononly supportsbottom-rightandbottom-leftrender.viewport.width/render.viewport.heightaffect both the HTML viewport and the PNG / animated capture size; when omitted they fall back to1242 x 1656- If you want to test a Xiaohongshu-friendly portrait pipeline, try
720 x 960or1080 x 1440first - HTML + PNG remain the primary stable outputs; Animated WebP, MP4, and Live are optional enhancement outputs
- CLI import flags override config defaults when explicitly passed, including
--import-photos=falseand--live-import-photos=false - Animated WebP export requires
img2webp; MP4 export requiresffmpeg; if either tool is missing, the command keeps successful HTML + PNG output and prints a warning render.animated.duration_ms/--animated-durationis not only for.webp/.mp4; when Live export is enabled it also affects the overallmotion.movtimelinerender.animated.fps/--animated-fpsis most intuitive for Animated WebP / MP4; in Live mode it means capture density rather than a fixed final playback FPS formotion.mov- Live package export requires
ffmpeg + exiftool; the current implementation is mainly intended for macOS / iPhone import workflows, and writescover.jpg,motion.mov, andmanifest.jsonin each page package directory - When
render.live.assembleor--live-assembleis also enabled,makelivemust be available; if it is missing, the command only prints a warning and still keeps HTML + PNG and.live/package output <page>.live/is an intermediate package, not the final directly importable asset; after assembly, final outputs are written to<out>/apple-live/or the directory specified byrender.live.output_dir/--live-output-dir- When only Live export is enabled, the renderer still captures frames on the animation timeline, but it does not emit root-level
.webpor.mp4files capture-htmlstill only converts existing HTML into PNG files; if you want it to reuse configured export dimensions, pass--configso it can readrender.viewport.width/height--themesupports one-off theme overrides, including the theme selected by weekly mode--authorsupports one-off author overrides--configcan explicitly select another config file--prompt-extraappends one-off natural-language guidance for the Markdown -> deck JSON stage, such as pacing, tone, or structure--prompt-extraonly affects deck generation; it does not directly change HTML rendering, PNG capture, Animated / Live export, orpublish-xhsbehavior--asset-manifestreads a YAML / JSON poster manifest before rendering and hydrates matching work covers into suitable pages; local image paths are resolved relative to the manifest fileenrich-postersextracts titles from《work title》text and bold list leads, searches providers, and writes a manifest; review it before final renders when accuracy matters--auto-posterssearches poster candidates during a single--inputrender and hydrates the deck immediately; when combined with--asset-manifest, the manual manifest wins--poster-sourcesrestricts providers forenrich-posters/--auto-posters; by default it searchesbilibili,bangumi,anilist, thenmydramalist; currently supported values arebilibili,bangumi,anilist, andmydramalist--prepare-xhswrites<out>/xhs-publish-meta.jsonafter the main render flow successfully generates standard PNG files; it reuses the same title rewriting, topic generation, and validation as--publish-xhs, but does not open the browser or publish--publish-xhspublishes to Xiaohongshu after the main render flow successfully generates standard PNG files; the title comes from the Markdown H1 and the body contains only 3-6 topic hashtags- When the auto-publish title exceeds
xhs.publish.title_generation.max_runes,--publish-xhs/--prepare-xhsuses the sameai.command/ai.argsto rewrite it according toxhs.publish.title_generation.enabled; code validates the length but does not truncate it locally - When
--xhs-tagsis omitted,--publish-xhs/--prepare-xhsuses the sameai.command/ai.argsto generate topics according toxhs.publish.topic_generation.enabled; AI command failures, invalid JSON, or no valid topics skip preparation / publishing and return an error instead of falling back to local rule-based inference --xhs-tagsmanually overrides AI topics, for example--xhs-tags "AI agent,data safety,engineering reflection"; it is valid only with--publish-xhsor--prepare-xhsand skips AI topic generation--xhs-mode/--xhs-schedule-atoverride the automatic Xiaohongshu publish mode and schedule time for one run; they are valid only with--publish-xhsor--prepare-xhs- When
xhs.publish.chrome_argsis omitted, Xiaohongshu publishing usesdisable-background-networking,disable-component-update,no-first-run, andno-default-browser-check; setchrome_args: []to launch without extra args for debugging xhs.publish.chrome_argsentries may include or omit the leading--, andname=valuearguments are supported
Using codex:
ai:
command: codex
args:
- exec
- --ignore-user-config
- --disable
- apps
- --disable
- plugins
- --disable
- remote_plugin
- --ephemeral
- --skip-git-repo-check
- --sandbox
- read-only
- --color
- never
- --model
- gpt-5.6-solFor direct codex calls, mark2note sends the prompt through stdin. Custom AI CLIs retain the legacy -p <prompt> invocation, so adjust their arguments to match your local setup.
./mark2note --help
./mark2note --input ./article.md
./mark2note --input ./article.md --out ./output/preview
./mark2note --input ./article.md --config ./configs/config.yaml
./mark2note --input ./article.md --config ./config.yaml
./mark2note --input ./article.md --theme warm-paper --author "Your Name"
./mark2note --input ./article.md --theme plum-ink
./mark2note --input ./article.md --theme sage-mist
./mark2note --from-deck ./output/preview/deck.json --theme plum-ink --out ./output/preview-plum-ink
./mark2note --input ./article.md --prompt-extra "make the cover more attention-grabbing and frame it like an experience recap"
./mark2note --input ./article.md --auto-posters --prepare-xhs
./mark2note --input ./article.md --auto-posters --prepare-xhs --xhs-mode schedule --xhs-schedule-at "2026-05-07 07:00:00"
./mark2note publish-xhs --meta ./output/preview/xhs-publish-meta.json
./mark2note enrich-posters --input ./article.md --out ./posters.yaml
./mark2note --input ./article.md --asset-manifest ./posters.yaml --prepare-xhs
./mark2note --input ./article.md --theme fresh-green --prompt-extra "make the output concise but keep image pages" --live=false --publish-xhs
./mark2note --input ./article.md --theme fresh-green --prepare-xhs --xhs-tags "AI agent,data safety,engineering reflection"
./mark2note --input ./article.md --animated --animated-format webp --animated-duration 2400 --animated-fps 8
./mark2note --input ./article.md --animated --animated-format mp4 --animated-duration 2400 --animated-fps 8
./mark2note --input ./article.md --import-photos --import-album "mark2note"
./mark2note --input ./article.md --live --live-cover-frame middle
./mark2note --input ./article.md --live --live-assemble --live-import-photos --live-import-album "mark2note-live"
./mark2note --input ./article.md --live --live-assemble --live-output-dir ./output/apple-live
./mark2note capture-html --input ./output/preview/p02-quote.html
./mark2note capture-html --input ./output/preview
./mark2note publish-xhs --account main --title "Title" --content "Body" --images ./cover.jpg
./mark2note publish-xhs --account main --mode schedule --schedule-at "2026-04-18 20:30:00" --title-file ./title.txt --content-file ./body.md --images ./cover.jpg,./detail.jpg
./mark2note publish-xhs --account main --title-file ./title.txt --content-file ./body.md --live-report ./output/report.json --live-pages p01-cover,p02-bulletsNote: in directory mode, capture-html only scans the current directory, does not recurse into subdirectories, only processes lowercase .html, and writes PNG output next to the source HTML files.
The safer workflow is to generate PNG files and publish metadata first, review the images, then replay the metadata:
./mark2note \
--input ./article.md \
--auto-posters \
--prepare-xhs
./mark2note publish-xhs --meta ./output/preview/xhs-publish-meta.json--prepare-xhs reuses xhs.publish defaults for account, browser path, browser profile, headless mode, publish mode, originality declaration, and content-copy preference. It generates and validates the title, topics, and image list, then writes <out>/xhs-publish-meta.json. It does not open the browser or publish. For scheduled metadata on a single run, add --xhs-mode schedule --xhs-schedule-at "2026-05-07 07:00:00".
The main render command can also automatically invoke the Xiaohongshu publish flow after standard PNG files are generated successfully. This flow reuses xhs.publish defaults for account, browser path, browser profile, headless mode, publish mode, originality declaration, and content-copy preference. It publishes the standard PNG pages from the current render, not Live Photo artifacts.
./mark2note \
--input ./article.md \
--theme fresh-green \
--prompt-extra "make the output concise but keep image pages" \
--live=false \
--publish-xhsAuto-publish behavior:
- the title comes from the first Markdown H1; when absent, it falls back to the cleaned input filename
- titles within
xhs.publish.title_generation.max_runesare used as-is; longer titles trigger AI rewriting, and empty or still-too-long results stop publishing with an error - the body contains only topic hashtags, for example
#AI代理 #数据安全 #工程反思 - topics are generated by AI, and automatic results target 3-6 topics
- use
--xhs-tagsto manually override AI topics; manual values skip topic generation and are still rendered as body hashtags:
./mark2note \
--input ./article.md \
--theme fresh-green \
--publish-xhs \
--xhs-tags "AI agent,data safety,engineering reflection"Rules:
--publish-xhsand--prepare-xhsare supported only with--input, not--from-deck--publish-xhsand--prepare-xhscannot be used together--xhs-tagsis accepted only with--publish-xhsor--prepare-xhs- render failures skip metadata preparation and publishing
- if no generated standard PNG is found, or a listed PNG path is missing, the command prints the render summary first and then returns an error
publish-xhs publishes generated image assets or Live Photo outputs to Xiaohongshu Creator Center.
Current behavior:
immediatemode uses the normal publish flow; page visibility is controlled separately byvisibilityschedulemode submits a scheduled publish- media source is exclusive: use
--imagesfor standard image posts or--live-reportfor the Live pipeline
mark2note publish-xhs --account <name> [flags]
mark2note publish-xhs --meta <file>
mark2note publish-xhs --help--config <file>: config file path, defaultconfigs/config.yaml--meta <file>: replay metadata written by--prepare-xhs/--publish-xhs(usually<out>/xhs-publish-meta.json);--metamust be used standalone and cannot be combined with manual publish fields--account <name>: publish account / profile target; when omitted it falls back toxhs.publish.account--title <text>: inline title text; exactly one of--title/--title-fileis required--title-file <file>: title file path--content <text>: inline content text; exactly one of--content/--content-fileis required--content-file <file>: content file path--tags <csv>: comma-separated tags--mode <name>: publish timing/flow, supportingimmediateandschedule; when omitted it falls back toxhs.publish.mode, otherwise defaults toimmediate--schedule-at <time>: scheduled publish time inYYYY-MM-DD HH:MM:SS, parsed in Asia/Shanghai; when omitted it can fall back toxhs.publish.schedule_at--images <csv>: comma-separated image paths for standard image posts--live-report <file>: Live delivery report path used to resolve publishable Live assets--live-pages <csv>: ordered Live page subset; valid only with--live-report--chrome <path>: Chrome binary path; when omitted it first falls back toxhs.publish.browser_path--headless: run browser automation headless; the built-in default istrue, butxhs.publish.headlesscan override it--profile-dir <dir>: browser profile directory; when omitted it first falls back toxhs.publish.profile_dir--stop-before-submit: upload assets, fill fields, set schedule if needed, then stop before clicking the final publish/scheduled publish button; the browser is kept open for manual inspection. Use it with--headless=false, or setxhs.publish.headless: false, when you need to inspect the page manually
publish-xhs uses a dedicated browser user-data directory to keep the Xiaohongshu Creator Center login state, cookies, and session data. --profile-dir points to that browser profile directory.
Its precedence is:
- CLI
--profile-dir - config
xhs.publish.profile_dir - if both are absent, after the final
accountis resolved it automatically falls back toos.UserConfigDir()/mark2note/xhs/profiles/<account>
Why it matters:
- after you log in once by QR scan, reusing the same profile usually avoids repeated login
- different accounts can use different persistent browser profiles
- account-specific session problems are easier to isolate and debug
Notes:
--profile-dirandxhs.publish.profile_dirnow support~expansion, so~/.config/...works directly- the automatic fallback path still depends on the OS; only explicit
~/.config/...configuration forces that directory style - if you explicitly point multiple accounts to the same
profile_dir, they will share the same browser session directory, which is bad for isolation and debugging
Example config:
xhs:
publish:
account: walker
headless: false
browser_path: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
profile_dir: ~/.config/mark2note/xhs/profiles/walker
# mode controls publish timing/flow; values: immediate = normal immediate flow, schedule = scheduled flow.
mode: immediate
# visibility controls page visibility; values: public = public, only-self = visible only to yourself.
visibility: public
schedule_at: ""Example command:
./mark2note publish-xhs \
--account walker \
--profile-dir ~/.config/mark2note/xhs/profiles/walker \
--title "Published this card set today" \
--content "Post body" \
--images ./output/cover.jpg,./output/detail.jpg--metareplays saved publish metadata and must be used standalone, without--title,--content,--tags,--images,--account, or other manual publish fields- without
--meta, exactly one of--title/--title-fileis required - without
--meta, content may come from--content/--content-file; if--tagsis provided, content may be omitted - exactly one media source is required:
--imagesor--live-report --mode schedulerequires--schedule-atorxhs.publish.schedule_at--live-pagesis accepted only with--live-report
--prepare-xhs renders Markdown, handles the title and topics, and writes xhs-publish-meta.json; --publish-xhs writes the same metadata before browser automation starts. The standalone command can replay that file without regenerating PNG files or topics:
./mark2note publish-xhs --meta ./output/preview/xhs-publish-meta.json./mark2note publish-xhs \
--account main \
--title "Title" \
--content "Body" \
--tags "efficiency,AI" \
--images ./cover.jpg,./detail.jpg./mark2note publish-xhs \
--account main \
--mode schedule \
--schedule-at "2026-04-18 20:30:00" \
--title-file ./title.txt \
--content-file ./body.md \
--images ./cover.jpg./mark2note publish-xhs \
--account main \
--title-file ./title.txt \
--content-file ./body.md \
--live-report ./output/report.json \
--live-pages p01-cover,p02-bulletsIf the command prints a message similar to “not logged in to Xiaohongshu creator center”, the current browser profile does not have a valid login session yet. Open the browser with the same profile directory, complete the QR login to Xiaohongshu Creator Center, then retry the publish command.
For first-time login or expired sessions, it is best to disable --headless, or set xhs.publish.headless: false in config, complete the QR login once, and then reuse the same profile directory.
go test ./...
go build -o ./mark2note ./cmd/mark2noteTo inspect CLI help:
./mark2note --help
./mark2note publish-xhs --helpSee LICENSE.