One HTML file, three platform formats — rendering video without an editor

2026-11-04来源 dev.to WebDevEnglish
One HTML file, three platform formats — rendering video without an editor

The same HTML file rendered at three platform sizes

Every time I ship a short video I do the same stupid thing four times.

Open the editor. Set the sequence to 9:16. Export. Re-check the text isn't clipped.
Change the sequence to 3:4. Nudge everything that no longer fits. Export. Re-check.
Then 16:9, because the same content should also go to YouTube.

Four exports, four manual checks, and the versions drift apart because I fixed a
typo in one of them and forgot the others.

The fix wasn't a better editor. It was admitting the layout was never the
creative part — it's arithmetic.

Layout is already parametric; we just don't treat it that way

If your animation is HTML or Canvas, you almost certainly size things relative to
the viewport. A bar chart doesn't have "a width"; it has the available width.

That means the 3:4 version isn't a different design. It's the same design
evaluated at a different viewport.

So the renderer takes a list of targets:

node scripts/render-video-seek.js chart.html --duration=8 \ --presets=douyin,xiaohongshu,square --out=dist/chart.mp4 
dist/chart-douyin.mp4 1080×1920 TikTok / Reels / Shorts dist/chart-xiaohongshu.mp4 1080×1440 Xiaohongshu 3:4 dist/chart-square.mp4 1080×1080 Square 1:1 

One process, three files, three sizes. The --out matters: without it every
render writes next to the source HTML under the same name, so the second pass
silently overwrites the first. (I learned that the annoying way.)

Presets are just named sizes, and there are eleven of them — douyin/tiktok/
vertical, xiaohongshu/xhs, instagram/ig, square, bilibili/youtube/
landscape. Single ratio: --preset=xiaohongshu. A typo fails loudly and lists
the valid names instead of quietly rendering at 1920×1080.

What this does not solve

I want to be precise, because "one command, all formats" sounds like it solves
more than it does.

It guarantees the size, not the composition. 9:16 and 16:9 carry different
information density. A landscape bar chart dropped into 9:16 will render
perfectly and still look wrong, because the font that was right at 1920 wide is
now competing with half the horizontal space. You still have to look.

Vertical leaves deliberate empty space. The bottom of a 9:16 frame is where
the platform puts its progress bar and buttons. Templates reserve about 15% for
burned-in subtitles. That gap is not a layout bug, and I've now written that in
three places because people keep "fixing" it.

The other thing that annoyed me: duration

The natural order of work is script → voiceover → picture. Most tools invert it:
you fix the video duration first, then bend scenes around it.

So if you pass audio and don't pass --duration, the video takes the audio's
length:

node scripts/render-video-seek.js title-card.html --fps=30 \ --audio=voiceover.m4a --out=out/intro.mp4 

Two details that took longer than they should have:

  • Round up to a whole frame, never down. Rounding down makes the video shorter than the audio by up to one frame, and the last syllable gets cut.
  • The old code silently truncated. If the audio was shorter than the video, ffmpeg's -shortest ended the file early — the back half of the picture vanished and the log still said "done". Now a mismatch over 0.35s prints a warning that says how much, which side loses, and how to keep the tail (--audio-fit=pad fills with silence).

For the agent case

If a script or an agent drives this, --json prints one JSON object on stdout
with everything else on stderr:

{ "output": "out/intro.mp4", "width": 1080, "height": 1920, "fps": 30, "frames": 330, "duration": 11.0, "bytes": 756553 } 

duration is measured from the encoded file, not from what you asked for. When
you copy the video stream instead of re-encoding it, ffmpeg can only cut on frame
boundaries, so "I asked for 2.02s and got 2.50s" is normal and the honest number
is the one on disk.

The trade-off I'd make again

The whole thing is deterministic: a page that is a pure function of frame(t),
a headless browser that seeks to each timestamp, a screenshot, an encode. Frame
217 is identical on every run, which is what makes "change one caption and
re-render" safe — and what makes it possible to test video output at all.

That determination is also the reason text never comes out garbled. Not because
the renderer is clever, but because nothing is being generated. It's your HTML,
drawn.

There's an 11-second clip on the project page that is literally one HTML file rendered
three ways, if you want to see what the output looks like before reading any more code:

https://wx4766.github.io/html-to-video/

Code, eleven templates, MIT: github.com/wx4766/html-to-video

If you render video from code, I'd like to know where your pipeline hurts —
especially if it's the part I assumed was fine.

原文:https://www.a8q9.cn/en/article/one-html-file-three-platform-formats-rendering-video-without-an-editor-d661f9.html

相关文章