Vibe Coding with HTMX: Why Hypermedia is the Ultimate AI Pair-Programming Hack published
We need to reformat the article body into clean markdown, preserving facts, no extra commentary. The article body appears duplicated (the quoted part repeated). We need to output only the reformatted article body as markdown, nothing else. Must break into short paragraphs, add section headings where topic shifts, use bullet/numbered lists, inline code or fenced code blocks where code appears. Preserve every fact, name, number, quotation exactly. Do not add intro/conclusion. Do not repeat article title as heading. Do not wrap whole answer in code fence.
We need to parse the given raw article body (the part after "Article:" including the quoted text). The raw includes a leading quote and then the article text, then repeated again? Actually the article appears twice: first a quoted block starting with "Vibe coding"-prompting an LLM... then later again the same block repeated. Probably due to copy-paste error. We need to preserve every fact, name, number, and quotation exactly as given. If we deduplicate, we would be removing information (duplicate). The instruction: "Preserve every fact, name, number, and quotation exactly as given. Never invent, add, remove, or reorder information." So we must keep duplicates as they appear. However we can restructure (break into paragraphs, headings) but cannot remove any content. So we need to output the entire article body exactly as given, but with markdown formatting (paragraph breaks, headings, lists, code fences). We must not add or remove any content. That means we must keep the duplicated text verbatim, just reformat.
Thus we need to take the raw article body (starting after "Article:" line) and apply markdown formatting while preserving all characters exactly (except we can insert line breaks for paragraphs, headings, etc). However inserting line breaks changes the raw text (adds newline characters). Is that allowed? The instruction says "Break the text into short, well-structured paragraphs separated by a blank line." That implies we can add blank lines between paragraphs. That's restructuring, not removing content. Adding blank lines is okay. Adding headings (##) adds new characters not originally present. Is that allowed? The instruction says "Add Markdown section headings (## Heading) where the topic clearly shifts". So we can add headings. That's adding content, but it's allowed per instruction. Similarly we can add bullet/numbered lists, code fences. So we can add markdown syntax but must not alter the original wording besides adding markdown markers and line breaks.
Thus we need to identify sections and add headings accordingly. Also need to convert enumerations to bullet/numbered lists. Also need to format code blocks with fenced triple backticks and language tag where appropriate.
We must preserve quotations exactly, including the opening and closing quotes. The article includes many quoted strings like "Vibe coding"-prompting an LLM... We must keep them.
We need to decide headings. The article has sections: maybe "The Core Problem: Why LLMs Struggle with Modern Frontend SPAs", "The Hypermedia Collapse", "The Advantages of HTMX for Vibe Coding" with subpoints 1.,2.,3., "Hands-On Example: The Live Search & Quick Edit Pattern" with subparts 1. The Main Shell (server.js), 2. Interactive Endpoints (Search & In-Place Editing), then code blocks. Then "Best Practices for Prompting AI with HTMX" with subpoints 1.,2. Then a table "When HTMX Is the Right Vibe (and When It's Not)" with columns. Then "The Takeaway". Also there is a repeated duplicate of the entire article after that? Actually after the first occurrence, the article repeats again from ""Vibe coding"-prompting an LLM...". So we need to include that duplicate as well, likely with same headings again? But we should not add extra headings that misrepresent duplication? We could treat the duplicate as a second occurrence of the same content; we could just repeat the same formatted sections again. That would preserve the duplicate.
Thus we need to output the formatted article body twice, with same headings each time.
Let's extract the raw article body exactly as given (including the leading and trailing quotes?). The raw after "Article:" starts with a double quote then the text, ends with a double quote? Actually the article block is enclosed in double quotes at start and end? The given: Article:
"Vibe coding"-prompting an LLM, accepting diffs, testing the feature, and shipping without obsessing over every boilerplate line-feels like magic until your frontend stack collapses under its own weight. If you’ve tried vibe coding with modern single-page application (SPA) architectures (React, Next.js, state machines, hydration lifecycles, and three layers of client-side cache), you know the breaking point: State synchronization hell: The AI invents a useEffect loop that triggers 40 rerenders. Context bloat: Feeding schemas, types, actions, components, and Tailwind tokens consumes half your model's context window before you even ask for business logic. Ghost bugs: The LLM misses an edge case where a client cache isn't invalidated after a server mutation. Enter HTMX . When you shift from client-state SPAs to hypermedia-driven interfaces, LLMs go from erratic code spitters to hyper-competent engineering partners. Here is why HTMX is the best-kept secret for vibe coding, along with a production-ready blueprint to try it yourself. The Core Problem: Why LLMs Struggle with Modern Frontend SPAs To understand why HTMX shines with AI, look at what an LLM must track when building a typical React or Vue feature: Backend route & input validation schemas. ORM/database queries. API serialization & JSON response contracts. Frontend data fetching (React Query, RTK Query, Axios). Client state / store management (Zustand, Redux, Context). Rendering markup, DOM event listeners, and hydration boundaries. [Database] ↔ [Server Logic] ↔ [JSON API] ↔ [State Store] ↔ [Virtual DOM] ↔ [Real DOM] Every boundary is an opportunity for hallucination. If the backend changes a field from snake_case to camelCase , your AI assistant frequently fails to update the frontend state transformer three files away. The Hypermedia Collapse HTMX collapses that entire pipeline: [Database] ↔ [Server Logic + HTML Template] ↔ [Real DOM via HTMX] The server returns plain HTML chunks. HTMX swaps them directly into the DOM. There is no JSON serialization layer, no client-side store, and no synchronization logic. The state lives where it belongs: on the server . Because LLMs are trained on decades of server-side templates (Django, Rails, Laravel, Go templates, Express/EJS), they write server-rendered HTML with near-perfect accuracy on the first shot. The Advantages of HTMX for Vibe Coding 1. Minimal Cognitive & Token Overhead When you prompt an AI to create a feature with HTMX, you don’t need to paste 8 files. You paste one template or one server endpoint. The LLM can hold your entire route logic and its visual representation in a single prompt. 2. Zero State Desync Because there is no separate client-side cache, bugs like "I clicked delete, but the item still appears until I refresh" simply don't happen. The server deletes the row and returns an empty string or the updated table markup. Done. 3. Framework Agnostic Whether you vibe code in Python (FastAPI/Flask), Go (Echo/Chi), Node.js (Express/Hono), or Rust (Axum), your HTMX syntax remains identical: hx-get hx-post hx-target hx-swap Hands-On Example: The Live Search & Quick Edit Pattern Let’s build an interactive search-and-edit interface. We'll use Node.js + Express with inline HTML template literals to keep everything in one compact file. 1. The Main Shell ( server.js ) Here is how simple your application entry point is. Notice how HTMX attributes handle all the client-side behaviors that usually require hundreds of lines of React state. javascript const express = require('express'); const app = express(); app.use(express.urlencoded({ extended: true })); app.use(express.json()); // In-memory mock store let items = [ { id: 1, name: "Dark Mode UI Kit", status: "Active" }, { id: 2, name: "Analytics Dashboard", status: "Pending" }, { id: 3, name: "Webhook Dispatcher", status: "Archived" } ]; // Base layout app.get('/', (req, res) => { res.send(<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>HTMX + Vibe Coding Dashboard</title> <script src="[https://unpkg.com/htmx.org@2.0.0](https://unpkg.com/htmx.org@2.0.0)"></script> <script src="[https://cdn.tailwindcss.com](https://cdn.tailwindcss.com)"></script> </head> <body class="bg-neutral-950 text-neutral-100 p-8 font-sans"> <div class="max-w-2xl mx-auto space-y-6"> <h1 class="text-2xl font-bold tracking-tight">Project Overview</h1> <!-- Live Search Input --> <div> <input type="text" name="q" placeholder="Type to filter..." hx-get="/search" hx-trigger="keyup changed delay:300ms, search" hx-target="#items-list" class="w-full bg-neutral-900 border border-neutral-800 rounded px-4 py-2 focus:outline-none focus:border-neutral-500" /> </div> <!-- Dynamic Content Swap Target --> <div id="items-list" class="space-y-2"> ${renderList(items)} </div> </div> </body> </html>); }); function renderList(list) { if (list.length === 0) { return <p class="text-neutral-500 text-sm">No items found.</p>; } return list.map(item => <div id="item-${item.id}" class="flex items-center justify-between p-4 bg-neutral-900 border border-neutral-800 rounded"> <div> <div class="font-medium">${item.name}</div> <div class="text-xs text-neutral-400">Status: ${item.status}</div> </div> <button hx-get="/items/${item.id}/edit" hx-target="#item-${item.id}" hx-swap="outerHTML" class="text-xs bg-neutral-800 hover:bg-neutral-700 px-3 py-1.5 rounded transition"> Edit </button> </div>).join(''); } 2. Interactive Endpoints (Search & In-Place Editing) Now ask your LLM to add live filtering and in-place row editing. Instead of coordinating mutations and optimistic updates, the server simply returns small pieces of HTML: // Live search query endpoint app.get('/search', (req, res) => { const query = (req.query.q || '').toLowerCase(); const filtered = items.filter(item => item.name.toLowerCase().includes(query)); res.send(renderList(filtered)); }); // Returns the inline edit form app.get('/items/:id/edit', (req, res) => { const item = items.find(i => i.id === parseInt(req.params.id)); if (!item) return res.sendStatus(404); res.send(<form id="item-${item.id}" hx-put="/items/${item.id}" hx-target="#item-${item.id}" hx-swap="outerHTML" class="flex items-center gap-3 p-4 bg-neutral-900 border border-neutral-700 rounded"> <input type="text" name="name" value="${item.name}" class="bg-neutral-950 border border-neutral-800 px-3 py-1 text-sm rounded flex-1 focus:outline-none" /> <select name="status" class="bg-neutral-950 border border-neutral-800 px-2 py-1 text-sm rounded"> <option value="Active" ${item.status === 'Active' ? 'selected' : ''}>Active</option> <option value="Pending" ${item.status === 'Pending' ? 'selected' : ''}>Pending</option> <option value="Archived" ${item.status === 'Archived' ? 'selected' : ''}>Archived</option> </select> <button type="submit" class="text-xs bg-emerald-600 hover:bg-emerald-500 px-3 py-1.5 rounded font-medium">Save</button> <button type="button" hx-get="/items/${item.id}" hx-target="#item-${item.id}" hx-swap="outerHTML" class="text-xs bg-neutral-800 hover:bg-neutral-700 px-3 py-1.5 rounded"> Cancel </button> </form>); }); // Saves the update and swaps the row back to display mode app.put('/items/:id', (req, res) => { const item = items.find(i => i.id === parseInt(req.params.id)); if (!item) return res.sendStatus(404); item.name = req.body.name || item.name; item.status = req.body.status || item.status; res.send(<div id="item-${item.id}" class="flex items-center justify-between p-4 bg-neutral-900 border border-neutral-800 rounded"> <div> <div class="font-medium">${item.name}</div> <div class="text-xs text-neutral-400">Status: ${item.status}</div> </div> <button hx-get="/items/${item.id}/edit" hx-target="#item-${item.id}" hx-swap="outerHTML" class="text-xs bg-neutral-800 hover:bg-neutral-700 px-3 py-1.5 rounded transition"> Edit </button> </div>); }); // Read single row (used by "Cancel") app.get('/items/:id', (req, res) => { const item = items.find(i => i.id === parseInt(req.params.id)); if (!item) return res.sendStatus(404); res.send(<div id="item-${item.id}" class="flex items-center justify-between p-4 bg-neutral-900 border border-neutral-800 rounded"> <div> <div class="font-medium">${item.name}</div> <div class="text-xs text-neutral-400">Status: ${item.status}</div> </div> <button hx-get="/items/${item.id}/edit" hx-target="#item-${item.id}" hx-swap="outerHTML" class="text-xs bg-neutral-800 hover:bg-neutral-700 px-3 py-1.5 rounded transition"> Edit </button> </div>); }); app.listen(3000, () => console.log('Listening on http://localhost:3000')); Best Practices for Prompting AI with HTMX When prompting your AI agent (Cursor, Claude, ChatGPT, Windsurf, Copilot), use these rules to keep outputs razor-sharp: 1. Give it the "Hypermedia Rules" System Prompt Place this in your .cursorrules, system prompt, or conversation start: We build using Server-Side Rendered HTML and HTMX. Do NOT create JSON APIs or client-side stores unless explicitly requested. Every endpoint returning interactive data should return an HTML snippet. Always use proper hx-target, hx-swap, and hx-indicator attributes. Prefer outerHTML swaps for modifying existing components in-place. 2. Prompt for Endpoints and Snippets Together Instead of asking: > "Create a REST API for deleting an order and write a React hook to mutate it." > Prompt like this: > "Write a DELETE /orders/:id endpoint that deletes the record from the DB and returns an HTTP 200 with an empty body, using hx-swap="delete" on the client button to remove the row from the DOM." > The AI will output 8 lines of code instead of 60. When HTMX Is the Right Vibe (and When It's Not) | Ideal for HTMX + AI | Better with Traditional SPAs | |---|---| | CRUD apps, admin portals, dashboards | Offline-first mobile-style apps | | Content platforms, internal tooling | Canvas/WebGL heavy apps (Figma, games) | | Prototypes & fast MVP launches | High-density drag-and-drop spreadsheets | | Single-developer or small-team builds | Multi-team decoupled API ecosystems | The Takeaway Vibe coding is all about tight feedback loops. The more indirection your stack requires-build steps, compile errors, bundler hiccups, and hydration mismatches-the more your flow state breaks. HTMX gives your AI assistant direct access to the DOM through the simplest protocol on earth: standard HTTP and HTML. The next time you fire up your AI code editor to build a tool, skip the heavy frontend bundle. Pair your model with HTMX and watch your ideas turn into functional software at lightspeed.
"Vibe coding"-prompting an LLM, accepting diffs, testing the feature, and shipping without obsessing over every boilerplate line-feels like magic until your frontend stack collapses under its own weight. If you’ve tried vibe coding with modern single-page application (SPA) architectures (React, Next.js, state machines, hydration lifecycles, and three layers of client-side cache), you know the breaking point: - State synchronization hell: The AI invents a useEffect loop that triggers 40 rerenders. - Context bloat: Feeding schemas, types, actions, components, and Tailwind tokens consumes half your model's context window before you even ask for business logic. - Ghost bugs: The LLM misses an edge case where a client cache isn't invalidated after a server mutation. Enter HTMX. When you shift from client-state SPAs to hypermedia-driven interfaces, LLMs go from erratic code spitters to hyper-competent engineering partners. Here is why HTMX is the best-kept secret for vibe coding, along with a production-ready blueprint to try it yourself. The Core Problem: Why LLMs Struggle with Modern Frontend SPAs To understand why HTMX shines with AI, look at what an LLM must track when building a typical React or Vue feature: - Backend route & input validation schemas. - ORM/database queries. - API serialization & JSON response contracts. - Frontend data fetching (React Query, RTK Query, Axios). - Client state / store management (Zustand, Redux, Context). - Rendering markup, DOM event listeners, and hydration boundaries. [Database] ↔ [Server Logic] ↔ [JSON API] ↔ [State Store] ↔ [Virtual DOM] ↔ [Real DOM] Every boundary is an opportunity for hallucination. If the backend changes a field from snake_case to camelCase , your AI assistant frequently fails to update the frontend state transformer three files away. The Hypermedia Collapse HTMX collapses that entire pipeline: [Database] ↔ [Server Logic + HTML Template] ↔ [Real DOM via HTMX] The server returns plain HTML chunks. HTMX swaps them directly into the DOM. There is no JSON serialization layer, no client-side store, and no synchronization logic. The state lives where it belongs: on the server. Because LLMs are trained on decades of server-side templates (Django, Rails, Laravel, Go templates, Express/EJS), they write server-rendered HTML with near-perfect accuracy on the first shot. The Advantages of HTMX for Vibe Coding 1. Minimal Cognitive & Token Overhead When you prompt an AI to create a feature with HTMX, you don’t need to paste 8 files. You paste one template or one server endpoint. The LLM can hold your entire route logic and its visual representation in a single prompt. 2. Zero State Desync Because there is no separate client-side cache, bugs like "I clicked delete, but the item still appears until I refresh" simply don't happen. The server deletes the row and returns an empty string or the updated table markup. Done. 3. Framework Agnostic Whether you vibe code in Python (FastAPI/Flask), Go (Echo/Chi), Node.js (Express/Hono), or Rust (Axum), your HTMX syntax remains identical: hx-get hx-post hx-target hx-swap Hands-On Example: The Live Search & Quick Edit Pattern Let’s build an interactive search-and-edit interface. We'll use Node.js + Express with inline HTML template literals to keep everything in one compact file. 1. The Main Shell (server.js ) Here is how simple your application entry point is. Notice how HTMX attributes handle all the client-side behaviors that usually require hundreds of lines of React state. javascript const express = require('express'); const app = express(); app.use(express.urlencoded({ extended: true })); app.use(express.json()); // In-memory mock store let items = [ { id: 1, name: "Dark Mode UI Kit", status: "Active" }, { id: 2, name: "Analytics Dashboard", status: "Pending" }, { id: 3, name: "Webhook Dispatcher", status: "Archived" } ]; // Base layout app.get('/', (req, res) => { res.send(<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>HTMX + Vibe Coding Dashboard</title> <script src="[https://unpkg.com/htmx.org@2.0.0](https://unpkg.com/htmx.org@2.0.0)"></script> <script src="[https://cdn.tailwindcss.com](https://cdn.tailwindcss.com)"></script> </head> <body class="bg-neutral-950 text-neutral-100 p-8 font-sans"> <div class="max-w-2xl mx-auto space-y-6"> <h1 class="text-2xl font-bold tracking-tight">Project Overview</h1> <!-- Live Search Input --> <div> <input type="text" name="q" placeholder="Type to filter..." hx-get="/search" hx-trigger="keyup changed delay:300ms, search" hx-target="#items-list" class="w-full bg-neutral-900 border border-neutral-800 rounded px-4 py-2 focus:outline-none focus:border-neutral-500" /> </div> <!-- Dynamic Content Swap Target --> <div id="items-list" class="space-y-2"> ${renderList(items)} </div> </div> </body> </html>); }); function renderList(list) { if (list.length === 0) { return <p class="text-neutral-500 text-sm">No items found.</p>; } return list.map(item => <div id="item-${item.id}" class="flex items-center justify-between p-4 bg-neutral-900 border border-neutral-800 rounded"> <div> <div class="font-medium">${item.name}</div> <div class="text-xs text-neutral-400">Status: ${item.status}</div> </div> <button hx-get="/items/${item.id}/edit" hx-target="#item-${item.id}" hx-swap="outerHTML" class="text-xs bg-neutral-800 hover:bg-neutral-700 px-3 py-1.5 rounded transition"> Edit </button> </div>).join(''); } 2. Interactive Endpoints (Search & In-Place Editing) Now ask your LLM to add live filtering and in-place row editing. Instead of coordinating mutations and optimistic updates, the server simply returns small pieces of HTML: // Live search query endpoint app.get('/search', (req, res) => { const query = (req.query.q || '').toLowerCase(); const filtered = items.filter(item => item.name.toLowerCase().includes(query)); res.send(renderList(filtered)); }); // Returns the inline edit form app.get('/items/:id/edit', (req, res) => { const item = items.find(i => i.id === parseInt(req.params.id)); if (!item) return res.sendStatus(404); res.send(<form id="item-${item.id}" hx-put="/items/${item.id}" hx-target="#item-${item.id}" hx-swap="outerHTML" class="flex items-center gap-3 p-4 bg-neutral-900 border border-neutral-700 rounded"> <input type="text" name="name" value="${item.name}" class="bg-neutral-950 border border-neutral-800 px-3 py-1 text-sm rounded flex-1 focus:outline-none" /> <select name="status" class="bg-neutral-950 border border-neutral-800 px-2 py-1 text-sm rounded"> <option value="Active" ${item.status === 'Active' ? 'selected' : ''}>Active</option> <option value="Pending" ${item.status === 'Pending' ? 'selected' : ''}>Pending</option> <option value="Archived" ${item.status === 'Archived' ? 'selected' : ''}>Archived</option> </select> <button type="submit" class="text-xs bg-emerald-600 hover:bg-emerald-500 px-3 py-1.5 rounded font-medium">Save</button> <button type="button" hx-get="/items/${item.id}" hx-target="#item-${item.id}" hx-swap="outerHTML" class="text-xs bg-neutral-800 hover:bg-neutral-700 px-3 py-1.5 rounded"> Cancel </button> </form>); }); // Saves the update and swaps the row back to display mode app.put('/items/:id', (req, res) => { const item = items.find(i => i.id === parseInt(req.params.id)); if (!item) return res.sendStatus(404); item.name = req.body.name || item.name; item.status = req.body.status || item.status; res.send(<div id="item-${item.id}" class="flex items-center justify-between p-4 bg-neutral-900 border border-neutral-800 rounded"> <div> <div class="font-medium">${item.name}</div> <div class="text-xs text-neutral-400">Status: ${item.status}</div> </div> <button hx-get="/items/${item.id}/edit" hx-target="#item-${item.id}" hx-swap="outerHTML" class="text-xs bg-neutral-800 hover:bg-neutral-700 px-3 py-1.5 rounded transition"> Edit </button> </div>); }); // Read single row (used by "Cancel") app.get('/items/:id', (req, res) => { const item = items.find(i => i.id === parseInt(req.params.id)); if (!item) return res.sendStatus(404); res.send(` <div id="item-${item.id}" class="flex items-center justify-between p-4 bg-neutral-900 border border-neutral-800 rounded"> <div> <div class="font-medium">${item.name}</div> <div class="text-xs text-neutral-400">Status: ${item.status}</div> </div> <button hx-get="/items/${item.id}/edit" hx-target="#item-${item.id}" hx-swap="outerHTML" class="text-xs bg-neutral-800 hover:bg-neutral-700 px-3 py-1.5 rounded transition">
Comments
No comments yet. Start the discussion.