<?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: EvvyTools</title>
    <description>The latest articles on DEV Community by EvvyTools (@evvytools).</description>
    <link>https://dev.to/evvytools</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%2F3824924%2Feb242606-f29d-491d-bdf0-f1e92b554de8.png</url>
      <title>DEV Community: EvvyTools</title>
      <link>https://dev.to/evvytools</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/evvytools"/>
    <language>en</language>
    <item>
      <title>Why Code Comments and Inline Documentation Confuse Standard Readability Formulas</title>
      <dc:creator>EvvyTools</dc:creator>
      <pubDate>Tue, 07 Jul 2026 09:17:41 +0000</pubDate>
      <link>https://dev.to/evvytools/why-code-comments-and-inline-documentation-confuse-standard-readability-formulas-2mn3</link>
      <guid>https://dev.to/evvytools/why-code-comments-and-inline-documentation-confuse-standard-readability-formulas-2mn3</guid>
      <description>&lt;p&gt;Run a README or a docstring-heavy file through a general-purpose readability checker and you will sometimes get a score that makes no sense relative to how the document actually reads. The culprit is usually the code itself getting swept into the word and sentence count, and formulas built for prose have no concept of what a code block is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually goes wrong
&lt;/h2&gt;

&lt;p&gt;Readability formulas split text into words and sentences using punctuation as the primary signal, periods for sentence boundaries, whitespace for word boundaries. Code does not respect either convention. A single line like &lt;code&gt;const userConfig = await fetchUserConfiguration(userId, { includeDeleted: false });&lt;/code&gt; gets parsed as one enormous "word" by character count, or as multiple short sentence fragments if the formula treats semicolons and periods inside the code as sentence breaks. Either way, the resulting number reflects an artifact of the parsing, not the actual difficulty of the surrounding prose.&lt;/p&gt;

&lt;p&gt;Fenced code blocks in markdown are especially prone to this if a readability script naively strips markdown syntax but does not specifically detect and exclude the content inside triple-backtick blocks. The comment text explaining the code might be perfectly clear, grade 8, easy to follow, while the raw code sitting next to it drags a whole-document average toward something meaningless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inline code spans cause a subtler version of the same problem
&lt;/h2&gt;

&lt;p&gt;Even outside full code blocks, inline code spans, &lt;code&gt;variableName&lt;/code&gt;, &lt;code&gt;functionCall()&lt;/code&gt;, &lt;code&gt;--flag-name&lt;/code&gt;, get counted as regular words by formulas that are not specifically stripping markdown code syntax first. A sentence like "Set the &lt;code&gt;maxRetries&lt;/code&gt; option to control how many times &lt;code&gt;fetchData()&lt;/code&gt; will retry before it throws a &lt;code&gt;NetworkTimeoutError&lt;/code&gt;" contains three inline code spans that will inflate a naive word-length or syllable count, even though a developer reading that sentence experiences it as perfectly normal technical prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to get a number that actually means something
&lt;/h2&gt;

&lt;p&gt;Before running a readability check on technical content, strip fenced code blocks and inline code spans first, and run the formula only against the remaining prose. Most markdown parsing libraries make this straightforward, since code blocks and inline code spans are already distinctly tagged in the parsed document tree. A &lt;a href="https://evvytools.com/tools/writing-content/reading-level-analyzer/" rel="noopener noreferrer"&gt;reading level analyzer&lt;/a&gt; or similar tool applied to raw markdown without this preprocessing step will produce numbers that are not comparable across documents with different amounts of embedded code, which makes tracking a readability trend over time actively misleading.&lt;/p&gt;

&lt;p&gt;Comments extracted from source code deserve the same treatment, but from the opposite direction: pull just the comment text out of the source file and run the formula against that, ignoring the code it is attached to. This gives you an honest signal about whether your inline documentation itself is clear, separate from whatever the code surrounding it looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters beyond getting a clean number
&lt;/h2&gt;

&lt;p&gt;Teams that try to enforce a readability standard on technical writing without handling code exclusion end up with a check that fires unpredictably, sometimes flagging genuinely clear prose because of an unrelated code block sitting nearby. That kind of noise is exactly what causes a team to stop trusting an automated check and eventually disable it, even though the underlying idea, keeping documentation prose readable, was sound. Fixing the code-stripping step first is what makes the rest of a readability-based quality process actually usable for technical content.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example showing the size of the distortion
&lt;/h2&gt;

&lt;p&gt;Take a short README section: three sentences of plain explanatory prose, followed by a five-line code block, followed by two more sentences of prose. Run the whole section through a formula with no code stripping and you might see a reported grade level of 16 or higher, driven entirely by the code block's long variable names and lack of natural sentence boundaries. Strip the code block first and run the same formula against just the five sentences of prose, and the honest number might be grade 8 or 9, a completely different picture of how accessible the actual writing is.&lt;/p&gt;

&lt;p&gt;This gap is not a minor rounding difference. It is large enough to make a document look like it needs a rewrite when the writing itself is already fine, or worse, to hide a genuinely dense paragraph of prose behind a code block that happens to offset the average in the other direction. Neither outcome helps a team trying to actually improve their documentation, which is why code stripping has to happen before the formula runs, not as an afterthought applied to the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building this into your own tooling
&lt;/h2&gt;

&lt;p&gt;If you are writing a script to check documentation readability across a repository, budget time for the preprocessing step before you budget time for picking a formula. A regex-based approach that strips triple-backtick fenced blocks and single-backtick inline spans will handle the majority of cases in standard markdown. YAML frontmatter blocks, HTML comments, and embedded configuration snippets deserve the same treatment, since none of them represent the actual prose you are trying to evaluate.&lt;/p&gt;

&lt;p&gt;Test your stripping logic against a few real files from your own documentation before trusting the readability numbers it produces. Documentation with heavy use of inline code for configuration keys, environment variable names, or CLI flags will show the distortion most clearly, and confirming your preprocessing handles those cases correctly is worth the extra half hour before you start acting on the scores it generates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tables, admonition blocks, and other markdown extensions
&lt;/h2&gt;

&lt;p&gt;Fenced code and inline code spans are the most obvious culprits, but they are not the only markdown constructs that confuse a naive readability check. Tables get rendered as rows of pipe-separated cells in raw markdown, and a formula run against the raw table syntax will count table borders and alignment markers as word-like tokens, producing nonsense. Admonition blocks, the colored "note" or "warning" boxes many documentation frameworks support, often use custom syntax extensions beyond standard markdown, which a generic stripping script will not recognize unless you specifically account for the framework you are using.&lt;/p&gt;

&lt;p&gt;The safest approach is to render the markdown to a structured format first, an abstract syntax tree or a parsed HTML document, and then walk that structure extracting only the nodes that represent actual prose paragraphs and list items. This is more setup work than a quick regex, but it scales much better across a documentation site that uses tables, admonitions, and custom components alongside plain paragraphs, since each node type can be explicitly included or excluded rather than guessed at with pattern matching that will eventually miss an edge case.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in an actual CI check
&lt;/h2&gt;

&lt;p&gt;Once the stripping and parsing step is solid, wiring it into continuous integration is straightforward: run the readability formula only against the extracted prose nodes for each changed file, compare against a baseline, and flag genuine outliers. The value of doing the preprocessing correctly compounds here, because a CI check that occasionally fires on a false positive from an unstripped code block will train contributors to ignore it within a few weeks, the same failure mode any noisy automated check eventually suffers from regardless of how good the underlying idea was.&lt;/p&gt;

&lt;p&gt;For a broader look at why different readability formulas can disagree with each other even on ordinary prose, see EvvyTools' explainer on &lt;a href="https://evvytools.com/blog/why-readability-scores-disagree-on-the-same-paragraph/" rel="noopener noreferrer"&gt;why readability scores disagree on the same paragraph&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;References: the &lt;a href="https://commonmark.org" rel="noopener noreferrer"&gt;CommonMark specification&lt;/a&gt; documents exactly how fenced code blocks and inline code spans are defined in markdown, the &lt;a href="https://developers.google.com/style" rel="noopener noreferrer"&gt;Google Developer Documentation Style Guide&lt;/a&gt; covers writing clear inline comments and prose separately from code samples, and the &lt;a href="https://www.writethedocs.org" rel="noopener noreferrer"&gt;Write the Docs community&lt;/a&gt; maintains further resources on documentation tooling and quality checks for technical writing teams.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>writing</category>
      <category>documentation</category>
      <category>tools</category>
    </item>
    <item>
      <title>How to Add a Readability Check to a Documentation CI Pipeline Without Blocking Every Pull Request</title>
      <dc:creator>EvvyTools</dc:creator>
      <pubDate>Tue, 07 Jul 2026 09:13:25 +0000</pubDate>
      <link>https://dev.to/evvytools/how-to-add-a-readability-check-to-a-documentation-ci-pipeline-without-blocking-every-pull-request-2i78</link>
      <guid>https://dev.to/evvytools/how-to-add-a-readability-check-to-a-documentation-ci-pipeline-without-blocking-every-pull-request-2i78</guid>
      <description>&lt;p&gt;Docs teams that adopt readability tooling usually make the same early mistake: wiring the check up as a hard CI gate on day one. A new contributor pushes a perfectly reasonable API doc, the pipeline fails on a grade-level threshold nobody explained to them, and the whole team quietly starts ignoring the check within a month. Here is a sequence that avoids that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Run the check in report-only mode first
&lt;/h2&gt;

&lt;p&gt;Before any pull request can fail because of it, add the readability check as a CI step that posts a comment with the score but does not block merging. This gives you real data on your existing docs corpus, what your typical grade level actually is today, before you pick a threshold that might be unrealistic for your content.&lt;/p&gt;

&lt;p&gt;Most CI systems (GitHub Actions, GitLab CI, CircleCI) support posting a PR comment from a script's output. Point that script at a &lt;a href="https://evvytools.com/tools/writing-content/reading-level-analyzer/" rel="noopener noreferrer"&gt;reading level analyzer&lt;/a&gt; or an equivalent formula library, and have it comment the Flesch-Kincaid grade level and word count on every changed markdown file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Collect two to three weeks of baseline data
&lt;/h2&gt;

&lt;p&gt;Let the report-only check run across real pull requests for a few weeks. Look at the range of scores your existing, already-published docs are landing in. If your published API reference sits at grade 12 to 14 because of unavoidable technical vocabulary, a rule that requires grade 9 or below is going to fail on legitimate content constantly.&lt;/p&gt;

&lt;p&gt;This step also surfaces which formula fits your content best. If Gunning Fog consistently reports two to three grades higher than Flesch-Kincaid on your docs because of technical terms tripping its complex-word counter, that is useful to know before you commit to enforcing either one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Set a threshold based on your own baseline, not a generic target
&lt;/h2&gt;

&lt;p&gt;Once you have real numbers, set the CI threshold at something like "one grade level above your current median," not an arbitrary number pulled from a generic style guide that was never written with your docs in mind. This keeps the check meaningful: it catches genuine outliers, sentences or pages that got noticeably harder to read, without punishing normal technical writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Make the failure message actionable
&lt;/h2&gt;

&lt;p&gt;A failing check that just prints "readability score too high" teaches a contributor nothing. Have the script identify the specific sentences driving the score up, typically the longest ones or the ones with the most polysyllabic words, and print those in the CI output. Contributors who can see exactly which sentence to shorten will fix it in one commit instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Exempt reference material explicitly, not implicitly
&lt;/h2&gt;

&lt;p&gt;Decide upfront which doc types are exempt from the grade-level ceiling (API references, changelogs with version-specific terminology) versus which types should be held to it strictly (onboarding guides, tutorials, README introductions). Encode that as a path-based rule in the CI config rather than leaving it to reviewer judgment, which drifts over time and creates inconsistent enforcement.&lt;/p&gt;

&lt;p&gt;Following this sequence, report-only first, baseline before threshold, actionable failures, explicit exemptions, gets you a check that catches real regressions instead of one that gets muted in the team's notification settings within a month.&lt;/p&gt;

&lt;p&gt;For background on why different formulas can disagree by several grade levels on identical text, see the longer explainer on &lt;a href="https://evvytools.com/blog/why-readability-scores-disagree-on-the-same-paragraph/" rel="noopener noreferrer"&gt;why readability scores disagree on the same paragraph&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling the edge cases that break naive implementations
&lt;/h2&gt;

&lt;p&gt;A few situations trip up readability CI checks that work fine in testing. Code blocks and inline code spans inside markdown will confuse a formula that is not stripping them first, since a long variable name or a multi-line code sample will get counted as extremely long "words" and "sentences," wrecking the score for a page that is otherwise perfectly readable prose. Make sure your script strips fenced code blocks and inline code spans before running any formula against the remaining text.&lt;/p&gt;

