<?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: Usama Ejaz</title>
    <description>The latest articles on DEV Community by Usama Ejaz (@usamaejaz).</description>
    <link>https://dev.to/usamaejaz</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%2F152224%2F70cc3870-b493-4156-bd10-fb81a929ba5f.jpg</url>
      <title>DEV Community: Usama Ejaz</title>
      <link>https://dev.to/usamaejaz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/usamaejaz"/>
    <language>en</language>
    <item>
      <title>Who Drafts, Who Reviews, Who Publishes: A Review Workflow for a Small Company Blog</title>
      <dc:creator>Usama Ejaz</dc:creator>
      <pubDate>Fri, 30 Oct 2026 05:51:35 +0000</pubDate>
      <link>https://dev.to/usamaejaz/who-drafts-who-reviews-who-publishes-a-review-workflow-for-a-small-company-blog-20gd</link>
      <guid>https://dev.to/usamaejaz/who-drafts-who-reviews-who-publishes-a-review-workflow-for-a-small-company-blog-20gd</guid>
      <description>&lt;p&gt;A company blog with one writer has no process. The same person writes, rereads and hits publish, and nobody notices that three different jobs just happened.&lt;/p&gt;

&lt;p&gt;The second writer is where it breaks. Drafts sit in a shared doc waiting for the founder... or someone publishes without a second read and a customer spots the wrong price before anyone on the team does.&lt;/p&gt;

&lt;p&gt;I think the fix is smaller than most teams make it. One named person per job, one login per person, and one review pass that ends in a clear yes or a written list of changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three jobs, not three people
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Writer.&lt;/strong&gt; Drafts the post, adds images, and says when it's ready. Usually a marketer, a founder, or whoever is closest to the subject, like a support lead writing up a question customers keep asking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reviewer.&lt;/strong&gt; Reads the draft before anyone outside the company does. The job is catching what the writer can't see: a claim that isn't true any more, a customer named without permission, a paragraph that only makes sense to the person who wrote it. The reviewer approves or sends it back with a comment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Publisher.&lt;/strong&gt; Owns the public details and the timing: URL, search title and description, featured image, tags, publish date. On a small team the reviewer usually does this too. It still helps to keep it separate in your head, because "approved" and "live" are different moments.&lt;/p&gt;

&lt;p&gt;The one rule that holds it together: the writer shouldn't be the only person who reads a post before it's public. Two people can cover all three jobs as long as that holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  How small teams usually split it
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Team&lt;/th&gt;
&lt;th&gt;Writes&lt;/th&gt;
&lt;th&gt;Reviews&lt;/th&gt;
&lt;th&gt;Publishes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Founder and one marketer&lt;/td&gt;
&lt;td&gt;Marketer&lt;/td&gt;
&lt;td&gt;Founder&lt;/td&gt;
&lt;td&gt;Marketer, after approval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marketer plus subject experts (engineering, support, sales)&lt;/td&gt;
&lt;td&gt;Experts draft, marketer edits&lt;/td&gt;
&lt;td&gt;Marketer, plus the expert for accuracy on technical posts&lt;/td&gt;
&lt;td&gt;Marketer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agency or freelancer writing for the company&lt;/td&gt;
&lt;td&gt;Agency&lt;/td&gt;
&lt;td&gt;Someone at the company&lt;/td&gt;
&lt;td&gt;Someone at the company&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Founder writes, team member checks&lt;/td&gt;
&lt;td&gt;Founder&lt;/td&gt;
&lt;td&gt;Whoever knows the topic best&lt;/td&gt;
&lt;td&gt;Founder&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The agency row is the one to get right first. If an outside writer can publish directly, there's no checkpoint between a contractor's draft and your own domain. Give them access to write and submit, and keep approval and publishing inside the company.&lt;/p&gt;

&lt;h2&gt;
  
  
  One post, start to finish
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Draft where it will be published.&lt;/strong&gt; A draft written in a separate doc has to be pasted in later, and that's where formatting breaks, images go missing and the live version drifts from the approved one. Most blog platforms keep drafts private until publish, so there's no risk in starting there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Submit with a note.&lt;/strong&gt; "Pricing figures checked against the pricing page on Tuesday" or "not sure about the second section" tells the reviewer where to spend their time. It takes ten seconds and saves a full reread.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Approve, or request changes in writing.&lt;/strong&gt; No "let's discuss". If the reviewer wants changes, they write them down next to the post. The writer gets a list instead of a meeting, and the reasons stay recorded. The writer edits and submits again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Check the public details, then publish or schedule.&lt;/strong&gt; Approval clears the words. Before it goes out, whoever publishes checks what readers and search engines see first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slug.&lt;/strong&gt; Short and permanent. Changing it after publishing means a redirect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO title and description.&lt;/strong&gt; The description is what shows under the link in search results and chat previews.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Featured image.&lt;/strong&gt; It becomes the preview when the post is shared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tags or categories.&lt;/strong&gt; Use ones that already exist. A new tag for one post is a dead end for readers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the writer and publisher are in different time zones, check which zone the scheduler uses. "Tuesday 9 AM" is not the same moment for both of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edits to live posts go through review too
&lt;/h2&gt;

&lt;p&gt;A price goes up, a feature gets renamed, a screenshot goes stale. Treat these edits like drafts. A "quick fix" on a live post is exactly how an unreviewed rewrite ends up on the company domain. If your platform keeps revision history, you also get a record of what changed and when.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the reviewer actually checks
&lt;/h2&gt;

&lt;p&gt;A review that tries to check everything turns into a rewrite, and the writer stops submitting. These are the things a second reader catches that the writer usually can't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Facts that change.&lt;/strong&gt; Prices, plan limits, feature names, dates, anything phrased as "currently". These are the claims that cost you a support ticket when they're wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Names and permissions.&lt;/strong&gt; Any customer, partner or employee named or quoted, and whether they agreed to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promises.&lt;/strong&gt; Anything that reads as a commitment: delivery times, guarantees, "coming soon".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reader's next step.&lt;/strong&gt; Whether someone who reaches the end knows where to go next, and whether that link works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clarity for an outsider.&lt;/strong&gt; Internal shorthand, project codenames, steps the writer skipped because they know them by heart.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tone, word choice and structure are the writer's call unless something is actually wrong. "I would have written it differently" isn't a reason to send it back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate logins, not a shared one
&lt;/h2&gt;

&lt;p&gt;Every person who writes or reviews should have their own account on the blog. With one shared login you can't tell who changed a live post, and removing one person means changing the password for everyone. It also lets you match permissions to the table above: writers draft and submit, approval and publishing stay with the people who answer for what appears on the domain.&lt;/p&gt;

&lt;p&gt;The same applies if you draft with an AI assistant connected to the blog. Connect it with a writer's permissions, not an admin's, so it can draft and submit but can't approve its own work.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you don't need any of this
&lt;/h2&gt;

&lt;p&gt;If one person writes and publishes everything, the review step is that person rereading the post the next morning. If a colleague only reads a post now and then, pasting it into a shared doc for comments is fine. The setup above starts paying off when a second person needs to draft or publish on their own, or when you stop being sure who changed a live post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the queue moving
&lt;/h2&gt;

&lt;p&gt;The most common failure isn't a bad review. It's a draft waiting for review with nobody looking at it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agree on a turnaround&lt;/strong&gt; the reviewer can actually keep, like two working days, and say it out loud. A writer who knows when they'll hear back keeps writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review in batches, publish on a schedule.&lt;/strong&gt; Approve three posts in one sitting, then schedule them a week apart. Writing, review and publishing run on their own calendars, and the blog keeps publishing through a launch or a holiday.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I run &lt;a href="https://justblogged.com" rel="noopener noreferrer"&gt;JustBlogged&lt;/a&gt;, a blog platform, and this is the flow we built its team reviews around. It works the same on any platform that gives each person their own login&lt;/p&gt;

</description>
      <category>blogging</category>
      <category>writing</category>
      <category>productivity</category>
      <category>startup</category>
    </item>
    <item>
      <title>Your Hosted Blog Got an F on a Security Headers Scan: What a Meta Tag Can Actually Fix</title>
      <dc:creator>Usama Ejaz</dc:creator>
      <pubDate>Mon, 26 Oct 2026 05:45:03 +0000</pubDate>
      <link>https://dev.to/usamaejaz/your-hosted-blog-got-an-f-on-a-security-headers-scan-what-a-meta-tag-can-actually-fix-56c</link>
      <guid>https://dev.to/usamaejaz/your-hosted-blog-got-an-f-on-a-security-headers-scan-what-a-meta-tag-can-actually-fix-56c</guid>
      <description>&lt;p&gt;Most of the points a security headers scanner takes off a hosted blog are for HTTP response headers. Those come from the host. A theme only controls HTML, and only two of these policies work from HTML at all: a referrer policy via &lt;code&gt;&amp;lt;meta name="referrer"&amp;gt;&lt;/code&gt; and a Content Security Policy via &lt;code&gt;&amp;lt;meta http-equiv="Content-Security-Policy"&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;HSTS, X-Frame-Options and X-Content-Type-Options only work as real headers. Browsers ignore them in a meta tag.&lt;/p&gt;

&lt;p&gt;I run a blog host (&lt;a href="https://justblogged.com" rel="noopener noreferrer"&gt;JustBlogged&lt;/a&gt;), so people send me these reports and ask which lines they can fix themselves. On October 24, 2026 I sat down and actually checked, with the scanner and with a real browser. The part I didnt expect... MDN's HTTP Observatory gives you credit for a &lt;code&gt;frame-ancestors&lt;/code&gt; directive in a meta tag, which browsers ignore. Your grade goes up and nothing about framing changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Observatory scores
&lt;/h2&gt;

&lt;p&gt;The Observatory's scoring is public and its source is on GitHub. Every site starts at 100 and loses points per failed test. Bonuses only apply if you're still at 90 or above after penalties. For a typical hosted blog that means the grade is just 100 minus penalties.&lt;/p&gt;

&lt;p&gt;I ran its public API against one blog per platform. Every scan hits the front page, so read these as platform defaults on that day:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Blog tested&lt;/th&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Grade&lt;/th&gt;
&lt;th&gt;Where the points went&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;demo.ghost.io&lt;/td&gt;
&lt;td&gt;Ghost(Pro), platform subdomain&lt;/td&gt;
&lt;td&gt;F (20)&lt;/td&gt;
&lt;td&gt;No CSP -25, no HSTS -20, no X-Frame-Options -20, no X-Content-Type-Options -5, referrer policy -5, SRI -5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="http://www.404media.co" rel="noopener noreferrer"&gt;www.404media.co&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Ghost(Pro), custom domain&lt;/td&gt;
&lt;td&gt;F (20)&lt;/td&gt;
&lt;td&gt;Same six deductions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;en.blog.wordpress.com&lt;/td&gt;
&lt;td&gt;WordPress.com&lt;/td&gt;
&lt;td&gt;F (20)&lt;/td&gt;
&lt;td&gt;No CSP -25, SRI -50, no X-Content-Type-Options -5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;octocat.github.io&lt;/td&gt;
&lt;td&gt;GitHub Pages&lt;/td&gt;
&lt;td&gt;F (10)&lt;/td&gt;
&lt;td&gt;No CSP -25, no HTTPS redirect -20, no HSTS -20, no X-Frame-Options -20, no X-Content-Type-Options -5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jamstack.org&lt;/td&gt;
&lt;td&gt;Netlify&lt;/td&gt;
&lt;td&gt;C- (45)&lt;/td&gt;
&lt;td&gt;No CSP -25, no X-Frame-Options -20, no X-Content-Type-Options -5, SRI -5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;townhall.hashnode.com&lt;/td&gt;
&lt;td&gt;Hashnode&lt;/td&gt;
&lt;td&gt;C (50)&lt;/td&gt;
&lt;td&gt;No CSP -25, no X-Frame-Options -20, no X-Content-Type-Options -5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;herman.bearblog.dev&lt;/td&gt;
&lt;td&gt;Bear Blog&lt;/td&gt;
&lt;td&gt;C (50)&lt;/td&gt;
&lt;td&gt;No CSP -25, invalid X-Frame-Options -20, referrer policy -5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;on.substack.com&lt;/td&gt;
&lt;td&gt;Substack&lt;/td&gt;
&lt;td&gt;B (70)&lt;/td&gt;
&lt;td&gt;CSP allows unsafe inline -20, no X-Content-Type-Options -5, SRI -5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few rows are worth reading before you try to fix anything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Neither Ghost(Pro) blog sent a single security header, subdomain or custom domain. Ghost's head output includes &lt;code&gt;&amp;lt;meta name="referrer" content="no-referrer-when-downgrade"&amp;gt;&lt;/code&gt;, which sends the full page URL to other HTTPS sites. The browser default is &lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt;, so that tag is less private than sending nothing. Thats the -5.&lt;/li&gt;
&lt;li&gt;WordPress.com's -50 is from scripts loaded with protocol-relative URLs like &lt;code&gt;//stats.wp.com/w.js&lt;/code&gt;. On an HTTPS page those still load over HTTPS, but the rule penalizes any external script without an explicit &lt;code&gt;https:&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The GitHub Pages site didnt redirect HTTP to HTTPS. Thats the "Enforce HTTPS" checkbox in the repo settings, so ticking it removes that -20.&lt;/li&gt;
&lt;li&gt;Bear Blog sends &lt;code&gt;X-Frame-Options: ALLOWALL&lt;/code&gt;, which isnt a defined value. The scanner calls it invalid, and in my Chromium test a page with that header framed exactly like a page with no header.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which deductions a theme can remove
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Typical penalty&lt;/th&gt;
&lt;th&gt;Works from a meta tag?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Content Security Policy&lt;/td&gt;
&lt;td&gt;-25 if missing&lt;/td&gt;
&lt;td&gt;Yes, with limits. The CSP spec excludes &lt;code&gt;frame-ancestors&lt;/code&gt;, &lt;code&gt;report-uri&lt;/code&gt; and &lt;code&gt;sandbox&lt;/code&gt; from meta delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Referrer Policy&lt;/td&gt;
&lt;td&gt;-5 if unsafe&lt;/td&gt;
&lt;td&gt;Yes, via &lt;code&gt;&amp;lt;meta name="referrer"&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strict-Transport-Security&lt;/td&gt;
&lt;td&gt;-20 if missing&lt;/td&gt;
&lt;td&gt;No. RFC 6797 section 8.5 tells browsers not to heed it in a meta element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X-Frame-Options&lt;/td&gt;
&lt;td&gt;-20 if missing&lt;/td&gt;
&lt;td&gt;No. Setting it in a meta tag has no effect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X-Content-Type-Options&lt;/td&gt;
&lt;td&gt;-5 if missing&lt;/td&gt;
&lt;td&gt;No. Header only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP to HTTPS redirect&lt;/td&gt;
&lt;td&gt;-20 if missing&lt;/td&gt;
&lt;td&gt;No. Host or server setting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subresource Integrity&lt;/td&gt;
&lt;td&gt;-5 to -50&lt;/td&gt;
&lt;td&gt;Only for scripts your theme adds, not ones the platform injects&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So on a host that doesnt let you set headers, a theme can win back at most the CSP and referrer points. Thats 30 of the 80 Ghost(Pro) lost. The other 50 have to come from the host.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Chromium actually does with each meta tag
&lt;/h2&gt;

&lt;p&gt;I ran headless Chromium 153.0.8010.12 through Playwright 1.63.0, served test pages from one local origin and framed them from another. Each protection tried once as a header and once as a meta tag:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Page sent&lt;/th&gt;
&lt;th&gt;Loaded in a cross-origin iframe?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No header, no meta tag&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Content-Security-Policy: frame-ancestors 'none'&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;No, blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same policy in a meta tag&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;X-Frame-Options: DENY&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;No, blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;meta http-equiv="X-Frame-Options" content="DENY"&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;X-Frame-Options: ALLOWALL&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Chromium even tells you in the console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The Content Security Policy directive 'frame-ancestors' is ignored when delivered via a &amp;lt;meta&amp;gt; element.
X-Frame-Options may only be set via an HTTP header sent along with a document.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The meta tags that are supposed to work did. &lt;code&gt;&amp;lt;meta name="referrer" content="no-referrer"&amp;gt;&lt;/code&gt; removed the Referer header from an image request, and with two referrer meta tags on a page the second one won.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scanner and the browser disagree
&lt;/h2&gt;

&lt;p&gt;The Observatory doesnt treat the ignored &lt;code&gt;frame-ancestors&lt;/code&gt; tag the way the browser does. Its HTML parser reads CSP meta tags, and the X-Frame-Options test passes whenever the merged policy contains &lt;code&gt;frame-ancestors&lt;/code&gt;, without checking where it came from.&lt;/p&gt;

&lt;p&gt;I ran the analyzer's test functions locally against a page whose only protection was a meta CSP with &lt;code&gt;frame-ancestors 'none'&lt;/code&gt;. The X-Frame-Options result came back "implemented via CSP" at +5 instead of -20. Thats 25 points for a tag Chromium ignores.&lt;/p&gt;

