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. 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. 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 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; + } +} 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} + ); } diff --git a/app/robots.ts b/app/robots.ts new file mode 100644 index 0000000..9d074fa --- /dev/null +++ b/app/robots.ts @@ -0,0 +1,13 @@ +import type { MetadataRoute } from "next"; + +export const dynamic = "force-static"; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: "*", + allow: "/", + }, + sitemap: "https://corelinkplatform.ir/sitemap.xml", + }; +} diff --git a/app/sitemap.ts b/app/sitemap.ts new file mode 100644 index 0000000..04c4fb9 --- /dev/null +++ b/app/sitemap.ts @@ -0,0 +1,14 @@ +import type { MetadataRoute } from "next"; + +export const dynamic = "force-static"; + +export default function sitemap(): MetadataRoute.Sitemap { + return [ + { + url: "https://corelinkplatform.ir", + lastModified: new Date("2026-08-06"), + changeFrequency: "weekly", + priority: 1, + }, + ]; +} 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", diff --git a/tests/site-quality.test.mjs b/tests/site-quality.test.mjs new file mode 100644 index 0000000..6b50d23 --- /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 { fileURLToPath } from "node:url"; +import test from "node:test"; + +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(/