&lt;p&gt;Tables and bullet lists have a similar problem: formulas built around sentence structure do not know what to do with a table cell or a one-word bullet point, and will sometimes report wildly inflated or deflated scores on pages that are mostly structured data rather than prose. Consider running the check only against prose paragraphs, and skipping table cells, code blocks, and single-line list items entirely.&lt;/p&gt;

&lt;p&gt;Auto-generated content, API reference pages built from OpenAPI specs or docstrings, should usually be excluded from the check altogether rather than exempted case by case. That content is typically generated from a template your team does not hand-edit line by line, so a readability score on it is not actionable in the way a score on hand-written prose is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What good looks like after a few months
&lt;/h2&gt;

&lt;p&gt;Teams that get this right end up with a CI check that fires rarely, catches real problems when it does, and gets fixed in a single commit each time. The failure comments reference specific sentences, the threshold reflects the team's actual writing rather than a generic target, and reference material with an unavoidably higher technical vocabulary floor is excluded rather than fought against. That is a very different experience than the blanket, ungraduated check most teams start with, and it is the difference between a tool the team trusts and one they quietly disable six months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rolling this out to a team that has never had a readability gate before
&lt;/h2&gt;

&lt;p&gt;If your team has zero existing tooling around prose quality, readability is a reasonable first check to introduce, precisely because it can start as report-only and never actually block anyone while you build trust in it. Announce it as an informational addition before it ships, not a new requirement. Nothing kills adoption of a quality gate faster than a team discovering a new blocking check in their pull request queue with no warning and no context for why the number matters.&lt;/p&gt;

&lt;p&gt;Pair the rollout with a short internal doc, even three or four sentences, explaining which formula you picked, why, and what the current baseline looks like. This matters more than the tooling itself. A contributor who understands "we use Flesch-Kincaid, our docs currently average grade 10, and pages above grade 13 usually mean an unnecessarily long sentence, not necessarily bad writing" will engage with a failing check productively. A contributor who just sees a red X with an unfamiliar formula name attached will not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring whether the check is actually helping
&lt;/h2&gt;

&lt;p&gt;After a quarter or two of running the check as a hard gate, look back at what it actually caught. Good outcomes look like: a handful of genuinely improved sentences, contributors who mention the check positively or reference it when writing new docs, and a stable or slowly improving baseline score over time. Bad outcomes look like: contributors routinely disabling or overriding the check, a growing list of path-based exemptions because the threshold keeps hitting content it should not, or the check firing so rarely that nobody remembers it exists. If you are seeing the bad outcomes, it is worth revisiting the threshold and the exemption list rather than assuming the check itself was a mistake, since most of these failure modes trace back to a threshold that was set before the baseline data step above was actually done.&lt;/p&gt;

&lt;p&gt;Tooling references: &lt;a href="https://docs.github.com/actions" rel="noopener noreferrer"&gt;GitHub Actions documentation&lt;/a&gt; covers how to post PR comments from a workflow script, the &lt;a href="https://pypi.org" rel="noopener noreferrer"&gt;textstat project on PyPI&lt;/a&gt; ecosystem hosts several open-source readability formula libraries you can wire into a pipeline directly, and the &lt;a href="https://vale.sh" rel="noopener noreferrer"&gt;Vale linter project&lt;/a&gt; is a popular open-source tool for enforcing broader prose style rules in CI alongside readability scoring.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>writing</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
    <item>
      <title>Why Your HTTP Security Headers Affect More Than Your Security Score</title>
      <dc:creator>EvvyTools</dc:creator>
      <pubDate>Mon, 06 Jul 2026 09:14:29 +0000</pubDate>
      <link>https://dev.to/evvytools/why-your-http-security-headers-affect-more-than-your-security-score-226</link>
      <guid>https://dev.to/evvytools/why-your-http-security-headers-affect-more-than-your-security-score-226</guid>
      <description>&lt;p&gt;Most teams treat security headers as a compliance checkbox: run a scanner, get a letter grade, add whatever headers get you from a C to an A, move on. That framing misses something worth knowing, which is that several of these headers also shape how crawlers and browsers evaluate your site in ways that show up outside any security audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content-Security-Policy can silently break rendering
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;Content-Security-Policy&lt;/code&gt; header that's too restrictive can block inline scripts or styles your page actually needs, which is a problem you'll usually notice fast in a browser but can be much harder to catch when it's a crawler rendering the page instead of a human. Google renders pages the way a browser would before judging their content quality, so a CSP that blocks a script your layout depends on can produce the same "page looks broken" outcome for a crawler that a misconfigured robots.txt rule can, just through a completely different mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Referrer-Policy affects your own analytics
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Referrer-Policy&lt;/code&gt; controls how much of the referring URL gets passed along when someone clicks a link from your site to another one, or from another site to yours. Set it too aggressively (&lt;code&gt;no-referrer&lt;/code&gt; everywhere) and you lose visibility into which of your own pages are driving outbound clicks in your analytics. Set it too permissively and you might be leaking full query-string URLs, including anything sensitive that ended up in a URL parameter, to third-party sites your users click through to. &lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt; is a reasonable default for most sites: full referrer on same-origin navigation, origin-only across origins.&lt;/p&gt;

&lt;h2&gt;
  
  
  X-Frame-Options and embeddability
&lt;/h2&gt;

&lt;p&gt;If you ever want your own content embeddable in an iframe elsewhere (a widget, a partner integration, a preview card), a blanket &lt;code&gt;X-Frame-Options: DENY&lt;/code&gt; will block that entirely, with no partial-allow option. &lt;code&gt;Content-Security-Policy&lt;/code&gt;'s &lt;code&gt;frame-ancestors&lt;/code&gt; directive is the more flexible modern replacement, letting you allowlist specific origins instead of an all-or-nothing rule. Worth checking which one your stack is actually sending, since a lot of default configs still ship the older, less flexible header.&lt;/p&gt;

&lt;h2&gt;
  
  
  HSTS has a real, sticky downside if you get it wrong
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Strict-Transport-Security&lt;/code&gt; tells browsers to only ever connect to your domain over HTTPS, for a duration you specify, and browsers cache that instruction aggressively. If you set a long &lt;code&gt;max-age&lt;/code&gt; and then later need to run a subdomain over plain HTTP for some reason, users who already cached the HSTS policy simply can't reach it until the policy expires, no matter what your server does. Set it deliberately, understand the &lt;code&gt;includeSubDomains&lt;/code&gt; and &lt;code&gt;preload&lt;/code&gt; flags before turning them on, and don't copy a max-age value from a template without knowing what you're committing to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions-Policy can quietly disable embeds you rely on
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Permissions-Policy&lt;/code&gt; (the successor to &lt;code&gt;Feature-Policy&lt;/code&gt;) lets you turn off browser features like camera, microphone, geolocation, or autoplay for your own page and for anything embedded in it. Teams usually add it for the obvious reason: lock down features you don't use so a compromised third-party script can't quietly turn on the microphone. That part works as advertised and is worth doing.&lt;/p&gt;

&lt;p&gt;Where it gets messy is with third-party embeds you actually want. A YouTube video embed expects &lt;code&gt;autoplay&lt;/code&gt; and sometimes &lt;code&gt;fullscreen&lt;/code&gt; to be allowed for the frame it sits in. A payment widget or an identity verification vendor often needs &lt;code&gt;camera&lt;/code&gt; or &lt;code&gt;publickey-credentials-get&lt;/code&gt; passed through explicitly, because permissions don't propagate to iframes by default under a restrictive policy. If you write a blanket &lt;code&gt;Permissions-Policy&lt;/code&gt; that disables everything and never add the specific &lt;code&gt;allow=&lt;/code&gt; exceptions those embeds need, the embed doesn't error loudly, it just silently fails to do the one thing it was there for, like a video that never autoplays or a scanner that never gets camera access. Debugging that is annoying precisely because the page looks fine, nothing throws a console error a casual glance would catch, and the feature just doesn't work.&lt;/p&gt;

&lt;p&gt;The fix is boring but effective: whenever you add a third-party embed, check its documentation for which permissions it expects the parent frame to delegate, and add those explicitly rather than copying a maximally locked-down policy from a template and assuming it'll be fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-Origin-Resource-Policy and Cross-Origin-Opener-Policy can break resource loading
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Cross-Origin-Resource-Policy&lt;/code&gt; (CORP) and &lt;code&gt;Cross-Origin-Opener-Policy&lt;/code&gt; (COOP) exist mainly to isolate your page from cross-origin attacks like Spectre-style side channels, and browsers have been pushing sites toward setting them more aggressively over the last few years. The side effect that catches people off guard is that both headers can block perfectly legitimate cross-origin loads if the values don't match what the other side expects.&lt;/p&gt;

&lt;p&gt;Set &lt;code&gt;Cross-Origin-Resource-Policy: same-origin&lt;/code&gt; on an asset your own CDN serves from a different subdomain than your main site, and the browser will refuse to let your main page load it, because from the browser's point of view that's a different origin making the request. The asset returns fine over the network, dev tools show a 200, and the resource still never renders because the policy blocked it after the fact. The usual fix is &lt;code&gt;cross-origin&lt;/code&gt; or &lt;code&gt;same-site&lt;/code&gt; instead of &lt;code&gt;same-origin&lt;/code&gt;, depending on how your subdomains are actually laid out.&lt;/p&gt;

&lt;p&gt;COOP causes a subtler version of the same problem with popups and OAuth flows. A strict &lt;code&gt;Cross-Origin-Opener-Policy: same-origin&lt;/code&gt; can sever the &lt;code&gt;window.opener&lt;/code&gt; reference a login popup relies on to communicate back to the page that opened it, which breaks the "log in via popup, then refresh the parent page" pattern some auth providers use. If you support any third-party login flow that depends on window messaging between a popup and its opener, test that flow specifically after tightening COOP, because it's not the kind of break that shows up in a normal page load.&lt;/p&gt;

&lt;h2&gt;
  
  
  CORS headers and CDN caching don't always agree
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; and CDN caching interact in a way that trips people up because each system is reasoning about the request independently. If your CDN caches a response and that cached response included an &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; header scoped to one specific origin, every subsequent request for that same cached asset gets served with the same CORS header, regardless of which origin is actually asking for it this time.&lt;/p&gt;

&lt;p&gt;That means a font or JSON file that legitimately needs to be fetchable from multiple origins can end up cached with only the first requesting origin's value baked into the response, silently breaking cross-origin fetches from every other origin until the cache expires or gets purged. The standard fix is to add &lt;code&gt;Vary: Origin&lt;/code&gt; alongside your CORS header so the CDN treats different origins as different cache entries instead of collapsing them into one. It costs you some cache efficiency in exchange for correctness, which is usually the right trade for anything served to more than one origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this connects back to crawling
&lt;/h2&gt;

&lt;p&gt;Headers and crawl directives are two separate mechanisms that occasionally interact in confusing ways, similar to how robots.txt and a page's &lt;code&gt;noindex&lt;/code&gt; meta tag are separate mechanisms people conflate. &lt;a href="https://evvytools.com/blog/why-robots-txt-quietly-blocks-pages-you-want-indexed/" rel="noopener noreferrer"&gt;I wrote a longer breakdown of the robots.txt side of that confusion here&lt;/a&gt;, including why blocking a folder in robots.txt doesn't remove already-indexed pages the way people expect.&lt;/p&gt;

&lt;p&gt;I run new deployments through &lt;a href="https://evvytools.com/tools/dev-tech/http-security-grader/" rel="noopener noreferrer"&gt;EvvyTools' HTTP Security Header Grader&lt;/a&gt; alongside a normal Lighthouse pass, mostly because it catches the headers that don't show up in a standard performance audit at all.&lt;/p&gt;

&lt;p&gt;For deeper reference, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" rel="noopener noreferrer"&gt;MDN's HTTP headers documentation&lt;/a&gt; covers every header mentioned here in more depth, &lt;a href="https://owasp.org/www-project-secure-headers/" rel="noopener noreferrer"&gt;OWASP's Secure Headers Project&lt;/a&gt; has a solid checklist if you're setting these up from scratch, and &lt;a href="https://www.w3.org/TR/CSP3/" rel="noopener noreferrer"&gt;the W3C's CSP specification&lt;/a&gt; is the authoritative source if you need to resolve an edge case the summaries don't cover.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>seo</category>
      <category>tools</category>
    </item>
    <item>
      <title>How to Test a Wildcard Pattern Before You Ship It, Not After</title>
      <dc:creator>EvvyTools</dc:creator>
      <pubDate>Mon, 06 Jul 2026 09:14:28 +0000</pubDate>
      <link>https://dev.to/evvytools/how-to-test-a-wildcard-pattern-before-you-ship-it-not-after-dl0</link>
      <guid>https://dev.to/evvytools/how-to-test-a-wildcard-pattern-before-you-ship-it-not-after-dl0</guid>
      <description>&lt;p&gt;Wildcard and regex patterns share a specific failure mode: they almost always match more, or less, than the person writing them assumed. The pattern reads correctly in your head. The engine reading it doesn't share your assumptions, and it will match exactly what you wrote, not what you meant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Write down what you actually want matched and excluded
