@pyreon/zero 0.12.2 → 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.
- package/lib/actions.js +97 -0
- package/lib/actions.js.map +1 -0
- package/lib/ai.js +503 -0
- package/lib/ai.js.map +1 -0
- package/lib/api-routes.js +137 -0
- package/lib/api-routes.js.map +1 -0
- package/lib/compression.js +80 -0
- package/lib/compression.js.map +1 -0
- package/lib/cors.js +57 -0
- package/lib/cors.js.map +1 -0
- package/lib/csp.js +119 -0
- package/lib/csp.js.map +1 -0
- package/lib/env.js +217 -0
- package/lib/env.js.map +1 -0
- package/lib/favicon.js +424 -0
- package/lib/favicon.js.map +1 -0
- package/lib/i18n-routing.js +167 -0
- package/lib/i18n-routing.js.map +1 -0
- package/lib/index.js +80 -22
- package/lib/index.js.map +1 -1
- package/lib/link.js +5 -0
- package/lib/link.js.map +1 -1
- package/lib/logger.js +78 -0
- package/lib/logger.js.map +1 -0
- package/lib/meta.js +336 -0
- package/lib/meta.js.map +1 -0
- package/lib/middleware.js +53 -0
- package/lib/middleware.js.map +1 -0
- package/lib/og-image.js +233 -0
- package/lib/og-image.js.map +1 -0
- package/lib/rate-limit.js +76 -0
- package/lib/rate-limit.js.map +1 -0
- package/lib/testing.js +179 -0
- package/lib/testing.js.map +1 -0
- package/lib/theme.js +11 -2
- package/lib/theme.js.map +1 -1
- package/lib/types/actions.d.ts +27 -24
- package/lib/types/actions.d.ts.map +1 -1
- package/lib/types/ai.d.ts +76 -95
- package/lib/types/ai.d.ts.map +1 -1
- package/lib/types/api-routes.d.ts +37 -33
- package/lib/types/api-routes.d.ts.map +1 -1
- package/lib/types/cache.d.ts +26 -22
- package/lib/types/cache.d.ts.map +1 -1
- package/lib/types/client.d.ts +13 -9
- package/lib/types/client.d.ts.map +1 -1
- package/lib/types/compression.d.ts +14 -10
- package/lib/types/compression.d.ts.map +1 -1
- package/lib/types/config.d.ts +39 -4
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/cors.d.ts +20 -16
- package/lib/types/cors.d.ts.map +1 -1
- package/lib/types/csp.d.ts +42 -61
- package/lib/types/csp.d.ts.map +1 -1
- package/lib/types/env.d.ts +26 -26
- package/lib/types/env.d.ts.map +1 -1
- package/lib/types/favicon.d.ts +58 -54
- package/lib/types/favicon.d.ts.map +1 -1
- package/lib/types/font.d.ts +68 -65
- package/lib/types/font.d.ts.map +1 -1
- package/lib/types/i18n-routing.d.ts +43 -37
- package/lib/types/i18n-routing.d.ts.map +1 -1
- package/lib/types/image-plugin.d.ts +49 -45
- package/lib/types/image-plugin.d.ts.map +1 -1
- package/lib/types/image.d.ts +47 -36
- package/lib/types/image.d.ts.map +1 -1
- package/lib/types/index.d.ts +1961 -56
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/link.d.ts +61 -56
- package/lib/types/link.d.ts.map +1 -1
- package/lib/types/logger.d.ts +37 -48
- package/lib/types/logger.d.ts.map +1 -1
- package/lib/types/meta.d.ts +180 -105
- package/lib/types/meta.d.ts.map +1 -1
- package/lib/types/middleware.d.ts +8 -4
- package/lib/types/middleware.d.ts.map +1 -1
- package/lib/types/og-image.d.ts +63 -59
- package/lib/types/og-image.d.ts.map +1 -1
- package/lib/types/rate-limit.d.ts +20 -16
- package/lib/types/rate-limit.d.ts.map +1 -1
- package/lib/types/script.d.ts +23 -19
- package/lib/types/script.d.ts.map +1 -1
- package/lib/types/seo.d.ts +47 -43
- package/lib/types/seo.d.ts.map +1 -1
- package/lib/types/testing.d.ts +64 -27
- package/lib/types/testing.d.ts.map +1 -1
- package/lib/types/theme.d.ts +22 -12
- package/lib/types/theme.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/actions.ts +1 -3
- package/src/adapters/bun.ts +2 -0
- package/src/adapters/cloudflare.ts +2 -0
- package/src/adapters/netlify.ts +2 -0
- package/src/adapters/node.ts +2 -0
- package/src/adapters/validate.ts +16 -0
- package/src/adapters/vercel.ts +2 -0
- package/src/compression.ts +19 -3
- package/src/entry-server.ts +28 -5
- package/src/index.ts +1 -0
- package/src/link.tsx +6 -0
- package/src/meta.tsx +41 -13
- package/src/rate-limit.ts +11 -9
- package/src/theme.tsx +12 -1
- package/src/vite-plugin.ts +5 -1
- package/lib/types/adapters/bun.d.ts +0 -6
- package/lib/types/adapters/bun.d.ts.map +0 -1
- package/lib/types/adapters/cloudflare.d.ts +0 -26
- package/lib/types/adapters/cloudflare.d.ts.map +0 -1
- package/lib/types/adapters/index.d.ts +0 -13
- package/lib/types/adapters/index.d.ts.map +0 -1
- package/lib/types/adapters/netlify.d.ts +0 -21
- package/lib/types/adapters/netlify.d.ts.map +0 -1
- package/lib/types/adapters/node.d.ts +0 -6
- package/lib/types/adapters/node.d.ts.map +0 -1
- package/lib/types/adapters/static.d.ts +0 -7
- package/lib/types/adapters/static.d.ts.map +0 -1
- package/lib/types/adapters/vercel.d.ts +0 -21
- package/lib/types/adapters/vercel.d.ts.map +0 -1
- package/lib/types/app.d.ts +0 -24
- package/lib/types/app.d.ts.map +0 -1
- package/lib/types/entry-server.d.ts +0 -37
- package/lib/types/entry-server.d.ts.map +0 -1
- package/lib/types/error-overlay.d.ts +0 -6
- package/lib/types/error-overlay.d.ts.map +0 -1
- package/lib/types/fs-router.d.ts +0 -47
- package/lib/types/fs-router.d.ts.map +0 -1
- package/lib/types/isr.d.ts +0 -9
- package/lib/types/isr.d.ts.map +0 -1
- package/lib/types/not-found.d.ts +0 -7
- package/lib/types/not-found.d.ts.map +0 -1
- package/lib/types/types.d.ts +0 -111
- package/lib/types/types.d.ts.map +0 -1
- package/lib/types/utils/use-intersection-observer.d.ts +0 -10
- package/lib/types/utils/use-intersection-observer.d.ts.map +0 -1
- package/lib/types/utils/with-headers.d.ts +0 -6
- package/lib/types/utils/with-headers.d.ts.map +0 -1
- package/lib/types/vite-plugin.d.ts +0 -17
- 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
|