<?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: Joan Miquel Torres</title>
    <description>The latest articles on DEV Community by Joan Miquel Torres (@bitifet).</description>
    <link>https://dev.to/bitifet</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%2F104985%2F80bedd33-709d-48ce-8fd9-fc22e9cf9db2.jpeg</url>
      <title>DEV Community: Joan Miquel Torres</title>
      <link>https://dev.to/bitifet</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bitifet"/>
    <language>en</language>
    <item>
      <title>Agentp: Turn OpenCode Into a Headless AI Engine for Your Editor, Terminal, and Telegram</title>
      <dc:creator>Joan Miquel Torres</dc:creator>
      <pubDate>Wed, 17 Jun 2026 22:26:14 +0000</pubDate>
      <link>https://dev.to/bitifet/agentp-turn-opencode-into-a-headless-ai-engine-for-your-editor-terminal-and-telegram-14gj</link>
      <guid>https://dev.to/bitifet/agentp-turn-opencode-into-a-headless-ai-engine-for-your-editor-terminal-and-telegram-14gj</guid>
      <description>&lt;p&gt;I've always felt out of step with the prevailing trends.&lt;/p&gt;

&lt;p&gt;Before the AI explosion, the mantra was "ship fast" — the Minimum Viable Product. If you weren't first, you were nobody. Quality, testing, documentation? Nice-to-haves. I could never stomach shipping "human slop" just to be first.&lt;/p&gt;

&lt;p&gt;Now we're in the AI era, and suddenly everyone is alarmed about "AI slop." And I find myself out of step again — because from where I stand, the AI helps me produce the opposite.&lt;/p&gt;

&lt;p&gt;Just as an example, I have a personal side project called &lt;em&gt;&lt;a href="https://smarkform.bitifet.net" rel="noopener noreferrer"&gt;SmarkForm&lt;/a&gt;&lt;/em&gt; and very little time to invest in it (but I keep pushing). Before AI it had a few "not-to-break-again" tests and a bare "just-the-docs" Jekyll site on GitHub Pages with often outdated code snippets and only a separate &lt;em&gt;Examples&lt;/em&gt; section (which &lt;a href="https://smarkform.bitifet.net/resources/examples" rel="noopener noreferrer"&gt;still exists&lt;/a&gt;) as the sole real demo.&lt;/p&gt;

