What it is
A friend sent me ps.ps and I could not tell what it was. It is a Chinese-language portrait-photography blog: 28 short posts on lighting, composition, posing and 妆造, running Z-BlogPHP 1.7.4 on nginx, with the whole site — 53 pages, 84 assets, 9.8 MB of images — mirrored here and browsable offline. The domain is the joke: .ps is Palestine, and PS is Photoshop.
Version history
Straight from the project’s own card data — what each version added.
53 pages + 84 assets mirrored (10.6 MB); URL→file routing table, 5 documented rewrite rules, fidelity proved by re-fetch + replay byte comparison, and a headless-Chromium render check with zero failed requests.
Files on the site
What broke, and what I’d change
Not written yet. This is the part only I can write — what actually went wrong, and what I’d do differently. It lives in builds/reflections/psps-mirror.md (see _TEMPLATE.md); as soon as that file has words in it, this section fills in on the next build.
Everything else on this page — the spec, the version history, the file counts — is generated from the project’s own build notes and files, so nothing here is guesswork.
Build spec
The project’s own build notes, rendered. Open the raw .md ↗
Show the spec
Build spec — full mirror of ps.ps
Slug: psps · Status: done · Date: 21 Oct 2026 · Origin: https://ps.ps
Goal
Isaac was sent ps.ps with “what even is that”. Answer that three ways at once:
- say what the site is in plain English,
- say whether it is safe to open,
- keep a full, browsable copy of it in the portfolio, plus a long-form teardown.
The mirror is the deliverable — a screenshot would not survive the origin going away, and this is an aggregated content blog whose posts are mostly images and short lists.
Deliverables
demos/psps-mirror/ | 53 pages + 84 assets, 10.6 MB, browsable with no server |demos/psps-mirror/MIRROR.json | URL → local path → origin sha256, plus the rewrite rules |demos/psps-analysis.html | long-form teardown (the card's “Read the analysis”) |prompts/psps-mirror-prompt.md | this spec |Routing (Z-BlogPHP uses query strings, static hosting does not)
/ | index.html |/?id=N | p/N.html |/?cate=N | cate/N.html |/?cate=N&page=M | cate/N-M.html |/?page=M | page/M.html |/?date=YYYY-MM | date/YYYY-MM.html |/?date=YYYY-MM&page=M | date/YYYY-MM-M.html |/zb_users/…, /zb_system/… | same path, kept at mirror root |Rewrite rules (the only edits inside any page)
- Internal absolute links
https://ps.ps/…→ the mapping above, relative to the file's depth. - Root-absolute assets → depth-relative, so
/portfolio/demos/psps-mirror/works under a subpath. - Search form
actiondisabled (#+onsubmit="return false") — a static mirror must not POST to the zb_system/script/c_html_js_add.php?…saved as.jsso the server sends a JavaScript content-type;feed.phpsaved asfeed.xmlso browsers parse it instead of downloading it.<link rel="canonical">andog:urlare left pointing at the origin — they are metadata, not
live blog. The box stays visible, inert, and the analysis page says so.
bloghost/ajaxurl inside it blanked for the same reason as (3).
navigation.
Verification (do this, don't claim it)
- Link closure: parse every local HTML file, resolve every relative
href/src/action, assert the - Fidelity: re-fetch each origin URL, replay the exact rewrite function, byte-compare with the local
- Render: serve the folder, load 5 representative pages in headless Chromium, assert 0 failed requests
- Safety, passive only: headers, HTTP→HTTPS behaviour, unknown-path response, outbound links,
target exists. Repeat the crawl until no new URL appears. Result: 0 unresolved.
file. Any mismatch is a bug in the rewrite, not in the capture.
and non-empty innerText.
URLhaus. Never log in, never submit a form.
Pitfalls hit on this build
- Grep-based
?page=rewriting corrupts?date=YYYY-MM&page=M— parse the query string, then match - A “skip if
rel="canonical"within N chars” heuristic silently skips the<link>that follows it. Match - The idempotent pass must pass unknown paths through untouched, or a second run mangles already-relative
- The theme's icon font is referenced from CSS, not HTML — parse
url()in stylesheets too.
longest-first (cate&page, then cate, then date&page, then date, then page).
the enclosing tag instead.
links (="./q/x" → "./x" class of bug).
Known gaps (state them, don't paper over)
?id=2, ?id=21, ?cate=3/5/6 are 404 at the origin. No sitemap and no tag index, so the crawl is bounded by what the pages link to. Search and comments are server-side and cannot work statically. Snapshot: 21 Oct 2026.