&lt;p&gt;I'd leave it out. It makes the report say your pages cant be framed when they still can.&lt;/p&gt;

&lt;h2&gt;
  
  
  A meta CSP on a platform that injects its own scripts
&lt;/h2&gt;

&lt;p&gt;The CSP deduction only fully clears when &lt;code&gt;script-src&lt;/code&gt; blocks inline scripts. In the analyzer, a policy allowing inline scripts still scored -20 and one limiting scripts to &lt;code&gt;'self'&lt;/code&gt; moved to +5.&lt;/p&gt;

&lt;p&gt;On a hosted blog there are inline scripts whether you added them or not. I took a real post on a blog I control, inserted each candidate policy right after &lt;code&gt;&amp;lt;meta charset&amp;gt;&lt;/code&gt;, and counted CSP errors in the console:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy inserted&lt;/th&gt;
&lt;th&gt;CSP errors&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;upgrade-insecure-requests; object-src 'none'; base-uri 'self'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;default-src 'self'; script-src 'self'&lt;/code&gt; plus font, image and style sources&lt;/td&gt;
&lt;td&gt;7: three inline scripts, three inline styles, Cloudflare's analytics beacon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same, with &lt;code&gt;'unsafe-inline'&lt;/code&gt; added to &lt;code&gt;style-src&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;4: three inline scripts and the beacon&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of those inline scripts were the blog's own site-level custom code. I could allow them with hashes, but then every edit means updating the hashes.&lt;/p&gt;

&lt;p&gt;The third cant be allowed that way at all. Cloudflare's bot detection injects it, it carries a request ID and a timestamp, and its hash changed between two consecutive page loads. Cloudflare's JavaScript detections docs say it copies nonces from a CSP response header into the scripts it injects, and that nonces set through meta tags arent supported. So any blog behind Cloudflare with that feature on hits the same wall. Platform scripts for members, search or analytics are a smaller version of the same problem, since their hashes change whenever the platform updates them.&lt;/p&gt;

&lt;p&gt;The policy I'd actually put on a hosted blog is the one with zero errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;http-equiv=&lt;/span&gt;&lt;span class="s"&gt;"Content-Security-Policy"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"upgrade-insecure-requests; object-src 'none'; base-uri 'self'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"referrer"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"strict-origin-when-cross-origin"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It upgrades leftover &lt;code&gt;http://&lt;/code&gt; subresources, blocks plugin content, and stops injected markup from changing the base URL for relative links. The scanner still takes 20 points for it because scripts arent restricted. I think thats the right trade on a blog you dont fully control... none of it can break the page.&lt;/p&gt;

&lt;p&gt;Put the CSP tag as high in &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; as you can. The spec says a meta policy doesnt apply to content before it, and in my test an inline script above the tag ran while the same script below it was blocked.&lt;/p&gt;

&lt;p&gt;The referrer tag matches the browser default, so on most platforms it changes nothing. On Ghost it overrides the looser platform tag, as long as yours comes later in the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where those tags go
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ghost&lt;/strong&gt;: Settings &amp;gt; Code Injection &amp;gt; Site header. Ghost writes its own referrer tag near the top of its head output, so view source after saving and check yours comes after it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netlify and Cloudflare Pages&lt;/strong&gt;: both read a &lt;code&gt;_headers&lt;/code&gt; file from the publish directory. These are real headers, so X-Frame-Options, &lt;code&gt;frame-ancestors&lt;/code&gt; and X-Content-Type-Options all work there. Cloudflare notes the file doesnt apply to responses from Pages Functions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Pages&lt;/strong&gt;: no custom headers and no headers file. Use the meta tags and tick "Enforce HTTPS".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted WordPress&lt;/strong&gt;: you control the server, so set real headers there. Core's &lt;code&gt;send_frame_options_header()&lt;/code&gt; sends X-Frame-Options and &lt;code&gt;frame-ancestors 'self'&lt;/code&gt;, but it's hooked to the login and admin screens, not public posts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caution even where you can set headers: HSTS with &lt;code&gt;includeSubDomains&lt;/code&gt; and &lt;code&gt;preload&lt;/code&gt; is hard to undo. Dont add &lt;code&gt;preload&lt;/code&gt; on a whim.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the grade is worth a host change
&lt;/h2&gt;

&lt;p&gt;For a blog that just publishes posts, I think the protections that matter most to readers are the HTTPS redirect and HSTS. Both come from the host. If yours sends neither, thats worth raising with them.&lt;/p&gt;

&lt;p&gt;CSP, X-Frame-Options and SRI deductions mostly measure how much of the response the host lets you control. Chasing them where you cant set headers ends in either a policy that breaks platform scripts or a meta tag that only fools the scanner.&lt;/p&gt;

&lt;p&gt;If a specific grade is a hard requirement, like a client security review, you need a host where you write the headers yourself: Netlify or Cloudflare Pages with &lt;code&gt;_headers&lt;/code&gt;, or your own server. And then check it in the Observatory and in the browser console, because as above, the two dont always agree&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>blogging</category>
      <category>csp</category>
    </item>
    <item>
      <title>Post Dates Off by One Day After a WordPress Migration: Jekyll, Hugo and Eleventy Tested</title>
      <dc:creator>Usama Ejaz</dc:creator>
      <pubDate>Fri, 23 Oct 2026 05:49:44 +0000</pubDate>
      <link>https://dev.to/usamaejaz/post-dates-off-by-one-day-after-a-wordpress-migration-jekyll-hugo-and-eleventy-tested-pp2</link>
      <guid>https://dev.to/usamaejaz/post-dates-off-by-one-day-after-a-wordpress-migration-jekyll-hugo-and-eleventy-tested-pp2</guid>
      <description>&lt;p&gt;A WordPress post at &lt;code&gt;/2023/04/05/late-post/&lt;/code&gt; shows up at &lt;code&gt;/2023/04/06/late-post/&lt;/code&gt; on the new static site. The timestamp is fine. A tool along the way read the UTC version of the publish time, or converted it into the build machine's zone, and the calendar day moved.&lt;/p&gt;

&lt;p&gt;It only hits posts published close to midnight, so checking your three newest posts proves very little.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://justblogged.com" rel="noopener noreferrer"&gt;JustBlogged&lt;/a&gt;, the blog platform I run, keeps dates out of post URLs, so I'd never seen this break myself. On October 23, 2026 I pushed one WordPress post through the usual static route: a WXR export, the &lt;code&gt;wordpress-export-to-markdown&lt;/code&gt; converter, then Jekyll, Hugo and Eleventy. Each one built with the machine clock set to Los Angeles, UTC and Karachi.&lt;/p&gt;

&lt;p&gt;Short version... the day moved at the converter on its defaults, in Jekyll on any machine not set to the post's zone, and in Eleventy on any machine for date-only posts. Hugo kept the day it was given.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which WordPress date your old URLs came from
&lt;/h2&gt;

&lt;p&gt;WordPress stores two timestamps per post: &lt;code&gt;post_date&lt;/code&gt; in the site's own time zone and &lt;code&gt;post_date_gmt&lt;/code&gt; in UTC. Dated permalinks use the first one. &lt;code&gt;get_permalink()&lt;/code&gt; splits &lt;code&gt;post_date&lt;/code&gt; into year, month and day, and the source comment says to read it as local time whatever the server's PHP zone is.&lt;/p&gt;

&lt;p&gt;The export carries the same moment three ways. For a post published at 11:30pm on April 5 on a site set to Los Angeles (UTC-7 in April):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;pubDate&amp;gt;&lt;/span&gt;Thu, 06 Apr 2023 06:30:00 +0000&lt;span class="nt"&gt;&amp;lt;/pubDate&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;wp:post_date&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;![CDATA[2023-04-05 23:30:00]]&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/wp:post_date&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;wp:post_date_gmt&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;![CDATA[2023-04-06 06:30:00]]&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/wp:post_date_gmt&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only &lt;code&gt;wp:post_date&lt;/code&gt; has the day that's in the live URL. Anything that reads &lt;code&gt;pubDate&lt;/code&gt; or &lt;code&gt;post_date_gmt&lt;/code&gt; and writes the date in UTC gives this post April 6.&lt;/p&gt;

&lt;h2&gt;
  
  
  The converter writes UTC dates unless you pass --timezone
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;wordpress-export-to-markdown&lt;/code&gt; 3.0.6 reads &lt;code&gt;pubDate&lt;/code&gt; and applies &lt;code&gt;--timezone&lt;/code&gt;, which defaults to &lt;code&gt;utc&lt;/code&gt;. It also drops the time unless you pass &lt;code&gt;--include-time&lt;/code&gt;. Same export, three runs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flags&lt;/th&gt;
&lt;th&gt;File written&lt;/th&gt;
&lt;th&gt;Front matter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Defaults&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2023-04-06-late-post.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;date: 2023-04-06&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--timezone=America/Los_Angeles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2023-04-05-late-post.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;date: 2023-04-05&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--timezone=America/Los_Angeles --include-time=true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2023-04-05-late-post.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;date: 2023-04-05T23:30:00.000-07:00&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use the zone from WordPress Settings &amp;gt; General as an IANA name (&lt;code&gt;America/Los_Angeles&lt;/code&gt;, &lt;code&gt;Asia/Karachi&lt;/code&gt;) and pass &lt;code&gt;--include-time=true&lt;/code&gt;. The full timestamp with its offset is the only form all three generators below read back to the right day, and it keeps the time for your feed and &lt;code&gt;datePublished&lt;/code&gt; markup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same date, three generators
&lt;/h2&gt;

&lt;p&gt;I built two posts in each generator with a &lt;code&gt;/:year/:month/:day/:slug/&lt;/code&gt; style URL, using Jekyll 4.4.1, Hugo 0.168.0 and Eleventy 3.1.7. One post had &lt;code&gt;date: 2023-04-05 23:30:00 -0700&lt;/code&gt;. The other had only a date (the &lt;code&gt;2023-04-05-&lt;/code&gt; filename prefix in Jekyll, &lt;code&gt;date: 2023-04-05&lt;/code&gt; in Hugo and Eleventy). Eleventy has no built-in date permalink, so its URL came from a directory data file using &lt;code&gt;getFullYear()&lt;/code&gt;, &lt;code&gt;getMonth()&lt;/code&gt; and &lt;code&gt;getDate()&lt;/code&gt;. No config set a time zone. Each cell is the day that ended up in the URL.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Generator&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Built in Los Angeles&lt;/th&gt;
&lt;th&gt;Built in UTC&lt;/th&gt;
&lt;th&gt;Built in Karachi&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jekyll 4.4.1&lt;/td&gt;
&lt;td&gt;23:30 -07:00&lt;/td&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;06&lt;/td&gt;
&lt;td&gt;06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jekyll 4.4.1&lt;/td&gt;
&lt;td&gt;Date only&lt;/td&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hugo 0.168.0&lt;/td&gt;
&lt;td&gt;23:30 -07:00&lt;/td&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hugo 0.168.0&lt;/td&gt;
&lt;td&gt;Date only&lt;/td&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eleventy 3.1.7&lt;/td&gt;
&lt;td&gt;23:30 -07:00&lt;/td&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;06&lt;/td&gt;
&lt;td&gt;06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eleventy 3.1.7&lt;/td&gt;
&lt;td&gt;Date only&lt;/td&gt;
&lt;td&gt;04&lt;/td&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Jekyll keeps the moment but converts it to the build machine's zone before filling in the URL, so the offset in your front matter doesn't protect the day. A laptop in Los Angeles and a CI runner on UTC produce different URLs from the same repo. The Jekyll configuration docs say the default zone comes from the OS locally and from the server's setting on a remote host.&lt;/p&gt;

&lt;p&gt;Hugo kept the written offset in every build. Its &lt;code&gt;timeZone&lt;/code&gt; setting only decides how to read dates without an offset, and with it unset those came out as UTC. The written day stays in the URL, though templates that print the time will label it UTC.&lt;/p&gt;

&lt;p&gt;Eleventy turns every front matter date into a UTC instant. A date with no time becomes midnight UTC, which is still the previous evening anywhere west of London, so local getters on a machine in the Americas put the post on the 4th. The Eleventy dates page lists this as a common pitfall for displaying dates. It hits permalinks built from those dates too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pinning each one
&lt;/h2&gt;

&lt;p&gt;Jekyll, in &lt;code&gt;_config.yml&lt;/code&gt;. With this line the post went back to &lt;code&gt;/2023/04/05/&lt;/code&gt; on the UTC build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;America/Los_Angeles&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hugo, in &lt;code&gt;hugo.toml&lt;/code&gt;, so offset-less dates are read the way WordPress meant them. My dated URLs were already right without it and stayed right with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;timeZone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"America/Los_Angeles"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eleventy, build the URL in a named zone instead of the machine's. Eleventy depends on Luxon, but add it to your own &lt;code&gt;package.json&lt;/code&gt; before requiring it. With the converter's full timestamps, this gave &lt;code&gt;/2023/04/05/late-post/&lt;/code&gt; on all three machines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// posts/posts.11tydata.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DateTime&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;luxon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;permalink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromJSDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;America/Los_Angeles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy/LL/dd&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fileSlug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That file needs posts to have a time. On date-only posts it shifts midnight UTC back into Los Angeles time and moves them to the 4th again. If some posts have to stay date-only, build their URL with &lt;code&gt;getUTCFullYear()&lt;/code&gt;, &lt;code&gt;getUTCMonth()&lt;/code&gt; and &lt;code&gt;getUTCDate()&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Listing the posts that moved
&lt;/h2&gt;

&lt;p&gt;This compares the local day in the WordPress export with the day in the new site's sitemap and prints posts that moved or went missing. It assumes the new URLs keep a &lt;code&gt;/YYYY/MM/DD/&lt;/code&gt; segment and end with the WordPress slug.&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="c1"&gt;# date_drift.py: python3 date_drift.py export.xml _site/sitemap.xml
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;etree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ElementTree&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;