&lt;p&gt;Nowadays almost every code snippet in the documentation is a working example of a &lt;em&gt;SmarkForm-powered&lt;/em&gt; form whose source code can be edited in place. The test suite has been fully migrated to Playwright, covering up to 5 platforms and including a suite of co-located tests that ensure every example in the documentation keeps working. Moreover, the most recent inline examples are AI-authored (in Copilot's words: "SmarkForm's clean, declarative API makes it a natural fit for AI-assisted development"). The last "AI-free" bastion in the repository was the actual source code of the library, but nowadays I use AI there too — just with a more thorough review and a test-first approach.&lt;/p&gt;

&lt;p&gt;Put simply: my documentation is better. I write more tests than I could have imagined before. The code is cleaner. Even the worst AI-generated test is harmless: the most it can do is be useless. Unlike buggy production code rushed out to win a race, it won't break anything — an occasional garbage-collection pass is all you need.&lt;/p&gt;

&lt;p&gt;The same goes for tooling. Every few years — sometimes months — a new IDE becomes the baseline, and if you haven't switched you're suddenly irrelevant. I use Neovim and tmux. Not because they're trendy, but because I spent years evolving a workflow that works across physical terminals, remote servers, and whatever machine I happen to be sitting at. And, more importantly, it lets me focus on what I'm doing rather than how to do it. I'm not about to throw that away for a shinier editor.&lt;/p&gt;

&lt;p&gt;That's the mindset behind the &lt;em&gt;agentp trio&lt;/em&gt;. It started with just &lt;strong&gt;agentp&lt;/strong&gt;, a simple CLI tool that pipes a prompt to an OpenCode server and returns the final answer while you see what's going on in a spare monitor (when you have it). Then came &lt;strong&gt;ocmux&lt;/strong&gt;, a project manager that manages a tmux session labeled as "Opencode" and keeps a dedicated OpenCode server and TUI for each project in a separate window. Finally, &lt;strong&gt;tgagentp&lt;/strong&gt; is a Telegram bot that lets me talk to my projects (and even send and receive files) while away from the keyboard — and so much more.&lt;/p&gt;

&lt;p&gt;Agentp grew from there — piece by piece, idea by idea — into a set of three zero-dependency Node.js CLI tools. It's heavily AI-assisted, including the tests. I review every stage before shipping, but the real test is using it every day. Bugs happen; I value a working feature more than a flawless one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In summary, now I can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pipe a prompt straight from Vim and replace my selection with the answer.&lt;/li&gt;
&lt;li&gt;See what's going on in an Opencode TUI that automatically switches to the right project.&lt;/li&gt;
&lt;li&gt;Get notified in Telegram when the answer is ready.&lt;/li&gt;
&lt;li&gt;Talk to the agent in charge of my project from Telegram while away from the keyboard.&lt;/li&gt;
&lt;li&gt;Handle multiple projects and servers simultaneously from a Telegram group with topics.&lt;/li&gt;
&lt;li&gt;Queue messages while a server is busy and get threaded replies.&lt;/li&gt;
&lt;li&gt;Leave private comments and public (for the agent awareness) notes in my telegram conversation.&lt;/li&gt;
&lt;li&gt;Send files to the agent and ask the agent to send files to me through Telegram.&lt;/li&gt;
&lt;li&gt;Record a Telegram conversation and inject it as context into the next agentp response.&lt;/li&gt;
&lt;li&gt;Threaded replies, permission request handling, and more...&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Three Tools
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;agentp&lt;/code&gt; — The Pipe
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Stdin in, answer out. That's the core loop.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you get used to writing text in Vim, all other text input methods feel clunky — and OpenCode TUI's editor &lt;a href="https://github.com/anomalyco/opencode/issues/9836" rel="noopener noreferrer"&gt;is no exception&lt;/a&gt;. Its &lt;code&gt;/editor&lt;/code&gt; command lets you edit prompts in an external editor, but the whole TUI screen blanks out during the edit, which means a complete loss of context.&lt;/p&gt;

&lt;p&gt;My solution: I open the OpenCode TUI in a dedicated tmux session that I can maximize on a spare vertical monitor or just switch back and forth when working from a laptop. Then I write my prompts directly in Vim, visually select them, and send them to OpenCode by &lt;em&gt;filtering&lt;/em&gt; them through &lt;code&gt;agentp&lt;/code&gt;. I can either ask for a code snippet and get the answer in place, or use the &lt;code&gt;--qa&lt;/code&gt; modifier to keep my prompt together with the answer. The latter lets me maintain a kind of logbook of prompts and answers so I can go back to review, copy chunks of code (or former prompts), etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic usage:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"Summarize this file"&lt;/span&gt; | agentp
&lt;span class="nb"&gt;cat &lt;/span&gt;prompt.txt | agentp

&lt;span class="c"&gt;# Target a specific session by name (partial match works)&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;prompt.txt | agentp &lt;span class="nt"&gt;--session&lt;/span&gt; &lt;span class="s2"&gt;"My Task"&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;prompt.txt | agentp &lt;span class="nt"&gt;--session&lt;/span&gt; &lt;span class="s2"&gt;"New Task"&lt;/span&gt; &lt;span class="nt"&gt;--new&lt;/span&gt;   &lt;span class="c"&gt;# create if not found&lt;/span&gt;

&lt;span class="c"&gt;# Pull the last N answers without sending a new prompt&lt;/span&gt;
agentp &lt;span class="nt"&gt;--getLast&lt;/span&gt; 5
agentp &lt;span class="nt"&gt;--getLast&lt;/span&gt; 3 &lt;span class="nt"&gt;--qa&lt;/span&gt;    &lt;span class="c"&gt;# full QA pairs with rulers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Real magic from Vim/Neovim:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;,'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;!&lt;/span&gt;agentp &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="k"&gt;qa&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This replaces your visual selection with the answer. The optional &lt;code&gt;--qa&lt;/code&gt; modifier preserves the prompt + answer with labels, so you keep the full context.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚀 &lt;strong&gt;Spoiler:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Passing the output of &lt;code&gt;ocmux&lt;/code&gt; (without arguments) ensures the prompt goes to the right server and automatically switches the TUI in your spare monitor (or wherever) to the right project, instantly.&lt;/p&gt;


&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;,'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;!&lt;/span&gt;agentp &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="k"&gt;qa&lt;/span&gt; $&lt;span class="p"&gt;(&lt;/span&gt;ocmux&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ocmux&lt;/code&gt; — The Project Manager
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Each project gets its own tmux window with a dedicated &lt;code&gt;opencode serve&lt;/code&gt; + TUI pane. Auto-restarts dead panes, pins window names, stores state in &lt;code&gt;.ocmux.json&lt;/code&gt; (discovered upward like &lt;code&gt;.git&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Having the OpenCode TUI aside while you send work to it and receive answers in place is great. But what if you want to switch to another project while the agent is processing?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open another OpenCode Server + TUI in a new tmux window.&lt;/li&gt;
&lt;li&gt;Remember the port of each server.&lt;/li&gt;
&lt;li&gt;Manually switch to the right TUI pane and zoom it.&lt;/li&gt;
&lt;li&gt;Pass the correct port to &lt;code&gt;agentp&lt;/code&gt; every time you call it from a different directory…&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a big deal on its own. But by the time you finish working on the second project, the first one has probably finished — and you'd want to switch back.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ocmux&lt;/em&gt; handles all of that. With no arguments, it switches the Opencode tmux session to the window for the current project (based on the working directory) and prints the server URL.&lt;/p&gt;

&lt;p&gt;Combine it with &lt;code&gt;agentp&lt;/code&gt; as &lt;code&gt;agentp $(ocmux)&lt;/code&gt; or &lt;code&gt;agentp --qa $(ocmux)&lt;/code&gt; and you not only send the prompt to the right server and get the answer back — at the same time, OpenCode automatically switches to the right TUI window for that project. It feels like magic when you're juggling multiple projects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ocmux serve ~/projects/myapp        &lt;span class="c"&gt;# create (new window)&lt;/span&gt;
ocmux serve &lt;span class="nt"&gt;--print-logs&lt;/span&gt; ~/projects/myapp  &lt;span class="c"&gt;# also print server logs to terminal&lt;/span&gt;
ocmux list                          &lt;span class="c"&gt;# list all&lt;/span&gt;
ocmux list &lt;span class="nt"&gt;-l&lt;/span&gt;                       &lt;span class="c"&gt;# list with full URLs&lt;/span&gt;
ocmux ~/projects/myapp              &lt;span class="c"&gt;# switch (shows url)&lt;/span&gt;
ocmux                               &lt;span class="c"&gt;# same as ocmux $(pwd)&lt;/span&gt;
ocmux &lt;span class="nb"&gt;kill&lt;/span&gt; ~/projects/myapp         &lt;span class="c"&gt;# remove&lt;/span&gt;
ocmux &lt;span class="nb"&gt;kill&lt;/span&gt;                          &lt;span class="c"&gt;# same as ocmux $(pwd)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ocmux&lt;/code&gt; also supports &lt;code&gt;--git&lt;/code&gt; and &lt;code&gt;--GIT&lt;/code&gt; flags for git base directory resolution: &lt;code&gt;--git&lt;/code&gt; matches with either worktrees or repository roots, while &lt;code&gt;--GIT&lt;/code&gt; requires an actual repository root (not a worktree).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ocmux --last&lt;/code&gt; prints the URL of the active tmux window — useful when calling &lt;code&gt;agentp&lt;/code&gt; from outside the project directory.&lt;/p&gt;

&lt;p&gt;When an opencode server crashes, &lt;code&gt;ocmux resurrect&lt;/code&gt; reads &lt;code&gt;.ocmux.json&lt;/code&gt;, kills the stale tmux window, and starts a fresh server + TUI in the same directory. Works even with a dead tmux window (stale state file).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;tgagentp&lt;/code&gt; — The Telegram Bridge
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;A Telegram bot that routes messages to your OpenCode servers. Multi-chat, multi-server, slash-commands for everything.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ever wanted to work on a project while away from the keyboard? Writing notes is fine, but you get no feedback — you can't see or explore the project environment. What if you could send a prompt to an agent handling your project and get the answer back in Telegram? Queue messages while the server is busy and get threaded replies? Send files to the agent, or — even better — ask the agent to send files to you?&lt;/p&gt;

&lt;p&gt;That's &lt;em&gt;tgagentp&lt;/em&gt;. And way more: handle multiple servers simultaneously (with a Telegram group using topics), "record" your messages so the next &lt;code&gt;agentp --qa&lt;/code&gt; prepends the conversation as context… And vice versa: get prompts and responses from &lt;code&gt;agentp --qa&lt;/code&gt; delivered to Telegram so you can follow the conversation from anywhere — or just go grab a coffee and get notified when the agent finishes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tgagentp                                  &lt;span class="c"&gt;# default port 4096&lt;/span&gt;
tgagentp &lt;span class="nt"&gt;--think&lt;/span&gt;                          &lt;span class="c"&gt;# start with thinking forwarding enabled&lt;/span&gt;
tgagentp &lt;span class="nt"&gt;--dev&lt;/span&gt;                            &lt;span class="c"&gt;# enable /shutdown for remote restart&lt;/span&gt;
&lt;span class="nv"&gt;TGAGENTP_ROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/projects tgagentp      &lt;span class="c"&gt;# enable /serve and /new commands&lt;/span&gt;
&lt;span class="nv"&gt;TGAGENTP_ALLOWED_CHAT_IDS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"123,-456"&lt;/span&gt; tgagentp  &lt;span class="c"&gt;# restrict to specific chats&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Slash commands:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/help&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show available commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show current server, session, agent, and health&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/servers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List/switch between ocmux projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/sessions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List/switch/create/rename sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/agents&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List/switch active agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/models&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List providers and models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/serve &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start a server in an existing project (requires &lt;code&gt;TGAGENTP_ROOT&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/new &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a directory, init git, and start a server (requires &lt;code&gt;TGAGENTP_ROOT&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/allow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Approve a tool permission once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/reject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deny a tool permission&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/always&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Approve a permission and remember the choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/answer &amp;lt;number&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Respond to a structured question from the AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/markdown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send original markdown response as &lt;code&gt;.md&lt;/code&gt; file (reply to get that specific one)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//&amp;lt;command&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send a raw TUI command (e.g., &lt;code&gt;//init&lt;/code&gt;, &lt;code&gt;//clear&lt;/code&gt;) — answer or confirmation forwarded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/queue &amp;lt;msg&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Queue message when busy — auto-sent after current task finishes (replies chain!)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/record&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Record / pause / retrofill conversation for &lt;code&gt;agentp&lt;/code&gt; context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/flush&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clear all queued messages (manual or auto-queued)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/note &amp;lt;text&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Forward context for agent awareness (agent replies "Ack", info informs future responses)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/comment &amp;lt;text&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Save a comment in chat (not forwarded to agent — context via reply quoting)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/think&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Toggle real-time thinking message forwarding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/cancel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Abort the running prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/disconnect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Disconnect from current server, clear ownership and connection file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/force-switch &amp;lt;server&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Switch server bypassing ownership check (two-phase matching)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/resurrect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restart a crashed server and reconnect the chat to the new instance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Permission prompts from OpenCode (tool access requests) are forwarded automatically — respond with &lt;code&gt;/allow&lt;/code&gt;, &lt;code&gt;/reject&lt;/code&gt;, or &lt;code&gt;/always&lt;/code&gt; directly in the chat.&lt;/p&gt;

&lt;p&gt;When the AI asks a structured question (e.g., tool configuration), tgagentp forwards it as a numbered multiple-choice poll — respond with &lt;code&gt;/answer &amp;lt;number&amp;gt;&lt;/code&gt;. If the command produces a quick answer, it arrives immediately; otherwise a confirmation (&lt;code&gt;✅ /init submitted.&lt;/code&gt;) is sent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File sharing:&lt;/strong&gt; Ask the agent to send you a file and it will know how. Send a file to the chat and the agent will be notified and can download it on demand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;!!&lt;/code&gt; wildcard:&lt;/strong&gt; Use &lt;code&gt;!!&lt;/code&gt; in any command to reference the previous user message. For example, &lt;code&gt;/queue !!&lt;/code&gt; queues your last message, &lt;code&gt;/note !!&lt;/code&gt; sends it as a note.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;On formatting:&lt;/strong&gt; The agent produces Markdown, but Telegram only supports a limited subset (bold, italic, code, pre). tgagentp converts Markdown to Telegram's HTML automatically, which works great for prose, lists, and code — but complex tables, nested formatting, or raw HTML may not survive the trip. If something looks mangled, use &lt;code&gt;/markdown&lt;/code&gt; to download the original response as a &lt;code&gt;.md&lt;/code&gt; file and read it comfortably in any Markdown viewer or editor.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;agentp&lt;/code&gt; and &lt;code&gt;ocmux&lt;/code&gt; together
&lt;/h3&gt;

&lt;p&gt;Combine both tools and you never need to think about ports or URLs again. &lt;code&gt;agentp $(ocmux)&lt;/code&gt; sends your prompt to the right server for the current project AND switches the TUI to show that project — all in one command. From Vim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;,'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;!&lt;/span&gt;agentp &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="k"&gt;qa&lt;/span&gt; $&lt;span class="p"&gt;(&lt;/span&gt;ocmux&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You may think you can just run &lt;code&gt;ocmux&lt;/code&gt; once and pass the URL as a&lt;br&gt;
literal to &lt;code&gt;agentp&lt;/code&gt; — the command is recorded in your Vim command history, after all.&lt;/p&gt;

&lt;p&gt;But calling &lt;code&gt;ocmux&lt;/code&gt; every time also &lt;strong&gt;automatically switches the TUI to the&lt;br&gt;
right project in the "Opencode" tmux session.&lt;/strong&gt; That's the real value.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Agentp Gateway
&lt;/h2&gt;

&lt;p&gt;The killer integration: &lt;code&gt;agentp&lt;/code&gt; and &lt;code&gt;tgagentp&lt;/code&gt; talk to each other through a tiny HTTP gateway.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;agentp --tg&lt;/code&gt;&lt;/strong&gt; — forwards the answer to your Telegram chat after every pipe (hard error if tgagentp not running).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;agentp --no-tg&lt;/code&gt;&lt;/strong&gt; — explicitly disable Telegram forwarding (overrides auto-detection in &lt;code&gt;--qa&lt;/code&gt; mode).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;agentp --qa&lt;/code&gt;&lt;/strong&gt; — auto-detects tgagentp and sends the full QA pair (rulers + prompt + answer) if tgagentp is running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/record&lt;/code&gt;&lt;/strong&gt; — buffers the Telegram conversation. On the next &lt;code&gt;agentp&lt;/code&gt; call, the gateway returns the buffer, and &lt;code&gt;--qa&lt;/code&gt; prepends it to stdout with rulers — so OpenCode sees the full Telegram thread as context. Retroactively buffer past messages with &lt;code&gt;/record N&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;agentp --flush&lt;/code&gt;&lt;/strong&gt; — clears the buffer without prepending.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;agentp --getLast 5&lt;/code&gt;&lt;/strong&gt; — retrieves the last 5 assistant answers from session history (or QA pairs with &lt;code&gt;--getLast 5 --qa&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exclusive ownership&lt;/strong&gt; — each server belongs to at most one chat. New chats start disconnected. &lt;code&gt;/servers switch &amp;lt;name&amp;gt; --force&lt;/code&gt; takes over and notifies the previous owner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-queue&lt;/strong&gt; — when a server is unreachable, messages are automatically queued and delivered when it comes back. &lt;code&gt;/flush&lt;/code&gt; clears the queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server health detection&lt;/strong&gt; — tgagentp periodically checks server connectivity. Dead servers are shown as ❌ unreachable in &lt;code&gt;/status&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-chat&lt;/strong&gt; — each Telegram chat or forum thread has independent server, session, and recorder state. Perfect for teams sharing one bot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto tmux switch&lt;/strong&gt; — every message or &lt;code&gt;/note&lt;/code&gt; from any chat/topic automatically selects and zooms the corresponding server's tmux window, so the TUI follows the conversation across topics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message splitting&lt;/strong&gt; — long answers are split at 4096 characters (respecting newlines) to stay within Telegram limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State persistence&lt;/strong&gt; — chat-to-server directory mappings survive restarts via &lt;code&gt;/tmp/tgagentp-connections.json&lt;/code&gt;. On reboot, tgagentp re-discovers the live server URL from &lt;code&gt;.ocmux.json&lt;/code&gt; and reconnects automatically. &lt;code&gt;/shutdown clear&lt;/code&gt; wipes the saved state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote resurrect&lt;/strong&gt; — &lt;code&gt;/resurrect&lt;/code&gt; restarts a crashed server from Telegram: calls &lt;code&gt;resurrectServer()&lt;/code&gt; from the library, then transfers session state (&lt;code&gt;serverOwners&lt;/code&gt;, active session/agent) to the new URL.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;,'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;!&lt;/span&gt;agentp &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="k"&gt;qa&lt;/span&gt; &lt;span class="p"&gt;--&lt;/span&gt;tg          " answer &lt;span class="k"&gt;in&lt;/span&gt; editor &lt;span class="p"&gt;+&lt;/span&gt; Telegram &lt;span class="p"&gt;(&lt;/span&gt;hard error &lt;span class="k"&gt;if&lt;/span&gt; no tgagentp&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;,'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;!&lt;/span&gt;agentp &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="k"&gt;qa&lt;/span&gt;               " same &lt;span class="p"&gt;+&lt;/span&gt; Telegram &lt;span class="k"&gt;only&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; tgagentp detected
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;,'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;!&lt;/span&gt;agentp &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="k"&gt;qa&lt;/span&gt; &lt;span class="p"&gt;--&lt;/span&gt;flush       " flush /record &lt;span class="k"&gt;buffer&lt;/span&gt;
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;,'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;!&lt;/span&gt;agentp                    " &lt;span class="k"&gt;only&lt;/span&gt; replaces&lt;span class="p"&gt;.&lt;/span&gt; Useful &lt;span class="k"&gt;for&lt;/span&gt; simple snippets&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;,'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;!&lt;/span&gt;agentp &lt;span class="p"&gt;--&lt;/span&gt;no&lt;span class="p"&gt;-&lt;/span&gt;tg            " same &lt;span class="k"&gt;as&lt;/span&gt; above&lt;span class="p"&gt;,&lt;/span&gt; explicitly skip Telegram
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Environment variables:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TELEGRAM_BOT_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Bot token from &lt;a class="mentioned-user" href="https://dev.to/botfather"&gt;@botfather&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TGAGENTP_ALLOWED_CHAT_IDS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;all&lt;/td&gt;
&lt;td&gt;Comma-separated chat IDs to allow (e.g. &lt;code&gt;"123456,-789012"&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TGAGENTP_ROOT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Root directory for &lt;code&gt;/serve&lt;/code&gt; and &lt;code&gt;/new&lt;/code&gt; commands (must be writable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TGAGENTP_PORT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;random&lt;/td&gt;
&lt;td&gt;Port for the agentp gateway (agentp --tg discovers it automatically)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TGAGENTP_DEBOUNCE_MS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;5000&lt;/td&gt;
&lt;td&gt;Debounce interval for queued-agentp Telegram notifications (ms)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OPENCODE_SERVER_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Password for authenticated OpenCode servers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All of this with &lt;strong&gt;zero npm dependencies&lt;/strong&gt; — just Node.js 18+ stdlib.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Setup Works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Always-visible TUI, hands-free&lt;/strong&gt; — The dedicated TUI lives on a spare monitor, a virtual desktop, or a tmux window. You work elsewhere. The TUI shows the full picture without you touching it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-project agility&lt;/strong&gt; — Each project gets its own server and TUI. Switch projects with a single &lt;code&gt;ocmux&lt;/code&gt; command (or automatically via &lt;code&gt;ocmux&lt;/code&gt; with no args), and the displayed TUI follows. The same auto-switch works from Telegram: every message or &lt;code&gt;/note&lt;/code&gt; selects the right tmux window, so the TUI follows you across topics. Start a task in project A, switch to project B while A runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editor ↔ terminal ↔ Telegram loop&lt;/strong&gt; — Pipe from Vim with &lt;code&gt;agentp&lt;/code&gt;, get the answer inline. Enable &lt;code&gt;--tg&lt;/code&gt; (implicit with &lt;code&gt;--qa&lt;/code&gt;) and the result also lands in Telegram — so even if you moved to another device or context, you know when it's done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote awareness&lt;/strong&gt; — tgagentp monitors progress, forwards permission prompts, and queues follow-ups from anywhere. &lt;code&gt;/record&lt;/code&gt; recovers Telegram conversation context for your next &lt;code&gt;agentp&lt;/code&gt; call. Notifications pop the moment a piped task finishes or needs input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful degradation&lt;/strong&gt; — &lt;code&gt;--tg&lt;/code&gt; in auto mode silently skips Telegram if tgagentp isn't running. Explicit &lt;code&gt;--tg&lt;/code&gt; errors pre-send, warns post-send.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async processing&lt;/strong&gt; — tgagentp never blocks the polling loop. Commands stay responsive even while a prompt runs.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; agentp

&lt;span class="c"&gt;# Start a project server&lt;/span&gt;
ocmux serve ~/projects/myapp

&lt;span class="c"&gt;# Pipe a prompt&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"Explain this codebase"&lt;/span&gt; | agentp

&lt;span class="c"&gt;# With Telegram (set up a bot with @BotFather first)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TELEGRAM_BOT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-token"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENCODE_SERVER_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-password"&lt;/span&gt;   &lt;span class="c"&gt;# optional but recommended&lt;/span&gt;
tgagentp &lt;span class="nt"&gt;--think&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/bitifet/agentp" rel="noopener noreferrer"&gt;github.com/bitifet/agentp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/agentp" rel="noopener noreferrer"&gt;npmjs.com/package/agentp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenCode:&lt;/strong&gt; &lt;a href="https://github.com/anthropics/claude-code" rel="noopener noreferrer"&gt;github.com/anthropics/claude-code&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Feedback? Issues? PRs welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opencode</category>
      <category>ai</category>
      <category>vim</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How We Gave Every Documentation Example Its Own Test — and Why It Caught Real Bugs</title>
      <dc:creator>Joan Miquel Torres</dc:creator>
      <pubDate>Tue, 03 Mar 2026 17:01:24 +0000</pubDate>
      <link>https://dev.to/bitifet/how-we-gave-every-documentation-example-its-own-test-and-why-it-caught-real-bugs-4k9d</link>
      <guid>https://dev.to/bitifet/how-we-gave-every-documentation-example-its-own-test-and-why-it-caught-real-bugs-4k9d</guid>
      <description>&lt;p&gt;Documentation examples have a dirty secret: they're code, but they're rarely&lt;br&gt;
treated &lt;em&gt;like&lt;/em&gt; code.&lt;/p&gt;

&lt;p&gt;We write them carefully when we publish them, and then — slowly, quietly —&lt;br&gt;
they drift. The API changes. A parameter gets renamed. The behavior shifts&lt;br&gt;
slightly. Yet the docs example keeps running in our heads as the canonical&lt;br&gt;
demonstration, even after it stops being accurate.&lt;/p&gt;

&lt;p&gt;For &lt;a href="https://github.com/bitifet/SmarkForm" rel="noopener noreferrer"&gt;SmarkForm&lt;/a&gt; — a markup-driven form&lt;br&gt;
library whose entire value proposition is its declarative HTML API — this risk&lt;br&gt;
is especially acute. Our documentation site is full of &lt;em&gt;interactive&lt;/em&gt; examples.&lt;br&gt;
People copy them. People &lt;em&gt;trust&lt;/em&gt; them. If they're wrong, the library looks&lt;br&gt;
broken — because to the person reading the docs, the example &lt;em&gt;is&lt;/em&gt; the library.&lt;/p&gt;

&lt;p&gt;This is the story of how we solved it.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Starting Point: A Library With Living Examples
&lt;/h2&gt;

&lt;p&gt;SmarkForm's documentation is a Jekyll site with dozens of&lt;br&gt;
&lt;a href="https://smarkform.bitifet.net/about/showcase" rel="noopener noreferrer"&gt;interactive playground examples&lt;/a&gt;.&lt;br&gt;
Each example is a fully functional mini form, rendered live in the browser,&lt;br&gt;
showing features like nested subforms, variable-length lists, context-driven&lt;br&gt;
hotkeys, and more.&lt;/p&gt;

&lt;p&gt;The examples aren't just screenshots — they use a component called&lt;br&gt;
&lt;code&gt;sampletabs_tpl&lt;/code&gt; that renders a tabbed interface with a live form, the HTML&lt;br&gt;
source, the JavaScript source, and a JSON import/export playground. Anyone&lt;br&gt;
reading the docs can click Import, tweak the JSON, and see the form respond.&lt;/p&gt;

&lt;p&gt;That richness is exactly what makes untested examples so dangerous. There's a&lt;br&gt;
lot of moving parts to get right.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Foundation: Migrating to Playwright
&lt;/h2&gt;

&lt;p&gt;Before we could build co-located tests, we had to fix the foundation. In&lt;br&gt;
October 2025, the existing test suite — Puppeteer + Mocha, over 2,000 lines —&lt;br&gt;
was migrated to &lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt;. This wasn't just a&lt;br&gt;
tooling swap. It opened the door to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Running tests across &lt;strong&gt;Chromium, Firefox, and WebKit&lt;/strong&gt; with a single command.&lt;/li&gt;
&lt;li&gt;A much cleaner API for async interactions.&lt;/li&gt;
&lt;li&gt;Better tracing and debugging when things went wrong.&lt;/li&gt;
&lt;li&gt;A modern, actively maintained ecosystem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Playwright migration itself was a prerequisite for everything that followed.&lt;br&gt;
With a solid, multi-browser foundation in place, the next question became: how&lt;br&gt;
do we use it to test the documentation examples?&lt;/p&gt;


&lt;h2&gt;
  
  
  The Idea: Co-Located Tests
&lt;/h2&gt;

&lt;p&gt;The insight was simple: &lt;strong&gt;put the test right next to the example&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not in a separate &lt;code&gt;test/&lt;/code&gt; folder that grows out of sync. Not in a spreadsheet&lt;br&gt;
of "things to manually verify." Right there, in the same Markdown file, in the&lt;br&gt;
same &lt;code&gt;{% capture %}&lt;/code&gt; block that describes the example.&lt;/p&gt;

&lt;p&gt;Here's what it looks like in practice. A documentation example in SmarkForm's&lt;br&gt;
Jekyll site looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;{% capture my_example_html %}
&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;data-smark&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt; &lt;span class="na"&gt;data-smark&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-smark=&lt;/span&gt;&lt;span class="s"&gt;'{"action":"export"}'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Save&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
{% endcapture %}

{% include components/sampletabs_tpl.md
    formId="my_example"
    htmlSource=my_example_html
    tests=false
%}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tests=false&lt;/code&gt; means: &lt;em&gt;I know there's no custom test here; that's intentional&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;But for examples where behavior matters, you add a capture block with real&lt;br&gt;
Playwright test code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;{% capture my_example_tests %}
export default async ({ page, expect, id, root, readField, writeField }) =&amp;gt; {
    await expect(root).toBeVisible();&lt;span class="sb"&gt;

    const input = root.locator('input[name="username"]');
    await input.fill('alice');

    expect(await readField('username')).toBe('alice');
&lt;/span&gt;};
{% endcapture %}

{% include components/sampletabs_tpl.md
    formId="my_example"
    htmlSource=my_example_html
    tests=my_example_tests
%}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;export default async function&lt;/code&gt; is a real Playwright test. It gets&lt;br&gt;
extracted, executed, and reported as a test result — for every browser.&lt;/p&gt;


&lt;h2&gt;
  
  
  Keeping It Navigable: Vim Fold Markers
&lt;/h2&gt;

&lt;p&gt;There's a side effect of co-location that isn't immediately obvious: the&lt;br&gt;
documentation files get &lt;em&gt;long&lt;/em&gt;. Very long. A single showcase page might contain&lt;br&gt;
a dozen &lt;code&gt;{% capture %}&lt;/code&gt; blocks — HTML source, CSS source, JavaScript, notes,&lt;br&gt;
and now tests — each running tens or hundreds of lines.&lt;/p&gt;

&lt;p&gt;The file is perfectly machine-readable. For a human editor reviewing or&lt;br&gt;
updating a specific example, it can become a wall of text.&lt;/p&gt;

&lt;p&gt;The SmarkForm project's solution to this is &lt;strong&gt;vim fold markers&lt;/strong&gt; — a&lt;br&gt;
convention borrowed directly from how &lt;code&gt;src/&lt;/code&gt; files are written — applied to&lt;br&gt;
the Markdown documentation files as well.&lt;/p&gt;

&lt;p&gt;In the library source code, functions are bracketed like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="c1"&gt;//{{{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... implementation&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="c1"&gt;//}}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In documentation Markdown files, capture blocks are bracketed with HTML&lt;br&gt;
comments wrapped in &lt;code&gt;{% raw %}&lt;/code&gt; to prevent Jekyll from consuming them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;{% raw %} &lt;span class="c"&gt;&amp;lt;!-- basic_form_html {{{ --&amp;gt;&lt;/span&gt; {% endraw %}
{% capture basic_form_html %}
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;data-smark&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Name:&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;data-smark&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
{% endcapture %}{% raw %} &lt;span class="c"&gt;&amp;lt;!-- }}} --&amp;gt;&lt;/span&gt; {% endraw %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The closing marker sits on the &lt;strong&gt;same line&lt;/strong&gt; as &lt;code&gt;{% endcapture %}&lt;/code&gt; — one&lt;br&gt;
less line to scroll past. When a capture's closing &lt;code&gt;%}&lt;/code&gt; falls on its own&lt;br&gt;
line (a formatting choice sometimes made for long captures), the closing&lt;br&gt;
marker follows on the next line instead.&lt;/p&gt;

&lt;p&gt;With vim's &lt;code&gt;foldmethod=marker&lt;/code&gt; active, every capture block collapses to a&lt;br&gt;
single line — just the fold label. A documentation file with twelve captures&lt;br&gt;
becomes twelve navigable lines. You can jump directly to the example you want,&lt;br&gt;
expand it, edit it, and collapse it again — without losing your place in the&lt;br&gt;
surrounding structure.&lt;/p&gt;
&lt;h3&gt;
  
  
  Enabling It
&lt;/h3&gt;

&lt;p&gt;The project ships configuration files for the three most common editors, so&lt;br&gt;
this works automatically once a one-time prerequisite is met.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vim / Neovim&lt;/strong&gt; — native support, no plugins required.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;set exrc&lt;/code&gt; to your &lt;code&gt;~/.vimrc&lt;/code&gt; or &lt;code&gt;~/.config/nvim/init.vim&lt;/code&gt; once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nb"&gt;exrc&lt;/span&gt;   " allow project&lt;span class="p"&gt;-&lt;/span&gt;level &lt;span class="p"&gt;.&lt;/span&gt;vimrc &lt;span class="k"&gt;files&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nb"&gt;secure&lt;/span&gt; " optional&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;sandbox&lt;/span&gt; project vimrc &lt;span class="p"&gt;(&lt;/span&gt;recommended&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the project's &lt;code&gt;.vimrc&lt;/code&gt; takes over and sets &lt;code&gt;foldmethod=marker&lt;/code&gt;&lt;br&gt;
whenever you open a file in the project directory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VS Code&lt;/strong&gt; — install the recommended extension.&lt;/p&gt;

&lt;p&gt;When you open the project in VS Code, you'll see a prompt to install&lt;br&gt;
recommended extensions. Accept it to install&lt;br&gt;
&lt;a href="https://marketplace.visualstudio.com/items?itemName=jmfirth.vscode-custom-folding" rel="noopener noreferrer"&gt;Custom Folding&lt;/a&gt;&lt;br&gt;
(listed in &lt;code&gt;.vscode/extensions.json&lt;/code&gt;). The &lt;code&gt;.vscode/settings.json&lt;/code&gt; file&lt;br&gt;
already configures &lt;code&gt;{{{&lt;/code&gt;/&lt;code&gt;}}}&lt;/code&gt; as the fold markers, so no further setup is&lt;br&gt;
needed. Once installed, &lt;code&gt;Fold All&lt;/code&gt; (&lt;code&gt;Ctrl+K, Ctrl+0&lt;/code&gt;) collapses all marked&lt;br&gt;
blocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Emacs&lt;/strong&gt; — install the &lt;code&gt;folding&lt;/code&gt; package once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;M-x package-install RET folding RET
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the project's &lt;code&gt;.dir-locals.el&lt;/code&gt; enables &lt;code&gt;folding-mode&lt;/code&gt;&lt;br&gt;
automatically whenever Emacs opens a file in the project directory.&lt;/p&gt;

&lt;p&gt;Even if your editor doesn't support fold markers natively, the pattern is still&lt;br&gt;
useful: the &lt;code&gt;{{{&lt;/code&gt; / &lt;code&gt;}}}&lt;/code&gt; strings are greppable labels. A quick &lt;code&gt;grep '{{{' showcase.md&lt;/code&gt;&lt;br&gt;
gives you an instant index of all captures in a file.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Architecture: Collector + Runner
&lt;/h2&gt;

&lt;p&gt;Under the hood, this works through a two-phase pipeline.&lt;/p&gt;
&lt;h3&gt;
  
  
  Phase 1: The Collector
&lt;/h3&gt;

&lt;p&gt;A Node.js script (&lt;code&gt;scripts/collect-docs-examples.js&lt;/code&gt;) scans the entire &lt;code&gt;docs/&lt;/code&gt;&lt;br&gt;
folder. For each Markdown file, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extracts all &lt;code&gt;{% capture %}&lt;/code&gt; blocks into an in-memory map.&lt;/li&gt;
&lt;li&gt;Finds every &lt;code&gt;{% include components/sampletabs_tpl.md %}&lt;/code&gt; call.&lt;/li&gt;
&lt;li&gt;Resolves each parameter: &lt;code&gt;htmlSource&lt;/code&gt;, &lt;code&gt;cssSource&lt;/code&gt;, &lt;code&gt;jsSource&lt;/code&gt;, &lt;code&gt;tests&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;Applies transformations:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;$$&lt;/code&gt; → &lt;code&gt;-${formId}&lt;/code&gt; (ensures unique DOM IDs per example)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;█&lt;/code&gt; (a filled square character) → four spaces (an indentation hack for
Jekyll, which strips leading whitespace in liquid captures)&lt;/li&gt;
&lt;li&gt;Strips &lt;code&gt;!important&lt;/code&gt; from CSS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Skips purely illustrative examples (&lt;code&gt;jsSource="-"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Writes everything to a JSON manifest at &lt;code&gt;test/.cache/docs_examples.json&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The collector also handles some subtleties. Jekyll filters like&lt;br&gt;
&lt;code&gt;{{ variable | replace: "old", "new" }}&lt;/code&gt; are simulated in JavaScript so the&lt;br&gt;
resolved content is identical to what Jekyll would produce. Docs-only&lt;br&gt;
parameters like &lt;code&gt;demoValue&lt;/code&gt; (which seeds a default value in the rendered page&lt;br&gt;
but is irrelevant to tests) are explicitly stripped.&lt;/p&gt;
&lt;h3&gt;
  
  
  Phase 2: The Runner
&lt;/h3&gt;

&lt;p&gt;A Playwright test file (&lt;code&gt;test/co_located_tests.tests.js&lt;/code&gt;) loads the manifest&lt;br&gt;
and generates one test per example. For each example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It assembles a minimal HTML page containing the example's HTML, CSS, and
JavaScript, plus a &lt;code&gt;&amp;lt;script src="/dist/SmarkForm.umd.js"&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It writes that page to a temp file and serves it via the local test server.&lt;/li&gt;
&lt;li&gt;It navigates Playwright to the page and waits for SmarkForm to initialize.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smoke checks&lt;/strong&gt; always run: the form container is visible; console error
and page error counts match expectations.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;tests&lt;/code&gt; is a code string (not &lt;code&gt;"false"&lt;/code&gt;), the code is written to a
temporary &lt;code&gt;.mjs&lt;/code&gt; file and dynamically imported. The exported default
function is called with &lt;code&gt;{ page, expect, id, root, readField, writeField }&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The helpers passed to each test function are worth noting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;root&lt;/code&gt; — a Playwright locator pointing to &lt;code&gt;#myForm-${id}&lt;/code&gt;, the example's
container.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;readField(path)&lt;/code&gt; — exports a field's current value via SmarkForm's own
&lt;code&gt;.find(path).export()&lt;/code&gt; API.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;writeField(path, value)&lt;/code&gt; — imports a value into a field.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means co-located tests can test behavior at the SmarkForm API level, not&lt;br&gt;
just at the DOM level. You can assert &lt;code&gt;expect(await readField('price')).toBe(42)&lt;/code&gt;&lt;br&gt;
instead of digging into the DOM for the raw input value.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Enforcement Rule
&lt;/h3&gt;

&lt;p&gt;One critical design decision: &lt;strong&gt;every example must declare &lt;code&gt;tests=...&lt;/code&gt;&lt;/strong&gt;, even&lt;br&gt;
if just &lt;code&gt;tests=false&lt;/code&gt;. If an example is missing the parameter, the test suite&lt;br&gt;
fails with a clear message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Example showcase.md_basic_form is missing co-located tests.
Please add a tests= parameter to the {% include %} block,
or use tests=false to explicitly disable testing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This enforcement ensures the question "does this example have a test?" is&lt;br&gt;
always explicitly answered. You can't accidentally omit it. The quality floor&lt;br&gt;
only goes up.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Naming Evolution
&lt;/h2&gt;

&lt;p&gt;The system went through a quick naming evolution that's worth mentioning because&lt;br&gt;
it reflects the conceptual clarity that emerged over time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first test harness was called &lt;code&gt;docs_examples.tests.js&lt;/code&gt; — named after
what it tested (docs examples).&lt;/li&gt;
&lt;li&gt;It was soon renamed to &lt;code&gt;co_located_tests.tests.js&lt;/code&gt; — named after the
&lt;em&gt;strategy&lt;/em&gt; (tests living alongside the code they test).&lt;/li&gt;
&lt;li&gt;A companion file, &lt;code&gt;co_located_tests_validation.tests.js&lt;/code&gt;, handles the
meta-level: it tests the manifest itself, verifying that every example has
valid &lt;code&gt;tests&lt;/code&gt; and error-count declarations, and that transformations
(no stray &lt;code&gt;$$&lt;/code&gt;, no &lt;code&gt;█&lt;/code&gt; characters) were applied correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The naming made it clearer that the &lt;em&gt;principle&lt;/em&gt; — co-location — was the&lt;br&gt;
important thing, not the specific mechanism.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Workflow: Interactive Test Picking
&lt;/h2&gt;

&lt;p&gt;Writing tests is one thing. &lt;em&gt;Running a single test while you're developing&lt;/em&gt;&lt;br&gt;
is another. The co-located tests are loaded and run by Playwright, which means&lt;br&gt;
you can already filter them with &lt;code&gt;-g&lt;/code&gt; and &lt;code&gt;--project&lt;/code&gt;. But the project added&lt;br&gt;
something nicer: an interactive test picker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run &lt;span class="nb"&gt;test&lt;/span&gt;:pick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This drops you into an interactive shell menu where you choose:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What to test&lt;/strong&gt;: regular tests, co-located tests (all), or co-located
tests for a specific documentation file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which example&lt;/strong&gt;: if you chose a specific file, which &lt;code&gt;formId&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which browser&lt;/strong&gt;: Chromium, Firefox, or WebKit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After selecting, it builds the right Playwright command and runs it. It also&lt;br&gt;
remembers your last choice, so a &lt;code&gt;--repeat&lt;/code&gt; flag lets you quickly re-run the&lt;br&gt;
same test after changing something.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run &lt;span class="nb"&gt;test&lt;/span&gt;:pick &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--repeat&lt;/span&gt; &lt;span class="nt"&gt;--headed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This combination — pick once, repeat with &lt;code&gt;--headed&lt;/code&gt; or &lt;code&gt;--debug&lt;/code&gt; — was a&lt;br&gt;
genuine quality-of-life improvement for the write-test-fix loop.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bugs It Caught
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. Writing tests for existing examples&lt;br&gt;
immediately surfaced bugs that had been lurking unnoticed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Hotkeys State Bug
&lt;/h3&gt;

&lt;p&gt;While writing a test for the 2nd-level hotkeys example in the showcase, the&lt;br&gt;
author (&lt;a href="https://github.com/bitifet" rel="noopener noreferrer"&gt;@bitifet&lt;/a&gt;) encoded an expectation that&lt;br&gt;
seemed obvious: &lt;em&gt;releasing the Alt key while Ctrl is held should return to&lt;br&gt;
the 1st-level hotkeys display&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The test was committed with a note: &lt;em&gt;"It fails while checking that releasing&lt;br&gt;
ALT returns to previous status if Ctrl is hold. But this is a REAL bug, so&lt;br&gt;
the test is Ok."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The very next commit fixed &lt;code&gt;src/lib/hotkeys.js&lt;/code&gt;. The bug had existed in the&lt;br&gt;
library, silently, until a documentation example was tested.&lt;/p&gt;

&lt;h3&gt;
  
  
  The datetime-local Naming Bug
&lt;/h3&gt;

&lt;p&gt;When the &lt;code&gt;datetime-local&lt;/code&gt; component type was added, a co-located test caught&lt;br&gt;
a naming inconsistency: the example source used &lt;code&gt;"datetimeLocal"&lt;/code&gt; (camelCase)&lt;br&gt;
while the implementation expected &lt;code&gt;"datetime-local"&lt;/code&gt; (kebab-case). The test&lt;br&gt;
failed; the example source was corrected.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Smoke Check Safety Net
&lt;/h3&gt;

&lt;p&gt;Beyond specific behavioral tests, the smoke checks — which run for &lt;em&gt;every&lt;/em&gt;&lt;br&gt;
example — have caught examples that failed to initialize at all due to&lt;br&gt;
a breaking API change. If an example produces a console error it's not&lt;br&gt;
expected to, the test fails immediately. No manual clicking through the docs&lt;br&gt;
required.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;At the time of the 0.12.6 release (which introduced co-located tests), the&lt;br&gt;
library described it this way in the changelog:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Major improvements to testing infrastructure and coverage:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrated the test suite to Playwright, covering Chromium, Firefox, and WebKit.&lt;/li&gt;
&lt;li&gt;Added smoke tests for all examples in the documentation.&lt;/li&gt;
&lt;li&gt;Co-located, feature-specific tests for each example are now possible — and enforced.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The suite covers dozens of interactive documentation examples, each exercised&lt;br&gt;
across three browsers, all driven by the same living documentation that users&lt;br&gt;
read and trust.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Makes This Approach Work
&lt;/h2&gt;

&lt;p&gt;A few principles made the co-located test strategy effective in SmarkForm:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The test is right next to what it tests.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's no context-switching between "the docs example" and "the test for the&lt;br&gt;
docs example." They're in the same file, a few lines apart. When you update&lt;br&gt;
the example, you immediately see its test and update it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Tests are enforced, not optional.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The "you must declare &lt;code&gt;tests=&lt;/code&gt; or &lt;code&gt;tests=false&lt;/code&gt;" rule means there's no&lt;br&gt;
ambiguity. It's not a linting warning; it's a test failure. Every example is&lt;br&gt;
accounted for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The test interface is ergonomic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;readField&lt;/code&gt; / &lt;code&gt;writeField&lt;/code&gt; helpers let you interact with the form at&lt;br&gt;
the SmarkForm API level. You don't have to know whether a number field renders&lt;br&gt;
as an &lt;code&gt;&amp;lt;input type="number"&amp;gt;&lt;/code&gt; or something else — you just call&lt;br&gt;
&lt;code&gt;readField('price')&lt;/code&gt; and assert on the returned value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The test picker reduces friction.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When testing one specific example in one specific browser is two menu selections&lt;br&gt;
away, you do it more often. Lower friction → more tests written → more bugs&lt;br&gt;
found earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The examples are isolated.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each example gets its own minimal HTML page, its own SmarkForm instance, its&lt;br&gt;
own error tracking. There's no bleed-through between examples. The smoke checks&lt;br&gt;
for one example don't affect another.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaways for Your Own Project
&lt;/h2&gt;

&lt;p&gt;If your project has a documentation site with interactive examples, here's the&lt;br&gt;
core idea distilled:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat documentation examples as first-class test subjects.&lt;/strong&gt; They're not&lt;br&gt;
just prose — they're code that runs in your users' browsers (or in their&lt;br&gt;
heads when they copy-paste). Test them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Put the test near the example.&lt;/strong&gt; The closer the test is to what it tests,&lt;br&gt;
the more likely it will be maintained. Co-location is the key.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enforce test presence.&lt;/strong&gt; An optional test is a test that won't get written.&lt;br&gt;
Make it a breaking build failure when a test is absent and undeclared.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Invest in ergonomic helpers.&lt;/strong&gt; The test interface should feel natural to&lt;br&gt;
someone who knows the library API. If writing a test requires knowing the&lt;br&gt;
DOM structure, that's a leaky abstraction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Make the test loop fast.&lt;/strong&gt; A test picker, a &lt;code&gt;--repeat&lt;/code&gt; flag, a &lt;code&gt;--headed&lt;/code&gt;&lt;br&gt;
mode — any investment in the write-test-fix loop pays compound interest.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Co-located tests for documentation examples started as an experiment in the&lt;br&gt;
SmarkForm project and quickly became one of the most practically impactful&lt;br&gt;
improvements to the development workflow. They've caught real bugs — bugs that&lt;br&gt;
had existed in the library, invisible, until someone wrote down what the correct&lt;br&gt;
behavior &lt;em&gt;should&lt;/em&gt; be and a test confirmed it wasn't.&lt;/p&gt;

&lt;p&gt;More than the bugs, though, what co-located tests provide is &lt;em&gt;confidence&lt;/em&gt; — the&lt;br&gt;
confidence to refactor, to add a feature, to change a behavior, and know that&lt;br&gt;
the documentation examples will tell you immediately if you've broken something&lt;br&gt;
that matters.&lt;/p&gt;

&lt;p&gt;Documentation and tests have always had a troubled relationship. Co-location is&lt;br&gt;
one way to give them a common home — and to stop treating the docs as a place&lt;br&gt;
where bugs go to hide.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;SmarkForm is an open-source, markup-driven form library for building complex&lt;br&gt;
HTML forms with nested subforms, variable-length lists, and JSON import/export.&lt;br&gt;
→ &lt;a href="https://smarkform.bitifet.net" rel="noopener noreferrer"&gt;smarkform.bitifet.net&lt;/a&gt; · &lt;a href="https://github.com/bitifet/SmarkForm" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>automation</category>
      <category>documentation</category>
      <category>testing</category>
    </item>
    <item>
      <title>Generating a Parametric SVG Logo with Pug</title>
      <dc:creator>Joan Miquel Torres</dc:creator>
      <pubDate>Mon, 02 Feb 2026 23:22:00 +0000</pubDate>
      <link>https://dev.to/bitifet/generating-a-parametric-svg-logo-with-pug-8m0</link>
      <guid>https://dev.to/bitifet/generating-a-parametric-svg-logo-with-pug-8m0</guid>
      <description>&lt;p&gt;Designing a logo is usually treated as a one-off, visual task.&lt;br&gt;
Designing a &lt;em&gt;logo system&lt;/em&gt;—with variants, themes, sizes, and guarantees of consistency—is a very different problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn44fa3v5i5jwvxy0ke0v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn44fa3v5i5jwvxy0ke0v.png" alt="New SmarkForm logo" width="406" height="76"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article I’ll show how I ended up generating &lt;strong&gt;real SVG logo files&lt;/strong&gt;, fully parametric, using &lt;strong&gt;Pug&lt;/strong&gt;, &lt;strong&gt;embedded and subsetted fonts&lt;/strong&gt;, and a &lt;strong&gt;CLI-driven pipeline&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This approach has been used to create a &lt;strong&gt;new &lt;em&gt;SmarkForm&lt;/em&gt; logo&lt;/strong&gt;, which will &lt;strong&gt;replace the current one starting from upcoming version 0.13.0&lt;/strong&gt;.&lt;br&gt;
The logo is not just an asset anymore: it is &lt;em&gt;generated&lt;/em&gt;, reproducible, and version-controlled.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://smarkform.bitifet.net" rel="noopener noreferrer"&gt;SmarkForm&lt;/a&gt;&lt;/strong&gt; is a free and open-source toolkit for &lt;strong&gt;declarative, markup-driven form generation&lt;/strong&gt; — from simple inputs to complex nested forms and dynamic lists, with first-class &lt;strong&gt;JSON import/export&lt;/strong&gt;. It is framework-agnostic and styling-agnostic by design, and &lt;strong&gt;that same philosophy now applies to its branding&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;single logo definition&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Variants for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;light / dark&lt;/li&gt;
&lt;li&gt;monochrome&lt;/li&gt;
&lt;li&gt;compact / full&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fonts &lt;strong&gt;embedded and subsetted&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Output as &lt;strong&gt;real &lt;code&gt;.svg&lt;/code&gt; files&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generated via &lt;strong&gt;CLI&lt;/strong&gt;, not a browser&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reproducible and scriptable&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short: &lt;strong&gt;branding as code&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Pug?
&lt;/h2&gt;

&lt;p&gt;Pug gives you three things that are surprisingly powerful for SVG work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Logic&lt;/strong&gt; (conditions, defaults, variants)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parametrization&lt;/strong&gt; (options passed from CLI)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readable structure&lt;/strong&gt; for complex SVG trees&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;SVG is XML.&lt;br&gt;
Pug is extremely good at generating structured XML.&lt;/p&gt;

&lt;p&gt;That combination is criminally underrated.&lt;/p&gt;


&lt;h2&gt;
  
  
  Turning SVG into a First-Class Template
&lt;/h2&gt;

&lt;p&gt;The key decision was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don’t generate HTML that &lt;em&gt;contains&lt;/em&gt; SVG.&lt;br&gt;
Generate SVG directly so that it can be linked everywhere.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;doctype svg&lt;/code&gt; instead of (default in Pug) &lt;code&gt;doctype html&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The root node is &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Follow svg specs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  A Pug mixin as the logo engine
&lt;/h3&gt;

&lt;p&gt;Implementing the svg as a Pug mixin let me actually start with an HTML document showcasing different parameters combinations.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Even colors and fonts were originally parametyzed &lt;strong&gt;allowing to examine several configurations side by side&lt;/strong&gt; until the winner was choosen.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At the end of the process only functional parameters have been kept.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mixin smarkformLogo(options = {})
  -
    const {
      mode = 'light',
      compact = false,
      monochrome = false,
      size = 100
    } = options;

    const height = size;
    const width  = compact
      ? Math.round(height * 1.105)
      : Math.round(height * 4.1);

  svg(
    xmlns="http://www.w3.org/2000/svg"
    width=width
    height=height
    viewBox=`0 0 ${width} ${height}`
    role="img"
    aria-label="SmarkForm logo"
  )
    // SVG content here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point we already have something valuable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dimensions are computed&lt;/li&gt;
&lt;li&gt;Variants are driven by data&lt;/li&gt;
&lt;li&gt;The SVG is no longer static&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Choosing and Subsetting Fonts
&lt;/h2&gt;

&lt;p&gt;External fonts are fragile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network-dependent&lt;/li&gt;
&lt;li&gt;Inconsistent&lt;/li&gt;
&lt;li&gt;Sometimes forbidden in branding assets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the decision was to &lt;strong&gt;embed the font directly in the SVG&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Picking a font with the right license
&lt;/h3&gt;

&lt;p&gt;For SmarkForm I used &lt;strong&gt;Work Sans&lt;/strong&gt;, available from&lt;br&gt;
👉 &lt;a href="https://fonts.google.com" rel="noopener noreferrer"&gt;https://fonts.google.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Google Fonts is particularly useful because you can filter by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open-source licenses (SIL Open Font License, Apache 2.0, etc.)&lt;/li&gt;
&lt;li&gt;Font weights&lt;/li&gt;
&lt;li&gt;Variable fonts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes it easy to ensure your branding assets are legally safe to embed and redistribute.&lt;/p&gt;
&lt;h3&gt;
  
  
  Subsetting the font
&lt;/h3&gt;

&lt;p&gt;Instead of embedding a full font file, I generated &lt;strong&gt;subsetted WOFF2 files&lt;/strong&gt; containing only the glyphs actually used in the logo.&lt;/p&gt;

&lt;p&gt;This keeps the SVG:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Smaller&lt;/li&gt;
&lt;li&gt;Faster to load&lt;/li&gt;
&lt;li&gt;More intentional&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools like &lt;code&gt;pyftsubset&lt;/code&gt; (from fonttools) are perfect for this step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Examnple:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pyftsubset static/WorkSans-Regular.ttf &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;SmarkForm}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--flavor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;woff2 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;WorkSans-SmarkForm-Regular.woff2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;In my case, for stylistic reasons, I used two different faces of the font so I had to do this twice (once for WorkSans-Regular and one for WorkSans-SemiBold).&lt;/p&gt;

&lt;p&gt;You can stick with one or pick for even different fonts. I reckon keeping things simpler is better for a logo though.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Embedding Fonts via Base64
&lt;/h2&gt;

&lt;p&gt;Once you have your subsetted &lt;code&gt;.woff2&lt;/code&gt;, embedding it is straightforward.&lt;/p&gt;

&lt;p&gt;From the shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;base64&lt;/span&gt; &amp;lt; WorkSans-SmarkForm-Regular.woff2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inline it into the template.&lt;/p&gt;

&lt;p&gt;A small stylistic trick I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WorkSans_regular_b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
  base64 &amp;lt; WorkSans-SmarkForm-Regular.woff2
`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Declaring binary (base64) data in a variable at the beginning keeps the actual code cleaner afterwards.&lt;/li&gt;
&lt;li&gt;The backticks sit &lt;em&gt;outside&lt;/em&gt; the base64 block, so alignment stays clean.&lt;/li&gt;
&lt;li&gt;Newlines and spaces are stripped afterwards (avoiding css issues).&lt;/li&gt;
&lt;li&gt;Assuming the file is in the current directory, in Vim you can simply type
&lt;code&gt;:vip!bash&lt;/code&gt; to insert in place the file contents encoded in base64.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A subtle but important detail
&lt;/h3&gt;

&lt;p&gt;Base64 &lt;strong&gt;itself&lt;/strong&gt; allows arbitrary whitespace and newlines.&lt;br&gt;
CSS &lt;strong&gt;does not&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you forget to strip whitespace, your font may silently fail to load.&lt;br&gt;
This is one of those details that’s obvious &lt;em&gt;after&lt;/em&gt; you hit it once.&lt;/p&gt;


&lt;h2&gt;
  
  
  Wiring Fonts to SVG Text via CSS
&lt;/h2&gt;

&lt;p&gt;Fonts are referenced via CSS inside &lt;code&gt;&amp;lt;defs&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defs
  style.
    @font-face {
      font-family: 'WS-Regular';
      src: url('data:font/woff2;base64,#{WorkSans_regular_b64}') format('woff2');
    }

    @font-face {
      font-family: 'WS-SemiBold';
      src: url('data:font/woff2;base64,#{WorkSans_SemiBold_b64}') format('woff2');
    }

    text.regular {
      font-family: 'WS-Regular';
      dominant-baseline: middle;
    }

    text.semibold {
      font-family: 'WS-SemiBold';
      dominant-baseline: middle;
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.regular&lt;/code&gt; / &lt;code&gt;.semibold&lt;/code&gt; classes don’t mean anything special by themselves —&lt;br&gt;
they’re simply a clean way to bind &lt;strong&gt;specific font weights and behaviors&lt;/strong&gt; to specific parts of the logo.&lt;/p&gt;


&lt;h2&gt;
  
  
  Rendering Text Safely in SVG
&lt;/h2&gt;

&lt;p&gt;One small but important gotcha when generating SVG with Pug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;text.regular(
  x=lmargin
  y=height / 2
  font-size=height * 0.6
  fill=primaryColor
)='&amp;lt;'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;lt;&lt;/code&gt; &lt;strong&gt;must be passed as a string&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Otherwise it’s interpreted as markup and breaks the SVG.&lt;br&gt;
Once you do this explicitly, Pug behaves exactly as expected&lt;br&gt;
by replacing them with their correspondent HTML entities.&lt;/p&gt;


&lt;h2&gt;
  
  
  Compact vs Full Variants (Same Source)
&lt;/h2&gt;

&lt;p&gt;With logic in place, variants are trivial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if compact
  text.semibold(...) 'S'
  text.semibold(...) '}'
else
  text.regular(...) '&amp;lt;'
  text.semibold(...) 'Smark'
  text.semibold(...) 'Form'
  text.semibold(...) '}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No duplication.&lt;br&gt;
No parallel files.&lt;br&gt;
One definition, many outcomes.&lt;/p&gt;


&lt;h2&gt;
  
  
  Generating Real SVG Files from the CLI
&lt;/h2&gt;

&lt;p&gt;This is the payoff.&lt;/p&gt;

&lt;p&gt;Once the template outputs &lt;strong&gt;only SVG&lt;/strong&gt;, you can do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx pug-cli &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-O&lt;/span&gt; &lt;span class="s1"&gt;'{ compact: true, mode: "dark" }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &amp;lt; smarkform_logo.pug &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; smarkform_compact_dark.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that file is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A valid SVG&lt;/li&gt;
&lt;li&gt;Fully self-contained&lt;/li&gt;
&lt;li&gt;Embeddable anywhere&lt;/li&gt;
&lt;li&gt;Diff-friendly&lt;/li&gt;
&lt;li&gt;Reproducible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can script &lt;strong&gt;all variants&lt;/strong&gt; in seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fobvgzo96expf5akp7w4e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fobvgzo96expf5akp7w4e.png" alt="SmarkForm logo variants" width="589" height="577"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Integration into the SmarkForm project
&lt;/h2&gt;

&lt;p&gt;Integrating the new logo into the &lt;em&gt;SmarkForm&lt;/em&gt; codebase turned out to be refreshingly simple.&lt;/p&gt;

&lt;p&gt;The existing assets already lived under /docs/assets, so I introduced a new logo directory for better organization, with a src subdirectory to hold the source files.&lt;/p&gt;

&lt;p&gt;The Pug template that generates the logo variants now lives in &lt;code&gt;/docs/assets/logo/src&lt;/code&gt;, alongside a small Bash script responsible for rendering the desired SVG outputs. The script is intentionally minimal and largely self-documenting, making it easy to adjust or extend if new variants are needed in the future.&lt;/p&gt;

&lt;p&gt;Importantly, this script is not part of the build process. The generated SVG files are stable, self-contained assets and remain valid until the logo source itself changes. In practice, this means the script only needs to be executed when the Pug template is modified—keeping the build pipeline clean and avoiding unnecessary regeneration work.&lt;/p&gt;

&lt;p&gt;You can find both the generator template and the accompanying script in the aforementioned &lt;a href="https://github.com/bitifet/SmarkForm/tree/main/docs/assets/logo/src" rel="noopener noreferrer"&gt;/docs/assets/logo/src&lt;/a&gt; directory in the SmarkForm GitHub repository.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Scales
&lt;/h2&gt;

&lt;p&gt;This approach scales because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logos become &lt;strong&gt;data-driven&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Branding lives &lt;strong&gt;inside the repository&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Changes are auditable&lt;/li&gt;
&lt;li&gt;Variants never drift out of sync&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just as importantly, it respects SmarkForm’s philosophy:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;SmarkForm is markup-driven and styling-agnostic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The branding system does &lt;strong&gt;not&lt;/strong&gt; impose a visual identity on user forms.&lt;br&gt;
Instead, it provides optional, lightweight visual cues — icons or small banners — so end users may recognize a form as being powered by SmarkForm and know what kind of experience to expect.&lt;/p&gt;

&lt;p&gt;Developers and designers remain fully in control.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This started as “I just want a logo SVG”.&lt;/p&gt;

&lt;p&gt;It ended as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A parametric branding system&lt;/li&gt;
&lt;li&gt;A CLI-driven asset pipeline&lt;/li&gt;
&lt;li&gt;A single source of truth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you already treat &lt;strong&gt;UI as code&lt;/strong&gt;,&lt;br&gt;
there’s no good reason your &lt;strong&gt;branding assets&lt;/strong&gt; shouldn’t be treated the same way.&lt;/p&gt;

&lt;p&gt;Once you do it like this, going back feels… unnecessary 😉&lt;/p&gt;

</description>
      <category>automation</category>
      <category>design</category>
      <category>frontend</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>PostgreSQL and high availability in Node.js</title>
      <dc:creator>Joan Miquel Torres</dc:creator>
      <pubDate>Mon, 12 Aug 2024 17:54:04 +0000</pubDate>
      <link>https://dev.to/bitifet/postgresql-and-high-availability-in-nodej-5akj</link>
      <guid>https://dev.to/bitifet/postgresql-and-high-availability-in-nodej-5akj</guid>
      <description>&lt;p&gt;For those using PostgreSQL with Node.JS and worried about High Availability and outage resilience, I have published this wrapper solving important issues in node-pg:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/bitifet/node-postgres-ha" rel="noopener noreferrer"&gt;https://github.com/bitifet/node-postgres-ha&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am looking forward to cherry-pick the improvements as PRs to actual node-postgres but, by now, it fits our goals of preventing issues derived from eventual server or network malfunctions.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Simulate TCP network congestion with netjam</title>
      <dc:creator>Joan Miquel Torres</dc:creator>
      <pubDate>Tue, 06 Aug 2024 21:24:17 +0000</pubDate>
      <link>https://dev.to/bitifet/simulate-tcp-network-congestion-with-netjam-5dm9</link>
      <guid>https://dev.to/bitifet/simulate-tcp-network-congestion-with-netjam-5dm9</guid>
      <description>&lt;p&gt;Network issues always come in production.&lt;/p&gt;

&lt;p&gt;We tend to, at most, perform some stress tests to check what happen in&lt;br&gt;
(unrealistic) high load situations or even stop some critical services,&lt;br&gt;
like databases, to see how resilient is our application or service..&lt;/p&gt;

&lt;p&gt;But the truth is that there can be worst situations...&lt;/p&gt;

&lt;p&gt;I mean: If your database goes down, you'll rapidly get errors due to TCP&lt;br&gt;
connection attempts being rejected.&lt;/p&gt;

&lt;p&gt;But, when network speed decreases due to the amount of traffic, network errors,&lt;br&gt;
etc... You may end up with lots of pending requests that will never end causing&lt;br&gt;
much more trouble that could be easily avoided if we had properly detected the&lt;br&gt;
situation.&lt;/p&gt;

&lt;p&gt;To be able to address that situations we need to reproduce them, at least at&lt;br&gt;
some level.&lt;/p&gt;

&lt;p&gt;As a first approach, I tried with &lt;em&gt;iptables&lt;/em&gt; DROP rules, but this is drastic&lt;br&gt;
solution: Either you have a perfect connection or all packets are lost. And I&lt;br&gt;
wanted to play with several &lt;a href="https://node-postgres.com/apis/client#new-client" rel="noopener noreferrer"&gt;PostgreSQL&lt;br&gt;
timeouts&lt;/a&gt; and their effect in&lt;br&gt;
different situations.&lt;/p&gt;

&lt;p&gt;Finally I ended up implementing my own tool to simulate different levels of&lt;br&gt;
traffic congestion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.npmjs.com/package/netjam" rel="noopener noreferrer"&gt;Npm package&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/bitifet/netjam" rel="noopener noreferrer"&gt;Git repository&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don't know if there are other similar tools out there (at least I haven't&lt;br&gt;
found any until now: If you know any, please tell me in the comments).&lt;/p&gt;

&lt;p&gt;...And it's still far from perfect.&lt;/p&gt;

&lt;p&gt;But, at least, it helped me to learn a lot about how PostgreSQL timeouts work&lt;br&gt;
and the effects they can have in different situations.&lt;/p&gt;

&lt;p&gt;And it is a very simple-to-use tool. Specially now that I bundled it as a &lt;em&gt;npm&lt;/em&gt;&lt;br&gt;
package so that, if you have Node/NPM in your system, you only need to execute&lt;br&gt;
the following to get started:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx netjam &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, to create a &lt;em&gt;jamable&lt;/em&gt; TCP tunnel to local PostgreSQL Server&lt;br&gt;
listening in its default port (5432) you only need to execute the following&lt;br&gt;
command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx netjam localhost 5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you will get something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Server listening on port 5000

STATUS:
┌─────────────┬────────────────────────────┐
│   &lt;span class="o"&gt;(&lt;/span&gt;index&lt;span class="o"&gt;)&lt;/span&gt;   │           Values           │
├─────────────┼────────────────────────────┤
│ remoteHost  │        &lt;span class="s1"&gt;'localhost'&lt;/span&gt;         │
│ remotePort  │           &lt;span class="s1"&gt;'5432'&lt;/span&gt;           │
│ listenPort  │            5000            │
│  timestamp  │ &lt;span class="s1"&gt;'2024-08-06T18:52:11.042Z'&lt;/span&gt; │
│   waiting   │             0              │
│    open     │             0              │
│   closed    │             0              │
│  withError  │             0              │
│     tx      │             0              │
│     rx      │             0              │
│  iputDelay  │             0              │
│ outputDelay │             0              │
│ logInterval │       &lt;span class="s1"&gt;'0 (Disabled)'&lt;/span&gt;       │
└─────────────┴────────────────────────────┘

AVAILABLE COMMANDS:
  inputDelay    - Sets input delay to specified value
  outputDelay   - Sets output delay to specified value
  delay         - Sets overall balanced delay to specified value
  logInterval   - Show/Set status &lt;span class="o"&gt;(&lt;/span&gt;stderr&lt;span class="o"&gt;)&lt;/span&gt; logging interval &lt;span class="k"&gt;in &lt;/span&gt;msecs
  quit          - Quit the program

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By now it is capable to speed down transmission and reception speed by&lt;br&gt;
introducing small delays between packet transmission and reception at the other&lt;br&gt;
side.&lt;/p&gt;

&lt;p&gt;In the future it could be extended by introducing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Random (on customizable probability) transmission errors through data&lt;br&gt;
mangling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Random packet loosing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Delay configuration as ranges (so it will take a random value between given&lt;br&gt;
bounds for each tx/rx packet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Who knows... Any ideas?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Simplify Web Form Development with SmarkForm</title>
      <dc:creator>Joan Miquel Torres</dc:creator>
      <pubDate>Fri, 07 Jul 2023 11:28:00 +0000</pubDate>
      <link>https://dev.to/bitifet/simplify-web-form-development-with-smarkform-1j51</link>
      <guid>https://dev.to/bitifet/simplify-web-form-development-with-smarkform-1j51</guid>
      <description>&lt;p&gt;&lt;strong&gt;Introduction:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Creating web forms can often feel like reinventing the wheel. Every time we need to implement a form, we find ourselves going through the same process.&lt;/p&gt;

&lt;p&gt;For simple forms with a few plain fields, it may be pretty straightforward. However, as soon as we need a little more complexity, such as lists, nested data, dynamic options, and more, it can quickly become a time-consuming and effort-intensive task that diverts our focus from the core functionality of our applications. But fear not! With SmarkForm, you can break free from this repetitive cycle and streamline your form development process.&lt;/p&gt;

&lt;p&gt;SmarkForm is a powerful and straightforward JavaScript library that will greatly simplify form development in your web applications. With SmarkForm, you can quickly and efficiently create interactive forms without having to worry about writing complex JavaScript code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is SmarkForm?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is based on the idea that web forms should be easy to create and maintain directly in HTML markup, without the need for writing a lot of additional JavaScript code.&lt;/p&gt;

&lt;p&gt;We just need to add a few properties in so-called &lt;em&gt;data-smark&lt;/em&gt; attributes to make our HTML markup  more semantic and let SmarkForm library do the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features of SmarkForm:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy and quick creation of interactive forms.&lt;/li&gt;
&lt;li&gt;Complete separation between layout design and application logic.&lt;/li&gt;
&lt;li&gt;Support for complex data structures, such as nested objects and arrays.&lt;/li&gt;
&lt;li&gt;Ability to manually sort and add or remove items from a lists (arrays).&lt;/li&gt;
&lt;li&gt;(Comming soon) Dynamic loading of options for select fields based on the values of other form fields.&lt;/li&gt;
&lt;li&gt;Customization and extensibility through custom components.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Try it yourself:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Learn more about SmarkFor in its &lt;a href="https://smarkform.bitifet.net" rel="noopener noreferrer"&gt;Documentation site&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Don't miss its &lt;a href="https://smarkform.bitifet.net/resources/examples" rel="noopener noreferrer"&gt;examples section&lt;/a&gt; where you can check out its HTML source and JS example.&lt;/p&gt;

&lt;p&gt;Smarkform is available both as standard ESM and legacy UMD modules in &lt;a href="https://www.npmjs.com/package/smarkform" rel="noopener noreferrer"&gt;npm package&lt;/a&gt; and CDN.&lt;/p&gt;

&lt;p&gt;Former examples use UMD CDN so that they can be downloaded as single and fully functional HTML pages.&lt;/p&gt;

&lt;p&gt;It's styles are also served as separate CDN but you can use your own styling.&lt;/p&gt;

&lt;p&gt;Feel free to play modifying them as you like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Questions:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you have any questions or if you find this article unclear or incomplete, please let me know.&lt;/p&gt;

&lt;p&gt;I've made an effort to be approachable and prevent it from being tedious by excluding certain details that can already be found in the documentation and examples.&lt;/p&gt;

&lt;p&gt;However, if you believe that something is lacking, please provide your feedback in the comments.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>library</category>
    </item>
  </channel>
</rss>
