From a01628421eff5e8f16a088d1ba237c9c01d61d3b Mon Sep 17 00:00:00 2001 From: Memet Date: Fri, 11 Sep 2026 01:37:15 +0300 Subject: [PATCH 1/4] fix(web): derive sitemap from Nextra page map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hand-kept ROUTES array had drifted several sections behind the content tree — auth, oauth2, oidc, paseto and their sub-pages were absent from the sitemap. Walk getPageMap() instead so every routable page is included and the sitemap stays correct as pages are added. Priority now scales with route depth. --- web/app/sitemap.js | 87 +++++++++++++++++----------------------------- 1 file changed, 32 insertions(+), 55 deletions(-) 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), })); } From f551858b3e1463d020a2a32099d52d2dae24fe56 Mon Sep 17 00:00:00 2001 From: Memet Date: Fri, 11 Sep 2026 01:39:24 +0300 Subject: [PATCH 2/4] docs(web): add per-page meta descriptions to package overviews Only 14 of 160 pages carried a description; every package overview fell back to the site-wide default, giving 20+ pages the same SERP snippet and OG description. Add a distinct, RFC-anchored description to each of the 20 package overview pages and the compliance mapping. --- web/content/apikey/index.mdx | 1 + web/content/auth/index.mdx | 1 + web/content/challenge/index.mdx | 1 + web/content/compliance.mdx | 1 + web/content/crypto/index.mdx | 1 + web/content/jwe/index.mdx | 1 + web/content/jwk/index.mdx | 1 + web/content/jwks/index.mdx | 1 + web/content/jws/index.mdx | 1 + web/content/jwt/index.mdx | 1 + web/content/magic-link/index.mdx | 1 + web/content/oauth2/index.mdx | 1 + web/content/oidc/index.mdx | 1 + web/content/opaque/index.mdx | 1 + web/content/otp/index.mdx | 1 + web/content/paseto/index.mdx | 1 + web/content/passkey/index.mdx | 1 + web/content/password/index.mdx | 1 + web/content/security/index.mdx | 1 + web/content/session/index.mdx | 1 + web/content/ua/index.mdx | 1 + 21 files changed, 21 insertions(+) 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 --- From 77d3a10696190c3d622f5dcc31044abdca0cc86c Mon Sep 17 00:00:00 2001 From: Memet Date: Fri, 11 Sep 2026 01:44:32 +0300 Subject: [PATCH 3/4] feat(web): add dynamic OG image endpoint and apple-touch-icon /og renders a 1200x630 branded share card from title/desc query params (ImageResponse); pages seed it per-page so each share preview is specific. Social shares previously rendered with no image despite the summary_large_image Twitter card. Also add a generated apple-icon. --- web/app/apple-icon.js | 29 +++++++++++++++++ web/app/og/route.js | 74 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 web/app/apple-icon.js create mode 100644 web/app/og/route.js 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, + ); +} From 11e9164e960331087912ea6779702b111660046a Mon Sep 17 00:00:00 2001 From: Memet Date: Fri, 11 Sep 2026 01:44:32 +0300 Subject: [PATCH 4/4] fix(web): resolve canonical URL and OG card per page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The root layout's alternates.canonical: '/' was inherited by every page, so all doc pages emitted a canonical pointing at the homepage — telling search engines to treat them as duplicates. Resolve canonical (and og:url) to each page's own path in generateMetadata, and wire the per-page /og share image while re-declaring the OG siteName/type/locale the page-level override would otherwise drop. --- web/app/[[...mdxPath]]/page.jsx | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) 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;