<?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: Swift Labs</title>
    <description>The latest articles on DEV Community by Swift Labs (@swiftlabs).</description>
    <link>https://dev.to/swiftlabs</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%2F4193533%2F05260f6e-c396-4316-98c5-3d82fee02472.png</url>
      <title>DEV Community: Swift Labs</title>
      <link>https://dev.to/swiftlabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/swiftlabs"/>
    <language>en</language>
    <item>
      <title>gpt-4o-mini-tts replacement: what to use after OpenAI shuts it down on Jan 6, 2027</title>
      <dc:creator>Swift Labs</dc:creator>
      <pubDate>Mon, 09 Nov 2026 15:15:04 +0000</pubDate>
      <link>https://dev.to/swiftlabs/gpt-4o-mini-tts-replacement-what-to-use-after-openai-shuts-it-down-on-jan-6-2027-18bf</link>
      <guid>https://dev.to/swiftlabs/gpt-4o-mini-tts-replacement-what-to-use-after-openai-shuts-it-down-on-jan-6-2027-18bf</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://tts.deadlinkpatrol.com/guides/gpt-4o-mini-tts-replacement/" rel="noopener noreferrer"&gt;tts.deadlinkpatrol.com&lt;/a&gt;. Facts checked November 9, 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;From Swift Labs, makers of TTS Swap. Written with AI help and checked against OpenAI's docs, OpenAI's community forum and our own service on November 9, 2026. Not affiliated with or endorsed by OpenAI.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; OpenAI shuts down &lt;code&gt;gpt-4o-mini-tts&lt;/code&gt; on &lt;strong&gt;January 6, 2027&lt;/strong&gt;, along with &lt;code&gt;tts-1&lt;/code&gt; and &lt;code&gt;tts-1-hd&lt;/code&gt;. The replacement OpenAI lists is &lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt;, which runs on the Realtime API, not on &lt;code&gt;/v1/audio/speech&lt;/code&gt;. If your code only needs text in and audio out, an OpenAI-compatible endpoint keeps your &lt;code&gt;audio.speech.create&lt;/code&gt; call running. TTS Swap does that, but it does &lt;strong&gt;not&lt;/strong&gt; apply the &lt;code&gt;instructions&lt;/code&gt; parameter, so tone and style prompts have no effect there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OpenAI says
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI's &lt;a href="https://platform.openai.com/docs/deprecations" rel="noopener noreferrer"&gt;deprecations page&lt;/a&gt; (entry "2026-10-01: Text-to-speech models") lists &lt;code&gt;gpt-4o-mini-tts&lt;/code&gt; with a shutdown date of Jan 6, 2027 and &lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt; as the recommended replacement.&lt;/li&gt;
&lt;li&gt;OpenAI's &lt;a href="https://developers.openai.com/api/docs/guides/text-to-speech" rel="noopener noreferrer"&gt;text-to-speech guide&lt;/a&gt; says this is a change of API workflow: &lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt; isn't a supported model for &lt;code&gt;/v1/audio/speech&lt;/code&gt;. The guide's &lt;code&gt;gpt-4o-mini-tts&lt;/code&gt; examples pass an &lt;code&gt;instructions&lt;/code&gt; string such as "Speak in a cheerful and positive tone."&lt;/li&gt;
&lt;li&gt;On OpenAI's &lt;a href="https://community.openai.com/t/tts-shutdown-email-tts-1-gpt-4o-mini-tts-jan-6-2027-isnt-on-the-deprecations-page/1402731" rel="noopener noreferrer"&gt;community forum&lt;/a&gt;, OpenAI Support wrote on October 6 that migration means moving off &lt;code&gt;/v1/audio/speech&lt;/code&gt;, and that you can set &lt;code&gt;session.instructions&lt;/code&gt; or &lt;code&gt;response.instructions&lt;/code&gt; on the Realtime API. Support also wrote on October 8 that the retirement notice doesn't name a drop-in replacement for &lt;code&gt;/v1/audio/speech&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The same thread has users reporting that the Realtime path turns a few lines of code into far more, and that they want plain text in, speech out. Support said &lt;code&gt;gpt-audio-1.5&lt;/code&gt; through chat completions generates a response to a prompt and isn't guaranteed to read text verbatim.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you can't just change &lt;code&gt;gpt-4o-mini-tts&lt;/code&gt; to &lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt; in &lt;code&gt;audio.speech.create&lt;/code&gt;. That call isn't supported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find where you use it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"gpt-4o-mini-tts|instructions="&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.js"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.ts"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.env*"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check config files and environment variables too. The model name is often stored outside the code, and &lt;code&gt;instructions&lt;/code&gt; is easy to miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your options
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Keeps &lt;code&gt;/v1/audio/speech&lt;/code&gt; code&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;instructions&lt;/code&gt; (tone, style)&lt;/th&gt;
&lt;th&gt;Work involved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Realtime API (&lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;session.instructions&lt;/code&gt; / &lt;code&gt;response.instructions&lt;/code&gt;; test that it reads your text exactly&lt;/td&gt;
&lt;td&gt;Rewrite to sessions and streamed events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Another speech provider with its own API&lt;/td&gt;
&lt;td&gt;Usually no&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;td&gt;New client code, new voices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An OpenAI-compatible speech server (hosted or self-hosted)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Varies by server; check&lt;/td&gt;
&lt;td&gt;Change the base URL and API key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTS Swap&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not applied&lt;/td&gt;
&lt;td&gt;Change the base URL and API key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;If you rely on &lt;code&gt;instructions&lt;/code&gt;&lt;/strong&gt; to set tone, accent or pacing, TTS Swap is not a match. Compare the Realtime API and other providers against your current output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you pass &lt;code&gt;instructions&lt;/code&gt; but could live without it&lt;/strong&gt; (internal tools, prototypes, accessibility read-aloud, batch narration), an OpenAI-compatible endpoint is the least work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using TTS Swap with gpt-4o-mini-tts code
&lt;/h2&gt;

&lt;p&gt;TTS Swap answers the same &lt;code&gt;/v1/audio/speech&lt;/code&gt; request. Change the base URL and API key; your calls can keep &lt;code&gt;model="gpt-4o-mini-tts"&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://tts.deadlinkpatrol.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-tts-…&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;speech&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini-tts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alloy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_to_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be clear on what you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;instructions&lt;/code&gt; is not applied.&lt;/strong&gt; Our service doesn't read that field. The request is not rejected: &lt;code&gt;instructions&lt;/code&gt; is accepted but ignored, so it won't steer tone or style.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The voice won't sound like OpenAI's.&lt;/strong&gt; Audio comes from the open-source MeloTTS model, with one voice per language. Every voice name gets that same voice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WAV comes back&lt;/strong&gt; even when you ask for mp3 (the default). &lt;code&gt;pcm&lt;/code&gt; gives raw 24 kHz 16-bit mono. opus, aac and flac return a 400 error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;speed&lt;/code&gt;&lt;/strong&gt; is accepted but not applied yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Languages:&lt;/strong&gt; English by default; es, fr, zh, ja or ko via &lt;code&gt;extra_body={"language":"es"}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limits:&lt;/strong&gt; up to 4,096 characters per request. Paid keys are $19/month for 1,000,000 characters a month, 20 requests per minute per key. A shared daily capacity cap applies to the whole service. Over a limit you get an OpenAI-style 429.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free key:&lt;/strong&gt; 10,000 characters a month, 5 requests per minute, email only, no card. Free keys share a daily capacity limit, so on busy days they may get a 429 until 00:00 UTC.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Listen to the &lt;a href="https://tts.deadlinkpatrol.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=gpt-4o-mini-tts-replacement" rel="noopener noreferrer"&gt;unedited samples on the home page&lt;/a&gt; before you decide.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When does gpt-4o-mini-tts stop working?&lt;/strong&gt;&lt;br&gt;
January 6, 2027, according to OpenAI's deprecations page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I just change &lt;code&gt;gpt-4o-mini-tts&lt;/code&gt; to &lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
No. OpenAI's guide says &lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt; isn't supported on &lt;code&gt;/v1/audio/speech&lt;/code&gt;. It needs the Realtime API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does TTS Swap support the &lt;code&gt;instructions&lt;/code&gt; parameter?&lt;/strong&gt;&lt;br&gt;
No. The field is ignored, so the output doesn't change with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I move back later?&lt;/strong&gt;&lt;br&gt;
Before January 6, 2027, yes: change the base URL and API key back to OpenAI's and keep using &lt;code&gt;gpt-4o-mini-tts&lt;/code&gt;. After that date, &lt;code&gt;gpt-4o-mini-tts&lt;/code&gt; is gone, so moving back means a supported OpenAI model and API workflow, or another compatible provider with that provider's matching key. Always change the URL and key together; a URL without its matching key fails with a 401.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://tts.deadlinkpatrol.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=gpt-4o-mini-tts-replacement" rel="noopener noreferrer"&gt;Get a free key&lt;/a&gt; or a paid key ($19/month) on the home page.&lt;/li&gt;
&lt;li&gt;Copy-paste snippets: the &lt;a href="https://tts.deadlinkpatrol.com/migrate/gpt-4o-mini-tts?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=gpt-4o-mini-tts-replacement" rel="noopener noreferrer"&gt;gpt-4o-mini-tts migration guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Keeping the rest of your setup safe: &lt;a href="https://tts.deadlinkpatrol.com/guides/openai-base-url-tts/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=gpt-4o-mini-tts-replacement" rel="noopener noreferrer"&gt;Using OPENAI_BASE_URL to move text-to-speech off tts-1&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>openai</category>
      <category>tts</category>
      <category>api</category>
      <category>python</category>
    </item>
    <item>
      <title>tts-1-hd replacement: what to use after OpenAI shuts it down on Jan 6, 2027</title>
      <dc:creator>Swift Labs</dc:creator>
      <pubDate>Mon, 09 Nov 2026 13:21:58 +0000</pubDate>
      <link>https://dev.to/swiftlabs/tts-1-hd-replacement-what-to-use-after-openai-shuts-it-down-on-jan-6-2027-4k4d</link>
      <guid>https://dev.to/swiftlabs/tts-1-hd-replacement-what-to-use-after-openai-shuts-it-down-on-jan-6-2027-4k4d</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://tts.deadlinkpatrol.com/guides/tts-1-hd-replacement/" rel="noopener noreferrer"&gt;tts.deadlinkpatrol.com&lt;/a&gt;. Facts checked November 8, 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;From Swift Labs, makers of TTS Swap. Written with AI help and checked against OpenAI's docs and our own service on November 8, 2026. Not affiliated with or endorsed by OpenAI.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; OpenAI shuts down &lt;code&gt;tts-1-hd&lt;/code&gt; on &lt;strong&gt;January 6, 2027&lt;/strong&gt;, along with &lt;code&gt;tts-1&lt;/code&gt; and &lt;code&gt;gpt-4o-mini-tts&lt;/code&gt;. The replacement OpenAI lists is &lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt;, which runs on the Realtime API, not on &lt;code&gt;/v1/audio/speech&lt;/code&gt;. If you picked tts-1-hd for its voice quality, compare samples from OpenAI's Realtime API and other providers against your current output. TTS Swap keeps your &lt;code&gt;/v1/audio/speech&lt;/code&gt; code running; it uses MeloTTS, with no separate HD tier, so compare samples.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OpenAI says
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI's &lt;a href="https://platform.openai.com/docs/deprecations" rel="noopener noreferrer"&gt;deprecations page&lt;/a&gt; (entry "2026-10-01: Text-to-speech models") lists &lt;code&gt;tts-1-hd&lt;/code&gt; with a shutdown date of Jan 6, 2027 and &lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt; as the recommended replacement.&lt;/li&gt;
&lt;li&gt;OpenAI's &lt;a href="https://developers.openai.com/api/docs/guides/text-to-speech" rel="noopener noreferrer"&gt;text-to-speech guide&lt;/a&gt; says this is a change of API workflow: &lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt; isn't a supported model for &lt;code&gt;/v1/audio/speech&lt;/code&gt;. Existing Speech API integrations can keep working until shutdown.&lt;/li&gt;
&lt;li&gt;The same guide describes &lt;code&gt;tts-1&lt;/code&gt; as lower latency but lower quality than &lt;code&gt;tts-1-hd&lt;/code&gt;. That quality gap is why people chose HD, and it's what to check in any replacement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you can't just swap the model name from &lt;code&gt;tts-1-hd&lt;/code&gt; to &lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt; in &lt;code&gt;audio.speech.create&lt;/code&gt;. That call isn't supported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find where you use it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"tts-1-hd"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.js"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.ts"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.env*"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check config files and environment variables too. The model name is often stored outside the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your options
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Keeps &lt;code&gt;/v1/audio/speech&lt;/code&gt; code&lt;/th&gt;
&lt;th&gt;Voice quality&lt;/th&gt;
&lt;th&gt;Work involved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Realtime API (&lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;OpenAI voices&lt;/td&gt;
&lt;td&gt;Rewrite to sessions and streamed events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Another speech provider with its own API&lt;/td&gt;
&lt;td&gt;Usually no&lt;/td&gt;
&lt;td&gt;Varies; compare samples&lt;/td&gt;
&lt;td&gt;New client code, new voices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An OpenAI-compatible speech server (hosted or self-hosted)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Varies by model&lt;/td&gt;
&lt;td&gt;Change the base URL and API key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTS Swap&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;MeloTTS, one voice per language; no separate HD tier, so compare samples&lt;/td&gt;
&lt;td&gt;Change the base URL and API key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;If you chose tts-1-hd for how it sounds,&lt;/strong&gt; compare samples from the Realtime API and other providers against your current output before you decide. Quality varies, and only your own listening test can tell you whether a replacement works for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you mostly need the code to keep working&lt;/strong&gt; (internal tools, prototypes, accessibility read-aloud, batch narration where voice quality matters less), an OpenAI-compatible endpoint is the least work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using TTS Swap with tts-1-hd code
&lt;/h2&gt;

&lt;p&gt;TTS Swap answers the same &lt;code&gt;/v1/audio/speech&lt;/code&gt; request. Change the base URL and API key; your calls can keep &lt;code&gt;model="tts-1-hd"&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://tts.deadlinkpatrol.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-tts-…&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;speech&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tts-1-hd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alloy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_to_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be clear on what you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MeloTTS; no separate HD tier, so compare samples.&lt;/strong&gt; On TTS Swap, &lt;code&gt;tts-1-hd&lt;/code&gt; gives the same output as &lt;code&gt;tts-1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The voice won't sound like OpenAI's.&lt;/strong&gt; Audio comes from the open-source MeloTTS model, with one voice per language. Every voice name gets that same voice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WAV comes back&lt;/strong&gt; even when you ask for mp3 (the default). &lt;code&gt;pcm&lt;/code&gt; gives raw 24 kHz 16-bit mono. opus, aac and flac return a 400 error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;speed&lt;/code&gt;&lt;/strong&gt; is accepted but not applied yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Languages:&lt;/strong&gt; English by default; es, fr, zh, ja or ko via &lt;code&gt;extra_body={"language":"es"}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limits:&lt;/strong&gt; up to 4,096 characters per request. Paid keys are $19/month for 1,000,000 characters a month, 20 requests per minute per key. A shared daily capacity cap applies to the whole service. Over a limit you get an OpenAI-style 429.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free key:&lt;/strong&gt; 10,000 characters a month, 5 requests per minute, email only, no card. Free keys share a daily capacity limit, so on busy days they may get a 429 until 00:00 UTC.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Listen to the &lt;a href="https://tts.deadlinkpatrol.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=tts-1-hd-replacement" rel="noopener noreferrer"&gt;unedited samples on the home page&lt;/a&gt; before you decide.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When does tts-1-hd stop working?&lt;/strong&gt;&lt;br&gt;
January 6, 2027, according to OpenAI's deprecations page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I just change &lt;code&gt;tts-1-hd&lt;/code&gt; to &lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
No. OpenAI's guide says &lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt; isn't supported on &lt;code&gt;/v1/audio/speech&lt;/code&gt;. It needs the Realtime API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there an HD version on TTS Swap?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;tts-1-hd&lt;/code&gt; is accepted as a model name, but it produces the same MeloTTS audio as &lt;code&gt;tts-1&lt;/code&gt;. There's no separate HD tier, so compare samples.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I move back later?&lt;/strong&gt;&lt;br&gt;
Before January 6, 2027, yes: change the base URL and API key back to OpenAI's and keep using &lt;code&gt;tts-1-hd&lt;/code&gt;. After that date, &lt;code&gt;tts-1-hd&lt;/code&gt; is gone, so moving back means a supported OpenAI model and API workflow (such as &lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt; on the Realtime API), or another compatible provider with that provider's matching key. Always change the URL and key together; a URL without its matching key fails with a 401.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://tts.deadlinkpatrol.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=tts-1-hd-replacement" rel="noopener noreferrer"&gt;Get a free key&lt;/a&gt; or a paid key ($19/month) on the home page.&lt;/li&gt;
&lt;li&gt;Copy-paste snippets: the &lt;a href="https://tts.deadlinkpatrol.com/migrate/tts-1?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=tts-1-hd-replacement" rel="noopener noreferrer"&gt;tts-1 migration guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Keeping the rest of your setup safe: &lt;a href="https://tts.deadlinkpatrol.com/guides/openai-base-url-tts/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=tts-1-hd-replacement" rel="noopener noreferrer"&gt;Using OPENAI_BASE_URL to move text-to-speech off tts-1&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;— Swift Labs. Migration guide: &lt;a href="https://tts.deadlinkpatrol.com/migrate/tts-1?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=tts-1-hd-replacement" rel="noopener noreferrer"&gt;https://tts.deadlinkpatrol.com/migrate/tts-1?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=tts-1-hd-replacement&lt;/a&gt;&lt;/p&gt;

</description>
      <category>openai</category>
      <category>tts</category>
      <category>api</category>
      <category>python</category>
    </item>
    <item>
      <title>OpenAI's tts-1 shuts down Jan 6, 2027: your migration options, honestly compared</title>
      <dc:creator>Swift Labs</dc:creator>
      <pubDate>Sat, 31 Oct 2026 15:04:07 +0000</pubDate>
      <link>https://dev.to/swiftlabs/openais-tts-1-shuts-down-jan-6-2027-your-migration-options-honestly-compared-4j52</link>
      <guid>https://dev.to/swiftlabs/openais-tts-1-shuts-down-jan-6-2027-your-migration-options-honestly-compared-4j52</guid>
      <description>&lt;p&gt;&lt;strong&gt;Update (Nov 6, 2026):&lt;/strong&gt; Swift Labs makes TTS Swap, one of the options compared below. It isn't affiliated with or endorsed by OpenAI. It now has a free key: 10,000 characters a month, email only, no card, 5 requests a minute. You can get it from the form on &lt;a href="https://tts.deadlinkpatrol.com" rel="noopener noreferrer"&gt;https://tts.deadlinkpatrol.com&lt;/a&gt;. Paid keys are $19/mo. To switch, you change the base URL and API key. There's a step-by-step migration guide with curl, Node and Python examples at &lt;a href="https://tts.deadlinkpatrol.com/migrate/" rel="noopener noreferrer"&gt;https://tts.deadlinkpatrol.com/migrate/&lt;/a&gt;. The trade-offs are covered below: one voice per language, it returns WAV even when MP3 is requested, and speed isn't applied. If you need OpenAI's voices or other formats, its newer TTS models are still the better fit.&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;January 6, 2027&lt;/strong&gt;, OpenAI removes &lt;code&gt;tts-1&lt;/code&gt;, &lt;code&gt;tts-1-hd&lt;/code&gt; and both &lt;code&gt;gpt-4o-mini-tts&lt;/code&gt; snapshots from its API (&lt;a href="https://developers.openai.com/api/docs/deprecations" rel="noopener noreferrer"&gt;deprecations page&lt;/a&gt;, entry dated 2026-10-01). Those are every model that &lt;code&gt;/v1/audio/speech&lt;/code&gt; serves today, so any &lt;code&gt;client.audio.speech.create(...)&lt;/code&gt; call will start failing that day.&lt;/p&gt;

&lt;p&gt;The catch: the official replacement, &lt;code&gt;gpt-realtime-2.1-mini&lt;/code&gt;, &lt;strong&gt;does not run on &lt;code&gt;/v1/audio/speech&lt;/code&gt;&lt;/strong&gt;. OpenAI's own &lt;a href="https://developers.openai.com/api/docs/guides/text-to-speech" rel="noopener noreferrer"&gt;TTS guide&lt;/a&gt; says it's "a change of API workflow." Here is what that means and what your real options are.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Find every call site first
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"audio&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;speech|tts-1|gpt-4o-mini-tts|/v1/audio/speech"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.&lt;span class="o"&gt;{&lt;/span&gt;py,js,ts,go,rb&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also check config files and env vars where a model name may be stored. To monitor programmatically, &lt;code&gt;GET /v1/models&lt;/code&gt; now exposes a &lt;code&gt;shutdown_date&lt;/code&gt; field per model.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Option A: move to the Realtime API (OpenAI's path)
&lt;/h2&gt;

&lt;p&gt;You open a Realtime session (WebSocket, WebRTC or SIP), send your text as an &lt;code&gt;input_text&lt;/code&gt; conversation item, then call &lt;code&gt;response.create&lt;/code&gt; with audio output and collect the audio deltas. Things to plan for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's a conversational model, not a reader.&lt;/strong&gt; It responds to the prompt. OpenAI staff said in the &lt;a href="https://community.openai.com/t/tts-shutdown-email-tts-1-gpt-4o-mini-tts-jan-6-2027-isnt-on-the-deprecations-page/1402731" rel="noopener noreferrer"&gt;community thread&lt;/a&gt; that the docs don't guarantee verbatim reading. Use strict instructions ("Read the following text exactly, add nothing") and test with your real inputs, especially text that contains questions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voices differ.&lt;/strong&gt; &lt;code&gt;fable&lt;/code&gt;, &lt;code&gt;nova&lt;/code&gt; and &lt;code&gt;onyx&lt;/code&gt; aren't in the current Realtime list. Map them before cutover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More code.&lt;/strong&gt; A stateless HTTP POST becomes a session lifecycle with streaming events and error handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing units change&lt;/strong&gt; from per-character to per-token (audio output tokens). Re-estimate cost with a sample of real traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Option B: &lt;code&gt;gpt-audio-1.5&lt;/code&gt; via Chat Completions
&lt;/h2&gt;

&lt;p&gt;A single HTTP request to &lt;code&gt;/v1/chat/completions&lt;/code&gt; with audio output, no WebSocket. Same verbatim caveat as above: it generates a response, so validate output on your inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Option C: an OpenAI-compatible &lt;code&gt;/v1/audio/speech&lt;/code&gt; provider
&lt;/h2&gt;

&lt;p&gt;Many providers and self-hosted servers (e.g. Kokoro-FastAPI) accept the same request shape, so the migration is a &lt;code&gt;base_url&lt;/code&gt; and API key change. Before you pick one, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Formats:&lt;/strong&gt; does it really return MP3 when you ask for MP3, or something else?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voices:&lt;/strong&gt; do OpenAI voice names map to distinct voices or one voice?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parameters:&lt;/strong&gt; are &lt;code&gt;speed&lt;/code&gt; and &lt;code&gt;instructions&lt;/code&gt; applied or silently ignored?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors and limits:&lt;/strong&gt; same 400/429 shapes so your retry logic still works?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data handling:&lt;/strong&gt; is your text stored?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. A safe cutover checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Put the TTS base URL and model in config, not code.&lt;/li&gt;
&lt;li&gt;Run old and new side by side on a fixed test set; compare duration, and transcribe the new audio to confirm the words match the input.&lt;/li&gt;
&lt;li&gt;Check your audio pipeline handles the new format (WAV vs MP3 vs PCM).&lt;/li&gt;
&lt;li&gt;Ship behind a flag well before Jan 6, then flip.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I'm with Swift Labs, and we run &lt;a href="https://tts.deadlinkpatrol.com" rel="noopener noreferrer"&gt;TTS Swap&lt;/a&gt;, one Option C provider: a &lt;code&gt;base_url&lt;/code&gt; swap that keeps your &lt;code&gt;audio.speech.create&lt;/code&gt; calls working. Be aware of its trade-offs: it uses the open-source MeloTTS voice (one voice per language, not OpenAI's voices), returns WAV even when MP3 is requested, and ignores &lt;code&gt;speed&lt;/code&gt; for now. This article was drafted with AI assistance.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;— Swift Labs&lt;/p&gt;

</description>
      <category>openai</category>
      <category>tts</category>
      <category>python</category>
      <category>api</category>
    </item>
    <item>
      <title>W-2 Box 12 Codes TP and TT: What a Payroll Export Needs for Tax Year 2026</title>
      <dc:creator>Swift Labs</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:06:48 +0000</pubDate>
      <link>https://dev.to/swiftlabs/w-2-box-12-codes-tp-and-tt-what-a-payroll-export-needs-for-tax-year-2026-3j4p</link>
      <guid>https://dev.to/swiftlabs/w-2-box-12-codes-tp-and-tt-what-a-payroll-export-needs-for-tax-year-2026-3j4p</guid>
      <description>&lt;p&gt;If you build or maintain anything that produces W-2 data (a payroll export, an HR tool, a year-end script for a small business), tax year 2026 adds two new Box 12 codes: &lt;strong&gt;TP&lt;/strong&gt; for tips and &lt;strong&gt;TT&lt;/strong&gt; for overtime. Here's what the IRS 2026 General Instructions for Forms W-2 and W-3 say each one holds, and what your data needs to compute them.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Educational, not tax advice. Check the current IRS instructions and a CPA, enrolled agent or payroll provider before you file.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the codes exist
&lt;/h2&gt;

&lt;p&gt;Per the IRS instructions, P.L. 119-21 allows individuals a deduction for qualified tips and, for tax years beginning after 2024 and ending before 2029, a deduction of up to $12,500 ($25,000 if married filing jointly) for qualified overtime compensation. Employers report the amounts on the W-2 so workers can figure those deductions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code TP: total cash tips reported to the employer
&lt;/h2&gt;

&lt;p&gt;The IRS wording: "Report the total amount of cash tips reported to the employer."&lt;/p&gt;

&lt;p&gt;Two things matter for your data model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mandatory service charges are not qualified tips&lt;/strong&gt; per the IRS instructions, so they generally stay out of TP. If your system lumps service charges and tips into one earnings type, you'll need to split them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TP comes with an occupation code.&lt;/strong&gt; When you report TP, you must also list an occupation code in Box 14b. So every employee with a TP amount needs an occupation code stored somewhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Code TT: total qualified overtime compensation
&lt;/h2&gt;

&lt;p&gt;Per the IRS, qualified overtime is compensation paid to an individual required under section 7 of the Fair Labor Standards Act (FLSA) that is more than the regular rate at which the individual is employed. The IRS example: "only the 'half' portion of 'time-and-a-half' compensation would be reported using code TT."&lt;/p&gt;

&lt;p&gt;For an hourly worker, a working estimate is FLSA overtime hours × regular rate × 0.5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROUND_HALF_UP&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tt_estimate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flsa_ot_hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;regular_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Premium (&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;half&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;) portion of FLSA time-and-a-half overtime.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flsa_ot_hours&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;regular_rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;quantize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rounding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ROUND_HALF_UP&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 10 FLSA overtime hours at a $20.00 regular rate -&amp;gt; 100.00 for code TT
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tt_estimate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20.00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what that function needs: overtime hours that were required by FLSA section 7, kept separately. Overtime paid only because of a state rule or a company policy isn't described by that definition, so if your payroll data stores all overtime in one bucket, that's the gap to close. Check with an advisor on edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deadline
&lt;/h2&gt;

&lt;p&gt;The due date for filing 2026 Forms W-2 and W-3 with the SSA is February 1, 2027, on paper or electronically. Reconciling TP and TT from the payroll register in December leaves time to fix gaps before forms go out, instead of filing W-2c corrections later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist for your export
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pull 2026 cash tips reported by each employee, excluding mandatory service charges.&lt;/li&gt;
&lt;li&gt;Pull FLSA overtime hours and regular rates, and compute the premium (half) portion.&lt;/li&gt;
&lt;li&gt;Make sure every employee with TP has a Box 14b occupation code.&lt;/li&gt;
&lt;li&gt;Compare your totals with what the payroll provider will print.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.irs.gov/instructions/iw2w3" rel="noopener noreferrer"&gt;IRS: 2026 General Instructions for Forms W-2 and W-3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.irs.gov/publications/p15" rel="noopener noreferrer"&gt;IRS: Publication 15&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on the Swift Labs W-2 guides site. This article was created with the help of AI and checked for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>payroll</category>
      <category>tax</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Check Which WordPress Plugins Are Outdated on a Site, Without Logging In</title>
      <dc:creator>Swift Labs</dc:creator>
      <pubDate>Thu, 29 Oct 2026 14:12:23 +0000</pubDate>
      <link>https://dev.to/swiftlabs/how-to-check-which-wordpress-plugins-are-outdated-on-a-site-without-logging-in-22h5</link>
      <guid>https://dev.to/swiftlabs/how-to-check-which-wordpress-plugins-are-outdated-on-a-site-without-logging-in-22h5</guid>
      <description>&lt;p&gt;If you look after WordPress sites you can't log in to every week (client sites, a partner's site, a site you're about to quote on), you can still see a lot from the outside: which plugins the site loads and, for many of them, which version is installed. Here's how to do it by hand, and which signals to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. List the plugins the site loads
&lt;/h2&gt;

&lt;p&gt;Open the home page, view the page source, and search for &lt;code&gt;/wp-content/plugins/&lt;/code&gt;. The folder name after it is the plugin's slug, for example &lt;code&gt;/wp-content/plugins/contact-form-7/&lt;/code&gt;. The slug is also the plugin's address on WordPress.org: &lt;code&gt;wordpress.org/plugins/contact-form-7/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This only finds plugins that load files on that page. Plugins that run only in the admin area, or only on other pages, won't show up.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Find the installed version from a reliable source
&lt;/h2&gt;

&lt;p&gt;Two public signals are dependable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The plugin's readme.txt.&lt;/strong&gt; Many sites still serve &lt;code&gt;/wp-content/plugins/&amp;lt;slug&amp;gt;/readme.txt&lt;/code&gt;. Its &lt;code&gt;Stable tag:&lt;/code&gt; line normally matches the installed release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A generator meta tag.&lt;/strong&gt; Some plugins print their own version in the page head, for example &lt;code&gt;&amp;lt;meta name="generator" content="Elementor 4.2.3"&amp;gt;&lt;/code&gt;. Elementor, WooCommerce, Site Kit by Google and Slider Revolution do this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If both are present they should agree. If they don't, treat the version as unknown.&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="c"&gt;# readme.txt Stable tag&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/wp-content/plugins/contact-form-7/readme.txt | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"stable tag"&lt;/span&gt;

&lt;span class="c"&gt;# generator tags in the page head&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-io&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;meta name="generator"[^&amp;gt;]*&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Don't trust &lt;code&gt;?ver=&lt;/code&gt; numbers
&lt;/h2&gt;

&lt;p&gt;Script and stylesheet links often end in &lt;code&gt;?ver=1.2.3&lt;/code&gt;. That number is frequently the version of a bundled library, a theme setting or a cache-buster, not the plugin release. It's a hint at best. Reporting it as the installed version is how people end up telling a client a plugin is outdated when it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Compare with the latest release on WordPress.org
&lt;/h2&gt;

&lt;p&gt;The plugin's WordPress.org page shows the current version. For scripting, the public plugins API is quicker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="s2"&gt;"https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&amp;amp;request[slug]=contact-form-7"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'import json,sys; print(json.load(sys.stdin)["version"])'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;-g&lt;/code&gt; stops curl from treating the square brackets as a URL glob.) If the installed version is lower, the plugin is outdated. Premium plugins that aren't on WordPress.org can't be compared this way.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Check WordPress core and PHP too
&lt;/h2&gt;

&lt;p&gt;Core often shows in a generator tag (&lt;code&gt;WordPress 6.9&lt;/code&gt;) or in the RSS feed at &lt;code&gt;/feed/&lt;/code&gt;. Some servers send an &lt;code&gt;X-Powered-By: PHP/8.1&lt;/code&gt; header. php.net lists which PHP versions still get security fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Plugin slugs collected from &lt;code&gt;/wp-content/plugins/&lt;/code&gt; paths in the page source&lt;/li&gt;
&lt;li&gt;[ ] Installed version taken from readme.txt &lt;code&gt;Stable tag&lt;/code&gt; or a generator tag, never from &lt;code&gt;?ver=&lt;/code&gt; alone&lt;/li&gt;
&lt;li&gt;[ ] Both signals agree (or the version is marked unknown)&lt;/li&gt;
&lt;li&gt;[ ] Latest version taken from WordPress.org&lt;/li&gt;
&lt;li&gt;[ ] Core and PHP checked where they're exposed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By hand this takes a few minutes per site, and plugins update constantly, so it's worth scripting if you do it for more than a handful of sites.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://wpradar.deadlinkpatrol.com/guides/check-wordpress-plugin-versions-without-login" rel="noopener noreferrer"&gt;WP Update Radar&lt;/a&gt; by Swift Labs, which runs these same outside-in checks on a list of sites each week.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was created with the help of AI and checked for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The DMARC Record a Small Business Should Publish First (and When to Tighten It)</title>
      <dc:creator>Swift Labs</dc:creator>
      <pubDate>Sat, 24 Oct 2026 16:10:30 +0000</pubDate>
      <link>https://dev.to/swiftlabs/the-dmarc-record-a-small-business-should-publish-first-and-when-to-tighten-it-1pbi</link>
      <guid>https://dev.to/swiftlabs/the-dmarc-record-a-small-business-should-publish-first-and-when-to-tighten-it-1pbi</guid>
      <description>&lt;p&gt;If your business sends mail from its own domain (&lt;a href="mailto:you@yourbusiness.com"&gt;you@yourbusiness.com&lt;/a&gt;), three DNS records do the authentication work: SPF, DKIM and DMARC. SPF and DKIM prove a message came from a server you actually use. DMARC tells the receiving server what to do when a message claims to be from your domain but fails those checks, and where to send reports about it.&lt;/p&gt;

&lt;p&gt;With no DMARC record, anyone can put your domain in the From: line of a phishing email. The receiving server has no instructions from you, and you never hear about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one TXT record
&lt;/h2&gt;

&lt;p&gt;At your DNS host (Cloudflare, GoDaddy, Squarespace, Network Solutions, or wherever your DNS lives), add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host/Name: _dmarc
Type:      TXT
Value:     v=DMARC1; p=none; rua=mailto:dmarc-reports@yourbusiness.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap in a mailbox you really own. Some providers want the full name &lt;code&gt;_dmarc.yourbusiness.com&lt;/code&gt; instead of just &lt;code&gt;_dmarc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;p=none&lt;/code&gt; is monitoring mode. Nothing gets blocked or sent to spam, so it can't break your mail. It just starts the reports flowing so you can see who is sending as your domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tags, and what a small business should do with them
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Suggested setting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;v=DMARC1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Marks the record as DMARC; must come first&lt;/td&gt;
&lt;td&gt;Always &lt;code&gt;v=DMARC1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;p=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Policy for failing mail: &lt;code&gt;none&lt;/code&gt;, &lt;code&gt;quarantine&lt;/code&gt; (spam folder), &lt;code&gt;reject&lt;/code&gt; (block)&lt;/td&gt;
&lt;td&gt;Start with &lt;code&gt;none&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rua=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Where daily aggregate reports go&lt;/td&gt;
&lt;td&gt;A mailbox you own, or a monitoring service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pct=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Share of failing mail the policy applies to&lt;/td&gt;
&lt;td&gt;Omit (defaults to 100) until you move to quarantine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sp=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Policy for subdomains&lt;/td&gt;
&lt;td&gt;Omit; subdomains inherit &lt;code&gt;p=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;adkim=&lt;/code&gt; / &lt;code&gt;aspf=&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Strict or relaxed alignment&lt;/td&gt;
&lt;td&gt;Omit; relaxed is the default and suits most setups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ruf=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-message forensic reports&lt;/td&gt;
&lt;td&gt;Skip; few providers send them and they can include message content&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Two mistakes that quietly make DMARC useless
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;p=none&lt;/code&gt; with no &lt;code&gt;rua=&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;v=DMARC1; p=none;&lt;/code&gt; is valid but does nothing: no protection, no reports. Checkers still mark it "present", so it looks finished. Add a &lt;code&gt;rua=&lt;/code&gt; address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Two DMARC records.&lt;/strong&gt; If &lt;code&gt;_dmarc.yourbusiness.com&lt;/code&gt; has two TXT records starting with &lt;code&gt;v=DMARC1&lt;/code&gt;, receivers treat the domain as having no DMARC at all. Merge them into one. This often happens when a website builder or email provider adds its own record next to yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get SPF and DKIM right first
&lt;/h2&gt;

&lt;p&gt;DMARC passes when a message passes SPF &lt;em&gt;or&lt;/em&gt; DKIM &lt;strong&gt;and&lt;/strong&gt; the passing domain matches the From: domain. Before tightening anything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SPF:&lt;/strong&gt; a single TXT record at the root of the domain starting with &lt;code&gt;v=spf1&lt;/code&gt;, listing every service that sends as you (e.g. &lt;code&gt;include:_spf.google.com&lt;/code&gt; for Google Workspace, &lt;code&gt;include:spf.protection.outlook.com&lt;/code&gt; for Microsoft 365). Only one SPF record is allowed, and it has to stay under 10 DNS lookups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DKIM:&lt;/strong&gt; turn it on in your mail provider's admin console and publish the DNS records it gives you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then list everything else that sends as your domain: newsletter tool, invoicing or booking software, CRM, website contact forms. Each needs SPF or DKIM set up for your domain, or it will fail once you enforce DMARC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is DMARC required for a small sender?
&lt;/h2&gt;

&lt;p&gt;Gmail's sender guidelines require everyone sending to personal Gmail accounts to use SPF or DKIM. Senders of more than 5,000 messages a day must also publish DMARC, and &lt;code&gt;p=none&lt;/code&gt; is acceptable. Most small businesses are far below 5,000 a day, so strictly speaking they don't need it.&lt;/p&gt;

&lt;p&gt;Google still recommends SPF, DKIM and DMARC for every domain, and DMARC reports are the only way to see who is sending mail under your name. It costs one DNS record, and at &lt;code&gt;p=none&lt;/code&gt; there's no risk to your own mail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving from p=none to quarantine and reject
&lt;/h2&gt;

&lt;p&gt;Collect at least two to four weeks of reports at &lt;code&gt;p=none&lt;/code&gt;, then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Confirm every legitimate sender passes.&lt;/strong&gt; Each service you use should show SPF or DKIM passing and aligned in the reports. Fix the ones that don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step into quarantine gradually&lt;/strong&gt;, e.g. &lt;code&gt;v=DMARC1; p=quarantine; pct=25; rua=mailto:...&lt;/code&gt;, raising &lt;code&gt;pct&lt;/code&gt; to 100 over a couple of weeks while you watch the reports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch to reject&lt;/strong&gt; once quarantine at 100% has run cleanly: &lt;code&gt;v=DMARC1; p=reject; rua=mailto:...&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Reading the reports
&lt;/h2&gt;

&lt;p&gt;Aggregate reports arrive as compressed XML attachments, typically one a day from each large mailbox provider that received your mail. They're painful to read by hand. You can parse them yourself, or point &lt;code&gt;rua=&lt;/code&gt; at a service that reads them for you. DMARC allows several &lt;code&gt;rua&lt;/code&gt; addresses separated by commas, so you can keep your own mailbox in the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] One TXT record at &lt;code&gt;_dmarc.yourbusiness.com&lt;/code&gt; starting with &lt;code&gt;v=DMARC1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;p=none&lt;/code&gt; to start&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;rua=mailto:&lt;/code&gt; pointing at a mailbox you read, or a monitoring service&lt;/li&gt;
&lt;li&gt;[ ] One SPF record, under 10 lookups, covering every sender&lt;/li&gt;
&lt;li&gt;[ ] DKIM enabled with your mail provider and each sending tool&lt;/li&gt;
&lt;li&gt;[ ] 2-4 weeks of reports reviewed before &lt;code&gt;p=quarantine&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sources: Gmail email sender guidelines (support.google.com/a/answer/81126); DMARC tag definitions and multiple-record handling per RFC 7489 §6.3 and §6.6.3.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dns.deadlinkpatrol.com/dmarc-record-for-small-business" rel="noopener noreferrer"&gt;Email DNS Check&lt;/a&gt;, a free SPF/DKIM/DMARC checker by Swift Labs. We also build &lt;a href="https://mailguard.deadlinkpatrol.com" rel="noopener noreferrer"&gt;MailGuard&lt;/a&gt;, which reads DMARC reports and sends a plain-English weekly summary ($7/domain/month, 14-day trial).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was created with the help of AI and checked for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>email</category>
      <category>dns</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How Often Should You Check for Broken Links? A Schedule by Site Type</title>
      <dc:creator>Swift Labs</dc:creator>
      <pubDate>Fri, 23 Oct 2026 14:08:39 +0000</pubDate>
      <link>https://dev.to/swiftlabs/how-often-should-you-check-for-broken-links-a-schedule-by-site-type-emg</link>
      <guid>https://dev.to/swiftlabs/how-often-should-you-check-for-broken-links-a-schedule-by-site-type-emg</guid>
      <description>&lt;p&gt;"Check your links regularly" is common advice that doesn't tell you much. Regularly could mean every day or once a year. The honest answer is that the right schedule depends on &lt;strong&gt;how quickly your site can break&lt;/strong&gt;, and that comes down to two things: how often &lt;em&gt;you&lt;/em&gt; change it, and how many links point to things &lt;em&gt;you don't control&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This guide lays out a schedule by site type, lists the events that should trigger a check no matter what the schedule says, and explains why "how often" matters less than "what happens after the check."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why links break in the first place
&lt;/h2&gt;

&lt;p&gt;To pick a schedule, it helps to know where breakage actually comes from. Almost all of it has one of five causes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You changed something.&lt;/strong&gt; You renamed a page, changed a slug, removed a product, restructured the menu, or switched themes. This is the most common cause of &lt;em&gt;internal&lt;/em&gt; broken links, and it happens right when you make the change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A migration or redesign.&lt;/strong&gt; This is the same as the first cause but at scale. Changes to URL patterns (&lt;code&gt;/blog/post-name&lt;/code&gt; becoming &lt;code&gt;/post-name&lt;/code&gt;, &lt;code&gt;.html&lt;/code&gt; extensions dropped, category bases changed) can break hundreds of links in one go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Someone else changed something.&lt;/strong&gt; External sites you link to delete pages, restructure, or shut down. This is "link rot." It happens gradually, and you have no warning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content editors paste bad links.&lt;/strong&gt; A missing &lt;code&gt;https://&lt;/code&gt; makes a link relative, so &lt;code&gt;example.com&lt;/code&gt; on &lt;code&gt;/about/&lt;/code&gt; turns into &lt;code&gt;/about/example.com&lt;/code&gt; and returns a 404. A typo or a staging URL left in a post does the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Files and media go missing.&lt;/strong&gt; PDFs get replaced with new filenames, images are deleted from the media library, and CDN paths change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Causes 1, 2, 4 and 5 happen when you change something, so you can catch them by checking &lt;em&gt;after the change&lt;/em&gt;. Cause 3 happens at random, so you can only catch it by checking &lt;em&gt;on a schedule&lt;/em&gt;. A good routine handles both.&lt;/p&gt;

&lt;h2&gt;
  
  
  A starting schedule by site type
&lt;/h2&gt;

&lt;p&gt;These are sensible defaults, not rules. Change them based on what your first few scans turn up (more on that below).&lt;/p&gt;

&lt;h3&gt;
  
  
  Small brochure site (under ~50 pages, edited a few times a year)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Monthly&lt;/strong&gt; scheduled check, plus a check after any edit session.&lt;/p&gt;

&lt;p&gt;A local plumber's five-page site doesn't change much. Its internal links break only when someone edits it, so a scan after edits catches most problems. A monthly scan picks up external link rot (links to suppliers, certification bodies, review profiles) and any changes the host or platform made without telling you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Active blog or content site (new posts weekly)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Weekly&lt;/strong&gt; check.&lt;/p&gt;

&lt;p&gt;Every new post adds links, and older posts link to external sources that rot over time. The more content a site has, the more external links it has, and the more rot each month. Weekly is usually enough. If you publish every day, look for a checker that can scan new pages as they go live.&lt;/p&gt;

&lt;h3&gt;
  
  
  E-commerce store
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Weekly at minimum. Daily if products change often.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stores change all the time. Products sell out and get removed, collections get reorganized, and seasonal landing pages expire. Each removed product URL can leave broken links in category pages, blog posts that mention it, and emails. On a store, a broken link means a lost sale, so it costs more than it would on a brochure site.&lt;/p&gt;

&lt;h3&gt;
  
  
  Large site, or a site with frequent deploys
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Daily.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the site has thousands of pages, several editors, or a development team that deploys several times a week, things break between scans often enough that weekly checks let problems sit for days. Daily scans that &lt;strong&gt;only alert on new breakage&lt;/strong&gt; are the norm here. Without diffing, a daily report becomes noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agencies managing client sites
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Daily for every site under a care plan, with weekly or monthly reporting to the client.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The schedule and the report don't have to match. Checking daily means you find out about a break before the client does. Reporting monthly gives the client a summary they'll actually read. If you're charging for maintenance, finding out from the client that their booking page has been a 404 for two weeks is the outcome you most want to avoid.&lt;/p&gt;

&lt;h3&gt;
  
  
  Documentation and knowledge bases
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;On every build, plus a weekly scan of the live site.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Docs are full of cross-references and links to API references, code repositories and release notes, all of which move. Build-time checks catch internal mistakes before they ship. The weekly live scan catches external rot and anything the build check can't see, like redirects added at the server level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Events that should trigger a check right away
&lt;/h2&gt;

&lt;p&gt;Whatever your schedule, scan as soon as possible after any of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A site migration or redesign.&lt;/strong&gt; Scan the old site &lt;em&gt;before&lt;/em&gt; the switch so you have a list of URLs, then scan the new site straight after launch. Every old URL that now 404s needs a 301 redirect or a reason it doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A change to the URL structure.&lt;/strong&gt; Permalink settings, category bases, trailing slashes, language prefixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A theme or page-builder change.&lt;/strong&gt; Menus, footers and widgets often get rebuilt, and their links can quietly change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk deletion.&lt;/strong&gt; Discontinued products, old events, expired job posts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A domain change or HTTPS move.&lt;/strong&gt; Hard-coded absolute URLs in content are a classic source of breakage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A CMS or plugin update that touches routing.&lt;/strong&gt; This doesn't happen often, but when it does it hits every page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any time you get an unexplained traffic drop&lt;/strong&gt; on specific pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your checker has an API, wire a scan into your deploy pipeline so a scan starts as part of every deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tune the schedule after a month
&lt;/h2&gt;

&lt;p&gt;Your first scan is a baseline, and it will probably find a backlog. Fix that first. After that, watch how much &lt;em&gt;new&lt;/em&gt; breakage turns up between scans:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see between scans&lt;/th&gt;
&lt;th&gt;What it suggests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Almost never anything new&lt;/td&gt;
&lt;td&gt;Your schedule is fine, or you could scan less often&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A few new external breaks each time&lt;/td&gt;
&lt;td&gt;Normal link rot, so keep the schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New internal breaks most times&lt;/td&gt;
&lt;td&gt;Edits are breaking things, so scan more often or add post-edit checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bursts of new breaks after deploys&lt;/td&gt;
&lt;td&gt;Add a scan to your deploy process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lots of 403/429/timeouts on the same external sites&lt;/td&gt;
&lt;td&gt;Those sites block bots. Exclude them or verify them by hand&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The aim is that each scan turns up a small, manageable number of genuinely new problems. If every report is huge, either the backlog hasn't been cleared or the schedule is too infrequent for how fast the site changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What matters more than frequency
&lt;/h2&gt;

&lt;p&gt;A daily scan that nobody reads is worth less than a monthly scan that someone acts on. Three things decide whether a checking routine actually keeps links working:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Only alert on what's new.&lt;/strong&gt; If every report repeats last week's known issues, people stop reading them. Use a checker that compares each run against the previous one, or keep your own list of known issues and alert only on the difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Verify before you alert.&lt;/strong&gt; External sites go down briefly, and some answer automated requests with errors while working fine in a browser. A good checker rechecks a failure before reporting it. A rule we use ourselves: only count a link as broken if it returns 404 or 410 on two separate requests. One failed request isn't enough to call a link broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Give the fix an owner.&lt;/strong&gt; Every report should go to someone whose job includes fixing it. On an agency team, that means routing each client's alerts to the person who manages that client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Google care how quickly you fix broken links?
&lt;/h2&gt;

&lt;p&gt;Google's own documentation is useful here. Its Search Console help says that 404 responses "are not necessarily a problem, if the page has been removed without any replacement," and that if a page has moved, you should "use a 301 redirect to the new location." Its developer docs explain that URLs returning a 4xx status are removed from the index, and that Googlebot crawls them less often over time.&lt;/p&gt;

&lt;p&gt;So a broken link isn't a penalty in itself. The real cost is what the link would have done: a visitor who reaches a dead end instead of a product page, internal link value that leads nowhere, or a page that dropped out of the index because it moved without a redirect. Those costs add up the longer the link stays broken, so the useful question is how long you're willing to let a broken link hurt you before you notice. If the answer is "no more than a day" for your checkout and contact pages, those pages need daily checks, whatever you do with the rest of the site.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple setup that covers most sites
&lt;/h2&gt;

&lt;p&gt;If you'd rather not think about any of this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run a full scan today and fix everything it finds.&lt;/li&gt;
&lt;li&gt;Schedule automatic scans: monthly for small static sites, weekly for blogs and stores, daily for large sites and client sites you're paid to maintain.&lt;/li&gt;
&lt;li&gt;Make sure alerts only fire for &lt;strong&gt;new&lt;/strong&gt; breakage.&lt;/li&gt;
&lt;li&gt;Add a manual scan to your checklist for launches, migrations and theme changes.&lt;/li&gt;
&lt;li&gt;Look at the numbers after a month and adjust.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://deadlinkpatrol.com/blog/how-often-to-check-for-broken-links" rel="noopener noreferrer"&gt;DeadLinkPatrol blog&lt;/a&gt;. We build DeadLinkPatrol, a scheduled broken-link checker. If you want to see what a live crawl finds on your own site, the &lt;a href="https://deadlinkpatrol.com/check" rel="noopener noreferrer"&gt;free checker&lt;/a&gt; scans up to 25 pages without signup.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was created with the help of AI and checked for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Broken Link Checking for Docs Sites: Build-Time Checks Plus Live Monitoring</title>
      <dc:creator>Swift Labs</dc:creator>
      <pubDate>Wed, 21 Oct 2026 14:08:55 +0000</pubDate>
      <link>https://dev.to/swiftlabs/broken-link-checking-for-docs-sites-build-time-checks-plus-live-monitoring-jce</link>
      <guid>https://dev.to/swiftlabs/broken-link-checking-for-docs-sites-build-time-checks-plus-live-monitoring-jce</guid>
      <description>&lt;p&gt;Documentation sites are hard on links. They're full of cross-references between pages, anchors to specific sections, links to API references, code repositories, changelogs and third-party docs, and all of those move. Pages get renamed when the product changes, versions get archived, and the external projects you link to restructure their own docs. A reader who follows a "see Configuration" link and lands on a 404 in the middle of a task is a frustrated reader, and often becomes a support ticket.&lt;/p&gt;

&lt;p&gt;The good news is that docs teams have better tools for this than most websites, because docs are usually built from source files. The catch is that build-time checks only catch some problems. This guide covers both halves: what to check during the build, and what you can only catch by checking the live site.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three kinds of broken docs links
&lt;/h2&gt;

&lt;p&gt;It helps to separate them, because different tools catch each kind.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Internal page links.&lt;/strong&gt; A link from one doc page to another that doesn't exist (renamed file, deleted page, typo in a relative path). Your static site generator can catch these at build time, because it knows every page that exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anchor links.&lt;/strong&gt; A link to &lt;code&gt;/config#retry-options&lt;/code&gt; when the heading is now "Retry settings", so the anchor is &lt;code&gt;#retry-settings&lt;/code&gt;. The page loads, so nothing returns a 404, but the reader lands at the top of a long page instead of the section they wanted. Some generators can check these. Many link checkers ignore them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External links.&lt;/strong&gt; Links to GitHub, package registries, other projects' docs, blog posts and standards. Your build can't know whether these work, so you need a checker that makes real HTTP requests, and those links can break at any time after you deploy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's a fourth, sneakier kind: &lt;strong&gt;links that work in the build but break in production.&lt;/strong&gt; Server-level redirects, a base path that's different in production, a CDN rule, or a versioned-docs setup where old versions point at pages that only exist in the latest version. Only a check against the live site finds these.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: Turn on your generator's own checks
&lt;/h2&gt;

&lt;p&gt;Start here. It's free and takes a few minutes. Exact option names vary between versions, so check your generator's docs for the version you run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docusaurus
&lt;/h3&gt;

&lt;p&gt;Docusaurus checks internal links when you run a production build, and the &lt;code&gt;onBrokenLinks&lt;/code&gt; option in &lt;code&gt;docusaurus.config.js&lt;/code&gt; controls what happens when it finds one. In current versions the default is &lt;code&gt;'throw'&lt;/code&gt;, which fails the build. Keep it that way. There are related options for broken Markdown file links and for broken anchors (&lt;code&gt;onBrokenAnchors&lt;/code&gt;, which defaults to a warning in the versions that have it). If you can, set anchors to throw as well, so renamed headings fail the build instead of quietly sending readers to the top of the page.&lt;/p&gt;

&lt;p&gt;Note that these checks happen during &lt;code&gt;docusaurus build&lt;/code&gt;, not in the dev server, so make sure CI runs a real build on every pull request.&lt;/p&gt;

&lt;h3&gt;
  
  
  MkDocs
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;mkdocs build --strict&lt;/code&gt; in CI. Strict mode turns warnings, including warnings about links to documentation files that don't exist, into a failed build. Recent MkDocs versions also have a &lt;code&gt;validation&lt;/code&gt; section in &lt;code&gt;mkdocs.yml&lt;/code&gt; that controls how strictly different link problems are treated, including anchors. MkDocs validates links &lt;em&gt;between your Markdown files&lt;/em&gt;. It doesn't check external URLs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sphinx
&lt;/h3&gt;

&lt;p&gt;Sphinx warns about broken cross-references during a normal build (&lt;code&gt;-W&lt;/code&gt; turns warnings into errors, and &lt;code&gt;-n&lt;/code&gt; "nitpicky" mode reports missing references more aggressively). For external URLs it ships a dedicated builder: &lt;code&gt;sphinx-build -b linkcheck&lt;/code&gt; (or &lt;code&gt;make linkcheck&lt;/code&gt;) requests every external link and writes a report. It's slow on large sites, so most teams run it on a schedule rather than on every commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hugo
&lt;/h3&gt;

&lt;p&gt;Hugo's &lt;code&gt;ref&lt;/code&gt; and &lt;code&gt;relref&lt;/code&gt; shortcodes fail or warn (depending on &lt;code&gt;refLinksErrorLevel&lt;/code&gt;) when they point at a page that doesn't exist. Plain Markdown links aren't checked, so teams that care use &lt;code&gt;ref&lt;/code&gt;/&lt;code&gt;relref&lt;/code&gt; for internal links and an external checker for everything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  Everything else (GitBook, Mintlify, ReadMe, custom setups)
&lt;/h3&gt;

&lt;p&gt;Hosted docs platforms vary a lot. Some flag broken internal links in their editor, some don't, and few check external links on a schedule. If yours doesn't, you'll rely more on Layers 2 and 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: Run a link checker in CI
&lt;/h2&gt;

&lt;p&gt;Build checks handle internal links. For external links, add a dedicated link checker to your CI pipeline. Common choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;lychee&lt;/strong&gt;, a fast link checker written in Rust that works on Markdown, HTML and plain text. It has an official GitHub Action (&lt;code&gt;lycheeverse/lychee-action&lt;/code&gt;) and supports caching, exclusions and retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;markdown-link-check&lt;/strong&gt;, a Node tool that checks links in Markdown files and is popular in smaller repos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generator-specific actions&lt;/strong&gt;, such as the MkDocs link-check GitHub Action, which wrap a checker with sensible defaults for that generator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few lessons that save a lot of frustration:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't block pull requests on external links.&lt;/strong&gt; An external site going down for ten minutes shouldn't stop someone merging a typo fix. Block PRs on &lt;em&gt;internal&lt;/em&gt; link failures (Layer 1). Run external checks on a schedule or as a non-blocking job, and open an issue when something fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expect bot-blocking.&lt;/strong&gt; Some sites answer automated requests with 403, 429 or other non-200 codes while working fine in a browser. Social networks and some large platforms are known for it. Keep an exclusion list for known false positives, and review it every few months, because "excluded" can quietly turn into "never checked."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retry before failing.&lt;/strong&gt; Use your checker's retry settings, or have it check twice before reporting. A single timeout isn't a broken link.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use caching on a schedule.&lt;/strong&gt; Checking every external link in a large docs repo on every run is slow and can get you rate-limited. Caching results for a day or so is a reasonable compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: Monitor the live site
&lt;/h2&gt;

&lt;p&gt;Layers 1 and 2 check your &lt;em&gt;source&lt;/em&gt;. Your readers use the &lt;em&gt;deployed site&lt;/em&gt;, and the two differ more than you'd think:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redirects and rewrites&lt;/strong&gt; set up in your host, CDN or reverse proxy don't exist in the build environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base paths and versioning.&lt;/strong&gt; Docs served under &lt;code&gt;/docs/v2/&lt;/code&gt; can behave differently from the local build, especially with versioned docs where old versions link to paths that only exist in newer ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generated content.&lt;/strong&gt; API references generated from code, embedded changelogs or content pulled in at deploy time may never be seen by the source checker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Things that break after deploy.&lt;/strong&gt; Every external link you checked at build time can break next week. If you only check on commits, a docs section nobody edits never gets rechecked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assets.&lt;/strong&gt; Images, downloadable files and embedded media served from a separate bucket or CDN.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A scheduled crawl of the live site catches all of these. It works the way a reader does: it starts at your docs homepage, follows links, and records every link and image that returns an error, along with the page it was found on. Weekly is enough for most docs sites. If you deploy docs many times a day, run it daily and alert only on &lt;em&gt;new&lt;/em&gt; breakage, so the alerts don't become noise.&lt;/p&gt;

&lt;p&gt;Any crawler that records the "found on" page for each broken link works here: a scheduled lychee run against the deployed URL, a hosted monitor, or a small script of your own. What matters is that it hits the live site, rechecks failures before reporting them, and compares each run with the previous one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docs-specific habits that prevent breakage
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Redirect when you rename.&lt;/strong&gt; Most docs platforms and hosts support redirect maps. Make "add a redirect" part of the checklist for renaming or moving any page, the same way you'd update the sidebar. Old docs URLs get bookmarked, linked from forum answers and Stack Overflow, and cited in other projects' docs, so they need to keep working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Link to stable URLs.&lt;/strong&gt; For external references, prefer permalinks: tagged release URLs instead of &lt;code&gt;main&lt;/code&gt; branch file links (which break when files move), versioned API docs instead of "latest" where the content matters, and DOIs or archived copies for papers and specs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Link to headings deliberately.&lt;/strong&gt; If your generator lets you set explicit heading IDs, use them for sections that get linked often. Then changing the heading text doesn't change the anchor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep an eye on archived versions.&lt;/strong&gt; Versioned docs multiply your link count. Decide how far back you'll maintain links. For versions you've frozen, you can exclude external links from checks and add a banner pointing readers to the current version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Give link reports an owner.&lt;/strong&gt; Route scheduled link failures to a real place, such as a docs issue label, a Slack channel or a weekly triage, with a named owner. A report nobody owns gets ignored.&lt;/p&gt;

&lt;h2&gt;
  
  
  A setup that covers all three layers
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every pull request:&lt;/strong&gt; production build with the generator's broken-link checks set to fail (internal pages, plus anchors if supported).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nightly or weekly in CI:&lt;/strong&gt; lychee or similar against the source for external links, non-blocking, opening an issue when something fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekly (daily if you deploy often):&lt;/strong&gt; a crawl of the live docs site, alerting only on new breakage, with the "found on" page included.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always:&lt;/strong&gt; redirects for moved pages, stable external URLs, and explicit IDs for frequently linked headings.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://deadlinkpatrol.com/blog/broken-link-checker-for-documentation-sites" rel="noopener noreferrer"&gt;DeadLinkPatrol blog&lt;/a&gt;. We build DeadLinkPatrol, a scheduled broken-link checker. If you want to see what a live crawl finds on your own site, the &lt;a href="https://deadlinkpatrol.com/check" rel="noopener noreferrer"&gt;free checker&lt;/a&gt; scans up to 25 pages without signup.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was created with the help of AI and checked for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>devops</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Search Console "Not Found (404)" Errors: Which to Fix and Which to Ignore</title>
      <dc:creator>Swift Labs</dc:creator>
      <pubDate>Mon, 19 Oct 2026 07:34:44 +0000</pubDate>
      <link>https://dev.to/swiftlabs/search-console-not-found-404-errors-which-to-fix-and-which-to-ignore-16l6</link>
      <guid>https://dev.to/swiftlabs/search-console-not-found-404-errors-which-to-fix-and-which-to-ignore-16l6</guid>
      <description>&lt;p&gt;Open the Page indexing report in Google Search Console on almost any site that has been around for a while and you'll find a "Not found (404)" row, sometimes with hundreds or thousands of URLs. It looks like an emergency. Most of the time it isn't, but a few of those URLs probably do matter, and the trick is to work out which ones.&lt;/p&gt;

&lt;p&gt;This guide explains what Google actually says about 404s, gives you a simple triage to sort the list into "fix", "redirect" and "leave alone", and shows how to stop the list from growing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Google says about 404s
&lt;/h2&gt;

&lt;p&gt;It's worth starting from Google's own documentation rather than SEO folklore.&lt;/p&gt;

&lt;p&gt;In the Search Console help for the Page indexing report, Google describes a "Not found (404)" URL as one that "returned a 404 error when requested," discovered "without any explicit request or sitemap," possibly "as a link from another page, or possibly the page existed before and was deleted." It then says plainly: &lt;strong&gt;"404 responses are not necessarily a problem, if the page has been removed without any replacement. If your page has moved, use a 301 redirect to the new location."&lt;/strong&gt; It also notes that Googlebot "will probably continue to try this URL for some period of time" and "will crawl it less and less often."&lt;/p&gt;

&lt;p&gt;Google's developer documentation on HTTP status codes adds that all 4xx errors except 429 are treated the same way: the URL is removed from the index if it was indexed before, "newly encountered 404 pages aren't processed," and "the crawling frequency gradually decreases."&lt;/p&gt;

&lt;p&gt;That gives you two practical conclusions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A 404 is the correct response for a page that's genuinely gone.&lt;/strong&gt; Returning one doesn't penalize your site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 404 on a URL that &lt;em&gt;should&lt;/em&gt; work is a real problem.&lt;/strong&gt; It's not a penalty either, but the page drops out of the index and any traffic or link value it had is lost.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So you're not trying to get the 404 count to zero. You're trying to find the 404s that shouldn't be 404s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Export the list
&lt;/h2&gt;

&lt;p&gt;In Search Console, go to &lt;strong&gt;Indexing → Pages&lt;/strong&gt;, click the &lt;strong&gt;Not found (404)&lt;/strong&gt; row, and export the examples. Search Console shows a sample of affected URLs, not necessarily every one, so treat the export as a starting point rather than a full inventory.&lt;/p&gt;

&lt;p&gt;Add two columns to the spreadsheet: &lt;strong&gt;Category&lt;/strong&gt; and &lt;strong&gt;Action&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Sort every URL into one of five buckets
&lt;/h2&gt;

&lt;p&gt;Go through the list (filter by URL pattern to speed it up) and put each URL into one of these buckets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket A: Pages that moved and still have an equivalent
&lt;/h3&gt;

&lt;p&gt;A product renamed from &lt;code&gt;/blue-widget&lt;/code&gt; to &lt;code&gt;/widgets/blue&lt;/code&gt;. A blog post whose slug changed. A service page merged into another service page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action: 301 redirect to the closest equivalent.&lt;/strong&gt; This is exactly what Google's help recommends, and it's the most valuable fix on the list, because these URLs often had rankings, backlinks, or both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket B: Pages deleted on purpose with no replacement
&lt;/h3&gt;

&lt;p&gt;A discontinued product with no successor. An event from 2019. A job ad that has been filled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action: leave it as a 404 (or a 410 if you want to signal "gone for good").&lt;/strong&gt; Don't redirect these to the homepage. Google tends to treat irrelevant redirects like that as soft 404s anyway, and users who land on your homepage instead of what they clicked get confused. If the deleted page had good backlinks and there's a &lt;em&gt;genuinely related&lt;/em&gt; page, such as the category it belonged to, a redirect there is reasonable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket C: URLs that never existed
&lt;/h3&gt;

&lt;p&gt;Typos in links on other sites, URLs with junk parameters, spam bots probing &lt;code&gt;/wp-login.php&lt;/code&gt; on a site that isn't WordPress, paths with tracking garbage tacked on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action: ignore.&lt;/strong&gt; You don't control other people's typos, and these URLs never had value. The exception is a typo'd URL that has a strong backlink pointing at it. A redirect to the intended page recovers that value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket D: URLs created by your own broken links
&lt;/h3&gt;

&lt;p&gt;This is the bucket people miss. The URL is in Search Console because &lt;strong&gt;your own site links to it&lt;/strong&gt;. Common causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A link edited in one place but not in another (old menu item, footer, sidebar widget)&lt;/li&gt;
&lt;li&gt;A relative link missing its protocol: an editor types &lt;code&gt;example.com/page&lt;/code&gt; instead of &lt;code&gt;https://example.com/page&lt;/code&gt;, and the browser resolves it relative to the current page, so you get &lt;code&gt;/about/example.com/page&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A template that builds URLs from data that's sometimes empty (&lt;code&gt;/products/&lt;/code&gt; + nothing)&lt;/li&gt;
&lt;li&gt;Links in old blog posts pointing at pages that have since moved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Action: fix the link at the source, and add a redirect if the URL has external value.&lt;/strong&gt; A redirect alone isn't enough. If your own pages link to a 404, every visitor who clicks it hits a dead end, and internal links pass through a redirect hop for no reason. Find the page that contains the link and edit it.&lt;/p&gt;

&lt;p&gt;Search Console's URL Inspection tool can sometimes show you where Google found a URL, but it doesn't reliably list every page that links to it. A crawler that records the source page for every broken link, the "found on" page, is the faster way to find these.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket E: URLs that should work but don't
&lt;/h3&gt;

&lt;p&gt;The URL is correct, the page should exist, and it returns 404 anyway. Typical causes are a deployment that dropped a route, a CMS page set to draft by mistake, a permalink flush that didn't run, or a server rule that's too broad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action: fix the page itself. This is a bug, not a content decision.&lt;/strong&gt; If a Bucket E URL is a revenue page (pricing, checkout, contact, booking), it's the most urgent item on the whole list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Prioritize
&lt;/h2&gt;

&lt;p&gt;With everything bucketed, work in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bucket E on important pages.&lt;/strong&gt; Broken pages that should be working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bucket D.&lt;/strong&gt; Your own links pointing at 404s, starting with sitewide elements (menus, footers), because one fix there repairs hundreds of instances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bucket A, ordered by value.&lt;/strong&gt; If you can see which moved URLs had traffic (in analytics or Search Console's performance report) or backlinks (in any backlink tool), redirect those first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bucket C typos with strong backlinks.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Everything else in B and C: nothing to do.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On a typical site, steps 1 and 2 involve a fairly short list and do most of the good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Validate and wait
&lt;/h2&gt;

&lt;p&gt;After fixing, you can use &lt;strong&gt;Validate fix&lt;/strong&gt; on the Not found (404) row in Search Console. Expect it to take a while. Google rechecks on its own schedule, and the help docs say Googlebot keeps trying removed URLs "for some period of time." URLs you intentionally left as 404s will stay in the report for a while, and that's fine.&lt;/p&gt;

&lt;p&gt;Don't judge your work by whether the 404 count goes down. Judge it by whether the URLs in buckets A, D and E now resolve correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Search Console alone isn't enough
&lt;/h2&gt;

&lt;p&gt;Search Console is a good &lt;em&gt;lagging&lt;/em&gt; indicator, but it has gaps for this job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's slow.&lt;/strong&gt; URLs show up after Googlebot crawls them, which can be days or weeks after a link broke.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a sample.&lt;/strong&gt; The export won't necessarily include every affected URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It only sees what Google crawls.&lt;/strong&gt; It won't show broken links in pages Google skips, in noindexed sections, or links to &lt;em&gt;external&lt;/em&gt; sites that have died. Your visitors still click those.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It mixes good 404s with bad ones.&lt;/strong&gt; You have to do the triage above every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A scheduled link crawler fills these gaps. It works from your side: it crawls your pages, checks every link it finds, and tells you which page each broken link sits on, usually within a day of the break. That turns Bucket D from detective work into a to-do list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stopping new 404s from building up
&lt;/h2&gt;

&lt;p&gt;A few habits keep the report small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redirect when you rename.&lt;/strong&gt; Make "add a 301" part of the process for changing any published URL. Most CMSs have redirect plugins or settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan after big changes.&lt;/strong&gt; Migrations, redesigns, permalink changes and bulk deletions all need a crawl before and after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paste full URLs.&lt;/strong&gt; Train editors to paste links with &lt;code&gt;https://&lt;/code&gt;, or use a CMS link picker for internal links.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor on a schedule.&lt;/strong&gt; Weekly for most sites, daily for stores and client sites you're paid to maintain, with alerts only for &lt;em&gt;new&lt;/em&gt; breakage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check your sitemap.&lt;/strong&gt; A sitemap that lists 404 URLs sends Google mixed signals. Regenerate it after deletions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;404s for pages that are really gone are normal. Google says so.&lt;/li&gt;
&lt;li&gt;Redirect moved pages to their closest equivalent with a 301.&lt;/li&gt;
&lt;li&gt;Fix your &lt;em&gt;own&lt;/em&gt; links that point to 404s at the source. This is the most commonly missed fix.&lt;/li&gt;
&lt;li&gt;Treat a 404 on a page that should exist as a bug, and fix it first.&lt;/li&gt;
&lt;li&gt;Ignore typos and junk URLs unless they carry real backlinks.&lt;/li&gt;
&lt;li&gt;Use a crawler that records the "found on" page, so you can see where the broken links come from without waiting for Google.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://deadlinkpatrol.com/blog/search-console-404-errors-fix-or-ignore" rel="noopener noreferrer"&gt;DeadLinkPatrol blog&lt;/a&gt;. We build DeadLinkPatrol, a scheduled broken-link checker. If you want to see what a live crawl finds on your own site, the &lt;a href="https://deadlinkpatrol.com/check" rel="noopener noreferrer"&gt;free checker&lt;/a&gt; scans up to 25 pages without signup.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was created with the help of AI and checked for accuracy before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>google</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