&lt;/h2&gt;

&lt;p&gt;Before touching the pattern itself, list concrete example strings, some that should match, some that shouldn't. This sounds obvious and gets skipped constantly. A pattern meant to catch query-string URLs (&lt;code&gt;?sort=price&lt;/code&gt;) needs explicit test cases for URLs with a literal &lt;code&gt;?&lt;/code&gt; in a different context, like an encoded character in a slug, so you can confirm the pattern doesn't also catch those.&lt;/p&gt;

&lt;p&gt;Write this list down somewhere durable, not just in your head while you iterate. A plain text file with one example per line, prefixed with MATCH or NO-MATCH, takes two minutes to create and becomes the seed for the automated test suite described further down. Skipping this step is how a pattern ships that technically works on the three examples the author tried and fails on the fourth one a user actually sends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Test the narrowest version first
&lt;/h2&gt;

&lt;p&gt;Start with the most specific pattern that covers your primary case, then broaden it deliberately, testing after each change. Going the other direction, starting broad and trying to add exceptions, tends to produce patterns nobody can fully reason about six months later, because each exception was bolted on to fix one specific test case without re-checking the others.&lt;/p&gt;

&lt;p&gt;The narrow-first approach also makes code review easier. A reviewer can look at a tight pattern and confirm it matches the stated intent in a few seconds. A pattern that's been broadened repeatedly with negative lookaheads bolted on for each edge case usually takes longer to verify than to rewrite from scratch, and reviewers often just wave it through instead, which is how subtle bugs survive review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Check greedy vs. lazy quantifiers explicitly
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.*&lt;/code&gt; and &lt;code&gt;.*?&lt;/code&gt; (greedy vs. lazy) produce dramatically different results on strings with repeated delimiters, and this is one of the most common sources of "why did this match the whole string instead of just the part between the first two quotes" bugs. Test any pattern with a quantifier against a string containing the delimiter more than once, not just the simple single-occurrence case that usually gets written first.&lt;/p&gt;

&lt;p&gt;A concrete example: matching text inside quotes with &lt;code&gt;".*"&lt;/code&gt; against &lt;code&gt;"first" and "second"&lt;/code&gt; greedily matches from the first quote to the last, swallowing both pairs and the word "and" in between. Switching to &lt;code&gt;".*?"&lt;/code&gt; stops at the first closing quote instead. Neither choice is universally correct, it depends on what you're parsing, but you have to test both against a multi-occurrence string to know which one you actually need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Run it against real production data, not synthetic examples
&lt;/h2&gt;

&lt;p&gt;Synthetic test strings are written by the same person who wrote the pattern, which means they tend to avoid the exact edge cases that would actually break it. Pulling a sample of real URLs, log lines, or user input and running the pattern against all of them surfaces mismatches a clean synthetic test suite won't.&lt;/p&gt;

&lt;p&gt;If you don't have production data handy, the next best option is user-submitted content that's already messy by nature: free-text form fields, uploaded filenames, or copy-pasted URLs with tracking parameters attached. These sources reliably contain the stray whitespace, mixed casing, and unexpected punctuation that a hand-written test list tends to omit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Confirm the match groups, not just whether it matched
&lt;/h2&gt;

&lt;p&gt;A pattern can technically "match" while capturing the wrong groups, which is often worse than not matching at all, since downstream code will happily extract the wrong substring and keep going. Always inspect the actual captured groups during testing, not just the pass/fail result.&lt;/p&gt;

&lt;p&gt;This matters most with nested or optional groups, where a group can be present in the match but empty, or absent entirely and return null depending on the engine. Code that assumes a captured group is always a non-empty string will throw or silently produce garbage the first time that assumption breaks, so test the group values explicitly, not just whether the overall pattern returned true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Test for catastrophic backtracking before the pattern touches user input
&lt;/h2&gt;

&lt;p&gt;If any part of a pattern accepts user-supplied text and runs it through a regex with nested quantifiers, like &lt;code&gt;(a+)+&lt;/code&gt; or &lt;code&gt;(.*)*&lt;/code&gt;, you need to test what happens on adversarial input before shipping, not after a server hangs in production. This failure mode is called catastrophic backtracking, or ReDoS (regular expression denial of service): certain input strings cause the engine to try an exponential number of ways to fail a match, and a string of just a few dozen characters can pin a CPU core for seconds or minutes.&lt;/p&gt;

&lt;p&gt;The test for this is straightforward: take your pattern and feed it a string designed to almost match but not quite, repeated enough times to be interesting, for example thirty repetitions of a character followed by one character that breaks the match. Time how long the pattern takes to run against it. If the time grows sharply as you add a few more repetitions rather than staying roughly flat, the pattern has backtracking risk and needs to be rewritten, usually by making quantifiers more specific or using atomic groups or possessive quantifiers if your engine supports them. This test costs a few minutes and catches a class of bug that's genuinely dangerous if the pattern ever runs against anything a user controls, like a search box, a filter, or a custom validation rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Check the pattern against the actual engine flavor it will run in
&lt;/h2&gt;

&lt;p&gt;Not every regex engine agrees on syntax. PCRE, POSIX, and JavaScript's engine all diverge on details like lookbehind support, possessive quantifiers, and how character classes handle Unicode, and a pattern that works when you test it in one context can behave differently, or fail to compile at all, in another. This matters most when the same pattern is duplicated across a frontend written in JavaScript and a backend written in Python or another language, which is common for form validation that's checked client-side and re-checked server-side.&lt;/p&gt;

&lt;p&gt;If your pattern needs to work identically in both places, test it in both places, not just the one you happened to write it in first. A quick way to catch this early is running the exact same pattern and test strings through each target language's regex implementation and diffing the results. A mismatch means the pattern needs a compatible rewrite, or the two checks need to tolerate slightly different behavior on purpose. &lt;a href="https://docs.python.org/3/library/re.html" rel="noopener noreferrer"&gt;Python's official &lt;code&gt;re&lt;/code&gt; module documentation&lt;/a&gt; is a solid reference for exactly which syntax it supports and where it differs from PCRE, since some patterns that look identical on paper parse differently between the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Turn your test cases into an automated suite that runs in CI
&lt;/h2&gt;

&lt;p&gt;Manual testing catches problems the day you write the pattern. It does nothing for the day six months later when someone tweaks the pattern to fix an unrelated bug and breaks a case nobody remembered to check by hand. The fix is turning the match and non-match examples from Step 1 into an actual automated test, however small, that runs whenever the pattern changes.&lt;/p&gt;

&lt;p&gt;This doesn't need to be elaborate. A simple test file with an array of &lt;code&gt;{ input, shouldMatch }&lt;/code&gt; pairs and a loop that asserts each one, wired into whatever test runner your project already uses, is enough. The goal isn't full coverage of every conceivable string, it's a tripwire: if a future edit to the pattern breaks a case that used to pass, the test fails in CI before it ships, instead of surfacing as a support ticket after a user hits it in production. Patterns that guard anything security-relevant, like input sanitization or auth token formats, benefit from this most, since a silent regression there is the kind of bug that doesn't announce itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this discipline matters beyond regex specifically
&lt;/h2&gt;

