diff --git a/CHANGELOG.md b/CHANGELOG.md index 7189542..f7dcd80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Added the single-file Image Metadata Inspector & Cleaner for JPEG, PNG, and WebP with honest partial/opaque reporting, authoritative Privacy Clean, ICC preservation, and fail-closed verification before save. - Pinned the immutable `secure-metadata v0.1.1` browser Release artifact as a same-origin dependency with exact provenance and SHA-256 release-gate coverage. - Added per-file and aggregate compression metrics that distinguish byte savings from larger generated results. +- Added production crawler discovery files, canonical URLs, page-specific Open Graph metadata, SEO regression coverage, and search-engine submission guidance for securetools.app. - Added per-image output dimension/pixel checks and a 200-megapixel aggregate resize-output workload limit. - Added JPEG, PNG, and WebP input/output, lossy quality controls for JPEG/WebP, deterministic white JPEG transparency, metadata-stripping canvas re-encoding, collision-safe Unicode names, and ZIP batch output. - Added per-file, queue, dimension, decoded-pixel, and 200-megapixel aggregate-work protections with recoverable errors. diff --git a/README.md b/README.md index f995931..c675656 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ The [documentation index](./docs/README.md) links to the maintained sources of t - architecture and delivery; - privacy, local processing, and network boundaries; - production dependencies and vendoring; +- search discovery, canonical metadata, and search-engine submission; - production and planned tool status; - release QA evidence; - UX, accessibility, localization, and format-specific privacy audits. diff --git a/about/index.html b/about/index.html index b215fbe..2e88f15 100644 --- a/about/index.html +++ b/about/index.html @@ -5,6 +5,9 @@ + + + diff --git a/docs/README.md b/docs/README.md index 521d02a..80bf9c3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ The root [README](../README.md) introduces Secure Tools. This directory owns det | [Privacy model](./privacy-model.md) | Local-processing and network boundaries, storage, security controls, and bounded privacy claims | | [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 | | [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/seo.md b/docs/seo.md new file mode 100644 index 0000000..9167d96 --- /dev/null +++ b/docs/seo.md @@ -0,0 +1,63 @@ +# Search discovery and metadata + +## Production origin + +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. + +## 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. +- 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. + +## Page metadata contract + +Every indexable page has: + +- one HTTPS canonical URL on `securetools.app` using the directory route's 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, and an `og:url` equal to the canonical URL; +- no accidental `noindex` or `nofollow` directive. + +No `og:image` is declared because the repository does not yet contain a reviewed production share image. Twitter/X card tags are omitted because they would duplicate the current Open Graph fields without an image-specific presentation. + +## Language and structured-data decisions + +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. + +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. + +## 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. + +## Maintenance + +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 + +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. + +DNS ownership is already verified; do not add a Search Console HTML tag or tracking script. + +### Bing Webmaster Tools + +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. diff --git a/index.html b/index.html index 0937429..c7d9508 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,8 @@ + + diff --git a/privacy/index.html b/privacy/index.html index edf8f47..537063b 100644 --- a/privacy/index.html +++ b/privacy/index.html @@ -5,6 +5,9 @@ + + + diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..550290e --- /dev/null +++ b/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://securetools.app/sitemap.xml diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..5f23eef --- /dev/null +++ b/sitemap.xml @@ -0,0 +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/ + diff --git a/tests/image-resize.test.mjs b/tests/image-resize.test.mjs index df1981b..c11cead 100644 --- a/tests/image-resize.test.mjs +++ b/tests/image-resize.test.mjs @@ -100,7 +100,7 @@ assert.match(html, /name="resize-mode"[^>]*value="pixels"/); assert.match(html, assert.match(html, /id="aspect-ratio"[^>]*checked/); assert.match(html, /id="allow-enlargement"/); assert.match(html, /id="output-format"[\s\S]*value="original"[\s\S]*value="jpeg"[\s\S]*value="png"[\s\S]*value="webp"/); assert.match(html, /role="status" aria-live="polite"/); assert.match(html, /assets\/vendor\/jszip\/jszip\.min\.js/); -assert.match(html, /connect-src 'none'/); assert.doesNotMatch(html, /unsafe-inline|unsafe-eval|https?:\/\/(?!github\.com)/); +assert.match(html, /connect-src 'none'/); assert.doesNotMatch(html, /unsafe-inline|unsafe-eval/); assert.doesNotMatch(html, /]+src="https?:\/\//i); assert.doesNotMatch(html, /]+rel="stylesheet"[^>]+href="https?:/i); assert.match(app, /finally\s*\{[\s\S]*state\.busy = false[\s\S]*elements\.progress\.hidden = true/); assert.match(app, /URL\.revokeObjectURL/); assert.match(logic, /decoded\?\.close\(\)/); assert.match(read("tools/shared/image.js"), /imageOrientation: "from-image"/); diff --git a/tests/run-all.mjs b/tests/run-all.mjs index 0aba6cc..0b587ee 100644 --- a/tests/run-all.mjs +++ b/tests/run-all.mjs @@ -35,6 +35,7 @@ for (const test of [ "tests/pdf-split.test.mjs", "tests/security-hardening.test.mjs", "tests/release-gate.test.mjs", + "tests/seo-foundation.test.mjs", "tests/home-structure.test.mjs", "tests/typography-i18n-layout.test.mjs", "tests/pdf-to-images.test.mjs", diff --git a/tests/seo-foundation.test.mjs b/tests/seo-foundation.test.mjs new file mode 100644 index 0000000..cec2b84 --- /dev/null +++ b/tests/seo-foundation.test.mjs @@ -0,0 +1,94 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +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 origin = "https://securetools.app"; + +const indexableRoutes = new Map([ + ["index.html", "/"], + ["about/index.html", "/about/"], + ["privacy/index.html", "/privacy/"], + ["tools/pdf/index.html", "/tools/pdf/"], + ["tools/pdf/images-to-pdf/index.html", "/tools/pdf/images-to-pdf/"], + ["tools/pdf/merge/index.html", "/tools/pdf/merge/"], + ["tools/pdf/split/index.html", "/tools/pdf/split/"], + ["tools/pdf/organize/index.html", "/tools/pdf/organize/"], + ["tools/pdf/to-images/index.html", "/tools/pdf/to-images/"], + ["tools/pdf/metadata/index.html", "/tools/pdf/metadata/"], + ["tools/image/index.html", "/tools/image/"], + ["tools/image/converter/index.html", "/tools/image/converter/"], + ["tools/image/resize/index.html", "/tools/image/resize/"], + ["tools/image/compress/index.html", "/tools/image/compress/"], + ["tools/image/metadata/index.html", "/tools/image/metadata/"], + ["tools/privacy/index.html", "/tools/privacy/"], + ["tools/scan/index.html", "/tools/scan/"], + ["tools/media/index.html", "/tools/media/"], +]); + +const excludedRoutes = ["404.html", "tools/image-to-pdf/index.html"]; +const expectedUrls = [...indexableRoutes.values()].map((route) => `${origin}${route}`); +const titles = new Set(); +const descriptions = new Set(); + +function values(html, pattern) { + return [...html.matchAll(pattern)].map((match) => match[1]); +} + +for (const [relativeFile, route] of indexableRoutes) { + const html = read(relativeFile); + const expectedUrl = `${origin}${route}`; + const canonical = values(html, //g); + const title = values(html, /([^<]+)<\/title>/g); + const description = values(html, /<meta name="description" content="([^"]+)">/g); + + assert.deepEqual(canonical, [expectedUrl], `${relativeFile}: one exact canonical URL`); + assert.equal(title.length, 1, `${relativeFile}: one title`); + assert.ok(title[0].trim(), `${relativeFile}: non-empty title`); + assert.equal(description.length, 1, `${relativeFile}: one description`); + assert.ok(description[0].trim(), `${relativeFile}: non-empty description`); + assert.deepEqual(values(html, /<meta property="og:type" content="([^"]+)">/g), ["website"], `${relativeFile}: Open Graph type`); + assert.deepEqual(values(html, /<meta property="og:site_name" content="([^"]+)">/g), ["Secure Tools"], `${relativeFile}: Open Graph site name`); + assert.deepEqual(values(html, /<meta property="og:url" content="([^"]+)">/g), [expectedUrl], `${relativeFile}: Open Graph URL matches canonical`); + assert.equal(values(html, /<meta property="og:title" content="([^"]+)">/g).length, 1, `${relativeFile}: one Open Graph title`); + assert.equal(values(html, /<meta property="og:description" content="([^"]+)">/g).length, 1, `${relativeFile}: one Open Graph description`); + assert.doesNotMatch(html, /<meta name="robots" content="[^"]*(?:noindex|nofollow)/i, `${relativeFile}: indexable`); + assert.doesNotMatch(html, /securetoolsproject\.github\.io/i, `${relativeFile}: no legacy canonical host`); + assert.doesNotMatch(html, /hreflang=/i, `${relativeFile}: no fabricated locale URL`); + titles.add(title[0]); + descriptions.add(description[0]); +} + +assert.equal(titles.size, indexableRoutes.size, "page titles are unique"); +assert.equal(descriptions.size, indexableRoutes.size, "page descriptions are unique"); + +for (const relativeFile of excludedRoutes) { + const html = read(relativeFile); + assert.match(html, /<meta name="robots" content="noindex">/i, `${relativeFile}: intentional noindex`); + assert.doesNotMatch(html, /rel="canonical"/i, `${relativeFile}: excluded from canonical inventory`); +} + +const robots = read("robots.txt"); +assert.equal(robots, `User-agent: *\nAllow: /\n\nSitemap: ${origin}/sitemap.xml\n`); +assert.doesNotMatch(robots, /^Disallow:\s*\/$/im, "robots.txt does not block the site"); + +const sitemap = read("sitemap.xml"); +assert.match(sitemap, /^<\?xml version="1\.0" encoding="UTF-8"\?>/); +assert.match(sitemap, /<urlset xmlns="http:\/\/www\.sitemaps\.org\/schemas\/sitemap\/0\.9">/); +assert.match(sitemap, /<\/urlset>\s*$/); +assert.doesNotMatch(sitemap, /<(?:lastmod|changefreq|priority)>/); +const sitemapUrls = values(sitemap, /<loc>([^<]+)<\/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.equal(read("CNAME").trim(), "securetools.app", "GitHub Pages custom domain"); + +const productionHtml = [...indexableRoutes.keys()].map(read).join("\n"); +assert.doesNotMatch(productionHtml, /google-analytics|googletagmanager|gtag\(|meta pixel|facebook\.net\/.*fbevents|session replay/i); +assert.doesNotMatch(productionHtml, /<script[^>]+src="https?:\/\//i, "no remote runtime scripts"); +assert.doesNotMatch(productionHtml, /<link[^>]+href="https?:\/\/[^\"]+"[^>]+rel="stylesheet"/i, "no remote stylesheets or fonts"); + +console.log("SEO route, crawler, canonical, metadata, privacy, and custom-domain checks passed."); diff --git a/tools/image/compress/index.html b/tools/image/compress/index.html index 3799945..7e2825d 100644 --- a/tools/image/compress/index.html +++ b/tools/image/compress/index.html @@ -1,7 +1,10 @@ <!doctype html> <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' blob: data:; connect-src 'none'; object-src 'none'; frame-src 'none'; base-uri 'self'; form-action 'self'"> -<meta name="description" content="Compress JPEG, PNG, and WebP images locally in your browser."><meta name="theme-color" content="#f7f7f5"><meta property="og:type" content="website"><meta property="og:title" content="Image Compressor — Secure Tools"><meta property="og:description" content="Compress JPEG, PNG, and WebP images locally in your browser."><title>Image Compressor — Secure Tools + + + + Image Compressor — Secure Tools diff --git a/tools/image/converter/index.html b/tools/image/converter/index.html index 13aa254..6c24ddb 100644 --- a/tools/image/converter/index.html +++ b/tools/image/converter/index.html @@ -5,6 +5,9 @@ + + + diff --git a/tools/image/index.html b/tools/image/index.html index 3afd113..a39b35d 100644 --- a/tools/image/index.html +++ b/tools/image/index.html @@ -5,6 +5,9 @@ + + + diff --git a/tools/image/metadata/index.html b/tools/image/metadata/index.html index 29dda62..916bb9c 100644 --- a/tools/image/metadata/index.html +++ b/tools/image/metadata/index.html @@ -1,7 +1,10 @@ -Image Metadata Inspector & Cleaner — Secure Tools + + + + Image Metadata Inspector & Cleaner — Secure Tools diff --git a/tools/image/resize/index.html b/tools/image/resize/index.html index 0339239..96bd4ff 100644 --- a/tools/image/resize/index.html +++ b/tools/image/resize/index.html @@ -3,7 +3,10 @@ - Image Resize — Secure Tools + + + + Image Resize — Secure Tools diff --git a/tools/media/index.html b/tools/media/index.html index 6e7e6f6..91a2f24 100644 --- a/tools/media/index.html +++ b/tools/media/index.html @@ -1,6 +1,9 @@ - Media tools — Secure Tools + + + + Media tools — Secure Tools

Tool category

Media tools

Small audio and video utilities designed for local processing.

  • Coming soon

    Audio converter

    Convert common audio formats on your device.

  • Coming soon

    Video trim

    Trim a clip without sending it to a server.

This category is planned. No unavailable item is presented as working.

diff --git a/tools/pdf/images-to-pdf/index.html b/tools/pdf/images-to-pdf/index.html index 2d332ad..109fc7a 100644 --- a/tools/pdf/images-to-pdf/index.html +++ b/tools/pdf/images-to-pdf/index.html @@ -5,6 +5,9 @@ + + + diff --git a/tools/pdf/index.html b/tools/pdf/index.html index 395fc34..dc19c59 100644 --- a/tools/pdf/index.html +++ b/tools/pdf/index.html @@ -1,6 +1,9 @@ - PDF tools — Secure Tools + + + + PDF tools — Secure Tools diff --git a/tools/pdf/merge/index.html b/tools/pdf/merge/index.html index e750924..929af7e 100644 --- a/tools/pdf/merge/index.html +++ b/tools/pdf/merge/index.html @@ -1,6 +1,9 @@ - Merge PDF — Secure Tools + + + + Merge PDF — Secure Tools diff --git a/tools/pdf/metadata/index.html b/tools/pdf/metadata/index.html index 7d236c2..5fda596 100644 --- a/tools/pdf/metadata/index.html +++ b/tools/pdf/metadata/index.html @@ -1,7 +1,10 @@ -PDF Metadata Inspector & Cleaner — Secure Tools + + + + PDF Metadata Inspector & Cleaner — Secure Tools diff --git a/tools/pdf/organize/index.html b/tools/pdf/organize/index.html index cee1d4f..19a6d18 100644 --- a/tools/pdf/organize/index.html +++ b/tools/pdf/organize/index.html @@ -1,7 +1,10 @@ -Organize PDF — Secure Tools + + + + Organize PDF — Secure Tools diff --git a/tools/pdf/split/index.html b/tools/pdf/split/index.html index 7cdf913..7ee1c1a 100644 --- a/tools/pdf/split/index.html +++ b/tools/pdf/split/index.html @@ -1,6 +1,9 @@ - Split PDF — Secure Tools + + + + Split PDF — Secure Tools diff --git a/tools/pdf/to-images/index.html b/tools/pdf/to-images/index.html index 7aacb7f..7cf5bd8 100644 --- a/tools/pdf/to-images/index.html +++ b/tools/pdf/to-images/index.html @@ -1,6 +1,9 @@ -PDF to Images — Secure Tools + + + + PDF to Images — Secure Tools diff --git a/tools/privacy/index.html b/tools/privacy/index.html index c85fe5e..3f8d148 100644 --- a/tools/privacy/index.html +++ b/tools/privacy/index.html @@ -1,6 +1,9 @@ - Privacy metadata tools — Secure Tools + + + + Privacy metadata tools — Secure Tools
diff --git a/tools/scan/index.html b/tools/scan/index.html index b09d064..888b9c7 100644 --- a/tools/scan/index.html +++ b/tools/scan/index.html @@ -1,6 +1,9 @@ - Scan and OCR tools — Secure Tools + + + + Scan and OCR tools — Secure Tools

Tool category

Scan & OCR tools

Turn scans into useful documents while keeping source material on your device.

  • Coming soon

    Image to text

    Recognize text from an image locally.

  • Coming soon

    Document scanner

    Crop and clean photographed documents.

This category is planned. No unavailable item is presented as working.