From 5a61a703d6e65c22389e1226a9f4b46c15b09cf7 Mon Sep 17 00:00:00 2001 From: maruson08 Date: Tue, 1 Sep 2026 16:10:12 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=8D[SEO]=20Prepare=20Hub=20apex=20?= =?UTF-8?q?cutover=20artifacts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 7 + .github/workflows/validate.yml | 27 ++++ public/_headers | 5 + public/_redirects | 18 +++ public/index.html | 8 + public/libraries/index.html | 8 + public/libraries/secure-metadata/index.html | 8 + public/principles/index.html | 8 + public/principles/privacy/index.html | 8 + public/products/desktop-pet/index.html | 8 + public/products/index.html | 8 + public/products/local-ai/index.html | 8 + public/products/web-utilities/index.html | 16 +- public/project/index.html | 8 + public/robots.txt | 4 + public/sitemap.xml | 13 ++ scripts/validate-h3-hub-cutover.py | 155 ++++++++++++++++++++ scripts/validate-h3-url-map.py | 51 +++++++ 18 files changed, 364 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/validate.yml create mode 100644 public/_headers create mode 100644 public/_redirects create mode 100644 public/robots.txt create mode 100644 public/sitemap.xml create mode 100644 scripts/validate-h3-hub-cutover.py diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6719c9d..d87477a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -35,6 +35,10 @@ jobs: required_files=( public/index.html public/404.html + public/_headers + public/_redirects + public/robots.txt + public/sitemap.xml ) for file in "${required_files[@]}"; do @@ -60,6 +64,9 @@ jobs: fi done + python scripts/validate-h3-url-map.py + python scripts/validate-h3-hub-cutover.py + echo "Validated static deployment output: public/" - name: Deploy to Cloudflare Pages diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..c8baaad --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,27 @@ +name: Validate Hub + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + name: Validate static Hub + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout source + # actions/checkout v6.0.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - name: Validate H3 migration contracts + run: | + set -euo pipefail + python scripts/validate-h3-url-map.py + python scripts/validate-h3-hub-cutover.py diff --git a/public/_headers b/public/_headers new file mode 100644 index 0000000..21bffbe --- /dev/null +++ b/public/_headers @@ -0,0 +1,5 @@ +https://secure-tools-hub-53i.pages.dev/* + X-Robots-Tag: noindex, nofollow + +https://:version.secure-tools-hub-53i.pages.dev/* + X-Robots-Tag: noindex, nofollow diff --git a/public/_redirects b/public/_redirects new file mode 100644 index 0000000..f7d2f37 --- /dev/null +++ b/public/_redirects @@ -0,0 +1,18 @@ +/about/ https://tools.securetools.app/about/ 301 +/privacy/ https://tools.securetools.app/privacy/ 301 +/tools/pdf/ https://tools.securetools.app/tools/pdf/ 301 +/tools/pdf/images-to-pdf/ https://tools.securetools.app/tools/pdf/images-to-pdf/ 301 +/tools/pdf/merge/ https://tools.securetools.app/tools/pdf/merge/ 301 +/tools/pdf/split/ https://tools.securetools.app/tools/pdf/split/ 301 +/tools/pdf/organize/ https://tools.securetools.app/tools/pdf/organize/ 301 +/tools/pdf/to-images/ https://tools.securetools.app/tools/pdf/to-images/ 301 +/tools/pdf/metadata/ https://tools.securetools.app/tools/pdf/metadata/ 301 +/tools/image/ https://tools.securetools.app/tools/image/ 301 +/tools/image/converter/ https://tools.securetools.app/tools/image/converter/ 301 +/tools/image/resize/ https://tools.securetools.app/tools/image/resize/ 301 +/tools/image/compress/ https://tools.securetools.app/tools/image/compress/ 301 +/tools/image/metadata/ https://tools.securetools.app/tools/image/metadata/ 301 +/tools/privacy/ https://tools.securetools.app/tools/privacy/ 301 +/tools/scan/ https://tools.securetools.app/tools/scan/ 301 +/tools/media/ https://tools.securetools.app/tools/media/ 301 +/tools/image-to-pdf/ https://tools.securetools.app/tools/image-to-pdf/ 301 diff --git a/public/index.html b/public/index.html index 9e69d83..dbbf7af 100644 --- a/public/index.html +++ b/public/index.html @@ -4,14 +4,22 @@ + + + + + + + + Secure Tools — Local-first software ecosystem diff --git a/public/libraries/index.html b/public/libraries/index.html index f5c55d2..e13c39c 100644 --- a/public/libraries/index.html +++ b/public/libraries/index.html @@ -4,14 +4,22 @@ + + + + + + + + Libraries — Secure Tools diff --git a/public/libraries/secure-metadata/index.html b/public/libraries/secure-metadata/index.html index b089ceb..888117c 100644 --- a/public/libraries/secure-metadata/index.html +++ b/public/libraries/secure-metadata/index.html @@ -4,14 +4,22 @@ + + + + + + + + Secure Metadata — Secure Tools diff --git a/public/principles/index.html b/public/principles/index.html index 9750904..b6978ef 100644 --- a/public/principles/index.html +++ b/public/principles/index.html @@ -4,14 +4,22 @@ + + + + + + + + Principles — Secure Tools diff --git a/public/principles/privacy/index.html b/public/principles/privacy/index.html index 3777ae3..82bb3a6 100644 --- a/public/principles/privacy/index.html +++ b/public/principles/privacy/index.html @@ -4,14 +4,22 @@ + + + + + + + + Privacy model — Secure Tools diff --git a/public/products/desktop-pet/index.html b/public/products/desktop-pet/index.html index f8120e8..b4bafa0 100644 --- a/public/products/desktop-pet/index.html +++ b/public/products/desktop-pet/index.html @@ -4,14 +4,22 @@ + + + + + + + + Secure Desktop Pet — Secure Tools diff --git a/public/products/index.html b/public/products/index.html index a7068de..fc3d98e 100644 --- a/public/products/index.html +++ b/public/products/index.html @@ -4,14 +4,22 @@ + + + + + + + + Products — Secure Tools diff --git a/public/products/local-ai/index.html b/public/products/local-ai/index.html index e714d3d..f636779 100644 --- a/public/products/local-ai/index.html +++ b/public/products/local-ai/index.html @@ -4,14 +4,22 @@ + + + + + + + + Secure Local AI — Secure Tools diff --git a/public/products/web-utilities/index.html b/public/products/web-utilities/index.html index 8291bfb..440f643 100644 --- a/public/products/web-utilities/index.html +++ b/public/products/web-utilities/index.html @@ -4,14 +4,22 @@ + + + + + + + + Secure Tools Web Utilities — Secure Tools @@ -54,7 +62,7 @@

