About — Secure Tools
diff --git a/docs/README.md b/docs/README.md
index 6f0ede1..0e04b24 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -11,7 +11,7 @@ The root [README](../README.md) introduces Secure Tools. This directory owns det
| [Dependencies](./dependencies.md) | Production runtime inventory, versions, vendoring, licenses, and integrity ownership |
| [Tool status](./tool-status.md) | Production and planned surfaces, supported formats, behavior, and resource boundaries |
| [Search discovery and metadata](./seo.md) | Canonical routes, crawler files, metadata policy, maintenance, and submission steps |
-| [Cloudflare Pages migration bridge](./cloudflare-pages-bridge.md) | H3.2 Direct Upload provenance plus H3.3 parallel-domain activation, validation, SEO isolation, and rollback contracts |
+| [Cloudflare Pages migration bridge](./cloudflare-pages-bridge.md) | H3.2/H3.3 provenance plus the prepared H3.5 hostname-specific indexing, activation, validation, and rollback contracts |
| [Image Metadata privacy](./image-metadata-privacy.md) | Format-specific inspection, cleaning, preservation, and verification semantics |
| [UX consistency audit](./ux-consistency-audit.md) | Shared interaction, accessibility, responsive, theme, and historical browser-QA findings |
| [i18n copy review](./i18n-copy-review.md) | Localization coverage and editorial review record |
diff --git a/docs/cloudflare-pages-bridge.md b/docs/cloudflare-pages-bridge.md
index 01a2070..a6521fc 100644
--- a/docs/cloudflare-pages-bridge.md
+++ b/docs/cloudflare-pages-bridge.md
@@ -1,6 +1,6 @@
# Cloudflare Pages migration bridge
-Status: H3.2 is deployed and validated. H3.3 preparation targets a parallel `tools.securetools.app` custom domain, but the preparation pull request must remain unmerged until the activation gate below is satisfied. H3.3 does not authorize an apex migration.
+Status: H3.3 is complete. `tools.securetools.app` is active on the Direct Upload Pages project and remains non-indexable under the live H3.3 deployment. H3.4A prepares, but does not activate, the later H3.5 indexing and canonical cutover.
## Deployment identity
@@ -8,117 +8,84 @@ Status: H3.2 is deployed and validated. H3.3 preparation targets a parallel `too
| --- | --- |
| Cloudflare Pages project | `secure-tools-web-bridge` |
| Production branch | `main` |
+| Custom domain | `https://tools.securetools.app` |
| Stable validation URL | `https://secure-tools-web-bridge.pages.dev` |
| Deployment mechanism | GitHub Actions Direct Upload through Wrangler |
-| Current custom domains | None before H3.3 activation |
-| H3.3 target custom domain | `https://tools.securetools.app` |
-The stable Pages hostname is a validated application endpoint and must remain available throughout H3.3 as the rollback and comparison endpoint. `securetools.app` and `www.securetools.app` remain outside the Pages project.
-
-The H3.3 domain contract is:
+The H3.3 architecture remains live while the H3.4A pull request is open:
```text
+securetools.app
+→ existing GitHub Pages Web Utilities production
+
tools.securetools.app
-→ Cloudflare Pages project secure-tools-web-bridge
+→ Cloudflare Pages parallel endpoint
+→ X-Robots-Tag: noindex, nofollow
+→ canonical securetools.app
secure-tools-web-bridge.pages.dev
-→ retained validation and rollback endpoint
-
-securetools.app
-→ unchanged GitHub Pages production deployment
+→ validation and rollback endpoint
+→ X-Robots-Tag: noindex, nofollow
```
-## Provenance and isolation
+No H3.4A feature-branch push deploys to Pages because the workflow deploys only on `main` or explicit manual dispatch.
-The bridge workflow is `.github/workflows/deploy-cloudflare-bridge.yml`:
+## Provenance and artifact isolation
```text
Secure_Tools main
→ GitHub Actions validation
-→ temporary bridge artifact
+→ temporary bridge artifact without repository CNAME
→ GitHub Deployment
→ Cloudflare Pages Direct Upload
```
-The existing GitHub Pages production path remains independent:
+The required `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` values remain GitHub Actions secrets. The workflow validates the intended project, `main` production branch, custom domain, Direct Upload source, and analytics-disabled state without logging secret values.
-```text
-Secure_Tools main repository root + CNAME
-→ GitHub Pages
-→ https://securetools.app
-```
+The source repository has no `_headers`, `_redirects`, `_worker.js`, or `functions/` deployment behavior. CI creates the Cloudflare-only `_headers` file inside the runner's temporary artifact.
-The workflow copies only application files to `${{ runner.temp }}/secure-tools-web-bridge`. It excludes the repository `CNAME` and injects this bridge-only file into that temporary directory:
+## Prepared H3.5 indexing split
+
+Cloudflare Pages `_headers` supports absolute hostname patterns. H3.4A therefore prepares:
```text
-/*
+https://secure-tools-web-bridge.pages.dev/*
X-Robots-Tag: noindex, nofollow
-```
-
-The source artifact therefore retains its current canonical, Open Graph, sitemap, robots, and GitHub Pages behavior. The Pages bridge and H3.3 custom domain remain accessible for QA while their Cloudflare static responses instruct crawlers not to index or follow them.
-
-## Credentials and project contract
-
-The required GitHub Actions secret names are:
-
-- `CLOUDFLARE_API_TOKEN`
-- `CLOUDFLARE_ACCOUNT_ID`
-Their values remain secret and must never enter source, logs, pull-request text, or untrusted workflows. The token remains limited to the intended Cloudflare account and required Pages permissions.
-
-After H3.3 activation, every deployment queries the authenticated Pages project state and requires the expected name, `main` production branch, stable Pages subdomain, exactly the `tools.securetools.app` custom domain, no Git integration, and no Cloudflare Web Analytics configuration.
-
-## H3.3 manual browser QA gate
-
-Do not attach the custom domain until a human has completed all of these checks against `https://secure-tools-web-bridge.pages.dev`:
-
-- one representative PDF operation;
-- one representative image operation;
-- one representative metadata operation; and
-- browser Network-panel confirmation that selected user files are not uploaded.
-
-Static and contract tests do not satisfy this manual gate. Record the human result on the H3.3 issue before activation.
-
-## Custom-domain activation
-
-After the manual gate has explicit evidence, use this exact order:
+https://:version.secure-tools-web-bridge.pages.dev/*
+ X-Robots-Tag: noindex, nofollow
+```
-1. In Cloudflare, open **Workers & Pages → `secure-tools-web-bridge` → Custom domains → Set up a domain**.
-2. Enter `tools.securetools.app`, continue, and activate it through the Pages project.
-3. Because `securetools.app` is already a Cloudflare-managed zone, allow Cloudflare Pages to create and manage the associated `tools` DNS record.
-4. Wait until the Pages custom domain reports active and its TLS certificate is valid.
-5. Validate HTTPS, all 19 routes, representative assets, exact `X-Robots-Tag: noindex, nofollow`, and the existing production SEO inventory with `node tests/deployment-smoke.mjs https://tools.securetools.app`.
-6. Revalidate `secure-tools-web-bridge.pages.dev` and `securetools.app`, then merge the prepared pull request. The resulting `main` deployment continuously validates the immutable deployment, stable Pages alias, custom domain, and existing production isolation.
+Expected H3.5 behavior after coordinated merge and deployment:
-Do not manually create a Pages-target CNAME before associating the hostname with the Pages project. Do not change apex or `www` records. Cloudflare may represent the managed record internally; the invariant is that `tools.securetools.app` resolves through `secure-tools-web-bridge` while apex records remain unchanged.
+```text
+tools.securetools.app
+→ indexable
+→ canonical tools.securetools.app
-## Continuous deployment validation
+secure-tools-web-bridge.pages.dev
+*.secure-tools-web-bridge.pages.dev
+→ X-Robots-Tag: noindex, nofollow
+→ canonical tools.securetools.app
+```
-Every `main` push and optional manual dispatch performs:
+The static hostname rules are the smallest transparent solution: they preserve `pages.dev` duplicate-host protection without a Worker, Pages Function, redirect, or external zone rule. The workflow verifies all 19 routes, seven representative assets, redirect absence, indexing-header mode, and tools-host canonical/`og:url` values on the immutable URL, stable alias, and custom domain.
-1. the complete repository test suite;
-2. an explicit secret-name prerequisite check;
-3. creation of a temporary static artifact without `CNAME`, `_redirects`, Workers, or Pages Functions;
-4. injection of the bridge-only `_headers` rule;
-5. authenticated verification of project identity, production branch, custom-domain contract, Direct Upload mode, and analytics isolation;
-6. Direct Upload with source SHA and branch provenance;
-7. the shared deployment smoke contract against the immutable deployment URL, stable Pages alias, H3.3 custom domain, and existing GitHub Pages production;
-8. HTTP 200 without redirects for all 19 H3.1 routes on each endpoint;
-9. representative CSS, JavaScript, icon, and vendored-library checks;
-10. exact `X-Robots-Tag: noindex, nofollow` on Pages routes and assets, plus confirmation that production does not inherit that bridge-only header; and
-11. canonical and `og:url` values that intentionally continue to identify `https://securetools.app/...`, while the legacy `/tools/image-to-pdf/` alias retains its existing source-level noindex and stays outside the canonical inventory.
+## H3.5 activation gate
-Existing static tests continue to cover representative PDF, image, metadata, privacy, local-processing, dependency-integrity, CSP, and network invariants.
+The H3.4A pull request must not merge independently. H3.5 must coordinate its merge with the Hub apex cutover, legacy path redirects, Hub canonical activation, retirement of the old Secure_Tools GitHub Pages apex path, and Web Utilities indexing activation.
-During H3.3, both Pages hostnames remain non-indexable while canonical and `og:url` metadata continue to identify the existing production host. `tools.securetools.app` must not be added to production sitemap files. No Search Console operation belongs to H3.3.
+After deployment, require all of the following before treating the SEO cutover as complete:
-## H3.3 rollback
+- `tools.securetools.app` returns HTTP 200 for all 19 routes and has no response-level noindex header;
+- stable and immutable `pages.dev` endpoints return exact `X-Robots-Tag: noindex, nofollow`;
+- all canonical-bearing pages and `og:url` values identify the tools host;
+- social image URLs, sitemap, and robots sitemap declaration use the tools host;
+- the legacy alias retains page-level noindex and remains outside the sitemap; and
+- the Hub apex, redirect, and Search Console steps are validated by their owning H3.5 work.
-If `tools.securetools.app` is unhealthy after activation:
+## Rollback
-1. remove the `tools` DNS record associated with Pages if Cloudflare does not remove it as part of detachment;
-2. detach `tools.securetools.app` from the Pages project's Custom domains configuration;
-3. confirm `secure-tools-web-bridge.pages.dev` remains healthy and no custom-domain record remains; and
-4. leave `securetools.app`, its root `CNAME`, GitHub Pages, apex and `www` DNS, Search Console, redirects, sitemap, and production metadata untouched.
+If the H3.5 Web Utilities SEO deployment is unhealthy, use the immutable or stable Pages endpoint for diagnosis while keeping those hostnames non-indexable. Coordinate rollback with the Hub/apex migration owner; do not independently create DNS records, change the old apex, or publish conflicting canonical and sitemap states.
-The bridge is additive, so rollback never requires a change to the existing production site. H3.3 does not move the apex, create legacy redirects, begin H3.4, or change application behavior.
+H3.4A itself changes no DNS, Cloudflare custom domain, GitHub Pages configuration, Search Console property, redirect, Worker, Pages Function, or application behavior.
diff --git a/docs/seo.md b/docs/seo.md
index fef0401..b863f7c 100644
--- a/docs/seo.md
+++ b/docs/seo.md
@@ -1,14 +1,19 @@
# Search discovery and metadata
-## Production origin
+Status: this document describes the prepared H3.5 Web Utilities SEO contract. The H3.4A pull request must remain unmerged until the coordinated production cutover. While the branch is open, live `securetools.app` and `tools.securetools.app` retain their H3.3 behavior.
-The canonical public origin is `https://securetools.app`. GitHub Pages serves the custom domain recorded in the root `CNAME` file. Canonical and sitemap URLs must never use the legacy GitHub Pages hostname.
+## Canonical origin
+
+The final Web Utilities canonical origin is `https://tools.securetools.app`. All 18 canonical-bearing pages use self-referencing URLs on that host with their existing paths and trailing slashes.
+
+The root `CNAME` remains `securetools.app` in H3.4A because this preparation task does not change the current GitHub Pages custom domain. H3.5 must coordinate retirement of that legacy production path with the Hub apex cutover before this branch is merged as a production release.
## Crawler discovery
-- `/robots.txt` allows public crawling and points to `https://securetools.app/sitemap.xml`.
-- `/sitemap.xml` lists the 18 real canonical pages intended for indexing.
-- The 404 page and the legacy `/tools/image-to-pdf/` redirect are intentionally `noindex` and absent from the sitemap.
+- `/robots.txt` allows public crawling and points to `https://tools.securetools.app/sitemap.xml`.
+- `/sitemap.xml` lists exactly the 18 canonical Web Utilities pages intended for indexing.
+- The 404 page and legacy `/tools/image-to-pdf/` alias are intentionally `noindex` and absent from the sitemap.
+- No Hub, old apex, GitHub Pages, or `pages.dev` URL belongs in the Web Utilities sitemap.
- Static assets, tests, documentation files, and generated user downloads are not sitemap entries.
The sitemap is reviewed static XML. It omits speculative `lastmod`, `changefreq`, and `priority` values. `tests/seo-foundation.test.mjs` keeps it synchronized with the explicit public-route inventory.
@@ -17,23 +22,51 @@ The sitemap is reviewed static XML. It omits speculative `lastmod`, `changefreq`
Every indexable page has:
-- one HTTPS canonical URL on `securetools.app` using the directory route's trailing slash;
+- one HTTPS canonical URL on `tools.securetools.app` using the existing directory route and trailing slash;
- one non-empty, page-specific title and meta description;
-- `og:type=website`, `og:site_name=Secure Tools`, page-specific Open Graph title and description, an `og:url` equal to the canonical URL, and the reviewed same-origin 1200 × 630 share image;
-- a Twitter/X `summary_large_image` card that reuses the page title, description, and share image;
-- no accidental `noindex` or `nofollow` directive.
+- `og:type=website`, `og:site_name=Secure Tools`, page-specific Open Graph title and description, and an `og:url` equal to the canonical URL;
+- absolute `og:image` and `twitter:image` URLs at `https://tools.securetools.app/assets/images/og-image.png` using the existing reviewed 1200 × 630 asset;
+- a Twitter/X `summary_large_image` card that reuses the page title, description, and share image; and
+- no accidental page-level `noindex` or `nofollow` directive.
+
+Favicon and Apple touch icon files remain same-origin assets under `/assets/icons/`. No remote image service, runtime generator, manifest, service worker, or install behavior is introduced.
+
+## Pages hostname isolation
+
+The Cloudflare deployment artifact uses absolute hostname patterns in `_headers`:
+
+```text
+https://secure-tools-web-bridge.pages.dev/*
+ X-Robots-Tag: noindex, nofollow
+
+https://:version.secure-tools-web-bridge.pages.dev/*
+ X-Robots-Tag: noindex, nofollow
+```
-The shared preview is served statically from `https://securetools.app/assets/images/og-image.png`. Favicon and Apple touch icon files are also same-origin assets under `/assets/icons/`; no remote image service, runtime generator, manifest, service worker, or install behavior is involved.
+This leaves `tools.securetools.app` without the bridge header while retaining duplicate-host protection on stable, branch, and immutable `pages.dev` URLs. It requires no Worker, Pages Function, redirect, or zone-level Transform Rule. The legacy `/tools/image-to-pdf/` page keeps its independent HTML `noindex` directive on every hostname.
-## Language and structured-data decisions
+## Language and structured data
-All six interface languages share the same routable page URL and switch client-side. Secure Tools therefore does not publish fabricated locale URLs, sitemap entries, or `hreflang` tags. The existing i18n runtime continues to update the visible copy, document language, title, description, and matching Open Graph text without changing canonical identity.
+All six interface languages share the same routable page URL and switch client-side. Secure Tools does not publish fabricated locale URLs, sitemap entries, or `hreflang` tags. The i18n runtime continues to update visible copy, document language, title, description, and matching Open Graph text without changing canonical identity.
-JSON-LD is intentionally deferred. The current static metadata already describes each page accurately, and this task does not add speculative ratings, reviews, FAQs, paid offers, organization claims, or language routes. A future schema should be introduced only with a clearly represented product model and dedicated validation.
+JSON-LD remains intentionally absent. H3.4A does not invent schema, ratings, reviews, FAQs, offers, language routes, feeds, or manifest URLs.
## Privacy boundary
-Search discovery is implemented with static text, XML, and HTML metadata. It adds no analytics, telemetry, tracker, cookie, verification script, external font, remote SEO runtime, or processing request. Search Console ownership remains DNS-based.
+Search discovery uses static text, XML, HTML metadata, and static Pages response headers. It adds no analytics, telemetry, tracker, cookie, verification script, external font, remote SEO runtime, Worker, Pages Function, or file-processing request.
+
+## H3.5 coordinated activation
+
+Do not merge the H3.4A pull request as an ordinary application release. Activate it close to all of these H3.5 operations:
+
+1. Hub apex cutover at `securetools.app`;
+2. retirement of the Secure_Tools GitHub Pages apex custom-domain path;
+3. legacy Web Utilities path redirects owned by the H3.5 migration plan;
+4. Hub canonical activation;
+5. deployment of this tools-host canonical, social metadata, sitemap, robots, and hostname-specific indexing contract; and
+6. deployed validation that `tools.securetools.app` is indexable while all `pages.dev` aliases remain noindex.
+
+Prolonged partial activation is unsafe because crawlers could see conflicting canonicals, a new sitemap before the intended host is indexable, duplicate content on `pages.dev`, or Web Utilities metadata published from the old apex while the Hub cutover is incomplete.
## Maintenance
@@ -42,23 +75,16 @@ When an indexable route is added, renamed, redirected, or retired:
1. update its title, description, canonical, and Open Graph metadata;
2. update the route inventory and sitemap together;
3. keep redirects and error pages out of the sitemap and mark them `noindex` when appropriate;
-4. run `node tests/seo-foundation.test.mjs` and `node tests/run-all.mjs`;
-5. inspect the rendered canonical, console, and Network panel before release.
-
-## Search engine submission
-
-After the merged GitHub Pages deployment reaches production:
-
-### Google Search Console
+4. run `node tests/seo-foundation.test.mjs` and `node tests/run-all.mjs`; and
+5. inspect the rendered canonical, response headers, console, and Network panel before release.
-1. Open the `securetools.app` Domain property.
-2. Open **Sitemaps** and submit `sitemap.xml`.
-3. Use URL Inspection for `https://securetools.app/` and request indexing when appropriate.
-4. Inspect major PDF, Image, and Metadata tool URLs after sitemap discovery.
-5. Monitor Page indexing and sitemap processing over the following days.
+## Search Console after H3.5
-DNS ownership is already verified; do not add a Search Console HTML tag or tracking script.
+H3.4A performs no Search Console operation. After the coordinated H3.5 cutover:
-### Bing Webmaster Tools
+- the existing `securetools.app` property monitors the Hub;
+- a `https://tools.securetools.app/` URL-prefix property monitors Web Utilities;
+- submit `https://tools.securetools.app/sitemap.xml` to that URL-prefix property; and
+- do not use the root-property Change of Address tool for this partial subdomain migration.
-Configure Bing after the production sitemap is available. Prefer importing the verified Google Search Console property when Bing offers that option; otherwise add `https://securetools.app/sitemap.xml` directly through Bing Webmaster Tools.
+Bing Webmaster Tools should likewise receive the tools-host sitemap only after H3.5 is live and validated.
diff --git a/index.html b/index.html
index 7e77573..7e0a66d 100644
--- a/index.html
+++ b/index.html
@@ -9,20 +9,20 @@
-
-
+
+
-
+
-
+
Secure Tools — Private tools for everyday files
diff --git a/privacy/index.html b/privacy/index.html
index 8eb87e6..41438c0 100644
--- a/privacy/index.html
+++ b/privacy/index.html
@@ -9,21 +9,21 @@
-
+
-
+
-
+
-
+
Privacy — Secure Tools
diff --git a/robots.txt b/robots.txt
index 550290e..7fbeca0 100644
--- a/robots.txt
+++ b/robots.txt
@@ -1,4 +1,4 @@
User-agent: *
Allow: /
-Sitemap: https://securetools.app/sitemap.xml
+Sitemap: https://tools.securetools.app/sitemap.xml
diff --git a/sitemap.xml b/sitemap.xml
index 5f23eef..db18d1c 100644
--- a/sitemap.xml
+++ b/sitemap.xml
@@ -1,21 +1,21 @@
- https://securetools.app/
- https://securetools.app/about/
- https://securetools.app/privacy/
- https://securetools.app/tools/pdf/
- https://securetools.app/tools/pdf/images-to-pdf/
- https://securetools.app/tools/pdf/merge/
- https://securetools.app/tools/pdf/split/
- https://securetools.app/tools/pdf/organize/
- https://securetools.app/tools/pdf/to-images/
- https://securetools.app/tools/pdf/metadata/
- https://securetools.app/tools/image/
- https://securetools.app/tools/image/converter/
- https://securetools.app/tools/image/resize/
- https://securetools.app/tools/image/compress/
- https://securetools.app/tools/image/metadata/
- https://securetools.app/tools/privacy/
- https://securetools.app/tools/scan/
- https://securetools.app/tools/media/
+ https://tools.securetools.app/
+ https://tools.securetools.app/about/
+ https://tools.securetools.app/privacy/
+ https://tools.securetools.app/tools/pdf/
+ https://tools.securetools.app/tools/pdf/images-to-pdf/
+ https://tools.securetools.app/tools/pdf/merge/
+ https://tools.securetools.app/tools/pdf/split/
+ https://tools.securetools.app/tools/pdf/organize/
+ https://tools.securetools.app/tools/pdf/to-images/
+ https://tools.securetools.app/tools/pdf/metadata/
+ https://tools.securetools.app/tools/image/
+ https://tools.securetools.app/tools/image/converter/
+ https://tools.securetools.app/tools/image/resize/
+ https://tools.securetools.app/tools/image/compress/
+ https://tools.securetools.app/tools/image/metadata/
+ https://tools.securetools.app/tools/privacy/
+ https://tools.securetools.app/tools/scan/
+ https://tools.securetools.app/tools/media/
diff --git a/tests/cloudflare-bridge.test.mjs b/tests/cloudflare-bridge.test.mjs
index 95785d1..465d0dc 100644
--- a/tests/cloudflare-bridge.test.mjs
+++ b/tests/cloudflare-bridge.test.mjs
@@ -14,6 +14,9 @@ assert.match(workflow, /secure-tools-web-bridge/);
assert.match(workflow, /pages deploy .* --project-name=secure-tools-web-bridge --branch=main --commit-hash=\$\{\{ github\.sha \}\}/);
assert.match(workflow, /gitHubToken: \$\{\{ secrets\.GITHUB_TOKEN \}\}/);
assert.match(workflow, /X-Robots-Tag: noindex, nofollow/);
+assert.match(workflow, /https:\/\/secure-tools-web-bridge\.pages\.dev\/\*/);
+assert.match(workflow, /https:\/\/:version\.secure-tools-web-bridge\.pages\.dev\/\*/);
+assert.doesNotMatch(workflow, /printf '\/\*\\n X-Robots-Tag/);
assert.match(workflow, /steps\.deploy\.outputs\.deployment-url/);
assert.match(workflow, /api\.cloudflare\.com\/client\/v4\/accounts\/\$\{CLOUDFLARE_ACCOUNT_ID\}\/pages\/projects\/secure-tools-web-bridge/);
assert.match(workflow, /\["tools\.securetools\.app"\]/);
@@ -24,22 +27,24 @@ assert.match(workflow, /Pages project state:/);
assert.match(workflow, /\[\[ ! -e "\$BRIDGE_DIRECTORY\/CNAME" \]\]/);
assert.match(workflow, /\[\[ ! -e "\$BRIDGE_DIRECTORY\/_redirects" \]\]/);
assert.doesNotMatch(workflow, /securetools\.app\/tools/);
-assert.match(workflow, /node tests\/deployment-smoke\.mjs "\$DEPLOYMENT_URL"/);
-assert.match(workflow, /node tests\/deployment-smoke\.mjs https:\/\/secure-tools-web-bridge\.pages\.dev/);
-assert.match(workflow, /node tests\/deployment-smoke\.mjs https:\/\/tools\.securetools\.app/);
-assert.match(workflow, /node tests\/deployment-smoke\.mjs https:\/\/securetools\.app production/);
+assert.match(workflow, /node tests\/deployment-smoke\.mjs "\$DEPLOYMENT_URL" noindex/);
+assert.match(workflow, /node tests\/deployment-smoke\.mjs https:\/\/secure-tools-web-bridge\.pages\.dev noindex/);
+assert.match(workflow, /node tests\/deployment-smoke\.mjs https:\/\/tools\.securetools\.app indexable/);
+assert.doesNotMatch(workflow, /node tests\/deployment-smoke\.mjs https:\/\/securetools\.app/);
const deploymentSmoke = fs.readFileSync("tests/deployment-smoke.mjs", "utf8");
const routeLines = deploymentSmoke.match(/^ "\/(?:"|[^"].*\/"),$/gm) || [];
assert.equal(routeLines.length, 19, "deployment smoke must validate all 19 H3.1 routes");
assert.match(deploymentSmoke, /redirect: "manual"/);
-assert.match(deploymentSmoke, /\["bridge", "production"\]/);
+assert.match(deploymentSmoke, /\["noindex", "indexable"\]/);
assert.match(deploymentSmoke, /"x-robots-tag"/);
assert.match(deploymentSmoke, /"noindex, nofollow"/);
-assert.match(deploymentSmoke, /new URL\("https:\/\/securetools\.app"\)/);
+assert.match(deploymentSmoke, /new URL\("https:\/\/tools\.securetools\.app"\)/);
assert.match(deploymentSmoke, /canonicalExcludedRoutes = new Set\(\["\/tools\/image-to-pdf\/"\]\)/);
assert.match(deploymentSmoke, /"canonical"/);
assert.match(deploymentSmoke, /"og:url"/);
+assert.match(deploymentSmoke, /"og:image"/);
+assert.match(deploymentSmoke, /"twitter:image"/);
assert.equal(fs.readFileSync("CNAME", "utf8").trim(), "securetools.app");
assert.ok(!fs.existsSync("_headers"), "bridge headers must not enter the GitHub Pages artifact");
diff --git a/tests/deployment-smoke.mjs b/tests/deployment-smoke.mjs
index 68be39b..173cf71 100644
--- a/tests/deployment-smoke.mjs
+++ b/tests/deployment-smoke.mjs
@@ -35,13 +35,14 @@ const assets = [
const canonicalExcludedRoutes = new Set(["/tools/image-to-pdf/"]);
const base = new URL(process.argv[2] || "");
-const mode = process.argv[3] || "bridge";
-const canonicalBase = new URL("https://securetools.app");
+const indexing = process.argv[3] || "noindex";
+const canonicalBase = new URL("https://tools.securetools.app");
+const socialImage = new URL("/assets/images/og-image.png", canonicalBase).href;
assert.equal(base.protocol, "https:", "deployment validation requires HTTPS");
assert.equal(base.pathname, "/", "deployment base URL must not contain a path");
-assert.ok(["bridge", "production"].includes(mode), "deployment mode must be bridge or production");
-if (mode === "production") assert.equal(base.origin, canonicalBase.origin, "production validation must target securetools.app");
+assert.ok(["noindex", "indexable"].includes(indexing), "indexing mode must be noindex or indexable");
+if (indexing === "indexable") assert.equal(base.origin, canonicalBase.origin, "indexable validation must target tools.securetools.app");
function metadataValue(html, selectorName, selectorValue, valueName) {
for (const tag of html.match(/<(?:link|meta)\b[^>]*>/gi) || []) {
@@ -60,8 +61,8 @@ async function request(pathname) {
assert.equal(response.status, 200, `${url.href} must return HTTP 200 without a redirect`);
assert.equal(
response.headers.get("x-robots-tag"),
- mode === "bridge" ? "noindex, nofollow" : null,
- mode === "bridge" ? `${url.href} must remain non-indexable` : `${url.href} must not inherit the bridge noindex header`,
+ indexing === "noindex" ? "noindex, nofollow" : null,
+ indexing === "noindex" ? `${url.href} must remain non-indexable` : `${url.href} must not inherit the pages.dev noindex header`,
);
return response;
}
@@ -72,14 +73,20 @@ for (const route of routes) {
const expectedCanonical = new URL(route, canonicalBase).href;
const canonical = metadataValue(html, "rel", "canonical", "href");
const openGraphUrl = metadataValue(html, "property", "og:url", "content");
+ const openGraphImage = metadataValue(html, "property", "og:image", "content");
+ const twitterImage = metadataValue(html, "name", "twitter:image", "content");
if (canonicalExcludedRoutes.has(route)) {
assert.equal(canonical, "", `${route} must remain outside the canonical inventory`);
assert.equal(openGraphUrl, "", `${route} must remain outside the Open Graph inventory`);
+ assert.equal(openGraphImage, "", `${route} must remain outside the social image inventory`);
+ assert.equal(twitterImage, "", `${route} must remain outside the X image inventory`);
assert.match(html, //i, `${route} must retain its source-level noindex`);
} else {
assert.equal(canonical, expectedCanonical, `${route} canonical changed`);
assert.equal(openGraphUrl, expectedCanonical, `${route} og:url changed`);
+ assert.equal(openGraphImage, socialImage, `${route} og:image changed`);
+ assert.equal(twitterImage, socialImage, `${route} twitter:image changed`);
}
}
@@ -88,4 +95,4 @@ for (const asset of assets) {
await response.arrayBuffer();
}
-console.log(`Deployment smoke checks passed for ${base.origin}: mode=${mode}, 19 routes, 7 assets, no redirects, expected indexing header, 18 production canonicals plus the intentional noindex legacy alias.`);
+console.log(`Deployment smoke checks passed for ${base.origin}: indexing=${indexing}, 19 routes, 7 assets, no redirects, expected indexing header, 18 tools-host canonical and social metadata pages plus the intentional noindex legacy alias.`);
diff --git a/tests/seo-foundation.test.mjs b/tests/seo-foundation.test.mjs
index ae107fe..d78490e 100644
--- a/tests/seo-foundation.test.mjs
+++ b/tests/seo-foundation.test.mjs
@@ -6,7 +6,8 @@ import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const read = (relative) => fs.readFileSync(path.join(root, relative), "utf8");
const readBytes = (relative) => fs.readFileSync(path.join(root, relative));
-const origin = "https://securetools.app";
+const origin = "https://tools.securetools.app";
+const legacyOrigin = "https://securetools.app";
const indexableRoutes = new Map([
["index.html", "/"],
@@ -31,6 +32,7 @@ const indexableRoutes = new Map([
const excludedRoutes = ["404.html", "tools/image-to-pdf/index.html"];
const allHtmlRoutes = [...indexableRoutes.keys(), ...excludedRoutes];
+assert.equal(indexableRoutes.size + 1, 19, "all 19 H3 migration routes remain represented");
const shareImagePath = "assets/images/og-image.png";
const shareImageUrl = `${origin}/${shareImagePath}`;
const iconLinks = new Map([
@@ -77,6 +79,8 @@ for (const [relativeFile, route] of indexableRoutes) {
assert.equal(values(html, //g).length, 1, `${relativeFile}: Twitter image alternative`);
assert.doesNotMatch(html, //);
@@ -140,6 +146,8 @@ const sitemapUrls = values(sitemap, /([^<]+)<\/loc>/g);
assert.deepEqual(sitemapUrls, expectedUrls, "sitemap exactly matches the canonical public route inventory");
assert.equal(new Set(sitemapUrls).size, sitemapUrls.length, "sitemap URLs are unique");
for (const url of sitemapUrls) assert.ok(url.startsWith(`${origin}/`), `${url}: production origin`);
+assert.doesNotMatch(sitemap, /https:\/\/securetools\.app/i, "sitemap omits the old Web Utilities host");
+assert.doesNotMatch(sitemap, /pages\.dev/i, "sitemap omits Pages hostnames");
assert.equal(read("CNAME").trim(), "securetools.app", "GitHub Pages custom domain");
diff --git a/tools/image/compress/index.html b/tools/image/compress/index.html
index f737c23..a3ba882 100644
--- a/tools/image/compress/index.html
+++ b/tools/image/compress/index.html
@@ -7,17 +7,17 @@
-
+
-
-
+
+
-
+
Image Compressor — Secure Tools
diff --git a/tools/image/converter/index.html b/tools/image/converter/index.html
index 6ede302..0900258 100644
--- a/tools/image/converter/index.html
+++ b/tools/image/converter/index.html
@@ -10,21 +10,21 @@
-
+
-
+
-
+
-
+
Image Converter — Secure Tools
diff --git a/tools/image/index.html b/tools/image/index.html
index f7b9b49..28b63cc 100644
--- a/tools/image/index.html
+++ b/tools/image/index.html
@@ -10,21 +10,21 @@
-
+
-
+
-
+
-
+
Image tools — Secure Tools
diff --git a/tools/image/metadata/index.html b/tools/image/metadata/index.html
index 948f36f..6eeff01 100644
--- a/tools/image/metadata/index.html
+++ b/tools/image/metadata/index.html
@@ -7,17 +7,17 @@
-
+
-
-
+
+
-
+
Image Metadata Inspector & Cleaner — Secure Tools
diff --git a/tools/image/resize/index.html b/tools/image/resize/index.html
index fbb7724..01524dc 100644
--- a/tools/image/resize/index.html
+++ b/tools/image/resize/index.html
@@ -9,17 +9,17 @@
-
+
-
-
+
+
-
+
Image Resize — Secure Tools
diff --git a/tools/media/index.html b/tools/media/index.html
index 1363d66..185851f 100644
--- a/tools/media/index.html
+++ b/tools/media/index.html
@@ -6,17 +6,17 @@
-
+
-
-
+
+
-
+
Media tools — Secure ToolsSkip to main content