{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "DEPLOY1 Blog",
  "home_page_url": "https://deploy1.blog/",
  "feed_url": "https://deploy1.blog/feeds/feed.json",
  "description": "Engineering notes from the DEPLOY1 team.",
  "language": "en",
  "items": [
    {
      "id": "https://deploy1.blog/posts/quality-gate-dataset/",
      "url": "https://deploy1.blog/posts/quality-gate-dataset/",
      "title": "The DEPLOY1 quality gate dataset: what our checks actually measure",
      "summary": "Every change to this repository passes the same quality gate: typecheck, workspace manifest check, lint, format, tests and build. This dispatch documents exactly what each gate covers.",
      "content_text": "The point of a machine-readable dataset is that you can check it, not that it looks impressive. This one describes the checks that run before anything in this repository ships. # The gates Gate What it measures Failure means Typecheck Strict TypeScript across every workspace A type contract was broken Manifest check Workspace dependency declarations Declared and actual dependencies diverged Lint ESLint rules, including type-aware rules Code violates a project convention Format Prettier Formatting drifted from the committed style Tests Vitest suites per workspace Behaviour regressed Build Vite site build and the blog generator The deployable artifact cannot be produced # Why the order matters Typecheck runs first because it is the cheapest way to catch a broken contract. Tests run after formatting so a failure is easier to read. The build runs last because a build failure after passing tests usually means an environment or configuration problem, not a logic problem. # Reading the output Each gate is independently runnable, so a local run can isolate a single check. CI runs them as separate jobs, which means a red build tells you exactly which gate failed without needing to bisect a monolithic script. # Keeping it honest A gate that is routinely disabled is worse than no gate at all. If a check cannot pass, the fix is to change the code or the rule deliberately — not to relax the check so the pipeline turns green.",
      "date_published": "2026-09-21T00:00:00.000Z",
      "tags": [
        "process",
        "tooling",
        "ci",
        "delivery"
      ],
      "_d1_format": "dataset"
    },
    {
      "id": "https://deploy1.blog/posts/latency-conversion-sandbox/",
      "url": "https://deploy1.blog/posts/latency-conversion-sandbox/",
      "title": "Latency and conversion sandbox: the model behind the homepage slider",
      "summary": "The homepage sandbox is a static, transparent model — not a prediction. Here is every formula, constant and assumption behind the numbers it shows, so you can disagree with them.",
      "content_text": "The latency slider on the homepage is one of the most-read parts of this site, so it should be honest about what it is: a static model that runs entirely in the browser, with no data collection, no account and no server round trip. # What the model does The slider takes one input — Largest Contentful Paint in seconds, from 0.6 to 4.5 — and produces four outputs. Every output is a linear function of that input, which makes the model easy to audit and impossible to hide behind. # The formulas Projected conversion is a declining line across the slider range: conversion(lcp) = max(1.1, 5.5 - (lcp × 0.95)) Bounce probability rises with the same input: bounce(lcp) = min(68, 4.5 + (lcp × 11.2)) Annual impact is expressed against an explicit baseline, so the constant is visible: annualImpact(lcp) = (conversion(lcp) - 3.2) × $85,000 The perceived performance score is a compressed, 100-point scale: score(lcp) = max(18, round(105 - (lcp × 22))) # What it is not It is not a prediction for your traffic, and it does not come from a study of your users. The constants are chosen so the curve is monotone and legible at a glance. If you want a real model, run a real experiment on your own funnel — that is the only version of this number worth acting on. # Why publish the formulas A slider that shows impressive-looking numbers with no explanation is a trust problem waiting to happen. Publishing the arithmetic means you can check it, argue with it, and reuse it. Everything on this page runs in your browser: no cookies, no beacons, no requests.",
      "date_published": "2026-09-20T00:00:00.000Z",
      "tags": [
        "performance",
        "tooling",
        "accessibility",
        "html-css"
      ],
      "_d1_format": "interactive"
    },
    {
      "id": "https://deploy1.blog/posts/fixed-scope-deployments/",
      "url": "https://deploy1.blog/posts/fixed-scope-deployments/",
      "title": "Fixed-scope delivery: when productized sprints work — and when they don't",
      "summary": "Deploy1 runs on fixed-price, fixed-window sprints: $500 in a day, $5,000 in seven days, $20,000 in thirty. This post is the honest version of when that model wins, and where it breaks.",
      "content_text": "Much software outsourcing is priced by the hour and delivered &quot;when it&#39;s done.&quot; We sell the opposite: a fixed scope, a fixed price, and a fixed delivery window. That model has real, describable strengths — and just as real limitations. Since we ask every new client to trust it, we owe you the honest version of both. # What we sell Three standardized sprints: Sprint Price Window What it fits DEPLOY1 $500 1 day A single-page site or product landing page DEPLOY7 $5,000 7 days A multi-page growth system with conversion tooling DEPLOY30 $20,000 30 days A custom application, portal or internal tool Two things make this not just hourly billing with a nicer label: the scope is locked before work begins, and the price doesn&#39;t move when the scope doesn&#39;t. # Why the model works # Scope lock is a forcing function An open-ended discovery phase is where &quot;six-week projects&quot; go to die. Locking scope up front forces every requirement onto the table before a single line of code exists. A large share of development work is deciding — what the page does, where the data comes from, when the button appears. Fixed-scope delivery makes that decision process visible and bounded . You make the calls early, we execute them fast. # Density beats duration A one-day window concentrates attention. The sprint starts with everything decided, and there are no calendar gaps where momentum leaks away. Many multi-week engagements contain barely more focused working time than a well-run intensive week. # Fixed risk on both sides You know the price before we start. We know the effort before we commit. When both parties understand the boundary, neither side is incentivized to blur it. # Where the model breaks Honesty requires naming the failure modes: Vague requirements. If you &quot;don&#39;t know yet&quot; what you need, a fixed scope is fiction. We tell those clients exactly that — the right move is usually a small paid discovery sprint or a deliberately narrower scope. Undiscoverable constraints. Scope lock assumes the requirements are knowable. Legacy systems, undocumented APIs and third-party integrations that don&#39;t exist on paper are where fixed price becomes guesswork. When something looks like it depends on an integration we can&#39;t inspect, we flag it before quoting. Mid-build discovery. The biggest risk. If, during the build, we uncover that the &quot;simple&quot; form actually needs an institutional SSO flow, the fixed scope was wrong — and pretending otherwise would blow the window or the budget. Our answer to that third one is explicit scope governance: scope changes are raised during the sprint, not at delivery changes map to add-on sprints or a credibly adjusted price, agreed before the work we swallow small surprises; we don&#39;t pretend large ones don&#39;t exist A fixed price is a promise about a known scope. The discipline is knowing, in any given project, whether the scope is actually known. # When to pick each tier The tier isn&#39;t about the client&#39;s budget — it&#39;s about the size of the decision space . DEPLOY1 for a page that exists to communicate and convert. One page, one message, one decision. DEPLOY7 when you need several pages working together with forms, booking and measurement — still a content problem, but a structured one. DEPLOY30 when you&#39;re building a system: auth, data, workflows, roles. At this tier the architecture is the deliverable, not the interface. And sometimes the honest answer is &quot;none of these.&quot; We&#39;ll recommend no-touch for projects that genuinely need a long engagement with an on-site team. Fixed scope is the right default, not the only option. # What we can guarantee Two commitments we can make with certainty: The price for the locked scope. Full ownership — source code and design assets transfer to you at handover. What we don&#39;t do is sell you a guarantee we can&#39;t define. Speed is a property of a bounded problem. The productized sprint works because it binds the problem honestly — and that, more than the price tag, is the product.",
      "date_published": "2026-09-17T00:00:00.000Z",
      "tags": [
        "productized-service",
        "scoping",
        "process",
        "delivery"
      ],
      "_d1_format": "field-note"
    },
    {
      "id": "https://deploy1.blog/posts/server-side-lead-intake/",
      "url": "https://deploy1.blog/posts/server-side-lead-intake/",
      "title": "Server-side lead intake without a form framework",
      "summary": "How we replaced a direct-to-provider form POST with a first-party endpoint that validates every field server-side, honeypots bots and rate-limits abuse — in a couple of hundred lines.",
      "content_text": "Every marketing site needs a contact form, and there are a thousand ways to build one. Most of them share one flaw: the browser talks directly to a third-party provider, and the provider is asked to trust whatever the browser sends. Our consultation form used to work that way. This post is about the replacement: a first-party intake endpoint that treats incoming submissions as untrusted input, the same way we&#39;d treat any other API traffic. # The problem with the naive approach The classic implementation is a form whose action points at a provider&#39;s URL with a few hidden fields: &lt;form action=&quot;https://provider.example/submit&quot; method=&quot;POST&quot;&gt; &lt;input type=&quot;hidden&quot; name=&quot;_captcha&quot; value=&quot;false&quot; /&gt; &lt;input type=&quot;text&quot; name=&quot;name&quot; required /&gt; ... &lt;/form&gt; Problems with this, in order of severity: No server-side validation. required is a browser hint. Anyone can send whatever they want to the provider with a curl one-liner — including spam, HTML and malicious URLs. The provider address is public. The destination email lives in the page source, which means it&#39;s in spam databases almost immediately. No rate limiting. The endpoint is open to the internet by design. That&#39;s a spam pump waiting to happen. # The architecture: static page, dynamic endpoint A static site can&#39;t render server-side logic, but it doesn&#39;t have to. It only needs to call it. Our endpoint lives in the same Cloudflare Worker that serves the site: The browser POST s JSON to /api/lead — same origin, no CORS. The worker validates every field, applies a honeypot check and rate limiting. If valid, the worker relays the submission to the email provider server-side , where the destination address is never exposed to the client. async function handleIntake(request, env) { if (request.method !== &quot;POST&quot;) return json(405, { error: &quot;Method not allowed.&quot; }); const body = await request.json(); // rejects non-JSON const parsed = parseIntakePayload(body); // full validation if (!parsed.value) return json(400, { error: parsed.error }); await relayToEmail(env, parsed.value); // server-side delivery return json(200, { ok: true }); } # Validation: treat it like an API input The browser&#39;s built-in validation is a convenience for humans, not a security boundary. So the worker re-validates everything with explicit length and shape limits: function parseIntakePayload(body) { const name = trimTo(body.name, 120); const email = trimTo(body.email, 254); if (!isPlausibleEmail(email)) return { error: &quot;Email address is not valid.&quot; }; if (body.website &amp;&amp; !isSafeUrl(body.website)) return { error: &quot;Website URL is not valid.&quot; }; // ... } Three details that matter: Length limits everywhere. Don&#39;t just validate the email — bound every string. A 2 MB &quot;company name&quot; is a disk and logging concern. Normalize, then validate. Trimming before checking means &quot; a@b.com &quot; passes on purpose, and &quot; &quot;&lt;script&gt;…&quot; fails on the email check rather than on a stack of trimming edge cases. Never reflect user input verbatim. Error messages describe what was wrong; they don&#39;t echo the offending value back, which prevents both stored reflection and noisy inputs. # The honeypot and the rate limit Spam is a numbers game, so we play the numbers: Honeypot. A field that&#39;s invisible to humans ( aria-hidden , positioned off-screen, tabindex=&quot;-1&quot; ). Real users never fill it in; bots that naively fill every field do. If it&#39;s non-empty, we reject silently — a real user never sees the error. Rate limiting. Keyed on cf-connecting-ip plus the current time window: const key = `intake:${ip}:${Math.floor(Date.now() / WINDOW_MS)}`; const current = Number((await kv.get(key)) ?? &quot;0&quot;); if (current &gt;= MAX) return json(429, { error: &quot;Too many submissions.&quot; }); await kv.put(key, String(current + 1), { expirationTtl: WINDOW_SECONDS + 30 }); We keep an in-memory fallback so the endpoint still rate-limits even if KV is unavailable — and the IP is never stored beyond the expiry of the rate-limit counter, so the limiter retains no personal data. # What the visitor actually experiences On the page, the form submits with fetch and JSON, and the UI responds to the three states independently: success, a recoverable validation error, or the rate-limit message. The endpoint returns structured JSON so the client never has to sentence-parse an HTML page to know whether the send worked. The whole thing — validation, honeypot, rate limiting, relay — comes to a couple of hundred lines, and it&#39;s covered by unit tests that stub the relay and assert each failure mode returns the right status code. # The transferable lesson The principle here generalizes beyond forms: any browser-shipped &quot;validation&quot; is presentation, and any third-party endpoint you trust by default is a risk. Move the boundary where the security actually lives — the server — and treat every submission like the potentially hostile input it is. Your contact form is your first customer-facing system. It deserves the same care as your last one.",
      "date_published": "2026-09-11T00:00:00.000Z",
      "tags": [
        "security",
        "forms",
        "validation",
        "cloudflare-workers"
      ],
      "_d1_format": "blueprint"
    },
    {
      "id": "https://deploy1.blog/posts/minimal-static-site-generator/",
      "url": "https://deploy1.blog/posts/minimal-static-site-generator/",
      "title": "A minimal static site generator within a few hundred lines",
      "summary": "This blog runs on a hand-rolled static generator with no framework. Here is how it works: strict frontmatter validation, a constrained Markdown renderer, and plain HTML templates.",
      "content_text": "In the previous post we argued that marketing sites should be static. To prove the point, we built this blog that way: no framework, no bundler, no CSS preprocessor. The entire generator is one Node script plus two small library modules — a few hundred lines of logic, on top of the HTML templates themselves. Here&#39;s why, and how it works. # Why not use an existing SSG? It&#39;s a fair question. There are excellent static site generators — and for many sites they&#39;re the right tool. But for a small blog that we maintain, the cost of a generator framework is the same as any other framework: version churn, plugin ecosystems, and a build that behaves differently than the code you actually wrote. A minimal generator has real advantages: You can read all of it. Every behavior of the build is in one directory, not under node_modules . It does exactly one thing. No incremental rebuild state, no watch mode, no plugin hooks we&#39;d never use. It fails loudly. A strict validator that throws on bad metadata catches let&#39;s-rebuild-later problems in CI, where they belong. # The pipeline The build is a sequence of four steps: Load and validate every content/posts/*.md file. Render markdown to sanitized HTML. Assemble pages from templates and write them to dist/ . Generate the machine-readable files: sitemap, RSS, robots, search index. # 1. Strict content validation Each post starts with frontmatter: title: &quot;A minimal static site generator&quot; slug: minimal-static-site-generator description: &quot;A post about building things simply.&quot; date: 2026-09-04 category: Engineering tags: [&quot;static-site-generator&quot;, &quot;node&quot;] author: deploy1-team parseFrontmatter splits on the delimiter, and validatePost enforces the schema. Every field is checked — date format, category against an allowlist, description length, tag character set: function validatePost({ meta, content, slug, file }) { assertString(meta, &quot;title&quot;, file); assertDate(meta.date, &quot;date&quot;, file); if (!CATEGORIES.has(meta.category)) { throw new Error(`${file}: unknown category &quot;${meta.category}&quot;`); } // ... } The key design choice: the build throws on invalid content. A typo in a slug or a description that&#39;s too short fails CI, not quietly ships. # 2. A constrained Markdown renderer We wrap marked with a renderer that has three lockdown rules: Raw HTML from markdown is discarded ( renderer.html = () =&gt; &quot;&quot; ). Link href s are checked against a safe-protocol allowlist; external links get rel=&quot;noopener noreferrer&quot; . Code blocks are escaped before insertion. const renderer = Object.create(marked.renderer); renderer.html = () =&gt; &quot;&quot;; renderer.code = ({ text, lang }) =&gt; `&lt;pre&gt;&lt;code${lang ? ` class=&quot;language-${lang}&quot;` : &quot;&quot;}&gt;${escapeHtml(text)}&lt;/code&gt;&lt;/pre&gt;`; Even though our own team writes the content, a renderer that cannot emit raw HTML removes an entire class of bugs pre-emptively. # 3. Templates and asset hashing Pages are assembled by string templates. There&#39;s no JSX and no component system — for a site with four page shapes (home, article, category listing, 404), functions like postCard(post) are enough. Build-time asset hashing keeps caching honest. The CSS gets a content hash: const hash = crypto.createHash(&quot;sha1&quot;).update(source).digest(&quot;hex&quot;).slice(0, 8); Hashed assets are served with an immutable cache header; HTML pages avoid long-lived caching — a short no-cache window with stale-while-revalidate at the edge — so new posts appear immediately. # 4. Machine files Every listing and search-ui feed is generated data, not a plugin: sitemap.xml — every unique URL, including category and tag archives. rss.xml — publish dates from the validated frontmatter. robots.txt — pointing to the sitemap. search-index.json — title, description, tags and a plain-text excerpt, consumed by a ~150-line client-side search module. # The point isn&#39;t the code The generator is small not because we&#39;re clever, but because a blog is small. The discipline that makes it useful is the validation: human-authored content is run through an uncompromising machine check, every build, in CI. If you&#39;re reaching for a static-site framework, ask what a minimal generator gives you first. In many cases the answer is &quot;everything we need, with nothing we have to maintain.&quot; Simple on the outside. Sophisticated underneath — one file at a time.",
      "date_published": "2026-09-04T00:00:00.000Z",
      "tags": [
        "static-site-generator",
        "node",
        "tooling",
        "markdown"
      ],
      "_d1_format": "deep-dive"
    },
    {
      "id": "https://deploy1.blog/posts/static-first-marketing-sites/",
      "url": "https://deploy1.blog/posts/static-first-marketing-sites/",
      "title": "Static-first: why our marketing sites don't reach for a framework",
      "summary": "For a marketing site, the best runtime is often none at all. A look at the decision framework we use before adding React, Next.js or any build framework to a content site.",
      "content_text": "Most new marketing-site projects start with the same question from the client: &quot;What framework will you build this in?&quot; And usually, the more useful question is the one we ask instead: &quot;Does this page need to compute anything?&quot; If the honest answer is no, we build it static. This post explains why, and where the line is. # The default is not a framework The marketing pages of a typical business do a small number of things: present information link to other pages collect contact details maybe integrate a booking calendar or a checkout None of those require a client-side renderer. Serving pre-rendered HTML from a CDN satisfies all of them, with fewer moving parts than the smallest React setup. What a framework buys you is dynamism : data that changes per-user, interactive UI at scale, and complex state. A marketing site almost never has those. When you add a framework without needing it, you pay for it in four ways: JavaScript shipped to the browser — a bundle that must parse and execute before some pages can show anything at all. Runtime risk — hydration bugs, version churn, and more surface area for security issues. Build complexity — Node version constraints, lockfile conflicts, and deployment steps that don&#39;t exist for plain assets. Longer iteration cycles — every visual tweak travels through a build pipeline. # The decision framework we use Before reaching for a tool, we ask ourselves four questions: Is the content read, not edited? A blog, a brochure, a documentation site — static generation is the right tool. Does the page depend on the reader? If the HTML changes based on who&#39;s looking, you need a server or client-side logic. Are we building a product or a page? An authenticated portal is a product. A landing page is a page. Don&#39;t build the first with the tools of the second. Can we measure the downside? The cost of a static site is the cost of a rebuild when content changes. With a small content model, that cost rounds to zero. When any of the first three answers point toward the application side, we switch tools — but we switch deliberately , not by default. # What &quot;static&quot; actually buys you Serving files instead of rendering HTML per request has two practical effects that matter to a business: Caching is trivial. A static asset can be cached aggressively at the edge with a max-age and immutable header. Meaningful performance gains don&#39;t come from micro-optimizing JavaScript — they come from not forcing the network to do work twice. The deploy is boring. Changing a page means regenerating HTML and pushing files. There is no blue/green dance, no migration risk, no &quot;the deploy broke something&quot; fear that makes teams avoid shipping. # Where we stop being static The static-first approach has a hard boundary: anything that mutates state. Booking a call, submitting a form, checking out — that&#39;s where the server has to exist. Here&#39;s the important nuance: a static site can talk to a server without stopping being static. Your pages are static; your endpoints are dynamic. The HTML never waits on a database query — the interactive bits call an API only when the visitor triggers them. So the architecture of a modern marketing site is: Static HTML, CSS and a little vanilla JavaScript — shipped from the edge, cached forever, rendered instantly. A thin API layer — does exactly the mutations the page needs (lead intake, search, checkouts). A strict content pipeline — markdown in, validated, generated, agreed-upon URLs out. # The uncomfortable truth The reason a lot of agencies reach for a framework on a brochure site often has little to do with technology. In many cases it&#39;s because React and Next.js are what the marketplace recognizes, and &quot;we wrote HTML&quot; doesn&#39;t sound like work. It is work — skilled work. Semantic, accessible, well-typed markup is hard to write and easy to get wrong. But it doesn&#39;t need a JavaScript runtime to serve . A framework is a tool for a job, not a badge of quality. The best runtime for a marketing page is usually nothing at all. We built this blog the same way — you can read exactly how in our next post.",
      "date_published": "2026-08-26T00:00:00.000Z",
      "tags": [
        "static-sites",
        "performance",
        "architecture",
        "html-css"
      ],
      "_d1_format": "research"
    }
  ]
}