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
7 changes: 4 additions & 3 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -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.
Expand Down
88 changes: 88 additions & 0 deletions QUALITY_GATES.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ 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

```bash
npm ci
npm run dev
npm run build
npm test
```

## Internal preview tooling
Expand Down
37 changes: 37 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
7 changes: 6 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -39,7 +41,10 @@ export default function RootLayout({
}>) {
return (
<html lang="fa" dir="rtl">
<body>{children}</body>
<body>
<a className="skip-link" href="#top">پرش به محتوای اصلی</a>
{children}
</body>
</html>
);
}
13 changes: 13 additions & 0 deletions app/robots.ts
Original file line number Diff line number Diff line change
@@ -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",
};
}
14 changes: 14 additions & 0 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -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,
},
];
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
74 changes: 74 additions & 0 deletions tests/site-quality.test.mjs
Original file line number Diff line number Diff line change
@@ -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, /<html\b(?=[^>]*\blang=["']fa["'])(?=[^>]*\bdir=["']rtl["'])[^>]*>/i);
assert.equal((html.match(/<h1\b/gi) ?? []).length, 1, "home page must have exactly one h1");
assert.match(html, /href=["']#top["'][^>]*>\s*پرش به محتوای اصلی/i);

for (const image of html.match(/<img\b[^>]*>/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, /<meta(?=[^>]*\bname=["']description["'])(?=[^>]*\bcontent=["'][^"']{40,}["'])[^>]*>/i);
assert.match(html, /<link(?=[^>]*\brel=["']canonical["'])(?=[^>]*\bhref=["']https:\/\/corelinkplatform\.ir\/?["'])[^>]*>/i);
assert.match(html, /<meta(?=[^>]*\bproperty=["']og:title["'])[^>]*>/i);
assert.match(html, /<meta(?=[^>]*\bname=["']robots["'])(?=[^>]*\bcontent=["'][^"']*index[^"']*follow[^"']*["'])[^>]*>/i);
assert.match(robots, /Sitemap:\s*https:\/\/corelinkplatform\.ir\/sitemap\.xml/i);
assert.match(sitemap, /<loc>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 = 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`);
});
Loading