<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lars Jansen</title>
    <description>The latest articles on DEV Community by Lars Jansen (@larsjansen).</description>
    <link>https://dev.to/larsjansen</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F368653%2F390b0211-9ac1-4941-ad79-0735c21a1a1e.jpeg</url>
      <title>DEV Community: Lars Jansen</title>
      <link>https://dev.to/larsjansen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/larsjansen"/>
    <language>en</language>
    <item>
      <title>I built a small HTML-to-MP4 tool because recording browser animations was the wrong workflow</title>
      <dc:creator>Lars Jansen</dc:creator>
      <pubDate>Sun, 21 Jun 2026 08:36:32 +0000</pubDate>
      <link>https://dev.to/larsjansen/i-built-a-small-html-to-mp4-tool-because-recording-browser-animations-was-the-wrong-workflow-d5b</link>
      <guid>https://dev.to/larsjansen/i-built-a-small-html-to-mp4-tool-because-recording-browser-animations-was-the-wrong-workflow-d5b</guid>
      <description>&lt;p&gt;I’ve been building a small project called TextToDeck, but the useful part of this story isn’t that I’m building another tool for making short social videos.&lt;/p&gt;

&lt;p&gt;The useful part is that I found one annoying step in my own publishing workflow, repeated it enough times to understand exactly why it kept getting in the way, and then built a small custom system around that specific friction instead of turning the whole thing into a much bigger product than it needed to be.&lt;/p&gt;

&lt;p&gt;That was important because most custom tools fail when they start too wide, especially when the original problem was actually narrow, repeatable, and already understood from real use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow that finally annoyed me enough
&lt;/h2&gt;

&lt;p&gt;I write articles and build notes for my blog, Old Stack Journal, and I often want a short vertical video version of the post after publishing because the same idea that works as an article can usually work as a readable visual summary.&lt;/p&gt;

&lt;p&gt;I didn’t need anything fancy, and I didn’t need a full video editor, because most of these videos are basically animated slides with a headline, a few points, a takeaway, and a footer.&lt;/p&gt;

&lt;p&gt;My manual workflow already worked, but it was the sort of workflow that slowly trains you not to use it when the day has already been long enough.&lt;/p&gt;

&lt;p&gt;I’d write the article, generate or hand-edit a small HTML animation, open it in the browser, record the animation with OBS, trim the capture in Clipchamp, export the final MP4, check that it looked readable on mobile, and then upload it manually wherever I wanted to use it.&lt;/p&gt;

&lt;p&gt;I could use Canva or another design tool for some of this, and for a lot of people that’d probably be the right answer, but it still puts me back inside a manual editing workflow when the thing I’m starting from is already structured text. I don’t want to open a design tool, choose a layout, paste bits of an article into slides, adjust spacing, check whether the branding still looks right, export the video, and repeat the same decisions every time I publish something. Canva is useful when you want a flexible design surface, but I wanted the opposite: a repeatable publishing step where the template already knows what it’s meant to do.&lt;/p&gt;

&lt;p&gt;That’s not difficult work, but it’s enough little steps that you start skipping it when you’re tired, especially when the article itself was already the main creative job.&lt;/p&gt;

&lt;p&gt;I work long days outside this project, so that matters more than it probably sounds. A tool doesn’t have to save me five hours to be worth building; sometimes it only has to remove the one awkward step that stops the rest of the workflow from happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first useful simplification
&lt;/h2&gt;

&lt;p&gt;The simplification was this: stop treating every promo video like a video editing job when the real job is usually turning an existing written update into a short visual version.&lt;/p&gt;

&lt;p&gt;Most of the source material already exists inside the written post. There’s a title, excerpt, main idea, category, tags, date, and maybe a featured image, which means the video doesn’t need to begin as a blank canvas.&lt;/p&gt;

&lt;p&gt;Once I accepted that, the product became much smaller and easier to reason about.&lt;/p&gt;

&lt;p&gt;TextToDeck is built around a narrow flow: choose a template, fill structured fields, preview the HTML animation, queue the render, and download the MP4.&lt;/p&gt;

&lt;p&gt;The public site describes it as a way to turn posts, updates, changelogs, and announcements into short vertical video decks you can preview, render, and download, and I like that wording because it keeps the product honest.&lt;/p&gt;

&lt;p&gt;It’s not pretending to be a full editor, and it’s not pretending that AI is magically making a video from nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTML and CSS are good enough for this job
&lt;/h2&gt;

&lt;p&gt;The technical bet is pretty simple, because a lot of short social videos are just moving layout with some timing and restraint.&lt;/p&gt;

