<?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: Dmitriy Gabdrakhmanov</title>
    <description>The latest articles on DEV Community by Dmitriy Gabdrakhmanov (@wpservicelab).</description>
    <link>https://dev.to/wpservicelab</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%2F4212347%2Fac303aa8-958a-4823-b4d9-d63c8c1be8b8.jpg</url>
      <title>DEV Community: Dmitriy Gabdrakhmanov</title>
      <link>https://dev.to/wpservicelab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wpservicelab"/>
    <language>en</language>
    <item>
      <title>Rebuilding a 7,230-URL site from the Wayback Machine into WordPress: CDX, raw bytes and legacy ?p= URLs</title>
      <dc:creator>Dmitriy Gabdrakhmanov</dc:creator>
      <pubDate>Wed, 28 Oct 2026 10:41:44 +0000</pubDate>
      <link>https://dev.to/wpservicelab/rebuilding-a-7230-url-site-from-the-wayback-machine-into-wordpress-cdx-raw-bytes-and-legacy-p-36p3</link>
      <guid>https://dev.to/wpservicelab/rebuilding-a-7230-url-site-from-the-wayback-machine-into-wordpress-cdx-raw-bytes-and-legacy-p-36p3</guid>
      <description>&lt;p&gt;When a site goes offline without a backup, or you buy an expired domain with history, the usual advice is to run a downloader from GitHub. Tools like wayback-machine-downloader do a fair job of fetching files, but you end up with a folder of static HTML, legacy URLs like &lt;code&gt;?p=123&lt;/code&gt; saved as files your host won't serve, and no record of what the archive never had.&lt;/p&gt;

&lt;p&gt;I've built 150+ sites on expired domains over the past two years, and since spring 2026 I restore sites from the archive into WordPress with my own CLI pipeline. The largest so far was 63,000 pages. Here is how it works, using a real run on &lt;code&gt;criminalbrief.com&lt;/code&gt;, a mystery fiction blog archived since 2007.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Use the CDX index, not the calendar
&lt;/h2&gt;

&lt;p&gt;The calendar only shows when the crawler visited a given URL, usually the homepage. Deep posts may have been captured once, years apart. The CDX index shows everything the archive holds for a domain:&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="s2"&gt;"https://web.archive.org/cdx/search/cdx?url=criminalbrief.com/*&amp;amp;to=20250901&amp;amp;output=json&amp;amp;fl=original,statuscode,timestamp,mimetype,digest&amp;amp;collapse=urlkey"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; inventory.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For criminalbrief.com this gave &lt;strong&gt;7,230 known URLs&lt;/strong&gt; (the CDX index plus URLs linked from saved pages). Every URL gets a status and a reason in a ledger. The result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;4,822 (66.7%) restored&lt;/strong&gt; on their original URLs: posts, pages, categories and media, about 3,000 of them HTML pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1,418 filtered by policy&lt;/strong&gt; (410): &lt;code&gt;?rest_route=&lt;/code&gt; calls (1,228), feeds, filter parameters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;922 never captured&lt;/strong&gt; (410): addresses linked from saved pages that the archive has no copy of, often broken links like &lt;code&gt;/Bob%20Martinez/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;32 admin URLs&lt;/strong&gt; excluded&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;36 failed&lt;/strong&gt;: the archive kept returning errors after retries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxxdierjlvdny5apb8cjd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxxdierjlvdny5apb8cjd.jpg" alt="QA report header for criminalbrief.com: 4,822 restored out of 7,230 URLs" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;External files are counted separately: images and scripts hosted on other sites that the archive never saved (261 files on 204 hosts here). They are listed in the report, and nothing is substituted from live sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Fetch raw bytes with &lt;code&gt;id_&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A normal archive URL returns rewritten HTML: toolbar, &lt;code&gt;wombat.js&lt;/code&gt;, and every link pointing to &lt;code&gt;web.archive.org&lt;/code&gt;. Add &lt;code&gt;id_&lt;/code&gt; right after the timestamp and you get the bytes the original server sent:&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="s2"&gt;"https://web.archive.org/web/20150920071842id_/http://criminalbrief.com/?p=9666"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; page-9666.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two more things matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gap-fill.&lt;/strong&gt; A post captured in 2015 may reference a stylesheet or image that was only captured in 2014 or 2017. A second pass looks for those files across the whole window and deduplicates them by the CDX &lt;code&gt;digest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits.&lt;/strong&gt; The archive answers &lt;code&gt;429&lt;/code&gt; often. I keep about 1 request per second and respect &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Keep legacy &lt;code&gt;?p=&lt;/code&gt; and &lt;code&gt;?cat=&lt;/code&gt; URLs alive in WordPress
&lt;/h2&gt;

