diff --git a/web/app/[[...mdxPath]]/page.jsx b/web/app/[[...mdxPath]]/page.jsx
index 56004ad8..be6cfe31 100644
--- a/web/app/[[...mdxPath]]/page.jsx
+++ b/web/app/[[...mdxPath]]/page.jsx
@@ -6,7 +6,35 @@ export const generateStaticParams = generateStaticParamsFor('mdxPath');
export async function generateMetadata(props) {
const params = await props.params;
const { metadata } = await importPage(params.mdxPath);
- return metadata;
+
+ // Per-page canonical. The root layout declares alternates.canonical: '/',
+ // which every page would otherwise inherit — pointing all sub-pages'
+ // canonical at the homepage and telling search engines to treat them as
+ // duplicates. Resolve it to this page's own path instead.
+ const pathname = Array.isArray(params.mdxPath) && params.mdxPath.length ? `/${params.mdxPath.join('/')}` : '/';
+
+ // Point the page's OG/Twitter card at the dynamic /og endpoint, seeded with
+ // this page's own title and description so every share card is specific.
+ const title = typeof metadata?.title === 'string' ? metadata.title : '@exortek/auth';
+ const heading = title.replace(/\s+[—-]\s+Overview$/i, '').trim();
+ const desc = typeof metadata?.description === 'string' ? metadata.description : '';
+ const ogUrl = `/og?title=${encodeURIComponent(heading)}${desc ? `&desc=${encodeURIComponent(desc)}` : ''}`;
+
+ return {
+ ...metadata,
+ alternates: { ...metadata?.alternates, canonical: pathname },
+ // Re-declare the stable OG fields: setting openGraph here replaces the
+ // layout's object wholesale, so siteName/type/locale would be lost.
+ openGraph: {
+ type: 'website',
+ siteName: '@exortek/auth',
+ locale: 'en_US',
+ ...metadata?.openGraph,
+ url: pathname,
+ images: [{ url: ogUrl, width: 1200, height: 630 }],
+ },
+ twitter: { card: 'summary_large_image', ...metadata?.twitter, images: [ogUrl] },
+ };
}
const Wrapper = getMDXComponents().wrapper;
diff --git a/web/app/apple-icon.js b/web/app/apple-icon.js
new file mode 100644
index 00000000..2b568ba1
--- /dev/null
+++ b/web/app/apple-icon.js
@@ -0,0 +1,29 @@
+import { ImageResponse } from 'next/og';
+
+// Apple touch icon. Generated (rather than a static file) so it stays in sync
+// with the brand mark in icon.svg; the apple-icon convention wants a raster
+// type, so we render one at the standard 180×180.
+export const size = { width: 180, height: 180 };
+export const contentType = 'image/png';
+
+export default function appleIcon() {
+ return new ImageResponse(
+
+ @
+
,
+ size,
+ );
+}
diff --git a/web/app/og/route.js b/web/app/og/route.js
new file mode 100644
index 00000000..69f8ec20
--- /dev/null
+++ b/web/app/og/route.js
@@ -0,0 +1,74 @@
+import { ImageResponse } from 'next/og';
+
+// Dynamic Open Graph image endpoint. Pages point their openGraph/twitter image
+// at /og?title=…&desc=… (see [[...mdxPath]]/page.jsx generateMetadata) so each
+// share card is specific to the page. A route handler is used rather than a
+// file-based opengraph-image because the optional catch-all segment cannot
+// carry a static metadata child (the catch-all must be the last segment).
+export const contentType = 'image/png';
+
+const SIZE = { width: 1200, height: 630 };
+const BG = '#17181c';
+const CREAM = '#e4dcc7';
+const GREEN = '#12b76a';
+const MUTED = '#8a8f99';
+
+export function GET(request) {
+ const { searchParams } = new URL(request.url);
+ const heading = (searchParams.get('title') || '@exortek/auth').slice(0, 80);
+ const rawSub = searchParams.get('desc') || 'Authentication toolkit for Node.js';
+ const sub = rawSub.length > 150 ? `${rawSub.slice(0, 147)}…` : rawSub;
+
+ return new ImageResponse(
+
+
+ @exortek/
+ auth
+
+
+
+
+ {heading}
+
+
+ {sub}
+
+
+
+
+ ●
+ built on node:crypto · Node.js 22+ · zero-dependency
+
+
,
+ SIZE,
+ );
+}
diff --git a/web/app/sitemap.js b/web/app/sitemap.js
index 9abed7ca..6780962c 100644
--- a/web/app/sitemap.js
+++ b/web/app/sitemap.js
@@ -1,63 +1,40 @@
+import { getPageMap } from 'nextra/page-map';
+
const SITE_URL = 'https://auth.memet.dev';
-// Top-level routes plus the multi-page package sections. Kept explicit so the
-// sitemap stays correct regardless of how Nextra resolves the page map.
-const ROUTES = [
- '',
- 'guides',
- 'guides/password-login',
- 'guides/jwt-access-refresh',
- 'guides/jwks-verify',
- 'guides/two-factor',
- 'guides/passkey',
- 'guides/multi-step',
- 'guides/magic-link',
- 'guides/api-keys',
- 'guides/opaque-tokens',
- 'guides/nested-jwt',
- 'guides/security-hardening',
- 'comparison',
- 'compliance',
- 'crypto',
- 'password',
- 'otp',
- 'challenge',
- 'jwk',
- 'jws',
- 'jws/sign',
- 'jws/verify',
- 'jws/decode',
- 'jws/json',
- 'jws/errors',
- 'jwt',
- 'jwt/sign',
- 'jwt/verify',
- 'jwt/token-pair',
- 'jwt/stores',
- 'jwt/errors',
- 'jwe',
- 'jwe/encrypt',
- 'jwe/decrypt',
- 'jwe/decode',
- 'jwe/json',
- 'jwe/algorithms',
- 'jwe/errors',
- 'jwks',
- 'session',
- 'security',
- 'ua',
- 'apikey',
- 'magic-link',
- 'passkey',
- 'opaque',
-];
+// Walk the Nextra page map and collect every routable page. Deriving the
+// sitemap from the page map (rather than a hand-kept list) keeps it correct as
+// packages and pages are added — the previous static ROUTES array had already
+// drifted several sections behind the content tree.
+const collectRoutes = (nodes, out = new Set()) => {
+ for (const node of nodes ?? []) {
+ if (typeof node.route === 'string' && !node.route.includes('[')) {
+ out.add(node.route);
+ }
+ if (Array.isArray(node.children)) {
+ collectRoutes(node.children, out);
+ }
+ }
+ return out;
+};
+
+// Deeper pages get a lower priority; the home page ranks highest, package roots
+// above their sub-pages.
+const priorityFor = route => {
+ if (route === '/') return 1;
+ const depth = route.split('/').filter(Boolean).length;
+ return depth <= 1 ? 0.8 : 0.6;
+};
-export default function sitemap() {
+export default async function sitemap() {
+ const pageMap = await getPageMap();
+ const routes = [...collectRoutes(pageMap)].sort();
const lastModified = new Date();
- return ROUTES.map(route => ({
- url: route ? `${SITE_URL}/${route}` : SITE_URL,
+
+ return routes.map(route => ({
+ url: route === '/' ? SITE_URL : `${SITE_URL}${route}`,
lastModified,
changeFrequency: 'weekly',
- priority: route === '' ? 1 : 0.7,
+ priority: priorityFor(route),
}));
}
diff --git a/web/content/apikey/index.mdx b/web/content/apikey/index.mdx
index 0eed063d..72400dcc 100644
--- a/web/content/apikey/index.mdx
+++ b/web/content/apikey/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/apikey — Overview'
+description: 'Prefixed API keys (sk_live_id_secret) with HMAC-SHA256 hash-at-rest storage, scopes and middleware. Built on node:crypto.'
sidebarTitle: Overview
---
diff --git a/web/content/auth/index.mdx b/web/content/auth/index.mdx
index 08192e19..b166d8ea 100644
--- a/web/content/auth/index.mdx
+++ b/web/content/auth/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/auth — Overview'
+description: 'Umbrella package re-exporting all 20 @exortek authentication packages — install one dependency, get the whole toolkit.'
sidebarTitle: Overview
---
diff --git a/web/content/challenge/index.mdx b/web/content/challenge/index.mdx
index e7a79316..d8d37bc8 100644
--- a/web/content/challenge/index.mdx
+++ b/web/content/challenge/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/challenge — Overview'
+description: 'HMAC-signed multi-step flow tokens binding userId, method and step, with opt-in single-use — for MFA and step-up auth flows.'
sidebarTitle: Overview
---
diff --git a/web/content/compliance.mdx b/web/content/compliance.mdx
index ef0e325c..0e20a03f 100644
--- a/web/content/compliance.mdx
+++ b/web/content/compliance.mdx
@@ -1,5 +1,6 @@
---
title: 'Compliance mapping — NIST / OWASP / PCI-DSS / FIPS'
+description: 'How @exortek/auth packages map onto NIST SP 800-63B, OWASP ASVS 4.0.3, PCI-DSS 4.0 and FIPS — which package satisfies which control.'
sidebarTitle: Compliance
---
diff --git a/web/content/crypto/index.mdx b/web/content/crypto/index.mdx
index 5ec6f307..111a3cbb 100644
--- a/web/content/crypto/index.mdx
+++ b/web/content/crypto/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/crypto — Overview'
+description: 'Zero-dependency crypto primitives on node:crypto — hashing, HMAC, KDFs, AEAD ciphers, signatures, sealed tokens, encoding and CSPRNG.'
sidebarTitle: Overview
---
diff --git a/web/content/jwe/index.mdx b/web/content/jwe/index.mdx
index 44faaf2e..51828a7d 100644
--- a/web/content/jwe/index.mdx
+++ b/web/content/jwe/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/jwe — Overview'
+description: 'Encrypt and decrypt JWE with RSA-OAEP, ECDH-ES, A256KW and dir key management per RFC 7516. Zero-dependency, built on node:crypto.'
sidebarTitle: Overview
---
diff --git a/web/content/jwk/index.mdx b/web/content/jwk/index.mdx
index b8463672..d4efab7e 100644
--- a/web/content/jwk/index.mdx
+++ b/web/content/jwk/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/jwk — Overview'
+description: 'Generate, import, export and thumbprint JSON Web Keys per RFC 7517, 7638, 8037 and 9278. Zero-dependency, built on node:crypto.'
sidebarTitle: Overview
---
diff --git a/web/content/jwks/index.mdx b/web/content/jwks/index.mdx
index 47711bb8..e44df653 100644
--- a/web/content/jwks/index.mdx
+++ b/web/content/jwks/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/jwks — Overview'
+description: 'Fetch, cache and rotate JWKS by kid from a remote URI per RFC 7517 §5 and OpenID Connect Discovery. Verify third-party JWTs.'
sidebarTitle: Overview
---
diff --git a/web/content/jws/index.mdx b/web/content/jws/index.mdx
index e2ec3f3d..f3fdb5cf 100644
--- a/web/content/jws/index.mdx
+++ b/web/content/jws/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/jws — Overview'
+description: 'Sign and verify JWS with detached payloads, b64:false and JSON serialization per RFC 7515 and 7797. Algorithm allowlists mandatory.'
sidebarTitle: Overview
---
diff --git a/web/content/jwt/index.mdx b/web/content/jwt/index.mdx
index 2a80aa63..daa9ba09 100644
--- a/web/content/jwt/index.mdx
+++ b/web/content/jwt/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/jwt — Overview'
+description: 'Sign and verify JWTs with mandatory algorithm allowlists and claims validation per RFC 7519. alg:none refused by construction.'
sidebarTitle: Overview
---
diff --git a/web/content/magic-link/index.mdx b/web/content/magic-link/index.mdx
index 8e489793..1da17c4c 100644
--- a/web/content/magic-link/index.mdx
+++ b/web/content/magic-link/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/magic-link — Overview'
+description: 'Passwordless email-link auth with signed, short-lived, single-use tokens. Zero-dependency, built on node:crypto.'
sidebarTitle: Overview
---
diff --git a/web/content/oauth2/index.mdx b/web/content/oauth2/index.mdx
index 2c1c014e..4a2f0672 100644
--- a/web/content/oauth2/index.mdx
+++ b/web/content/oauth2/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/oauth2 — Overview'
+description: 'OAuth 2.1 relying-party flow, provider presets and a full authorization server — DPoP, PAR, JAR/JARM, device flow and token exchange.'
sidebarTitle: Overview
---
diff --git a/web/content/oidc/index.mdx b/web/content/oidc/index.mdx
index 146313d8..47b44777 100644
--- a/web/content/oidc/index.mdx
+++ b/web/content/oidc/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/oidc — Overview'
+description: 'OpenID Connect Core 1.0 on @exortek/oauth2 — discovery-first RP plus OP add-ons for UserInfo, JWKS, logout and session management.'
sidebarTitle: Overview
---
diff --git a/web/content/opaque/index.mdx b/web/content/opaque/index.mdx
index 27ef2b41..43d6626c 100644
--- a/web/content/opaque/index.mdx
+++ b/web/content/opaque/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/opaque — Overview'
+description: 'Opaque reference tokens with RFC 7662 introspection and RFC 7009 revocation. Store-backed, built on node:crypto.'
sidebarTitle: Overview
---
diff --git a/web/content/otp/index.mdx b/web/content/otp/index.mdx
index bf58ecba..995f3aea 100644
--- a/web/content/otp/index.mdx
+++ b/web/content/otp/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/otp — Overview'
+description: 'RFC 4226 HOTP and RFC 6238 TOTP with backup codes and otpauth:// provisioning URIs for authenticator apps. Built on node:crypto.'
sidebarTitle: Overview
---
diff --git a/web/content/paseto/index.mdx b/web/content/paseto/index.mdx
index 8ee00908..f38c20fd 100644
--- a/web/content/paseto/index.mdx
+++ b/web/content/paseto/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/paseto — Overview'
+description: 'PASETO v4 local (XChaCha20 + BLAKE2b) and public (Ed25519) tokens with no alg header. Zero-dependency, built on node:crypto.'
sidebarTitle: Overview
---
diff --git a/web/content/passkey/index.mdx b/web/content/passkey/index.mdx
index 1a663538..79a951e9 100644
--- a/web/content/passkey/index.mdx
+++ b/web/content/passkey/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/passkey — Overview'
+description: 'WebAuthn Level 3 / FIDO2 server verification with all seven attestation formats, MDS3 metadata and AAGUID lookup.'
sidebarTitle: Overview
---
diff --git a/web/content/password/index.mdx b/web/content/password/index.mdx
index 41c5a6e2..19060cb8 100644
--- a/web/content/password/index.mdx
+++ b/web/content/password/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/password — Overview'
+description: 'Argon2id, scrypt, bcrypt and PBKDF2 password hashing with strength checks, pepper, history and HIBP breach lookup. NIST SP 800-63B.'
sidebarTitle: Overview
---
diff --git a/web/content/security/index.mdx b/web/content/security/index.mdx
index 84883334..a3f5d03c 100644
--- a/web/content/security/index.mdx
+++ b/web/content/security/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/security — Overview'
+description: 'CSRF, rate limiting, helmet-style headers, CORS and safe redirects — defensive HTTP helpers per OWASP ASVS V13/V14.'
sidebarTitle: Overview
---
diff --git a/web/content/session/index.mdx b/web/content/session/index.mdx
index 04c3c0d5..325f33e4 100644
--- a/web/content/session/index.mdx
+++ b/web/content/session/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/session — Overview'
+description: 'Sealed-cookie sessions with rotation, revocation, sudo mode and Redis pub/sub. OWASP ASVS V3, RFC 6265.'
sidebarTitle: Overview
---
diff --git a/web/content/ua/index.mdx b/web/content/ua/index.mdx
index 8e240031..0d41ff73 100644
--- a/web/content/ua/index.mdx
+++ b/web/content/ua/index.mdx
@@ -1,5 +1,6 @@
---
title: '@exortek/ua — Overview'
+description: 'User-Agent parsing, device, browser and bot detection, Client Hints and fingerprinting for Node.js. Zero-dependency.'
sidebarTitle: Overview
---