&lt;span class="n"&gt;WP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{http://wordpress.org/export/1.2/}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;DAY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/(\d{4})/(\d{2})/(\d{2})/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;old_days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wxr&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wxr&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findtext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WP&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;post_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;post&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findtext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WP&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publish&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;local_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findtext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WP&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;post_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# what WordPress put in the URL
&lt;/span&gt;        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findtext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WP&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;post_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local_day&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new_days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;loc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;loc&amp;gt;(.*?)&amp;lt;/loc&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;removesuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;

&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;old_days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="nf"&gt;new_days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;moved    &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&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;Against the unpinned Jekyll build on UTC it printed &lt;code&gt;moved    late-post: 2023-04-05 -&amp;gt; 2023-04-06&lt;/code&gt;. After adding &lt;code&gt;timezone&lt;/code&gt; it printed nothing. It needs Python 3.9+ and only reads published posts, so drafts and pages don't add noise.&lt;/p&gt;

&lt;p&gt;If a post can't go back to its old day, because the new URLs are already indexed or a template depends on the UTC date, redirect each old dated path to the new one wherever your host keeps redirects.&lt;/p&gt;

&lt;p&gt;Jekyll and Eleventy can bring the shift back when you change build machines, with zero content changes, so I'd run the script again after any CI move. Picking a URL pattern without the date avoids the whole thing, but most people moving off WordPress don't get to make that choice&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>jekyll</category>
      <category>hugo</category>
      <category>eleventy</category>
    </item>
    <item>
      <title>Heading Anchors After a Blog Migration: 12 Headings Through 9 Markdown Renderers</title>
      <dc:creator>Usama Ejaz</dc:creator>
      <pubDate>Thu, 22 Oct 2026 05:44:59 +0000</pubDate>
      <link>https://dev.to/usamaejaz/heading-anchors-after-a-blog-migration-12-headings-through-9-markdown-renderers-448p</link>
      <guid>https://dev.to/usamaejaz/heading-anchors-after-a-blog-migration-12-headings-through-9-markdown-renderers-448p</guid>
      <description>&lt;p&gt;A 301 keeps the &lt;code&gt;#section&lt;/code&gt; part of a link. Your new page usually doesn't.&lt;/p&gt;

&lt;p&gt;When someone opens &lt;code&gt;/old-post/#step-3&lt;/code&gt; and the server answers with a 301 to &lt;code&gt;/new-post/&lt;/code&gt;, the browser carries &lt;code&gt;#step-3&lt;/code&gt; over to the new address, per &lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-10.2.2" rel="noopener noreferrer"&gt;RFC 9110 section 10.2.2&lt;/a&gt;. The reader lands on the right section only if the new page has an element with exactly that ID. The server can't fix it either... the fragment is never sent in the request (&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-17.11" rel="noopener noreferrer"&gt;RFC 9110 section 17.11&lt;/a&gt;), so no redirect rule can rename it.&lt;/p&gt;

&lt;p&gt;So it comes down to how the old and new platforms turn heading text into an &lt;code&gt;id&lt;/code&gt;. While writing migration docs for &lt;a href="https://justblogged.com" rel="noopener noreferrer"&gt;JustBlogged&lt;/a&gt;, the blog platform I run, I wanted actual answers instead of guesses. On October 22, 2026 I ran the same 12 headings through nine renderers.&lt;/p&gt;

&lt;p&gt;Jekyll, Hugo, Astro and pandoc's GFM mode gave identical IDs for all 12. Ghost, Python-Markdown (what MkDocs uses), pandoc's default Markdown, Eleventy's IdAttributePlugin and markdown-it-anchor's defaults each disagreed with them on several.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the browser does with #section after a redirect
&lt;/h2&gt;

&lt;p&gt;I served two test pages from a local Python server, with &lt;code&gt;/old/&lt;/code&gt; answering 301 to the new page, and opened &lt;code&gt;/old/#example-1&lt;/code&gt; in Chromium 141 through Playwright. The address bar ended on the new URL with &lt;code&gt;#example-1&lt;/code&gt; still attached, and the page scrolled to that heading.&lt;/p&gt;

&lt;p&gt;With a fragment the new page didn't have, the URL kept it and the page just stayed at the top. No error. That's why broken anchors go unnoticed after a migration: the page loads, the link checker sees a 200, and the reader scrolls around looking for the section.&lt;/p&gt;

&lt;p&gt;One browser rule helps when the old IDs were encoded. The HTML standard's &lt;a href="https://html.spec.whatwg.org/multipage/browsing-the-web.html#scrolling-to-a-fragment" rel="noopener noreferrer"&gt;fragment-scrolling steps&lt;/a&gt; look for an element whose ID equals the fragment as written, and if none exists, try again with the percent-decoded fragment. So &lt;code&gt;#caf%C3%A9-menus&lt;/code&gt; finds &lt;code&gt;id="café-menus"&lt;/code&gt;. That matters for Ghost, which writes percent-encoded IDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How nine renderers turned the same headings into IDs
&lt;/h2&gt;

&lt;p&gt;The test post had 12 H2 headings picked to hit the usual differences: punctuation, a straight and a curly apostrophe, accents, an emoji, Japanese text, a spaced hyphen, underscores, a code span and a repeated heading.&lt;/p&gt;

&lt;p&gt;Versions: Jekyll 4.4.1 (kramdown 2.5.2 with the GFM parser, Jekyll's default), Hugo 0.168.0 on defaults, Astro's Markdown pipeline (&lt;code&gt;@astrojs/markdown-remark&lt;/code&gt; 7.3.4), pandoc 3.1.3 in both &lt;code&gt;markdown&lt;/code&gt; and &lt;code&gt;gfm&lt;/code&gt; modes, Python-Markdown 3.11.2 with the &lt;code&gt;toc&lt;/code&gt; extension, markdown-it 15.0.3 with markdown-it-anchor 10.0.0 on defaults, Eleventy 3.1.7 with IdAttributePlugin on, and Ghost's &lt;code&gt;@tryghost/kg-lexical-html-renderer&lt;/code&gt; 1.4.5. The Ghost column comes from that renderer package run directly, not a live Ghost site.&lt;/p&gt;

&lt;p&gt;Two headings came out the same everywhere: &lt;code&gt;Using `noindex` on tag pages&lt;/code&gt; became &lt;code&gt;using-noindex-on-tag-pages&lt;/code&gt;, and the first of two "Example" headings became &lt;code&gt;example&lt;/code&gt;. The other ten split like this ("same" means identical to the first column):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Heading&lt;/th&gt;
&lt;th&gt;Jekyll, Hugo, Astro, pandoc gfm&lt;/th&gt;
&lt;th&gt;Ghost&lt;/th&gt;
&lt;th&gt;Python-Markdown&lt;/th&gt;
&lt;th&gt;Eleventy IdAttributePlugin&lt;/th&gt;
&lt;th&gt;markdown-it-anchor default&lt;/th&gt;
&lt;th&gt;pandoc markdown&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Set up DNS (A/AAAA records)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;set-up-dns-aaaaa-records&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;&lt;code&gt;set-up-dns-a-aaaa-records&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;set-up-dns-(a%2Faaaa-records)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What's new in v2.0?&lt;/td&gt;
&lt;td&gt;&lt;code&gt;whats-new-in-v20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;&lt;code&gt;whats-new-in-v2-0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;what's-new-in-v2.0%3F&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;whats-new-in-v2.0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ghost's 404 page (curly apostrophe)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ghosts-404-page&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ghost%E2%80%99s-404-page&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ghost-s-404-page&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ghost%E2%80%99s-404-page&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C++ &amp;amp; C#&lt;/td&gt;
&lt;td&gt;&lt;code&gt;c--c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;c-c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;c-c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;c-and-c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;c%2B%2B-%26-c%23&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;c-c&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Café menus, naïve UTF-8&lt;/td&gt;
&lt;td&gt;&lt;code&gt;café-menus-naïve-utf-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;caf%C3%A9-menus-na%C3%AFve-utf-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cafe-menus-naive-utf-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cafe-menus-naive-utf-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;caf%C3%A9-menus%2C-na%C3%AFve-utf-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(rocket emoji) Launch notes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-launch-notes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%F0%9F%9A%80-launch-notes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;launch-notes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;launch-notes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%F0%9F%9A%80-launch-notes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;launch-notes&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;日本語の見出し&lt;/td&gt;
&lt;td&gt;&lt;code&gt;日本語の見出し&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;percent-encoded form of the same text&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;empty &lt;code&gt;id=""&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;percent-encoded form of the same text&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Example (second time)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;example-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;&lt;code&gt;example_1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;example-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Step 1 - Install&lt;/td&gt;
&lt;td&gt;&lt;code&gt;step-1---install&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;step-1install&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;step-1-install&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;step-1-install&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;snake_case_names&lt;/td&gt;
&lt;td&gt;&lt;code&gt;snake_case_names&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;snakecasenames&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;&lt;code&gt;snake-case-names&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eleventy only adds heading IDs if you opt in to its &lt;a href="https://www.11ty.dev/docs/plugins/id-attribute/" rel="noopener noreferrer"&gt;IdAttributePlugin&lt;/a&gt;, which uses the built-in &lt;a href="https://www.11ty.dev/docs/filters/slugify/" rel="noopener noreferrer"&gt;slugify filter&lt;/a&gt; and &lt;code&gt;@sindresorhus/slugify&lt;/code&gt;. An Eleventy site that wired up markdown-it-anchor itself follows whatever slug function its config passes in, so check yours.&lt;/p&gt;

&lt;p&gt;Ghost's rules are short enough to read in its &lt;a href="https://cdn.jsdelivr.net/npm/@tryghost/kg-utils@1.1.8/build/esm/slugify.js" rel="noopener noreferrer"&gt;slugify source&lt;/a&gt;: remove ASCII punctuation including underscores, replace spaces with hyphens, delete any run of two or more hyphens outright, then percent-encode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which moves keep their anchors
&lt;/h2&gt;

&lt;p&gt;Jekyll, Hugo and Astro on default settings kept every anchor between each other, because all three follow GitHub's heading rules: &lt;a href="https://jekyllrb.com/docs/configuration/default/" rel="noopener noreferrer"&gt;Jekyll's default config&lt;/a&gt; sets kramdown's input to GFM, &lt;a href="https://gohugo.io/configuration/markup/" rel="noopener noreferrer"&gt;Hugo's&lt;/a&gt; &lt;code&gt;autoIDType&lt;/code&gt; defaults to &lt;code&gt;github&lt;/code&gt;, and &lt;a href="https://docs.astro.build/en/guides/markdown-content/" rel="noopener noreferrer"&gt;Astro&lt;/a&gt; uses &lt;code&gt;github-slugger&lt;/code&gt;. A Jekyll site on kramdown's plain parser, or a Hugo site set to &lt;code&gt;github-ascii&lt;/code&gt; or &lt;code&gt;blackfriday&lt;/code&gt;, is outside what I tested.&lt;/p&gt;

&lt;p&gt;Ghost to one of those is mixed. Plain letters, digits and spaces matched, and so did parentheses, a straight apostrophe and a question mark. Accents and Japanese text also worked in Chromium through the percent-decoding rule: &lt;code&gt;/old/#caf%C3%A9-menus-na%C3%AFve-utf-8&lt;/code&gt; landed on &lt;code&gt;id="café-menus-naïve-utf-8"&lt;/code&gt;. A curly apostrophe, an emoji, an underscore, a spaced hyphen or &lt;code&gt;&amp;amp;&lt;/code&gt; sent the reader to the top of the page.&lt;/p&gt;

&lt;p&gt;Python-Markdown and Eleventy's plugin transliterate accents to ASCII, so "Café menus" breaks in either direction between them and a GitHub-style generator. Python-Markdown gave the Japanese heading &lt;code&gt;_1&lt;/code&gt;, and Eleventy wrote an empty &lt;code&gt;id=""&lt;/code&gt; that no link can reach. Eleventy also numbers a repeated heading from 2, so the second "Example" became &lt;code&gt;example-2&lt;/code&gt; where everything except Python-Markdown wrote &lt;code&gt;example-1&lt;/code&gt;. markdown-it-anchor's default keeps punctuation and percent-encodes most of it, so almost any heading with punctuation changes when you leave it.&lt;/p&gt;

&lt;p&gt;WordPress core doesn't add heading IDs at all. Anchors on a WordPress post came from the heading block's HTML anchor field or a table-of-contents plugin, each with its own format. You can't predict those from the heading text, so read them out of the old pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  List the IDs your new pages are missing
&lt;/h2&gt;

&lt;p&gt;Run this before you change DNS, while the old site still answers. It reads a CSV of &lt;code&gt;old_url,new_url&lt;/code&gt; pairs (the redirect map you need anyway), fetches both pages, and prints every &lt;code&gt;id&lt;/code&gt; or &lt;code&gt;name&lt;/code&gt; on the old page with no match on the new one. A percent-decoded match counts as found, same as the browser. Standard library only.&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="c1"&gt;# Usage: python3 anchor_diff.py redirects.csv
# Each CSV row: old_url,new_url
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;html.parser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTMLParser&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unquote&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IDs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTMLParser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_starttag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ids_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anchor-diff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;IDs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])):&lt;/span&gt;
    &lt;span class="n"&gt;new_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ids_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;reachable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_ids&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;unquote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;new_ids&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ids_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reachable&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;unquote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reachable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s"&gt;missing on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="si"&gt;}&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;On my Ghost-rendered test page and its Astro-rendered replacement it printed five IDs: the curly apostrophe, the emoji, &lt;code&gt;c-c&lt;/code&gt;, &lt;code&gt;snakecasenames&lt;/code&gt; and &lt;code&gt;step-1install&lt;/code&gt;. The same five fragments that left Chromium at the top of the page. On a real site you'll also get theme IDs like navigation, comment and footnote anchors... skim past the ones nobody links to.&lt;/p&gt;

