From e49ea577911fed6c2785123b504fe560231b399f Mon Sep 17 00:00:00 2001 From: Javid Date: Thu, 6 Aug 2026 19:18:09 +0200 Subject: [PATCH 01/13] Add website quality acceptance tests --- tests/site-quality.test.mjs | 74 +++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/site-quality.test.mjs diff --git a/tests/site-quality.test.mjs b/tests/site-quality.test.mjs new file mode 100644 index 0000000..c2c3539 --- /dev/null +++ b/tests/site-quality.test.mjs @@ -0,0 +1,74 @@ +import assert from "node:assert/strict"; +import { readdir, readFile, stat } from "node:fs/promises"; +import path from "node:path"; +import test from "node:test"; + +const root = new URL("../", import.meta.url); +const out = new URL("../out/", import.meta.url); + +async function walk(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + const absolute = path.join(directory, entry.name); + if (entry.isDirectory()) files.push(...(await walk(absolute))); + else files.push(absolute); + } + return files; +} + +test("static page exposes baseline accessibility semantics", async () => { + const html = await readFile(new URL("index.html", out), "utf8"); + const sourceCss = await readFile(new URL("../app/globals.css", import.meta.url), "utf8"); + + assert.match(html, /]*\blang=["']fa["'])(?=[^>]*\bdir=["']rtl["'])[^>]*>/i); + assert.equal((html.match(/]*>\s*پرش به محتوای اصلی/i); + + for (const image of html.match(/]*>/gi) ?? []) { + assert.match(image, /\balt=["'][^"']*["']/i, `image is missing alt: ${image}`); + } + + assert.match(sourceCss, /:focus-visible/); + assert.match(sourceCss, /prefers-reduced-motion:\s*reduce/); +}); + +test("static export contains canonical SEO discovery metadata", async () => { + const html = await readFile(new URL("index.html", out), "utf8"); + const robots = await readFile(new URL("robots.txt", out), "utf8"); + const sitemap = await readFile(new URL("sitemap.xml", out), "utf8"); + + assert.match(html, /]*\bname=["']description["'])(?=[^>]*\bcontent=["'][^"']{40,}["'])[^>]*>/i); + assert.match(html, /]*\brel=["']canonical["'])(?=[^>]*\bhref=["']https:\/\/corelinkplatform\.ir\/?["'])[^>]*>/i); + assert.match(html, /]*\bproperty=["']og:title["'])[^>]*>/i); + assert.match(html, /]*\bname=["']robots["'])(?=[^>]*\bcontent=["'][^"']*index[^"']*follow[^"']*["'])[^>]*>/i); + assert.match(robots, /Sitemap:\s*https:\/\/corelinkplatform\.ir\/sitemap\.xml/i); + assert.match(sitemap, /https:\/\/corelinkplatform\.ir<\/loc>/i); +}); + +test("production HTML has no unreviewed analytics tracker", async () => { + const html = await readFile(new URL("index.html", out), "utf8"); + assert.doesNotMatch( + html, + /googletagmanager|google-analytics|plausible\.io|umami|posthog/i, + "analytics requires the documented privacy/consent review before shipping", + ); +}); + +test("static artifact stays inside release performance budgets", async () => { + const htmlBytes = (await stat(new URL("index.html", out))).size; + const staticDir = path.fileURLToPath(new URL("_next/static/", out)); + const files = await walk(staticDir); + + let jsBytes = 0; + let cssBytes = 0; + for (const file of files) { + const bytes = (await stat(file)).size; + if (file.endsWith(".js")) jsBytes += bytes; + if (file.endsWith(".css")) cssBytes += bytes; + } + + assert.ok(htmlBytes <= 200_000, `HTML budget exceeded: ${htmlBytes} bytes`); + assert.ok(jsBytes <= 2_000_000, `JS budget exceeded: ${jsBytes} bytes`); + assert.ok(cssBytes <= 500_000, `CSS budget exceeded: ${cssBytes} bytes`); +}); From 3720d4542e0aa29a87b6849170d8afd9a5eac4ec Mon Sep 17 00:00:00 2001 From: Javid Date: Thu, 6 Aug 2026 19:18:18 +0200 Subject: [PATCH 02/13] Publish explicit crawler policy --- app/robots.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 app/robots.ts diff --git a/app/robots.ts b/app/robots.ts new file mode 100644 index 0000000..736dd32 --- /dev/null +++ b/app/robots.ts @@ -0,0 +1,11 @@ +import type { MetadataRoute } from "next"; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: "*", + allow: "/", + }, + sitemap: "https://corelinkplatform.ir/sitemap.xml", + }; +} From 2eb16d4853837be2ae519579547c8f4d74ae0417 Mon Sep 17 00:00:00 2001 From: Javid Date: Thu, 6 Aug 2026 19:18:23 +0200 Subject: [PATCH 03/13] Publish canonical website sitemap --- app/sitemap.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 app/sitemap.ts diff --git a/app/sitemap.ts b/app/sitemap.ts new file mode 100644 index 0000000..c342c70 --- /dev/null +++ b/app/sitemap.ts @@ -0,0 +1,12 @@ +import type { MetadataRoute } from "next"; + +export default function sitemap(): MetadataRoute.Sitemap { + return [ + { + url: "https://corelinkplatform.ir", + lastModified: new Date("2026-08-06"), + changeFrequency: "weekly", + priority: 1, + }, + ]; +} From a646beb788ae06d8821e5ef3909c2f0843520e94 Mon Sep 17 00:00:00 2001 From: Javid Date: Thu, 6 Aug 2026 19:18:51 +0200 Subject: [PATCH 04/13] Document website quality gates --- QUALITY_GATES.md | 88 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 QUALITY_GATES.md diff --git a/QUALITY_GATES.md b/QUALITY_GATES.md new file mode 100644 index 0000000..e9c780f --- /dev/null +++ b/QUALITY_GATES.md @@ -0,0 +1,88 @@ +# Website quality and release gates + +WEB-04 turns accessibility, performance, SEO, analytics privacy and release +expectations into repeatable checks for the supported GitHub Pages artifact. + +## Required checks + +A website change is eligible for deployment only when a clean checkout passes: + +```bash +npm ci +npm run lint +npm test +``` + +`npm test` builds the same static `out/` artifact used by production and then +runs rendered-output and site-quality tests. + +## Accessibility baseline + +The release tests require: + +- Persian `lang=fa` and RTL document direction; +- one primary `h1` on the home page; +- a keyboard skip link; +- explicit `:focus-visible` treatment; +- reduced motion when `prefers-reduced-motion: reduce` is active; +- an `alt` attribute on every rendered image. + +These checks are a regression floor, not a claim of full WCAG conformance. +Material UI changes still require keyboard, responsive and contrast review. + +## Performance budgets + +The exported home artifact currently has deliberately conservative regression +ceilings: + +| Resource | Raw-byte ceiling | +| --- | ---: | +| `out/index.html` | 200 KB | +| `out/_next/static/**/*.js` | 2 MB | +| `out/_next/static/**/*.css` | 500 KB | + +A budget increase requires evidence explaining the user-visible tradeoff; do not +raise thresholds merely to make CI green. Runtime Core Web Vitals should be +measured against the deployed site when GitHub Pages is healthy. + +## SEO and discovery + +The static release must contain: + +- canonical URL `https://corelinkplatform.ir`; +- title, description and Open Graph metadata; +- index/follow robots metadata; +- `/robots.txt` pointing at `/sitemap.xml`; +- a canonical home entry in `/sitemap.xml`. + +Search visibility is not inferred from build success; indexing remains an +external crawler outcome. + +## Analytics privacy + +No analytics tracker is shipped today. CI rejects common tracker markers +(Google Analytics/Tag Manager, Plausible, Umami and PostHog) until a change +explicitly addresses: + +1. purpose and data minimization; +2. owner and retention; +3. consent/legal basis where applicable; +4. tenant/device/customer identifier handling; +5. privacy documentation and opt-out behavior; +6. verification against the deployed artifact. + +Never send access tokens, raw device credentials, private tenant data or +provider secrets to analytics. + +## Release evidence + +For every accepted website release retain: + +- PR and exact merged `main` SHA; +- successful Website CI for that SHA; +- successful GitHub Pages deployment for that SHA; +- deployed-site check confirming the expected public claims and discovery files; +- rollback commit/deployment evidence when rollback is required. + +GitHub service outages are external blockers: a successful local build or +uploaded artifact does not replace the final deployed-site evidence. From d492a26543f74abd5fe24464883ca8dcee998b39 Mon Sep 17 00:00:00 2001 From: Javid Date: Thu, 6 Aug 2026 19:19:05 +0200 Subject: [PATCH 05/13] Add SEO and keyboard navigation metadata --- app/layout.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/layout.tsx b/app/layout.tsx index f1c2783..4a16059 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -9,6 +9,8 @@ export const metadata: Metadata = { template: "%s | CoreLink Platform", }, description: "CoreLink دستگاه‌ها و پروتکل‌های مختلف را به یک مدل داده و API پایدار متصل می‌کند تا ساخت محصولات ناوگان، IoT و White-label سریع‌تر و قابل‌گسترش‌تر شود.", + alternates: { canonical: "/" }, + robots: { index: true, follow: true }, other: { "codex-preview": "development", "theme-color": "#0B1F3A", @@ -39,7 +41,10 @@ export default function RootLayout({ }>) { return ( - {children} + + پرش به محتوای اصلی + {children} + ); } From 9823f0e9f734b12193987597b31b7973019d6a4b Mon Sep 17 00:00:00 2001 From: Javid Date: Thu, 6 Aug 2026 19:19:16 +0200 Subject: [PATCH 06/13] Add keyboard and reduced-motion styles --- app/globals.css | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/app/globals.css b/app/globals.css index 7c0700d..b56a0b1 100644 --- a/app/globals.css +++ b/app/globals.css @@ -3029,3 +3029,40 @@ main[id] { line-height: 1.9; } } + + +/* Release accessibility baseline */ +.skip-link { + position: fixed; + inset-block-start: 12px; + inset-inline-start: 12px; + z-index: 1000; + padding: 10px 14px; + border-radius: 8px; + background: #ffffff; + color: #061529; + font-weight: 700; + transform: translateY(-160%); + transition: transform 0.15s ease; +} +.skip-link:focus { + transform: translateY(0); +} +:focus-visible { + outline: 3px solid #00c2ff; + outline-offset: 3px; +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} From 4333eb0e47f0daa0e1be8612da86bbef16172224 Mon Sep 17 00:00:00 2001 From: Javid Date: Thu, 6 Aug 2026 19:19:24 +0200 Subject: [PATCH 07/13] Run website quality gates in CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0c5c99b..c3c31c2 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "dev": "next dev", "build": "next build", - "test": "npm run build && node --test tests/rendered-html.test.mjs", + "test": "npm run build && node --test tests/rendered-html.test.mjs tests/site-quality.test.mjs", "test:sites": "npm run build:sites && node --test tests/sites-worker.test.mjs", "validate:artifact": "bash scripts/validate-artifact.sh", "lint": "bash scripts/sites-env.sh -- eslint . --ignore-pattern dist --ignore-pattern .next", From d3b4f77ffa96ed7e17c4d1783d490d61c82f53f4 Mon Sep 17 00:00:00 2001 From: Javid Date: Thu, 6 Aug 2026 19:19:34 +0200 Subject: [PATCH 08/13] Link website release quality gates --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ad7acd..bf5f0f2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ Official website for [CoreLink Platform](https://corelinkplatform.ir). - Deployment: `.github/workflows/deploy-pages.yml` on `main`. - Production has no application server or database dependency. -See [DEPLOYMENT.md](DEPLOYMENT.md) for release evidence and rollback. +See [DEPLOYMENT.md](DEPLOYMENT.md) for release evidence and rollback, and +[QUALITY_GATES.md](QUALITY_GATES.md) for accessibility, performance, SEO and +analytics-privacy acceptance. ## Local development @@ -20,6 +22,7 @@ See [DEPLOYMENT.md](DEPLOYMENT.md) for release evidence and rollback. npm ci npm run dev npm run build +npm test ``` ## Internal preview tooling From 5e98c4eca9802e97ff7173e01e8cf48f65269943 Mon Sep 17 00:00:00 2001 From: Javid Date: Thu, 6 Aug 2026 19:19:44 +0200 Subject: [PATCH 09/13] Require quality gates before website release --- DEPLOYMENT.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index bdce216..9dcdf52 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -12,7 +12,8 @@ From a clean checkout with Node 22.13 or newer: ```bash npm ci -npm run build +npm run lint +npm test test -d out ``` @@ -27,8 +28,8 @@ production deployment/rollback procedure. Before accepting a website release: -1. build the exact commit with `npm ci && npm run build`; -2. run repository checks; +1. build and test the exact commit with `npm ci && npm run lint && npm test`; +2. verify the accessibility, performance, SEO and privacy gates in [QUALITY_GATES.md](QUALITY_GATES.md); 3. review public maturity/device/SDK claims against repository/runtime evidence; 4. merge through review; 5. retain the successful GitHub Pages run URL and deployed commit. From 00aee01ea22024aab4dab36d9979e10a28475a0b Mon Sep 17 00:00:00 2001 From: Javid Date: Thu, 6 Aug 2026 19:19:53 +0200 Subject: [PATCH 10/13] Fix quality test path resolution --- tests/site-quality.test.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/site-quality.test.mjs b/tests/site-quality.test.mjs index c2c3539..30cf8c1 100644 --- a/tests/site-quality.test.mjs +++ b/tests/site-quality.test.mjs @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { readdir, readFile, stat } from "node:fs/promises"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import test from "node:test"; const root = new URL("../", import.meta.url); @@ -57,7 +58,7 @@ test("production HTML has no unreviewed analytics tracker", async () => { test("static artifact stays inside release performance budgets", async () => { const htmlBytes = (await stat(new URL("index.html", out))).size; - const staticDir = path.fileURLToPath(new URL("_next/static/", out)); + const staticDir = fileURLToPath(new URL("_next/static/", out)); const files = await walk(staticDir); let jsBytes = 0; From 9bf8820f04b1047e320df9f8e9b42c9a66e9c074 Mon Sep 17 00:00:00 2001 From: Javid Date: Thu, 6 Aug 2026 19:22:11 +0200 Subject: [PATCH 11/13] Make metadata routes static-export compatible --- app/robots.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/robots.ts b/app/robots.ts index 736dd32..9d074fa 100644 --- a/app/robots.ts +++ b/app/robots.ts @@ -1,5 +1,7 @@ import type { MetadataRoute } from "next"; +export const dynamic = "force-static"; + export default function robots(): MetadataRoute.Robots { return { rules: { From f5b1b4e98afc594d84871c15eba5193e2b322995 Mon Sep 17 00:00:00 2001 From: Javid Date: Thu, 6 Aug 2026 19:22:13 +0200 Subject: [PATCH 12/13] Make metadata routes static-export compatible --- app/sitemap.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/sitemap.ts b/app/sitemap.ts index c342c70..04c4fb9 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,5 +1,7 @@ import type { MetadataRoute } from "next"; +export const dynamic = "force-static"; + export default function sitemap(): MetadataRoute.Sitemap { return [ { From d2c22b1223cefd89f84ca4d5fef9bd75e9a16ee6 Mon Sep 17 00:00:00 2001 From: Javid Date: Thu, 6 Aug 2026 19:22:14 +0200 Subject: [PATCH 13/13] Keep quality test lint-clean --- tests/site-quality.test.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/site-quality.test.mjs b/tests/site-quality.test.mjs index 30cf8c1..6b50d23 100644 --- a/tests/site-quality.test.mjs +++ b/tests/site-quality.test.mjs @@ -4,7 +4,6 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import test from "node:test"; -const root = new URL("../", import.meta.url); const out = new URL("../out/", import.meta.url); async function walk(directory) {