Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions 404.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/assets/icons/favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
<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="The requested Secure Tools page could not be found.">
<meta name="robots" content="noindex">
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- 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 production favicon, Apple touch, and same-origin social preview assets with Open Graph and Twitter/X card metadata.
- 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.
Expand Down
13 changes: 13 additions & 0 deletions about/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/assets/icons/favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
<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="Why Secure Tools builds open, local-first file utilities that can be inspected instead of simply trusted.">
<link rel="canonical" href="https://securetools.app/about/">
Expand All @@ -12,6 +16,15 @@
<meta property="og:type" content="website">
<meta property="og:title" content="About — Secure Tools">
<meta property="og:description" content="Why Secure Tools builds open, local-first file utilities that can be inspected instead of simply trusted.">
<meta property="og:image" content="https://securetools.app/assets/images/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Secure Tools — Local-first browser utilities for PDF, Image, and Privacy">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="About — Secure Tools">
<meta name="twitter:description" content="Why Secure Tools builds open, local-first file utilities that can be inspected instead of simply trusted.">
<meta name="twitter:image" content="https://securetools.app/assets/images/og-image.png">
<meta name="twitter:image:alt" content="Secure Tools — Local-first browser utilities for PDF, Image, and Privacy">
<title>About — Secure Tools</title>
<script src="../js/theme-bootstrap.js"></script>
<link rel="stylesheet" href="../css/base.css">
Expand Down
Binary file added assets/icons/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icons/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icons/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icons/favicon.ico
Binary file not shown.
Binary file added assets/images/og-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions docs/seo.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ 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;
- `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.

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.
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.

## Language and structured-data decisions

Expand Down
13 changes: 13 additions & 0 deletions docs/v2-release-qa.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,17 @@ The user completed real local-browser release QA on 2026-08-27 in Chrome 151.0.7

The Metadata workflows manually covered Privacy Clean, Customize/custom cleaning, metadata removal, source/action UX, and the decoded summary/details flow. The triggering JPEG carried EXIF Orientation=6. Before the dependency patch, removing the complete EXIF APP1 segment could discard rendering-critical Orientation and make the cleaned image display rotated. With secure-metadata v0.1.1, one valid unambiguous IFD0 Orientation value from 1 through 8 is preserved, other targeted EXIF/GPS metadata is removed, and pixel data is neither decoded nor re-encoded. The user manually verified the corrected behavior, so the Orientation blocker is **RESOLVED / PASS**.

## Branding and social preview manual QA

The user completed real-browser validation for PR #66 on 2026-08-28. This evidence covers the favicon and social-preview polish added after the v2.0.0-rc.1 pre-release was published.

| Reported check | Result |
| --- | --- |
| Homepage favicon | PASS |
| Direct tool URL favicon | PASS |
| Light and Dark theme regression | PASS |
| New browser console errors | PASS — none observed |
| Broken static asset requests | PASS — none observed |
| Unexpected external network requests | PASS — none observed |

The evidence above records only the test areas and outcomes explicitly reported by the user; it does not invent unreported sub-test detail. Release-required interactive checks are complete, while the historical automated browser attempts remain **BLOCKED**.
13 changes: 13 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/assets/icons/favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
<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="Convert, inspect, and process files privately in your browser. No uploads, accounts, analytics, or tracking.">
<link rel="canonical" href="https://securetools.app/">
Expand All @@ -11,6 +15,15 @@
<meta property="og:type" content="website">
<meta property="og:title" content="Secure Tools — Private tools for everyday files">
<meta property="og:description" content="Convert, inspect, and process files privately in your browser. No uploads, accounts, analytics, or tracking.">
<meta property="og:image" content="https://securetools.app/assets/images/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Secure Tools — Local-first browser utilities for PDF, Image, and Privacy">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Secure Tools — Private tools for everyday files">
<meta name="twitter:description" content="Convert, inspect, and process files privately in your browser. No uploads, accounts, analytics, or tracking.">
<meta name="twitter:image" content="https://securetools.app/assets/images/og-image.png">
<meta name="twitter:image:alt" content="Secure Tools — Local-first browser utilities for PDF, Image, and Privacy">
<meta property="og:site_name" content="Secure Tools">
<title>Secure Tools — Private tools for everyday files</title>
<script src="js/theme-bootstrap.js"></script>
Expand Down
2 changes: 2 additions & 0 deletions js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ const translateDocument = () => {
document.querySelector('meta[name="description"]')?.setAttribute("content", t(`metadata.${pageKey}.description`));
document.querySelector('meta[property="og:title"]')?.setAttribute("content", t(`metadata.${pageKey}.title`));
document.querySelector('meta[property="og:description"]')?.setAttribute("content", t(`metadata.${pageKey}.description`));
document.querySelector('meta[name="twitter:title"]')?.setAttribute("content", t(`metadata.${pageKey}.title`));
document.querySelector('meta[name="twitter:description"]')?.setAttribute("content", t(`metadata.${pageKey}.description`));
};

export function initializeI18n() {
Expand Down
13 changes: 13 additions & 0 deletions privacy/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/assets/icons/favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
<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="How Secure Tools keeps file processing local, avoids tracking, and uses local storage only for preferences.">
<link rel="canonical" href="https://securetools.app/privacy/">
Expand All @@ -12,6 +16,15 @@
<meta property="og:type" content="website">
<meta property="og:title" content="Privacy — Secure Tools">
<meta property="og:description" content="How Secure Tools keeps file processing local, avoids tracking, and uses local storage only for preferences.">
<meta property="og:image" content="https://securetools.app/assets/images/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Secure Tools — Local-first browser utilities for PDF, Image, and Privacy">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Privacy — Secure Tools">
<meta name="twitter:description" content="How Secure Tools keeps file processing local, avoids tracking, and uses local storage only for preferences.">
<meta name="twitter:image" content="https://securetools.app/assets/images/og-image.png">
<meta name="twitter:image:alt" content="Secure Tools — Local-first browser utilities for PDF, Image, and Privacy">
<title>Privacy — Secure Tools</title>
<script src="../js/theme-bootstrap.js"></script>
<link rel="stylesheet" href="../css/base.css">
Expand Down
4 changes: 3 additions & 1 deletion tests/pdf-merge-and-categories.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ function testRoutesTranslationsAndPrivacy() {
if (/^(?:https?:|mailto:|#)/i.test(rawReference)) continue;
const reference = rawReference.split(/[?#]/)[0];
if (!reference) continue;
let target = path.resolve(path.dirname(file), reference);
let target = reference.startsWith("/")
? path.resolve(root, reference.slice(1))
: path.resolve(path.dirname(file), reference);
if (reference.endsWith("/") || (fs.existsSync(target) && fs.statSync(target).isDirectory())) target = path.join(target, "index.html");
assert.equal(fs.existsSync(target), true, `${relative} has missing ${attribute}: ${rawReference}`);
}
Expand Down
59 changes: 58 additions & 1 deletion tests/seo-foundation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 indexableRoutes = new Map([
Expand All @@ -29,6 +30,15 @@ const indexableRoutes = new Map([
]);

const excludedRoutes = ["404.html", "tools/image-to-pdf/index.html"];
const allHtmlRoutes = [...indexableRoutes.keys(), ...excludedRoutes];
const shareImagePath = "assets/images/og-image.png";
const shareImageUrl = `${origin}/${shareImagePath}`;
const iconLinks = new Map([
["assets/icons/favicon.ico", '<link rel="icon" href="/assets/icons/favicon.ico" sizes="any">'],
["assets/icons/favicon-32x32.png", '<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png">'],
["assets/icons/favicon-16x16.png", '<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png">'],
["assets/icons/apple-touch-icon.png", '<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">'],
]);
const expectedUrls = [...indexableRoutes.values()].map((route) => `${origin}${route}`);
const titles = new Set();
const descriptions = new Set();
Expand All @@ -54,6 +64,17 @@ for (const [relativeFile, route] of indexableRoutes) {
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`);
const openGraphTitle = values(html, /<meta property="og:title" content="([^"]+)">/g);
const openGraphDescription = values(html, /<meta property="og:description" content="([^"]+)">/g);
assert.deepEqual(values(html, /<meta property="og:image" content="([^"]+)">/g), [shareImageUrl], `${relativeFile}: same-origin Open Graph image`);
assert.deepEqual(values(html, /<meta property="og:image:width" content="([^"]+)">/g), ["1200"], `${relativeFile}: Open Graph image width`);
assert.deepEqual(values(html, /<meta property="og:image:height" content="([^"]+)">/g), ["630"], `${relativeFile}: Open Graph image height`);
assert.equal(values(html, /<meta property="og:image:alt" content="([^"]+)">/g).length, 1, `${relativeFile}: Open Graph image alternative`);
assert.deepEqual(values(html, /<meta name="twitter:card" content="([^"]+)">/g), ["summary_large_image"], `${relativeFile}: Twitter card type`);
assert.deepEqual(values(html, /<meta name="twitter:title" content="([^"]+)">/g), openGraphTitle, `${relativeFile}: Twitter title matches Open Graph`);
assert.deepEqual(values(html, /<meta name="twitter:description" content="([^"]+)">/g), openGraphDescription, `${relativeFile}: Twitter description matches Open Graph`);
assert.deepEqual(values(html, /<meta name="twitter:image" content="([^"]+)">/g), [shareImageUrl], `${relativeFile}: same-origin Twitter image`);
assert.equal(values(html, /<meta name="twitter:image:alt" content="([^"]+)">/g).length, 1, `${relativeFile}: Twitter image alternative`);
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`);
Expand All @@ -70,6 +91,42 @@ for (const relativeFile of excludedRoutes) {
assert.doesNotMatch(html, /rel="canonical"/i, `${relativeFile}: excluded from canonical inventory`);
}

for (const relativeFile of allHtmlRoutes) {
const html = read(relativeFile);
for (const [asset, markup] of iconLinks) {
assert.equal(html.split(markup).length - 1, 1, `${relativeFile}: one exact ${asset} link`);
assert.ok(fs.existsSync(path.join(root, asset)), `${relativeFile}: ${asset} resolves`);
}
}

function pngDimensions(relativeFile) {
const bytes = readBytes(relativeFile);
assert.deepEqual([...bytes.subarray(0, 8)], [137, 80, 78, 71, 13, 10, 26, 10], `${relativeFile}: PNG signature`);
return [bytes.readUInt32BE(16), bytes.readUInt32BE(20)];
}

assert.deepEqual(pngDimensions("assets/icons/favicon-16x16.png"), [16, 16]);
assert.deepEqual(pngDimensions("assets/icons/favicon-32x32.png"), [32, 32]);
assert.deepEqual(pngDimensions("assets/icons/apple-touch-icon.png"), [180, 180]);
assert.deepEqual(pngDimensions(shareImagePath), [1200, 630]);

const ico = readBytes("assets/icons/favicon.ico");
assert.equal(ico.readUInt16LE(0), 0, "favicon ICO reserved field");
assert.equal(ico.readUInt16LE(2), 1, "favicon ICO image type");
const icoCount = ico.readUInt16LE(4);
assert.equal(icoCount, 3, "favicon ICO image count");
const icoSizes = Array.from({ length: icoCount }, (_, index) => {
const offset = 6 + (index * 16);
const width = ico[offset] || 256;
const height = ico[offset + 1] || 256;
return `${width}x${height}`;
}).sort();
assert.deepEqual(icoSizes, ["16x16", "32x32", "48x48"]);

const i18nRuntime = read("js/i18n.js");
assert.match(i18nRuntime, /meta\[name="twitter:title"\]/, "i18n updates Twitter title");
assert.match(i18nRuntime, /meta\[name="twitter:description"\]/, "i18n updates Twitter description");

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");
Expand All @@ -91,4 +148,4 @@ assert.doesNotMatch(productionHtml, /google-analytics|googletagmanager|gtag\(|me
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.");
console.log("SEO route, branding asset, canonical, metadata, privacy, and custom-domain checks passed.");
5 changes: 5 additions & 0 deletions tools/image-to-pdf/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/assets/icons/favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">

<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 http-equiv="refresh" content="0; url=../pdf/images-to-pdf/">
<meta name="robots" content="noindex">
Expand Down
Loading
Loading