&lt;p&gt;They’re cards, headings, labels, quotes, bullet points, progress screens, launch notes, and closing frames. HTML and CSS are already good at that kind of layout, especially when the output is text-heavy and the design needs to stay consistent.&lt;/p&gt;

&lt;p&gt;So the templates are trusted HTML/CSS files, and the user supplies structured field values rather than raw code.&lt;/p&gt;

&lt;p&gt;That one decision keeps the product small and keeps the support problem smaller too. I don’t want version one to accept arbitrary user HTML, JavaScript, unrestricted CSS, or filesystem template paths, because that turns a practical rendering tool into a security problem and a debugging sink.&lt;/p&gt;

&lt;p&gt;The template defines the animation, layout, safe areas, timing, and fields, while the user edits the words inside a controlled shape.&lt;/p&gt;

&lt;p&gt;That boundary is doing a lot of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The current architecture
&lt;/h2&gt;

&lt;p&gt;The app is deliberately plain because I’m trying to keep the product understandable before making it clever.&lt;/p&gt;

&lt;p&gt;The public and member-facing side is a PHP/MySQL app. It handles the public site, template gallery, accounts, generator pages, render jobs, credits, job status pages, protected downloads, and the workspace.&lt;/p&gt;

&lt;p&gt;The rendering side is a private Node worker. It consumes queued jobs from MySQL, validates the trusted template and structured fields, calls the renderer, runs the browser and FFmpeg side of the work, writes job events, and exits.&lt;/p&gt;

&lt;p&gt;The live shape is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
→ PHP app
→ MySQL render job
→ private Node worker
→ Playwright / Chromium
→ FFmpeg
→ MP4 and thumbnail
→ protected PHP download route
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PHP is good at the boring product surface: accounts, forms, pages, validation, database records, access checks, and download authorization. Node is good for the rendering worker because Playwright, Chromium, and FFmpeg fit there more naturally.&lt;/p&gt;

&lt;p&gt;I don’t need the public website to be a Node app just because the renderer uses Node.&lt;/p&gt;

&lt;h2&gt;
  
  
  The queue changed the feel of the product
&lt;/h2&gt;

&lt;p&gt;The old manual version was a recording task, while the current version is a render job that can be queued, inspected, retried, failed, completed, and downloaded like a normal product action.&lt;/p&gt;

&lt;p&gt;That sounds like a small wording change, but it changes how the whole workflow feels. When a user submits the form, PHP creates a queued job and returns quickly. The private worker picks it up, renders the MP4 and thumbnail, updates the job status, and leaves the app to show queued, generating, ready, failed, or expired states.&lt;/p&gt;

&lt;p&gt;That means the user can see what’s happening without needing the browser request to sit there while Chromium and FFmpeg do their thing.&lt;/p&gt;

&lt;p&gt;It also means the product can grow slowly without throwing everything into one long request path. A database-backed queue is not glamorous, but it gives the product a clean place to put status, timing, failures, refunds, output metadata, and later queue position.&lt;/p&gt;

&lt;p&gt;For this stage, the worker is intentionally simple and only supports one concurrent render, which is fine because the point right now is to prove the workflow before pretending the product needs big infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The live site changed the questions
&lt;/h2&gt;

&lt;p&gt;TextToDeck is live now, and it has paying users, which changes the whole shape of the project in a useful way.&lt;/p&gt;

&lt;p&gt;A local prototype mostly asks whether the thing can work. A live product with real users asks whether the thing makes sense to someone who didn't watch it being built, whether the templates are clear enough to choose from, whether the pricing feels understandable, whether generated files are protected, whether job history is useful, whether the render status avoids raw developer language, and whether the product can survive the ordinary messiness of people using it outside my own machine.&lt;/p&gt;

&lt;p&gt;Those are much better questions because they're tied to use rather than possibility.&lt;/p&gt;

&lt;p&gt;It's easy to fool yourself with a local prototype because you already understand every hidden assumption inside it. You know which button to press, which field can be left empty, which template looks best, which test data is safe, and which rough edge you're pretending doesn't matter yet.&lt;/p&gt;

&lt;p&gt;A paying user doesn't care about any of that.&lt;/p&gt;

&lt;p&gt;That's a good thing because it forces the product to become clearer, not just technically functional.&lt;/p&gt;

&lt;p&gt;The template gallery now has controlled formats for article promos, build notes, product updates, launch notes, patch notes, feature highlights, quote promos, practical tips, opinion takeaways, before-and-after takeaways, changelog summaries, and launch announcements.&lt;/p&gt;

&lt;p&gt;That range sounds like a lot, but it's still narrow in the way that matters. The product isn't trying to become a video editor. It's still built around structured written updates being turned into short vertical MP4s.&lt;/p&gt;