&lt;p&gt;This same testing discipline applies almost identically to robots.txt wildcard rules, which use a simpler pattern syntax but fail in the same way: a &lt;code&gt;Disallow: /*?&lt;/code&gt; rule looks like it targets query strings specifically, and does, but also matches any path containing a literal &lt;code&gt;?&lt;/code&gt; anywhere, which can catch URLs nobody intended to block. &lt;a href="https://evvytools.com/blog/why-robots-txt-quietly-blocks-pages-you-want-indexed/" rel="noopener noreferrer"&gt;I broke down that exact failure mode and several others in a longer piece here&lt;/a&gt;, if you're dealing with crawl directives specifically rather than application code.&lt;/p&gt;

&lt;p&gt;I use &lt;a href="https://evvytools.com/tools/dev-tech/regex-tester/" rel="noopener noreferrer"&gt;EvvyTools' Regex Tester&lt;/a&gt; for quick iteration since it shows capture groups and match boundaries live as you edit, which speeds up step 5 considerably compared to running a script and printing results manually.&lt;/p&gt;

&lt;p&gt;For deeper reference, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions" rel="noopener noreferrer"&gt;MDN's regular expressions guide&lt;/a&gt; is the best general-purpose explainer, and &lt;a href="https://www.regular-expressions.info/" rel="noopener noreferrer"&gt;the regexp info reference site&lt;/a&gt; documents cross-engine differences (PCRE vs. JavaScript vs. POSIX) that matter if your pattern needs to work identically across a frontend and a backend written in different languages.&lt;/p&gt;

</description>
      <category>regex</category>
      <category>webdev</category>
      <category>tools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Write a Content Brief a Writer Will Actually Follow</title>
      <dc:creator>EvvyTools</dc:creator>
      <pubDate>Sun, 05 Jul 2026 09:15:33 +0000</pubDate>
      <link>https://dev.to/evvytools/how-to-write-a-content-brief-a-writer-will-actually-follow-44k</link>
      <guid>https://dev.to/evvytools/how-to-write-a-content-brief-a-writer-will-actually-follow-44k</guid>
      <description>&lt;p&gt;A brief that's too loose gets ignored in practice because the writer has to make every structural decision from scratch anyway, which means the brief added planning overhead without actually reducing the writer's workload. A brief that's too rigid also gets effectively ignored, or worse, followed to the letter, because following it word for word tends to produce flat, mechanical prose that reads like it was assembled from a checklist rather than written by a person with a point of view. The useful middle ground here is narrower than most content briefs actually land on, and hitting it takes a specific, repeatable set of steps rather than just "writing a more detailed brief."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Separate the non-negotiables from the suggestions
&lt;/h2&gt;

&lt;p&gt;Start by explicitly splitting your brief into two distinct lists, labeled differently in the document itself. The first list is what must be true in the final draft no matter what: the target keyword, the required internal links, the word count range, the intended audience. The second list is everything else that's useful context but not a hard requirement: tone notes, a rough optional outline, a couple of competitor examples worth glancing at. Writers who can't easily tell which parts of a brief are requirements and which are merely suggestions tend to default to treating everything in the document as a requirement, which is exactly what produces flat, template-shaped drafts that all sound the same regardless of who wrote them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Give the angle, not the outline
&lt;/h2&gt;

&lt;p&gt;State the specific angle the piece needs to take in one or two sentences, rather than handing over a full five-level H2/H3 outline for the writer to fill in like a form. "This piece should explain why the tool's default setting causes confusion for new users specifically, using a concrete before-and-after example from a real support ticket" tells the writer what the piece is actually for and why it needs to exist. A detailed pre-built outline, by contrast, tells the writer to fill in blanks under headings someone else already chose. The first approach produces original structure suited to the specific angle. The second reliably produces a shape that every reader who has seen enough content in this space will recognize as templated on sight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Name the one thing that has to be true by the end
&lt;/h2&gt;

&lt;p&gt;Every brief should include a single clear sentence answering "what does the reader now know or believe by the end of this piece that they didn't before they started reading it." If you genuinely cannot write that sentence, the brief doesn't have a clear enough job yet, and the draft that comes out of it won't either, no matter how skilled the writer is. Writers who don't know the actual goal of a specific piece default to covering the topic generically and safely, which is exactly where a lot of interchangeable, forgettable content in any given niche comes from in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Include real examples, not placeholder text
&lt;/h2&gt;

&lt;p&gt;A brief with "[insert example here]" as a placeholder gives the writer nothing concrete to work from and just defers the hard thinking to draft time. A brief that includes an actual example, even a rough, imperfect one, anchors the tone and the level of specificity you actually want far more effectively than an adjective like "conversational" or "authoritative" ever will on its own. Show the writer what good looks like instead of describing it abstractly and hoping they infer the same thing you have in your head.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Leave the sentence-level decisions alone entirely
&lt;/h2&gt;

&lt;p&gt;Do not specify exact sentence length, paragraph rhythm, or precise phrasing anywhere in the brief itself. This is the step most briefs get wrong, usually with genuinely good intentions behind the mistake: an editor wants visual and tonal consistency across a whole site, so the brief slowly starts dictating rhythm sentence by sentence over successive revisions. The result across a body of work is prose that all sounds statistically similar regardless of which writer actually produced it, and somewhat ironically, prose that reads as more mechanically uniform than natural, unconstrained human writing usually does on its own. Save structural consistency notes for a separate style guide document the writer references independently, and keep the brief itself focused on what the piece needs to accomplish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for more than just writer experience
&lt;/h2&gt;

&lt;p&gt;A brief that over-specifies structure at the sentence level produces finished text with unusually low sentence-length variance and unusually consistent phrasing across different writers on the same team, which happens to be almost exactly the statistical signature that makes writing look machine-generated even when every single word was typed by a human being. This isn't purely an aesthetic problem confined to how the prose feels to read. Teams that have started running finished content through AI detectors as an informal quality check have found that heavily templated briefs produce a measurably higher false-positive rate on genuinely human-written drafts, simply because the brief had already flattened the natural variance out of the writing before a human writer ever touched the page.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://contentmarketinginstitute.com/" rel="noopener noreferrer"&gt;Content Marketing Institute&lt;/a&gt; has published general guidance on brief structure that's worth cross-referencing against whatever template your own team is currently using. The &lt;a href="https://www.nngroup.com/" rel="noopener noreferrer"&gt;Nielsen Norman Group's usability research&lt;/a&gt; covers how overly rigid content templates measurably affect reader engagement and comprehension, which is really the flip side of the same underlying problem: briefs that over-constrain writers at the sentence level tend to under-serve the actual readers of the finished piece too, not just the writer producing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example of the two-tier split in practice
&lt;/h2&gt;

&lt;p&gt;Consider a brief for a piece about choosing a database index strategy. The non-negotiables list might read: target keyword "composite index order," minimum 1400 words, one internal link to the site's own indexing tool, audience is backend developers with some SQL experience but no deep DBA background. The suggestions list might read: consider opening with a slow-query war story, a rough optional structure moving from single-column to composite to covering indexes, maybe reference a specific ORM's default behavior if it fits naturally. Notice how the second list uses words like "consider" and "maybe" throughout, deliberately signaling to the writer that these are options to weigh, not steps to execute in order. A writer handed this brief still has to make real decisions: which war story, whether the ORM reference actually earns its place, how long each section needs to be to serve the angle rather than to fill a template slot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when teams skip the two-tier split
&lt;/h2&gt;

&lt;p&gt;Without an explicit split, briefs tend to accumulate detail unevenly over time as different editors add notes to a shared template. A well-meaning editor adds "should probably mention indexing overhead on writes" as a suggestion, a stricter editor later reads that same line as a requirement and starts rejecting drafts that omit it, and within a few months the "suggestions" section has quietly become a second non-negotiables list that nobody explicitly decided to create. The fix isn't more discipline from any single editor, it's making the two categories structurally distinct in the document itself, different heading, different formatting, so the ambiguity can't creep back in through normal editorial drift.&lt;/p&gt;

&lt;p&gt;If you're building content briefs at any real volume across a team, the &lt;a href="https://evvytools.com/tools/writing-content/content-brief-builder/" rel="noopener noreferrer"&gt;free content brief builder by EvvyTools&lt;/a&gt; generates the two-tier structure described above, non-negotiables clearly separated from suggestions, from just a target keyword and a rough topic description, and deliberately leaves every sentence-level decision to the individual writer by design rather than pre-specifying them. The &lt;a href="https://www.stc.org/" rel="noopener noreferrer"&gt;Society for Technical Communication&lt;/a&gt; publishes broader guidance on structuring writer instructions that's worth a look if you're designing a brief template from scratch rather than adapting an existing one. For more on why detector-based content quality checks sometimes end up catching templated writing style rather than actual AI usage, there's a related, more detailed piece on &lt;a href="https://evvytools.com/blog/why-ai-detectors-flag-human-writing-as-ai/" rel="noopener noreferrer"&gt;why AI detectors flag human writing as AI-generated&lt;/a&gt; that's worth reading before your own team adopts a detector as a hard quality gate on contributed or commissioned writing.&lt;/p&gt;

</description>
      <category>tools</category>
      <category>writing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why Technical Writers Get Falsely Flagged by AI Detectors More Than Anyone Else</title>
      <dc:creator>EvvyTools</dc:creator>
      <pubDate>Sun, 05 Jul 2026 09:15:32 +0000</pubDate>
      <link>https://dev.to/evvytools/why-technical-writers-get-falsely-flagged-by-ai-detectors-more-than-anyone-else-1j5h</link>
      <guid>https://dev.to/evvytools/why-technical-writers-get-falsely-flagged-by-ai-detectors-more-than-anyone-else-1j5h</guid>
      <description>&lt;p&gt;If you write documentation, API references, or runbooks for a living, and you've ever run a draft through an AI detector out of curiosity or because a client asked you to, you may have noticed something uncomfortable: your score tends to run higher than you'd expect. Not because you used AI to write any of it, but because of how disciplined technical writing is structured at a sentence level, which happens to overlap statistically with the patterns detectors are built to catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The style guide is working against you
&lt;/h2&gt;

&lt;p&gt;Most technical writing style guides mandate the exact features an AI detector treats as suspicious signals. Active voice, consistent terminology used the same way every time it appears, short declarative sentences, and parallel structure across similar sections of a document. A style guide that says "use the same term for the same concept every single time" is directly asking you to lower your own vocabulary diversity score, because vocabulary diversity is literally a measure of how much you vary your word choices across a passage. A guide that says "keep sentences short and direct, avoid subordinate clauses where possible" is asking you to lower your sentence-length variance, which is exactly what detectors read as low burstiness, one of their core suspicious signals.&lt;/p&gt;

&lt;p&gt;None of this is a coincidence in the sense that both language models and disciplined technical writers are optimizing toward the same statistical target: predictable, low-noise prose that's easy to parse quickly under time pressure. The model gets there because it was trained explicitly to minimize surprise at each next word. The technical writer gets there because a confused reader in the middle of a production incident at 3am is a reader who cannot afford ambiguity in a runbook step. Two completely different reasons for writing, arriving at the same statistical destination, and a detector cannot tell the difference between the two just by looking at the finished text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually causes real, costly problems
&lt;/h2&gt;

&lt;p&gt;Some organizations now run detector checks on contributed documentation, whether from external contractors or from community contributors to open source docs, as a spam filter or a plagiarism check before accepting a contribution. A contractor who has been writing clean, consistent API reference documentation for a decade can trip a detector's threshold simply by doing their job well and following the house style guide, and then has to explain a statistical artifact to a project maintainer who has never heard the word "perplexity" used this way and doesn't know what a false positive rate even means in this context.&lt;/p&gt;

&lt;p&gt;This is a real, measurable cost, not a hypothetical inconvenience. A flagged contribution that gets held up for extra review, or a contractor invoice that gets questioned because of a detector score attached to the deliverable, wastes time on a signal that was never validated for this use case in the first place. The fix isn't to deliberately write worse, more chaotic documentation just to dodge a score, that would be actively harmful to the people relying on the docs. The fix is for whoever is reviewing flagged content to understand what the score is actually measuring, and what it demonstrably is not measuring, before treating it as evidence of anything at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually distinguishes AI text from disciplined human text
&lt;/h2&gt;

&lt;p&gt;If detector scores alone aren't reliable for this specific population of writers, what does actually help separate the two in practice? A few things a purely statistical model has no access to at all: whether the writing references specific internal system names, unusual edge cases, or incident history that the writer would only plausibly know from direct, hands-on experience with the system being documented; whether the phrasing matches the same author's prior documentation in tone, structure, and characteristic word choices over time; and whether there are small, characteristic imperfections, a slightly awkward but functionally correct explanation of a genuinely tricky edge case, that tend to show up specifically in writing from someone who actually debugged the thing they're describing rather than someone summarizing it secondhand.&lt;/p&gt;

&lt;p&gt;None of these checks are automatable the way a single detector score is, which is exactly why they get skipped in practice in favor of the faster, cheaper, and considerably less reliable signal. The &lt;a href="https://developers.google.com/style" rel="noopener noreferrer"&gt;Google Developer Documentation Style Guide&lt;/a&gt; and the &lt;a href="https://learn.microsoft.com/en-us/style-guide/welcome/" rel="noopener noreferrer"&gt;Microsoft Writing Style Guide&lt;/a&gt; both codify, in explicit detail, the consistency-first approach that produces this exact statistical overlap with machine-generated text, for anyone who wants to see the actual published guidance technical writers across the industry are following and understand why the overlap exists structurally rather than coincidentally.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete example of the mismatch in practice
&lt;/h2&gt;

&lt;p&gt;Take a real sentence from a hypothetical API changelog: "The rate limit was increased from 100 to 500 requests per minute for authenticated endpoints." That's a textbook agentless passive, following house style exactly as written, because the changelog reader cares about the rate limit change, not about which engineer on the platform team flipped the config value. Run that same sentence through a detector tuned on general web prose and it will likely register as unusually predictable: common vocabulary, a passive construction, no rhetorical flourish anywhere in it. Now compare it to a sentence from that same writer's personal blog written the same week: "I spent way too long last night trying to figure out why our rate limiter kept doubling requests, and it turned out to be a stale cache entry nobody had touched in months." Same person, wildly different statistical profile, because the genre and the audience are different. A detector comparing only the changelog sentence against a generic human-writing baseline has no way to know it's looking at disciplined technical prose rather than machine output, and that's the exact gap that produces a false positive on a writer who did nothing wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reasonable process fix for teams that rely on these scores
&lt;/h2&gt;

&lt;p&gt;If your team relies on an AI detector as part of a contribution review process for documentation, it's worth running a few samples of your own team's most experienced, most trusted writers' historical documentation through the same detector first, before using any threshold from that tool against outside contributors. Seeing your own baseline false-positive rate on writing you already know for a fact is human and original is the fastest way to calibrate how much weight the score deserves in your actual review process, and it often produces a genuinely uncomfortable but useful result. The &lt;a href="https://www.writethedocs.org/" rel="noopener noreferrer"&gt;Write the Docs community guidelines&lt;/a&gt; are a good general reference for documentation review practices that don't lean on a single automated signal as a gate.&lt;/p&gt;

&lt;p&gt;There's a fuller breakdown of the underlying statistical reasoning, including the specific research on which populations of writers get flagged most often and why, over on &lt;a href="https://evvytools.com/blog/why-ai-detectors-flag-human-writing-as-ai/" rel="noopener noreferrer"&gt;EvvyTools' recent piece on why detectors misfire on genuine human writing&lt;/a&gt;. If you want to check where your own writing currently sits before anyone else runs it through a detector and hands you a number with no context, the site's own AI content detection tool breaks the score down sentence by sentence instead of handing you one opaque figure to argue about after the fact.&lt;/p&gt;

</description>
      <category>tools</category>
      <category>writing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why Your Team's SQL Style Guide Keeps Getting Ignored</title>
      <dc:creator>EvvyTools</dc:creator>
      <pubDate>Sat, 04 Jul 2026 09:15:32 +0000</pubDate>
      <link>https://dev.to/evvytools/why-your-teams-sql-style-guide-keeps-getting-ignored-3kj</link>
      <guid>https://dev.to/evvytools/why-your-teams-sql-style-guide-keeps-getting-ignored-3kj</guid>
      <description>&lt;p&gt;Most teams that write one don't actually have a SQL style guide problem. They have an enforcement problem wearing a style guide's clothes. The guide exists, it's reasonable, someone even linked it in the onboarding doc, and six months later half the queries in the codebase don't follow it anyway, and nobody quite remembers when it stopped mattering.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkr2wlw6m1wfxhb37l05x.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkr2wlw6m1wfxhb37l05x.jpeg" alt="open reference document on a stand" width="799" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by Luis Quintero on &lt;a href="https://www.pexels.com" rel="noopener noreferrer"&gt;Pexels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The guide is a suggestion, and suggestions decay
&lt;/h2&gt;

&lt;p&gt;A style guide that lives in a wiki page or a README has no mechanism forcing anyone to check it before writing code. New team members don't read onboarding docs closely enough to internalize keyword-case preferences from page four of a style guide, especially in their first week when there are a dozen more urgent things competing for attention. Existing team members forget it exists the moment they're deep in a debugging session and just want the query to work. Neither group is being careless. The guide simply has no teeth, and documentation without enforcement decays at a predictable rate regardless of how well it was written.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual review catches maybe half of it
&lt;/h2&gt;

&lt;p&gt;Reviewers are supposed to be the backstop, but reviewers are humans looking at logic, not whitespace police. A reviewer who's focused on whether a join is correct will let inconsistent casing slide because flagging it feels like nitpicking, and nobody wants to be the person who blocks a PR over &lt;code&gt;select&lt;/code&gt; versus &lt;code&gt;SELECT&lt;/code&gt; when the actual logic is sound. So it slides, repeatedly, until the guide is more theoretical than real, and the gap between "documented standard" and "actual codebase" widens every sprint.&lt;/p&gt;

&lt;p&gt;There's also a social cost reviewers are implicitly weighing that rarely gets said out loud: flagging style issues repeatedly on the same person's PRs reads as nitpicking even when it's technically correct, and most reviewers would rather preserve the working relationship than win every formatting argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix isn't a stricter guide, it's removing the human step
&lt;/h2&gt;

&lt;p&gt;The teams that actually maintain consistent SQL formatting long-term almost always have one thing in common: a formatter running automatically, either in a pre-commit hook or a CI check, so the convention gets applied without anyone deciding to apply it. The style guide becomes documentation of what the automated tool already enforces, rather than the primary enforcement mechanism itself, which is a meaningfully different (and much more durable) role for it to play.&lt;/p&gt;

&lt;p&gt;This matters because it removes the social cost of enforcement entirely. Nobody has to be the person nagging a teammate about casing in a PR comment. The tool just does it, silently, every time, and the conversation about style only comes up once, when the team decides what the automated config should say, instead of recurring on every single pull request forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  A pattern worth recognizing in other parts of your stack
&lt;/h2&gt;

&lt;p&gt;This isn't a SQL-specific problem, it's a general pattern about any written standard that lacks automated enforcement. API design guidelines, commit message conventions, and naming standards for cloud resources all suffer from the exact same decay curve: documented once with good intentions, followed closely for a few weeks, then quietly abandoned as soon as the initial enthusiasm fades and nobody's actively checking. If your team has other written standards that feel similarly unenforced, the same diagnosis probably applies, and the same fix (automate the check, stop relying on memory) is probably the right move there too.&lt;/p&gt;

&lt;p&gt;Recognizing this pattern is useful because it reframes the SQL formatting problem as not really about SQL at all. It's about the general principle that a standard without an automated check is really just a suggestion, no matter how carefully it was written or how good the reasoning behind it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;Format on save in the editor, a pre-commit hook that blocks a commit if formatting doesn't match, or a CI step that fails the build. Any of the three works, and most teams end up with some combination of at least two for redundancy. What doesn't work is a written guide with no automated check behind it, because the guide is only as strong as someone's memory in the moment they're writing code, and memory under deadline pressure is not a reliable enforcement mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting from "guide nobody follows" to "convention that sticks"
&lt;/h2&gt;

&lt;p&gt;If your team is at the "we have a guide nobody follows" stage, the fastest unstick is picking a formatter, running it against a few real queries to confirm the output matches what your team actually wants, and then wiring it into whichever step (save, commit, or CI) your team is most likely to actually respect given how your workflow already operates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://evvytools.com/tools/dev-tech/sql-formatter/" rel="noopener noreferrer"&gt;https://evvytools.com/tools/dev-tech/sql-formatter/&lt;/a&gt; is a fast way to preview what a given convention produces before you commit your team to it, with no setup required to just see the output on a real query. The reasoning behind why different formatters default to different conventions in the first place is covered in more depth in &lt;a href="https://evvytools.com/blog/why-sql-formatters-disagree-on-keyword-case-and-indent-style/" rel="noopener noreferrer"&gt;this piece on SQL formatter keyword case and indent style&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For how other language communities solved the same enforcement problem, &lt;a href="https://go.dev/blog/gofmt" rel="noopener noreferrer"&gt;Go's gofmt documentation&lt;/a&gt; and the &lt;a href="https://prettier.io/" rel="noopener noreferrer"&gt;Prettier project&lt;/a&gt; are both worth studying, since they turned "the style guide" into "the tool" for their respective ecosystems and largely ended the recurring debate that SQL teams are still having today. The &lt;a href="https://black.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;Black documentation&lt;/a&gt; is another good reference point for how Python's community made the same transition from written convention to automated enforcement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SQL lagged behind other languages on this
&lt;/h2&gt;

&lt;p&gt;It's worth asking why application languages solved this years before most SQL codebases did. Part of the answer is tooling maturity: Go shipped &lt;code&gt;gofmt&lt;/code&gt; as part of the language's own toolchain from very early on, which meant there was never really a period where teams had to choose to adopt formatting discipline, it was just there by default from day one. SQL has no single toolchain in that sense. It's a query language embedded across dozens of different application stacks, ORMs, migration tools, and BI platforms, each with its own conventions and none of them treating a shared formatter as a first-class default the way &lt;code&gt;go build&lt;/code&gt; does.&lt;/p&gt;

&lt;p&gt;That fragmentation is exactly why the problem persists longer in SQL-heavy codebases than in, say, a pure Go or Python repo. There's no single obvious tool everyone already has installed. Picking one, configuring it, and wiring it into your specific stack is a deliberate decision a team has to make, rather than something that comes for free with the language. That extra friction is real, but it's also a one-time cost, and it's considerably smaller than the ongoing cost of a style guide nobody follows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal that tells you it's time to fix this
&lt;/h2&gt;

&lt;p&gt;If your team has had the same "can we agree on SQL formatting" conversation more than once, that's the signal. A style question that keeps resurfacing despite being nominally settled isn't actually settled, it's just quiet for a while between flare-ups. That pattern is the clearest sign that documentation alone isn't going to solve it, and that the fix has to be structural rather than another round of writing the guide down more clearly.&lt;/p&gt;

</description>
      <category>tools</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Add an Automated SQL Formatting Check to Your CI Pipeline</title>
      <dc:creator>EvvyTools</dc:creator>
      <pubDate>Sat, 04 Jul 2026 09:15:30 +0000</pubDate>
      <link>https://dev.to/evvytools/how-to-add-an-automated-sql-formatting-check-to-your-ci-pipeline-4dlg</link>
      <guid>https://dev.to/evvytools/how-to-add-an-automated-sql-formatting-check-to-your-ci-pipeline-4dlg</guid>
      <description>&lt;p&gt;If your team has agreed on a SQL style but nobody's enforcing it, you'll drift back to inconsistent formatting within a month. New hires don't know the unwritten convention, tired developers on a Friday skip the extra step, and the style guide quietly stops mattering because nothing actually checks it. The fix is to enforce it in CI instead of trusting everyone to remember every time.&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%2Fugckcuvqvprr3a50y6ve.jpeg" 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%2Fugckcuvqvprr3a50y6ve.jpeg" alt="automated pipeline gears and pass-fail checkmarks" width="799" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by Wolfgang Weiser on &lt;a href="https://www.pexels.com" rel="noopener noreferrer"&gt;Pexels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Pick a formatter and lock its config
&lt;/h2&gt;

&lt;p&gt;Whatever formatter your team uses, pin its version and its configuration (keyword case, indent width, comma style) in a config file committed to the repo. If the formatter's defaults can drift between versions, an unpinned dependency will silently reformat your entire codebase on the next &lt;code&gt;npm install&lt;/code&gt; or &lt;code&gt;pip install&lt;/code&gt;, which is its own kind of chaos and defeats the whole point of having a consistent standard in the first place.&lt;/p&gt;

&lt;p&gt;Treat this config file the same way you'd treat an ESLint or Prettier config: check it in, review changes to it deliberately, and don't let it get bumped as a side effect of an unrelated dependency update.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: Add a check script, not a fix script
&lt;/h2&gt;

&lt;p&gt;The CI check should fail the build if formatting doesn't match, not silently reformat and commit on your behalf. A script that runs the formatter against every changed &lt;code&gt;.sql&lt;/code&gt; file, diffs the output against the committed version, and exits non-zero on any difference is enough:&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="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; origin/main &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'*.sql'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;sql-formatter &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/formatted.sql
  diff &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /tmp/formatted.sql &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps formatting as a deliberate developer action (run the formatter locally, commit the result) rather than something CI does silently on your behalf, which avoids surprising force-pushes or unexpected diffs showing up on merge that nobody remembers authorizing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Scope it to changed files only, not the whole repo
&lt;/h2&gt;

&lt;p&gt;Running the check against every SQL file in the repo on every PR will fail on day one if your codebase has years of inconsistently formatted queries sitting in it already. Scope the check to files touched in the current diff so you're only enforcing the new convention going forward, and clean up the historical backlog separately, in its own dedicated PR, on its own schedule, reviewed on its own merits as a pure formatting change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Document the exception path
&lt;/h2&gt;

&lt;p&gt;Some generated SQL (migrations produced by an ORM, vendor-provided scripts, third-party seed data) shouldn't be reformatted, either because it's not really "yours" to restyle or because reformatting it would make future diffs against the upstream source harder to read. Exclude those paths explicitly in the CI config rather than letting developers add ad-hoc &lt;code&gt;# noformat&lt;/code&gt; comments that nobody remembers the exact syntax for six months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Make the fix path obvious in the failure message
&lt;/h2&gt;

&lt;p&gt;A CI failure that just says "formatting check failed" sends people hunting for what to do next, usually into a Slack thread asking someone else how to fix it. Have the script print the exact command to run locally to reformat and pass the check, so the failure resolves in one copy-paste instead of a back-and-forth conversation that costs more time than the original formatting issue would have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Decide what happens on a formatting-only PR
&lt;/h2&gt;

&lt;p&gt;Once the check exists, you'll eventually get a pull request that's purely a reformat with no logic changes, either from the initial rollout or from someone cleaning up a file they happened to be touching anyway. Treat these as their own category: fast to review (diff the AST or the query's semantic content if your tooling supports it, not just the text), but still worth a real look, since a bug in the formatter itself could theoretically alter behavior in an edge case. Don't let "it's just formatting" become an excuse to merge without any review at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Revisit the config occasionally, but rarely
&lt;/h2&gt;

&lt;p&gt;A locked formatting config is a feature, not a limitation, but it shouldn't be permanently frozen either. If the team's needs change (a new database engine gets adopted, a new hire strongly prefers a different comma style and makes a good case for it) revisit the config deliberately, as its own decision, rather than letting it drift through unrelated PRs. The goal is stability with an explicit, rare escape hatch, not an unchangeable rule enforced for its own sake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worth the setup cost
&lt;/h2&gt;

&lt;p&gt;The one-time cost of wiring this up is small compared to the recurring cost of relitigating the same case-and-comma argument every few months, which is what happens by default once a style guide has no enforcement behind it. Once this check exists, new team members follow the convention automatically because the CI system teaches them on their very first PR, rather than a wiki page they may or may not have read closely.&lt;/p&gt;

&lt;p&gt;Once this is running, the case-versus-lowercase, leading-comma-versus-trailing debates mostly disappear, because the CI check makes the team's choice binding instead of aspirational. For background on why formatters disagree on these defaults in the first place, see &lt;a href="https://evvytools.com/blog/why-sql-formatters-disagree-on-keyword-case-and-indent-style/" rel="noopener noreferrer"&gt;this breakdown of SQL formatter keyword case and indent conventions&lt;/a&gt;. If you want to spot-check formatting output manually before wiring up CI, &lt;a href="https://evvytools.com" rel="noopener noreferrer"&gt;EvvyTools&lt;/a&gt; has a browser-based SQL formatter that's a fast way to see the target output for a given query before you commit your team to a specific config.&lt;/p&gt;

&lt;p&gt;For general CI pipeline patterns beyond SQL specifically, &lt;a href="https://docs.github.com/en/actions" rel="noopener noreferrer"&gt;GitHub's Actions documentation&lt;/a&gt; and &lt;a href="https://docs.gitlab.com/ee/ci/" rel="noopener noreferrer"&gt;GitLab's CI/CD documentation&lt;/a&gt; both cover the check-and-fail pattern used here in more general terms, and either is a good reference if you're setting up your very first automated formatting gate. If your stack also includes a general-purpose linter, the &lt;a href="https://eslint.org/docs/latest/" rel="noopener noreferrer"&gt;ESLint documentation&lt;/a&gt; is a useful comparison point for how a mature ecosystem structures the same "check, don't auto-fix in CI" pattern for application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling the rollout without blocking every open PR at once
&lt;/h2&gt;

&lt;p&gt;If you're adding this check to a repository that already has open pull requests in flight, don't flip the CI gate to blocking on day one. Every open PR touching a &lt;code&gt;.sql&lt;/code&gt; file will suddenly fail, not because of anything the author did wrong on that PR specifically, but because the check is now retroactively applying to code written before the standard existed. Run the check in warn-only mode for a week or two first, let people see and fix the failures at their own pace, then switch it to a hard block once the backlog of in-flight PRs has mostly cleared out naturally.&lt;/p&gt;

&lt;p&gt;This staged rollout matters more than it might seem. A hard, blocking gate that lands with zero warning reads as hostile to whoever's PR happens to be open at the time, even though the goal (consistent formatting) is genuinely good for the whole team. A short warning period costs almost nothing and avoids that entirely preventable friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring whether the gate is actually working
&lt;/h2&gt;

&lt;p&gt;After a month or two of the check running, it's worth actually looking at whether it's doing its job. A useful signal: are formatting-only PRs still showing up regularly, or has the codebase stabilized into consistent formatting with the check mostly just confirming what developers are already producing locally before they push? If formatting-only fixup commits keep appearing regularly, that's a sign either the local tooling isn't wired in well enough (developers are relying on CI to catch what a pre-commit hook should catch earlier) or the config needs another look. Either way, the CI check gives you a concrete, measurable signal instead of a vague sense that "formatting seems better now."&lt;/p&gt;

</description>
      <category>tools</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why Joist Spacing Cascades Into Beam and Footing Costs on a Deck Build</title>
      <dc:creator>EvvyTools</dc:creator>
      <pubDate>Fri, 03 Jul 2026 09:57:00 +0000</pubDate>
      <link>https://dev.to/evvytools/why-joist-spacing-cascades-into-beam-and-footing-costs-on-a-deck-build-29ki</link>
      <guid>https://dev.to/evvytools/why-joist-spacing-cascades-into-beam-and-footing-costs-on-a-deck-build-29ki</guid>
      <description>&lt;p&gt;Joist spacing on a deck reads like a small decision. Twelve inches on center, sixteen, twenty-four - pick one, put a check mark next to it on the plan, move on. What most build guides do not explain clearly is that the check mark you put on that line rewrites every other number on the material list. The beam gets bigger or smaller, the footings deepen or shallow, the post count shifts, and the fastener count follows.&lt;/p&gt;

&lt;p&gt;Here is why the cascade happens and why the number is worth thinking about carefully before it locks in.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz1j6bvno6v8t9h1eg6c3.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz1j6bvno6v8t9h1eg6c3.jpeg" alt="deck framing showing beam, joists, and posts before boards go on" width="800" height="1067"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by Muharrem Alper on &lt;a href="https://www.pexels.com" rel="noopener noreferrer"&gt;Pexels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Spacing changes the tributary load per joist
&lt;/h2&gt;

&lt;p&gt;A joist carries the deck load between the two beams (or between the ledger and one beam) it spans. The area of deck load it carries is called its tributary area, and it is the joist span times half the distance to the next joist on each side.&lt;/p&gt;

&lt;p&gt;At 12-inch on center spacing, each joist carries the load from 12 inches of deck floor. At 24-inch spacing, each joist carries 24 inches of floor - twice as much. The joist itself sees more load per foot of length, which is why the span table shortens the maximum span when the spacing widens: a 2x8 might span 12 feet at 16-inch spacing but only 10 feet at 24-inch spacing.&lt;/p&gt;

&lt;p&gt;So changing spacing changes the joist size you need for the same deck length. Widen the spacing, and you might have to upsize from 2x8 to 2x10, which increases the cost per linear foot of joist even though the joist count went down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spacing changes the beam load
&lt;/h2&gt;

&lt;p&gt;The beam under the joists carries the total load from every joist that lands on it. Fewer joists at wider spacing does not mean less load - the total floor area is the same. But the load per contact point on the beam is bigger, which means the beam and its posts have to handle bigger point loads.&lt;/p&gt;

&lt;p&gt;For most residential decks this shows up as either upgrading the beam from a doubled 2x10 to a doubled 2x12, or reducing the maximum span between posts. The &lt;a href="https://www.awc.org/" rel="noopener noreferrer"&gt;American Wood Council&lt;/a&gt; publishes the beam span tables that quantify this.&lt;/p&gt;

&lt;p&gt;The rule of thumb: wider joist spacing means either a beefier beam or more posts. Neither is free. Sometimes both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spacing changes the footing depth and diameter
&lt;/h2&gt;

&lt;p&gt;Footings have to handle the point load from the post above. When joist spacing widens and beam loads concentrate, footings can get bigger too. Local frost line determines minimum depth, but diameter and reinforcement come from load.&lt;/p&gt;

&lt;p&gt;A residential deck with joists at 16-inch spacing might have 12-inch diameter footings. Push to 24-inch spacing on the same footprint and load, and depending on beam layout, footings could go to 14 or 16 inch diameter. That is one bag of concrete more per footing, and if there are eight footings, that math is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spacing changes the fastener count
&lt;/h2&gt;

&lt;p&gt;Fewer joists means fewer joist hangers, fewer joist-to-ledger structural screws, and fewer joist-to-beam nails. Those all get cheaper.&lt;/p&gt;

&lt;p&gt;But fewer joists means more fasteners per joist connecting the boards, because each board has to bear on fewer support points. If the deck was going to have 26 boards at two fasteners per joist per board:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;13 joists (16-inch spacing): 676 board fasteners&lt;/li&gt;
&lt;li&gt;9 joists (24-inch spacing): 468 board fasteners&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The board fastener count drops. But the hanger and structural connection savings from fewer joists partially offset the gain, and if you had to upgrade the joist size to allow the wider spacing, the joist-per-piece cost went up. Connector manufacturers like &lt;a href="https://www.strongtie.com/" rel="noopener noreferrer"&gt;Simpson Strong-Tie&lt;/a&gt; publish fastener specs and load ratings that are worth checking against your specific joist size and connector before you commit to a fastener count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is easy to get wrong on a takeoff
&lt;/h2&gt;

&lt;p&gt;Most manual takeoffs work one line at a time. Someone counts joists based on the spacing that day, then counts boards, then quotes a beam based on a rule of thumb, then quotes footings based on the beam.&lt;/p&gt;

&lt;p&gt;Change spacing at any point and the person doing the takeoff has to re-derive every other line. Miss any of them and the material list disagrees with the plan by one line item that will show up as a shortage or an overrun.&lt;/p&gt;

&lt;p&gt;The fix is to model the cascade once and let the inputs drive it. Spreadsheets work. Calculators built for the purpose work better because they include the span-table lookups and code minimums that a homegrown spreadsheet usually skips.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://evvytools.com/tools/home-real-estate/decking-calculator/" rel="noopener noreferrer"&gt;free decking calculator from EvvyTools&lt;/a&gt; handles the cascade end to end: joists, beams, posts, footings, and fasteners recalculate together when spacing changes, plus a 20-year cost comparison between wood and composite. Running the same deck at 16-inch and again at 24-inch shows the whole downstream difference in one comparison.&lt;/p&gt;

&lt;p&gt;The detailed writeup of how spacing decisions cascade through the plan, when to pick each spacing, and what board manufacturers require lives in the guide on &lt;a href="https://evvytools.com/blog/how-to-choose-deck-joist-spacing-composite-or-wood/" rel="noopener noreferrer"&gt;choosing deck joist spacing for composite or wood boards&lt;/a&gt; if you want the full breakdown.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzymxue4fnthdmg2mrhjd.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzymxue4fnthdmg2mrhjd.jpeg" alt="homeowner comparing deck material takeoffs on paper" width="800" height="1200"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by Sema Bedia Bağçalı on &lt;a href="https://www.pexels.com" rel="noopener noreferrer"&gt;Pexels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers look like in practice
&lt;/h2&gt;

&lt;p&gt;Take a straightforward 12x16 residential deck as a worked example. Same footprint, same live load, same boards, only spacing changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;16-inch spacing: 13 joists, 2x8 joist size, doubled 2x10 beam, 4 posts, 4 footings, roughly 26 boards&lt;/li&gt;
&lt;li&gt;24-inch spacing: 9 joists, 2x10 joist size, doubled 2x10 beam with a possible mid-span brace, 5 posts, 5 footings, roughly 26 boards&lt;/li&gt;
&lt;li&gt;12-inch spacing: 17 joists, 2x8 joist size, doubled 2x10 beam, 4 posts, 4 footings, roughly 26 boards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The board count is unchanged because the deck footprint is unchanged. Everything else shifts. Which of the three is cheapest depends on your local lumber prices and whether the upsize from 2x8 to 2x10 costs more than the four joists saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why estimators default to 16-inch on center
&lt;/h2&gt;

&lt;p&gt;Almost every residential deck estimate you will see starts at 16-inch on center. This is not a code minimum. It is the spacing that works with the widest range of common boards, in the widest range of common patterns, at the widest range of common deck sizes. It is the safe default that fits almost everything without forcing an upsize or a downgrade anywhere in the cascade.&lt;/p&gt;

&lt;p&gt;That is why a contractor who does not know your board choice yet will quote 16-inch. It is not laziness. It is optionality. It leaves the door open for you to change from wood to composite, or from straight-across to diagonal, without redoing the framing plan. Widening to 24-inch closes some of those doors. Tightening to 12-inch is easy to add if the load justifies it later.&lt;/p&gt;

&lt;p&gt;Understanding this is why "why did you pick 16-inch" is often not a useful question to ask a contractor early. The more useful question is "does 16-inch still make sense given the board I picked, or should we reconsider."&lt;/p&gt;

&lt;h2&gt;
  
  
  The framing decision in one sentence
&lt;/h2&gt;

&lt;p&gt;The joist spacing you pick is not just about how far apart the joists sit - it is about the total load path from the boards down through the joists to the beam to the posts to the footings. Every one of those layers gets sized against a load that spacing changes.&lt;/p&gt;

&lt;p&gt;Pick spacing first, run the material list against it, and every downstream number falls out of the calculation instead of getting guessed at line by line. The &lt;a href="https://codes.iccsafe.org/" rel="noopener noreferrer"&gt;International Residential Code&lt;/a&gt; publishes the numeric floor for all of this if you want to sanity-check what your local building department is enforcing.&lt;/p&gt;

&lt;p&gt;For related planning writeups and other DIY estimation walkthroughs, the &lt;a href="https://evvytools.com/blog/" rel="noopener noreferrer"&gt;EvvyTools blog&lt;/a&gt; has a growing set of them.&lt;/p&gt;

&lt;p&gt;Get the cascade right on paper. Frame to the paper. The deck goes up faster and the material list balances at the end.&lt;/p&gt;

</description>
      <category>diy</category>
      <category>homeimprovement</category>
      <category>tools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Build a Deck Material Spreadsheet Before You Order Lumber</title>
      <dc:creator>EvvyTools</dc:creator>
      <pubDate>Fri, 03 Jul 2026 09:55:11 +0000</pubDate>
      <link>https://dev.to/evvytools/how-to-build-a-deck-material-spreadsheet-before-you-order-lumber-18d4</link>
      <guid>https://dev.to/evvytools/how-to-build-a-deck-material-spreadsheet-before-you-order-lumber-18d4</guid>
      <description>&lt;p&gt;The mistake most DIY deck builders make on the material order is treating the joist count, the beam layout, and the board count as three separate numbers to look up in three different places. They are not separate numbers. They are one calculation with joist spacing at the top and everything else cascading from it. A spreadsheet that models the cascade catches the errors that a paper takeoff misses.&lt;/p&gt;

&lt;p&gt;Here is how to build one that actually holds up when the lumber yard calls to confirm the order.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwk1v7n9hmcdb5wm60g24.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwk1v7n9hmcdb5wm60g24.jpeg" alt="notebook with deck material takeoff calculations on a workbench" width="800" height="1200"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="http://www.kaboompics.com" rel="noopener noreferrer"&gt;www.kaboompics.com&lt;/a&gt; on &lt;a href="https://www.pexels.com" rel="noopener noreferrer"&gt;Pexels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Set up the inputs at the top
&lt;/h2&gt;

&lt;p&gt;The spreadsheet starts with the values that drive everything else. Give these their own cells so the rest of the sheet reads from them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deck length (feet)&lt;/li&gt;
&lt;li&gt;Deck width (feet)&lt;/li&gt;
&lt;li&gt;Joist spacing (inches on center) - typically 12, 16, or 24&lt;/li&gt;
&lt;li&gt;Joist orientation (which direction the joists run)&lt;/li&gt;
&lt;li&gt;Board type (pressure treated wood, cedar, composite)&lt;/li&gt;
&lt;li&gt;Board width (5/4-inch wood, 1-inch composite, 2x6 solid)&lt;/li&gt;
&lt;li&gt;Board pattern (straight or diagonal)&lt;/li&gt;
&lt;li&gt;Live load (pounds per square foot, typically 40)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every cell below reads from these. Change joist spacing from 16 to 12 in one place and the whole material list updates.&lt;/p&gt;
&lt;h2&gt;
  
  
  Calculate joist count
&lt;/h2&gt;

&lt;p&gt;Joists run perpendicular to the deck boards. If the boards run the long way (typical), the joists run the short way. The count is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;joist_count = ceil(long_dimension_inches / joist_spacing_inches) + 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a 12x16 deck with joists running the 12-foot direction and 16-inch on-center spacing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;joist_count = ceil(192 / 16) + 1 = 12 + 1 = 13
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plus one is for the joist at the starting edge - you need a joist at both ends. Forget it and you are one joist short on install day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculate joist length
&lt;/h2&gt;

&lt;p&gt;Joist length is the short dimension of the deck (assuming boards run the long way), plus a small allowance for how the joists connect to the ledger and beam. Most yards sell in 2-foot increments (10, 12, 14, 16). Round up to the next available length.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;joist_length_needed = short_dimension_ft + 0.5   # feet
joist_length_ordered = ceil(joist_length_needed / 2) * 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Calculate joist size
&lt;/h2&gt;

&lt;p&gt;Joist size depends on span and spacing. The &lt;a href="https://www.awc.org/" rel="noopener noreferrer"&gt;American Wood Council&lt;/a&gt; publishes the residential span tables that all this comes from. For pressure-treated southern pine at 40 pounds per square foot live load:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2x6: span up to about 9 feet 6 inches at 16-inch on center&lt;/li&gt;
&lt;li&gt;2x8: span up to about 12 feet 6 inches&lt;/li&gt;
&lt;li&gt;2x10: span up to about 15 feet&lt;/li&gt;
&lt;li&gt;2x12: span up to about 17 feet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put the span table in a lookup range on the sheet. A VLOOKUP or INDEX/MATCH keyed off joist span and spacing picks the right size automatically. The advantage of a lookup over hard-coded values: change deck size or spacing and the joist size updates without you re-checking the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculate board count
&lt;/h2&gt;

&lt;p&gt;Boards run the long direction and land on joists. For 5/4-inch or 1-inch nominal boards (about 5.5 inches actual width), including a small gap between boards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;board_count = ceil(short_dimension_inches / (board_actual_width + gap_inches))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For 12-foot-wide deck with 5.5-inch actual-width boards and a 1/8-inch gap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;board_count = ceil(144 / 5.625) = 26 boards
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add 5 to 10 percent for cuts, waste, and defect selection. Boards do not come clean of knots and splits, and you will discard a few.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculate beam size and footing count
&lt;/h2&gt;

&lt;p&gt;Beams support the joists. Beam size depends on the tributary area of the joists that land on it, which comes from spacing and joist span. For most residential decks under 16 feet on the joist side, a doubled 2x10 or a doubled 2x12 beam works, with beam posts on 6- to 8-foot centers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;post_count_along_beam = ceil(beam_length_ft / max_post_span_ft) + 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Footing count matches post count. On most residential decks the footing depth depends on your local frost line, which is a value your building department publishes. The &lt;a href="https://planthardiness.ars.usda.gov/" rel="noopener noreferrer"&gt;USDA plant hardiness zone map&lt;/a&gt; is a rough proxy if you cannot get the exact frost-line depth from the town.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculate fasteners
&lt;/h2&gt;

&lt;p&gt;Fasteners scale with joist count and board count:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structural screws or nails for joist-to-beam and joist-to-ledger: about 4 per joist connection&lt;/li&gt;
&lt;li&gt;Board fasteners: typically 2 fasteners per joist per board (or the equivalent hidden clip count for composite)&lt;/li&gt;
&lt;li&gt;Hangers: one hanger per joist per connection point (typically one at the ledger)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hanger_count = joist_count
board_fastener_count = board_count * joist_count * 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Buy 10 percent extra fasteners. They are the cheapest line item on the list and running out mid-install is a Saturday killer. If you are picking specific hangers and structural screws, &lt;a href="https://www.strongtie.com/" rel="noopener noreferrer"&gt;Simpson Strong-Tie&lt;/a&gt; publishes maps from board manufacturer to compatible connector - worth checking before you order.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7p52we2k2wemaz8vvoov.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7p52we2k2wemaz8vvoov.jpeg" alt="lumber stacked on a driveway for a delivery" width="799" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by Mark Stebnicki on &lt;a href="https://www.pexels.com" rel="noopener noreferrer"&gt;Pexels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a running cost column
&lt;/h2&gt;

&lt;p&gt;Column A the item, column B the count, column C the unit cost, column D the extended cost. Look up unit costs from your local yard - the delivery price desk will quote per-piece rates for pressure-treated, composite, cedar, or whatever you are ordering. Adding an "as of date" cell in the corner reminds you when prices need to be refreshed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the 20-year check
&lt;/h2&gt;

&lt;p&gt;Composite boards cost 2 to 4 times more than pressure-treated wood upfront. They also do not need to be re-stained every few years. A row that adds staining supplies and labor for wood every 3 years for 20 years - or the equivalent replacement cost of a wood deck at year 15 versus composite lasting the full 20 - is what makes the composite-vs-wood cost conversation honest.&lt;/p&gt;

&lt;p&gt;Skipping this row is why "wood is cheaper" is only true for the first estimate and rarely true for the life of the deck. The specifics vary by climate, so if you want a reality check on how quickly untreated southern pine actually degrades in your area, &lt;a href="https://www.fs.usda.gov/" rel="noopener noreferrer"&gt;the US Forest Service&lt;/a&gt; publishes durability studies that hold up well over multi-decade windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-check against a calculator
&lt;/h2&gt;

&lt;p&gt;Once the spreadsheet is done, run the same inputs through the &lt;a href="https://evvytools.com/" rel="noopener noreferrer"&gt;EvvyTools&lt;/a&gt; Decking Calculator and compare. If the joist count matches, the board count matches within 5 percent, and the beam size matches, your spreadsheet math is good. If not, one of the two has a bug worth finding before the order goes in. The full explanation of how joist spacing changes the cascade is written up in the &lt;a href="https://evvytools.com/blog/how-to-choose-deck-joist-spacing-composite-or-wood/" rel="noopener noreferrer"&gt;guide on choosing deck joist spacing for composite or wood boards&lt;/a&gt; and the code reference is &lt;a href="https://codes.iccsafe.org/" rel="noopener noreferrer"&gt;the International Residential Code&lt;/a&gt; at ICC.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the spreadsheet catches that paper does not
&lt;/h2&gt;

&lt;p&gt;The pattern of DIY deck material errors is almost never one big mistake. It is usually a small change that did not propagate. You went from 16-inch spacing to 12-inch because the boards changed, but you forgot to recalculate the joist count. You changed the board type from wood to composite, but you did not update the beam size or the fastener count. A spreadsheet with all the inputs at the top makes those cascades automatic.&lt;/p&gt;

&lt;p&gt;Anyone comfortable with a spreadsheet can build one in an evening. The reward is a delivery slip and a framing plan that agree - which is the whole ballgame on a DIY deck.&lt;/p&gt;

&lt;p&gt;More project-planning writeups with the same pattern live at the &lt;a href="https://evvytools.com/blog/" rel="noopener noreferrer"&gt;EvvyTools blog&lt;/a&gt;, and other calculators for related DIY estimates are indexed at the &lt;a href="https://evvytools.com/tools/" rel="noopener noreferrer"&gt;EvvyTools tools directory&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>diy</category>
      <category>homeimprovement</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
    <item>
      <title>Six Free Running Calculators That Plan a Race Better Than a Spreadsheet</title>
      <dc:creator>EvvyTools</dc:creator>
      <pubDate>Thu, 02 Jul 2026 13:08:03 +0000</pubDate>
      <link>https://dev.to/evvytools/six-free-running-calculators-that-plan-a-race-better-than-a-spreadsheet-4131</link>
      <guid>https://dev.to/evvytools/six-free-running-calculators-that-plan-a-race-better-than-a-spreadsheet-4131</guid>
      <description>&lt;p&gt;Runners who plan races in spreadsheets end up in one of two places. Either the spreadsheet is a personal masterpiece that took a year to build, or it is a broken formula they copied from a forum in 2019 that quietly rounds the wrong way. Free specialized calculators do the same math faster and with fewer subtle errors. Here are six that cover the full training cycle from base mileage through race day, and none of them require an account or a subscription.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwrcvoz5hadgq2rp6a3oq.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwrcvoz5hadgq2rp6a3oq.jpeg" alt="A runner reviewing pace and split data on a phone at a track" width="799" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by RUN 4 FFWPU on &lt;a href="https://www.pexels.com" rel="noopener noreferrer"&gt;Pexels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Riegel formula race predictor
&lt;/h2&gt;

&lt;p&gt;The single most cited race prediction is the &lt;a href="https://en.wikipedia.org/wiki/Peter_Riegel" rel="noopener noreferrer"&gt;Riegel formula&lt;/a&gt;, which projects a longer race time from a shorter one using a power function. Any calculator that runs this formula honestly is useful. The version at &lt;a href="https://evvytools.com/tools/health-fitness/pace-calculator/" rel="noopener noreferrer"&gt;EvvyTools&lt;/a&gt; exposes the exponent so you can adjust for a less trained aerobic base, which most default calculators do not do.&lt;/p&gt;

&lt;p&gt;If your only tool is the default Riegel exponent, the marathon projection from a fast half PR will be optimistic by 8 to 15 minutes for a first-time marathoner. The ability to nudge the exponent from 1.06 to 1.08 or 1.10 is what makes the number honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Mile split table generator
&lt;/h2&gt;

&lt;p&gt;A split table is a mile-by-mile plan for a race. Generating one by hand from an average pace works, but it does not handle the conservation of the first 5K, the maintenance of miles 4-16, or the honest miles 17-20. A tool that produces the full table for a specific goal time, in your chosen units, and lets you export it to a wristband-sized print is worth more than the 30 minutes of arithmetic it saves.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://evvytools.com/tools/health-fitness/pace-calculator/" rel="noopener noreferrer"&gt;EvvyTools&lt;/a&gt; split output is one option. Another is the built-in split table view on the &lt;a href="https://www.runnersworld.com/" rel="noopener noreferrer"&gt;Runner's World&lt;/a&gt; site, which has the advantage of years of iteration but the disadvantage of requiring a slightly slower workflow to print. Either works for a first marathon.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pace conversion between units
&lt;/h2&gt;

&lt;p&gt;If you train in kilometers and race in a mile-based country, or vice versa, you need a fast conversion tool. Not because the math is hard, but because eyeballing 5:00 per kilometer as 8:03 per mile at 6 AM on race day is where errors happen.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://en.wikipedia.org/wiki/Pace_%28speed%29" rel="noopener noreferrer"&gt;Wikipedia entry on pace&lt;/a&gt; has the underlying formulas. Any calculator that does both directions cleanly is fine. The one at &lt;a href="https://evvytools.com/tools/" rel="noopener noreferrer"&gt;EvvyTools&lt;/a&gt; supports both units in the same interface and shows the split table in whichever unit you chose, which is what actually matters at the starting line.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Race pace by heart rate zone
&lt;/h2&gt;

&lt;p&gt;For runners training with heart rate zones (the classic Zone 2 through Zone 5 model), a calculator that translates race distance into a target zone is useful for calibrating effort in training. The reference on the underlying model is in the &lt;a href="https://en.wikipedia.org/wiki/VO2_max" rel="noopener noreferrer"&gt;Wikipedia entry on VO2 max&lt;/a&gt; and the derivative Karvonen heart rate reserve formula. A marathon is typically raced at the top of Zone 3 or the bottom of Zone 4 for most runners, which is significantly below the pace runners naturally drift toward when they feel fresh.&lt;/p&gt;

&lt;p&gt;Any calculator that produces target HR ranges from a max HR input is useful for verifying that your goal race pace lines up with your aerobic ceiling. If the target HR for your goal pace is above your lactate threshold, the goal pace is not physiologically sustainable and needs to come down.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Cumulative training load and taper predictor
&lt;/h2&gt;

&lt;p&gt;Not every training decision is about race day. The intermediate decisions in weeks 4 through 8 of a training block, when mileage should peak and taper should begin, benefit from a tool that shows the aggregate load curve. Free training load calculators, of which several exist in the community, help visualize when to back off and when to push.&lt;/p&gt;

&lt;p&gt;The exact calculator matters less than the discipline of updating it weekly. A number that gets checked once produces the same insight as no calculator at all. A number that gets updated every Sunday produces the taper timing that most self-coached runners get wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Race day pace band generator
&lt;/h2&gt;

&lt;p&gt;The last calculator in this list is the smallest and most useful. A pace band is a paper wristband with target elapsed times printed for each mile marker. On race day, you glance at your watch at each mile, compare to the band, and know instantly whether you are on plan.&lt;/p&gt;

&lt;p&gt;Most runners produce these ad-hoc from a spreadsheet. A calculator that outputs a print-ready pace band, sized for your goal time and adjusted for course profile if the tool supports it, is a small piece of paper that makes a large tactical difference. The &lt;a href="https://evvytools.com/tools/" rel="noopener noreferrer"&gt;tools directory at EvvyTools&lt;/a&gt; is worth browsing for the exact outputs you need for a specific race.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1gjjyi4x51znoq872dt0.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1gjjyi4x51znoq872dt0.jpeg" alt="A runner's pace band and race number laid out with a watch and shoes" width="800" height="946"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by Jean Marc Bonnel on &lt;a href="https://www.pexels.com" rel="noopener noreferrer"&gt;Pexels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern across all six
&lt;/h2&gt;

&lt;p&gt;The six tools have one thing in common. Each one replaces a spreadsheet formula with a purpose-built interface that handles the common edge cases (rounding, unit conversion, elevation adjustment) without asking the runner to think about them. The runner spends the training block on running, not on debugging their calculator.&lt;/p&gt;

&lt;p&gt;None of these tools is complicated. The distinction is that they are set up correctly out of the box. A spreadsheet you built in 2019 has whatever assumptions you made in 2019, whether or not you remember them. A tool from a maintained source has current assumptions and gets corrected when the underlying formulas evolve.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://evvytools.com/blog/pace-first-marathon-without-blowing-up-second-half/" rel="noopener noreferrer"&gt;companion guide on marathon pacing&lt;/a&gt; explains how to translate these calculator outputs into an actual race plan, and the general &lt;a href="https://evvytools.com/blog/" rel="noopener noreferrer"&gt;EvvyTools blog&lt;/a&gt; covers the training-side questions that determine whether the race plan is defensible in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why free calculators beat paid apps for most runners
&lt;/h2&gt;

&lt;p&gt;The paid running apps that dominate the market bundle calculators with training plan generation, workout logging, and social features. For runners who want all of that in one place, they are worth the money. For runners who already have a training plan and just need pace math on race week, they are overkill.&lt;/p&gt;

&lt;p&gt;A dedicated free calculator opens in 200 milliseconds, produces the output you need, and gets out of the way. It does not require an account, does not push notifications, and does not ask for your credit card. The tradeoff is that it does not know anything about your training history. If you have that history in a separate log (a paper training journal, a spreadsheet, or an app you already use for logging), the calculator on race week just needs to do the arithmetic.&lt;/p&gt;

&lt;p&gt;For most self-coached runners, the split is: use whatever you already use for logging your training, and use a fast standalone calculator for race week pace math. Merging both into one paid tool is a preference, not a requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to test in the last two weeks
&lt;/h2&gt;

&lt;p&gt;Two weeks before your marathon, run a final calibration of every tool you plan to use on race day. Cross-check the split table output against your goal time. Confirm the pace units match what your watch will report. Print the pace band if you plan to wear one, and verify the times are legible. This is a 15-minute exercise that catches misconfigured settings before race morning, which is not the time to discover them.&lt;/p&gt;

&lt;p&gt;The final tactical rule is worth repeating: use two independent calculators and cross-check the results. If two tools produce the same split table, you have a plan. If they disagree by more than a few seconds a mile, one of them is misconfigured and it is worth finding out which before race day, not after.&lt;/p&gt;

</description>
      <category>fitness</category>
      <category>tools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Build a Race Split Table for Your First Marathon</title>
      <dc:creator>EvvyTools</dc:creator>
      <pubDate>Thu, 02 Jul 2026 13:08:00 +0000</pubDate>
      <link>https://dev.to/evvytools/how-to-build-a-race-split-table-for-your-first-marathon-52fm</link>
      <guid>https://dev.to/evvytools/how-to-build-a-race-split-table-for-your-first-marathon-52fm</guid>
      <description>&lt;p&gt;A split table is a mile-by-mile plan for how fast you intend to run each mile of a race, adjusted for course profile, weather, and the tactical realities of running in a crowd. Elite runners have coaches who produce these tables for them. Everybody else usually writes their goal pace on a wristband and hopes for the best. This is a walkthrough of how to build a real split table, in about 30 minutes, for a first marathon.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq0ltjdvhefp229ztz1nk.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq0ltjdvhefp229ztz1nk.jpeg" alt="A runner reviewing training data and race notes at a desk" width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by Vijay  Saiwal on &lt;a href="https://www.pexels.com" rel="noopener noreferrer"&gt;Pexels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Pick a goal pace your training actually supports
&lt;/h2&gt;

&lt;p&gt;The starting point is not aspirational. It is your training data. Use the pace from the last four miles of your best 20-mile long run as the anchor. Subtract roughly 20 to 40 seconds per mile to estimate marathon pace. That estimate is your ceiling.&lt;/p&gt;

&lt;p&gt;If your best 20-mile long run finished at 9:45 per mile, a defensible marathon goal is somewhere around 9:15 to 9:25 per mile. That produces a 4:02 to 4:07 marathon. Anything faster is aspirational and the split table should be built for the honest number.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: Compute average pace for the goal
&lt;/h2&gt;

&lt;p&gt;Average pace equals goal time divided by 26.2 miles. Do the arithmetic yourself even if the tool does it for you, because errors in this step compound across the whole race.&lt;/p&gt;

&lt;p&gt;For a 4:00 goal: 240 minutes divided by 26.2 miles equals roughly 9.16 minutes per mile, which is 9:09.5 per mile. Round to 9:10 for the plan, or 9:09 if you want a small buffer. The &lt;a href="https://evvytools.com/tools/health-fitness/pace-calculator/" rel="noopener noreferrer"&gt;free race pace calculator from EvvyTools&lt;/a&gt; produces this to two decimal places and shows the exact per-mile total.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3: Build the miles 1-3 conservation
&lt;/h2&gt;

&lt;p&gt;Race adrenaline and crowd energy will make you feel faster than you are. Your first 3 miles should be planned at 10 to 15 seconds per mile slower than goal pace. For a 4:00 goal at 9:10 average, the first 3 miles are 9:20 to 9:25.&lt;/p&gt;

&lt;p&gt;The split table for miles 1 through 3 looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mile 1: 9:22 (elapsed 9:22)
Mile 2: 9:22 (elapsed 18:44)
Mile 3: 9:20 (elapsed 28:04)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not banking time later. It is refusing to spend energy that you will need in the second half.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Fill in miles 4-16 at goal pace
&lt;/h2&gt;

&lt;p&gt;Once you have thinned out from the starting crowd, settle into goal pace. This is the aerobic middle of the race, and it should feel repeatable and steady.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Miles 4-16: 9:10 per mile
Elapsed at mile 16: roughly 2:26:14
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you feel great in this section, ignore it and hold pace. If you feel bad, ignore it and hold pace. Neither feeling is a reliable signal for the second half. The &lt;a href="https://en.wikipedia.org/wiki/Marathon" rel="noopener noreferrer"&gt;Wikipedia entry on the marathon&lt;/a&gt; explains the physiology of why the middle miles are misleading. The lesson is to trust the plan more than the legs in this window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Fill in miles 17-20, the honest miles
&lt;/h2&gt;

&lt;p&gt;This is where the plan continues at goal pace, but effort will noticeably rise. You are approaching the wall in glycogen terms and your form will start compensating.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Miles 17-20: 9:10-9:15 per mile
Elapsed at mile 20: roughly 3:03:04
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have to slow slightly to 9:15 for a mile or two to stay controlled, that is fine. The goal is to arrive at mile 20 with enough left to hold the last 10K without a collapse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Plan the final 10K by contingency, not by wishful thinking
&lt;/h2&gt;

&lt;p&gt;The last 10K is where positive splits come from. Do not plan a fixed pace here. Plan two variants:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variant A (if you feel controlled at mile 20):&lt;/strong&gt; Hold 9:10 to 9:15 pace for miles 21-26.2. Small negative split possible if the last 5K feels manageable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variant B (if effort has climbed sharply by mile 20):&lt;/strong&gt; Slow to 9:20-9:25 for miles 21-24, then reassess. Do not force a pace that has stopped being sustainable. Losing 30 seconds a mile is a small cost. Losing 90 seconds a mile because you tried to force 9:10 and blew up is a large one.&lt;/p&gt;

&lt;p&gt;The runner who plans for both variants finishes near their goal. The runner who plans only for variant A and is forced into variant B at mile 22 usually finishes 15 minutes slower than they should have, because they did not switch modes fast enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Apply weather adjustments
&lt;/h2&gt;

&lt;p&gt;Every 5 degrees Fahrenheit above 55 costs roughly 4 to 10 seconds per mile at marathon pace. Humidity above 70 percent compounds. If race-day forecast is 65 degrees and 75 percent humidity, revise the goal pace by 10 to 20 seconds per mile before the race. Do not force the pre-planned pace into an environment it was not built for. &lt;a href="https://www.runnersworld.com/" rel="noopener noreferrer"&gt;Runner's World&lt;/a&gt; publishes standardized heat adjustment tables, and the &lt;a href="https://en.wikipedia.org/wiki/Jack_Daniels_%28coach%29" rel="noopener noreferrer"&gt;Jack Daniels&lt;/a&gt; coaching formulas are worth reading before you finalize.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Apply course elevation adjustments
&lt;/h2&gt;

&lt;p&gt;Flat course: no adjustment. Downhill start (Boston, New York): expect the first 5 miles to feel deceptively easy and hold conservative pace anyway. Rolling hills: mentally mark the uphill miles as 10 to 20 seconds slower and downhills as 5 to 10 seconds faster. Hilly finish (Newton hills): reserve 30 to 60 seconds of buffer specifically for those miles.&lt;/p&gt;

&lt;p&gt;Draw the elevation profile once and annotate the split table with which miles are the hardest. This is the difference between a plan built for the race in front of you and a plan built for a flat treadmill run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Print two copies
&lt;/h2&gt;

&lt;p&gt;One goes on your race bib or wristband. One goes to your support crew if you have one. Both should include the goal times at miles 5, 10, 13.1, 20, and 26.2, so you can check yourself at each mile marker and adjust in real time if you are running consistently ahead of plan (slow down) or consistently behind plan (either accept the new pace or diagnose why).&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F64wg5y4xxw4008ig4sje.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F64wg5y4xxw4008ig4sje.jpeg" alt="Split table and elevation profile printed on paper next to running gear" width="800" height="1200"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by Aedrian Salazar on &lt;a href="https://www.pexels.com" rel="noopener noreferrer"&gt;Pexels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The check most first-timers skip
&lt;/h2&gt;

&lt;p&gt;Before you leave for the race, verify the total of the split table equals your goal time exactly. If your table adds up to 3:58:47 for a 4:00 goal, either your average pace is slightly too fast, or you have a rounding error. Fix it before race day.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://evvytools.com/blog/pace-first-marathon-without-blowing-up-second-half/" rel="noopener noreferrer"&gt;companion guide on pacing a first marathon&lt;/a&gt; walks through how to translate this table into actual pacing discipline during the race, and the &lt;a href="https://evvytools.com/tools/" rel="noopener noreferrer"&gt;EvvyTools tools directory&lt;/a&gt; has the calculators for splits, race prediction, and unit conversion in one place. The most important thing about a split table is that you built one. The second most important thing is that you trust it more than your legs in the first hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake most runners make with the table on race day
&lt;/h2&gt;

&lt;p&gt;The single most common failure mode of a split table is not that it was built badly. It is that the runner threw it away by mile 5 because the pace felt too slow. The plan on paper says 9:22 for miles 1 through 3, and the crowd is running 8:50, and the runner decides the plan was too conservative and speeds up. This is the exact mistake the table was built to prevent.&lt;/p&gt;

&lt;p&gt;The mental discipline is to accept that the first 5K is going to feel slow to easy, and that this feeling is the plan working correctly. If your first mile felt exhausting at goal pace, either the goal is too fast or you did not warm up properly. If the first mile felt easy, that is what the plan intended. Speeding up because it felt easy is how you turn a 4:00 finish into a 4:18 finish. The runners who trust the table finish near goal. The runners who abandon it usually miss by 10 to 20 minutes and blame the training rather than the pacing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do if you fall off pace by mile 15
&lt;/h2&gt;

&lt;p&gt;If by mile 15 you are consistently 15 to 30 seconds per mile behind the split table, do not try to make up the time in miles 16 to 20. That is the exact strategy that produces the wall. The right move is to accept a slightly slower finish, hold the current pace steady, and let the final 10K unfold at whatever pace stays sustainable. A 4:07 finish with a controlled last 10K feels vastly better than a 4:15 finish that included a 20-minute walk. Your body will thank you the next morning as well.&lt;/p&gt;

&lt;p&gt;The corollary is that a split table is not sacred. It is a starting hypothesis. The mid-race version of the plan is to hold whatever pace is honest for the next 10K, using the table as a benchmark rather than an obligation. That is the mature use of the tool, and it separates first marathoners who finish strong from those who suffer.&lt;/p&gt;

</description>
      <category>fitness</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
  </channel>
</rss>
