@pyreon/zero 0.12.3 → 0.12.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/lib/favicon.js +151 -6
- package/lib/favicon.js.map +1 -1
- package/lib/index.js +343 -4039
- package/lib/index.js.map +1 -1
- package/lib/meta.js +22 -48
- package/lib/meta.js.map +1 -1
- package/lib/server.js +1534 -0
- package/lib/server.js.map +1 -0
- package/lib/theme.js +5 -2
- package/lib/theme.js.map +1 -1
- package/lib/types/favicon.d.ts +17 -0
- package/lib/types/favicon.d.ts.map +1 -1
- package/lib/types/index.d.ts +189 -1540
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/meta.d.ts +11 -46
- package/lib/types/meta.d.ts.map +1 -1
- package/lib/types/server.d.ts +455 -0
- package/lib/types/server.d.ts.map +1 -0
- package/lib/types/theme.d.ts +1 -1
- package/lib/types/theme.d.ts.map +1 -1
- package/package.json +15 -10
- package/src/favicon.ts +163 -33
- package/src/index.ts +47 -182
- package/src/meta.tsx +37 -3
- package/src/server.ts +70 -0
- package/src/theme.tsx +10 -3
- package/lib/fs-router-Dil4IKZR.js +0 -290
- package/lib/fs-router-Dil4IKZR.js.map +0 -1
package/lib/meta.js
CHANGED
|
@@ -2,17 +2,30 @@ import { useHead } from "@pyreon/head";
|
|
|
2
2
|
import { createContext } from "@pyreon/core";
|
|
3
3
|
import { signal } from "@pyreon/reactivity";
|
|
4
4
|
|
|
5
|
-
//#region src/
|
|
5
|
+
//#region src/i18n-routing.ts
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* Returns
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```ts
|
|
12
|
-
* const links = faviconLinks("de", { source: "./icon.svg", locales: { de: { source: "./icon-de.svg" } } })
|
|
13
|
-
* // → [{ rel: "icon", type: "image/svg+xml", href: "/de/favicon.svg" }, ...]
|
|
14
|
-
* ```
|
|
7
|
+
* Extract locale from a URL path.
|
|
8
|
+
* Returns { locale, pathWithoutLocale }.
|
|
15
9
|
*/
|
|
10
|
+
function extractLocaleFromPath(path, locales, defaultLocale) {
|
|
11
|
+
const segments = path.split("/").filter(Boolean);
|
|
12
|
+
const firstSegment = segments[0]?.toLowerCase();
|
|
13
|
+
if (firstSegment && locales.includes(firstSegment)) return {
|
|
14
|
+
locale: firstSegment,
|
|
15
|
+
pathWithoutLocale: "/" + segments.slice(1).join("/") || "/"
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
locale: defaultLocale,
|
|
19
|
+
pathWithoutLocale: path
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** @internal Context for the current locale. */
|
|
23
|
+
const LocaleCtx = createContext("en");
|
|
24
|
+
/** Current locale signal — set by the server middleware or client-side detection. */
|
|
25
|
+
const localeSignal = signal("en");
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/meta.tsx
|
|
16
29
|
function faviconLinks(locale, config) {
|
|
17
30
|
const hasLocaleOverride = locale && config.locales?.[locale];
|
|
18
31
|
const prefix = hasLocaleOverride ? `/${locale}` : "";
|
|
@@ -44,49 +57,10 @@ function faviconLinks(locale, config) {
|
|
|
44
57
|
});
|
|
45
58
|
return links;
|
|
46
59
|
}
|
|
47
|
-
|
|
48
|
-
//#endregion
|
|
49
|
-
//#region src/i18n-routing.ts
|
|
50
|
-
/**
|
|
51
|
-
* Extract locale from a URL path.
|
|
52
|
-
* Returns { locale, pathWithoutLocale }.
|
|
53
|
-
*/
|
|
54
|
-
function extractLocaleFromPath(path, locales, defaultLocale) {
|
|
55
|
-
const segments = path.split("/").filter(Boolean);
|
|
56
|
-
const firstSegment = segments[0]?.toLowerCase();
|
|
57
|
-
if (firstSegment && locales.includes(firstSegment)) return {
|
|
58
|
-
locale: firstSegment,
|
|
59
|
-
pathWithoutLocale: "/" + segments.slice(1).join("/") || "/"
|
|
60
|
-
};
|
|
61
|
-
return {
|
|
62
|
-
locale: defaultLocale,
|
|
63
|
-
pathWithoutLocale: path
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
/** @internal Context for the current locale. */
|
|
67
|
-
const LocaleCtx = createContext("en");
|
|
68
|
-
/** Current locale signal — set by the server middleware or client-side detection. */
|
|
69
|
-
const localeSignal = signal("en");
|
|
70
|
-
|
|
71
|
-
//#endregion
|
|
72
|
-
//#region src/og-image.ts
|
|
73
|
-
/**
|
|
74
|
-
* Compute the OG image path for a template and locale.
|
|
75
|
-
*
|
|
76
|
-
* @example
|
|
77
|
-
* ```ts
|
|
78
|
-
* ogImagePath("default", "de") // → "/og/default-de.png"
|
|
79
|
-
* ogImagePath("default") // → "/og/default.png"
|
|
80
|
-
* ogImagePath("hero", "en", "images") // → "/images/hero-en.png"
|
|
81
|
-
* ```
|
|
82
|
-
*/
|
|
83
60
|
function ogImagePath(templateName, locale, outDir = "og", format = "png") {
|
|
84
61
|
const ext = format === "jpeg" ? "jpg" : "png";
|
|
85
62
|
return `/${outDir}/${templateName}${locale ? `-${locale}` : ""}.${ext}`;
|
|
86
63
|
}
|
|
87
|
-
|
|
88
|
-
//#endregion
|
|
89
|
-
//#region src/meta.tsx
|
|
90
64
|
const resolveStr = (v) => typeof v === "function" ? v() : v;
|
|
91
65
|
/**
|
|
92
66
|
* Declarative meta component for SSR-compatible page metadata.
|
package/lib/meta.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meta.js","names":[],"sources":["../src/favicon.ts","../src/i18n-routing.ts","../src/og-image.ts","../src/meta.tsx"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport type { Plugin } from 'vite'\n\nlet sharpWarned = false\nfunction warnSharpMissing() {\n if (sharpWarned) return\n sharpWarned = true\n // oxlint-disable-next-line no-console\n console.warn(\n '\\n[zero:favicon] sharp not installed — favicons will not be generated. Install for full support: bun add -D sharp\\n',\n )\n}\n\n// ─── Favicon generation plugin ──────────────────────────────────────────────\n//\n// Generates all favicon formats from a single source file (SVG or PNG):\n// - favicon.ico (16x16 + 32x32 combined)\n// - favicon.svg (copied if source is SVG)\n// - apple-touch-icon.png (180x180)\n// - icon-192.png (for web manifest)\n// - icon-512.png (for web manifest)\n// - site.webmanifest\n//\n// Usage:\n// import { faviconPlugin } from \"@pyreon/zero\"\n// export default { plugins: [zero(), faviconPlugin({ source: \"./icon.svg\" })] }\n\nexport interface FaviconLocaleConfig {\n /** Locale-specific source icon (SVG or PNG). */\n source: string\n /** Optional dark mode variant for this locale. */\n darkSource?: string\n}\n\nexport interface FaviconPluginConfig {\n /** Path to the source icon (SVG or PNG, at least 512x512 for PNG). */\n source: string\n /** Theme color for web manifest. Default: \"#ffffff\" */\n themeColor?: string\n /** Background color for web manifest. Default: \"#ffffff\" */\n backgroundColor?: string\n /** App name for web manifest. Uses package.json name if not set. */\n name?: string\n /** Generate web manifest. Default: true */\n manifest?: boolean\n /**\n * Dark mode favicon (SVG only).\n * When provided, the SVG favicon uses prefers-color-scheme media query\n * to switch between light and dark variants.\n */\n darkSource?: string\n /**\n * Locale-specific icon overrides. Each key is a locale code,\n * value is a source icon (and optional dark variant).\n * Locales not in this map use the base `source`.\n *\n * Generated files are placed under `/{locale}/` prefix:\n * /de/favicon.svg, /de/favicon-32x32.png, etc.\n *\n * @example\n * ```ts\n * faviconPlugin({\n * source: \"./icon.svg\",\n * locales: {\n * de: { source: \"./icon-de.svg\" },\n * cs: { source: \"./icon-cs.svg\" },\n * },\n * })\n * ```\n */\n locales?: Record<string, FaviconLocaleConfig>\n}\n\ninterface FaviconSize {\n size: number\n name: string\n}\n\nconst SIZES: FaviconSize[] = [\n { size: 16, name: 'favicon-16x16.png' },\n { size: 32, name: 'favicon-32x32.png' },\n { size: 180, name: 'apple-touch-icon.png' },\n { size: 192, name: 'icon-192.png' },\n { size: 512, name: 'icon-512.png' },\n]\n\n/**\n * Favicon generation Vite plugin.\n *\n * Generates all required favicon formats at build time from a single source.\n * In dev mode, serves the source directly.\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { faviconPlugin } from \"@pyreon/zero\"\n *\n * export default {\n * plugins: [faviconPlugin({ source: \"./src/assets/icon.svg\" })],\n * }\n * ```\n */\nexport function faviconPlugin(config: FaviconPluginConfig): Plugin {\n const themeColor = config.themeColor ?? '#ffffff'\n const backgroundColor = config.backgroundColor ?? '#ffffff'\n const generateManifest = config.manifest !== false\n\n let root = ''\n let isBuild = false\n\n return {\n name: 'pyreon-zero-favicon',\n enforce: 'pre',\n\n configResolved(resolvedConfig) {\n root = resolvedConfig.root\n isBuild = resolvedConfig.command === 'build'\n },\n\n // Dev server: serve generated favicons on-the-fly\n configureServer(server) {\n const sourcePath = join(root, config.source)\n const devCache = new Map<string, Uint8Array>()\n\n server.middlewares.use(async (req, res, next) => {\n const url = req.url ?? ''\n\n // Resolve locale-specific source: /{locale}/favicon.svg → locale source\n const localeSource = resolveLocaleSource(url, config, root)\n\n // Serve source as favicon.svg in dev\n const svgUrl = localeSource ? localeSource.url : url\n const svgPath = localeSource ? localeSource.sourcePath : sourcePath\n const isSvgSource = localeSource ? localeSource.source.endsWith('.svg') : config.source.endsWith('.svg')\n\n if (svgUrl.endsWith('/favicon.svg') && isSvgSource) {\n try {\n const content = await readFile(svgPath, 'utf-8')\n res.setHeader('Content-Type', 'image/svg+xml')\n res.end(content)\n return\n } catch { /* fall through */ }\n }\n\n // Serve generated PNGs on-demand (supports /{locale}/favicon-32x32.png)\n const baseName = svgUrl.split('/').pop() ?? ''\n const sizeMatch = SIZES.find((s) => s.name === baseName)\n if (sizeMatch) {\n const cacheKey = `${svgPath}:${sizeMatch.size}`\n let png = devCache.get(cacheKey)\n if (!png) {\n const result = await resizeToPng(svgPath, sizeMatch.size)\n if (result) {\n png = result\n devCache.set(cacheKey, result)\n }\n }\n if (png) {\n res.setHeader('Content-Type', 'image/png')\n res.setHeader('Cache-Control', 'no-cache')\n res.end(Buffer.from(png))\n return\n }\n }\n\n // Serve generated ICO on-demand\n if (baseName === 'favicon.ico') {\n const cacheKey = `ico:${svgPath}`\n let ico: Uint8Array | undefined = devCache.get(cacheKey)\n if (!ico) {\n const result = await generateIco(svgPath)\n if (result) {\n ico = result\n devCache.set(cacheKey, result)\n }\n }\n if (ico) {\n res.setHeader('Content-Type', 'image/x-icon')\n res.setHeader('Cache-Control', 'no-cache')\n res.end(Buffer.from(ico))\n return\n }\n }\n\n // Serve manifest (supports /{locale}/site.webmanifest)\n if (baseName === 'site.webmanifest' && generateManifest) {\n const prefix = localeSource ? `/${localeSource.locale}` : ''\n const manifest = {\n name: config.name ?? 'App',\n short_name: config.name ?? 'App',\n icons: [\n { src: `${prefix}/icon-192.png`, sizes: '192x192', type: 'image/png' },\n { src: `${prefix}/icon-512.png`, sizes: '512x512', type: 'image/png' },\n ],\n theme_color: themeColor,\n background_color: backgroundColor,\n display: 'standalone',\n }\n res.setHeader('Content-Type', 'application/manifest+json')\n res.end(JSON.stringify(manifest, null, 2))\n return\n }\n\n next()\n })\n },\n\n // Inject favicon <link> tags into HTML\n transformIndexHtml() {\n const isSvg = config.source.endsWith('.svg')\n const tags: Array<{\n tag: string\n attrs: Record<string, string>\n injectTo: 'head'\n }> = []\n\n if (isSvg) {\n tags.push({\n tag: 'link',\n attrs: { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },\n injectTo: 'head',\n })\n }\n\n tags.push(\n {\n tag: 'link',\n attrs: { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' },\n injectTo: 'head',\n },\n {\n tag: 'link',\n attrs: { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png' },\n injectTo: 'head',\n },\n {\n tag: 'link',\n attrs: { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' },\n injectTo: 'head',\n },\n )\n\n if (generateManifest) {\n tags.push({\n tag: 'link',\n attrs: { rel: 'manifest', href: '/site.webmanifest' },\n injectTo: 'head',\n })\n }\n\n tags.push({\n tag: 'meta',\n attrs: { name: 'theme-color', content: themeColor },\n injectTo: 'head',\n })\n\n return tags\n },\n\n async generateBundle() {\n if (!isBuild) return\n\n // Generate favicons for the base (default) source\n await generateFaviconSet.call(this, root, config.source, config.darkSource, '', config, themeColor, backgroundColor, generateManifest)\n\n // Generate locale-specific favicon sets\n if (config.locales) {\n for (const [locale, localeConfig] of Object.entries(config.locales)) {\n await generateFaviconSet.call(this, root, localeConfig.source, localeConfig.darkSource, `${locale}/`, config, themeColor, backgroundColor, generateManifest)\n }\n }\n },\n }\n}\n\n/**\n * Wrap two SVGs into a single SVG that switches based on prefers-color-scheme.\n */\nfunction wrapSvgWithDarkMode(lightSvg: string, darkSvg: string): string {\n // Extract viewBox from light SVG\n const viewBoxMatch = lightSvg.match(/viewBox=\"([^\"]*)\"/)\n const viewBox = viewBoxMatch?.[1] ?? '0 0 32 32'\n\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"${viewBox}\">\n <style>\n :root { color-scheme: light dark; }\n @media (prefers-color-scheme: dark) { .light { display: none; } }\n @media (prefers-color-scheme: light), (prefers-color-scheme: no-preference) { .dark { display: none; } }\n </style>\n <g class=\"light\">${stripSvgWrapper(lightSvg)}</g>\n <g class=\"dark\">${stripSvgWrapper(darkSvg)}</g>\n</svg>`\n}\n\nfunction stripSvgWrapper(svg: string): string {\n return svg\n .replace(/<svg[^>]*>/, '')\n .replace(/<\\/svg>\\s*$/, '')\n .trim()\n}\n\n/**\n * Resolve the source path for a locale-prefixed favicon URL.\n * Returns null if the URL is not locale-prefixed or locale has no override.\n */\nfunction resolveLocaleSource(\n url: string,\n config: FaviconPluginConfig,\n rootDir: string,\n): { locale: string; url: string; source: string; sourcePath: string } | null {\n if (!config.locales) return null\n\n for (const [locale, localeConfig] of Object.entries(config.locales)) {\n const prefix = `/${locale}/`\n if (url.startsWith(prefix)) {\n return {\n locale,\n url,\n source: localeConfig.source,\n sourcePath: join(rootDir, localeConfig.source),\n }\n }\n }\n return null\n}\n\n/**\n * Generate a complete favicon set (SVG, PNGs, ICO, manifest) with a file prefix.\n * Called once for base (prefix = '') and once per locale (prefix = '{locale}/').\n */\nasync function generateFaviconSet(\n this: any,\n rootDir: string,\n source: string,\n darkSource: string | undefined,\n prefix: string,\n config: FaviconPluginConfig,\n themeColor: string,\n backgroundColor: string,\n generateManifest: boolean,\n): Promise<void> {\n const sourcePath = join(rootDir, source)\n if (!existsSync(sourcePath)) {\n // oxlint-disable-next-line no-console\n console.warn(`[zero:favicon] Source not found: ${sourcePath}`)\n return\n }\n\n const isSvg = source.endsWith('.svg')\n\n // Copy SVG as favicon.svg\n if (isSvg) {\n const svgContent = await readFile(sourcePath, 'utf-8')\n let finalSvg = svgContent\n\n if (darkSource) {\n const darkPath = join(rootDir, darkSource)\n if (existsSync(darkPath)) {\n const darkSvg = await readFile(darkPath, 'utf-8')\n finalSvg = wrapSvgWithDarkMode(svgContent, darkSvg)\n }\n }\n\n this.emitFile({\n type: 'asset',\n fileName: `${prefix}favicon.svg`,\n source: finalSvg,\n })\n }\n\n // Generate PNG sizes via sharp\n for (const { size, name } of SIZES) {\n const pngBuffer = await resizeToPng(sourcePath, size)\n if (pngBuffer) {\n this.emitFile({\n type: 'asset',\n fileName: `${prefix}${name}`,\n source: pngBuffer,\n })\n }\n }\n\n // Generate favicon.ico (16 + 32)\n const ico = await generateIco(sourcePath)\n if (ico) {\n this.emitFile({\n type: 'asset',\n fileName: `${prefix}favicon.ico`,\n source: ico,\n })\n }\n\n // Generate web manifest\n if (generateManifest) {\n const manifestPrefix = prefix ? `/${prefix.slice(0, -1)}` : ''\n const manifest = {\n name: config.name ?? 'App',\n short_name: config.name ?? 'App',\n icons: [\n { src: `${manifestPrefix}/icon-192.png`, sizes: '192x192', type: 'image/png' },\n { src: `${manifestPrefix}/icon-512.png`, sizes: '512x512', type: 'image/png' },\n ],\n theme_color: themeColor,\n background_color: backgroundColor,\n display: 'standalone',\n }\n\n this.emitFile({\n type: 'asset',\n fileName: `${prefix}site.webmanifest`,\n source: JSON.stringify(manifest, null, 2),\n })\n }\n}\n\n/**\n * Get favicon link tags for a specific locale.\n * Returns link objects suitable for `useHead()` or direct HTML injection.\n *\n * @example\n * ```ts\n * const links = faviconLinks(\"de\", { source: \"./icon.svg\", locales: { de: { source: \"./icon-de.svg\" } } })\n * // → [{ rel: \"icon\", type: \"image/svg+xml\", href: \"/de/favicon.svg\" }, ...]\n * ```\n */\nexport function faviconLinks(\n locale: string | undefined,\n config: FaviconPluginConfig,\n): Array<{ rel: string; type?: string; sizes?: string; href: string }> {\n const hasLocaleOverride = locale && config.locales?.[locale]\n const prefix = hasLocaleOverride ? `/${locale}` : ''\n const isSvg = (hasLocaleOverride ? config.locales![locale]!.source : config.source).endsWith('.svg')\n\n const links: Array<{ rel: string; type?: string; sizes?: string; href: string }> = []\n\n if (isSvg) {\n links.push({ rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon.svg` })\n }\n\n links.push(\n { rel: 'icon', type: 'image/png', sizes: '32x32', href: `${prefix}/favicon-32x32.png` },\n { rel: 'icon', type: 'image/png', sizes: '16x16', href: `${prefix}/favicon-16x16.png` },\n { rel: 'apple-touch-icon', sizes: '180x180', href: `${prefix}/apple-touch-icon.png` },\n )\n\n if (config.manifest !== false) {\n links.push({ rel: 'manifest', href: `${prefix}/site.webmanifest` })\n }\n\n return links\n}\n\nasync function resizeToPng(input: string, size: number): Promise<Uint8Array | null> {\n try {\n const sharp = await import('sharp').then((m) => m.default ?? m)\n return await sharp(input).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } } as any).png().toBuffer()\n } catch {\n warnSharpMissing()\n return null\n }\n}\n\nasync function generateIco(input: string): Promise<Uint8Array | null> {\n try {\n const sharp = await import('sharp').then((m) => m.default ?? m)\n const png16 = await sharp(input).resize(16, 16, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } } as any).png().toBuffer()\n const png32 = await sharp(input).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } } as any).png().toBuffer()\n\n // ICO format: header + directory entries + PNG data\n return createIcoFromPngs([\n { buffer: png16, size: 16 },\n { buffer: png32, size: 32 },\n ])\n } catch {\n warnSharpMissing()\n return null\n }\n}\n\nexport interface IcoEntry {\n buffer: Buffer\n size: number\n}\n\n/** @internal Exported for testing */\nexport function createIcoFromPngs(entries: IcoEntry[]): Uint8Array {\n const headerSize = 6\n const dirEntrySize = 16\n const dirSize = dirEntrySize * entries.length\n let dataOffset = headerSize + dirSize\n\n // ICO header\n const header = Buffer.alloc(headerSize)\n header.writeUInt16LE(0, 0) // reserved\n header.writeUInt16LE(1, 2) // type: icon\n header.writeUInt16LE(entries.length, 4) // count\n\n // Directory entries\n const dirEntries = Buffer.alloc(dirSize)\n const dataBuffers: Buffer[] = []\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i]!\n const offset = i * dirEntrySize\n dirEntries.writeUInt8(entry.size === 256 ? 0 : entry.size, offset) // width\n dirEntries.writeUInt8(entry.size === 256 ? 0 : entry.size, offset + 1) // height\n dirEntries.writeUInt8(0, offset + 2) // palette\n dirEntries.writeUInt8(0, offset + 3) // reserved\n dirEntries.writeUInt16LE(1, offset + 4) // color planes\n dirEntries.writeUInt16LE(32, offset + 6) // bits per pixel\n dirEntries.writeUInt32LE(entry.buffer.length, offset + 8) // size\n dirEntries.writeUInt32LE(dataOffset, offset + 12) // offset\n\n dataOffset += entry.buffer.length\n dataBuffers.push(entry.buffer)\n }\n\n return Buffer.concat([header, dirEntries, ...dataBuffers])\n}\n","import { createContext } from '@pyreon/core'\nimport { signal } from '@pyreon/reactivity'\nimport type { Plugin } from 'vite'\n\n// ─── Localized routing ─────────────────────────────────────────────────────\n//\n// Adds locale-prefixed routes to Zero's file-system router:\n// - /about → /en/about, /de/about, /cs/about\n// - / → /en, /de, /cs (or default locale without prefix)\n// - Automatic locale detection from Accept-Language header\n// - Redirect to preferred locale\n// - hreflang link generation\n//\n// Usage:\n// import { i18nRouting } from \"@pyreon/zero\"\n// export default { plugins: [zero(), i18nRouting({ locales: [\"en\", \"de\"], defaultLocale: \"en\" })] }\n\nexport interface I18nRoutingConfig {\n /** Supported locales. e.g. [\"en\", \"de\", \"cs\"] */\n locales: string[]\n /** Default locale — served without prefix (/ instead of /en/). */\n defaultLocale: string\n /** Redirect root to detected locale. Default: true */\n detectLocale?: boolean\n /** Cookie name to persist locale preference. Default: \"locale\" */\n cookieName?: string\n /** URL strategy. Default: \"prefix-except-default\" */\n strategy?: 'prefix' | 'prefix-except-default'\n}\n\nexport interface LocaleContext {\n /** Current locale code. e.g. \"en\", \"de\" */\n locale: string\n /** All supported locales. */\n locales: string[]\n /** Default locale. */\n defaultLocale: string\n /** Build a localized path. e.g. localePath(\"/about\", \"de\") → \"/de/about\" */\n localePath: (path: string, locale?: string) => string\n /** Get hreflang alternates for the current path. */\n alternates: () => Array<{ locale: string; url: string }>\n}\n\n/**\n * Detect preferred locale from Accept-Language header.\n */\nexport function detectLocaleFromHeader(\n acceptLanguage: string | null | undefined,\n locales: string[],\n defaultLocale: string,\n): string {\n if (!acceptLanguage) return defaultLocale\n\n // Parse Accept-Language: en-US,en;q=0.9,de;q=0.8\n const preferred = acceptLanguage\n .split(',')\n .map((part) => {\n const [lang, q] = part.trim().split(';q=')\n return {\n lang: lang?.split('-')[0]?.toLowerCase() ?? '',\n quality: q ? Number.parseFloat(q) : 1,\n }\n })\n .sort((a, b) => b.quality - a.quality)\n\n for (const { lang } of preferred) {\n if (locales.includes(lang)) return lang\n }\n\n return defaultLocale\n}\n\n/**\n * Extract locale from a URL path.\n * Returns { locale, pathWithoutLocale }.\n */\nexport function extractLocaleFromPath(\n path: string,\n locales: string[],\n defaultLocale: string,\n): { locale: string; pathWithoutLocale: string } {\n const segments = path.split('/').filter(Boolean)\n const firstSegment = segments[0]?.toLowerCase()\n\n if (firstSegment && locales.includes(firstSegment)) {\n return {\n locale: firstSegment,\n pathWithoutLocale: '/' + segments.slice(1).join('/') || '/',\n }\n }\n\n return { locale: defaultLocale, pathWithoutLocale: path }\n}\n\n/**\n * Build a localized path.\n */\nexport function buildLocalePath(\n path: string,\n locale: string,\n defaultLocale: string,\n strategy: 'prefix' | 'prefix-except-default',\n): string {\n const clean = path === '/' ? '' : path\n if (strategy === 'prefix-except-default' && locale === defaultLocale) {\n return path\n }\n return `/${locale}${clean}`\n}\n\n/**\n * Create a LocaleContext for use in components and loaders.\n */\nexport function createLocaleContext(\n locale: string,\n path: string,\n config: I18nRoutingConfig,\n): LocaleContext {\n const strategy = config.strategy ?? 'prefix-except-default'\n\n return {\n locale,\n locales: config.locales,\n defaultLocale: config.defaultLocale,\n\n localePath(targetPath: string, targetLocale?: string) {\n return buildLocalePath(\n targetPath,\n targetLocale ?? locale,\n config.defaultLocale,\n strategy,\n )\n },\n\n alternates() {\n const { pathWithoutLocale } = extractLocaleFromPath(\n path,\n config.locales,\n config.defaultLocale,\n )\n return config.locales.map((loc) => ({\n locale: loc,\n url: buildLocalePath(pathWithoutLocale, loc, config.defaultLocale, strategy),\n }))\n },\n }\n}\n\n/**\n * I18n routing middleware for Zero's server.\n *\n * - Detects locale from URL prefix or Accept-Language header\n * - Redirects root to preferred locale (when detectLocale is true)\n * - Sets locale context for loaders and components\n *\n * @example\n * ```ts\n * // zero.config.ts\n * import { i18nRouting } from \"@pyreon/zero\"\n *\n * export default defineConfig({\n * plugins: [\n * i18nRouting({\n * locales: [\"en\", \"de\", \"cs\"],\n * defaultLocale: \"en\",\n * }),\n * ],\n * })\n * ```\n */\nexport function i18nRouting(config: I18nRoutingConfig): Plugin {\n const strategy = config.strategy ?? 'prefix-except-default'\n const detectEnabled = config.detectLocale !== false\n const cookieName = config.cookieName ?? 'locale'\n\n return {\n name: 'pyreon-zero-i18n-routing',\n\n // Route duplication is NOT handled here. The fs-router's `scanRouteFiles`\n // consumes the i18n config to duplicate routes per locale at build time.\n // This plugin only provides: (1) the server middleware for locale detection\n // and (2) the runtime hooks (useLocale, setLocale) for client-side use.\n configResolved() {},\n\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const url = req.url ?? '/'\n\n // Skip static assets\n if (url.startsWith('/@') || url.startsWith('/__') || url.includes('.')) {\n return next()\n }\n\n const { locale } = extractLocaleFromPath(\n url,\n config.locales,\n config.defaultLocale,\n )\n\n // Redirect root to detected locale\n if (detectEnabled && url === '/') {\n const cookies = parseCookies(req.headers.cookie)\n const preferredFromCookie = cookies[cookieName]\n const preferredFromHeader = detectLocaleFromHeader(\n req.headers['accept-language'],\n config.locales,\n config.defaultLocale,\n )\n const preferred = preferredFromCookie && config.locales.includes(preferredFromCookie)\n ? preferredFromCookie\n : preferredFromHeader\n\n if (strategy === 'prefix' || preferred !== config.defaultLocale) {\n res.writeHead(302, { Location: `/${preferred}/` })\n res.end()\n return\n }\n }\n\n // Attach locale context to request for loaders\n ;(req as any).__locale = locale\n ;(req as any).__localeContext = createLocaleContext(locale, url, config)\n\n // Update the module-level signal so useLocale() returns the correct value\n localeSignal.set(locale)\n\n next()\n })\n },\n }\n}\n\nfunction parseCookies(header: string | undefined): Record<string, string> {\n if (!header) return {}\n const result: Record<string, string> = {}\n for (const pair of header.split(';')) {\n const [key, value] = pair.trim().split('=')\n if (key && value) result[key] = decodeURIComponent(value)\n }\n return result\n}\n\n// ─── Reactive locale hook ───────────────────────────────────────────────────\n\n/** @internal Context for the current locale. */\nexport const LocaleCtx = createContext<string>('en')\n\n/** Current locale signal — set by the server middleware or client-side detection. */\nexport const localeSignal = signal('en')\n\n/**\n * Read the current locale reactively.\n *\n * Returns the locale signal value directly — reactive in both SSR and CSR.\n * The server middleware sets `localeSignal` per-request, and client-side\n * `setLocale()` updates it as well.\n *\n * @example\n * ```tsx\n * const locale = useLocale() // \"en\", \"de\", etc.\n * ```\n */\nexport function useLocale(): string {\n return localeSignal()\n}\n\n/**\n * Set the locale client-side and update the URL.\n *\n * @example\n * ```tsx\n * <button onClick={() => setLocale('de')}>Deutsch</button>\n * ```\n */\nexport function setLocale(\n locale: string,\n config: I18nRoutingConfig,\n): void {\n localeSignal.set(locale)\n\n // Persist to cookie\n if (typeof document !== 'undefined') {\n document.cookie = `${config.cookieName ?? 'locale'}=${locale}; path=/; max-age=31536000`\n }\n\n // Navigate to localized URL — use pushState to avoid full page reload\n if (typeof window !== 'undefined') {\n const strategy = config.strategy ?? 'prefix-except-default'\n const { pathWithoutLocale } = extractLocaleFromPath(\n window.location.pathname,\n config.locales,\n config.defaultLocale,\n )\n const newPath = buildLocalePath(pathWithoutLocale, locale, config.defaultLocale, strategy)\n window.history.pushState(null, '', newPath)\n // Dispatch popstate so @pyreon/router picks up the URL change\n window.dispatchEvent(new PopStateEvent('popstate'))\n }\n}\n","/**\n * OG Image generation plugin.\n *\n * Generates Open Graph images at build time from templates with\n * text overlays. Supports locale-specific text for i18n apps.\n * Uses sharp for image processing (same optional dep as favicon/image plugins).\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { ogImagePlugin } from \"@pyreon/zero/og-image\"\n *\n * export default {\n * plugins: [\n * ogImagePlugin({\n * locales: [\"en\", \"de\", \"cs\"],\n * templates: [{\n * name: \"default\",\n * background: \"./src/assets/og-bg.jpg\",\n * layers: [{\n * text: { en: \"Build faster\", de: \"Schneller bauen\", cs: \"Stavte rychleji\" },\n * y: \"40%\",\n * fontSize: 72,\n * }],\n * }],\n * }),\n * ],\n * }\n * ```\n */\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Plugin } from 'vite'\n\nlet sharpWarned = false\nfunction warnSharpMissing() {\n if (sharpWarned) return\n sharpWarned = true\n // oxlint-disable-next-line no-console\n console.warn(\n '\\n[zero:og-image] sharp not installed — OG images will not be generated. Install for full support: bun add -D sharp\\n',\n )\n}\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface OgImageLayer {\n /**\n * Text content. Can be:\n * - A string (same for all locales)\n * - A record mapping locale → text\n * - A function receiving locale and returning text\n */\n text: string | Record<string, string> | ((locale: string) => string)\n /** X position — number (px) or string with % (e.g. \"50%\"). Default: \"50%\" */\n x?: number | string\n /** Y position — number (px) or string with % (e.g. \"40%\"). Default: \"50%\" */\n y?: number | string\n /** Font size in px. Default: 64 */\n fontSize?: number\n /** Font family. Default: \"sans-serif\" */\n fontFamily?: string\n /** Font weight. Default: \"bold\" */\n fontWeight?: string\n /** Text color. Default: \"#ffffff\" */\n color?: string\n /** Text anchor (alignment). Default: \"middle\" */\n textAnchor?: 'start' | 'middle' | 'end'\n /** Max width in px before wrapping. Default: 80% of image width. */\n maxWidth?: number\n}\n\nexport interface OgImageTemplate {\n /** Template name — used for output file naming. */\n name: string\n /**\n * Background: path to an image file, or a solid color config.\n *\n * @example \"./src/assets/og-bg.jpg\"\n * @example { color: \"#0066ff\", width: 1200, height: 630 }\n */\n background: string | { color: string; width?: number; height?: number }\n /** Output width. Default: 1200 */\n width?: number\n /** Output height. Default: 630 */\n height?: number\n /** Output format. Default: \"png\" */\n format?: 'png' | 'jpeg'\n /** JPEG quality (1-100). Default: 90 */\n quality?: number\n /** Text layers to overlay on the background. */\n layers?: OgImageLayer[]\n}\n\nexport interface OgImagePluginConfig {\n /** Templates to generate. */\n templates: OgImageTemplate[]\n /** Locales to generate for. When omitted, generates a single image per template. */\n locales?: string[]\n /** Output directory prefix. Default: \"og\" */\n outDir?: string\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction resolvePosition(value: number | string | undefined, dimension: number, fallback = '50%'): number {\n if (value === undefined) value = fallback\n if (typeof value === 'number') return value\n if (value.endsWith('%')) return Math.round((Number.parseFloat(value) / 100) * dimension)\n return Number.parseInt(value, 10) || 0\n}\n\nfunction resolveLayerText(layer: OgImageLayer, locale: string): string {\n if (typeof layer.text === 'string') return layer.text\n if (typeof layer.text === 'function') return layer.text(locale)\n return layer.text[locale] ?? layer.text[Object.keys(layer.text)[0] ?? ''] ?? ''\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\n/**\n * Build an SVG overlay with text layers.\n * @internal Exported for testing.\n */\nexport function buildTextOverlaySvg(\n layers: OgImageLayer[],\n width: number,\n height: number,\n locale: string,\n): string {\n const textElements = layers.map((layer) => {\n const text = resolveLayerText(layer, locale)\n const x = resolvePosition(layer.x, width, '50%')\n const y = resolvePosition(layer.y, height, '50%')\n const fontSize = layer.fontSize ?? 64\n const fontFamily = layer.fontFamily ?? 'sans-serif'\n const fontWeight = layer.fontWeight ?? 'bold'\n const color = layer.color ?? '#ffffff'\n const anchor = layer.textAnchor ?? 'middle'\n const maxWidth = layer.maxWidth ?? Math.round(width * 0.8)\n\n // Word wrapping via tspan elements.\n // Width estimation: Latin chars ~0.55em, CJK chars ~1.0em, narrow chars ~0.35em.\n const words = text.split(' ')\n const lines: string[] = []\n let currentLine = ''\n\n const estimateWidth = (s: string): number => {\n let width = 0\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i)\n if (code >= 0x3000 && code <= 0x9FFF) {\n // CJK characters — full width\n width += fontSize * 1.0\n } else if (code <= 0x7E && 'iljft!|:;.,\\''.includes(s[i]!)) {\n // Narrow Latin characters\n width += fontSize * 0.35\n } else {\n // Regular Latin characters\n width += fontSize * 0.55\n }\n }\n return width\n }\n\n for (const word of words) {\n const testLine = currentLine ? `${currentLine} ${word}` : word\n if (estimateWidth(testLine) > maxWidth && currentLine) {\n lines.push(currentLine)\n currentLine = word\n } else {\n currentLine = testLine\n }\n }\n if (currentLine) lines.push(currentLine)\n\n const tspans = lines\n .map((line, i) => {\n const dy = i === 0 ? '0' : `${fontSize * 1.2}`\n return `<tspan x=\"${x}\" dy=\"${dy}\">${escapeXml(line)}</tspan>`\n })\n .join('')\n\n return `<text x=\"${x}\" y=\"${y}\" font-size=\"${fontSize}\" font-family=\"${escapeXml(fontFamily)}\" font-weight=\"${fontWeight}\" fill=\"${color}\" text-anchor=\"${anchor}\" dominant-baseline=\"middle\">${tspans}</text>`\n })\n\n return `<svg width=\"${width}\" height=\"${height}\" xmlns=\"http://www.w3.org/2000/svg\">${textElements.join('')}</svg>`\n}\n\n/**\n * Render an OG image from a template for a specific locale.\n * @internal Exported for testing.\n */\nexport async function renderOgImage(\n template: OgImageTemplate,\n locale: string,\n rootDir: string,\n): Promise<Uint8Array | null> {\n try {\n const sharp = await import('sharp').then((m) => m.default ?? m)\n const width = template.width ?? 1200\n const height = template.height ?? 630\n\n let pipeline: any\n if (typeof template.background === 'string') {\n const bgPath = join(rootDir, template.background)\n pipeline = sharp(bgPath).resize(width, height, { fit: 'cover' })\n } else {\n pipeline = (sharp as any)({\n create: {\n width,\n height,\n channels: 4,\n background: template.background.color,\n },\n })\n }\n\n // Overlay text layers if any\n if (template.layers && template.layers.length > 0) {\n const svgOverlay = buildTextOverlaySvg(template.layers, width, height, locale)\n pipeline = pipeline.composite([{\n input: Buffer.from(svgOverlay),\n top: 0,\n left: 0,\n }])\n }\n\n if (template.format === 'jpeg') {\n return await pipeline.jpeg({ quality: template.quality ?? 90 }).toBuffer()\n }\n return await pipeline.png().toBuffer()\n } catch {\n warnSharpMissing()\n return null\n }\n}\n\n// ─── Path utility ───────────────────────────────────────────────────────────\n\n/**\n * Compute the OG image path for a template and locale.\n *\n * @example\n * ```ts\n * ogImagePath(\"default\", \"de\") // → \"/og/default-de.png\"\n * ogImagePath(\"default\") // → \"/og/default.png\"\n * ogImagePath(\"hero\", \"en\", \"images\") // → \"/images/hero-en.png\"\n * ```\n */\nexport function ogImagePath(\n templateName: string,\n locale?: string,\n outDir = 'og',\n format: 'png' | 'jpeg' = 'png',\n): string {\n const ext = format === 'jpeg' ? 'jpg' : 'png'\n const suffix = locale ? `-${locale}` : ''\n return `/${outDir}/${templateName}${suffix}.${ext}`\n}\n\n// ─── Vite plugin ────────────────────────────────────────────────────────────\n\n/**\n * OG image generation Vite plugin.\n *\n * Generates Open Graph images at build time. In dev, generates on-demand.\n * Requires `sharp` as an optional dependency.\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { ogImagePlugin } from \"@pyreon/zero/og-image\"\n *\n * export default {\n * plugins: [\n * ogImagePlugin({\n * locales: [\"en\", \"de\"],\n * templates: [{\n * name: \"default\",\n * background: { color: \"#0066ff\" },\n * layers: [{ text: { en: \"Hello\", de: \"Hallo\" }, fontSize: 72 }],\n * }],\n * }),\n * ],\n * }\n * ```\n */\nexport function ogImagePlugin(config: OgImagePluginConfig): Plugin {\n const outDir = config.outDir ?? 'og'\n let root = ''\n let isBuild = false\n\n return {\n name: 'pyreon-zero-og-image',\n enforce: 'pre',\n\n configResolved(resolvedConfig) {\n root = resolvedConfig.root\n isBuild = resolvedConfig.command === 'build'\n },\n\n // Dev: generate on-demand\n configureServer(server) {\n const devCache = new Map<string, Uint8Array>()\n\n server.middlewares.use(async (req, res, next) => {\n const url = req.url ?? ''\n if (!url.startsWith(`/${outDir}/`)) return next()\n\n // Parse: /og/default-en.png → template=default, locale=en\n const fileName = url.slice(outDir.length + 2) // strip /{outDir}/\n const match = fileName.match(/^(.+?)(?:-([a-z]{2,5}))?\\.(png|jpe?g)$/)\n if (!match) return next()\n\n const [, templateName, locale, ext] = match\n const template = config.templates.find((t) => t.name === templateName)\n if (!template) return next()\n\n const resolvedLocale = locale ?? config.locales?.[0] ?? 'en'\n const cacheKey = `${templateName}:${resolvedLocale}`\n\n let buffer = devCache.get(cacheKey)\n if (!buffer) {\n const result = await renderOgImage(template, resolvedLocale, root)\n if (!result) return next()\n buffer = result\n devCache.set(cacheKey, result)\n }\n\n const contentType = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'image/png'\n res.setHeader('Content-Type', contentType)\n res.setHeader('Cache-Control', 'no-cache')\n res.end(Buffer.from(buffer))\n })\n },\n\n // Build: generate all variants\n async generateBundle() {\n if (!isBuild) return\n\n for (const template of config.templates) {\n const locales = config.locales ?? [undefined]\n const format = template.format ?? 'png'\n const ext = format === 'jpeg' ? 'jpg' : 'png'\n\n for (const locale of locales) {\n // Validate background exists if it's a file path\n if (typeof template.background === 'string') {\n const bgPath = join(root, template.background)\n if (!existsSync(bgPath)) {\n // oxlint-disable-next-line no-console\n console.warn(`[zero:og-image] Background not found: ${bgPath}`)\n continue\n }\n }\n\n const buffer = await renderOgImage(template, locale ?? 'en', root)\n if (!buffer) continue\n\n const suffix = locale ? `-${locale}` : ''\n this.emitFile({\n type: 'asset',\n fileName: `${outDir}/${template.name}${suffix}.${ext}`,\n source: buffer,\n })\n }\n }\n },\n }\n}\n","import type { VNodeChild } from '@pyreon/core'\nimport type { UseHeadInput } from '@pyreon/head'\nimport { useHead } from '@pyreon/head'\nimport type { FaviconPluginConfig } from './favicon'\nimport { faviconLinks } from './favicon'\nimport type { I18nRoutingConfig } from './i18n-routing'\nimport { extractLocaleFromPath } from './i18n-routing'\nimport { ogImagePath } from './og-image'\n\n// ─── Meta component ────────────────────────────────────────────────────────\n\nexport interface MetaProps {\n /** Page title. Accepts reactive accessor `() => string`. */\n title?: string | (() => string)\n /** Page description. Accepts reactive accessor. */\n description?: string | (() => string)\n /** Canonical URL. Also sets og:url. */\n canonical?: string\n /** Open Graph image URL. Also sets twitter:image. */\n image?: string\n /** Image alt text for accessibility. */\n imageAlt?: string\n /** Image width in pixels (og:image:width). Helps crawlers layout before loading. */\n imageWidth?: number\n /** Image height in pixels (og:image:height). */\n imageHeight?: number\n /** Open Graph type. Default: \"website\" */\n type?: 'website' | 'article' | 'product' | 'profile'\n /** Site name for og:site_name. */\n siteName?: string\n /** Twitter card type. Default: \"summary_large_image\" */\n twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player'\n /** Twitter @handle. */\n twitterSite?: string\n /** Twitter creator @handle. */\n twitterCreator?: string\n /** Locale. Default: \"en_US\" */\n locale?: string\n /** Alternate locales for hreflang. */\n alternateLocales?: Array<{ locale: string; url: string }>\n /** Robots directives. Default: \"index, follow\" */\n robots?: string\n /** Convenience: set `true` to emit `noindex, nofollow`. Overrides `robots`. */\n noIndex?: boolean\n /** Published time (ISO 8601) for article type. */\n publishedTime?: string\n /** Modified time (ISO 8601) for article type. */\n modifiedTime?: string\n /** Article author. */\n author?: string\n /** Article tags. */\n tags?: string[]\n /** JSON-LD structured data object. */\n jsonLd?: Record<string, unknown>\n /** Additional custom meta tags. */\n extra?: Array<{ name?: string; property?: string; content: string }>\n /**\n * Open Graph video URL. Also sets og:video:type if the URL ends with\n * a known extension (.mp4, .webm).\n */\n video?: string\n /** Video width in pixels. */\n videoWidth?: number\n /** Video height in pixels. */\n videoHeight?: number\n /**\n * Open Graph audio URL.\n */\n audio?: string\n /**\n * I18n routing config — when provided, auto-generates hreflang alternate\n * links for all locales based on the current path.\n * Also sets og:locale and og:locale:alternate.\n */\n i18n?: I18nRoutingConfig\n /** Base URL for building absolute hreflang URLs. e.g. \"https://example.com\" */\n origin?: string\n /**\n * Favicon plugin config — when provided, injects locale-aware favicon\n * `<link>` tags into `<head>`. Uses the current locale to select\n * the correct favicon set.\n */\n favicon?: FaviconPluginConfig\n /**\n * OG image template name — auto-resolves to the correct locale-specific\n * OG image path generated by `ogImagePlugin`.\n * Sets both `og:image` and `twitter:image` unless `image` is also provided.\n */\n ogTemplate?: string\n /** Output directory for OG images. Default: \"og\" */\n ogImageDir?: string\n /** OG image format. Default: \"png\" */\n ogImageFormat?: 'png' | 'jpeg'\n children?: VNodeChild\n}\n\nconst resolveStr = (v: string | (() => string) | undefined): string | undefined =>\n typeof v === 'function' ? v() : v\n\n/**\n * Declarative meta component for SSR-compatible page metadata.\n *\n * Supports reactive title/description — when passed as `() => string` accessors,\n * they are forwarded to `useHead()` as a reactive getter so updates propagate\n * automatically via signal tracking.\n *\n * @example\n * ```tsx\n * <Meta title=\"My Page\" description=\"...\" image=\"/og.jpg\" canonical=\"https://...\" />\n * ```\n *\n * @example Reactive title\n * ```tsx\n * const count = signal(0)\n * <Meta title={() => `${count()} items`} />\n * ```\n */\nexport function Meta(props: MetaProps): VNodeChild {\n const hasReactiveTitle = typeof props.title === 'function'\n const hasReactiveDescription = typeof props.description === 'function'\n\n // If title or description are reactive accessors, pass a getter to useHead\n // so it re-evaluates when the signals change.\n if (hasReactiveTitle || hasReactiveDescription) {\n useHead((): UseHeadInput => {\n const title = resolveStr(props.title)\n const description = resolveStr(props.description)\n const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]\n const tags = buildMetaTags(resolved)\n const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }\n if (title) input.title = title\n return input\n })\n } else {\n const title = resolveStr(props.title)\n const description = resolveStr(props.description)\n const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]\n const tags = buildMetaTags(resolved)\n const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }\n if (title) input.title = title\n useHead(input)\n }\n\n return props.children ?? null\n}\n\ninterface MetaTagEntry {\n name?: string\n property?: string\n content: string\n [key: string]: string | undefined\n}\n\ninterface LinkTagEntry {\n rel: string\n href?: string\n hreflang?: string\n type?: string\n sizes?: string\n [key: string]: string | undefined\n}\n\ninterface ScriptTagEntry {\n type: string\n children: string\n}\n\ninterface MetaTags {\n meta: MetaTagEntry[]\n link: LinkTagEntry[]\n script: ScriptTagEntry[]\n}\n\nexport function buildMetaTags(\n props: Omit<MetaProps, 'title' | 'description' | 'children'> & {\n title?: string\n description?: string\n },\n): MetaTags {\n const meta: MetaTagEntry[] = []\n const link: LinkTagEntry[] = []\n const script: ScriptTagEntry[] = []\n\n const {\n title, description, canonical, imageAlt, imageWidth, imageHeight,\n type = 'website', siteName,\n twitterCard = 'summary_large_image', twitterSite, twitterCreator,\n locale = 'en_US', alternateLocales,\n publishedTime, modifiedTime, author, tags, jsonLd, extra,\n video, videoWidth, videoHeight, audio,\n favicon, ogTemplate, ogImageDir, ogImageFormat,\n } = props\n\n // noIndex convenience overrides robots\n const robots = props.noIndex ? 'noindex, nofollow' : (props.robots ?? 'index, follow')\n\n // Resolve image: explicit `image` prop takes precedence over `ogTemplate`\n const image = props.image ?? (\n ogTemplate\n ? ogImagePath(ogTemplate, locale !== 'en_US' ? locale : undefined, ogImageDir, ogImageFormat)\n : undefined\n )\n\n // Auto-resolve image dimensions for OG template images\n const resolvedImageWidth = imageWidth ?? (ogTemplate && !props.image ? 1200 : undefined)\n const resolvedImageHeight = imageHeight ?? (ogTemplate && !props.image ? 630 : undefined)\n\n if (description) meta.push({ name: 'description', content: description })\n if (robots) meta.push({ name: 'robots', content: robots })\n if (author) meta.push({ name: 'author', content: author })\n\n if (title) meta.push({ property: 'og:title', content: title })\n if (description) meta.push({ property: 'og:description', content: description })\n if (canonical) meta.push({ property: 'og:url', content: canonical })\n if (image) meta.push({ property: 'og:image', content: image })\n if (imageAlt) meta.push({ property: 'og:image:alt', content: imageAlt })\n if (resolvedImageWidth) meta.push({ property: 'og:image:width', content: String(resolvedImageWidth) })\n if (resolvedImageHeight) meta.push({ property: 'og:image:height', content: String(resolvedImageHeight) })\n meta.push({ property: 'og:type', content: type })\n if (siteName) meta.push({ property: 'og:site_name', content: siteName })\n meta.push({ property: 'og:locale', content: locale })\n\n // Video\n if (video) {\n meta.push({ property: 'og:video', content: video })\n if (videoWidth) meta.push({ property: 'og:video:width', content: String(videoWidth) })\n if (videoHeight) meta.push({ property: 'og:video:height', content: String(videoHeight) })\n // Auto-detect video type from extension\n if (video.endsWith('.mp4')) meta.push({ property: 'og:video:type', content: 'video/mp4' })\n else if (video.endsWith('.webm')) meta.push({ property: 'og:video:type', content: 'video/webm' })\n }\n\n // Audio\n if (audio) {\n meta.push({ property: 'og:audio', content: audio })\n }\n\n if (type === 'article') {\n if (publishedTime) meta.push({ property: 'article:published_time', content: publishedTime })\n if (modifiedTime) meta.push({ property: 'article:modified_time', content: modifiedTime })\n if (author) meta.push({ property: 'article:author', content: author })\n if (tags) for (const tag of tags) meta.push({ property: 'article:tag', content: tag })\n }\n\n meta.push({ name: 'twitter:card', content: twitterCard })\n if (title) meta.push({ name: 'twitter:title', content: title })\n if (description) meta.push({ name: 'twitter:description', content: description })\n if (image) meta.push({ name: 'twitter:image', content: image })\n if (imageAlt) meta.push({ name: 'twitter:image:alt', content: imageAlt })\n if (twitterSite) meta.push({ name: 'twitter:site', content: twitterSite })\n if (twitterCreator) meta.push({ name: 'twitter:creator', content: twitterCreator })\n\n if (canonical) link.push({ rel: 'canonical', href: canonical })\n if (alternateLocales) {\n for (const alt of alternateLocales) {\n link.push({ rel: 'alternate', hreflang: alt.locale, href: alt.url })\n }\n }\n\n if (jsonLd) {\n script.push({\n type: 'application/ld+json',\n children: JSON.stringify({ '@context': 'https://schema.org', ...jsonLd }),\n })\n }\n\n if (extra) for (const tag of extra) meta.push(tag)\n\n // I18n: auto-generate hreflang alternates from i18nRouting config\n if (props.i18n) {\n const i18nConfig = props.i18n\n const origin = props.origin ?? ''\n const currentPath = canonical?.replace(origin, '') ?? '/'\n const { pathWithoutLocale } = extractLocaleFromPath(\n currentPath,\n i18nConfig.locales,\n i18nConfig.defaultLocale,\n )\n const strategy = i18nConfig.strategy ?? 'prefix-except-default'\n\n for (const loc of i18nConfig.locales) {\n const localizedPath =\n strategy === 'prefix-except-default' && loc === i18nConfig.defaultLocale\n ? pathWithoutLocale\n : `/${loc}${pathWithoutLocale === '/' ? '' : pathWithoutLocale}`\n\n link.push({\n rel: 'alternate',\n hreflang: loc,\n href: `${origin}${localizedPath}`,\n })\n\n // og:locale:alternate for non-current locales\n if (loc !== locale) {\n meta.push({ property: 'og:locale:alternate', content: loc })\n }\n }\n\n // x-default hreflang pointing to default locale\n link.push({\n rel: 'alternate',\n hreflang: 'x-default',\n href: `${origin}${pathWithoutLocale}`,\n })\n }\n\n // Favicon: inject locale-aware favicon links\n if (favicon) {\n const faviconLocale = locale !== 'en_US' ? locale : undefined\n for (const fl of faviconLinks(faviconLocale, favicon)) {\n link.push(fl as LinkTagEntry)\n }\n // Theme color meta from favicon config\n if (favicon.themeColor) {\n meta.push({ name: 'theme-color', content: favicon.themeColor })\n }\n }\n\n return { meta, link, script }\n}\n"],"mappings":";;;;;;;;;;;;;;;AA2aA,SAAgB,aACd,QACA,QACqE;CACrE,MAAM,oBAAoB,UAAU,OAAO,UAAU;CACrD,MAAM,SAAS,oBAAoB,IAAI,WAAW;CAClD,MAAM,SAAS,oBAAoB,OAAO,QAAS,QAAS,SAAS,OAAO,QAAQ,SAAS,OAAO;CAEpG,MAAM,QAA6E,EAAE;AAErF,KAAI,MACF,OAAM,KAAK;EAAE,KAAK;EAAQ,MAAM;EAAiB,MAAM,GAAG,OAAO;EAAe,CAAC;AAGnF,OAAM,KACJ;EAAE,KAAK;EAAQ,MAAM;EAAa,OAAO;EAAS,MAAM,GAAG,OAAO;EAAqB,EACvF;EAAE,KAAK;EAAQ,MAAM;EAAa,OAAO;EAAS,MAAM,GAAG,OAAO;EAAqB,EACvF;EAAE,KAAK;EAAoB,OAAO;EAAW,MAAM,GAAG,OAAO;EAAwB,CACtF;AAED,KAAI,OAAO,aAAa,MACtB,OAAM,KAAK;EAAE,KAAK;EAAY,MAAM,GAAG,OAAO;EAAoB,CAAC;AAGrE,QAAO;;;;;;;;;ACvXT,SAAgB,sBACd,MACA,SACA,eAC+C;CAC/C,MAAM,WAAW,KAAK,MAAM,IAAI,CAAC,OAAO,QAAQ;CAChD,MAAM,eAAe,SAAS,IAAI,aAAa;AAE/C,KAAI,gBAAgB,QAAQ,SAAS,aAAa,CAChD,QAAO;EACL,QAAQ;EACR,mBAAmB,MAAM,SAAS,MAAM,EAAE,CAAC,KAAK,IAAI,IAAI;EACzD;AAGH,QAAO;EAAE,QAAQ;EAAe,mBAAmB;EAAM;;;AA0J3D,MAAa,YAAY,cAAsB,KAAK;;AAGpD,MAAa,eAAe,OAAO,KAAK;;;;;;;;;;;;;;ACSxC,SAAgB,YACd,cACA,QACA,SAAS,MACT,SAAyB,OACjB;CACR,MAAM,MAAM,WAAW,SAAS,QAAQ;AAExC,QAAO,IAAI,OAAO,GAAG,eADN,SAAS,IAAI,WAAW,GACI,GAAG;;;;;ACzKhD,MAAM,cAAc,MAClB,OAAO,MAAM,aAAa,GAAG,GAAG;;;;;;;;;;;;;;;;;;;AAoBlC,SAAgB,KAAK,OAA8B;CACjD,MAAM,mBAAmB,OAAO,MAAM,UAAU;CAChD,MAAM,yBAAyB,OAAO,MAAM,gBAAgB;AAI5D,KAAI,oBAAoB,uBACtB,eAA4B;EAC1B,MAAM,QAAQ,WAAW,MAAM,MAAM;EACrC,MAAM,cAAc,WAAW,MAAM,YAAY;EAEjD,MAAM,OAAO,cADI;GAAE,GAAG;GAAO;GAAO;GAAa,CACb;EACpC,MAAM,QAAsB;GAAE,MAAM,KAAK;GAAM,MAAM,KAAK;GAAM,QAAQ,KAAK;GAAQ;AACrF,MAAI,MAAO,OAAM,QAAQ;AACzB,SAAO;GACP;MACG;EACL,MAAM,QAAQ,WAAW,MAAM,MAAM;EACrC,MAAM,cAAc,WAAW,MAAM,YAAY;EAEjD,MAAM,OAAO,cADI;GAAE,GAAG;GAAO;GAAO;GAAa,CACb;EACpC,MAAM,QAAsB;GAAE,MAAM,KAAK;GAAM,MAAM,KAAK;GAAM,QAAQ,KAAK;GAAQ;AACrF,MAAI,MAAO,OAAM,QAAQ;AACzB,UAAQ,MAAM;;AAGhB,QAAO,MAAM,YAAY;;AA8B3B,SAAgB,cACd,OAIU;CACV,MAAM,OAAuB,EAAE;CAC/B,MAAM,OAAuB,EAAE;CAC/B,MAAM,SAA2B,EAAE;CAEnC,MAAM,EACJ,OAAO,aAAa,WAAW,UAAU,YAAY,aACrD,OAAO,WAAW,UAClB,cAAc,uBAAuB,aAAa,gBAClD,SAAS,SAAS,kBAClB,eAAe,cAAc,QAAQ,MAAM,QAAQ,OACnD,OAAO,YAAY,aAAa,OAChC,SAAS,YAAY,YAAY,kBAC/B;CAGJ,MAAM,SAAS,MAAM,UAAU,sBAAuB,MAAM,UAAU;CAGtE,MAAM,QAAQ,MAAM,UAClB,aACI,YAAY,YAAY,WAAW,UAAU,SAAS,QAAW,YAAY,cAAc,GAC3F;CAIN,MAAM,qBAAqB,eAAe,cAAc,CAAC,MAAM,QAAQ,OAAO;CAC9E,MAAM,sBAAsB,gBAAgB,cAAc,CAAC,MAAM,QAAQ,MAAM;AAE/E,KAAI,YAAa,MAAK,KAAK;EAAE,MAAM;EAAe,SAAS;EAAa,CAAC;AACzE,KAAI,OAAQ,MAAK,KAAK;EAAE,MAAM;EAAU,SAAS;EAAQ,CAAC;AAC1D,KAAI,OAAQ,MAAK,KAAK;EAAE,MAAM;EAAU,SAAS;EAAQ,CAAC;AAE1D,KAAI,MAAO,MAAK,KAAK;EAAE,UAAU;EAAY,SAAS;EAAO,CAAC;AAC9D,KAAI,YAAa,MAAK,KAAK;EAAE,UAAU;EAAkB,SAAS;EAAa,CAAC;AAChF,KAAI,UAAW,MAAK,KAAK;EAAE,UAAU;EAAU,SAAS;EAAW,CAAC;AACpE,KAAI,MAAO,MAAK,KAAK;EAAE,UAAU;EAAY,SAAS;EAAO,CAAC;AAC9D,KAAI,SAAU,MAAK,KAAK;EAAE,UAAU;EAAgB,SAAS;EAAU,CAAC;AACxE,KAAI,mBAAoB,MAAK,KAAK;EAAE,UAAU;EAAkB,SAAS,OAAO,mBAAmB;EAAE,CAAC;AACtG,KAAI,oBAAqB,MAAK,KAAK;EAAE,UAAU;EAAmB,SAAS,OAAO,oBAAoB;EAAE,CAAC;AACzG,MAAK,KAAK;EAAE,UAAU;EAAW,SAAS;EAAM,CAAC;AACjD,KAAI,SAAU,MAAK,KAAK;EAAE,UAAU;EAAgB,SAAS;EAAU,CAAC;AACxE,MAAK,KAAK;EAAE,UAAU;EAAa,SAAS;EAAQ,CAAC;AAGrD,KAAI,OAAO;AACT,OAAK,KAAK;GAAE,UAAU;GAAY,SAAS;GAAO,CAAC;AACnD,MAAI,WAAY,MAAK,KAAK;GAAE,UAAU;GAAkB,SAAS,OAAO,WAAW;GAAE,CAAC;AACtF,MAAI,YAAa,MAAK,KAAK;GAAE,UAAU;GAAmB,SAAS,OAAO,YAAY;GAAE,CAAC;AAEzF,MAAI,MAAM,SAAS,OAAO,CAAE,MAAK,KAAK;GAAE,UAAU;GAAiB,SAAS;GAAa,CAAC;WACjF,MAAM,SAAS,QAAQ,CAAE,MAAK,KAAK;GAAE,UAAU;GAAiB,SAAS;GAAc,CAAC;;AAInG,KAAI,MACF,MAAK,KAAK;EAAE,UAAU;EAAY,SAAS;EAAO,CAAC;AAGrD,KAAI,SAAS,WAAW;AACtB,MAAI,cAAe,MAAK,KAAK;GAAE,UAAU;GAA0B,SAAS;GAAe,CAAC;AAC5F,MAAI,aAAc,MAAK,KAAK;GAAE,UAAU;GAAyB,SAAS;GAAc,CAAC;AACzF,MAAI,OAAQ,MAAK,KAAK;GAAE,UAAU;GAAkB,SAAS;GAAQ,CAAC;AACtE,MAAI,KAAM,MAAK,MAAM,OAAO,KAAM,MAAK,KAAK;GAAE,UAAU;GAAe,SAAS;GAAK,CAAC;;AAGxF,MAAK,KAAK;EAAE,MAAM;EAAgB,SAAS;EAAa,CAAC;AACzD,KAAI,MAAO,MAAK,KAAK;EAAE,MAAM;EAAiB,SAAS;EAAO,CAAC;AAC/D,KAAI,YAAa,MAAK,KAAK;EAAE,MAAM;EAAuB,SAAS;EAAa,CAAC;AACjF,KAAI,MAAO,MAAK,KAAK;EAAE,MAAM;EAAiB,SAAS;EAAO,CAAC;AAC/D,KAAI,SAAU,MAAK,KAAK;EAAE,MAAM;EAAqB,SAAS;EAAU,CAAC;AACzE,KAAI,YAAa,MAAK,KAAK;EAAE,MAAM;EAAgB,SAAS;EAAa,CAAC;AAC1E,KAAI,eAAgB,MAAK,KAAK;EAAE,MAAM;EAAmB,SAAS;EAAgB,CAAC;AAEnF,KAAI,UAAW,MAAK,KAAK;EAAE,KAAK;EAAa,MAAM;EAAW,CAAC;AAC/D,KAAI,iBACF,MAAK,MAAM,OAAO,iBAChB,MAAK,KAAK;EAAE,KAAK;EAAa,UAAU,IAAI;EAAQ,MAAM,IAAI;EAAK,CAAC;AAIxE,KAAI,OACF,QAAO,KAAK;EACV,MAAM;EACN,UAAU,KAAK,UAAU;GAAE,YAAY;GAAsB,GAAG;GAAQ,CAAC;EAC1E,CAAC;AAGJ,KAAI,MAAO,MAAK,MAAM,OAAO,MAAO,MAAK,KAAK,IAAI;AAGlD,KAAI,MAAM,MAAM;EACd,MAAM,aAAa,MAAM;EACzB,MAAM,SAAS,MAAM,UAAU;EAE/B,MAAM,EAAE,sBAAsB,sBADV,WAAW,QAAQ,QAAQ,GAAG,IAAI,KAGpD,WAAW,SACX,WAAW,cACZ;EACD,MAAM,WAAW,WAAW,YAAY;AAExC,OAAK,MAAM,OAAO,WAAW,SAAS;GACpC,MAAM,gBACJ,aAAa,2BAA2B,QAAQ,WAAW,gBACvD,oBACA,IAAI,MAAM,sBAAsB,MAAM,KAAK;AAEjD,QAAK,KAAK;IACR,KAAK;IACL,UAAU;IACV,MAAM,GAAG,SAAS;IACnB,CAAC;AAGF,OAAI,QAAQ,OACV,MAAK,KAAK;IAAE,UAAU;IAAuB,SAAS;IAAK,CAAC;;AAKhE,OAAK,KAAK;GACR,KAAK;GACL,UAAU;GACV,MAAM,GAAG,SAAS;GACnB,CAAC;;AAIJ,KAAI,SAAS;EACX,MAAM,gBAAgB,WAAW,UAAU,SAAS;AACpD,OAAK,MAAM,MAAM,aAAa,eAAe,QAAQ,CACnD,MAAK,KAAK,GAAmB;AAG/B,MAAI,QAAQ,WACV,MAAK,KAAK;GAAE,MAAM;GAAe,SAAS,QAAQ;GAAY,CAAC;;AAInE,QAAO;EAAE;EAAM;EAAM;EAAQ"}
|
|
1
|
+
{"version":3,"file":"meta.js","names":[],"sources":["../src/i18n-routing.ts","../src/meta.tsx"],"sourcesContent":["import { createContext } from '@pyreon/core'\nimport { signal } from '@pyreon/reactivity'\nimport type { Plugin } from 'vite'\n\n// ─── Localized routing ─────────────────────────────────────────────────────\n//\n// Adds locale-prefixed routes to Zero's file-system router:\n// - /about → /en/about, /de/about, /cs/about\n// - / → /en, /de, /cs (or default locale without prefix)\n// - Automatic locale detection from Accept-Language header\n// - Redirect to preferred locale\n// - hreflang link generation\n//\n// Usage:\n// import { i18nRouting } from \"@pyreon/zero\"\n// export default { plugins: [zero(), i18nRouting({ locales: [\"en\", \"de\"], defaultLocale: \"en\" })] }\n\nexport interface I18nRoutingConfig {\n /** Supported locales. e.g. [\"en\", \"de\", \"cs\"] */\n locales: string[]\n /** Default locale — served without prefix (/ instead of /en/). */\n defaultLocale: string\n /** Redirect root to detected locale. Default: true */\n detectLocale?: boolean\n /** Cookie name to persist locale preference. Default: \"locale\" */\n cookieName?: string\n /** URL strategy. Default: \"prefix-except-default\" */\n strategy?: 'prefix' | 'prefix-except-default'\n}\n\nexport interface LocaleContext {\n /** Current locale code. e.g. \"en\", \"de\" */\n locale: string\n /** All supported locales. */\n locales: string[]\n /** Default locale. */\n defaultLocale: string\n /** Build a localized path. e.g. localePath(\"/about\", \"de\") → \"/de/about\" */\n localePath: (path: string, locale?: string) => string\n /** Get hreflang alternates for the current path. */\n alternates: () => Array<{ locale: string; url: string }>\n}\n\n/**\n * Detect preferred locale from Accept-Language header.\n */\nexport function detectLocaleFromHeader(\n acceptLanguage: string | null | undefined,\n locales: string[],\n defaultLocale: string,\n): string {\n if (!acceptLanguage) return defaultLocale\n\n // Parse Accept-Language: en-US,en;q=0.9,de;q=0.8\n const preferred = acceptLanguage\n .split(',')\n .map((part) => {\n const [lang, q] = part.trim().split(';q=')\n return {\n lang: lang?.split('-')[0]?.toLowerCase() ?? '',\n quality: q ? Number.parseFloat(q) : 1,\n }\n })\n .sort((a, b) => b.quality - a.quality)\n\n for (const { lang } of preferred) {\n if (locales.includes(lang)) return lang\n }\n\n return defaultLocale\n}\n\n/**\n * Extract locale from a URL path.\n * Returns { locale, pathWithoutLocale }.\n */\nexport function extractLocaleFromPath(\n path: string,\n locales: string[],\n defaultLocale: string,\n): { locale: string; pathWithoutLocale: string } {\n const segments = path.split('/').filter(Boolean)\n const firstSegment = segments[0]?.toLowerCase()\n\n if (firstSegment && locales.includes(firstSegment)) {\n return {\n locale: firstSegment,\n pathWithoutLocale: '/' + segments.slice(1).join('/') || '/',\n }\n }\n\n return { locale: defaultLocale, pathWithoutLocale: path }\n}\n\n/**\n * Build a localized path.\n */\nexport function buildLocalePath(\n path: string,\n locale: string,\n defaultLocale: string,\n strategy: 'prefix' | 'prefix-except-default',\n): string {\n const clean = path === '/' ? '' : path\n if (strategy === 'prefix-except-default' && locale === defaultLocale) {\n return path\n }\n return `/${locale}${clean}`\n}\n\n/**\n * Create a LocaleContext for use in components and loaders.\n */\nexport function createLocaleContext(\n locale: string,\n path: string,\n config: I18nRoutingConfig,\n): LocaleContext {\n const strategy = config.strategy ?? 'prefix-except-default'\n\n return {\n locale,\n locales: config.locales,\n defaultLocale: config.defaultLocale,\n\n localePath(targetPath: string, targetLocale?: string) {\n return buildLocalePath(\n targetPath,\n targetLocale ?? locale,\n config.defaultLocale,\n strategy,\n )\n },\n\n alternates() {\n const { pathWithoutLocale } = extractLocaleFromPath(\n path,\n config.locales,\n config.defaultLocale,\n )\n return config.locales.map((loc) => ({\n locale: loc,\n url: buildLocalePath(pathWithoutLocale, loc, config.defaultLocale, strategy),\n }))\n },\n }\n}\n\n/**\n * I18n routing middleware for Zero's server.\n *\n * - Detects locale from URL prefix or Accept-Language header\n * - Redirects root to preferred locale (when detectLocale is true)\n * - Sets locale context for loaders and components\n *\n * @example\n * ```ts\n * // zero.config.ts\n * import { i18nRouting } from \"@pyreon/zero\"\n *\n * export default defineConfig({\n * plugins: [\n * i18nRouting({\n * locales: [\"en\", \"de\", \"cs\"],\n * defaultLocale: \"en\",\n * }),\n * ],\n * })\n * ```\n */\nexport function i18nRouting(config: I18nRoutingConfig): Plugin {\n const strategy = config.strategy ?? 'prefix-except-default'\n const detectEnabled = config.detectLocale !== false\n const cookieName = config.cookieName ?? 'locale'\n\n return {\n name: 'pyreon-zero-i18n-routing',\n\n // Route duplication is NOT handled here. The fs-router's `scanRouteFiles`\n // consumes the i18n config to duplicate routes per locale at build time.\n // This plugin only provides: (1) the server middleware for locale detection\n // and (2) the runtime hooks (useLocale, setLocale) for client-side use.\n configResolved() {},\n\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const url = req.url ?? '/'\n\n // Skip static assets\n if (url.startsWith('/@') || url.startsWith('/__') || url.includes('.')) {\n return next()\n }\n\n const { locale } = extractLocaleFromPath(\n url,\n config.locales,\n config.defaultLocale,\n )\n\n // Redirect root to detected locale\n if (detectEnabled && url === '/') {\n const cookies = parseCookies(req.headers.cookie)\n const preferredFromCookie = cookies[cookieName]\n const preferredFromHeader = detectLocaleFromHeader(\n req.headers['accept-language'],\n config.locales,\n config.defaultLocale,\n )\n const preferred = preferredFromCookie && config.locales.includes(preferredFromCookie)\n ? preferredFromCookie\n : preferredFromHeader\n\n if (strategy === 'prefix' || preferred !== config.defaultLocale) {\n res.writeHead(302, { Location: `/${preferred}/` })\n res.end()\n return\n }\n }\n\n // Attach locale context to request for loaders\n ;(req as any).__locale = locale\n ;(req as any).__localeContext = createLocaleContext(locale, url, config)\n\n // Update the module-level signal so useLocale() returns the correct value\n localeSignal.set(locale)\n\n next()\n })\n },\n }\n}\n\nfunction parseCookies(header: string | undefined): Record<string, string> {\n if (!header) return {}\n const result: Record<string, string> = {}\n for (const pair of header.split(';')) {\n const [key, value] = pair.trim().split('=')\n if (key && value) result[key] = decodeURIComponent(value)\n }\n return result\n}\n\n// ─── Reactive locale hook ───────────────────────────────────────────────────\n\n/** @internal Context for the current locale. */\nexport const LocaleCtx = createContext<string>('en')\n\n/** Current locale signal — set by the server middleware or client-side detection. */\nexport const localeSignal = signal('en')\n\n/**\n * Read the current locale reactively.\n *\n * Returns the locale signal value directly — reactive in both SSR and CSR.\n * The server middleware sets `localeSignal` per-request, and client-side\n * `setLocale()` updates it as well.\n *\n * @example\n * ```tsx\n * const locale = useLocale() // \"en\", \"de\", etc.\n * ```\n */\nexport function useLocale(): string {\n return localeSignal()\n}\n\n/**\n * Set the locale client-side and update the URL.\n *\n * @example\n * ```tsx\n * <button onClick={() => setLocale('de')}>Deutsch</button>\n * ```\n */\nexport function setLocale(\n locale: string,\n config: I18nRoutingConfig,\n): void {\n localeSignal.set(locale)\n\n // Persist to cookie\n if (typeof document !== 'undefined') {\n document.cookie = `${config.cookieName ?? 'locale'}=${locale}; path=/; max-age=31536000`\n }\n\n // Navigate to localized URL — use pushState to avoid full page reload\n if (typeof window !== 'undefined') {\n const strategy = config.strategy ?? 'prefix-except-default'\n const { pathWithoutLocale } = extractLocaleFromPath(\n window.location.pathname,\n config.locales,\n config.defaultLocale,\n )\n const newPath = buildLocalePath(pathWithoutLocale, locale, config.defaultLocale, strategy)\n window.history.pushState(null, '', newPath)\n // Dispatch popstate so @pyreon/router picks up the URL change\n window.dispatchEvent(new PopStateEvent('popstate'))\n }\n}\n","import type { VNodeChild } from '@pyreon/core'\nimport type { UseHeadInput } from '@pyreon/head'\nimport { useHead } from '@pyreon/head'\nimport type { I18nRoutingConfig } from './i18n-routing'\nimport { extractLocaleFromPath } from './i18n-routing'\n\n// ─── Inline helpers (no node:fs dependency) ─────────────────────────────────\n// These are inlined to avoid importing from favicon.ts/og-image.ts which\n// pull in node:fs at the top level — making Meta unsafe for client bundles.\n\n/** Favicon plugin config shape (type-only). */\ninterface FaviconPluginConfig {\n source: string\n themeColor?: string\n manifest?: boolean\n locales?: Record<string, { source: string; darkSource?: string }>\n [key: string]: unknown\n}\n\nfunction faviconLinks(\n locale: string | undefined,\n config: FaviconPluginConfig,\n): Array<{ rel: string; type?: string; sizes?: string; href: string }> {\n const hasLocaleOverride = locale && config.locales?.[locale]\n const prefix = hasLocaleOverride ? `/${locale}` : ''\n const isSvg = (hasLocaleOverride ? config.locales![locale]!.source : config.source).endsWith('.svg')\n const links: Array<{ rel: string; type?: string; sizes?: string; href: string }> = []\n if (isSvg) links.push({ rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon.svg` })\n links.push(\n { rel: 'icon', type: 'image/png', sizes: '32x32', href: `${prefix}/favicon-32x32.png` },\n { rel: 'icon', type: 'image/png', sizes: '16x16', href: `${prefix}/favicon-16x16.png` },\n { rel: 'apple-touch-icon', sizes: '180x180', href: `${prefix}/apple-touch-icon.png` },\n )\n if (config.manifest !== false) links.push({ rel: 'manifest', href: `${prefix}/site.webmanifest` })\n return links\n}\n\nfunction ogImagePath(templateName: string, locale?: string, outDir = 'og', format: 'png' | 'jpeg' = 'png'): string {\n const ext = format === 'jpeg' ? 'jpg' : 'png'\n const suffix = locale ? `-${locale}` : ''\n return `/${outDir}/${templateName}${suffix}.${ext}`\n}\n\n// ─── Meta component ────────────────────────────────────────────────────────\n\nexport interface MetaProps {\n /** Page title. Accepts reactive accessor `() => string`. */\n title?: string | (() => string)\n /** Page description. Accepts reactive accessor. */\n description?: string | (() => string)\n /** Canonical URL. Also sets og:url. */\n canonical?: string\n /** Open Graph image URL. Also sets twitter:image. */\n image?: string\n /** Image alt text for accessibility. */\n imageAlt?: string\n /** Image width in pixels (og:image:width). Helps crawlers layout before loading. */\n imageWidth?: number\n /** Image height in pixels (og:image:height). */\n imageHeight?: number\n /** Open Graph type. Default: \"website\" */\n type?: 'website' | 'article' | 'product' | 'profile'\n /** Site name for og:site_name. */\n siteName?: string\n /** Twitter card type. Default: \"summary_large_image\" */\n twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player'\n /** Twitter @handle. */\n twitterSite?: string\n /** Twitter creator @handle. */\n twitterCreator?: string\n /** Locale. Default: \"en_US\" */\n locale?: string\n /** Alternate locales for hreflang. */\n alternateLocales?: Array<{ locale: string; url: string }>\n /** Robots directives. Default: \"index, follow\" */\n robots?: string\n /** Convenience: set `true` to emit `noindex, nofollow`. Overrides `robots`. */\n noIndex?: boolean\n /** Published time (ISO 8601) for article type. */\n publishedTime?: string\n /** Modified time (ISO 8601) for article type. */\n modifiedTime?: string\n /** Article author. */\n author?: string\n /** Article tags. */\n tags?: string[]\n /** JSON-LD structured data object. */\n jsonLd?: Record<string, unknown>\n /** Additional custom meta tags. */\n extra?: Array<{ name?: string; property?: string; content: string }>\n /**\n * Open Graph video URL. Also sets og:video:type if the URL ends with\n * a known extension (.mp4, .webm).\n */\n video?: string\n /** Video width in pixels. */\n videoWidth?: number\n /** Video height in pixels. */\n videoHeight?: number\n /**\n * Open Graph audio URL.\n */\n audio?: string\n /**\n * I18n routing config — when provided, auto-generates hreflang alternate\n * links for all locales based on the current path.\n * Also sets og:locale and og:locale:alternate.\n */\n i18n?: I18nRoutingConfig\n /** Base URL for building absolute hreflang URLs. e.g. \"https://example.com\" */\n origin?: string\n /**\n * Favicon plugin config — when provided, injects locale-aware favicon\n * `<link>` tags into `<head>`. Uses the current locale to select\n * the correct favicon set.\n */\n favicon?: FaviconPluginConfig\n /**\n * OG image template name — auto-resolves to the correct locale-specific\n * OG image path generated by `ogImagePlugin`.\n * Sets both `og:image` and `twitter:image` unless `image` is also provided.\n */\n ogTemplate?: string\n /** Output directory for OG images. Default: \"og\" */\n ogImageDir?: string\n /** OG image format. Default: \"png\" */\n ogImageFormat?: 'png' | 'jpeg'\n children?: VNodeChild\n}\n\nconst resolveStr = (v: string | (() => string) | undefined): string | undefined =>\n typeof v === 'function' ? v() : v\n\n/**\n * Declarative meta component for SSR-compatible page metadata.\n *\n * Supports reactive title/description — when passed as `() => string` accessors,\n * they are forwarded to `useHead()` as a reactive getter so updates propagate\n * automatically via signal tracking.\n *\n * @example\n * ```tsx\n * <Meta title=\"My Page\" description=\"...\" image=\"/og.jpg\" canonical=\"https://...\" />\n * ```\n *\n * @example Reactive title\n * ```tsx\n * const count = signal(0)\n * <Meta title={() => `${count()} items`} />\n * ```\n */\nexport function Meta(props: MetaProps): VNodeChild {\n const hasReactiveTitle = typeof props.title === 'function'\n const hasReactiveDescription = typeof props.description === 'function'\n\n // If title or description are reactive accessors, pass a getter to useHead\n // so it re-evaluates when the signals change.\n if (hasReactiveTitle || hasReactiveDescription) {\n useHead((): UseHeadInput => {\n const title = resolveStr(props.title)\n const description = resolveStr(props.description)\n const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]\n const tags = buildMetaTags(resolved)\n const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }\n if (title) input.title = title\n return input\n })\n } else {\n const title = resolveStr(props.title)\n const description = resolveStr(props.description)\n const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]\n const tags = buildMetaTags(resolved)\n const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }\n if (title) input.title = title\n useHead(input)\n }\n\n return props.children ?? null\n}\n\ninterface MetaTagEntry {\n name?: string\n property?: string\n content: string\n [key: string]: string | undefined\n}\n\ninterface LinkTagEntry {\n rel: string\n href?: string\n hreflang?: string\n type?: string\n sizes?: string\n [key: string]: string | undefined\n}\n\ninterface ScriptTagEntry {\n type: string\n children: string\n}\n\ninterface MetaTags {\n meta: MetaTagEntry[]\n link: LinkTagEntry[]\n script: ScriptTagEntry[]\n}\n\nexport function buildMetaTags(\n props: Omit<MetaProps, 'title' | 'description' | 'children'> & {\n title?: string\n description?: string\n },\n): MetaTags {\n const meta: MetaTagEntry[] = []\n const link: LinkTagEntry[] = []\n const script: ScriptTagEntry[] = []\n\n const {\n title, description, canonical, imageAlt, imageWidth, imageHeight,\n type = 'website', siteName,\n twitterCard = 'summary_large_image', twitterSite, twitterCreator,\n locale = 'en_US', alternateLocales,\n publishedTime, modifiedTime, author, tags, jsonLd, extra,\n video, videoWidth, videoHeight, audio,\n favicon, ogTemplate, ogImageDir, ogImageFormat,\n } = props\n\n // noIndex convenience overrides robots\n const robots = props.noIndex ? 'noindex, nofollow' : (props.robots ?? 'index, follow')\n\n // Resolve image: explicit `image` prop takes precedence over `ogTemplate`\n const image = props.image ?? (\n ogTemplate\n ? ogImagePath(ogTemplate, locale !== 'en_US' ? locale : undefined, ogImageDir, ogImageFormat)\n : undefined\n )\n\n // Auto-resolve image dimensions for OG template images\n const resolvedImageWidth = imageWidth ?? (ogTemplate && !props.image ? 1200 : undefined)\n const resolvedImageHeight = imageHeight ?? (ogTemplate && !props.image ? 630 : undefined)\n\n if (description) meta.push({ name: 'description', content: description })\n if (robots) meta.push({ name: 'robots', content: robots })\n if (author) meta.push({ name: 'author', content: author })\n\n if (title) meta.push({ property: 'og:title', content: title })\n if (description) meta.push({ property: 'og:description', content: description })\n if (canonical) meta.push({ property: 'og:url', content: canonical })\n if (image) meta.push({ property: 'og:image', content: image })\n if (imageAlt) meta.push({ property: 'og:image:alt', content: imageAlt })\n if (resolvedImageWidth) meta.push({ property: 'og:image:width', content: String(resolvedImageWidth) })\n if (resolvedImageHeight) meta.push({ property: 'og:image:height', content: String(resolvedImageHeight) })\n meta.push({ property: 'og:type', content: type })\n if (siteName) meta.push({ property: 'og:site_name', content: siteName })\n meta.push({ property: 'og:locale', content: locale })\n\n // Video\n if (video) {\n meta.push({ property: 'og:video', content: video })\n if (videoWidth) meta.push({ property: 'og:video:width', content: String(videoWidth) })\n if (videoHeight) meta.push({ property: 'og:video:height', content: String(videoHeight) })\n // Auto-detect video type from extension\n if (video.endsWith('.mp4')) meta.push({ property: 'og:video:type', content: 'video/mp4' })\n else if (video.endsWith('.webm')) meta.push({ property: 'og:video:type', content: 'video/webm' })\n }\n\n // Audio\n if (audio) {\n meta.push({ property: 'og:audio', content: audio })\n }\n\n if (type === 'article') {\n if (publishedTime) meta.push({ property: 'article:published_time', content: publishedTime })\n if (modifiedTime) meta.push({ property: 'article:modified_time', content: modifiedTime })\n if (author) meta.push({ property: 'article:author', content: author })\n if (tags) for (const tag of tags) meta.push({ property: 'article:tag', content: tag })\n }\n\n meta.push({ name: 'twitter:card', content: twitterCard })\n if (title) meta.push({ name: 'twitter:title', content: title })\n if (description) meta.push({ name: 'twitter:description', content: description })\n if (image) meta.push({ name: 'twitter:image', content: image })\n if (imageAlt) meta.push({ name: 'twitter:image:alt', content: imageAlt })\n if (twitterSite) meta.push({ name: 'twitter:site', content: twitterSite })\n if (twitterCreator) meta.push({ name: 'twitter:creator', content: twitterCreator })\n\n if (canonical) link.push({ rel: 'canonical', href: canonical })\n if (alternateLocales) {\n for (const alt of alternateLocales) {\n link.push({ rel: 'alternate', hreflang: alt.locale, href: alt.url })\n }\n }\n\n if (jsonLd) {\n script.push({\n type: 'application/ld+json',\n children: JSON.stringify({ '@context': 'https://schema.org', ...jsonLd }),\n })\n }\n\n if (extra) for (const tag of extra) meta.push(tag)\n\n // I18n: auto-generate hreflang alternates from i18nRouting config\n if (props.i18n) {\n const i18nConfig = props.i18n\n const origin = props.origin ?? ''\n const currentPath = canonical?.replace(origin, '') ?? '/'\n const { pathWithoutLocale } = extractLocaleFromPath(\n currentPath,\n i18nConfig.locales,\n i18nConfig.defaultLocale,\n )\n const strategy = i18nConfig.strategy ?? 'prefix-except-default'\n\n for (const loc of i18nConfig.locales) {\n const localizedPath =\n strategy === 'prefix-except-default' && loc === i18nConfig.defaultLocale\n ? pathWithoutLocale\n : `/${loc}${pathWithoutLocale === '/' ? '' : pathWithoutLocale}`\n\n link.push({\n rel: 'alternate',\n hreflang: loc,\n href: `${origin}${localizedPath}`,\n })\n\n // og:locale:alternate for non-current locales\n if (loc !== locale) {\n meta.push({ property: 'og:locale:alternate', content: loc })\n }\n }\n\n // x-default hreflang pointing to default locale\n link.push({\n rel: 'alternate',\n hreflang: 'x-default',\n href: `${origin}${pathWithoutLocale}`,\n })\n }\n\n // Favicon: inject locale-aware favicon links\n if (favicon) {\n const faviconLocale = locale !== 'en_US' ? locale : undefined\n for (const fl of faviconLinks(faviconLocale, favicon)) {\n link.push(fl as LinkTagEntry)\n }\n // Theme color meta from favicon config\n if (favicon.themeColor) {\n meta.push({ name: 'theme-color', content: favicon.themeColor })\n }\n }\n\n return { meta, link, script }\n}\n"],"mappings":"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"}
|