@pyreon/zero 0.12.1 → 0.12.3

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.
Files changed (140) hide show
  1. package/lib/actions.js +97 -0
  2. package/lib/actions.js.map +1 -0
  3. package/lib/ai.js +503 -0
  4. package/lib/ai.js.map +1 -0
  5. package/lib/api-routes.js +137 -0
  6. package/lib/api-routes.js.map +1 -0
  7. package/lib/compression.js +80 -0
  8. package/lib/compression.js.map +1 -0
  9. package/lib/cors.js +57 -0
  10. package/lib/cors.js.map +1 -0
  11. package/lib/csp.js +119 -0
  12. package/lib/csp.js.map +1 -0
  13. package/lib/env.js +217 -0
  14. package/lib/env.js.map +1 -0
  15. package/lib/favicon.js +424 -0
  16. package/lib/favicon.js.map +1 -0
  17. package/lib/i18n-routing.js +167 -0
  18. package/lib/i18n-routing.js.map +1 -0
  19. package/lib/index.js +1631 -179
  20. package/lib/index.js.map +1 -1
  21. package/lib/link.js +5 -0
  22. package/lib/link.js.map +1 -1
  23. package/lib/logger.js +78 -0
  24. package/lib/logger.js.map +1 -0
  25. package/lib/meta.js +336 -0
  26. package/lib/meta.js.map +1 -0
  27. package/lib/middleware.js +53 -0
  28. package/lib/middleware.js.map +1 -0
  29. package/lib/og-image.js +233 -0
  30. package/lib/og-image.js.map +1 -0
  31. package/lib/rate-limit.js +76 -0
  32. package/lib/rate-limit.js.map +1 -0
  33. package/lib/testing.js +179 -0
  34. package/lib/testing.js.map +1 -0
  35. package/lib/theme.js +11 -2
  36. package/lib/theme.js.map +1 -1
  37. package/lib/types/actions.d.ts +27 -24
  38. package/lib/types/actions.d.ts.map +1 -1
  39. package/lib/types/ai.d.ts +163 -0
  40. package/lib/types/ai.d.ts.map +1 -0
  41. package/lib/types/api-routes.d.ts +37 -33
  42. package/lib/types/api-routes.d.ts.map +1 -1
  43. package/lib/types/cache.d.ts +26 -22
  44. package/lib/types/cache.d.ts.map +1 -1
  45. package/lib/types/client.d.ts +13 -9
  46. package/lib/types/client.d.ts.map +1 -1
  47. package/lib/types/compression.d.ts +14 -10
  48. package/lib/types/compression.d.ts.map +1 -1
  49. package/lib/types/config.d.ts +39 -4
  50. package/lib/types/config.d.ts.map +1 -1
  51. package/lib/types/cors.d.ts +20 -16
  52. package/lib/types/cors.d.ts.map +1 -1
  53. package/lib/types/csp.d.ts +88 -0
  54. package/lib/types/csp.d.ts.map +1 -0
  55. package/lib/types/env.d.ts +118 -0
  56. package/lib/types/env.d.ts.map +1 -0
  57. package/lib/types/favicon.d.ts +70 -24
  58. package/lib/types/favicon.d.ts.map +1 -1
  59. package/lib/types/font.d.ts +68 -65
  60. package/lib/types/font.d.ts.map +1 -1
  61. package/lib/types/i18n-routing.d.ts +43 -37
  62. package/lib/types/i18n-routing.d.ts.map +1 -1
  63. package/lib/types/image-plugin.d.ts +49 -45
  64. package/lib/types/image-plugin.d.ts.map +1 -1
  65. package/lib/types/image.d.ts +47 -36
  66. package/lib/types/image.d.ts.map +1 -1
  67. package/lib/types/index.d.ts +1961 -46
  68. package/lib/types/index.d.ts.map +1 -1
  69. package/lib/types/link.d.ts +61 -56
  70. package/lib/types/link.d.ts.map +1 -1
  71. package/lib/types/logger.d.ts +57 -0
  72. package/lib/types/logger.d.ts.map +1 -0
  73. package/lib/types/meta.d.ts +180 -69
  74. package/lib/types/meta.d.ts.map +1 -1
  75. package/lib/types/middleware.d.ts +8 -4
  76. package/lib/types/middleware.d.ts.map +1 -1
  77. package/lib/types/og-image.d.ts +111 -0
  78. package/lib/types/og-image.d.ts.map +1 -0
  79. package/lib/types/rate-limit.d.ts +20 -16
  80. package/lib/types/rate-limit.d.ts.map +1 -1
  81. package/lib/types/script.d.ts +23 -19
  82. package/lib/types/script.d.ts.map +1 -1
  83. package/lib/types/seo.d.ts +47 -43
  84. package/lib/types/seo.d.ts.map +1 -1
  85. package/lib/types/testing.d.ts +64 -27
  86. package/lib/types/testing.d.ts.map +1 -1
  87. package/lib/types/theme.d.ts +22 -12
  88. package/lib/types/theme.d.ts.map +1 -1
  89. package/package.json +37 -12
  90. package/src/actions.ts +1 -3
  91. package/src/adapters/bun.ts +2 -0
  92. package/src/adapters/cloudflare.ts +84 -0
  93. package/src/adapters/index.ts +13 -1
  94. package/src/adapters/netlify.ts +86 -0
  95. package/src/adapters/node.ts +2 -0
  96. package/src/adapters/validate.ts +16 -0
  97. package/src/adapters/vercel.ts +86 -0
  98. package/src/ai.ts +623 -0
  99. package/src/compression.ts +19 -3
  100. package/src/csp.ts +207 -0
  101. package/src/entry-server.ts +28 -5
  102. package/src/env.ts +344 -0
  103. package/src/favicon.ts +221 -80
  104. package/src/index.ts +42 -2
  105. package/src/link.tsx +6 -0
  106. package/src/logger.ts +144 -0
  107. package/src/meta.tsx +124 -14
  108. package/src/og-image.ts +378 -0
  109. package/src/rate-limit.ts +11 -9
  110. package/src/theme.tsx +12 -1
  111. package/src/types.ts +1 -1
  112. package/src/vite-plugin.ts +5 -1
  113. package/lib/types/adapters/bun.d.ts +0 -6
  114. package/lib/types/adapters/bun.d.ts.map +0 -1
  115. package/lib/types/adapters/index.d.ts +0 -10
  116. package/lib/types/adapters/index.d.ts.map +0 -1
  117. package/lib/types/adapters/node.d.ts +0 -6
  118. package/lib/types/adapters/node.d.ts.map +0 -1
  119. package/lib/types/adapters/static.d.ts +0 -7
  120. package/lib/types/adapters/static.d.ts.map +0 -1
  121. package/lib/types/app.d.ts +0 -24
  122. package/lib/types/app.d.ts.map +0 -1
  123. package/lib/types/entry-server.d.ts +0 -37
  124. package/lib/types/entry-server.d.ts.map +0 -1
  125. package/lib/types/error-overlay.d.ts +0 -6
  126. package/lib/types/error-overlay.d.ts.map +0 -1
  127. package/lib/types/fs-router.d.ts +0 -47
  128. package/lib/types/fs-router.d.ts.map +0 -1
  129. package/lib/types/isr.d.ts +0 -9
  130. package/lib/types/isr.d.ts.map +0 -1
  131. package/lib/types/not-found.d.ts +0 -7
  132. package/lib/types/not-found.d.ts.map +0 -1
  133. package/lib/types/types.d.ts +0 -111
  134. package/lib/types/types.d.ts.map +0 -1
  135. package/lib/types/utils/use-intersection-observer.d.ts +0 -10
  136. package/lib/types/utils/use-intersection-observer.d.ts.map +0 -1
  137. package/lib/types/utils/with-headers.d.ts +0 -6
  138. package/lib/types/utils/with-headers.d.ts.map +0 -1
  139. package/lib/types/vite-plugin.d.ts +0 -17
  140. package/lib/types/vite-plugin.d.ts.map +0 -1
