fix(seo): serve real 404, use clean URLs in sitemap, canonicals and links#126
Merged
Conversation
…inks
Search Console reported 6 pages indexed vs 7 not indexed. Two root causes:
1. No docs/404.html existed, so Cloudflare Pages fell through to index.html
with HTTP 200 for ANY unknown path — a global soft-404 giving Google an
infinite surface of duplicate pages ("alternate page with proper canonical").
2. The sitemap and every canonical/og:url declared ".html" URLs, which Pages
308-redirects to their clean form. Google reported those as "page with
redirect" and kept them out of the index.
Changes:
- Add docs/404.html (noindex, no canonical, site header/footer + styles.css).
Pages serves it with a real HTTP 404 now that the file exists, which is what
removes the soft-404. Uses root-absolute asset paths so it renders correctly
when served for deep unknown paths.
- Rewrite docs/sitemap.xml to clean URLs and add the missing /privacy entry.
The 404 is deliberately absent.
- Point canonical + og:url at clean URLs on all five subpages; normalise the
JSON-LD downloadUrl in index.html to https://pdf-apps.com/download.
- Convert 98 internal .html hrefs to root-absolute clean URLs across 6 pages.
Coupled workflow fix (the .html link rewrite would otherwise break releases):
release.yml bumps docs/changelog.html via a regex ANCHORED to the footer
changelog link with required=True. Anchoring is mandatory — a global v{old}
rewrite previously clobbered historical <span class="version-tag"> headings.
The anchor now accepts both /changelog and legacy changelog.html; verified the
old anchor no longer matches the new markup, i.e. the next release would have
failed. Fixture in tests/test_release_bump_script.py updated to the new link
shape, plus two regression tests covering the anchor and the real changelog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SEO clean-URL work branched from 46168bb, before the 1.14.0 and 1.14.1 release bumps landed on main. Both sides edited the same footer line on all six docs pages: this branch changed the href (changelog.html -> /changelog) while main bumped the version number (v1.13.17 -> v1.14.1). Resolved each of the six conflicts by taking BOTH sides: the clean href from this branch and the current version from main, i.e. <a href="/changelog">v1.14.1</a> No version string was allowed to regress to 1.13.17. The only remaining 1.13.17 occurrence in docs/ is its historical <span class="version-tag"> heading in changelog.html, which is correct. Auto-merged cleanly and verified: app/constants.py at 1.14.1, the v1.14.1 / v1.14.0 changelog entries intact with a single "Latest" badge, and the index.html eyebrow / download.html eyebrow / JSON-LD softwareVersion all at 1.14.1 while keeping the branch's clean downloadUrl. Re-ran the release.yml bump dry-run on copies with the correct new base (OLD=1.14.1 -> NEW=1.14.2): all 18 required targets matched (exit 0, no "unchanged (no match!)"), all 20 historical changelog headings unchanged, and only the footer link plus the marketing eyebrows were rewritten. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deploying pdfapps with
|
| Latest commit: |
308e955
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://99699a48.pdfapps.pages.dev |
| Branch Preview URL: | https://fix-seo-clean-urls-404.pdfapps.pages.dev |
404.html is served for any unknown path, including deep ones such as /a/b/c. A relative href="favicon.ico" resolved against that base URL (-> /a/b/favicon.ico) and 404'd, which is why every other asset on the page already uses a root-absolute path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Google Search Console reports 6 pages indexed / 7 not indexed. The audit traced this to two root causes:
1. Global soft-404.
docs/404.htmldid not exist. Cloudflare Pages therefore fell through toindex.htmlwith HTTP 200 for any nonexistent path. Every typo, stale link and crawler probe returned a full copy of the home page with a200, i.e. an infinite surface of duplicates. This is what produced the "alternate page with proper canonical" report.2. Sitemap and canonicals pointed at redirecting URLs.
sitemap.xmland every<link rel="canonical">/og:urldeclared.htmlURLs, which Pages 308-redirects to their clean form. Google classified 4 of them as "page with redirect" and left them out of the index./privacywas missing from the sitemap entirely.Fix
docs/404.html— same header/footer/typography as the rest of the site (styles.css),<meta name="robots" content="noindex, follow">, no canonical (a self-referential one would be misleading; one pointing at/would make every 404 a duplicate of the home page). Cloudflare Pages serves a root404.htmlwith a real HTTP 404, which is what eliminates the soft-404. Assets use root-absolute paths so the page renders correctly when served for deep unknown paths like/a/b/c.docs/sitemap.xmlrewritten to clean URLs + added the missing/privacy.lastmodadded. The 404 is deliberately not listed.og:urlon all five subpages moved to clean URLs.index.htmlwas already correct.downloadUrl→https://pdf-apps.com/download(the only.htmlURL in the block;urlwas already clean).hrefs converted from.htmlto root-absolute clean URLs across the 6 pages, plus the new 404.Coupled
release.ymlchange (this is the load-bearing part)release.ymlbumpsdocs/changelog.htmlvia a regex anchored to the footer changelog link,required=True. The anchoring is deliberate: a globalv{old}rewrite previously clobbered the topmost historical<span class="version-tag">heading on every release (that is how v1.13.16 "disappeared").Rewriting the footer link to
/changelogwould have made the old anchor stop matching — and because it isrequired=True, the next release would have failed. Proven, not assumed:The anchor now accepts both forms and stays anchored (never global):
rf'(<a href="(?:/changelog|changelog\.html)">)v{old_re}(</a>)'tests/test_release_bump_script.py'sfake_repofixture was updated to the new link shape (this fixture has broken CI before), plus two new regression tests: one asserting the footer link is bumped while the historical heading survives, one asserting the realdocs/changelog.htmlstill matches the real workflow regex at the currentAPP_VERSION.Validation
Bump dry-run (
OLD=1.13.17 → NEW=1.14.2, executed against a full copy of the repo, real files untouched):0— 18 targets updated, norequired=Truetarget left unmatchedv1.13.17is the historical heading (correct)Other checks:
href/srcresolve to existing files; zero.htmlinternal links remain; in-page anchors resolve to realidsog:urlcoherence verified programmaticallypytest -q: 478 passed, 2 skippedruff checkon the touched Python file: cleanrelease.ymlparses as valid YAMLNotes
v<version>string:404.htmlis not a bump target, so it would go stale after every release.🤖 Generated with Claude Code