&lt;p&gt;Backlinks often point to &lt;code&gt;/?p=9666&lt;/code&gt;, &lt;code&gt;/?cat=7&lt;/code&gt;, &lt;code&gt;/?m=201405&lt;/code&gt; or &lt;code&gt;.html&lt;/code&gt; paths. With pretty permalinks on, WordPress runs &lt;code&gt;redirect_canonical&lt;/code&gt; and sends &lt;code&gt;/?p=9666&lt;/code&gt; to a 301 on the new slug. &lt;code&gt;/?cat=7&lt;/code&gt; gets rendered by the theme's archive template, and the original markup is lost.&lt;/p&gt;

&lt;p&gt;What I do at build time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;import posts with their &lt;strong&gt;original IDs&lt;/strong&gt; in &lt;code&gt;wp_posts&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;convert old encodings to UTF-8 and recalculate serialized string lengths (&lt;code&gt;s:N:"..."&lt;/code&gt;) when replacing the hostname&lt;/li&gt;
&lt;li&gt;add an &lt;strong&gt;MU plugin router&lt;/strong&gt; that matches &lt;code&gt;path + query&lt;/code&gt; against a route map before WordPress parses query vars&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The smallest piece of this you can try yourself is stopping the canonical redirect for legacy query URLs:&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="c1"&gt;// Minimal example: keep legacy ?p=, ?cat=, ?tag=, ?m= URLs on their exact address.&lt;/span&gt;
&lt;span class="c1"&gt;// Not the full router: it only stops the 301, the theme still renders the page.&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;'redirect_canonical'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$redirect_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$requested_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;parse_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$requested_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;PHP_URL_QUERY&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="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/^(p|cat|tag|m)=/'&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;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&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;$redirect_url&lt;/span&gt;&lt;span class="p"&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the full router, &lt;code&gt;/?p=9666&lt;/code&gt; and &lt;code&gt;/?cat=7&lt;/code&gt; answer &lt;code&gt;200 OK&lt;/code&gt; with their original markup, stored as editable HTML blocks, and new posts work in the normal editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. 410 instead of redirecting everything to the homepage
&lt;/h2&gt;

&lt;p&gt;Redirecting every missing URL to &lt;code&gt;/&lt;/code&gt; hides what was lost, and Google says redirecting many old URLs to an irrelevant page like the homepage &lt;a href="https://developers.google.com/search/docs/crawling-indexing/site-move-with-url-changes" rel="noopener noreferrer"&gt;may be treated as a soft 404&lt;/a&gt;. In this run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;200&lt;/code&gt; for restored pages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;301&lt;/code&gt; only where the original site redirected in the archive (0 in this run)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;410&lt;/code&gt; for junk and never-captured URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Google, &lt;a href="https://developers.google.com/crawling/docs/troubleshooting/http-status-codes" rel="noopener noreferrer"&gt;404 and 410 are handled the same&lt;/a&gt;. The point is an honest status instead of a homepage redirect. Every non-200 URL goes into &lt;code&gt;redirects.csv&lt;/code&gt; and the QA report with a reason. A few real rows from the report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;URL                                                              status  reason
http://criminalbrief.com/?feed=rss                               gone    not restored by policy (feed, media or non-page address)
http://criminalbrief.com/?antiselect=1031223                     gone    not restored by policy (feed, media or non-page address)
https://criminalbrief.com/Bob%20Martinez/                        gone    no copy of this address in the web archive
http://www.criminalbrief.com/stats/webalizer/usage_200706.html   failed  the archive copy could not be downloaded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Failed URLs answer 410 for now and are retried automatically on the next run. Anyone can run &lt;code&gt;curl -I&lt;/code&gt; on any of these addresses and check that the live response matches the report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you launch a restored site
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Compare with CDX, not the calendar.&lt;/strong&gt; Count &lt;code&gt;statuscode:200&lt;/code&gt; + &lt;code&gt;mimetype:text/html&lt;/code&gt; URLs in the index and check that deep pages from earlier years made it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test legacy URLs.&lt;/strong&gt; Take 10-20 addresses from your backlink profile, especially &lt;code&gt;?p=&lt;/code&gt;, &lt;code&gt;?cat=&lt;/code&gt; and &lt;code&gt;.html&lt;/code&gt;, and run &lt;code&gt;curl -I&lt;/code&gt;. They should answer &lt;code&gt;200 OK&lt;/code&gt; with no redirect to a new slug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grep for archive traces.&lt;/strong&gt; Search the files and the SQL dump for &lt;code&gt;web.archive.org&lt;/code&gt;, &lt;code&gt;wayback&lt;/code&gt;, &lt;code&gt;wombat&lt;/code&gt; and &lt;code&gt;/web/20&lt;/code&gt;. A clean build has zero matches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request a URL that never existed.&lt;/strong&gt; It should return 410 or 404, not a 301 to &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;More screenshots and report excerpts: &lt;a href="https://wpservicelab.com/samples/" rel="noopener noreferrer"&gt;wpservicelab.com/samples&lt;/a&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>seo</category>
      <category>php</category>
    </item>
  </channel>
</rss>