&lt;p&gt;Not every missing ID needs fixing. The ones worth keeping are linked from somewhere: your own posts (search your export for &lt;code&gt;href&lt;/code&gt; values containing &lt;code&gt;#&lt;/code&gt;), docs or forum threads pointing at a section, anything you shared yourself. A heading nobody linked to can take the new ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the old ID on the new page
&lt;/h2&gt;

&lt;p&gt;Two ways: set the heading's ID explicitly, or leave the new ID alone and put an empty element with the old ID right before the heading. I built both in Jekyll, Hugo, Astro and pandoc.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jekyll and pandoc&lt;/strong&gt; accept an explicit ID after the heading text: &lt;code&gt;## Step 1 - Install {#step-1install}&lt;/code&gt;. Both produced &lt;code&gt;&amp;lt;h2 id="step-1install"&amp;gt;&lt;/code&gt;, per kramdown's &lt;a href="https://kramdown.gettalong.org/syntax.html#specifying-a-header-id" rel="noopener noreferrer"&gt;header ID syntax&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hugo&lt;/strong&gt; accepted the same &lt;code&gt;{#step-1install}&lt;/code&gt; syntax with no config change. It dropped raw HTML from Markdown though, because &lt;code&gt;renderer.unsafe&lt;/code&gt; defaults to &lt;code&gt;false&lt;/code&gt;, so the empty-element alias won't survive unless you change that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Astro&lt;/strong&gt; didn't understand &lt;code&gt;{#...}&lt;/code&gt;. It slugged the braces into the ID and produced &lt;code&gt;step-1---install-step-1install&lt;/code&gt;. Writing the heading as raw HTML, &lt;code&gt;&amp;lt;h2 id="step-1install"&amp;gt;Step 1 - Install&amp;lt;/h2&amp;gt;&lt;/code&gt;, kept the ID, and so did an empty element before a Markdown heading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python-Markdown&lt;/strong&gt; can take an explicit ID through the attr_list extension, per its &lt;a href="https://python-markdown.github.io/extensions/toc/" rel="noopener noreferrer"&gt;toc documentation&lt;/a&gt;. I didn't build that case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The empty-element alias lets the heading keep its new clean ID while old links still land:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"ghost%E2%80%99s-404-page"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
## Ghost's 404 page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the old ID exactly as the script prints it, percent signs included. In Chromium that span caught both &lt;code&gt;#ghost%E2%80%99s-404-page&lt;/code&gt; and the typed curly-apostrophe version, because the browser encodes the typed character before matching.&lt;/p&gt;

&lt;p&gt;I still haven't tested Jekyll on kramdown's plain parser or Hugo's other &lt;code&gt;autoIDType&lt;/code&gt; values, so if you're on one of those, run the script before you trust the table&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>markdown</category>
      <category>seo</category>
      <category>blogging</category>
    </item>
    <item>
      <title>What a Password-Protected Blog Post Still Leaks: WordPress 7.1.3 and Hugo Tested</title>
      <dc:creator>Usama Ejaz</dc:creator>
      <pubDate>Wed, 21 Oct 2026 05:42:38 +0000</pubDate>
      <link>https://dev.to/usamaejaz/what-a-password-protected-blog-post-still-leaks-wordpress-713-and-hugo-tested-3c11</link>
      <guid>https://dev.to/usamaejaz/what-a-password-protected-blog-post-still-leaks-wordpress-713-and-hugo-tested-3c11</guid>
      <description>&lt;p&gt;A password on a WordPress post hides the body and the excerpt. That's it.&lt;/p&gt;

&lt;p&gt;The title, the URL, the date and the featured image are still public, and WordPress keeps listing the post in its sitemap, its RSS feed and its REST API. On a static site it's looser... the host's password covers the page, but the generator already copied the post's text into your feed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://justblogged.com" rel="noopener noreferrer"&gt;JustBlogged&lt;/a&gt;, the blog platform I run, doesn't have per-post passwords, so I wanted to see what the platforms that do actually hide. On October 21, 2026 I installed WordPress 7.1.3 locally with the default Twenty Twenty-Five theme, published one protected post with a featured image, and requested every public address that could mention it from a logged-out terminal. Then I did the same with a Hugo 0.168.0 build.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a logged-out visitor got from WordPress
&lt;/h2&gt;

&lt;p&gt;The post was titled "Client notes Q3". Its body and its hand-written excerpt each had a marker word in them, and it had an uploaded featured image. Neither marker word showed up anywhere. Everything else did:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Address&lt;/th&gt;
&lt;th&gt;What came back&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The post URL&lt;/td&gt;
&lt;td&gt;200, the heading "Protected: Client notes Q3", the password form, and the featured image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home page&lt;/td&gt;
&lt;td&gt;The post in the latest-posts list, with title and featured image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/feed/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An item titled "Protected: Client notes Q3" with link and date, the description "There is no excerpt because this is a protected post.", and the password form as content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/wp-sitemap-posts-post-1.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The post's URL, next to the public posts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/wp-json/wp/v2/posts/5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Title, link, date and &lt;code&gt;featured_media&lt;/code&gt; ID, with &lt;code&gt;content&lt;/code&gt; and &lt;code&gt;excerpt&lt;/code&gt; empty and &lt;code&gt;"protected": true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/wp-json/wp/v2/media?parent=5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The featured image's record and its direct file URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The image file in &lt;code&gt;/wp-content/uploads/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;200, no password asked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/client-notes/embed/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The prefixed title and the protected-excerpt sentence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Site search for a marker word&lt;/td&gt;
&lt;td&gt;No results&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This matches WordPress's own &lt;a href="https://wordpress.org/documentation/article/protect-posts-with-password/" rel="noopener noreferrer"&gt;password protection docs&lt;/a&gt;: the password replaces the content and excerpt and adds "Protected: " to the title. The same page warns that custom fields aren't covered, so a theme or plugin that prints post meta will print it on a protected post unless it checks &lt;code&gt;post_password_required()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Search coming back empty is on purpose. For logged-out visitors &lt;code&gt;WP_Query&lt;/code&gt; adds a &lt;code&gt;post_password = ''&lt;/code&gt; condition to every search.&lt;/p&gt;

&lt;p&gt;Two details for readers who do have the password:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The protected post was sent with &lt;code&gt;Cache-Control: no-cache, must-revalidate, max-age=0, no-store, private&lt;/code&gt;. A public post got no cache header at all. So a page cache that respects headers won't serve one reader's unlocked copy to the next person.&lt;/li&gt;
&lt;li&gt;The password lives in a &lt;code&gt;wp-postpass_&lt;/code&gt; cookie that lasts 10 days by default, changeable with the &lt;a href="https://developer.wordpress.org/reference/hooks/post_password_expires/" rel="noopener noreferrer"&gt;post_password_expires filter&lt;/a&gt;. WordPress only remembers one password at a time, so opening two posts with different passwords means typing the first one again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting the post out of the feed, sitemap and listings
&lt;/h2&gt;

&lt;p&gt;The WordPress docs include a snippet that removes protected posts from every query except their own page. I saved it as a must-use plugin at &lt;code&gt;wp-content/mu-plugins/hide-protected.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;exclude_protected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$where&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$where&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;" AND &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$wpdb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.post_password = '' "&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;exclude_protected_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nf"&gt;is_single&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nf"&gt;is_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nf"&gt;is_admin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'posts_where'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'exclude_protected'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'pre_get_posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'exclude_protected_action'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With it on, the post was gone from the home page, the feed, the sitemap and the REST API's post list. Its own URL still showed the password form.&lt;/p&gt;

&lt;p&gt;Two things didn't change. &lt;code&gt;/wp-json/wp/v2/posts/5&lt;/code&gt; still returned the title to anyone asking for that ID, and the media endpoint still listed the image with its file URL.&lt;/p&gt;

&lt;p&gt;The image is the part no query filter reaches. Files in &lt;code&gt;wp-content/uploads&lt;/code&gt; are normally served straight by the web server without WordPress running, so there's no password check, and the default theme draws the featured image on the password page itself. If a picture has to stay private I wouldn't make it the featured image or upload it to a public site's media library at all. Same with the title and slug... if the subject alone gives it away, the title has to be neutral before you publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static sites: the page is protected, the feed isn't
&lt;/h2&gt;

&lt;p&gt;A static site has no server code to check a password, so protection comes from the host. Netlify has a shared-password visibility setting on Pro and Enterprise according to its &lt;a href="https://www.netlify.com/knowledge-base/how-to-password-protect-a-netlify-site-or-deploy-previews/" rel="noopener noreferrer"&gt;knowledge base&lt;/a&gt; (as of October 21, 2026). &lt;a href="https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app/" rel="noopener noreferrer"&gt;Cloudflare Access&lt;/a&gt; can put a login in front of an app on a domain in your Cloudflare account. On your own server, basic auth does the same.&lt;/p&gt;

&lt;p&gt;All of these protect the paths you give them. The generator already wrote the post into files at other paths.&lt;/p&gt;

&lt;p&gt;In my Hugo build, with minimal templates and no theme, the default output put the post's text in both &lt;code&gt;/index.xml&lt;/code&gt; and &lt;code&gt;/posts/index.xml&lt;/code&gt;, and its URL in &lt;code&gt;/sitemap.xml&lt;/code&gt;. A password rule on &lt;code&gt;/posts/client-notes/&lt;/code&gt; covers none of those.&lt;/p&gt;

&lt;p&gt;One build option in front matter took it out of both feeds, the sitemap and the section listing, while the page still rendered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Client notes Q3&lt;/span&gt;
&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hugo's &lt;a href="https://gohugo.io/content-management/build-options/" rel="noopener noreferrer"&gt;build options docs&lt;/a&gt; say &lt;code&gt;list: never&lt;/code&gt; keeps the page out of every page collection, and the built-in feed and sitemap templates read from those collections. A theme can build its feed, related posts or search index its own way though, so I'd rebuild and grep the output folder for a phrase from the post before trusting it.&lt;/p&gt;

&lt;p&gt;Pagefind is the one that catches people. It reads the built HTML, not Hugo's collections, so an unlisted page that still renders still gets indexed. Its &lt;a href="https://pagefind.app/docs/indexing/" rel="noopener noreferrer"&gt;indexing docs&lt;/a&gt; recommend marking the pages you want indexed with &lt;code&gt;data-pagefind-body&lt;/code&gt;, which drops every page without it. Other generators have the same problem in whichever plugin writes the feed, sitemap and search index.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ghost doesn't do per-post passwords
&lt;/h2&gt;

&lt;p&gt;Ghost restricts posts by membership instead: public, members only, paid members only, or specific tiers, per its &lt;a href="https://ghost.org/help/protected-content/" rel="noopener noreferrer"&gt;protected content guide&lt;/a&gt;. Readers get in by signing in. The only shared password is Private Site Mode, which covers the whole site, and Ghost's &lt;a href="https://ghost.org/help/where-can-i-find-my-rss-feed/" rel="noopener noreferrer"&gt;RSS help page&lt;/a&gt; says the public feed is off while that's on, with a private feed URL under Settings, General, "Make this site private".&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one fits
&lt;/h2&gt;

&lt;p&gt;A WordPress post password is fine when the title and the fact the post exists can be public and only the body matters, like a client update or a family post. With the snippet above it stays out of the feed and sitemap. The featured image still needs to be neutral or gone.&lt;/p&gt;

&lt;p&gt;A shared password also gets forwarded along with the link. It keeps search engines and casual visitors out, and tells you nothing about who actually read it.&lt;/p&gt;

&lt;p&gt;When the post's existence has to stay hidden too, WordPress's Private status went further in my test: 404 for logged-out visitors, not in the sitemap, 401 from the REST API. But the docs limit private posts to Administrators and Editors, so it's for notes to yourself or a co-editor, not readers. For a group of readers you need a real login (Ghost members, or a host-level rule on a separate path) plus the feed, sitemap and search exclusions above.&lt;/p&gt;

&lt;p&gt;Anything that would do real damage if the password leaked I wouldn't put on a public blog's server at all&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>privacy</category>
      <category>webdev</category>
      <category>blogging</category>
    </item>
    <item>
      <title>Edited a Blog Post but Readers Still See the Old Version? How to Find the Cache Holding It</title>
      <dc:creator>Usama Ejaz</dc:creator>
      <pubDate>Tue, 20 Oct 2026 05:47:46 +0000</pubDate>
      <link>https://dev.to/usamaejaz/edited-a-blog-post-but-readers-still-see-the-old-version-how-to-find-the-cache-holding-it-1285</link>
      <guid>https://dev.to/usamaejaz/edited-a-blog-post-but-readers-still-see-the-old-version-how-to-find-the-cache-holding-it-1285</guid>
      <description>&lt;p&gt;You fix a typo in a published post, reload, and it's fixed. Then someone replies with a screenshot of the old version.&lt;/p&gt;

&lt;p&gt;Something between the host and that reader is serving a stored copy. It's one of four places: the reader's browser, the host's CDN edge, a page cache on the host's servers, or a copy kept on some other service entirely (a social preview, a feed reader, Google). The first three show up in the post's response headers, so that's where I start.&lt;/p&gt;

&lt;p&gt;On October 20, 2026 I pulled the headers from public blogs on nine platforms. Edge lifetimes went from 25 seconds on Hashnode to 12 hours on Bear. And the usual trick of adding a query string to bust the cache only worked on two of the four CDNs I tried it on.&lt;/p&gt;

&lt;h2&gt;
  
  
  One curl command
&lt;/h2&gt;

&lt;p&gt;Run it against the public URL, from a terminal that isn't logged in to the blog:&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;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-D&lt;/span&gt; - https://yourblog.com/your-post/ &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'^(cache-control|age|cf-cache-status|x-vercel-cache|cache-status|x-cache|last-modified):'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I look at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cache-control: max-age=N&lt;/code&gt; lets a browser reuse its copy for N seconds without asking. &lt;code&gt;s-maxage=N&lt;/code&gt; does the same for shared caches like a CDN, and &lt;a href="https://www.rfc-editor.org/rfc/rfc9111#name-s-maxage" rel="noopener noreferrer"&gt;RFC 9111&lt;/a&gt; says it overrides &lt;code&gt;max-age&lt;/code&gt; there.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;no-cache&lt;/code&gt; or &lt;code&gt;max-age=0&lt;/code&gt; means a stored copy has to be revalidated before reuse. It does not mean "don't store it", which &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching" rel="noopener noreferrer"&gt;MDN's caching guide&lt;/a&gt; is clear about.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;age&lt;/code&gt; is how many seconds the copy has been sitting in a shared cache. &lt;code&gt;age: 5961&lt;/code&gt; is an edge copy about 100 minutes old.&lt;/li&gt;
&lt;li&gt;The cache-status header tells you if the edge answered from storage. The name depends on the CDN: &lt;code&gt;cf-cache-status&lt;/code&gt; on Cloudflare, &lt;code&gt;x-vercel-cache&lt;/code&gt; on Vercel, &lt;code&gt;cache-status&lt;/code&gt; on Netlify, &lt;code&gt;x-cache&lt;/code&gt; on Fastly. &lt;code&gt;HIT&lt;/code&gt; means a stored copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What nine platforms sent
&lt;/h2&gt;

&lt;p&gt;One or two requests per platform, to a real site on it. It's a snapshot... hosts change these, and individual sites add their own rules. Medium gave me a 403, so it's not here.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Site checked&lt;/th&gt;
&lt;th&gt;Cache-Control on the post HTML&lt;/th&gt;
&lt;th&gt;Edge signal&lt;/th&gt;
&lt;th&gt;How long an old copy can live&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Pages&lt;/td&gt;
&lt;td&gt;karpathy.github.io, jekyllrb.com&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max-age=600&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;x-cache&lt;/code&gt; via Fastly (Varnish)&lt;/td&gt;
&lt;td&gt;10 minutes in a browser that already loaded it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netlify&lt;/td&gt;
&lt;td&gt;docs.netlify.com&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public, max-age=0, must-revalidate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cache-status: "Netlify Edge"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Until the next deploy, which &lt;a href="https://www.netlify.com/knowledge-base/how-to-control-cdn-caching-on-netlify/" rel="noopener noreferrer"&gt;Netlify says&lt;/a&gt; clears the CDN cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;nextjs.org/blog&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public, max-age=0, must-revalidate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;x-vercel-cache: HIT&lt;/code&gt;, &lt;code&gt;age: 7426&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Until the next deployment, which &lt;a href="https://vercel.com/docs/cdn-cache/purge" rel="noopener noreferrer"&gt;gets its own cache key&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hashnode&lt;/td&gt;
&lt;td&gt;townhall.hashnode.com&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public, s-maxage=25&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x-vercel-cache: MISS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;25 seconds at the edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ghost(Pro)&lt;/td&gt;
&lt;td&gt;404media.co&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public, max-age=0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;x-cache: MISS, HIT, HIT&lt;/code&gt;, &lt;code&gt;age: 8531&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Browsers revalidate every time; the edge copy depends on Ghost(Pro) purging it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordPress.com&lt;/td&gt;
&lt;td&gt;wordpress.com/blog&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;max-age=137, must-revalidate&lt;/code&gt; (156 on an earlier request)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x-nananana: Batcache-Hit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;About two and a half minutes in a browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Substack&lt;/td&gt;
&lt;td&gt;on.substack.com&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no-cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cf-cache-status: HIT&lt;/code&gt;, &lt;code&gt;age: 1470&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Edge copy kept despite &lt;code&gt;no-cache&lt;/code&gt;; lifetime not exposed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bear&lt;/td&gt;
&lt;td&gt;herman.bearblog.dev&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public, s-maxage=43200, max-age=0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cf-cache-status: HIT&lt;/code&gt;, &lt;code&gt;age: 5842&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Up to 12 hours at the edge unless Bear purges on edit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I couldn't find current docs on when Ghost(Pro) or Bear purge the edge after an edit. The closest thing is a 2019 reply on the &lt;a href="https://forum.ghost.org/t/how-is-x-cache-invalidate-used/8117" rel="noopener noreferrer"&gt;Ghost forum&lt;/a&gt; saying their custom &lt;code&gt;X-Cache-Invalidate&lt;/code&gt; header is used on Ghost(Pro)'s caching layer. So the header test on your own post is the only reliable answer I have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: rule out the browser
&lt;/h2&gt;

&lt;p&gt;Only GitHub Pages and WordPress.com in that table give browsers a positive &lt;code&gt;max-age&lt;/code&gt;. Inside that window, a browser that already has the page can show its stored copy when you click back to the post.&lt;/p&gt;

&lt;p&gt;A normal reload sends &lt;code&gt;Cache-Control: max-age=0&lt;/code&gt; with the stored validators, so the browser checks with the server first. A force reload (Ctrl+Shift+R, Cmd+Shift+R on a Mac) sends &lt;code&gt;no-cache&lt;/code&gt; and skips the stored copy. If either one shows the edit, it was the browser, and everyone else's browser catches up within the &lt;code&gt;max-age&lt;/code&gt;. That's 10 minutes at most on GitHub Pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: check the edge copy's age
&lt;/h2&gt;

&lt;p&gt;If a force reload still shows the old text, and the response has a &lt;code&gt;HIT&lt;/code&gt; with an &lt;code&gt;age&lt;/code&gt; longer than the time since your edit, the edge copy was stored before you saved. How long it stays is &lt;code&gt;s-maxage&lt;/code&gt; or whatever rules the CDN has. On Bear that's 43,200 seconds.&lt;/p&gt;

&lt;p&gt;Substack's row is the odd one. &lt;a href="https://developers.cloudflare.com/cache/concepts/default-cache-behavior/" rel="noopener noreferrer"&gt;Cloudflare's docs&lt;/a&gt; say it doesn't cache HTML by default and skips responses marked &lt;code&gt;no-cache&lt;/code&gt;. So a &lt;code&gt;HIT&lt;/code&gt; on &lt;code&gt;no-cache&lt;/code&gt; HTML means Substack set its own cache rule, with a lifetime the response doesn't tell you.&lt;/p&gt;

&lt;p&gt;To confirm the edge is the problem, request the post with a throwaway query string like &lt;code&gt;?nocache=21404&lt;/code&gt;. The result depended on the CDN:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Plain URL&lt;/th&gt;
&lt;th&gt;With a random query string&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bear&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HIT&lt;/code&gt;, &lt;code&gt;age: 5961&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MISS&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Substack&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HIT&lt;/code&gt;, &lt;code&gt;age: 1600&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MISS&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Pages&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HIT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HIT&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ghost(Pro)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HIT&lt;/code&gt;, &lt;code&gt;age: 8531&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HIT&lt;/code&gt;, &lt;code&gt;age: 8651&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On Bear and Substack the query string went to the origin. If that version has your edit and the plain URL doesn't, the edge copy is stale and gets replaced when it expires or the host purges it.&lt;/p&gt;

&lt;p&gt;On GitHub Pages and Ghost(Pro) it still came back as a hit, and on Ghost(Pro) the &lt;code&gt;age&lt;/code&gt; kept counting up. The cache was ignoring the query string. On those hosts this test can't tell the edge from the origin, which I think a lot of "just add ?v=2" advice misses.&lt;/p&gt;

&lt;p&gt;If you put your own Cloudflare proxy in front of a hosted blog, you now have two edge caches with separate rules stacked on each other. Check both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: check logged out
&lt;/h2&gt;

&lt;p&gt;Page caches often treat signed-in visitors differently. The &lt;a href="https://github.com/Automattic/batcache" rel="noopener noreferrer"&gt;Batcache readme&lt;/a&gt;, which is the cache behind &lt;code&gt;Batcache-Hit&lt;/code&gt; on WordPress.com, says it works "by serving old pages to new users," and that once visitors "have logged in or left a comment they will always get fresh pages."&lt;/p&gt;

&lt;p&gt;So if you're signed in to your own blog, you see the edit and logged-out readers get the stored page. A private window or the curl above carries none of your cookies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: the copies that aren't on your host
&lt;/h2&gt;

&lt;p&gt;If the post URL serves the new version and someone still sees the old one, the copy lives on another service, and each one refreshes on its own schedule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Link previews on social networks and chat apps keep the title, description and image they fetched first.&lt;/li&gt;
&lt;li&gt;Feed readers only see the edit after they poll your feed again, which can be hours for a small blog.&lt;/li&gt;
&lt;li&gt;Google's title and snippet come from its last crawl. Search Console's &lt;a href="https://support.google.com/webmasters/answer/9012289" rel="noopener noreferrer"&gt;URL Inspection tool&lt;/a&gt; lets you run a live test and request indexing. Google says that "typically takes only a day or so" but can take longer, and requests have a daily limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I build &lt;a href="https://justblogged.com" rel="noopener noreferrer"&gt;JustBlogged&lt;/a&gt;, and when I ran the same curl on its post pages they came back with no &lt;code&gt;Cache-Control&lt;/code&gt;, no &lt;code&gt;age&lt;/code&gt;, no cache-status header and no &lt;code&gt;Last-Modified&lt;/code&gt;. Nothing tells a browser or CDN to keep the page, so when someone there reports an old version it's always step 4. The flip side is every request goes to the origin, and I'm still not sure that's the right default&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>blogging</category>
      <category>cdn</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why New Blog Posts Reach Feed Readers Hours Late, and Which Platforms Send a WebSub Hub</title>
      <dc:creator>Usama Ejaz</dc:creator>
      <pubDate>Mon, 19 Oct 2026 05:46:43 +0000</pubDate>
      <link>https://dev.to/usamaejaz/why-new-blog-posts-reach-feed-readers-hours-late-and-which-platforms-send-a-websub-hub-146f</link>
      <guid>https://dev.to/usamaejaz/why-new-blog-posts-reach-feed-readers-hours-late-and-which-platforms-send-a-websub-hub-146f</guid>
      <description>&lt;p&gt;A new post shows up in a feed reader only when the reader next fetches your feed. Most readers fetch on a schedule, and for a small blog that schedule is slow. Feedly's free plan checks a feed anywhere from every 30 minutes to once a day.&lt;/p&gt;

&lt;p&gt;The way around it is a WebSub hub. Your feed names a hub, your platform pings it when you publish, and readers subscribed to the hub get the post when the hub pushes it. On October 19, 2026 I fetched one feed from each of ten blogging platforms to see which ones declare a hub. Three did.&lt;/p&gt;

&lt;h2&gt;
  
  
  How often readers actually check
&lt;/h2&gt;

&lt;p&gt;These come from each reader's own docs as of October 19, 2026.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reader&lt;/th&gt;
&lt;th&gt;How often it polls&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Feedly Basic&lt;/td&gt;
&lt;td&gt;30 minutes to 1 day, depending on how popular the site is and how often it publishes&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.feedly.com/article/212-how-often-does-feedly-update" rel="noopener noreferrer"&gt;Feedly help&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feedly Pro&lt;/td&gt;
&lt;td&gt;15 minutes to 1 hour&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feedly Pro+ and Enterprise&lt;/td&gt;
&lt;td&gt;As often as every 7 minutes&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inoreader&lt;/td&gt;
&lt;td&gt;Active feeds every 30 minutes to a few hours; few subscribers at most hourly; quiet for a month about daily&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.inoreader.com/feed-fetcher" rel="noopener noreferrer"&gt;Inoreader feed fetcher&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Miniflux (self-hosted)&lt;/td&gt;
&lt;td&gt;Round-robin with a 60-minute minimum by default; the optional &lt;code&gt;entry_frequency&lt;/code&gt; scheduler adapts from 5 minutes to 24 hours based on the last week&lt;/td&gt;
&lt;td&gt;&lt;a href="https://miniflux.app/docs/configuration.html" rel="noopener noreferrer"&gt;Miniflux configuration&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The popularity factor is what hurts a personal blog. A handful of subscribers on free accounts puts you at the slow end of every schedule, and publishing once a week makes you look like a feed that rarely changes.&lt;/p&gt;

&lt;p&gt;Both Feedly and Inoreader say what skips the schedule. Feedly says sites using "the PuSH protocol" show up "in near real-time." Inoreader says that when it finds a hub it "receives new items as soon as you publish them" and falls back to polling every few hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a hub changes
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.w3.org/TR/websub/" rel="noopener noreferrer"&gt;WebSub&lt;/a&gt; is the W3C Recommendation that grew out of PubSubHubbub, the "PuSH" Feedly mentions. Three parties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your feed advertises a hub with &lt;code&gt;rel="hub"&lt;/code&gt; and its own address with &lt;code&gt;rel="self"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A reader that supports WebSub subscribes to the hub for that feed.&lt;/li&gt;
&lt;li&gt;When you publish, your platform tells the hub, and the hub fetches the feed and pushes it to every subscriber.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The spec leaves one step open: "The specific mechanism for the publisher to inform the hub is left unspecified." In practice the public hubs accept the same thing, a form POST with &lt;code&gt;hub.mode=publish&lt;/code&gt; and &lt;code&gt;hub.url&lt;/code&gt; set to your feed. That's documented on &lt;a href="https://pubsubhubbub.appspot.com/" rel="noopener noreferrer"&gt;Google's public hub&lt;/a&gt;, which anyone can use without an account.&lt;/p&gt;

&lt;p&gt;It doesn't help readers that never look for a hub, and it doesn't stop polling. It removes the wait for the readers that support it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which platforms declare one
&lt;/h2&gt;

&lt;p&gt;One public feed per platform. I looked for &lt;code&gt;rel="hub"&lt;/code&gt; in the XML and in response headers. I also sent each response's &lt;code&gt;ETag&lt;/code&gt; and &lt;code&gt;Last-Modified&lt;/code&gt; back as &lt;code&gt;If-None-Match&lt;/code&gt; and &lt;code&gt;If-Modified-Since&lt;/code&gt;, which is what Inoreader says its fetcher does, to see if the server answers an unchanged feed with &lt;code&gt;304 Not Modified&lt;/code&gt;. A 304 doesn't speed up delivery, but without it every poll downloads the whole file.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform (feed checked)&lt;/th&gt;
&lt;th&gt;Hub&lt;/th&gt;
&lt;th&gt;304 on an unchanged feed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Medium (&lt;code&gt;medium.com/feed/@ev&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Yes, &lt;code&gt;http://medium.superfeedr.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No validators sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordPress.com (&lt;code&gt;wordpress.com/blog/feed/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Yes, &lt;code&gt;https://wordpress.com/blog/?pushpress=hub&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blogger (&lt;code&gt;blog.chromium.org/feeds/posts/default&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Yes, &lt;code&gt;http://pubsubhubbub.appspot.com/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No, sent &lt;code&gt;Last-Modified&lt;/code&gt; but answered the replay with 200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted WordPress 7.2 alpha (&lt;code&gt;wordpress.org/news/feed/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ghost 6.71 (&lt;code&gt;404media.co/rss/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Substack (&lt;code&gt;lennysnewsletter.com/feed&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bear (&lt;code&gt;herman.bearblog.dev/feed/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DEV (&lt;code&gt;dev.to/feed/ben&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hashnode (&lt;code&gt;townhall.hashnode.com/rss.xml&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No validators sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JustBlogged (&lt;code&gt;justblogged.com/blog/rss.xml&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No validators sent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One feed per platform is a sample. A self-hosted WordPress or Ghost site can behave differently because of plugins, themes or the cache in front of it. Several Blogger blogs I tried redirected their feed to FeedBurner, which serves its own copy, so check the URL your readers are actually subscribed to.&lt;/p&gt;

&lt;p&gt;The last row is mine. I build &lt;a href="https://justblogged.com" rel="noopener noreferrer"&gt;JustBlogged&lt;/a&gt;, and its own blog feed carries every full post, 594,833 bytes this morning, with no hub, no &lt;code&gt;ETag&lt;/code&gt; and no &lt;code&gt;Last-Modified&lt;/code&gt;. So readers of that blog wait for their reader's schedule, and every poll downloads the whole file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your own feed
&lt;/h2&gt;

&lt;p&gt;First, does it declare a hub? Either as a link in the XML or as an HTTP &lt;code&gt;Link&lt;/code&gt; header, which the spec says subscribers check first.&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="nv"&gt;FEED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://example.com/feed.xml
curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FEED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &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;[a-z:]*link[^&amp;gt;]*rel=.hub.[^&amp;gt;]*&amp;gt;'&lt;/span&gt;
curl &lt;span class="nt"&gt;-sSIL&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FEED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^link:.*hub'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, does it answer 304 when nothing changed? This sends back both validators the way a reader would:&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="nv"&gt;H&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FEED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;ETAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$H&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^etag:'&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f2-&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\r'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;LM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$H&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^last-modified:'&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f2-&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\r'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"If-None-Match: &lt;/span&gt;&lt;span class="nv"&gt;$ETAG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"If-Modified-Since: &lt;/span&gt;&lt;span class="nv"&gt;$LM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FEED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send both headers. WordPress.com answered 304 to &lt;code&gt;If-Modified-Since&lt;/code&gt; but 200 to its own &lt;code&gt;ETag&lt;/code&gt; alone, so a one-header test can report a failure real readers never hit.&lt;/p&gt;

&lt;p&gt;While the headers are open, look at &lt;code&gt;cache-control&lt;/code&gt; and &lt;code&gt;age&lt;/code&gt;. The Bear feed came back with &lt;code&gt;s-maxage=43200&lt;/code&gt; and a Cloudflare cache hit, which lets the edge keep a copy for up to 12 hours. I didn't check whether Bear purges it on publish. On any host, if the feed is cached at a CDN and not purged on publish, the new post isn't in the feed readers fetch, and no hub fixes that. Fetch your feed right after publishing and confirm the item is there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a hub yourself
&lt;/h2&gt;

&lt;p&gt;Self-hosted WordPress: the &lt;a href="https://wordpress.org/plugins/pubsubhubbub/" rel="noopener noreferrer"&gt;WebSub plugin&lt;/a&gt; (4.0.1, 100,000+ active installs as of October 2026) adds hub links to your feeds and pings hubs on publish and update. It uses Google's hub, Superfeedr and WebSubHub by default.&lt;/p&gt;

&lt;p&gt;Static site generator: two edits. Add the hub link next to &lt;code&gt;rel="self"&lt;/code&gt; in your RSS channel, with the Atom namespace on &lt;code&gt;&amp;lt;rss&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;rss&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"2.0"&lt;/span&gt; &lt;span class="na"&gt;xmlns:atom=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2005/Atom"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;channel&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;atom:link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"self"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/rss+xml"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/feed.xml"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;atom:link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"hub"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://pubsubhubbub.appspot.com/"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ping at the end of your deploy, after the new feed is live:&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;-sS&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://pubsubhubbub.appspot.com/ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; hub.mode&lt;span class="o"&gt;=&lt;/span&gt;publish &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; hub.url&lt;span class="o"&gt;=&lt;/span&gt;https://example.com/feed.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;204&lt;/code&gt; means the hub accepted the ping, not that anyone got notified. I sent that request for a feed with no hub link at all and Google's hub still answered 204. Delivery only happens for readers that found the hub in your feed and subscribed, so the link has to be live first. And &lt;code&gt;rel="self"&lt;/code&gt; has to be the exact URL subscribers use, because that's the topic the hub tracks.&lt;/p&gt;

&lt;p&gt;On a hosted platform without a hub there's no setting to flip. Your subscribers' readers poll on their own schedule&lt;/p&gt;

</description>
      <category>rss</category>
      <category>webdev</category>
      <category>blogging</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Leaving WordPress With AMP: Find Every /amp/ and ?amp=1 URL Before the Old Site Goes Offline</title>
      <dc:creator>Usama Ejaz</dc:creator>
      <pubDate>Mon, 19 Oct 2026 05:46:07 +0000</pubDate>
      <link>https://dev.to/usamaejaz/leaving-wordpress-with-amp-find-every-amp-and-amp1-url-before-the-old-site-goes-offline-38k</link>
      <guid>https://dev.to/usamaejaz/leaving-wordpress-with-amp-find-every-amp-and-amp1-url-before-the-old-site-goes-offline-38k</guid>
      <description>&lt;p&gt;If your WordPress blog ever ran an AMP plugin, every post had a second address. Usually &lt;code&gt;/post-name/amp/&lt;/code&gt; or &lt;code&gt;/post-name/?amp=1&lt;/code&gt;. Those URLs are still in Google's index and in old shares, and once you move to a host without AMP the &lt;code&gt;/amp/&lt;/code&gt; ones start returning 404.&lt;/p&gt;

&lt;p&gt;Google's steps for &lt;a href="https://developers.google.com/search/docs/crawling-indexing/amp/remove-amp" rel="noopener noreferrer"&gt;removing AMP pages while keeping the canonical page&lt;/a&gt; are short: take the &lt;code&gt;rel="amphtml"&lt;/code&gt; link off the canonical page, and answer each AMP URL with a 301 or 302 to that canonical page.&lt;/p&gt;

&lt;p&gt;The hard part is knowing which AMP URLs exist. I read the source of the two main AMP plugins to find out, and wrote a small script to list them. Everything below was checked on October 18, 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which format your site used
&lt;/h2&gt;

&lt;p&gt;It depends on the plugin and one setting. I downloaded the current releases from WordPress.org and read the code that builds each URL. Examples assume &lt;code&gt;/%postname%/&lt;/code&gt; permalinks and AMP enabled for that content type.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin and setting&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Page&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AMP 2.5.5, Query parameter (default for new installs)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/post-name/?amp=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/about/?amp=1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMP 2.5.5, Path suffix&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/post-name/amp/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/about/amp/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMP 2.5.5, Legacy transitional&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/post-name/?amp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/about/?amp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMP 2.5.5, Legacy reader&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/post-name/amp/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/about/?amp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMP for WP 1.1.19, default with pretty permalinks&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/post-name/amp/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/about/amp/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMP for WP 1.1.19, "Change End Point to ?amp" on&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/post-name/?amp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/about/?amp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The official &lt;a href="https://wordpress.org/plugins/amp/" rel="noopener noreferrer"&gt;AMP plugin&lt;/a&gt; lists 300,000+ active installs and was last updated in April 2025. &lt;a href="https://wordpress.org/plugins/accelerated-mobile-pages/" rel="noopener noreferrer"&gt;AMP for WP&lt;/a&gt; lists 70,000+ and was updated on October 13, 2026.&lt;/p&gt;

&lt;p&gt;Legacy reader is the one that surprised me. It uses &lt;code&gt;/amp/&lt;/code&gt; for posts but falls back to &lt;code&gt;?amp&lt;/code&gt; for pages, attachments, and any URL that already has a query string... so one site can carry both shapes. If you switched the setting at some point, both shapes may also be indexed from different years.&lt;/p&gt;

&lt;p&gt;Fastest way to tell: open any post's HTML source on the old site and search for &lt;code&gt;amphtml&lt;/code&gt;. The link it prints is the exact AMP URL for that page.&lt;/p&gt;

&lt;h2&gt;
  
  
  List every AMP URL while WordPress is still up
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;amphtml&lt;/code&gt; link is the best inventory you'll get, and it only exists while WordPress is serving pages. This reads one sitemap, fetches each URL, and writes the AMP address next to the canonical one:&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Usage: ./amp-map.sh https://old-blog.example.com/wp-sitemap-posts-post-1.xml &amp;gt; amp-map.csv&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;loc&amp;gt;[^&amp;lt;]*'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/&amp;lt;loc&amp;gt;//'&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; page&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;amp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;link[^&amp;gt;]*rel="amphtml"[^&amp;gt;]*&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'s/.*href="([^"]+)".*/\1/'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$amp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$amp&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran it against five posts on ampforwp.com, the AMP for WP plugin's own site, and every row came back the same shape, e.g. &lt;code&gt;https://ampforwp.com/the-change/amp/,https://ampforwp.com/the-change/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Run it once per post sitemap. WordPress core lists them at &lt;code&gt;/wp-sitemap.xml&lt;/code&gt;, Yoast at &lt;code&gt;/sitemap_index.xml&lt;/code&gt;. If AMP was on for pages, categories or the home page, run those sitemaps too.&lt;/p&gt;

&lt;p&gt;It only sees pages that print the link today. For posts where AMP was turned off earlier but the AMP URL is still indexed, the &lt;a href="https://support.google.com/webmasters/answer/7450883" rel="noopener noreferrer"&gt;AMP status report in Search Console&lt;/a&gt; lists the AMP URLs Google knows about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: let the plugin redirect before you move
&lt;/h2&gt;

&lt;p&gt;If WordPress stays up for a few weeks before the DNS change, the official AMP plugin can do Google's two steps for you. In its code, when a URL is requested with the AMP endpoint but AMP isn't available for that content, it redirects to the non-AMP URL and prints an HTML comment in place of the &lt;code&gt;amphtml&lt;/code&gt; link. So you leave the plugin active, turn AMP off for your content types under Supported Templates, and the old AMP URLs start redirecting while the posts are still on WordPress.&lt;/p&gt;

&lt;p&gt;Two things to know when you test it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It sends a 302 to anyone with &lt;code&gt;manage_options&lt;/code&gt; and a 301 to everyone else. Test logged out, or with curl.&lt;/li&gt;
&lt;li&gt;A path-suffix URL can take two hops. With AMP off for posts I saw &lt;code&gt;/post-name/amp/&lt;/code&gt; 301 to &lt;code&gt;/post-name/?amp=1&lt;/code&gt;, which then 301'd to the post. Crawlers follow both, but the redirects you add on the new host should go straight to the final URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I didn't check whether AMP for WP does the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redirect the /amp/ paths on the new host
&lt;/h2&gt;

&lt;p&gt;After the cutover, WordPress isn't answering anymore, so the redirects have to live wherever the posts went. The two shapes behave very differently there.&lt;/p&gt;

&lt;p&gt;The query forms (&lt;code&gt;?amp=1&lt;/code&gt;, &lt;code&gt;?amp&lt;/code&gt;) often just load the normal post, because many hosts ignore query parameters they don't use. That's what I got testing on &lt;a href="https://justblogged.com" rel="noopener noreferrer"&gt;JustBlogged&lt;/a&gt;, the blog host I run: 200, the normal post, canonical pointing at the clean URL. Google asks for a redirect, so this isn't an exact match, but I'm fine with it. The page isn't AMP, has no &lt;code&gt;amphtml&lt;/code&gt; link, and its canonical names the clean URL, which is the same signal Google uses to &lt;a href="https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls" rel="noopener noreferrer"&gt;consolidate duplicate URLs&lt;/a&gt; like tracking-parameter variants.&lt;/p&gt;

&lt;p&gt;The path forms (&lt;code&gt;/post-name/amp/&lt;/code&gt;, &lt;code&gt;/post-name/amp&lt;/code&gt;) are the real problem. They 404, and a 404 drops whatever links and indexing those URLs had. Each one needs a 301.&lt;/p&gt;

&lt;p&gt;Point the redirect at the post's new path, not its old WordPress URL. If the old permalink was &lt;code&gt;/2023/05/post-name/&lt;/code&gt; and the post now lives at &lt;code&gt;/post-name&lt;/code&gt;, the redirect is &lt;code&gt;/2023/05/post-name/amp/&lt;/code&gt; to &lt;code&gt;/post-name&lt;/code&gt;. One hop instead of two.&lt;/p&gt;

&lt;p&gt;Google warns against one shortcut: emptying the AMP file instead of redirecting it. An empty document is invalid AMP, and Google keeps serving the oldest valid version it has.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check every AMP URL after DNS moves
&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;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;, &lt;span class="nt"&gt;-f1&lt;/span&gt; amp-map.csv | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; u&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code} &lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; %{redirect_url}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every path-form row should be a 301 to the new post URL, and that URL should be 200. A 404 is a missing or mistyped redirect. A redirect to the old WordPress URL is a chain you can shorten.&lt;/p&gt;

&lt;p&gt;After that, the AMP status report's count of indexed AMP pages should drop as Google recrawls them, and the redirected AMP URLs should show up under "Page with redirect" in the Page indexing report&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>seo</category>
      <category>webdev</category>
      <category>blogging</category>
    </item>
    <item>
      <title>Search Console's "Why Pages Aren't Indexed" on a Blog: Which Rows Actually Need Work</title>
      <dc:creator>Usama Ejaz</dc:creator>
      <pubDate>Mon, 19 Oct 2026 05:45:31 +0000</pubDate>
      <link>https://dev.to/usamaejaz/search-consoles-why-pages-arent-indexed-on-a-blog-which-rows-actually-need-work-3cih</link>
      <guid>https://dev.to/usamaejaz/search-consoles-why-pages-arent-indexed-on-a-blog-which-rows-actually-need-work-3cih</guid>
      <description>&lt;p&gt;The first time I opened the Page indexing report for a blog I run, the "Why pages aren't indexed" table had more URLs in it than the blog had posts. That looked bad. It mostly wasn't.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;http://&lt;/code&gt; address that redirects, a &lt;code&gt;?utm_source=&lt;/code&gt; link whose canonical points at the clean URL, an RSS feed sent with a noindex header... all of those land in that table, and none of them should be in search results anyway. Google's &lt;a href="https://support.google.com/webmasters/answer/7440203" rel="noopener noreferrer"&gt;Page indexing report documentation&lt;/a&gt; says the same: most sites have some unindexed pages, and that's fine as long as you understand the reason and your important pages are indexed.&lt;/p&gt;

&lt;p&gt;The rows that need work are the ones holding a URL you actually want in search. On a blog that almost always means a post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sort by URL first, reason second
&lt;/h2&gt;

&lt;p&gt;Open Indexing, then Pages, and click a row in "Why pages aren't indexed". The details page lists example URLs for that reason, capped at 1,000. For each one I ask a single question: would I want this exact URL as a search result?&lt;/p&gt;

&lt;p&gt;If not, the reason Google gives barely matters. If yes, open it in URL Inspection, which shows the live status, the canonical you declared, and the canonical Google picked.&lt;/p&gt;

&lt;p&gt;I do it in that order because the reason names are easy to misread. "Duplicate without user-selected canonical" sounds like a fault, but Google calls it working as intended when the URL it chose is the right one. "Crawled - currently not indexed" sounds final, but Google says it may index the page later and that you don't need to resubmit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each reason usually holds on a blog
&lt;/h2&gt;

&lt;p&gt;Reason names are the ones in the report as of October 2026. The middle column is my summary of where these come from on blogs. The last column follows Google's documentation for each reason.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;th&gt;Typical blog URLs&lt;/th&gt;
&lt;th&gt;Act?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Page with redirect&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;http://&lt;/code&gt; and &lt;code&gt;www&lt;/code&gt; variants, old slugs, the platform subdomain you used before a custom domain&lt;/td&gt;
&lt;td&gt;No, unless a current post URL is redirecting. Check the target is indexed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alternate page with proper canonical tag&lt;/td&gt;
&lt;td&gt;Tracking parameters, trailing-slash and case variants that canonicalize to the clean URL&lt;/td&gt;
&lt;td&gt;No. Google says no action is needed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate without user-selected canonical&lt;/td&gt;
&lt;td&gt;Duplicate URLs that declare no canonical&lt;/td&gt;
&lt;td&gt;Only if Google picked the wrong URL. Then declare the canonical.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate, Google chose different canonical than user&lt;/td&gt;
&lt;td&gt;A canonical pointing at a page with different content, near-identical posts, a cross-posted copy Google treats as the original&lt;/td&gt;
&lt;td&gt;Yes. Compare both canonicals in URL Inspection.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL marked 'noindex'&lt;/td&gt;
&lt;td&gt;Feeds, search results, tag pages you chose to hide, drafts and previews&lt;/td&gt;
&lt;td&gt;Only if a post is in the list.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL blocked by robots.txt&lt;/td&gt;
&lt;td&gt;Admin paths, internal search&lt;/td&gt;
&lt;td&gt;Only if a post is in the list. robots.txt doesn't keep a URL out of search; noindex does.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not found (404)&lt;/td&gt;
&lt;td&gt;Deleted posts, mistyped inbound links, old URLs from a migration&lt;/td&gt;
&lt;td&gt;301 it if the content moved. A removed post with no replacement can stay 404.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Soft 404&lt;/td&gt;
&lt;td&gt;Empty tag pages, archive pages past the last post, an error page served with 200&lt;/td&gt;
&lt;td&gt;Yes. Return a real 404 or put real content there.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server error (5xx), Redirect error&lt;/td&gt;
&lt;td&gt;Outages, redirect loops, chains from more than one migration&lt;/td&gt;
&lt;td&gt;Yes, before anything else.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crawled - currently not indexed&lt;/td&gt;
&lt;td&gt;Thin tag and archive pages, sometimes posts&lt;/td&gt;
&lt;td&gt;Only for posts you care about.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discovered - currently not indexed&lt;/td&gt;
&lt;td&gt;New posts, deep archive pages&lt;/td&gt;
&lt;td&gt;Usually wait. Google gives no specific fix.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Predict the row before Google recrawls
&lt;/h2&gt;

&lt;p&gt;You don't have to wait for a recrawl to know where a URL will land. The status code, the &lt;code&gt;Location&lt;/code&gt; header, &lt;code&gt;X-Robots-Tag&lt;/code&gt;, the canonical link and the robots meta tag decide most of it. This prints all five for one URL:&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;-sS&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; page.html &lt;span class="s2"&gt;"https://example.com/blog/my-post?utm_source=x"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'^(HTTP|location|x-robots-tag)'&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oiE&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;link[^&amp;gt;]*rel="?canonical[^&amp;gt;]*&amp;gt;|&amp;lt;meta[^&amp;gt;]*name="?robots[^&amp;gt;]*&amp;gt;'&lt;/span&gt; page.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran it against the blog on &lt;a href="https://justblogged.com" rel="noopener noreferrer"&gt;JustBlogged&lt;/a&gt;, the platform I build, on 17 October 2026. It lives at &lt;code&gt;/blog&lt;/code&gt; on the main domain:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;URL requested&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;th&gt;Expected row&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http://justblogged.com/blog/blog-404-page&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;301 to &lt;code&gt;https://&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Page with redirect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://www.justblogged.com/blog/blog-404-page&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;301 to the apex&lt;/td&gt;
&lt;td&gt;Page with redirect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/blog/blog-404-page/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200, canonical without the slash&lt;/td&gt;
&lt;td&gt;Alternate page with proper canonical tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/blog/blog-404-page?utm_source=x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200, canonical to the clean URL&lt;/td&gt;
&lt;td&gt;Alternate page with proper canonical tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/blog/BLOG-404-PAGE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200, canonical to lowercase&lt;/td&gt;
&lt;td&gt;Alternate page with proper canonical tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/blog/rss.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200, &lt;code&gt;X-Robots-Tag: noindex, follow&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;URL marked 'noindex'&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/blog/search?q=dns&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200, &lt;code&gt;X-Robots-Tag: noindex, follow&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;URL marked 'noindex'&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/blog/tags/dns&lt;/code&gt; (tag is "DNS")&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;Not found (404)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is one I didn't expect. Post slugs are case-insensitive and canonicalize to lowercase, but tag URLs are case-sensitive, so &lt;code&gt;/blog/tags/DNS&lt;/code&gt; is 200 and &lt;code&gt;/blog/tags/dns&lt;/code&gt; is 404. Any site linking to a tag with the wrong case shows up under Not found. Worth checking on whatever platform you're on, since slugs and tags are often handled by different routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  robots.txt under /blog does nothing
&lt;/h2&gt;

&lt;p&gt;This one catches a lot of blogs that live under a path. That blog serves a file at &lt;code&gt;/blog/robots.txt&lt;/code&gt;, but Google's &lt;a href="https://developers.google.com/search/docs/crawling-indexing/robots/robots_txt" rel="noopener noreferrer"&gt;robots.txt documentation&lt;/a&gt; says crawlers only read the file at the top level of the host. Anything under &lt;code&gt;/blog&lt;/code&gt; is governed by the root &lt;code&gt;robots.txt&lt;/code&gt;, and a rule in the blog's own file has no effect on Google.&lt;/p&gt;

&lt;p&gt;So the noindex header is what keeps &lt;code&gt;/blog/search&lt;/code&gt; out. The root file doesn't block it, Googlebot fetches it, reads the header, and files it under "URL marked 'noindex'". If you block it in robots.txt instead, Google's &lt;a href="https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls" rel="noopener noreferrer"&gt;canonicalization guide&lt;/a&gt; warns it can still be indexed without its content, which is the "Indexed, though blocked by robots.txt" warning.&lt;/p&gt;

&lt;p&gt;Platform defaults matter here too. WordPress sites created on 6.4 or later have &lt;a href="https://make.wordpress.org/core/2023/10/16/changes-to-attachment-pages/" rel="noopener noreferrer"&gt;attachment pages disabled&lt;/a&gt; and redirect them, so they show under Page with redirect. A site upgraded from an older version keeps them on, and they can turn up as indexable URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order I fix rows that hold posts
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Server errors and redirect errors. Google can't evaluate a page it can't fetch, and an outage or loop usually hits every post at once.&lt;/li&gt;
&lt;li&gt;Posts under "URL marked 'noindex'". Usually a leftover staging setting, a theme option, or a per-post SEO toggle. Remove it, then inspect the live URL.&lt;/li&gt;
&lt;li&gt;Soft 404s. Real 404 for pages that are gone, real content for pages thin enough to look like errors.&lt;/li&gt;
&lt;li&gt;"Duplicate, Google chose different canonical than user". Google won't pick a declared canonical that isn't similar to the page, so check the target has the same content. Cross-posted copies on Medium, DEV or Hashnode are the usual suspect when the canonical tag on the copy is missing.&lt;/li&gt;
&lt;li&gt;Posts under "Crawled - currently not indexed". No fix attached. What you control is whether the post adds something your other posts don't, and whether your own posts link to it. Resubmitting it every day doesn't help.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once every example URL under a reason is fixed, click Validate Fix on that reason. Google says validation typically takes up to about two weeks, and not to click it again until it has passed or failed.&lt;/p&gt;

&lt;p&gt;If you moved hosts recently, most of the 404 and redirect rows are probably old URLs, and the migration redirects are the thing to fix before the report itself&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>blogging</category>
      <category>google</category>
    </item>
    <item>
      <title>Google Shows the Wrong Site Name for Your Blog: Where It Comes From and How to Fix It</title>
      <dc:creator>Usama Ejaz</dc:creator>
      <pubDate>Fri, 16 Oct 2026 06:19:32 +0000</pubDate>
      <link>https://dev.to/usamaejaz/google-shows-the-wrong-site-name-for-your-blog-where-it-comes-from-and-how-to-fix-it-3c1h</link>
      <guid>https://dev.to/usamaejaz/google-shows-the-wrong-site-name-for-your-blog-where-it-comes-from-and-how-to-fix-it-3c1h</guid>
      <description>&lt;p&gt;Google picks the site name it shows above your blog's results by itself. It reads your home page, and the strongest preference you can state there is the &lt;code&gt;name&lt;/code&gt; in &lt;code&gt;WebSite&lt;/code&gt; structured data. It also looks at &lt;code&gt;og:site_name&lt;/code&gt;, the &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, headings and other text on the home page, according to &lt;a href="https://developers.google.com/search/docs/appearance/site-names" rel="noopener noreferrer"&gt;Google's site names documentation&lt;/a&gt; (last updated 10 December 2025). When Google shows the wrong name, the fix is to make those signals agree, add fallbacks in &lt;code&gt;alternateName&lt;/code&gt;, and get the home page recrawled.&lt;/p&gt;

&lt;p&gt;Two cases have no markup fix. Google only supports site names for a domain or subdomain, so a blog at &lt;code&gt;example.com/blog&lt;/code&gt; can't get a name of its own. And Google can still decline a name it finds generic or already used by another site. The rest of this post covers how to see what your home page actually sends, what four platforms put there by default, and the fix for each wrong name you might be seeing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The markup Google asks for
&lt;/h2&gt;

&lt;p&gt;Google wants one &lt;code&gt;WebSite&lt;/code&gt; object on the root URL of the domain or subdomain, with a &lt;code&gt;name&lt;/code&gt; and the canonical home page &lt;code&gt;url&lt;/code&gt;. It doesn't need to be on every page. &lt;code&gt;alternateName&lt;/code&gt; is optional and takes a list in order of preference. Google's documentation suggests ending that list with your domain in lowercase, because it will strongly consider that as a backup when your preferred name isn't picked.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://schema.org&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WebSite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Field Notes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alternateName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Field Notes Blog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fieldnotes.dev&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://fieldnotes.dev/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your platform already prints a &lt;code&gt;WebSite&lt;/code&gt; block, change that block instead of pasting a second one into a custom head field. Google asks you to keep the site name properties in the same node and to avoid an additional &lt;code&gt;WebSite&lt;/code&gt; block on the home page. The Rich Results Test doesn't cover site names, so check the syntax with the &lt;a href="https://validator.schema.org/" rel="noopener noreferrer"&gt;Schema Markup Validator&lt;/a&gt; instead.&lt;/p&gt;

&lt;p&gt;This post is only about the home page, not the markup on individual posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check what your home page sends to Googlebot
&lt;/h2&gt;

&lt;p&gt;Viewing the source in your browser isn't always enough. On 16 October 2026 I requested the home pages of two Substack publications, &lt;code&gt;on.substack.com&lt;/code&gt; and &lt;code&gt;lennysnewsletter.com&lt;/code&gt;, twice each. With a Chrome user agent, the HTML had no JSON-LD at all. With Googlebot's user agent, the response was several times larger and contained a &lt;code&gt;WebSite&lt;/code&gt; block with the publication name. Neither response had &lt;code&gt;og:site_name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So check the version Google receives. The URL Inspection tool in Search Console shows the HTML Google crawled; &lt;a href="https://support.google.com/webmasters/answer/9012289" rel="noopener noreferrer"&gt;Google's help page for the tool&lt;/a&gt; explains the live test. For a quick look from a terminal, this fetches the home page with a Googlebot user agent and prints the title, &lt;code&gt;og:site_name&lt;/code&gt; and the start of any &lt;code&gt;WebSite&lt;/code&gt; block. The &lt;code&gt;tr&lt;/code&gt; step flattens the HTML so JSON-LD spread over several lines still matches.&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;-sL&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"&lt;/span&gt; https://example.com/ &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;'\n\t '&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;title[^&amp;gt;]*&amp;gt;[^&amp;lt;]*|og:site_name"[^&amp;gt;]*|"@type" ?: ?"WebSite"[^}]{0,200}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some hosts answer scripted requests with a bot challenge instead of the page. Bear Blog and Medium returned Cloudflare challenge pages to my requests, and one Hashnode blog returned a Vercel security checkpoint. URL Inspection is the reliable check in that case.&lt;/p&gt;

&lt;h2&gt;
  
  
  What four platforms put on a blog's home page
&lt;/h2&gt;

&lt;p&gt;I checked live home pages on 16 October 2026. Each row describes what the sites I sampled sent, which is the platform default unless the owner changed it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Sites checked&lt;/th&gt;
&lt;th&gt;Home page markup&lt;/th&gt;
&lt;th&gt;Where to change the name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ghost&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo.ghost.io&lt;/code&gt;, &lt;code&gt;platformer.news&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;WebSite&lt;/code&gt; with the site title as &lt;code&gt;name&lt;/code&gt;, plus matching &lt;code&gt;og:site_name&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The site title. No &lt;code&gt;alternateName&lt;/code&gt; in the default output.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Substack&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;on.substack.com&lt;/code&gt;, &lt;code&gt;lennysnewsletter.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;WebSite&lt;/code&gt; with the publication name, only in the Googlebot response. Title reads "Name&lt;/td&gt;
&lt;td&gt;Substack". No &lt;code&gt;og:site_name&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hashnode&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;townhall.hashnode.com&lt;/code&gt;, &lt;code&gt;blog.greenroots.info&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Blog&lt;/code&gt;, not &lt;code&gt;WebSite&lt;/code&gt;, with the full blog title as &lt;code&gt;name&lt;/code&gt;; the same text in &lt;code&gt;og:site_name&lt;/code&gt; and &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The blog title.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordPress&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;yoast.com&lt;/code&gt;, &lt;code&gt;longreads.com&lt;/code&gt;, &lt;code&gt;themeshaper.com&lt;/code&gt;, &lt;code&gt;ma.tt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;yoast.com and longreads.com had a &lt;code&gt;WebSite&lt;/code&gt; node. ThemeShaper and ma.tt had &lt;code&gt;og:site_name&lt;/code&gt; but no &lt;code&gt;WebSite&lt;/code&gt; node. WordPress core doesn't print JSON-LD itself, so the node depends on a plugin or theme.&lt;/td&gt;
&lt;td&gt;With Yoast SEO: Settings, then Site basics, which has &lt;a href="https://yoast.com/help/yoast-seo-settings-site-basics/" rel="noopener noreferrer"&gt;Website name and Alternate website name fields&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hashnode's choice of &lt;code&gt;Blog&lt;/code&gt; is a valid schema.org type, but Google's guide only describes &lt;code&gt;WebSite&lt;/code&gt;, and it doesn't say whether a &lt;code&gt;Blog&lt;/code&gt; node's name is read as a site name preference. On those blogs the title and &lt;code&gt;og:site_name&lt;/code&gt; carry the same text, so the signals at least agree. Long titles like "GreenRoots Blog - A Blog by Tapas Adhikary" are what Google warns may be truncated on some devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Match the wrong name to its cause
&lt;/h2&gt;

&lt;p&gt;The name Google shows usually tells you which rule you've run into.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google shows your domain instead of a name
&lt;/h3&gt;

&lt;p&gt;This is Google's fallback when it isn't confident in the name you gave. Confirm there is a &lt;code&gt;WebSite&lt;/code&gt; block on the root URL and that it validates. Then make the visible home page agree with it: if the structured data says "Field Notes" and the title says "Home | My Blog", you're sending two answers. Add a shorter or more specific &lt;code&gt;alternateName&lt;/code&gt;. A name that describes a category rather than a publication, like "Tech Blog", is one Google says it is unlikely to pick.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google still shows the old name after a rename
&lt;/h3&gt;

&lt;p&gt;The site name comes from the home page, so nothing changes until Google recrawls it. Request indexing for the home page in URL Inspection. Google's guide says crawling can take from several days to several weeks, and internal pages pick up the new name only after they are recrawled too. If you changed domains at the same time, the redirects and the Change of Address tool have to be done first.&lt;/p&gt;

&lt;h3&gt;
  
  
  A subdomain blog shows your main site's name
&lt;/h3&gt;

&lt;p&gt;Google says that when a subdomain's home page has no structured data, the domain-level site name may be used as a fallback. If &lt;code&gt;blog.example.com&lt;/code&gt; should carry its own name, its root URL needs its own &lt;code&gt;WebSite&lt;/code&gt; block. If showing the company name is fine with you, there is nothing to fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  A blog at /blog shows the main site's name
&lt;/h3&gt;

&lt;p&gt;This is the subdirectory rule, and markup won't change it. Google's own example marks &lt;code&gt;https://example.com/news&lt;/code&gt; as unsupported. My own product blog is in that position: it lives under &lt;code&gt;/blog&lt;/code&gt; on the main domain, its home page prints a valid &lt;code&gt;WebSite&lt;/code&gt; block with its own name, and under Google's rules that block can't give it a name separate from the one Google uses for the domain. For most company blogs that's the right result. If a separate name matters more than sharing the main domain, the blog has to move to a subdomain.&lt;/p&gt;

&lt;h3&gt;
  
  
  The name differs between www and the apex, or http and https
&lt;/h3&gt;

&lt;p&gt;Google asks for the same structured data on every duplicate home page, and when a variant redirects, it uses the redirect target. If every variant of your address redirects to one home page, there is nothing to align. A missing redirect leaves two home pages that each need the same markup. Pick one and redirect the other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google shows an acronym or a different form of your name
&lt;/h3&gt;

&lt;p&gt;Google says it may choose an acronym when the site is better known by one, and it generally won't give the same name to two sites that are global in nature. List the forms you're willing to accept in &lt;code&gt;alternateName&lt;/code&gt;, most preferred first. As a last resort, Google's documentation says you can set the lowercase domain itself as &lt;code&gt;name&lt;/code&gt;, and it will generally select that.&lt;/p&gt;

&lt;p&gt;I checked all of this while wiring the &lt;code&gt;WebSite&lt;/code&gt; block into the themes on &lt;a href="https://justblogged.com" rel="noopener noreferrer"&gt;JustBlogged&lt;/a&gt;. The Substack result surprised me the most... the markup only shows up when you ask as Googlebot, so a view-source check says it isn't there&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>blogging</category>
      <category>google</category>
    </item>
    <item>
      <title>Mixed Content After Moving a Blog to HTTPS: Which http:// Images Still Load</title>
      <dc:creator>Usama Ejaz</dc:creator>
      <pubDate>Fri, 16 Oct 2026 06:19:31 +0000</pubDate>
      <link>https://dev.to/usamaejaz/mixed-content-after-moving-a-blog-to-https-which-http-images-still-load-113n</link>
      <guid>https://dev.to/usamaejaz/mixed-content-after-moving-a-blog-to-https-which-http-images-still-load-113n</guid>
      <description>&lt;p&gt;Once a blog is on HTTPS, every image, script, stylesheet and iframe its pages still load over &lt;code&gt;http://&lt;/code&gt; is mixed content, and current browsers no longer handle it one way. A plain &lt;code&gt;&amp;lt;img src="http://..."&amp;gt;&lt;/code&gt; is rewritten to &lt;code&gt;https://&lt;/code&gt; and loads, but only if the image host answers on HTTPS; there is no fallback to the old address. Scripts, stylesheets and iframes are blocked. Responsive images are blocked too: any &lt;code&gt;http://&lt;/code&gt; URL inside &lt;code&gt;srcset&lt;/code&gt; or a &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; source stops the image from rendering, even when the same tag's &lt;code&gt;src&lt;/code&gt; is already &lt;code&gt;https://&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That last case is easy to miss after a migration, because a find-and-replace aimed at &lt;code&gt;src="http://&lt;/code&gt; or a quick look at the post's main image catches the &lt;code&gt;src&lt;/code&gt; and leaves the &lt;code&gt;srcset&lt;/code&gt;. Below is what I measured in Chromium, why the spec treats responsive images differently, a script that lists every &lt;code&gt;http://&lt;/code&gt; subresource on a live blog, and the order I'd fix them in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Chromium 141 did with each http:// reference
&lt;/h2&gt;

&lt;p&gt;On October 15, 2026 I ran a headless Chromium 141.0.7390.37 (through Playwright 1.56.1) against a test page served over HTTPS. The page pulled resources from two local hosts on the standard ports: one that served every file over both HTTP and HTTPS, and one that only spoke HTTP, the way an old personal image server or a lapsed CDN might. The server log recorded which scheme each request actually arrived on, and the page reported whether each image decoded. I ran the same page a second time with an &lt;code&gt;upgrade-insecure-requests&lt;/code&gt; meta tag in the head.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reference in the HTML&lt;/th&gt;
&lt;th&gt;Default behavior&lt;/th&gt;
&lt;th&gt;With &lt;code&gt;upgrade-insecure-requests&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;img src="http://..."&amp;gt;&lt;/code&gt;, host also serves HTTPS&lt;/td&gt;
&lt;td&gt;Upgraded to https, loads&lt;/td&gt;
&lt;td&gt;Upgraded, loads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;img src="http://..."&amp;gt;&lt;/code&gt;, host is HTTP only&lt;/td&gt;
&lt;td&gt;Upgraded, connection refused, broken image&lt;/td&gt;
&lt;td&gt;Same: broken image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;img srcset="http://... 1x"&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Blocked&lt;/td&gt;
&lt;td&gt;Upgraded, loads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;img src="https://..." srcset="http://... 300w, http://... 1024w"&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Blocked; the browser picked a srcset candidate and never used the https &lt;code&gt;src&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Upgraded, loads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;picture&amp;gt;&amp;lt;source srcset="http://..."&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Blocked; the fallback &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; was not used&lt;/td&gt;
&lt;td&gt;Upgraded, loads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inline &lt;code&gt;style="background-image:url(http://...)"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Upgraded, loads&lt;/td&gt;
&lt;td&gt;Upgraded, loads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;link rel="stylesheet" href="http://..."&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Blocked&lt;/td&gt;
&lt;td&gt;Upgraded, applied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;script src="http://..."&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Blocked&lt;/td&gt;
&lt;td&gt;Upgraded, ran&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;iframe src="http://..."&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Blocked&lt;/td&gt;
&lt;td&gt;Upgraded, loads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The console messages match the two outcomes. An upgraded image logs a warning ending in "This request was automatically upgraded to HTTPS", and a blocked one logs an error ending in "This request has been blocked; the content must be served over HTTPS." If you open DevTools on a migrated post and see only warnings, the page is relying on the upgrade, and each of those images depends on its host keeping HTTPS working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a responsive image breaks when its plain src would have loaded
&lt;/h2&gt;

&lt;p&gt;The automatic upgrade comes from the W3C &lt;a href="https://w3c.github.io/webappsec-mixed-content/" rel="noopener noreferrer"&gt;Mixed Content Level 2&lt;/a&gt; draft. It upgrades requests whose destination is an image, audio or video, then lists an exception: a request whose destination is "image" and whose initiator is "imageset" is left alone, and the spec gives the reason as "for historical reasons". Requests that aren't upgraded are blocked. MDN's &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Mixed_content" rel="noopener noreferrer"&gt;mixed content reference&lt;/a&gt; spells out the practical meaning: an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; set through &lt;code&gt;src&lt;/code&gt; is upgradable, while one set "via &lt;code&gt;srcset&lt;/code&gt; or &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt;" is blockable.&lt;/p&gt;

&lt;p&gt;All three engines now apply the upgrade to plain images. Chrome announced the plan in &lt;a href="https://blog.chromium.org/2019/10/no-more-mixed-messages-about-https.html" rel="noopener noreferrer"&gt;2019&lt;/a&gt;, and its update on that post says mixed images are "autoupgraded and blocked if they fail to load over https://". Firefox followed in &lt;a href="https://blog.mozilla.org/security/2024/06/05/firefox-will-upgrade-more-mixed-content-in-version-127/" rel="noopener noreferrer"&gt;version 127&lt;/a&gt;, where images, audio and video "are not loaded if they are unavailable via HTTPS". WebKit's &lt;a href="https://webkit.org/blog/15865/webkit-features-in-safari-18-0/" rel="noopener noreferrer"&gt;Safari 18.0 notes&lt;/a&gt; say images and media "will now be auto-upgraded to HTTPS, in adherence with Mixed Content Level 2". I only ran Chromium, so the srcset result above is measured in the engine behind Chrome and Edge, and for Firefox and Safari it follows from the spec rather than from my test.&lt;/p&gt;

&lt;p&gt;The practical consequence is that the scheme in &lt;code&gt;srcset&lt;/code&gt; decides the image. A browser that chooses a candidate from &lt;code&gt;srcset&lt;/code&gt; doesn't go back to &lt;code&gt;src&lt;/code&gt; when that candidate is blocked, which is why the row with an https &lt;code&gt;src&lt;/code&gt; still rendered as a broken image.&lt;/p&gt;

&lt;h2&gt;
  
  
  List every http:// subresource on the live blog
&lt;/h2&gt;

&lt;p&gt;Checking posts one at a time in DevTools doesn't scale to an archive of a few hundred. The script below reads your sitemap, parses each page, and prints every &lt;code&gt;http://&lt;/code&gt; subresource with the outcome the table predicts. For references a browser would upgrade, it also requests the &lt;code&gt;https://&lt;/code&gt; version, so an image that will break is labelled &lt;code&gt;BROKEN&lt;/code&gt; before a reader finds it. It uses only the Python standard library.&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="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Find http:// subresources on an HTTPS blog and predict what browsers do with each.
Usage: python3 mixed_content_scan.py https://example.com/sitemap.xml  (or page URLs)&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;html.parser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTMLParser&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlsplit&lt;/span&gt;

&lt;span class="n"&gt;UA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mixed-content-scan/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UA&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;https_ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;u&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://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):]&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UA&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip_address&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;urlsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="n"&gt;CSS_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url\(\s*[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\"]?(http://[^&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\")\s]+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTMLParser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_picture&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_starttag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;picture&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_picture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;style&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;srcset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; srcset&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_picture&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;picture&amp;gt;&amp;lt;source src&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;img&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;video&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; src&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upgraded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; src&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;video&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;poster&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;video poster&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;poster&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upgraded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stylesheet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;link stylesheet&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CSS_URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;style&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inline style url()&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upgraded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_endtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;picture&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_picture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;style&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CSS_URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;style&amp;gt; url()&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upgraded (blocked if a font)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;locs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;loc&amp;gt;\s*([^&amp;lt;\s]+)\s*&amp;lt;/loc&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;locs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upgraded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;is_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocked (IP address host)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;https_ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upgraded, but https copy fails: BROKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; pages scanned, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; http:// subresources found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with your sitemap or with individual post URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 mixed_content_scan.py https://example.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints one line per reference. With illustrative URLs, the output looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blocked                                  &amp;lt;img srcset&amp;gt;           http://old.example.com/wp-content/uploads/hero-1024x576.jpg  on https://example.com/hello-world
upgraded, but https copy fails: BROKEN   &amp;lt;img src&amp;gt;              http://photos.example.net/2014/cat.jpg  on https://example.com/my-cat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I checked the classifications against the Chromium test page above, then ran it over the 48 URLs in my own blog's sitemap, which returned no &lt;code&gt;http://&lt;/code&gt; subresources. It reads the HTML the server sends, so it won't see URLs inside external stylesheets or markup that JavaScript inserts after load, and it marks every &lt;code&gt;url()&lt;/code&gt; inside a &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; block as an image even though a font loaded that way is blocked. Those gaps are small on most blogs, but if a theme uses custom fonts from an old host, open one page in DevTools as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix each reference at its source, by the kind of host
&lt;/h2&gt;

&lt;p&gt;The script's output falls into four groups, and each has a different fix. References to your own old address are absolute &lt;code&gt;http://yourdomain.com/...&lt;/code&gt; URLs written into posts before the site had HTTPS. Rewrite them to &lt;code&gt;https://&lt;/code&gt; in the content itself. On a WordPress install, WP-CLI's &lt;a href="https://developer.wordpress.org/cli/commands/search-replace/" rel="noopener noreferrer"&gt;search-replace&lt;/a&gt; handles serialized data, and its documented examples skip the &lt;code&gt;guid&lt;/code&gt; column. Run it with &lt;code&gt;--dry-run&lt;/code&gt; first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp search-replace &lt;span class="s1"&gt;'http://example.com'&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com'&lt;/span&gt; &lt;span class="nt"&gt;--skip-columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;guid &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're leaving WordPress, do the same replacement in the export file before importing it anywhere, so the new platform never stores the old scheme.&lt;/p&gt;

&lt;p&gt;Third-party hosts that answer on HTTPS only need the scheme changed. The upgrade already loads them, but editing the URL removes the dependency on browser behavior, and it fixes the copy of your post that RSS readers and scrapers receive.&lt;/p&gt;

&lt;p&gt;Hosts with no working HTTPS are where readers already see broken images. Download the files while the HTTP address still answers and serve them from your own blog, then point every reference at the new copy. If the HTTP address is already gone, the image has to come from a backup or the Wayback Machine.&lt;/p&gt;

&lt;p&gt;Scripts and iframes are usually old embed codes and widgets. A blocked &lt;code&gt;http://&lt;/code&gt; embed is often a service that has since moved to HTTPS under a new snippet, so replace the snippet rather than editing the scheme.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add upgrade-insecure-requests after the rewrite
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/upgrade-insecure-requests" rel="noopener noreferrer"&gt;upgrade-insecure-requests&lt;/a&gt; directive tells the browser to rewrite every non-navigational &lt;code&gt;http://&lt;/code&gt; request to &lt;code&gt;https://&lt;/code&gt; before sending it. In the test, it rescued every blocked row: srcset images, the picture source, the stylesheet, the script and the iframe. It can be sent as a response header or placed in the page head:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;http-equiv=&lt;/span&gt;&lt;span class="s"&gt;"Content-Security-Policy"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"upgrade-insecure-requests"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works as a safety net for references the rewrite missed. It can't fix a host without HTTPS, which failed in both runs, and MDN notes there's no fallback to HTTP. It also only affects browsers loading your pages, so the &lt;code&gt;http://&lt;/code&gt; URLs remain in your stored content and in your feed. The upgraded script row shows the other reason to keep it as a backstop: a script you would have noticed as blocked now runs silently from whatever currently answers at that address.&lt;/p&gt;

&lt;p&gt;On a host where you control headers, send it as a header instead so it covers every response.&lt;/p&gt;

&lt;p&gt;I went down this path while moving old posts onto custom domains for &lt;a href="https://justblogged.com" rel="noopener noreferrer"&gt;JustBlogged&lt;/a&gt;. The srcset row is the one I didn't expect... a find-and-replace on &lt;code&gt;src&lt;/code&gt; looks done, and the image still doesn't render&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>https</category>
      <category>blogging</category>
    </item>
    <item>
      <title>DNSSEC When Moving a Blog's DNS: Remove the DS Record Before the Nameservers Change</title>
      <dc:creator>Usama Ejaz</dc:creator>
      <pubDate>Wed, 14 Oct 2026 05:51:27 +0000</pubDate>
      <link>https://dev.to/usamaejaz/dnssec-when-moving-a-blogs-dns-remove-the-ds-record-before-the-nameservers-change-nk0</link>
      <guid>https://dev.to/usamaejaz/dnssec-when-moving-a-blogs-dns-remove-the-ds-record-before-the-nameservers-change-nk0</guid>
      <description>&lt;p&gt;DNSSEC can only break a blog migration at one moment: when the domain's nameservers change. Everything else about a host move leaves it alone.&lt;/p&gt;

&lt;p&gt;Here is the failure. Your registrar holds a DS record that tells validating resolvers which key should be signing your zone. Move the zone to a new DNS provider while that DS record still describes the old provider's key, and resolvers like 1.1.1.1, 8.8.8.8 and Quad9 answer with SERVFAIL instead of an IP address. The blog disappears for anyone behind a validating resolver and keeps working for everyone else. So it can look fine on your laptop and be down for half your readers, which is the worst kind of outage to debug.&lt;/p&gt;

&lt;p&gt;The safe order is short: remove the DS record at the registrar, wait out its TTL, change the nameservers, then turn on DNSSEC at the new provider and add its DS record. The rest of this post is how to tell whether it applies to you, three queries that show the state of the chain with nothing but curl, and what to do if you already switched and things broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  A normal host move doesn't touch DNSSEC
&lt;/h2&gt;

&lt;p&gt;Most host moves are an edit inside your existing zone: a new A record for the apex, a new CNAME for &lt;code&gt;www&lt;/code&gt;. The provider that signs your zone signs the edited records too, and the DS record at the registrar still matches because the keys didn't change.&lt;/p&gt;

&lt;p&gt;DNSSEC becomes your problem when the nameservers change. That covers moving DNS to Cloudflare or Route 53, switching from your registrar's default nameservers to custom ones, going back the other way, and transferring the domain to a registrar whose DNS you'll use instead. In every one of those, the party signing the zone changes while the DS record at the registry stays where it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the chain with three queries
&lt;/h2&gt;

&lt;p&gt;Google Public DNS and Cloudflare's 1.1.1.1 both answer DNS queries as JSON over HTTPS. Google's &lt;a href="https://developers.google.com/speed/public-dns/docs/doh/json" rel="noopener noreferrer"&gt;JSON API&lt;/a&gt; takes &lt;code&gt;cd=1&lt;/code&gt; to disable DNSSEC validation, and &lt;a href="https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/" rel="noopener noreferrer"&gt;Cloudflare's&lt;/a&gt; takes the same parameter. Comparing a normal answer with a &lt;code&gt;cd=1&lt;/code&gt; answer tells you whether DNSSEC is the reason a name doesn't resolve.&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;# 1. Is there a DS record at the registry?&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;'https://dns.google/resolve?name=example.com&amp;amp;type=DS'&lt;/span&gt;

&lt;span class="c"&gt;# 2. Does the name resolve with validation on?&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;'https://dns.google/resolve?name=example.com&amp;amp;type=A'&lt;/span&gt;

&lt;span class="c"&gt;# 3. Does it resolve with validation off?&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;'https://dns.google/resolve?name=example.com&amp;amp;type=A&amp;amp;cd=1'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran queries 2 and 3 on October 14, 2026 against &lt;code&gt;dnssec-failed.org&lt;/code&gt;, a test domain whose DS record deliberately doesn't match the keys its nameservers publish. With validation on, Google returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"AD"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"CD"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"Comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"DNSSEC validation failure. Check http://dnsviz.net/d/dnssec-failed.org/dnssec/ ..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"extended_dns_errors"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"info_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"extra_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"No DNSKEY matches DS RRs of dnssec-failed.org"&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;cd=1&lt;/code&gt;, the same resolver returned &lt;code&gt;"Status":0&lt;/code&gt; and the A record &lt;code&gt;96.99.227.255&lt;/code&gt;. Cloudflare gave the same pair through &lt;code&gt;https://cloudflare-dns.com/dns-query&lt;/code&gt; (send the header &lt;code&gt;accept: application/dns-json&lt;/code&gt;), with the reason in its Comment field as &lt;code&gt;EDE(9): DNSKEY Missing&lt;/code&gt;. The records exist and the nameservers answer. Validation is the only thing stopping resolution.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;DS query&lt;/th&gt;
&lt;th&gt;Query 2&lt;/th&gt;
&lt;th&gt;Query 3 (cd=1)&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No answer&lt;/td&gt;
&lt;td&gt;Status 0&lt;/td&gt;
&lt;td&gt;Status 0&lt;/td&gt;
&lt;td&gt;DNSSEC is off at the registry. A nameserver change won't trip on it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer present&lt;/td&gt;
&lt;td&gt;Status 0, &lt;code&gt;"AD":true&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Status 0&lt;/td&gt;
&lt;td&gt;The chain validates. Remove the DS record before you change nameservers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer present&lt;/td&gt;
&lt;td&gt;Status 2 (SERVFAIL)&lt;/td&gt;
&lt;td&gt;Status 0 with an answer&lt;/td&gt;
&lt;td&gt;The chain is broken. Fix the DS record, not your A or CNAME records.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Either&lt;/td&gt;
&lt;td&gt;Status 2&lt;/td&gt;
&lt;td&gt;Status 2&lt;/td&gt;
&lt;td&gt;Something other than DNSSEC is failing, usually nameservers that don't answer for the zone.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The extended error code narrows it down further. &lt;a href="https://www.rfc-editor.org/rfc/rfc8914.html" rel="noopener noreferrer"&gt;RFC 8914&lt;/a&gt; defines code 9, DNSKEY Missing, as "A DS record existed at a parent, but no supported matching DNSKEY record could be found for the child". That is exactly what a DS record left over from the old provider produces. Code 10, RRSIGs Missing, means signatures were expected and none came back, which fits a new provider that isn't signing the zone yet. Cloudflare lists the codes 1.1.1.1 returns on its &lt;a href="https://developers.cloudflare.com/1.1.1.1/infrastructure/extended-dns-error-codes/" rel="noopener noreferrer"&gt;Extended DNS Error codes page&lt;/a&gt;. If you want a picture of the whole chain, &lt;a href="https://dnsviz.net/" rel="noopener noreferrer"&gt;DNSViz&lt;/a&gt; and Verisign's &lt;a href="https://dnssec-analyzer.verisignlabs.com/" rel="noopener noreferrer"&gt;DNSSEC Analyzer&lt;/a&gt; both take a domain and show where it breaks.&lt;/p&gt;

&lt;p&gt;Look at the TTL on the DS answer too, because it decides how long the move takes. On the same day, the DS record for &lt;code&gt;cloudflare.com&lt;/code&gt; came back from 1.1.1.1 with a TTL of 86400 seconds (one day) and from Google with 7212, which is just the time left in Google's cache. A fresh answer shows the registry's full value. A cached one shows what remains.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order that works when DNS moves to a new provider
&lt;/h2&gt;

&lt;p&gt;Cloudflare's &lt;a href="https://developers.cloudflare.com/dns/dnssec/" rel="noopener noreferrer"&gt;DNSSEC docs&lt;/a&gt; and Route 53's guide to &lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/migrate-dns-domain-in-use.html" rel="noopener noreferrer"&gt;migrating a domain that's in use&lt;/a&gt; describe the same sequence. Route 53 states the constraint plainly: "You can't have DNSSEC signing enabled across two providers at the same time."&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Recreate every record at the new provider: the blog's A and CNAME records, MX for email, and any TXT verification records. Leave DNSSEC off there for now.&lt;/li&gt;
&lt;li&gt;Remove the DS record at your registrar, and leave signing on at the old provider. Route 53's &lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-configuring-dnssec-disable.html" rel="noopener noreferrer"&gt;disable procedure&lt;/a&gt; warns that "some registrars have scheduled DS removal, for example once a day", so confirm it's gone by asking the registry's own nameserver, for example &lt;code&gt;dig +norec DS example.com @a.gtld-servers.net&lt;/code&gt; for a .com.&lt;/li&gt;
&lt;li&gt;Wait out the DS TTL. Cloudflare says it is commonly 24 to 48 hours for most TLDs, and its &lt;a href="https://developers.cloudflare.com/dns/dnssec/troubleshooting/" rel="noopener noreferrer"&gt;troubleshooting page&lt;/a&gt; says to wait at least 1.5 times the TTL: "If the DS TTL is 24 hours, wait 36 hours."&lt;/li&gt;
&lt;li&gt;Change the nameservers at the registrar and wait for the old NS TTL. Route 53 notes the NS record's typical TTL is two days unless you lowered it beforehand.&lt;/li&gt;
&lt;li&gt;Turn on DNSSEC at the new provider and add its DS record at the registrar. Cloudflare Registrar &lt;a href="https://developers.cloudflare.com/registrar/get-started/enable-dnssec/" rel="noopener noreferrer"&gt;adds the DS record itself&lt;/a&gt;, which "can take one to two days", and registrars that read CDS records can pick it up automatically. Everywhere else you paste the key tag, algorithm, digest type and digest by hand.&lt;/li&gt;
&lt;li&gt;Run the three queries again. Query 2 should now come back with &lt;code&gt;"AD":true&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Between steps 2 and 5 the domain resolves without DNSSEC protection. &lt;a href="https://www.rfc-editor.org/rfc/rfc6781.html" rel="noopener noreferrer"&gt;RFC 6781&lt;/a&gt; calls that window "the only viable operation" when the old and new providers don't cooperate. The way around it is multi-signer DNSSEC from &lt;a href="https://www.rfc-editor.org/rfc/rfc8901.html" rel="noopener noreferrer"&gt;RFC 8901&lt;/a&gt;, where both providers' keys are published during the move. Cloudflare supports it as an &lt;a href="https://developers.cloudflare.com/dns/dnssec/dnssec-active-migration/" rel="noopener noreferrer"&gt;active migration&lt;/a&gt;, but only if the old provider lets you add DNSKEY records at the zone apex. I think for a blog that's overkill... I'd take the unsigned day or two over coordinating key imports between two DNS companies, unless the old provider already documents multi-signer support.&lt;/p&gt;

&lt;h2&gt;
  
  
  What registrars do on their own when you change nameservers
&lt;/h2&gt;

&lt;p&gt;Some registrars handle part of this for you, and their help pages differ in ways that matter mid-move. These are from each registrar's docs as of October 14, 2026.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Registrar&lt;/th&gt;
&lt;th&gt;Documented behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Squarespace Domains&lt;/td&gt;
&lt;td&gt;DNSSEC is "automatically enabled" on supported TLDs and "automatically disabled if you switch to using custom nameservers" (&lt;a href="https://support.squarespace.com/hc/en-us/articles/31094668921229-DNSSEC-for-Squarespace-domains" rel="noopener noreferrer"&gt;source&lt;/a&gt;). You can add one DS record for custom nameservers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Namecheap&lt;/td&gt;
&lt;td&gt;Automatic on BasicDNS and PremiumDNS (&lt;a href="https://www.namecheap.com/support/knowledgebase/article.aspx/9723/2232/managing-dnssec-for-domains-pointed-to-premium-or-basicdns/" rel="noopener noreferrer"&gt;source&lt;/a&gt;). With custom DNS you manage DS records yourself, and turning DNSSEC back on re-activates "the records you previously configured" (&lt;a href="https://www.namecheap.com/support/knowledgebase/article.aspx/9722/2232/managing-dnssec-for-domains-pointed-to-custom-dns/" rel="noopener noreferrer"&gt;source&lt;/a&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Porkbun&lt;/td&gt;
&lt;td&gt;Recommends deleting DNSSEC records before transferring a domain in, then turning on Porkbun's DNSSEC or adding your own afterward (&lt;a href="https://kb.porkbun.com/article/89-how-to-transfer-a-domain-to-porkbun-with-no-downtime" rel="noopener noreferrer"&gt;source&lt;/a&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Registrar&lt;/td&gt;
&lt;td&gt;Publishes the DS record automatically from the zone's CDS records. "There is currently no way to manually control the exact timing of DS publication" (&lt;a href="https://developers.cloudflare.com/dns/dnssec/" rel="noopener noreferrer"&gt;source&lt;/a&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Namecheap is the one that bites. Switch to custom nameservers, re-enable DNSSEC, and the old DS values come back, describing keys the new provider doesn't have. Replace them with the new provider's values before you save. Squarespace disables DNSSEC as part of the nameserver change itself, so a resolver that cached the DS record earlier can still fail until that cache expires. If your registrar lets you remove the DS record as a separate step, do it a day or two ahead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Already switched and getting SERVFAIL
&lt;/h2&gt;

&lt;p&gt;Go to the registrar and either delete the DS record or replace it with the one your new provider shows. Deleting is quicker to get right, and you can add the new DS record once the domain resolves. Google's &lt;a href="https://developers.google.com/speed/public-dns/docs/troubleshooting/domains" rel="noopener noreferrer"&gt;troubleshooting page&lt;/a&gt; gives the same fix: "ask your domain registrar to remove stale DS records."&lt;/p&gt;

&lt;p&gt;Resolvers that cached the old DS record keep failing until its TTL runs out, so some readers can see the error for up to a day after the fix. Cloudflare's &lt;a href="https://one.one.one.one/purge-cache/" rel="noopener noreferrer"&gt;purge cache tool&lt;/a&gt; and Google's &lt;a href="https://developers.google.com/speed/public-dns/cache" rel="noopener noreferrer"&gt;flush cache page&lt;/a&gt; each clear one resolver's cache for a name you enter. That helps readers on those two resolvers and does nothing for an ISP's resolver. Don't touch your A and CNAME records while you wait. They aren't the problem, and editing them adds a second unknown.&lt;/p&gt;

&lt;p&gt;A broken chain also stops your new host from getting a certificate. Under CA/Browser Forum &lt;a href="https://cabforum.org/2025/06/18/ballot-sc-085v2-require-validation-of-dnssec-when-present-for-caa-and-dcv-lookups/" rel="noopener noreferrer"&gt;ballot SC-085v2&lt;/a&gt;, CAs have had to validate DNSSEC on CAA lookups since March 15, 2026, and the &lt;a href="https://cabforum.org/working-groups/server/baseline-requirements/documents/" rel="noopener noreferrer"&gt;Baseline Requirements&lt;/a&gt; say a validation error such as SERVFAIL "MUST NOT be treated as permission to issue." So a host that issues HTTPS automatically can sit at "pending" until the DS record is fixed, with no useful error. I wrote up the rest of that lookup in &lt;a href="https://dev.to/usamaejaz/caa-records-and-your-blog-host-which-record-applies-and-which-ca-to-allow-410e"&gt;CAA records and your blog host&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I went through all of this while wiring custom domains on &lt;a href="https://justblogged.com" rel="noopener noreferrer"&gt;JustBlogged&lt;/a&gt;. If a domain is stuck on "pending" after a nameserver change, those three queries tell you in a minute whether DNSSEC is the reason&lt;/p&gt;

</description>
      <category>dns</category>
      <category>security</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