package/lib/link.js CHANGED
@@ -76,9 +76,14 @@ function jsx(type, props, key) {
76
76
 
77
77
  //#endregion
78
78
  //#region src/link.tsx
79
+ const MAX_PREFETCH_CACHE = 200;
79
80
  const prefetched = /* @__PURE__ */ new Set();
80
81
  function doPrefetch(href) {
81
82
  if (prefetched.has(href)) return;
83
+ if (prefetched.size >= MAX_PREFETCH_CACHE) {
84
+ const first = prefetched.values().next().value;
85
+ if (first) prefetched.delete(first);
86
+ }
82
87
  prefetched.add(href);
83
88
  const docLink = document.createElement("link");
84
89
  docLink.rel = "prefetch";
package/lib/link.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"link.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../../../core/core/lib/jsx-runtime.js","../src/link.tsx"],"sourcesContent":["import { onMount, onUnmount } from '@pyreon/core'\n\n/**\n * Observes an element and calls `onIntersect` once it enters the viewport.\n * Automatically disconnects after the first intersection.\n *\n * @param getElement - Getter for the target element (may be undefined before mount).\n * @param onIntersect - Callback fired when the element becomes visible.\n * @param rootMargin - IntersectionObserver rootMargin. Default: \"200px\".\n */\nexport function useIntersectionObserver(\n getElement: () => HTMLElement | undefined,\n onIntersect: () => void,\n rootMargin = '200px',\n) {\n onMount(() => {\n const el = getElement()\n if (!el) return undefined\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n onIntersect()\n observer.disconnect()\n }\n }\n },\n { rootMargin },\n )\n\n observer.observe(el)\n onUnmount(() => observer.disconnect())\n return undefined\n })\n}\n","//#region src/h.ts\n/** Marker for fragment nodes — renders children without a wrapper element */\nconst Fragment = Symbol(\"Pyreon.Fragment\");\n/**\n* Hyperscript function — the compiled output of JSX.\n* `<div class=\"x\">hello</div>` → `h(\"div\", { class: \"x\" }, \"hello\")`\n*\n* Generic on P so TypeScript validates props match the component's signature\n* at the call site, then stores the result in the loosely-typed VNode.\n*/\n/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */\nconst EMPTY_PROPS = {};\nfunction h(type, props, ...children) {\n\treturn {\n\t\ttype,\n\t\tprops: props ?? EMPTY_PROPS,\n\t\tchildren: normalizeChildren(children),\n\t\tkey: props?.key ?? null\n\t};\n}\nfunction normalizeChildren(children) {\n\tfor (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);\n\treturn children;\n}\nfunction flattenChildren(children) {\n\tconst result = [];\n\tfor (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));\n\telse result.push(child);\n\treturn result;\n}\n\n//#endregion\n//#region src/jsx-runtime.ts\n/**\n* JSX automatic runtime.\n*\n* When tsconfig has `\"jsxImportSource\": \"@pyreon/core\"`, the TS/bundler compiler\n* rewrites JSX to imports from this file automatically:\n* <div class=\"x\" /> → jsx(\"div\", { class: \"x\" })\n*/\nfunction jsx(type, props, key) {\n\tconst { children, ...rest } = props;\n\tconst propsWithKey = key != null ? {\n\t\t...rest,\n\t\tkey\n\t} : rest;\n\tif (typeof type === \"function\") return h(type, children !== void 0 ? {\n\t\t...propsWithKey,\n\t\tchildren\n\t} : propsWithKey);\n\treturn h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);\n}\nconst jsxs = jsx;\n\n//#endregion\nexport { Fragment, jsx, jsxs };\n//# sourceMappingURL=jsx-runtime.js.map","import { createRef } from '@pyreon/core'\nimport { useRouter } from '@pyreon/router'\nimport { useIntersectionObserver } from './utils/use-intersection-observer'\n\n// ─── Link component with prefetching ────────────────────────────────────────\n//\n// Provides client-side navigation, prefetching, and active state tracking.\n// Three levels of API:\n//\n// 1. useLink(props) — composable returning handlers, state, and ref callback\n// 2. createLink(Comp) — HOC wrapping any component with link behavior\n// 3. Link — default <a>-based link (built on createLink)\n\nexport interface LinkProps {\n /** Target URL path. */\n href: string\n /** Link content. */\n children?: any\n /** CSS class name. */\n class?: string\n /** Class applied when this link matches the current route. */\n activeClass?: string\n /** Class applied when this link exactly matches the current route. */\n exactActiveClass?: string\n /** Prefetch strategy. Default: \"hover\" */\n prefetch?: 'hover' | 'viewport' | 'none'\n /** Open in new tab. */\n external?: boolean\n /** Inline styles. */\n style?: string\n /** ARIA label. */\n 'aria-label'?: string\n /** Additional click handler — called before navigation. Call e.preventDefault() to cancel. */\n onClick?: ((e: MouseEvent) => void) | undefined\n}\n\n/** Props passed to a custom component via createLink. */\nexport interface LinkRenderProps {\n href: string\n ref: import('@pyreon/core').Ref<HTMLAnchorElement>\n onClick: (e: MouseEvent) => void\n onMouseEnter: () => void\n onTouchStart: () => void\n isActive: () => boolean\n isExactActive: () => boolean\n /** Reactive class string — pass directly to element for auto-updates on route change. */\n class: (() => string) | string | undefined\n style?: string\n target?: string\n rel?: string\n 'aria-label'?: string\n children?: any\n}\n\n/** Return type of useLink. */\nexport interface UseLinkReturn {\n /** Ref object — attach to the root element for viewport-based prefetch. */\n ref: import('@pyreon/core').Ref<HTMLAnchorElement>\n /** Click handler — performs client-side navigation. */\n handleClick: (e: MouseEvent) => void\n /** Mouse enter handler — triggers hover prefetch. */\n handleMouseEnter: () => void\n /** Touch start handler — triggers prefetch on mobile. */\n handleTouchStart: () => void\n /** Whether the link partially matches the current route. */\n isActive: () => boolean\n /** Whether the link exactly matches the current route. */\n isExactActive: () => boolean\n /** Resolved class string including active classes. */\n classes: () => string\n}\n\nconst prefetched = new Set<string>()\n\nfunction doPrefetch(href: string) {\n if (prefetched.has(href)) return\n prefetched.add(href)\n\n const docLink = document.createElement('link')\n docLink.rel = 'prefetch'\n docLink.href = href\n docLink.as = 'document'\n document.head.appendChild(docLink)\n\n try {\n const chunkHint = document.createElement('link')\n chunkHint.rel = 'modulepreload'\n chunkHint.href = href\n document.head.appendChild(chunkHint)\n } catch {\n // modulepreload is a hint, not critical\n }\n}\n\n/**\n * Prefetch a route's JS chunk by injecting `<link rel=\"prefetch\">` into the\n * document head. Deduplicates — calling with the same href twice is a no-op.\n *\n * @example\n * prefetchRoute('/about')\n * prefetchRoute('/dashboard')\n */\nexport function prefetchRoute(href: string): void {\n doPrefetch(href)\n}\n\n/**\n * Composable that provides all link behavior — navigation, prefetching,\n * active state, and viewport observation.\n *\n * Use this for full control when `createLink` is too opinionated.\n *\n * @example\n * function MyLink(props: LinkProps) {\n * const link = useLink(props)\n * return (\n * <button ref={link.ref} class={link.classes()} onClick={link.handleClick}>\n * {props.children}\n * </button>\n * )\n * }\n */\nexport function useLink(props: LinkProps): UseLinkReturn {\n const router = useRouter()\n const elementRef = createRef<HTMLAnchorElement>()\n const strategy = props.prefetch ?? 'hover'\n\n function handleClick(e: MouseEvent) {\n // Call user's onClick first — they may call e.preventDefault()\n if (props.onClick) {\n ;(props.onClick as (e: MouseEvent) => void)(e)\n }\n\n if (\n e.defaultPrevented ||\n e.button !== 0 ||\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n e.altKey ||\n props.external\n ) {\n return\n }\n e.preventDefault()\n router.push(props.href)\n }\n\n function handleMouseEnter() {\n if (strategy === 'hover') {\n doPrefetch(props.href)\n }\n }\n\n function handleTouchStart() {\n if (strategy === 'hover' || strategy === 'viewport') {\n doPrefetch(props.href)\n }\n }\n\n if (strategy === 'viewport') {\n useIntersectionObserver(\n () => elementRef.current ?? undefined,\n () => doPrefetch(props.href),\n )\n }\n\n const isActive = () => {\n const currentPath = router.currentRoute()?.path\n if (!currentPath || !props.href) return false\n if (props.href === '/') return currentPath === '/'\n return currentPath.startsWith(props.href)\n }\n\n const isExactActive = () => {\n const currentPath = router.currentRoute()?.path\n if (!currentPath) return false\n return currentPath === props.href\n }\n\n const classes = () => {\n const cls: string[] = []\n if (props.class) cls.push(props.class)\n if (props.activeClass && isActive()) cls.push(props.activeClass)\n if (props.exactActiveClass && isExactActive()) cls.push(props.exactActiveClass)\n return cls.join(' ')\n }\n\n return {\n ref: elementRef,\n handleClick,\n handleMouseEnter,\n handleTouchStart,\n isActive,\n isExactActive,\n classes,\n }\n}\n\n/**\n * Higher-order component that wraps any component with link behavior.\n *\n * The wrapped component receives {@link LinkRenderProps} with all handlers,\n * active state, and accessibility attributes pre-wired.\n *\n * @example\n * // Custom button link\n * const ButtonLink = createLink((props) => (\n * <button\n * ref={props.ref}\n * class={props.class}\n * onClick={props.onClick}\n * onMouseEnter={props.onMouseEnter}\n * >\n * {props.children}\n * </button>\n * ))\n *\n * // Custom styled component\n * const CardLink = createLink((props) => (\n * <div\n * ref={props.ref}\n * class={`card ${props.isActive() ? \"card--active\" : \"\"}`}\n * onClick={props.onClick}\n * onMouseEnter={props.onMouseEnter}\n * >\n * {props.children}\n * </div>\n * ))\n *\n * // Usage\n * <ButtonLink href=\"/about\">About</ButtonLink>\n * <CardLink href=\"/posts\" prefetch=\"viewport\">Posts</CardLink>\n */\nexport function createLink(Component: (props: LinkRenderProps) => any): (props: LinkProps) => any {\n return function WrappedLink(props: LinkProps) {\n const link = useLink(props)\n\n return (\n <Component\n href={props.href}\n ref={link.ref}\n onClick={link.handleClick}\n onMouseEnter={link.handleMouseEnter}\n onTouchStart={link.handleTouchStart}\n isActive={link.isActive}\n isExactActive={link.isExactActive}\n class={link.classes}\n {...(props.style ? { style: props.style } : {})}\n {...(props.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}\n {...(props['aria-label'] ? { 'aria-label': props['aria-label'] } : {})}\n children={props.children}\n />\n )\n }\n}\n\n/**\n * Default navigation link built on an `<a>` tag.\n *\n * @example\n * <Link href=\"/about\" prefetch=\"viewport\">About</Link>\n * <Link href=\"/posts\" activeClass=\"nav-active\">Posts</Link>\n */\nexport const Link = createLink((props: LinkRenderProps) => (\n <a\n ref={props.ref}\n href={props.href}\n {...(props.class ? { class: props.class } : {})}\n {...(props.style ? { style: props.style } : {})}\n {...(props.target ? { target: props.target } : {})}\n {...(props.rel ? { rel: props.rel } : {})}\n {...(props['aria-label'] ? { 'aria-label': props['aria-label'] } : {})}\n {...(props.isExactActive() ? { 'aria-current': 'page' as const } : {})}\n onClick={props.onClick}\n onMouseEnter={props.onMouseEnter}\n onTouchStart={props.onTouchStart}\n >\n {props.children}\n </a>\n))\n"],"mappings":"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"}
1
+ {"version":3,"file":"link.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../../../core/core/lib/jsx-runtime.js","../src/link.tsx"],"sourcesContent":["import { onMount, onUnmount } from '@pyreon/core'\n\n/**\n * Observes an element and calls `onIntersect` once it enters the viewport.\n * Automatically disconnects after the first intersection.\n *\n * @param getElement - Getter for the target element (may be undefined before mount).\n * @param onIntersect - Callback fired when the element becomes visible.\n * @param rootMargin - IntersectionObserver rootMargin. Default: \"200px\".\n */\nexport function useIntersectionObserver(\n getElement: () => HTMLElement | undefined,\n onIntersect: () => void,\n rootMargin = '200px',\n) {\n onMount(() => {\n const el = getElement()\n if (!el) return undefined\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n onIntersect()\n observer.disconnect()\n }\n }\n },\n { rootMargin },\n )\n\n observer.observe(el)\n onUnmount(() => observer.disconnect())\n return undefined\n })\n}\n","//#region src/h.ts\n/** Marker for fragment nodes — renders children without a wrapper element */\nconst Fragment = Symbol(\"Pyreon.Fragment\");\n/**\n* Hyperscript function — the compiled output of JSX.\n* `<div class=\"x\">hello</div>` → `h(\"div\", { class: \"x\" }, \"hello\")`\n*\n* Generic on P so TypeScript validates props match the component's signature\n* at the call site, then stores the result in the loosely-typed VNode.\n*/\n/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */\nconst EMPTY_PROPS = {};\nfunction h(type, props, ...children) {\n\treturn {\n\t\ttype,\n\t\tprops: props ?? EMPTY_PROPS,\n\t\tchildren: normalizeChildren(children),\n\t\tkey: props?.key ?? null\n\t};\n}\nfunction normalizeChildren(children) {\n\tfor (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);\n\treturn children;\n}\nfunction flattenChildren(children) {\n\tconst result = [];\n\tfor (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));\n\telse result.push(child);\n\treturn result;\n}\n\n//#endregion\n//#region src/jsx-runtime.ts\n/**\n* JSX automatic runtime.\n*\n* When tsconfig has `\"jsxImportSource\": \"@pyreon/core\"`, the TS/bundler compiler\n* rewrites JSX to imports from this file automatically:\n* <div class=\"x\" /> → jsx(\"div\", { class: \"x\" })\n*/\nfunction jsx(type, props, key) {\n\tconst { children, ...rest } = props;\n\tconst propsWithKey = key != null ? {\n\t\t...rest,\n\t\tkey\n\t} : rest;\n\tif (typeof type === \"function\") return h(type, children !== void 0 ? {\n\t\t...propsWithKey,\n\t\tchildren\n\t} : propsWithKey);\n\treturn h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);\n}\nconst jsxs = jsx;\n\n//#endregion\nexport { Fragment, jsx, jsxs };\n//# sourceMappingURL=jsx-runtime.js.map","import { createRef } from '@pyreon/core'\nimport { useRouter } from '@pyreon/router'\nimport { useIntersectionObserver } from './utils/use-intersection-observer'\n\n// ─── Link component with prefetching ────────────────────────────────────────\n//\n// Provides client-side navigation, prefetching, and active state tracking.\n// Three levels of API:\n//\n// 1. useLink(props) — composable returning handlers, state, and ref callback\n// 2. createLink(Comp) — HOC wrapping any component with link behavior\n// 3. Link — default <a>-based link (built on createLink)\n\nexport interface LinkProps {\n /** Target URL path. */\n href: string\n /** Link content. */\n children?: any\n /** CSS class name. */\n class?: string\n /** Class applied when this link matches the current route. */\n activeClass?: string\n /** Class applied when this link exactly matches the current route. */\n exactActiveClass?: string\n /** Prefetch strategy. Default: \"hover\" */\n prefetch?: 'hover' | 'viewport' | 'none'\n /** Open in new tab. */\n external?: boolean\n /** Inline styles. */\n style?: string\n /** ARIA label. */\n 'aria-label'?: string\n /** Additional click handler — called before navigation. Call e.preventDefault() to cancel. */\n onClick?: ((e: MouseEvent) => void) | undefined\n}\n\n/** Props passed to a custom component via createLink. */\nexport interface LinkRenderProps {\n href: string\n ref: import('@pyreon/core').Ref<HTMLAnchorElement>\n onClick: (e: MouseEvent) => void\n onMouseEnter: () => void\n onTouchStart: () => void\n isActive: () => boolean\n isExactActive: () => boolean\n /** Reactive class string — pass directly to element for auto-updates on route change. */\n class: (() => string) | string | undefined\n style?: string\n target?: string\n rel?: string\n 'aria-label'?: string\n children?: any\n}\n\n/** Return type of useLink. */\nexport interface UseLinkReturn {\n /** Ref object — attach to the root element for viewport-based prefetch. */\n ref: import('@pyreon/core').Ref<HTMLAnchorElement>\n /** Click handler — performs client-side navigation. */\n handleClick: (e: MouseEvent) => void\n /** Mouse enter handler — triggers hover prefetch. */\n handleMouseEnter: () => void\n /** Touch start handler — triggers prefetch on mobile. */\n handleTouchStart: () => void\n /** Whether the link partially matches the current route. */\n isActive: () => boolean\n /** Whether the link exactly matches the current route. */\n isExactActive: () => boolean\n /** Resolved class string including active classes. */\n classes: () => string\n}\n\nconst MAX_PREFETCH_CACHE = 200\nconst prefetched = new Set<string>()\n\nfunction doPrefetch(href: string) {\n if (prefetched.has(href)) return\n // Evict oldest entries when cache is full\n if (prefetched.size >= MAX_PREFETCH_CACHE) {\n const first = prefetched.values().next().value\n if (first) prefetched.delete(first)\n }\n prefetched.add(href)\n\n const docLink = document.createElement('link')\n docLink.rel = 'prefetch'\n docLink.href = href\n docLink.as = 'document'\n document.head.appendChild(docLink)\n\n try {\n const chunkHint = document.createElement('link')\n chunkHint.rel = 'modulepreload'\n chunkHint.href = href\n document.head.appendChild(chunkHint)\n } catch {\n // modulepreload is a hint, not critical\n }\n}\n\n/**\n * Prefetch a route's JS chunk by injecting `<link rel=\"prefetch\">` into the\n * document head. Deduplicates — calling with the same href twice is a no-op.\n *\n * @example\n * prefetchRoute('/about')\n * prefetchRoute('/dashboard')\n */\nexport function prefetchRoute(href: string): void {\n doPrefetch(href)\n}\n\n/**\n * Composable that provides all link behavior — navigation, prefetching,\n * active state, and viewport observation.\n *\n * Use this for full control when `createLink` is too opinionated.\n *\n * @example\n * function MyLink(props: LinkProps) {\n * const link = useLink(props)\n * return (\n * <button ref={link.ref} class={link.classes()} onClick={link.handleClick}>\n * {props.children}\n * </button>\n * )\n * }\n */\nexport function useLink(props: LinkProps): UseLinkReturn {\n const router = useRouter()\n const elementRef = createRef<HTMLAnchorElement>()\n const strategy = props.prefetch ?? 'hover'\n\n function handleClick(e: MouseEvent) {\n // Call user's onClick first — they may call e.preventDefault()\n if (props.onClick) {\n ;(props.onClick as (e: MouseEvent) => void)(e)\n }\n\n if (\n e.defaultPrevented ||\n e.button !== 0 ||\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n e.altKey ||\n props.external\n ) {\n return\n }\n e.preventDefault()\n router.push(props.href)\n }\n\n function handleMouseEnter() {\n if (strategy === 'hover') {\n doPrefetch(props.href)\n }\n }\n\n function handleTouchStart() {\n if (strategy === 'hover' || strategy === 'viewport') {\n doPrefetch(props.href)\n }\n }\n\n if (strategy === 'viewport') {\n useIntersectionObserver(\n () => elementRef.current ?? undefined,\n () => doPrefetch(props.href),\n )\n }\n\n const isActive = () => {\n const currentPath = router.currentRoute()?.path\n if (!currentPath || !props.href) return false\n if (props.href === '/') return currentPath === '/'\n return currentPath.startsWith(props.href)\n }\n\n const isExactActive = () => {\n const currentPath = router.currentRoute()?.path\n if (!currentPath) return false\n return currentPath === props.href\n }\n\n const classes = () => {\n const cls: string[] = []\n if (props.class) cls.push(props.class)\n if (props.activeClass && isActive()) cls.push(props.activeClass)\n if (props.exactActiveClass && isExactActive()) cls.push(props.exactActiveClass)\n return cls.join(' ')\n }\n\n return {\n ref: elementRef,\n handleClick,\n handleMouseEnter,\n handleTouchStart,\n isActive,\n isExactActive,\n classes,\n }\n}\n\n/**\n * Higher-order component that wraps any component with link behavior.\n *\n * The wrapped component receives {@link LinkRenderProps} with all handlers,\n * active state, and accessibility attributes pre-wired.\n *\n * @example\n * // Custom button link\n * const ButtonLink = createLink((props) => (\n * <button\n * ref={props.ref}\n * class={props.class}\n * onClick={props.onClick}\n * onMouseEnter={props.onMouseEnter}\n * >\n * {props.children}\n * </button>\n * ))\n *\n * // Custom styled component\n * const CardLink = createLink((props) => (\n * <div\n * ref={props.ref}\n * class={`card ${props.isActive() ? \"card--active\" : \"\"}`}\n * onClick={props.onClick}\n * onMouseEnter={props.onMouseEnter}\n * >\n * {props.children}\n * </div>\n * ))\n *\n * // Usage\n * <ButtonLink href=\"/about\">About</ButtonLink>\n * <CardLink href=\"/posts\" prefetch=\"viewport\">Posts</CardLink>\n */\nexport function createLink(Component: (props: LinkRenderProps) => any): (props: LinkProps) => any {\n return function WrappedLink(props: LinkProps) {\n const link = useLink(props)\n\n return (\n <Component\n href={props.href}\n ref={link.ref}\n onClick={link.handleClick}\n onMouseEnter={link.handleMouseEnter}\n onTouchStart={link.handleTouchStart}\n isActive={link.isActive}\n isExactActive={link.isExactActive}\n class={link.classes}\n {...(props.style ? { style: props.style } : {})}\n {...(props.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}\n {...(props['aria-label'] ? { 'aria-label': props['aria-label'] } : {})}\n children={props.children}\n />\n )\n }\n}\n\n/**\n * Default navigation link built on an `<a>` tag.\n *\n * @example\n * <Link href=\"/about\" prefetch=\"viewport\">About</Link>\n * <Link href=\"/posts\" activeClass=\"nav-active\">Posts</Link>\n */\nexport const Link = createLink((props: LinkRenderProps) => (\n <a\n ref={props.ref}\n href={props.href}\n {...(props.class ? { class: props.class } : {})}\n {...(props.style ? { style: props.style } : {})}\n {...(props.target ? { target: props.target } : {})}\n {...(props.rel ? { rel: props.rel } : {})}\n {...(props['aria-label'] ? { 'aria-label': props['aria-label'] } : {})}\n {...(props.isExactActive() ? { 'aria-current': 'page' as const } : {})}\n onClick={props.onClick}\n onMouseEnter={props.onMouseEnter}\n onTouchStart={props.onTouchStart}\n >\n {props.children}\n </a>\n))\n"],"mappings":"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"}
package/lib/logger.js ADDED
@@ -0,0 +1,78 @@
1
+ //#region src/logger.ts
2
+ const COLORS = {
3
+ reset: "\x1B[0m",
4
+ dim: "\x1B[2m",
5
+ green: "\x1B[32m",
6
+ yellow: "\x1B[33m",
7
+ red: "\x1B[31m",
8
+ cyan: "\x1B[36m",
9
+ magenta: "\x1B[35m"
10
+ };
11
+ function methodColor(method, colors) {
12
+ if (!colors) return method.padEnd(7);
13
+ const padded = method.padEnd(7);
14
+ switch (method) {
15
+ case "GET": return `${COLORS.green}${padded}${COLORS.reset}`;
16
+ case "POST": return `${COLORS.cyan}${padded}${COLORS.reset}`;
17
+ case "PUT": return `${COLORS.yellow}${padded}${COLORS.reset}`;
18
+ case "PATCH": return `${COLORS.yellow}${padded}${COLORS.reset}`;
19
+ case "DELETE": return `${COLORS.red}${padded}${COLORS.reset}`;
20
+ default: return `${COLORS.magenta}${padded}${COLORS.reset}`;
21
+ }
22
+ }
23
+ function defaultFormat(entry, colors) {
24
+ const dur = entry.duration < 1 ? "<1ms" : entry.duration < 1e3 ? `${Math.round(entry.duration)}ms` : `${(entry.duration / 1e3).toFixed(2)}s`;
25
+ const dim = colors ? COLORS.dim : "";
26
+ const reset = colors ? COLORS.reset : "";
27
+ return ` ${methodColor(entry.method, colors)} ${entry.path} ${dim}${dur}${reset}`;
28
+ }
29
+ /**
30
+ * Request logging middleware.
31
+ *
32
+ * Logs incoming requests with method, path, and duration.
33
+ * Runs in middleware phase — logs timing from middleware start to
34
+ * microtask completion (approximate request duration).
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * // Basic usage
39
+ * loggerMiddleware()
40
+ *
41
+ * // Custom format
42
+ * loggerMiddleware({
43
+ * format: (e) => `${e.method} ${e.path} (${e.duration}ms)`,
44
+ * })
45
+ * ```
46
+ */
47
+ function loggerMiddleware(config) {
48
+ if ((config?.level ?? "all") === "none") return () => {};
49
+ const skip = config?.skip ?? [
50
+ "/__",
51
+ "/@",
52
+ "/node_modules"
53
+ ];
54
+ const isDev = typeof process !== "undefined" && process.env.NODE_ENV !== "production";
55
+ const colors = config?.colors ?? isDev;
56
+ return (ctx) => {
57
+ if (skip.some((p) => ctx.path.startsWith(p))) return;
58
+ const start = performance.now();
59
+ const entry = {
60
+ method: ctx.req.method ?? "GET",
61
+ path: ctx.path,
62
+ duration: 0,
63
+ timestamp: /* @__PURE__ */ new Date(),
64
+ userAgent: ctx.req.headers.get("user-agent") ?? void 0
65
+ };
66
+ queueMicrotask(() => {
67
+ entry.duration = performance.now() - start;
68
+ if (config?.format) {
69
+ const line = config.format(entry);
70
+ if (line) console.log(line);
71
+ } else console.log(defaultFormat(entry, colors));
72
+ });
73
+ };
74
+ }
75
+
76
+ //#endregion
77
+ export { loggerMiddleware };
78
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","names":[],"sources":["../src/logger.ts"],"sourcesContent":["/**\n * Request logging middleware.\n *\n * Logs HTTP requests with method, path, status, and duration.\n * Supports custom formatters and log levels.\n *\n * @example\n * ```ts\n * import { loggerMiddleware } from \"@pyreon/zero\"\n *\n * export default defineConfig({\n * middleware: [loggerMiddleware()],\n * })\n * ```\n */\nimport type { Middleware, MiddlewareContext } from '@pyreon/server'\n\nexport interface LoggerConfig {\n /**\n * Log level — controls which requests are logged.\n * - \"all\": log every request\n * - \"none\": disable logging\n * Default: \"all\"\n */\n level?: 'all' | 'none'\n /**\n * Custom log formatter. Receives request details and returns\n * the string to log (or null to skip).\n */\n format?: (entry: LogEntry) => string | null\n /**\n * Skip logging for these path prefixes.\n * Default: [\"/__\", \"/@\", \"/node_modules\"]\n */\n skip?: string[]\n /**\n * Enable colorized output (ANSI codes).\n * Default: true in development, false in production.\n */\n colors?: boolean\n}\n\nexport interface LogEntry {\n method: string\n path: string\n duration: number\n timestamp: Date\n userAgent?: string | undefined\n ip?: string | undefined\n}\n\nconst COLORS = {\n reset: '\\x1b[0m',\n dim: '\\x1b[2m',\n green: '\\x1b[32m',\n yellow: '\\x1b[33m',\n red: '\\x1b[31m',\n cyan: '\\x1b[36m',\n magenta: '\\x1b[35m',\n}\n\nfunction methodColor(method: string, colors: boolean): string {\n if (!colors) return method.padEnd(7)\n const padded = method.padEnd(7)\n switch (method) {\n case 'GET': return `${COLORS.green}${padded}${COLORS.reset}`\n case 'POST': return `${COLORS.cyan}${padded}${COLORS.reset}`\n case 'PUT': return `${COLORS.yellow}${padded}${COLORS.reset}`\n case 'PATCH': return `${COLORS.yellow}${padded}${COLORS.reset}`\n case 'DELETE': return `${COLORS.red}${padded}${COLORS.reset}`\n default: return `${COLORS.magenta}${padded}${COLORS.reset}`\n }\n}\n\nfunction defaultFormat(entry: LogEntry, colors: boolean): string {\n const dur = entry.duration < 1\n ? '<1ms'\n : entry.duration < 1000\n ? `${Math.round(entry.duration)}ms`\n : `${(entry.duration / 1000).toFixed(2)}s`\n\n const dim = colors ? COLORS.dim : ''\n const reset = colors ? COLORS.reset : ''\n\n return ` ${methodColor(entry.method, colors)} ${entry.path} ${dim}${dur}${reset}`\n}\n\n/**\n * Request logging middleware.\n *\n * Logs incoming requests with method, path, and duration.\n * Runs in middleware phase — logs timing from middleware start to\n * microtask completion (approximate request duration).\n *\n * @example\n * ```ts\n * // Basic usage\n * loggerMiddleware()\n *\n * // Custom format\n * loggerMiddleware({\n * format: (e) => `${e.method} ${e.path} (${e.duration}ms)`,\n * })\n * ```\n */\nexport function loggerMiddleware(config?: LoggerConfig): Middleware {\n const level = config?.level ?? 'all'\n if (level === 'none') return () => {}\n\n const skip = config?.skip ?? ['/__', '/@', '/node_modules']\n const isDev = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'\n const colors = config?.colors ?? isDev\n\n return (ctx: MiddlewareContext) => {\n // Skip internal paths\n if (skip.some((p) => ctx.path.startsWith(p))) return\n\n const start = performance.now()\n\n const entry: LogEntry = {\n method: ctx.req.method ?? 'GET',\n path: ctx.path,\n duration: 0,\n timestamp: new Date(),\n userAgent: ctx.req.headers.get('user-agent') ?? undefined,\n }\n\n // Use queueMicrotask to log after the middleware chain completes\n queueMicrotask(() => {\n entry.duration = performance.now() - start\n\n if (config?.format) {\n const line = config.format(entry)\n if (line) {\n // oxlint-disable-next-line no-console\n console.log(line)\n }\n } else {\n // oxlint-disable-next-line no-console\n console.log(defaultFormat(entry, colors))\n }\n })\n }\n}\n"],"mappings":";AAmDA,MAAM,SAAS;CACb,OAAO;CACP,KAAK;CACL,OAAO;CACP,QAAQ;CACR,KAAK;CACL,MAAM;CACN,SAAS;CACV;AAED,SAAS,YAAY,QAAgB,QAAyB;AAC5D,KAAI,CAAC,OAAQ,QAAO,OAAO,OAAO,EAAE;CACpC,MAAM,SAAS,OAAO,OAAO,EAAE;AAC/B,SAAQ,QAAR;EACE,KAAK,MAAO,QAAO,GAAG,OAAO,QAAQ,SAAS,OAAO;EACrD,KAAK,OAAQ,QAAO,GAAG,OAAO,OAAO,SAAS,OAAO;EACrD,KAAK,MAAO,QAAO,GAAG,OAAO,SAAS,SAAS,OAAO;EACtD,KAAK,QAAS,QAAO,GAAG,OAAO,SAAS,SAAS,OAAO;EACxD,KAAK,SAAU,QAAO,GAAG,OAAO,MAAM,SAAS,OAAO;EACtD,QAAS,QAAO,GAAG,OAAO,UAAU,SAAS,OAAO;;;AAIxD,SAAS,cAAc,OAAiB,QAAyB;CAC/D,MAAM,MAAM,MAAM,WAAW,IACzB,SACA,MAAM,WAAW,MACf,GAAG,KAAK,MAAM,MAAM,SAAS,CAAC,MAC9B,IAAI,MAAM,WAAW,KAAM,QAAQ,EAAE,CAAC;CAE5C,MAAM,MAAM,SAAS,OAAO,MAAM;CAClC,MAAM,QAAQ,SAAS,OAAO,QAAQ;AAEtC,QAAO,KAAK,YAAY,MAAM,QAAQ,OAAO,CAAC,GAAG,MAAM,KAAK,GAAG,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;AAqB7E,SAAgB,iBAAiB,QAAmC;AAElE,MADc,QAAQ,SAAS,WACjB,OAAQ,cAAa;CAEnC,MAAM,OAAO,QAAQ,QAAQ;EAAC;EAAO;EAAM;EAAgB;CAC3D,MAAM,QAAQ,OAAO,YAAY,eAAe,QAAQ,IAAI,aAAa;CACzE,MAAM,SAAS,QAAQ,UAAU;AAEjC,SAAQ,QAA2B;AAEjC,MAAI,KAAK,MAAM,MAAM,IAAI,KAAK,WAAW,EAAE,CAAC,CAAE;EAE9C,MAAM,QAAQ,YAAY,KAAK;EAE/B,MAAM,QAAkB;GACtB,QAAQ,IAAI,IAAI,UAAU;GAC1B,MAAM,IAAI;GACV,UAAU;GACV,2BAAW,IAAI,MAAM;GACrB,WAAW,IAAI,IAAI,QAAQ,IAAI,aAAa,IAAI;GACjD;AAGD,uBAAqB;AACnB,SAAM,WAAW,YAAY,KAAK,GAAG;AAErC,OAAI,QAAQ,QAAQ;IAClB,MAAM,OAAO,OAAO,OAAO,MAAM;AACjC,QAAI,KAEF,SAAQ,IAAI,KAAK;SAInB,SAAQ,IAAI,cAAc,OAAO,OAAO,CAAC;IAE3C"}
package/lib/meta.js ADDED
@@ -0,0 +1,336 @@
1
+ import { useHead } from "@pyreon/head";
2
+ import { createContext } from "@pyreon/core";
3
+ import { signal } from "@pyreon/reactivity";
4
+
5
+ //#region src/favicon.ts
6
+ /**
7
+ * Get favicon link tags for a specific locale.
8
+ * Returns link objects suitable for `useHead()` or direct HTML injection.
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
+ * ```
15
+ */
16
+ function faviconLinks(locale, config) {
17
+ const hasLocaleOverride = locale && config.locales?.[locale];
18
+ const prefix = hasLocaleOverride ? `/${locale}` : "";
19
+ const isSvg = (hasLocaleOverride ? config.locales[locale].source : config.source).endsWith(".svg");
20
+ const links = [];
21
+ if (isSvg) links.push({
22
+ rel: "icon",
23
+ type: "image/svg+xml",
24
+ href: `${prefix}/favicon.svg`
25
+ });
26
+ links.push({
27
+ rel: "icon",
28
+ type: "image/png",
29
+ sizes: "32x32",
30
+ href: `${prefix}/favicon-32x32.png`
31
+ }, {
32
+ rel: "icon",
33
+ type: "image/png",
34
+ sizes: "16x16",
35
+ href: `${prefix}/favicon-16x16.png`
36
+ }, {
37
+ rel: "apple-touch-icon",
38
+ sizes: "180x180",
39
+ href: `${prefix}/apple-touch-icon.png`
40
+ });
41
+ if (config.manifest !== false) links.push({
42
+ rel: "manifest",
43
+ href: `${prefix}/site.webmanifest`
44
+ });
45
+ return links;
46
+ }
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
+ function ogImagePath(templateName, locale, outDir = "og", format = "png") {
84
+ const ext = format === "jpeg" ? "jpg" : "png";
85
+ return `/${outDir}/${templateName}${locale ? `-${locale}` : ""}.${ext}`;
86
+ }
87
+
88
+ //#endregion
89
+ //#region src/meta.tsx
90
+ const resolveStr = (v) => typeof v === "function" ? v() : v;
91
+ /**
92
+ * Declarative meta component for SSR-compatible page metadata.
93
+ *
94
+ * Supports reactive title/description — when passed as `() => string` accessors,
95
+ * they are forwarded to `useHead()` as a reactive getter so updates propagate
96
+ * automatically via signal tracking.
97
+ *
98
+ * @example
99
+ * ```tsx
100
+ * <Meta title="My Page" description="..." image="/og.jpg" canonical="https://..." />
101
+ * ```
102
+ *
103
+ * @example Reactive title
104
+ * ```tsx
105
+ * const count = signal(0)
106
+ * <Meta title={() => `${count()} items`} />
107
+ * ```
108
+ */
109
+ function Meta(props) {
110
+ const hasReactiveTitle = typeof props.title === "function";
111
+ const hasReactiveDescription = typeof props.description === "function";
112
+ if (hasReactiveTitle || hasReactiveDescription) useHead(() => {
113
+ const title = resolveStr(props.title);
114
+ const description = resolveStr(props.description);
115
+ const tags = buildMetaTags({
116
+ ...props,
117
+ title,
118
+ description
119
+ });
120
+ const input = {
121
+ meta: tags.meta,
122
+ link: tags.link,
123
+ script: tags.script
124
+ };
125
+ if (title) input.title = title;
126
+ return input;
127
+ });
128
+ else {
129
+ const title = resolveStr(props.title);
130
+ const description = resolveStr(props.description);
131
+ const tags = buildMetaTags({
132
+ ...props,
133
+ title,
134
+ description
135
+ });
136
+ const input = {
137
+ meta: tags.meta,
138
+ link: tags.link,
139
+ script: tags.script
140
+ };
141
+ if (title) input.title = title;
142
+ useHead(input);
143
+ }
144
+ return props.children ?? null;
145
+ }
146
+ function buildMetaTags(props) {
147
+ const meta = [];
148
+ const link = [];
149
+ const script = [];
150
+ const { title, description, canonical, imageAlt, imageWidth, imageHeight, type = "website", siteName, twitterCard = "summary_large_image", twitterSite, twitterCreator, locale = "en_US", alternateLocales, publishedTime, modifiedTime, author, tags, jsonLd, extra, video, videoWidth, videoHeight, audio, favicon, ogTemplate, ogImageDir, ogImageFormat } = props;
151
+ const robots = props.noIndex ? "noindex, nofollow" : props.robots ?? "index, follow";
152
+ const image = props.image ?? (ogTemplate ? ogImagePath(ogTemplate, locale !== "en_US" ? locale : void 0, ogImageDir, ogImageFormat) : void 0);
153
+ const resolvedImageWidth = imageWidth ?? (ogTemplate && !props.image ? 1200 : void 0);
154
+ const resolvedImageHeight = imageHeight ?? (ogTemplate && !props.image ? 630 : void 0);
155
+ if (description) meta.push({
156
+ name: "description",
157
+ content: description
158
+ });
159
+ if (robots) meta.push({
160
+ name: "robots",
161
+ content: robots
162
+ });
163
+ if (author) meta.push({
164
+ name: "author",
165
+ content: author
166
+ });
167
+ if (title) meta.push({
168
+ property: "og:title",
169
+ content: title
170
+ });
171
+ if (description) meta.push({
172
+ property: "og:description",
173
+ content: description
174
+ });
175
+ if (canonical) meta.push({
176
+ property: "og:url",
177
+ content: canonical
178
+ });
179
+ if (image) meta.push({
180
+ property: "og:image",
181
+ content: image
182
+ });
183
+ if (imageAlt) meta.push({
184
+ property: "og:image:alt",
185
+ content: imageAlt
186
+ });
187
+ if (resolvedImageWidth) meta.push({
188
+ property: "og:image:width",
189
+ content: String(resolvedImageWidth)
190
+ });
191
+ if (resolvedImageHeight) meta.push({
192
+ property: "og:image:height",
193
+ content: String(resolvedImageHeight)
194
+ });
195
+ meta.push({
196
+ property: "og:type",
197
+ content: type
198
+ });
199
+ if (siteName) meta.push({
200
+ property: "og:site_name",
201
+ content: siteName
202
+ });
203
+ meta.push({
204
+ property: "og:locale",
205
+ content: locale
206
+ });
207
+ if (video) {
208
+ meta.push({
209
+ property: "og:video",
210
+ content: video
211
+ });
212
+ if (videoWidth) meta.push({
213
+ property: "og:video:width",
214
+ content: String(videoWidth)
215
+ });
216
+ if (videoHeight) meta.push({
217
+ property: "og:video:height",
218
+ content: String(videoHeight)
219
+ });
220
+ if (video.endsWith(".mp4")) meta.push({
221
+ property: "og:video:type",
222
+ content: "video/mp4"
223
+ });
224
+ else if (video.endsWith(".webm")) meta.push({
225
+ property: "og:video:type",
226
+ content: "video/webm"
227
+ });
228
+ }
229
+ if (audio) meta.push({
230
+ property: "og:audio",
231
+ content: audio
232
+ });
233
+ if (type === "article") {
234
+ if (publishedTime) meta.push({
235
+ property: "article:published_time",
236
+ content: publishedTime
237
+ });
238
+ if (modifiedTime) meta.push({
239
+ property: "article:modified_time",
240
+ content: modifiedTime
241
+ });
242
+ if (author) meta.push({
243
+ property: "article:author",
244
+ content: author
245
+ });
246
+ if (tags) for (const tag of tags) meta.push({
247
+ property: "article:tag",
248
+ content: tag
249
+ });
250
+ }
251
+ meta.push({
252
+ name: "twitter:card",
253
+ content: twitterCard
254
+ });
255
+ if (title) meta.push({
256
+ name: "twitter:title",
257
+ content: title
258
+ });
259
+ if (description) meta.push({
260
+ name: "twitter:description",
261
+ content: description
262
+ });
263
+ if (image) meta.push({
264
+ name: "twitter:image",
265
+ content: image
266
+ });
267
+ if (imageAlt) meta.push({
268
+ name: "twitter:image:alt",
269
+ content: imageAlt
270
+ });
271
+ if (twitterSite) meta.push({
272
+ name: "twitter:site",
273
+ content: twitterSite
274
+ });
275
+ if (twitterCreator) meta.push({
276
+ name: "twitter:creator",
277
+ content: twitterCreator
278
+ });
279
+ if (canonical) link.push({
280
+ rel: "canonical",
281
+ href: canonical
282
+ });
283
+ if (alternateLocales) for (const alt of alternateLocales) link.push({
284
+ rel: "alternate",
285
+ hreflang: alt.locale,
286
+ href: alt.url
287
+ });
288
+ if (jsonLd) script.push({
289
+ type: "application/ld+json",
290
+ children: JSON.stringify({
291
+ "@context": "https://schema.org",
292
+ ...jsonLd
293
+ })
294
+ });
295
+ if (extra) for (const tag of extra) meta.push(tag);
296
+ if (props.i18n) {
297
+ const i18nConfig = props.i18n;
298
+ const origin = props.origin ?? "";
299
+ const { pathWithoutLocale } = extractLocaleFromPath(canonical?.replace(origin, "") ?? "/", i18nConfig.locales, i18nConfig.defaultLocale);
300
+ const strategy = i18nConfig.strategy ?? "prefix-except-default";
301
+ for (const loc of i18nConfig.locales) {
302
+ const localizedPath = strategy === "prefix-except-default" && loc === i18nConfig.defaultLocale ? pathWithoutLocale : `/${loc}${pathWithoutLocale === "/" ? "" : pathWithoutLocale}`;
303
+ link.push({
304
+ rel: "alternate",
305
+ hreflang: loc,
306
+ href: `${origin}${localizedPath}`
307
+ });
308
+ if (loc !== locale) meta.push({
309
+ property: "og:locale:alternate",
310
+ content: loc
311
+ });
312
+ }
313
+ link.push({
314
+ rel: "alternate",
315
+ hreflang: "x-default",
316
+ href: `${origin}${pathWithoutLocale}`
317
+ });
318
+ }
319
+ if (favicon) {
320
+ const faviconLocale = locale !== "en_US" ? locale : void 0;
321
+ for (const fl of faviconLinks(faviconLocale, favicon)) link.push(fl);
322
+ if (favicon.themeColor) meta.push({
323
+ name: "theme-color",
324
+ content: favicon.themeColor
325
+ });
326
+ }
327
+ return {
328
+ meta,
329
+ link,
330
+ script
331
+ };
332
+ }
333
+
334
+ //#endregion
335
+ export { Meta, buildMetaTags };
336
+ //# sourceMappingURL=meta.js.map