&lt;p&gt;The live site also forced the boring product work earlier than I'd normally choose if I were only building for myself. Accounts, credits, protected downloads, render history, job status, template previews, and pricing pages aren't glamorous, but they're the difference between a local experiment and something someone can actually pay for and use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The WordPress part should stay boring on purpose
&lt;/h2&gt;

&lt;p&gt;The next serious workflow is still a WordPress companion plugin, and I'm trying to keep that boundary clean before the plugin gets clever.&lt;/p&gt;

&lt;p&gt;The plugin shouldn't render MP4 files inside WordPress because that's asking the wrong system to do the wrong job. WordPress is already good at being the publishing layer, and the render service is already responsible for the browser and FFmpeg work, so mixing those jobs would add complexity in the worst possible place.&lt;/p&gt;

&lt;p&gt;The plugin should read the post title, excerpt, body, publish date, categories, tags, and featured image. It should let the user choose a template and brand kit, draft or edit the video fields, request a render from the hosted service, show status, save the output link back to the post, and maybe copy a suggested caption.&lt;/p&gt;

&lt;p&gt;That's enough for the first useful version because WordPress should be the workflow layer rather than the rendering engine.&lt;/p&gt;

&lt;p&gt;The whole point is to meet the user where the written update already lives, not to turn WordPress into a local video rendering box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI fits, and where it doesn’t
&lt;/h2&gt;

&lt;p&gt;TextToDeck itself doesn’t use AI.&lt;/p&gt;

&lt;p&gt;That’s important because I don’t want anyone misunderstanding what the product does. The core TextToDeck workflow is template-based, not AI-generated. You choose a reusable template, fill in structured fields, preview the animation, render the MP4, and download the finished video.&lt;/p&gt;

&lt;p&gt;There’s no AI making the video, no AI designing the template, no AI deciding what gets rendered, and no AI sitting in the middle of the render pipeline.&lt;/p&gt;

&lt;p&gt;The only AI-related part I’m looking at is for the future WordPress plugin, and even there it’s only for drafting field suggestions from an existing post. The idea is that the plugin could use WordPress 7’s new AI integration system to suggest things like a shorter hook, three useful points, a takeaway line, a footer, or a caption.&lt;/p&gt;

&lt;p&gt;Even then, the user would still review and edit everything before rendering.&lt;/p&gt;

&lt;p&gt;That keeps the boundary clean. TextToDeck is a structured template renderer for written updates. The possible AI layer in WordPress is just a helper for turning post content into editable template fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger lesson
&lt;/h2&gt;

&lt;p&gt;The main thing this project has reminded me is that custom tools don't need to be huge to be worth building.&lt;/p&gt;

&lt;p&gt;A lot of the best small tools are workflow reducers. They take something you already do, remove a few awkward handoffs, and make the next repetition less painful.&lt;/p&gt;

&lt;p&gt;That's what I'm trying to do here.&lt;/p&gt;

&lt;p&gt;I'm not trying to replace video editors, social schedulers, design tools, or publishing platforms. I'm trying to remove the part where I open OBS, record a browser animation, trim it somewhere else, export it, check it again, and then hope I still feel like posting by the time I'm done.&lt;/p&gt;

&lt;p&gt;That might sound like a small problem, but it's a real one, and real small problems are usually a better starting point than imaginary large ones when you're building around your own workflow first.&lt;/p&gt;

&lt;p&gt;The fact that people have now paid for access makes that lesson sharper, not weaker. The project started from my own friction, but the only reason it can become a product is because other people recognise enough of that friction in their own workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it goes next
&lt;/h2&gt;

&lt;p&gt;The next milestone is still simple: keep tightening the path from a written post to a downloadable vertical MP4 without OBS or Clipchamp.&lt;/p&gt;

&lt;p&gt;Now that the site is live and has paying users, the work shifts from "can this render?" to "does this keep holding up when real people use it?"&lt;/p&gt;

&lt;p&gt;That means watching title length, mobile readability, safe areas, template repetition, render timing, output quality, upload compatibility, caption usefulness, support questions, pricing confusion, and whether the workflow still feels worth using after the novelty wears off.&lt;/p&gt;

&lt;p&gt;That last part is the real test because a custom tool isn't useful because it exists. It's useful because you keep reaching for it when the original job comes back around. Feel free to have a poke around at &lt;a href="https://texttodeck.com" rel="noopener noreferrer"&gt;https://texttodeck.com&lt;/a&gt;, if you want to have a play leave a comment and I'll give you some credits to test with. Would love the feedback:)&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>automation</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