Secure Tools Web Utilities

Secure Tools ecosystem.

- Open current Web Utilities site + Open Web Utilities View source
@@ -130,9 +138,9 @@

Website delivery and file processing are different

Entry point and migration boundary

- Current site: securetools.app - currently serves Web Utilities. A later ecosystem migration may move this product, - but no future subdomain is linked or implied here. + Web application: tools.securetools.app + hosts the parallel Web Utilities deployment and becomes the product's canonical + public endpoint during the coordinated H3.5 cutover.

diff --git a/public/project/index.html b/public/project/index.html index e597cb3..588c882 100644 --- a/public/project/index.html +++ b/public/project/index.html @@ -4,14 +4,22 @@ + + + + + + + + Project — Secure Tools diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..550290e --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://securetools.app/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..bdbb079 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,13 @@ + + + https://securetools.app/ + https://securetools.app/products/ + https://securetools.app/products/web-utilities/ + https://securetools.app/products/desktop-pet/ + https://securetools.app/products/local-ai/ + https://securetools.app/libraries/ + https://securetools.app/libraries/secure-metadata/ + https://securetools.app/principles/ + https://securetools.app/principles/privacy/ + https://securetools.app/project/ + diff --git a/scripts/validate-h3-hub-cutover.py b/scripts/validate-h3-hub-cutover.py new file mode 100644 index 0000000..f3507e2 --- /dev/null +++ b/scripts/validate-h3-hub-cutover.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Validate the prepared H3 Hub SEO and crawler contract.""" + +from __future__ import annotations + +import re +import struct +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +PUBLIC = ROOT / "public" +ORIGIN = "https://securetools.app" +SOCIAL_IMAGE = f"{ORIGIN}/assets/brand/social-preview.png" +EXPECTED_ROUTES = { + "/", + "/products/", + "/products/web-utilities/", + "/products/desktop-pet/", + "/products/local-ai/", + "/libraries/", + "/libraries/secure-metadata/", + "/principles/", + "/principles/privacy/", + "/project/", +} +EXPECTED_HEADERS = """https://secure-tools-hub-53i.pages.dev/* + X-Robots-Tag: noindex, nofollow + +https://:version.secure-tools-hub-53i.pages.dev/* + X-Robots-Tag: noindex, nofollow +""" + + +def route_for(index_file: Path) -> str: + relative = index_file.relative_to(PUBLIC).as_posix() + return "/" if relative == "index.html" else f"/{relative.removesuffix('index.html')}" + + +def values(markup: str, pattern: str) -> list[str]: + return re.findall(pattern, markup, flags=re.IGNORECASE) + + +def main() -> int: + errors: list[str] = [] + page_titles: set[str] = set() + page_descriptions: set[str] = set() + pages = {route_for(path): path for path in PUBLIC.rglob("index.html")} + if set(pages) != EXPECTED_ROUTES: + errors.append( + f"Hub route inventory mismatch: missing={sorted(EXPECTED_ROUTES - set(pages))}, " + f"unexpected={sorted(set(pages) - EXPECTED_ROUTES)}" + ) + + for route, path in sorted(pages.items()): + markup = path.read_text(encoding="utf-8") + expected_url = f"{ORIGIN}{route}" + checks = { + "canonical": values(markup, r'([^<]+)") + description = values(markup, r'II", image_bytes[16:24]) != (1200, 630): + errors.append("social preview dimensions must be exactly 1200 x 630") + except OSError as error: + errors.append(f"social preview is unavailable: {error}") + + if errors: + for error in errors: + print(f"ERROR: {error}", file=sys.stderr) + return 1 + + print(f"Validated {len(pages)} Hub routes and canonical metadata records.") + print(f"Validated {len(locations)} unique Hub sitemap URLs and robots.txt.") + print("Validated Pages-alias noindex configuration and 1200 x 630 PNG social image.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validate-h3-url-map.py b/scripts/validate-h3-url-map.py index fad082a..324bddd 100644 --- a/scripts/validate-h3-url-map.py +++ b/scripts/validate-h3-url-map.py @@ -12,6 +12,7 @@ REPOSITORY_ROOT = Path(__file__).resolve().parents[1] MAP_PATH = REPOSITORY_ROOT / "docs" / "migrations" / "h3-url-map.csv" PUBLIC_ROOT = REPOSITORY_ROOT / "public" +REDIRECTS_PATH = PUBLIC_ROOT / "_redirects" EXPECTED_FIELDS = ["old_url", "new_url", "status", "reason"] @@ -35,6 +36,7 @@ def main() -> int: redirect_count = 0 root_count = 0 current_hub_routes = hub_routes() + expected_redirects: dict[str, str] = {} for line_number, row in enumerate(rows, start=2): old_url = row.get("old_url", "") @@ -73,9 +75,57 @@ def main() -> int: errors.append(f"line {line_number}: redirect target does not preserve {old.path}") if old.path in current_hub_routes: errors.append(f"line {line_number}: legacy redirect collides with Hub route {old.path}") + expected_redirects[old.path] = new_url if root_count != 1: errors.append(f"expected one apex root reservation; found {root_count}") + if redirect_count != 18: + errors.append(f"expected 18 redirect inventory rows; found {redirect_count}") + + actual_redirects: dict[str, str] = {} + if not REDIRECTS_PATH.is_file(): + errors.append("public/_redirects is missing") + else: + for line_number, raw_line in enumerate( + REDIRECTS_PATH.read_text(encoding="utf-8").splitlines(), start=1 + ): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + fields = line.split() + if len(fields) != 3: + errors.append(f"_redirects line {line_number}: expected source destination status") + continue + source, destination, redirect_status = fields + target = urlsplit(destination) + if source in actual_redirects: + errors.append(f"_redirects line {line_number}: duplicate source {source}") + if "*" in source or ":" in source: + errors.append(f"_redirects line {line_number}: wildcard or placeholder is not allowed") + if source == "/": + errors.append("_redirects must not redirect the Hub root") + if source in current_hub_routes: + errors.append(f"_redirects line {line_number}: Hub route collision {source}") + if redirect_status != "301": + errors.append(f"_redirects line {line_number}: status must be 301") + if target.scheme != "https" or target.netloc != "tools.securetools.app": + errors.append(f"_redirects line {line_number}: target host must be tools.securetools.app") + if source != target.path: + errors.append(f"_redirects line {line_number}: target must preserve path {source}") + actual_redirects[source] = destination + + if actual_redirects != expected_redirects: + missing = sorted(expected_redirects.keys() - actual_redirects.keys()) + unexpected = sorted(actual_redirects.keys() - expected_redirects.keys()) + mismatched = sorted( + source + for source in expected_redirects.keys() & actual_redirects.keys() + if expected_redirects[source] != actual_redirects[source] + ) + errors.append( + "public/_redirects does not exactly match h3-url-map.csv " + f"(missing={missing}, unexpected={unexpected}, mismatched={mismatched})" + ) if errors: for error in errors: @@ -86,6 +136,7 @@ def main() -> int: f"Validated {len(rows)} inventory rows: " f"{redirect_count} redirects and {root_count} Hub-root no-redirect reservation." ) + print(f"Validated {len(actual_redirects)} exact Cloudflare Pages redirect rules.") print(f"Checked collisions against {len(current_hub_routes)} current Hub routes.") return 0 From 9f1d88ec872b0bb8a1a194f9c1aa93b495fc9f9c Mon Sep 17 00:00:00 2001 From: maruson08 Date: Tue, 1 Sep 2026 16:10:26 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D[Docs]=20Define=20coordinated?= =?UTF-8?q?=20H3.5=20cutover=20runbook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + docs/architecture.md | 10 +- docs/deployment.md | 23 +++- docs/migrations/h3-migration-contract.md | 12 ++ docs/migrations/h3.5-cutover-runbook.md | 165 +++++++++++++++++++++++ docs/seo.md | 55 ++++++++ 6 files changed, 259 insertions(+), 8 deletions(-) create mode 100644 docs/migrations/h3.5-cutover-runbook.md create mode 100644 docs/seo.md diff --git a/README.md b/README.md index 66698cb..ac1db18 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ validation target. - [Architecture](docs/architecture.md) - [Privacy model](docs/privacy-model.md) - [Deployment transparency](docs/deployment.md) +- [Search metadata](docs/seo.md) +- [H3.5 cutover runbook](docs/migrations/h3.5-cutover-runbook.md) - [H2.2 quality assurance](docs/h2.2-qa.md) The static site can be previewed by serving `public/` with any local static file server. diff --git a/docs/architecture.md b/docs/architecture.md index 043f135..114a216 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -44,11 +44,11 @@ of the circular CSS brand mark used by Web Utilities. The 1200×630 social previ specifically for the Hub and uses its catalog language and visual system; it is not a copy of the Web Utilities social image. -The social preview asset is ready, but host-dependent `og:image` and `twitter:image` metadata -is intentionally deferred until a separately reviewed production-domain migration establishes -the permanent Hub origin. Canonical and `og:url` metadata is deferred for the same reason. This -avoids coupling the public identity to a temporary validation hostname or claiming -`securetools.app` before migration. +H3.4B prepares the reviewed social preview at its final absolute +`https://securetools.app/assets/brand/social-preview.png` URL together with self-referencing +Hub canonical and `og:url` metadata. These source changes remain inactive while the H3.4B +pull request is unmerged. Pages validation aliases are covered by hostname-specific noindex +headers so they do not become alternate public identities after coordinated H3.5 activation. ## Product disclosure contract diff --git a/docs/deployment.md b/docs/deployment.md index bc5263c..935012d 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -41,9 +41,9 @@ permissions. ## Current validation deployment -The current deployment target is the Cloudflare Pages-managed `secure-tools-hub.pages.dev` -site. This endpoint is for validating the Hub deployment path and does not mean that the Hub -has become the Secure Tools production site. +The current deployment target is the Cloudflare Pages-managed +`https://secure-tools-hub-53i.pages.dev` site. This endpoint is for validating the Hub +deployment path and does not mean that the Hub has become the Secure Tools production site. ## Future production domain @@ -53,3 +53,20 @@ perform the future production migration. Any production domain migration and related redirects must be handled as a separate, explicitly reviewed milestone. + +## Prepared H3.5 deployment contract + +The unmerged H3.4B branch prepares static `public/_headers` and `public/_redirects` +artifacts. The hostname-specific header rules keep the stable and immutable +`*.secure-tools-hub-53i.pages.dev` aliases non-indexable without applying noindex to the +future custom domain. The redirects contain only the 18 explicit H3.1 legacy Web Utilities +paths and preserve each path on `https://tools.securetools.app`; the Hub root and all Hub +routes are excluded. + +Cloudflare Pages path redirects do not implement the future `www → apex` domain redirect. +That change requires a Cloudflare zone/account-level Redirect Rule or Bulk Redirect plus +proxied DNS during the coordinated H3.5 window. The exact activation and rollback order is +documented in [the H3.5 cutover runbook](./migrations/h3.5-cutover-runbook.md). + +The H3.4B pull request does not attach a custom domain, change DNS, activate a `www` rule, +modify Search Console, or deploy from its feature branch. diff --git a/docs/migrations/h3-migration-contract.md b/docs/migrations/h3-migration-contract.md index af59426..fd2c92d 100644 --- a/docs/migrations/h3-migration-contract.md +++ b/docs/migrations/h3-migration-contract.md @@ -158,3 +158,15 @@ Every gate requires recorded evidence and an explicit go/no-go decision. A failu 5. **H3.6 — Search Migration Monitoring:** submit host-specific sitemaps and monitor indexing, redirects, availability, and rollback signals. No later phase is authorized by this document alone. + +## H3.4B preparation + +[Issue #14](https://github.com/SecureToolsProject/hub/issues/14) prepares the Hub-owned H3.5 +artifacts on an unmerged branch: final Hub metadata, sitemap and robots files, +hostname-specific Pages-alias noindex headers, and the 18 explicit redirects derived from this +inventory. The executable sequence and rollback requirements are in +[h3.5-cutover-runbook.md](./h3.5-cutover-runbook.md). + +These files do not authorize or perform a merge, deployment, apex or `www` DNS change, +custom-domain attachment, zone redirect, Search Console operation, or Secure_Tools PR #73 +merge. H3.5 must coordinate those operations only after all preflight gates pass. diff --git a/docs/migrations/h3.5-cutover-runbook.md b/docs/migrations/h3.5-cutover-runbook.md new file mode 100644 index 0000000..d538334 --- /dev/null +++ b/docs/migrations/h3.5-cutover-runbook.md @@ -0,0 +1,165 @@ +# H3.5 coordinated cutover runbook + +Status: prepared by H3.4B; do not execute from the preparation pull request. + +This runbook coordinates the final architecture: + +```text +https://securetools.app +→ Secure Tools Project Hub + +https://tools.securetools.app +→ Secure Tools Web Utilities +``` + +The reviewed Hub preparation includes canonical and social metadata, a Hub-only sitemap, +production robots policy, Pages-alias noindex headers, and 18 explicit legacy redirects. +Secure_Tools PR #73 owns the separately reviewed Web Utilities SEO preparation. Neither +preparation pull request may be merged outside the H3.5 change window. + +## Preconditions and ownership + +- A named operator owns the Cloudflare, GitHub, DNS, TLS, HTTP, and rollback decisions. +- A second reviewer confirms recorded snapshots and the go/no-go decision. +- No Search Console change occurs until the HTTP, TLS, canonical, and redirect gates pass. +- Do not use Search Console Change of Address: this is a partial migration and the apex + remains active as the Hub. +- Preserve the existing Secure_Tools GitHub Pages workflow, repository `CNAME`, and custom + domain during initial stabilization unless a separately reviewed change requires removal. + +## A. Preflight and snapshots + +1. Confirm Hub H3.4B and Secure_Tools PR #73 are clean, reviewed, current with each repository's + `main`, and unmerged. +2. Record the exact head SHA of both preparation PRs and the current `main` SHA of both + repositories. +3. Verify all Hub repository validation and all Secure_Tools validation. +4. Verify HTTPS and expected content on: + - `https://secure-tools-hub-53i.pages.dev`; + - `https://tools.securetools.app`; + - `https://secure-tools-web-bridge.pages.dev`; and + - the existing `https://securetools.app` GitHub Pages production. +5. Snapshot, without exposing credential values: + - every current apex DNS record, type, value/target, proxy state, and TTL; + - the current `www` record, target, proxy state, and TTL; + - the Secure_Tools GitHub Pages custom-domain and HTTPS-enforcement state; + - Hub Pages project production branch, stable hostname, deployment, custom domains, and + analytics/Functions/Workers state; + - Web Utilities Pages project, tools custom domain, deployment, and indexing-header state; + - active CAA records and certificate-authority restrictions. +6. Store the snapshot in the H3.5 evidence record and explicitly identify the exact DNS values + to restore. Do not continue without a usable rollback record. +7. Recheck existing production immediately before the first mutation. Any unexplained DNS, + TLS, route, asset, or functional regression is a no-go. + +## B. Normalize `www` before the apex move + +1. Create a Cloudflare zone/account-level Redirect Rule or Bulk Redirect for: + + ```text + https://www.securetools.app/* + → 301 https://securetools.app/* + ``` + +2. Configure the Cloudflare-proxied placeholder DNS record required by the selected redirect + mechanism. Pages `_redirects` cannot implement this domain-level redirect. +3. Enable path and query-string preservation. Never send `www` to + `tools.securetools.app`. +4. While the apex still serves the existing Web Utilities production, validate representative + `www` paths and queries return one 301 hop to the equivalent working apex URL. +5. Confirm this removes the direct `www` dependency on GitHub Pages without changing the + apex origin. Roll back the `www` record and rule from the snapshot if validation fails. + +## C. Activate the Hub repository preparation + +1. Merge the reviewed H3.4B pull request using the repository's established merge method. +2. Wait for the automatic `main` deployment to `secure-tools-hub`. +3. Verify the immutable deployment and `secure-tools-hub-53i.pages.dev`: + - all 10 Hub routes and representative assets return 200; + - canonical, `og:url`, `og:image`, and `twitter:image` use + `https://securetools.app`; + - the Hub-only sitemap has 10 unique URLs and robots references it; + - stable and immutable Pages hostnames return + `X-Robots-Tag: noindex, nofollow`; + - all 18 legacy paths return a single 301 to the same path on + `tools.securetools.app`; and + - `/` renders the Hub and does not redirect. +4. Do not attach the apex yet. A deployment or validation failure is a no-go. + +## D. Activate the Web Utilities SEO identity + +1. Merge Secure_Tools PR #73 only after step C passes. +2. Wait for its automatic Cloudflare Pages deployment. +3. Validate all 19 H3.1 routes and representative assets on + `https://tools.securetools.app`. +4. Confirm the tools custom domain is indexable, all canonical and `og:url` values + self-reference the tools host, social images use that host, and its sitemap and robots + contain only Web Utilities URLs. +5. Confirm stable and immutable `secure-tools-web-bridge.pages.dev` hostnames remain + `noindex, nofollow`. +6. Confirm local file processing, privacy disclosures, and network behavior are unchanged. + Any mismatch blocks the apex move. + +## E. Attach the apex to the Hub + +1. Reconfirm the step A DNS snapshot, rollback values, existing GitHub Pages health, and CAA + compatibility. +2. In Cloudflare use **Workers & Pages → secure-tools-hub → Custom domains → Set up a + domain**, then enter `securetools.app`. +3. Because the apex is in a Cloudflare-managed zone, expect Pages association to change its + DNS relationship. Do not make unrelated DNS changes. +4. Wait for the custom domain to report Active and for a valid certificate covering + `securetools.app`. If CAA blocks issuance, stop and use a separately reviewed CAA change; + do not weaken certificate policy ad hoc. +5. Validate: + - `/` returns the Hub with 200 and no redirect; + - every Hub route, asset, sitemap, and robots response is correct; + - all 18 legacy paths return exactly one 301 to the identical + `tools.securetools.app` path; + - representative query strings survive the redirect; + - no Hub route collides with a redirect; and + - no loop, chain, mixed content, or certificate error exists. + +## F. Post-cutover isolation + +1. Revalidate the Hub apex, Hub Pages aliases, tools custom domain, tools Pages aliases, and + `www → apex` redirect. +2. Confirm the apex is indexable and Pages aliases remain non-indexable. +3. Confirm Web Utilities functionality and local-first network behavior on the tools host. +4. Keep the old GitHub Pages workflow, custom-domain configuration, `CNAME`, and known-good + deployment available as rollback infrastructure where practical during stabilization. +5. Record workflow runs, GitHub Deployments, immutable URLs, DNS state, headers, route matrix, + TLS evidence, and the final go/no-go decision. + +## G. Search activation + +Only after A–F pass: + +- submit `https://securetools.app/sitemap.xml` to the root property; +- monitor `https://tools.securetools.app/` separately and submit its sitemap to the tools + URL-prefix property; +- retain the domain property if already used for aggregate observation; and +- do not use Change of Address for this partial migration. + +Search Console changes and ongoing migration monitoring belong to the later monitoring phase, +not H3.4B. + +## Immediate rollback + +Rollback is triggered by a critical TLS, DNS, availability, route, redirect, canonical, +privacy, or functional failure that cannot be corrected safely inside the change window. + +1. Stop additional activation and preserve evidence. +2. Restore the snapshotted apex DNS relationship so `securetools.app` returns to the previous + GitHub Pages Web Utilities production. +3. Restore the previous `www` DNS/redirect behavior if the normalized rule prevents the + known-good service from being reached. +4. Confirm GitHub Pages custom-domain and HTTPS state match the snapshot. +5. Revalidate production routes, assets, TLS, and representative functions. +6. Revert or redeploy the Hub and Web Utilities SEO changes as needed to avoid conflicting + canonical, sitemap, robots, or indexing signals. +7. Leave `tools.securetools.app` available on the parallel Cloudflare deployment unless it is + itself the failure source. Do not destroy the bridge merely to restore the apex. + +Rollback completion requires recorded DNS, TLS, HTTP, and functional evidence. Cleanup of old +infrastructure is a later, separately reviewed decision. diff --git a/docs/seo.md b/docs/seo.md new file mode 100644 index 0000000..810d28e --- /dev/null +++ b/docs/seo.md @@ -0,0 +1,55 @@ +# Hub search metadata + +Status: H3.4B preparation only. The pull request must remain unmerged until the coordinated +H3.5 cutover. + +## Final canonical identity + +The final Hub origin is `https://securetools.app`. Each of the 10 public Hub routes has one +self-referencing absolute canonical and matching `og:url`. Open Graph and X image metadata +uses the existing 1200×630 PNG at +`https://securetools.app/assets/brand/social-preview.png`. + +`404.html` is an error document, not a canonical Hub route. It is excluded from the sitemap +and does not receive a canonical URL. + +## Crawler files + +- `public/sitemap.xml` lists exactly the 10 Hub canonical URLs. +- `public/robots.txt` allows crawling and references + `https://securetools.app/sitemap.xml`. +- No Web Utilities route, tools host, legacy redirect source, or Pages hostname appears in the + Hub sitemap. + +The static `public/_headers` contract applies +`X-Robots-Tag: noindex, nofollow` only to: + +```text +https://secure-tools-hub-53i.pages.dev/* +https://:version.secure-tools-hub-53i.pages.dev/* +``` + +The future `securetools.app` custom domain does not match those patterns and remains +indexable. No Worker or Pages Function is required. + +## Legacy path ownership + +`public/_redirects` contains exactly the 18 explicit 301 mappings in +`docs/migrations/h3-url-map.csv`. Every destination is the same path on +`https://tools.securetools.app`. The Hub root and all Hub routes are excluded; no wildcard +can swallow future Hub content. + +These redirects become reachable on the apex only after H3.5 attaches the custom domain to +the Hub Pages project. The H3.4B feature branch does not deploy or activate them. + +## Validation and search activation + +`scripts/validate-h3-hub-cutover.py` checks route metadata, social image dimensions, +sitemap, robots, and Pages-alias isolation. `scripts/validate-h3-url-map.py` compares the +redirect artifact directly with the H3.1 inventory and rejects duplicates, wildcards, wrong +hosts, changed paths, non-301 status, root redirects, and Hub-route collisions. + +Search Console remains unchanged during preparation. After H3.5 HTTP, TLS, canonical, and +redirect validation, submit the Hub sitemap to the root property and monitor Web Utilities +through the tools URL-prefix property. Do not use Change of Address for this partial +migration.