skills / wp-static-clone
AGENTS.md100 starsChanged 7 months agoMITView on GitHub
Detailed commands and recipes for each phase. Read this when you're implementing a specific step. Parent SKILL.md has the workflow, gotchas, and output structure. Fetch the index, then any child sitemaps it references, then concatenate page URLs into urls.txt. Adjust grep on the <loc> extraction if the source uses non-Yoast naming. Single invocation over urls.txt. The browser UA + Accept / Accept-Language headers are mandatory — Cloudflare's bot protection 403s the default Wget/1.x UA. If a non-English source has localised…
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,774 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository17 other files it gives its agents.
- astro-seo/AGENTS.mdAGENTS.md
- github-profile/AGENTS.mdAGENTS.md
- github-repo/AGENTS.mdAGENTS.md
- static-seo/AGENTS.mdAGENTS.md
- wp-readme-optimizer/AGENTS.mdAGENTS.md
- astro-github-actionsSkill
- astro-seoSkill
- content-seoSkill
- emdash-github-actionsSkill
- github-profileSkill
- github-repoSkill
- metadata-checkSkill
- readability-checkSkill
- static-seoSkill
- wp-github-actionsSkill
- wp-readme-optimizerSkill
- wp-static-cloneSkill
The file
We split this AGENTS.md into the rules it gives an agent
4 rules. About 0 are common, 0 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 59
If a non-English source has localised content negotiation, swap Accept-Language accordingly.
- Very specific: line 91
Leave the <a href="https://sitemaps.org"> link further down — it's still factually correct.
- Very specific: line 120
After scripts/strip-wp-runtime.py and scripts/selfhost-gravatars.py, audit for any source-domain URLs left:
- Very specific: line 184
If the site has any URL changes (slug changes, dropped /page/N/ archive pagination, redirect from /category/<term>/ to a flat /blog/, …
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
wp-static-clone — Implementation recipes
Detailed commands and recipes for each phase. Read this when you're implementing a specific step. Parent SKILL.md has the workflow, gotchas, and output structure.
Phase 1 — Sitemaps and Yoast XSL
Fetch the index, then any child sitemaps it references, then concatenate page URLs into urls.txt. Adjust grep on the <loc> extraction if the source uses non-Yoast naming.
mkdir -p output
ROOT=https://example.com # source site origin
curl -s -A "Mozilla/5.0" "$ROOT/sitemap_index.xml" \
| tee output/sitemap_index.xml \
| grep -oE '<loc>[^<]+</loc>' \
| sed -E 's|</?loc>||g' \
| grep -v 'image-sitemap' \
> child-sitemaps.txt
while read -r child; do
name=$(basename "$child")
curl -s -A "Mozilla/5.0" "$child" -o "output/$name"
grep -oE '<loc>[^<]+</loc>' "output/$name" \
| sed -E 's|</?loc>||g'
done < child-sitemaps.txt | sort -u > urls.txt
# Yoast XSL (purely so sitemaps render in browser; search engines parse XML directly)
mkdir -p output/wp-content/plugins/wordpress-seo/css
curl -s -A "Mozilla/5.0" \
"$ROOT/wp-content/plugins/wordpress-seo/css/main-sitemap.xsl" \
-o output/wp-content/plugins/wordpress-seo/css/main-sitemap.xslPhase 2 — One-shot wget
Single invocation over urls.txt. The browser UA + Accept / Accept-Language headers are mandatory — Cloudflare's bot protection 403s the default Wget/1.x UA.
wget -p -k -E -nH \
--restrict-file-names=unix \
-e robots=off \
-U "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" \
--header="Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" \
--header="Accept-Language: en,*;q=0.5" \
--wait=0.3 --random-wait \
-i urls.txt -P output/Flags:
-ppage requisites (CSS / JS / images / fonts referenced from HTML)-kconvert links to local-Eadd.htmlextension;/foo/→foo/index.html-nHskip the host directory wrapper--restrict-file-names=unixkeep filenames clean on macOS / Linux-e robots=offignore robots.txt--wait=0.3 --random-waitpaces requests so Cloudflare doesn't escalate to 403 partway through
If a non-English source has localised content negotiation, swap Accept-Language accordingly. Language doesn't matter to anything else in the pipeline (the runtime-strip and reply-link regexes match by class, not text).
Phase 3 — Pull missing absolutely-referenced assets
wget -p follows <script src>, <link rel="stylesheet">, <img src> / srcset, <source>, etc. It does not follow og:image, apple-touch-icon, JSON-LD image / logo, msapplication-TileImage, or <link rel="modulepreload">. Audit and fetch the long tail across all three asset roots:
SRC=example.com
grep -rhoE "https?://$SRC/wp-content/(uploads|themes|plugins)/[^\"<'> ]*" output/ \
| sort -u > missing.txt
wget -x -nH -e robots=off \
-U "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ..." \
-i missing.txt -P output/-x (force directories) is important so files land at their original paths inside wp-content/....
Phase 5 — Yoast XSL attribution edit
import pathlib
p = pathlib.Path('output/wp-content/plugins/wordpress-seo/css/main-sitemap.xsl')
old = ('Generated by <a href="https://yoa.st/1y5" target="_blank" '
'rel="noopener">Yoast SEO</a>, this is an XML Sitemap, meant '
'for consumption by search engines.')
new = ('Generated statically and deployed via a static host, this is an XML '
'Sitemap, meant for consumption by search engines.')
p.write_text(p.read_text(encoding='utf-8').replace(old, new), encoding='utf-8')Leave the <a href="https://sitemaps.org"> link further down — it's still factually correct.
Phase 6 — Plausible analytics
WordPress's Plausible plugin proxies the script through /wp-content/uploads/<hash>/pa-XXX.js and posts events back to /wp-json/.../v1/.... Replace the entire two-script block with the standard tracker:
import pathlib, re
PATTERN = re.compile(
r'<script[^>]*id="plausible-analytics-js"></script>\s*'
r'<script id="plausible-analytics-js-after">.*?</script>',
re.DOTALL,
)
REPLACEMENT = (
'<script defer data-domain="example.com" '
'src="https://plausible.io/js/script.js"></script>'
)
for path in pathlib.Path('output').rglob('*.html'):
text = path.read_text(encoding='utf-8')
new = PATTERN.sub(REPLACEMENT, text)
if new != text:
path.write_text(new, encoding='utf-8')Then delete the now-unused proxy file at output/wp-content/uploads/<hash>/pa-*.js.
Phase 6 — Audit remaining absolute URLs
After scripts/strip-wp-runtime.py and scripts/selfhost-gravatars.py, audit for any source-domain URLs left:
SRC=example.com
grep -hE "https?://$SRC/" --include="*.html" -r output/ \
| grep -vE 'rel="canonical"|og:url|"@id"|"url"|"item"|wp-json/oembed' \
| sort -uAnything left after the canonical / JSON-LD allow-list is something to triage. Common offenders:
- Author archive links (
<a class="wp-block-post-author-name__link" href=".../author/<slug>/">). On single-author sites WordPress often 301s these to the homepage. Probe withcurl -sI -o /dev/null -w "%{redirect_url}\n" <author-url>. If they redirect to/, strip the<a>wrapper and keep the inner text — don't waste a directory on a duplicate of the home page. modulepreloadabsolute URLs (<link rel="modulepreload" href="https://<domain>/wp-includes/...">). Convert to root-relative and fetch the underlying file (wget-pdoesn't followmodulepreload).- Server-rendered iframes pointing at custom PHP endpoints (e.g.
<iframe src="/<custom>/<script>.php">). These won't render statically. Drop the wrapping<p>. - Gravity Forms script blocks — wrappers have inline
<script>tags (noid, just bare<script>tags referencinggform.*) above and below the form, plus an in-<head>polyfill. Strip them all — match<script>...</script>whose body mentionsgform. Otherwise the page errors at load whengformis undefined. gform_not_foundparagraphs — pre-existing brokenness on the live site (failed shortcode render). Strip the paragraph and any orphaned lead-in heading.- Page archive pagination anchors (
<a href=".../page/N/">). The/page/N/URLs aren't in the sitemap, so the targets don't exist. Strip the link, keep the anchor text only.
Phase 7 — robots.txt
ROOT=https://example.com
curl -s -A "Mozilla/5.0" "$ROOT/robots.txt" -o output/robots.txt
# If you flatten output/* into the repo root, the sitemap reference is fine.
# If you keep things under output/ and configure the host accordingly, leave alone.
# If the deployed sitemap path differs, edit:
# sed -i.bak -E 's|^Sitemap: .*$|Sitemap: https://new.example.com/sitemap_index.xml|' output/robots.txt
# (Delete the .bak after — `-i.bak` is the portable form that works on both BSD and GNU sed.)Phase 8 — Verify
cd output && python3 -m http.server 8765 &
PORT=8765
# 1. Every URL in urls.txt resolves
while read -r url; do
path=$(echo "$url" | sed -E 's|^https?://[^/]+||; s|/?$|/|')
code=$(curl -sI -o /dev/null -w "%{http_code}" "http://localhost:$PORT$path")
[ "$code" = "200" ] || echo "MISS $code $path"
done < ../urls.txt
# 2. No remaining absolute source-domain URLs outside the allow-list
grep -hE "https?://example\.com/" --include="*.html" -r . \
| grep -vE 'rel="canonical"|og:url|"@id"|"url"|"item"|wp-json/oembed' \
| head
# 3. Spider-crawl for broken internal links
wget --spider -r -nd --no-parent -l 5 "http://localhost:$PORT/" 2>&1 \
| grep -E '404|broken'Then open the homepage and one deep page in a browser. Watch srcset images, sidebar widgets, the header banner — those break silently if missed.
Phase 9 — Deploy
Pick the recipe matching the target host from Phase 0.
Cloudflare Pages
Defaults: no build command, no build output directory, no root directory. Pages serves the repo root.
If the site has any URL changes (slug changes, dropped /page/N/ archive pagination, redirect from /category/<term>/ to a flat /blog/, etc.), add _redirects at the repo root:
/old-path/ /new-path/ 301
/category/* /blog/ 301Long-cache the hashed assets via _headers:
/wp-content/*
Cache-Control: public, max-age=31536000, immutable
/wp-includes/*
Cache-Control: public, max-age=31536000, immutablePush to GitHub and Pages auto-deploys. gh repo create <user>/<repo> --private --source=. --push works.
Netlify
Same _redirects and _headers syntax as Cloudflare Pages. Optional netlify.toml:
[build]
publish = "."Connect the repo via the Netlify dashboard.
Vercel
vercel.json at the repo root:
{
"redirects": [
{ "source": "/old-path/", "destination": "/new-path/", "permanent": true }
],
"headers": [
{
"source": "/wp-content/(.*)",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
}
],
"cleanUrls": false,
"trailingSlash": true
}trailingSlash: true matches WordPress's default URL shape (and what wget -E produces). If the source uses no trailing slashes, set to false.
Generic (nginx)
server {
listen 80;
server_name example.com;
root /var/www/wp-static-clone;
index index.html;
# Trailing-slash URLs map to <slug>/index.html
location / {
try_files $uri $uri/ $uri/index.html =404;
}
location ~ ^/wp-content/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}Generic (Apache)
.htaccess at the document root:
Options +MultiViews
DirectoryIndex index.html
<FilesMatch "^.+\.(jpg|jpeg|png|webp|svg|css|js|woff2?)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
ErrorDocument 404 /404.htmlDoes it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

