@flyo/nitro-next 2.1.0 → 2.2.1

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/dist/client.d.mts CHANGED
@@ -34,12 +34,35 @@ interface FlyoLanguageLink {
34
34
  * Pure — no React or server-only APIs, so it is safe to call from server *or*
35
35
  * client components.
36
36
  *
37
+ * This is a pure *data* helper — it maps a `translation[]` to switcher links and
38
+ * renders nothing. **The Flyo route helpers already call it for you and publish
39
+ * the result**, so page and entity routes need no switcher code at all; the
40
+ * footer just reads the store (see `publishLanguageLinks` / `readLanguageLinks`
41
+ * in `@flyo/nitro-next/server`). Call this directly only when you publish links
42
+ * by hand from a raw `translation[]` — e.g. on a custom route Flyo doesn't
43
+ * resolve:
44
+ *
45
+ * ```tsx
46
+ * getLanguageLinks(translations, { currentLang, locales });
47
+ * ```
48
+ *
49
+ * **Render each link as a native `<a>`, not `next/link`'s `<Link>`.** A language
50
+ * switch must refresh the shared chrome — the localized nav, footer and
51
+ * `<html lang>` — that lives in the root layout. In the Next.js App Router, soft
52
+ * (client-side) navigation re-renders only the page segment, *not* shared
53
+ * layouts, so a `<Link>` would leave that chrome in the previous language while
54
+ * only the page body updates. A plain `<a>` triggers a full-document navigation,
55
+ * forcing a fresh server render in the new locale so every part updates. (Regular
56
+ * nav links can stay `<Link>` — the nav is identical within a language.)
57
+ *
37
58
  * @example
38
59
  * ```tsx
39
60
  * const links = getLanguageLinks(page.translation, {
40
61
  * currentLang: lang,
41
62
  * locales: flyo.state.locales,
42
63
  * });
64
+ * // Native <a> (full-document nav) — NOT next/link, so shared layout chrome
65
+ * // re-renders in the new locale.
43
66
  * // links.map(l => l.exists
44
67
  * // ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>
45
68
  * // : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>)
package/dist/client.d.ts CHANGED
@@ -34,12 +34,35 @@ interface FlyoLanguageLink {
34
34
  * Pure — no React or server-only APIs, so it is safe to call from server *or*
35
35
  * client components.
36
36
  *
37
+ * This is a pure *data* helper — it maps a `translation[]` to switcher links and
38
+ * renders nothing. **The Flyo route helpers already call it for you and publish
39
+ * the result**, so page and entity routes need no switcher code at all; the
40
+ * footer just reads the store (see `publishLanguageLinks` / `readLanguageLinks`
41
+ * in `@flyo/nitro-next/server`). Call this directly only when you publish links
42
+ * by hand from a raw `translation[]` — e.g. on a custom route Flyo doesn't
43
+ * resolve:
44
+ *
45
+ * ```tsx
46
+ * getLanguageLinks(translations, { currentLang, locales });
47
+ * ```
48
+ *
49
+ * **Render each link as a native `<a>`, not `next/link`'s `<Link>`.** A language
50
+ * switch must refresh the shared chrome — the localized nav, footer and
51
+ * `<html lang>` — that lives in the root layout. In the Next.js App Router, soft
52
+ * (client-side) navigation re-renders only the page segment, *not* shared
53
+ * layouts, so a `<Link>` would leave that chrome in the previous language while
54
+ * only the page body updates. A plain `<a>` triggers a full-document navigation,
55
+ * forcing a fresh server render in the new locale so every part updates. (Regular
56
+ * nav links can stay `<Link>` — the nav is identical within a language.)
57
+ *
37
58
  * @example
38
59
  * ```tsx
39
60
  * const links = getLanguageLinks(page.translation, {
40
61
  * currentLang: lang,
41
62
  * locales: flyo.state.locales,
42
63
  * });
64
+ * // Native <a> (full-document nav) — NOT next/link, so shared layout chrome
65
+ * // re-renders in the new locale.
43
66
  * // links.map(l => l.exists
44
67
  * // ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>
45
68
  * // : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>)
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.tsx","../src/i18n.ts"],"sourcesContent":["'use client';\n\nimport { useEffect } from 'react';\nimport { highlightAndClick, wysiwyg, reload, scrollTo} from '@flyo/nitro-js-bridge';\nimport { Block, Entity } from \"@flyo/nitro-typescript\";\nimport type { ImageLoaderProps } from 'next/image';\n\n// Framework-agnostic language-links helper — re-exported here so client\n// components can build a language switcher from a page/entity `translation[]`\n// without importing from `/server` (which would pull server-only code into the\n// client bundle).\nexport { getLanguageLinks } from './i18n';\nexport type { FlyoLanguageLink } from './i18n';\n\nconst FLYO_CDN_HOST = 'storage.flyo.cloud';\n\n/**\n * Check if running in production environment\n */\nexport const isProd = process.env.NODE_ENV === 'production';\n\n/**\n * Type for WYSIWYG node structure\n */\nexport interface WysiwygNode {\n type: string;\n content?: WysiwygNode[];\n [key: string]: unknown;\n}\n\n/**\n * Type for WYSIWYG JSON that can be a node, array of nodes, or doc structure\n */\nexport type WysiwygJson = WysiwygNode | WysiwygNode[] | { type: 'doc'; content: WysiwygNode[] };\n\n/**\n * The minimal block shape `editable()` needs to wire live-editing.\n *\n * `editable()` reads only `uid`, so it deliberately accepts more than the full\n * {@link Block}. The per-block types generated from a project's OpenAPI schema\n * (e.g. `BlockHero`) are NOT structurally assignable to `Block`: their\n * `content`/`config`/`slots` carry an `_empty` marker that clashes with\n * `Block`'s index signatures (`{ [key: string]: BlockSlotValue }`). Typing\n * against the read-surface keeps both the generic `Block` and those generated\n * subtypes assignable, so callers don't need `as unknown as Block` casts.\n */\nexport type EditableBlock = Pick<Block, 'uid'>;\n\n/**\n * Helper function to get editable props\n */\nexport function editable(block: EditableBlock): { 'data-flyo-uid'?: string } {\n if (typeof block.uid === 'string' && block.uid.trim() !== '') {\n return { 'data-flyo-uid': block.uid };\n }\n return {};\n}\n\n/**\n * Internal client component that sets up live editing functionality\n */\nexport function FlyoClientWrapper({ \n children,\n}: { \n children: React.ReactNode;\n}) {\n useEffect(() => {\n if (typeof window === 'undefined') {\n return;\n }\n\n reload();\n \n scrollTo();\n\n const wireAll = () => {\n const elements = document.querySelectorAll('[data-flyo-uid]');\n elements.forEach((el) => {\n const uid = el.getAttribute('data-flyo-uid');\n if (uid && el instanceof HTMLElement) {\n highlightAndClick(uid, el);\n }\n });\n };\n\n wireAll();\n\n const observer = new MutationObserver((mutations) => {\n const hasRelevantChanges = mutations.some(mutation => \n Array.from(mutation.addedNodes).some(node => {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as Element;\n return element.hasAttribute('data-flyo-uid') || \n element.querySelector('[data-flyo-uid]');\n }\n return false;\n })\n );\n\n if (hasRelevantChanges) {\n wireAll();\n }\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n });\n\n return () => {\n observer.disconnect();\n };\n }, []);\n\n return <>{children}</>;\n}\n\n/**\n * WYSIWYG component for rendering ProseMirror/TipTap JSON content\n * \n * Uses the `wysiwyg()` function from `@flyo/nitro-js-bridge` to convert\n * nodes to HTML. All consecutive non-custom nodes are joined into a single\n * HTML string so no extra wrapper `<div>` elements are added around each node.\n * \n * The component wraps all output in a single `<div>` with an optional\n * `className` (defaults to `\"wysiwyg\"`).\n * \n * @example\n * ```tsx\n * import { FlyoWysiwyg } from '@flyo/nitro-next/client';\n * import CustomImage from './CustomImage';\n * \n * export default function MyComponent({ block }) {\n * return (\n * <FlyoWysiwyg \n * json={block.content.json} \n * className=\"wysiwyg\"\n * components={{\n * image: CustomImage\n * }} \n * />\n * );\n * }\n * ```\n */\nexport function FlyoWysiwyg({\n json,\n className = 'wysiwyg',\n components = {},\n}: {\n json: WysiwygJson;\n className?: string;\n components?: Record<string, React.ComponentType<{ node: WysiwygNode }>>;\n}) {\n let nodes: WysiwygNode[] = [];\n\n if (json) {\n if (Array.isArray(json)) {\n nodes = json;\n } else if ('type' in json && json.type === 'doc' && Array.isArray(json.content)) {\n nodes = json.content;\n } else {\n nodes = [json as WysiwygNode];\n }\n }\n\n // If no custom components are provided, render all nodes as a single HTML block\n const hasCustomComponents = nodes.some((node) => components[node.type]);\n\n if (!hasCustomComponents) {\n const html = nodes.map((node) => wysiwyg(node)).join('');\n return <div className={className} dangerouslySetInnerHTML={{ __html: html }} />;\n }\n\n // When custom components are used, group consecutive non-custom nodes\n // into single HTML blocks to avoid extra wrapper elements.\n const groups: ({ type: 'custom'; component: React.ComponentType<{ node: WysiwygNode }>; node: WysiwygNode } | { type: 'html'; html: string })[] = [];\n\n for (const node of nodes) {\n const Component = components[node.type];\n if (Component) {\n groups.push({ type: 'custom', component: Component, node });\n } else {\n const html = wysiwyg(node);\n const last = groups[groups.length - 1];\n if (last && last.type === 'html') {\n last.html += html;\n } else {\n groups.push({ type: 'html', html });\n }\n }\n }\n\n return (\n <div className={className}>\n {groups.map((group, index) => {\n if (group.type === 'custom') {\n return <group.component key={index} node={group.node} />;\n }\n return <div key={index} dangerouslySetInnerHTML={{ __html: group.html }} />;\n })}\n </div>\n );\n}\n\n/**\n * Image loader for Flyo CDN that automatically handles image transformations.\n * Adds Flyo CDN host if not already present and applies width transformations.\n * \n * @param src - The image source URL (relative or absolute)\n * @param width - The desired width for the image\n * @returns Transformed image URL with Flyo CDN parameters\n * \n * @example\n * ```tsx\n * <Image\n * loader={FlyoCdnLoader}\n * src=\"me.png\"\n * alt=\"Picture\"\n * width={500}\n * height={500}\n * />\n * ```\n */\nexport function FlyoCdnLoader({ src, width }: ImageLoaderProps): string {\n let imageUrl = src;\n\n // If src doesn't contain the Flyo CDN host, prefix it\n if (!src.includes(FLYO_CDN_HOST)) {\n // Remove leading slash if present to avoid double slashes\n const cleanSrc = src.startsWith('/') ? src.slice(1) : src;\n imageUrl = `https://${FLYO_CDN_HOST}/${cleanSrc}`;\n }\n\n // Append Flyo CDN transformation parameters\n return `${imageUrl}/thumb/${width}xnull?format=webp`;\n}\n\n/**\n * FlyoMetric component for tracking entity metrics in production\n * \n * Automatically sends a metric tracking request to the Flyo API when:\n * - The environment is production (NODE_ENV === 'production')\n * - The entity has a metric API URL configured\n * \n * @param entity - The entity object containing entity_metric.api\n * \n * @example\n * ```tsx\n * import { FlyoMetric } from '@flyo/nitro-next/client';\n * \n * export default function BlogPost(props: RouteParams) {\n * return nitroEntityRoute(props, {\n * resolver,\n * render: (entity: Entity) => (\n * <>\n * <FlyoMetric entity={entity} />\n * <article>\n * <h1>{entity.entity?.entity_title}</h1>\n * </article>\n * </>\n * )\n * });\n * }\n * ```\n */\nexport function FlyoMetric({ entity }: { entity: Entity }) {\n useEffect(() => {\n // Only track metrics in production and if API URL is available\n if (isProd && entity?.entity?.entity_metric?.api) {\n fetch(entity.entity.entity_metric.api);\n }\n }, [entity]);\n\n // This component doesn't render anything\n return null;\n}\n\n/**\n * A thin client wrapper that applies `editable()` to a root element while\n * allowing server-rendered children (e.g. `NitroSlot`) to be passed in.\n *\n * In Next.js, a file marked `'use client'` turns all of its imports into\n * client modules, so you cannot import server-only components like\n * `NitroSlot` directly. The workaround is to keep the server part separate\n * and pass it into this client wrapper via `children`.\n *\n * @example\n * ```tsx\n * // components/HeroBanner.tsx (server component – no 'use client')\n * import { Block } from '@flyo/nitro-typescript';\n * import { NitroSlot } from '@flyo/nitro-next/server';\n * import { EditableSection } from '@flyo/nitro-next/client';\n *\n * export function HeroBanner({ block }: { block: Block }) {\n * return (\n * <EditableSection block={block} className=\"hero\">\n * <h2>{block?.content?.title}</h2>\n * <NitroSlot slot={block.slots?.content} />\n * </EditableSection>\n * );\n * }\n * ```\n */\nexport function EditableSection({\n block,\n children,\n className,\n as: Tag = 'section',\n}: {\n block: EditableBlock;\n children: React.ReactNode;\n className?: string;\n as?: React.ElementType;\n}) {\n return (\n <Tag {...editable(block)} className={className}>\n {children}\n </Tag>\n );\n}\n","import type { Translation } from '@flyo/nitro-typescript';\n\n/**\n * A single language option for a language switcher, derived from a page's or\n * entity's `translation[]`. Framework-agnostic plain data — render it however\n * you like.\n */\nexport interface FlyoLanguageLink {\n /** Locale shortcode, e.g. `\"de\"`. */\n shortcode: string;\n /** Full language name from the CMS translation, e.g. `\"Deutsch\"` (when available). */\n name?: string;\n /** Fully-resolved localized href, or `null` when this locale has no linked translation. */\n href: string | null;\n /** Localized page/entity title, when available. */\n title?: string;\n /** `true` when this entry is the currently active locale. */\n isCurrent: boolean;\n /** `true` when a linked translation actually exists for this locale. */\n exists: boolean;\n}\n\n/**\n * Map a page's or entity's `translation[]` into a switcher-ready array of typed\n * links — the building block for a language switcher.\n *\n * Flyo only returns `translation` entries for languages that actually have a\n * translation. Pass `options.locales` (e.g. `flyo.state.locales`) to also get an\n * entry for every configured locale that is *missing* a translation — those come\n * back as `{ href: null, exists: false }` so you can render a fallback (a disabled\n * item, a link to the home page, …).\n *\n * Pure — no React or server-only APIs, so it is safe to call from server *or*\n * client components.\n *\n * @example\n * ```tsx\n * const links = getLanguageLinks(page.translation, {\n * currentLang: lang,\n * locales: flyo.state.locales,\n * });\n * // links.map(l => l.exists\n * // ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>\n * // : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>)\n * ```\n */\nexport function getLanguageLinks(\n translations: Translation[] | undefined,\n options?: { currentLang?: string; locales?: string[] },\n): FlyoLanguageLink[] {\n const currentLang = options?.currentLang;\n\n const byShortcode = new Map<string, Translation>();\n for (const t of translations ?? []) {\n const shortcode = t.language?.shortcode;\n if (shortcode) {\n byShortcode.set(shortcode, t);\n }\n }\n\n const toLink = (shortcode: string, t: Translation | undefined): FlyoLanguageLink => ({\n shortcode,\n name: t?.language?.name,\n href: t?.href ?? null,\n title: t?.title,\n isCurrent: shortcode === currentLang,\n exists: t?.href != null,\n });\n\n // When the full set of locales is known, emit an entry for every one so\n // callers can render fallbacks for languages that have no translation.\n if (options?.locales && options.locales.length > 0) {\n return options.locales.map((shortcode) => toLink(shortcode, byShortcode.get(shortcode)));\n }\n\n // Otherwise, just surface the translations that exist.\n return (translations ?? [])\n .filter((t) => t.language?.shortcode)\n .map((t) => toLink(t.language!.shortcode!, t));\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/client.tsx","../src/i18n.ts"],"sourcesContent":["'use client';\n\nimport { useEffect } from 'react';\nimport { highlightAndClick, wysiwyg, reload, scrollTo} from '@flyo/nitro-js-bridge';\nimport { Block, Entity } from \"@flyo/nitro-typescript\";\nimport type { ImageLoaderProps } from 'next/image';\n\n// Framework-agnostic language-links helper — re-exported here so client\n// components can build a language switcher from a page/entity `translation[]`\n// without importing from `/server` (which would pull server-only code into the\n// client bundle).\nexport { getLanguageLinks } from './i18n';\nexport type { FlyoLanguageLink } from './i18n';\n\nconst FLYO_CDN_HOST = 'storage.flyo.cloud';\n\n/**\n * Check if running in production environment\n */\nexport const isProd = process.env.NODE_ENV === 'production';\n\n/**\n * Type for WYSIWYG node structure\n */\nexport interface WysiwygNode {\n type: string;\n content?: WysiwygNode[];\n [key: string]: unknown;\n}\n\n/**\n * Type for WYSIWYG JSON that can be a node, array of nodes, or doc structure\n */\nexport type WysiwygJson = WysiwygNode | WysiwygNode[] | { type: 'doc'; content: WysiwygNode[] };\n\n/**\n * The minimal block shape `editable()` needs to wire live-editing.\n *\n * `editable()` reads only `uid`, so it deliberately accepts more than the full\n * {@link Block}. The per-block types generated from a project's OpenAPI schema\n * (e.g. `BlockHero`) are NOT structurally assignable to `Block`: their\n * `content`/`config`/`slots` carry an `_empty` marker that clashes with\n * `Block`'s index signatures (`{ [key: string]: BlockSlotValue }`). Typing\n * against the read-surface keeps both the generic `Block` and those generated\n * subtypes assignable, so callers don't need `as unknown as Block` casts.\n */\nexport type EditableBlock = Pick<Block, 'uid'>;\n\n/**\n * Helper function to get editable props\n */\nexport function editable(block: EditableBlock): { 'data-flyo-uid'?: string } {\n if (typeof block.uid === 'string' && block.uid.trim() !== '') {\n return { 'data-flyo-uid': block.uid };\n }\n return {};\n}\n\n/**\n * Internal client component that sets up live editing functionality\n */\nexport function FlyoClientWrapper({ \n children,\n}: { \n children: React.ReactNode;\n}) {\n useEffect(() => {\n if (typeof window === 'undefined') {\n return;\n }\n\n reload();\n \n scrollTo();\n\n const wireAll = () => {\n const elements = document.querySelectorAll('[data-flyo-uid]');\n elements.forEach((el) => {\n const uid = el.getAttribute('data-flyo-uid');\n if (uid && el instanceof HTMLElement) {\n highlightAndClick(uid, el);\n }\n });\n };\n\n wireAll();\n\n const observer = new MutationObserver((mutations) => {\n const hasRelevantChanges = mutations.some(mutation => \n Array.from(mutation.addedNodes).some(node => {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as Element;\n return element.hasAttribute('data-flyo-uid') || \n element.querySelector('[data-flyo-uid]');\n }\n return false;\n })\n );\n\n if (hasRelevantChanges) {\n wireAll();\n }\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n });\n\n return () => {\n observer.disconnect();\n };\n }, []);\n\n return <>{children}</>;\n}\n\n/**\n * WYSIWYG component for rendering ProseMirror/TipTap JSON content\n * \n * Uses the `wysiwyg()` function from `@flyo/nitro-js-bridge` to convert\n * nodes to HTML. All consecutive non-custom nodes are joined into a single\n * HTML string so no extra wrapper `<div>` elements are added around each node.\n * \n * The component wraps all output in a single `<div>` with an optional\n * `className` (defaults to `\"wysiwyg\"`).\n * \n * @example\n * ```tsx\n * import { FlyoWysiwyg } from '@flyo/nitro-next/client';\n * import CustomImage from './CustomImage';\n * \n * export default function MyComponent({ block }) {\n * return (\n * <FlyoWysiwyg \n * json={block.content.json} \n * className=\"wysiwyg\"\n * components={{\n * image: CustomImage\n * }} \n * />\n * );\n * }\n * ```\n */\nexport function FlyoWysiwyg({\n json,\n className = 'wysiwyg',\n components = {},\n}: {\n json: WysiwygJson;\n className?: string;\n components?: Record<string, React.ComponentType<{ node: WysiwygNode }>>;\n}) {\n let nodes: WysiwygNode[] = [];\n\n if (json) {\n if (Array.isArray(json)) {\n nodes = json;\n } else if ('type' in json && json.type === 'doc' && Array.isArray(json.content)) {\n nodes = json.content;\n } else {\n nodes = [json as WysiwygNode];\n }\n }\n\n // If no custom components are provided, render all nodes as a single HTML block\n const hasCustomComponents = nodes.some((node) => components[node.type]);\n\n if (!hasCustomComponents) {\n const html = nodes.map((node) => wysiwyg(node)).join('');\n return <div className={className} dangerouslySetInnerHTML={{ __html: html }} />;\n }\n\n // When custom components are used, group consecutive non-custom nodes\n // into single HTML blocks to avoid extra wrapper elements.\n const groups: ({ type: 'custom'; component: React.ComponentType<{ node: WysiwygNode }>; node: WysiwygNode } | { type: 'html'; html: string })[] = [];\n\n for (const node of nodes) {\n const Component = components[node.type];\n if (Component) {\n groups.push({ type: 'custom', component: Component, node });\n } else {\n const html = wysiwyg(node);\n const last = groups[groups.length - 1];\n if (last && last.type === 'html') {\n last.html += html;\n } else {\n groups.push({ type: 'html', html });\n }\n }\n }\n\n return (\n <div className={className}>\n {groups.map((group, index) => {\n if (group.type === 'custom') {\n return <group.component key={index} node={group.node} />;\n }\n return <div key={index} dangerouslySetInnerHTML={{ __html: group.html }} />;\n })}\n </div>\n );\n}\n\n/**\n * Image loader for Flyo CDN that automatically handles image transformations.\n * Adds Flyo CDN host if not already present and applies width transformations.\n * \n * @param src - The image source URL (relative or absolute)\n * @param width - The desired width for the image\n * @returns Transformed image URL with Flyo CDN parameters\n * \n * @example\n * ```tsx\n * <Image\n * loader={FlyoCdnLoader}\n * src=\"me.png\"\n * alt=\"Picture\"\n * width={500}\n * height={500}\n * />\n * ```\n */\nexport function FlyoCdnLoader({ src, width }: ImageLoaderProps): string {\n let imageUrl = src;\n\n // If src doesn't contain the Flyo CDN host, prefix it\n if (!src.includes(FLYO_CDN_HOST)) {\n // Remove leading slash if present to avoid double slashes\n const cleanSrc = src.startsWith('/') ? src.slice(1) : src;\n imageUrl = `https://${FLYO_CDN_HOST}/${cleanSrc}`;\n }\n\n // Append Flyo CDN transformation parameters\n return `${imageUrl}/thumb/${width}xnull?format=webp`;\n}\n\n/**\n * FlyoMetric component for tracking entity metrics in production\n * \n * Automatically sends a metric tracking request to the Flyo API when:\n * - The environment is production (NODE_ENV === 'production')\n * - The entity has a metric API URL configured\n * \n * @param entity - The entity object containing entity_metric.api\n * \n * @example\n * ```tsx\n * import { FlyoMetric } from '@flyo/nitro-next/client';\n * \n * export default function BlogPost(props: RouteParams) {\n * return nitroEntityRoute(props, {\n * resolver,\n * render: (entity: Entity) => (\n * <>\n * <FlyoMetric entity={entity} />\n * <article>\n * <h1>{entity.entity?.entity_title}</h1>\n * </article>\n * </>\n * )\n * });\n * }\n * ```\n */\nexport function FlyoMetric({ entity }: { entity: Entity }) {\n useEffect(() => {\n // Only track metrics in production and if API URL is available\n if (isProd && entity?.entity?.entity_metric?.api) {\n fetch(entity.entity.entity_metric.api);\n }\n }, [entity]);\n\n // This component doesn't render anything\n return null;\n}\n\n/**\n * A thin client wrapper that applies `editable()` to a root element while\n * allowing server-rendered children (e.g. `NitroSlot`) to be passed in.\n *\n * In Next.js, a file marked `'use client'` turns all of its imports into\n * client modules, so you cannot import server-only components like\n * `NitroSlot` directly. The workaround is to keep the server part separate\n * and pass it into this client wrapper via `children`.\n *\n * @example\n * ```tsx\n * // components/HeroBanner.tsx (server component – no 'use client')\n * import { Block } from '@flyo/nitro-typescript';\n * import { NitroSlot } from '@flyo/nitro-next/server';\n * import { EditableSection } from '@flyo/nitro-next/client';\n *\n * export function HeroBanner({ block }: { block: Block }) {\n * return (\n * <EditableSection block={block} className=\"hero\">\n * <h2>{block?.content?.title}</h2>\n * <NitroSlot slot={block.slots?.content} />\n * </EditableSection>\n * );\n * }\n * ```\n */\nexport function EditableSection({\n block,\n children,\n className,\n as: Tag = 'section',\n}: {\n block: EditableBlock;\n children: React.ReactNode;\n className?: string;\n as?: React.ElementType;\n}) {\n return (\n <Tag {...editable(block)} className={className}>\n {children}\n </Tag>\n );\n}\n","import type { Translation } from '@flyo/nitro-typescript';\n\n/**\n * A single language option for a language switcher, derived from a page's or\n * entity's `translation[]`. Framework-agnostic plain data — render it however\n * you like.\n */\nexport interface FlyoLanguageLink {\n /** Locale shortcode, e.g. `\"de\"`. */\n shortcode: string;\n /** Full language name from the CMS translation, e.g. `\"Deutsch\"` (when available). */\n name?: string;\n /** Fully-resolved localized href, or `null` when this locale has no linked translation. */\n href: string | null;\n /** Localized page/entity title, when available. */\n title?: string;\n /** `true` when this entry is the currently active locale. */\n isCurrent: boolean;\n /** `true` when a linked translation actually exists for this locale. */\n exists: boolean;\n}\n\n/**\n * Map a page's or entity's `translation[]` into a switcher-ready array of typed\n * links — the building block for a language switcher.\n *\n * Flyo only returns `translation` entries for languages that actually have a\n * translation. Pass `options.locales` (e.g. `flyo.state.locales`) to also get an\n * entry for every configured locale that is *missing* a translation — those come\n * back as `{ href: null, exists: false }` so you can render a fallback (a disabled\n * item, a link to the home page, …).\n *\n * Pure — no React or server-only APIs, so it is safe to call from server *or*\n * client components.\n *\n * This is a pure *data* helper — it maps a `translation[]` to switcher links and\n * renders nothing. **The Flyo route helpers already call it for you and publish\n * the result**, so page and entity routes need no switcher code at all; the\n * footer just reads the store (see `publishLanguageLinks` / `readLanguageLinks`\n * in `@flyo/nitro-next/server`). Call this directly only when you publish links\n * by hand from a raw `translation[]` — e.g. on a custom route Flyo doesn't\n * resolve:\n *\n * ```tsx\n * getLanguageLinks(translations, { currentLang, locales });\n * ```\n *\n * **Render each link as a native `<a>`, not `next/link`'s `<Link>`.** A language\n * switch must refresh the shared chrome — the localized nav, footer and\n * `<html lang>` — that lives in the root layout. In the Next.js App Router, soft\n * (client-side) navigation re-renders only the page segment, *not* shared\n * layouts, so a `<Link>` would leave that chrome in the previous language while\n * only the page body updates. A plain `<a>` triggers a full-document navigation,\n * forcing a fresh server render in the new locale so every part updates. (Regular\n * nav links can stay `<Link>` — the nav is identical within a language.)\n *\n * @example\n * ```tsx\n * const links = getLanguageLinks(page.translation, {\n * currentLang: lang,\n * locales: flyo.state.locales,\n * });\n * // Native <a> (full-document nav) — NOT next/link, so shared layout chrome\n * // re-renders in the new locale.\n * // links.map(l => l.exists\n * // ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>\n * // : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>)\n * ```\n */\nexport function getLanguageLinks(\n translations: Translation[] | undefined,\n options?: { currentLang?: string; locales?: string[] },\n): FlyoLanguageLink[] {\n const currentLang = options?.currentLang;\n\n const byShortcode = new Map<string, Translation>();\n for (const t of translations ?? []) {\n const shortcode = t.language?.shortcode;\n if (shortcode) {\n byShortcode.set(shortcode, t);\n }\n }\n\n const toLink = (shortcode: string, t: Translation | undefined): FlyoLanguageLink => ({\n shortcode,\n name: t?.language?.name,\n href: t?.href ?? null,\n title: t?.title,\n isCurrent: shortcode === currentLang,\n exists: t?.href != null,\n });\n\n // When the full set of locales is known, emit an entry for every one so\n // callers can render fallbacks for languages that have no translation.\n if (options?.locales && options.locales.length > 0) {\n return options.locales.map((shortcode) => toLink(shortcode, byShortcode.get(shortcode)));\n }\n\n // Otherwise, just surface the translations that exist.\n return (translations ?? [])\n .filter((t) => t.language?.shortcode)\n .map((t) => toLink(t.language!.shortcode!, t));\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.tsx","../src/i18n.ts"],"sourcesContent":["'use client';\n\nimport { useEffect } from 'react';\nimport { highlightAndClick, wysiwyg, reload, scrollTo} from '@flyo/nitro-js-bridge';\nimport { Block, Entity } from \"@flyo/nitro-typescript\";\nimport type { ImageLoaderProps } from 'next/image';\n\n// Framework-agnostic language-links helper — re-exported here so client\n// components can build a language switcher from a page/entity `translation[]`\n// without importing from `/server` (which would pull server-only code into the\n// client bundle).\nexport { getLanguageLinks } from './i18n';\nexport type { FlyoLanguageLink } from './i18n';\n\nconst FLYO_CDN_HOST = 'storage.flyo.cloud';\n\n/**\n * Check if running in production environment\n */\nexport const isProd = process.env.NODE_ENV === 'production';\n\n/**\n * Type for WYSIWYG node structure\n */\nexport interface WysiwygNode {\n type: string;\n content?: WysiwygNode[];\n [key: string]: unknown;\n}\n\n/**\n * Type for WYSIWYG JSON that can be a node, array of nodes, or doc structure\n */\nexport type WysiwygJson = WysiwygNode | WysiwygNode[] | { type: 'doc'; content: WysiwygNode[] };\n\n/**\n * The minimal block shape `editable()` needs to wire live-editing.\n *\n * `editable()` reads only `uid`, so it deliberately accepts more than the full\n * {@link Block}. The per-block types generated from a project's OpenAPI schema\n * (e.g. `BlockHero`) are NOT structurally assignable to `Block`: their\n * `content`/`config`/`slots` carry an `_empty` marker that clashes with\n * `Block`'s index signatures (`{ [key: string]: BlockSlotValue }`). Typing\n * against the read-surface keeps both the generic `Block` and those generated\n * subtypes assignable, so callers don't need `as unknown as Block` casts.\n */\nexport type EditableBlock = Pick<Block, 'uid'>;\n\n/**\n * Helper function to get editable props\n */\nexport function editable(block: EditableBlock): { 'data-flyo-uid'?: string } {\n if (typeof block.uid === 'string' && block.uid.trim() !== '') {\n return { 'data-flyo-uid': block.uid };\n }\n return {};\n}\n\n/**\n * Internal client component that sets up live editing functionality\n */\nexport function FlyoClientWrapper({ \n children,\n}: { \n children: React.ReactNode;\n}) {\n useEffect(() => {\n if (typeof window === 'undefined') {\n return;\n }\n\n reload();\n \n scrollTo();\n\n const wireAll = () => {\n const elements = document.querySelectorAll('[data-flyo-uid]');\n elements.forEach((el) => {\n const uid = el.getAttribute('data-flyo-uid');\n if (uid && el instanceof HTMLElement) {\n highlightAndClick(uid, el);\n }\n });\n };\n\n wireAll();\n\n const observer = new MutationObserver((mutations) => {\n const hasRelevantChanges = mutations.some(mutation => \n Array.from(mutation.addedNodes).some(node => {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as Element;\n return element.hasAttribute('data-flyo-uid') || \n element.querySelector('[data-flyo-uid]');\n }\n return false;\n })\n );\n\n if (hasRelevantChanges) {\n wireAll();\n }\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n });\n\n return () => {\n observer.disconnect();\n };\n }, []);\n\n return <>{children}</>;\n}\n\n/**\n * WYSIWYG component for rendering ProseMirror/TipTap JSON content\n * \n * Uses the `wysiwyg()` function from `@flyo/nitro-js-bridge` to convert\n * nodes to HTML. All consecutive non-custom nodes are joined into a single\n * HTML string so no extra wrapper `<div>` elements are added around each node.\n * \n * The component wraps all output in a single `<div>` with an optional\n * `className` (defaults to `\"wysiwyg\"`).\n * \n * @example\n * ```tsx\n * import { FlyoWysiwyg } from '@flyo/nitro-next/client';\n * import CustomImage from './CustomImage';\n * \n * export default function MyComponent({ block }) {\n * return (\n * <FlyoWysiwyg \n * json={block.content.json} \n * className=\"wysiwyg\"\n * components={{\n * image: CustomImage\n * }} \n * />\n * );\n * }\n * ```\n */\nexport function FlyoWysiwyg({\n json,\n className = 'wysiwyg',\n components = {},\n}: {\n json: WysiwygJson;\n className?: string;\n components?: Record<string, React.ComponentType<{ node: WysiwygNode }>>;\n}) {\n let nodes: WysiwygNode[] = [];\n\n if (json) {\n if (Array.isArray(json)) {\n nodes = json;\n } else if ('type' in json && json.type === 'doc' && Array.isArray(json.content)) {\n nodes = json.content;\n } else {\n nodes = [json as WysiwygNode];\n }\n }\n\n // If no custom components are provided, render all nodes as a single HTML block\n const hasCustomComponents = nodes.some((node) => components[node.type]);\n\n if (!hasCustomComponents) {\n const html = nodes.map((node) => wysiwyg(node)).join('');\n return <div className={className} dangerouslySetInnerHTML={{ __html: html }} />;\n }\n\n // When custom components are used, group consecutive non-custom nodes\n // into single HTML blocks to avoid extra wrapper elements.\n const groups: ({ type: 'custom'; component: React.ComponentType<{ node: WysiwygNode }>; node: WysiwygNode } | { type: 'html'; html: string })[] = [];\n\n for (const node of nodes) {\n const Component = components[node.type];\n if (Component) {\n groups.push({ type: 'custom', component: Component, node });\n } else {\n const html = wysiwyg(node);\n const last = groups[groups.length - 1];\n if (last && last.type === 'html') {\n last.html += html;\n } else {\n groups.push({ type: 'html', html });\n }\n }\n }\n\n return (\n <div className={className}>\n {groups.map((group, index) => {\n if (group.type === 'custom') {\n return <group.component key={index} node={group.node} />;\n }\n return <div key={index} dangerouslySetInnerHTML={{ __html: group.html }} />;\n })}\n </div>\n );\n}\n\n/**\n * Image loader for Flyo CDN that automatically handles image transformations.\n * Adds Flyo CDN host if not already present and applies width transformations.\n * \n * @param src - The image source URL (relative or absolute)\n * @param width - The desired width for the image\n * @returns Transformed image URL with Flyo CDN parameters\n * \n * @example\n * ```tsx\n * <Image\n * loader={FlyoCdnLoader}\n * src=\"me.png\"\n * alt=\"Picture\"\n * width={500}\n * height={500}\n * />\n * ```\n */\nexport function FlyoCdnLoader({ src, width }: ImageLoaderProps): string {\n let imageUrl = src;\n\n // If src doesn't contain the Flyo CDN host, prefix it\n if (!src.includes(FLYO_CDN_HOST)) {\n // Remove leading slash if present to avoid double slashes\n const cleanSrc = src.startsWith('/') ? src.slice(1) : src;\n imageUrl = `https://${FLYO_CDN_HOST}/${cleanSrc}`;\n }\n\n // Append Flyo CDN transformation parameters\n return `${imageUrl}/thumb/${width}xnull?format=webp`;\n}\n\n/**\n * FlyoMetric component for tracking entity metrics in production\n * \n * Automatically sends a metric tracking request to the Flyo API when:\n * - The environment is production (NODE_ENV === 'production')\n * - The entity has a metric API URL configured\n * \n * @param entity - The entity object containing entity_metric.api\n * \n * @example\n * ```tsx\n * import { FlyoMetric } from '@flyo/nitro-next/client';\n * \n * export default function BlogPost(props: RouteParams) {\n * return nitroEntityRoute(props, {\n * resolver,\n * render: (entity: Entity) => (\n * <>\n * <FlyoMetric entity={entity} />\n * <article>\n * <h1>{entity.entity?.entity_title}</h1>\n * </article>\n * </>\n * )\n * });\n * }\n * ```\n */\nexport function FlyoMetric({ entity }: { entity: Entity }) {\n useEffect(() => {\n // Only track metrics in production and if API URL is available\n if (isProd && entity?.entity?.entity_metric?.api) {\n fetch(entity.entity.entity_metric.api);\n }\n }, [entity]);\n\n // This component doesn't render anything\n return null;\n}\n\n/**\n * A thin client wrapper that applies `editable()` to a root element while\n * allowing server-rendered children (e.g. `NitroSlot`) to be passed in.\n *\n * In Next.js, a file marked `'use client'` turns all of its imports into\n * client modules, so you cannot import server-only components like\n * `NitroSlot` directly. The workaround is to keep the server part separate\n * and pass it into this client wrapper via `children`.\n *\n * @example\n * ```tsx\n * // components/HeroBanner.tsx (server component – no 'use client')\n * import { Block } from '@flyo/nitro-typescript';\n * import { NitroSlot } from '@flyo/nitro-next/server';\n * import { EditableSection } from '@flyo/nitro-next/client';\n *\n * export function HeroBanner({ block }: { block: Block }) {\n * return (\n * <EditableSection block={block} className=\"hero\">\n * <h2>{block?.content?.title}</h2>\n * <NitroSlot slot={block.slots?.content} />\n * </EditableSection>\n * );\n * }\n * ```\n */\nexport function EditableSection({\n block,\n children,\n className,\n as: Tag = 'section',\n}: {\n block: EditableBlock;\n children: React.ReactNode;\n className?: string;\n as?: React.ElementType;\n}) {\n return (\n <Tag {...editable(block)} className={className}>\n {children}\n </Tag>\n );\n}\n","import type { Translation } from '@flyo/nitro-typescript';\n\n/**\n * A single language option for a language switcher, derived from a page's or\n * entity's `translation[]`. Framework-agnostic plain data — render it however\n * you like.\n */\nexport interface FlyoLanguageLink {\n /** Locale shortcode, e.g. `\"de\"`. */\n shortcode: string;\n /** Full language name from the CMS translation, e.g. `\"Deutsch\"` (when available). */\n name?: string;\n /** Fully-resolved localized href, or `null` when this locale has no linked translation. */\n href: string | null;\n /** Localized page/entity title, when available. */\n title?: string;\n /** `true` when this entry is the currently active locale. */\n isCurrent: boolean;\n /** `true` when a linked translation actually exists for this locale. */\n exists: boolean;\n}\n\n/**\n * Map a page's or entity's `translation[]` into a switcher-ready array of typed\n * links — the building block for a language switcher.\n *\n * Flyo only returns `translation` entries for languages that actually have a\n * translation. Pass `options.locales` (e.g. `flyo.state.locales`) to also get an\n * entry for every configured locale that is *missing* a translation — those come\n * back as `{ href: null, exists: false }` so you can render a fallback (a disabled\n * item, a link to the home page, …).\n *\n * Pure — no React or server-only APIs, so it is safe to call from server *or*\n * client components.\n *\n * @example\n * ```tsx\n * const links = getLanguageLinks(page.translation, {\n * currentLang: lang,\n * locales: flyo.state.locales,\n * });\n * // links.map(l => l.exists\n * // ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>\n * // : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>)\n * ```\n */\nexport function getLanguageLinks(\n translations: Translation[] | undefined,\n options?: { currentLang?: string; locales?: string[] },\n): FlyoLanguageLink[] {\n const currentLang = options?.currentLang;\n\n const byShortcode = new Map<string, Translation>();\n for (const t of translations ?? []) {\n const shortcode = t.language?.shortcode;\n if (shortcode) {\n byShortcode.set(shortcode, t);\n }\n }\n\n const toLink = (shortcode: string, t: Translation | undefined): FlyoLanguageLink => ({\n shortcode,\n name: t?.language?.name,\n href: t?.href ?? null,\n title: t?.title,\n isCurrent: shortcode === currentLang,\n exists: t?.href != null,\n });\n\n // When the full set of locales is known, emit an entry for every one so\n // callers can render fallbacks for languages that have no translation.\n if (options?.locales && options.locales.length > 0) {\n return options.locales.map((shortcode) => toLink(shortcode, byShortcode.get(shortcode)));\n }\n\n // Otherwise, just surface the translations that exist.\n return (translations ?? [])\n .filter((t) => t.language?.shortcode)\n .map((t) => toLink(t.language!.shortcode!, t));\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/client.tsx","../src/i18n.ts"],"sourcesContent":["'use client';\n\nimport { useEffect } from 'react';\nimport { highlightAndClick, wysiwyg, reload, scrollTo} from '@flyo/nitro-js-bridge';\nimport { Block, Entity } from \"@flyo/nitro-typescript\";\nimport type { ImageLoaderProps } from 'next/image';\n\n// Framework-agnostic language-links helper — re-exported here so client\n// components can build a language switcher from a page/entity `translation[]`\n// without importing from `/server` (which would pull server-only code into the\n// client bundle).\nexport { getLanguageLinks } from './i18n';\nexport type { FlyoLanguageLink } from './i18n';\n\nconst FLYO_CDN_HOST = 'storage.flyo.cloud';\n\n/**\n * Check if running in production environment\n */\nexport const isProd = process.env.NODE_ENV === 'production';\n\n/**\n * Type for WYSIWYG node structure\n */\nexport interface WysiwygNode {\n type: string;\n content?: WysiwygNode[];\n [key: string]: unknown;\n}\n\n/**\n * Type for WYSIWYG JSON that can be a node, array of nodes, or doc structure\n */\nexport type WysiwygJson = WysiwygNode | WysiwygNode[] | { type: 'doc'; content: WysiwygNode[] };\n\n/**\n * The minimal block shape `editable()` needs to wire live-editing.\n *\n * `editable()` reads only `uid`, so it deliberately accepts more than the full\n * {@link Block}. The per-block types generated from a project's OpenAPI schema\n * (e.g. `BlockHero`) are NOT structurally assignable to `Block`: their\n * `content`/`config`/`slots` carry an `_empty` marker that clashes with\n * `Block`'s index signatures (`{ [key: string]: BlockSlotValue }`). Typing\n * against the read-surface keeps both the generic `Block` and those generated\n * subtypes assignable, so callers don't need `as unknown as Block` casts.\n */\nexport type EditableBlock = Pick<Block, 'uid'>;\n\n/**\n * Helper function to get editable props\n */\nexport function editable(block: EditableBlock): { 'data-flyo-uid'?: string } {\n if (typeof block.uid === 'string' && block.uid.trim() !== '') {\n return { 'data-flyo-uid': block.uid };\n }\n return {};\n}\n\n/**\n * Internal client component that sets up live editing functionality\n */\nexport function FlyoClientWrapper({ \n children,\n}: { \n children: React.ReactNode;\n}) {\n useEffect(() => {\n if (typeof window === 'undefined') {\n return;\n }\n\n reload();\n \n scrollTo();\n\n const wireAll = () => {\n const elements = document.querySelectorAll('[data-flyo-uid]');\n elements.forEach((el) => {\n const uid = el.getAttribute('data-flyo-uid');\n if (uid && el instanceof HTMLElement) {\n highlightAndClick(uid, el);\n }\n });\n };\n\n wireAll();\n\n const observer = new MutationObserver((mutations) => {\n const hasRelevantChanges = mutations.some(mutation => \n Array.from(mutation.addedNodes).some(node => {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as Element;\n return element.hasAttribute('data-flyo-uid') || \n element.querySelector('[data-flyo-uid]');\n }\n return false;\n })\n );\n\n if (hasRelevantChanges) {\n wireAll();\n }\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n });\n\n return () => {\n observer.disconnect();\n };\n }, []);\n\n return <>{children}</>;\n}\n\n/**\n * WYSIWYG component for rendering ProseMirror/TipTap JSON content\n * \n * Uses the `wysiwyg()` function from `@flyo/nitro-js-bridge` to convert\n * nodes to HTML. All consecutive non-custom nodes are joined into a single\n * HTML string so no extra wrapper `<div>` elements are added around each node.\n * \n * The component wraps all output in a single `<div>` with an optional\n * `className` (defaults to `\"wysiwyg\"`).\n * \n * @example\n * ```tsx\n * import { FlyoWysiwyg } from '@flyo/nitro-next/client';\n * import CustomImage from './CustomImage';\n * \n * export default function MyComponent({ block }) {\n * return (\n * <FlyoWysiwyg \n * json={block.content.json} \n * className=\"wysiwyg\"\n * components={{\n * image: CustomImage\n * }} \n * />\n * );\n * }\n * ```\n */\nexport function FlyoWysiwyg({\n json,\n className = 'wysiwyg',\n components = {},\n}: {\n json: WysiwygJson;\n className?: string;\n components?: Record<string, React.ComponentType<{ node: WysiwygNode }>>;\n}) {\n let nodes: WysiwygNode[] = [];\n\n if (json) {\n if (Array.isArray(json)) {\n nodes = json;\n } else if ('type' in json && json.type === 'doc' && Array.isArray(json.content)) {\n nodes = json.content;\n } else {\n nodes = [json as WysiwygNode];\n }\n }\n\n // If no custom components are provided, render all nodes as a single HTML block\n const hasCustomComponents = nodes.some((node) => components[node.type]);\n\n if (!hasCustomComponents) {\n const html = nodes.map((node) => wysiwyg(node)).join('');\n return <div className={className} dangerouslySetInnerHTML={{ __html: html }} />;\n }\n\n // When custom components are used, group consecutive non-custom nodes\n // into single HTML blocks to avoid extra wrapper elements.\n const groups: ({ type: 'custom'; component: React.ComponentType<{ node: WysiwygNode }>; node: WysiwygNode } | { type: 'html'; html: string })[] = [];\n\n for (const node of nodes) {\n const Component = components[node.type];\n if (Component) {\n groups.push({ type: 'custom', component: Component, node });\n } else {\n const html = wysiwyg(node);\n const last = groups[groups.length - 1];\n if (last && last.type === 'html') {\n last.html += html;\n } else {\n groups.push({ type: 'html', html });\n }\n }\n }\n\n return (\n <div className={className}>\n {groups.map((group, index) => {\n if (group.type === 'custom') {\n return <group.component key={index} node={group.node} />;\n }\n return <div key={index} dangerouslySetInnerHTML={{ __html: group.html }} />;\n })}\n </div>\n );\n}\n\n/**\n * Image loader for Flyo CDN that automatically handles image transformations.\n * Adds Flyo CDN host if not already present and applies width transformations.\n * \n * @param src - The image source URL (relative or absolute)\n * @param width - The desired width for the image\n * @returns Transformed image URL with Flyo CDN parameters\n * \n * @example\n * ```tsx\n * <Image\n * loader={FlyoCdnLoader}\n * src=\"me.png\"\n * alt=\"Picture\"\n * width={500}\n * height={500}\n * />\n * ```\n */\nexport function FlyoCdnLoader({ src, width }: ImageLoaderProps): string {\n let imageUrl = src;\n\n // If src doesn't contain the Flyo CDN host, prefix it\n if (!src.includes(FLYO_CDN_HOST)) {\n // Remove leading slash if present to avoid double slashes\n const cleanSrc = src.startsWith('/') ? src.slice(1) : src;\n imageUrl = `https://${FLYO_CDN_HOST}/${cleanSrc}`;\n }\n\n // Append Flyo CDN transformation parameters\n return `${imageUrl}/thumb/${width}xnull?format=webp`;\n}\n\n/**\n * FlyoMetric component for tracking entity metrics in production\n * \n * Automatically sends a metric tracking request to the Flyo API when:\n * - The environment is production (NODE_ENV === 'production')\n * - The entity has a metric API URL configured\n * \n * @param entity - The entity object containing entity_metric.api\n * \n * @example\n * ```tsx\n * import { FlyoMetric } from '@flyo/nitro-next/client';\n * \n * export default function BlogPost(props: RouteParams) {\n * return nitroEntityRoute(props, {\n * resolver,\n * render: (entity: Entity) => (\n * <>\n * <FlyoMetric entity={entity} />\n * <article>\n * <h1>{entity.entity?.entity_title}</h1>\n * </article>\n * </>\n * )\n * });\n * }\n * ```\n */\nexport function FlyoMetric({ entity }: { entity: Entity }) {\n useEffect(() => {\n // Only track metrics in production and if API URL is available\n if (isProd && entity?.entity?.entity_metric?.api) {\n fetch(entity.entity.entity_metric.api);\n }\n }, [entity]);\n\n // This component doesn't render anything\n return null;\n}\n\n/**\n * A thin client wrapper that applies `editable()` to a root element while\n * allowing server-rendered children (e.g. `NitroSlot`) to be passed in.\n *\n * In Next.js, a file marked `'use client'` turns all of its imports into\n * client modules, so you cannot import server-only components like\n * `NitroSlot` directly. The workaround is to keep the server part separate\n * and pass it into this client wrapper via `children`.\n *\n * @example\n * ```tsx\n * // components/HeroBanner.tsx (server component – no 'use client')\n * import { Block } from '@flyo/nitro-typescript';\n * import { NitroSlot } from '@flyo/nitro-next/server';\n * import { EditableSection } from '@flyo/nitro-next/client';\n *\n * export function HeroBanner({ block }: { block: Block }) {\n * return (\n * <EditableSection block={block} className=\"hero\">\n * <h2>{block?.content?.title}</h2>\n * <NitroSlot slot={block.slots?.content} />\n * </EditableSection>\n * );\n * }\n * ```\n */\nexport function EditableSection({\n block,\n children,\n className,\n as: Tag = 'section',\n}: {\n block: EditableBlock;\n children: React.ReactNode;\n className?: string;\n as?: React.ElementType;\n}) {\n return (\n <Tag {...editable(block)} className={className}>\n {children}\n </Tag>\n );\n}\n","import type { Translation } from '@flyo/nitro-typescript';\n\n/**\n * A single language option for a language switcher, derived from a page's or\n * entity's `translation[]`. Framework-agnostic plain data — render it however\n * you like.\n */\nexport interface FlyoLanguageLink {\n /** Locale shortcode, e.g. `\"de\"`. */\n shortcode: string;\n /** Full language name from the CMS translation, e.g. `\"Deutsch\"` (when available). */\n name?: string;\n /** Fully-resolved localized href, or `null` when this locale has no linked translation. */\n href: string | null;\n /** Localized page/entity title, when available. */\n title?: string;\n /** `true` when this entry is the currently active locale. */\n isCurrent: boolean;\n /** `true` when a linked translation actually exists for this locale. */\n exists: boolean;\n}\n\n/**\n * Map a page's or entity's `translation[]` into a switcher-ready array of typed\n * links — the building block for a language switcher.\n *\n * Flyo only returns `translation` entries for languages that actually have a\n * translation. Pass `options.locales` (e.g. `flyo.state.locales`) to also get an\n * entry for every configured locale that is *missing* a translation — those come\n * back as `{ href: null, exists: false }` so you can render a fallback (a disabled\n * item, a link to the home page, …).\n *\n * Pure — no React or server-only APIs, so it is safe to call from server *or*\n * client components.\n *\n * This is a pure *data* helper — it maps a `translation[]` to switcher links and\n * renders nothing. **The Flyo route helpers already call it for you and publish\n * the result**, so page and entity routes need no switcher code at all; the\n * footer just reads the store (see `publishLanguageLinks` / `readLanguageLinks`\n * in `@flyo/nitro-next/server`). Call this directly only when you publish links\n * by hand from a raw `translation[]` — e.g. on a custom route Flyo doesn't\n * resolve:\n *\n * ```tsx\n * getLanguageLinks(translations, { currentLang, locales });\n * ```\n *\n * **Render each link as a native `<a>`, not `next/link`'s `<Link>`.** A language\n * switch must refresh the shared chrome — the localized nav, footer and\n * `<html lang>` — that lives in the root layout. In the Next.js App Router, soft\n * (client-side) navigation re-renders only the page segment, *not* shared\n * layouts, so a `<Link>` would leave that chrome in the previous language while\n * only the page body updates. A plain `<a>` triggers a full-document navigation,\n * forcing a fresh server render in the new locale so every part updates. (Regular\n * nav links can stay `<Link>` — the nav is identical within a language.)\n *\n * @example\n * ```tsx\n * const links = getLanguageLinks(page.translation, {\n * currentLang: lang,\n * locales: flyo.state.locales,\n * });\n * // Native <a> (full-document nav) — NOT next/link, so shared layout chrome\n * // re-renders in the new locale.\n * // links.map(l => l.exists\n * // ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>\n * // : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>)\n * ```\n */\nexport function getLanguageLinks(\n translations: Translation[] | undefined,\n options?: { currentLang?: string; locales?: string[] },\n): FlyoLanguageLink[] {\n const currentLang = options?.currentLang;\n\n const byShortcode = new Map<string, Translation>();\n for (const t of translations ?? []) {\n const shortcode = t.language?.shortcode;\n if (shortcode) {\n byShortcode.set(shortcode, t);\n }\n }\n\n const toLink = (shortcode: string, t: Translation | undefined): FlyoLanguageLink => ({\n shortcode,\n name: t?.language?.name,\n href: t?.href ?? null,\n title: t?.title,\n isCurrent: shortcode === currentLang,\n exists: t?.href != null,\n });\n\n // When the full set of locales is known, emit an entry for every one so\n // callers can render fallbacks for languages that have no translation.\n if (options?.locales && options.locales.length > 0) {\n return options.locales.map((shortcode) => toLink(shortcode, byShortcode.get(shortcode)));\n }\n\n // Otherwise, just surface the translations that exist.\n return (translations ?? [])\n .filter((t) => t.language?.shortcode)\n .map((t) => toLink(t.language!.shortcode!, t));\n}\n"],"mappings":"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","names":[]}
package/dist/server.d.mts CHANGED
@@ -36,12 +36,35 @@ interface FlyoLanguageLink {
36
36
  * Pure — no React or server-only APIs, so it is safe to call from server *or*
37
37
  * client components.
38
38
  *
39
+ * This is a pure *data* helper — it maps a `translation[]` to switcher links and
40
+ * renders nothing. **The Flyo route helpers already call it for you and publish
41
+ * the result**, so page and entity routes need no switcher code at all; the
42
+ * footer just reads the store (see `publishLanguageLinks` / `readLanguageLinks`
43
+ * in `@flyo/nitro-next/server`). Call this directly only when you publish links
44
+ * by hand from a raw `translation[]` — e.g. on a custom route Flyo doesn't
45
+ * resolve:
46
+ *
47
+ * ```tsx
48
+ * getLanguageLinks(translations, { currentLang, locales });
49
+ * ```
50
+ *
51
+ * **Render each link as a native `<a>`, not `next/link`'s `<Link>`.** A language
52
+ * switch must refresh the shared chrome — the localized nav, footer and
53
+ * `<html lang>` — that lives in the root layout. In the Next.js App Router, soft
54
+ * (client-side) navigation re-renders only the page segment, *not* shared
55
+ * layouts, so a `<Link>` would leave that chrome in the previous language while
56
+ * only the page body updates. A plain `<a>` triggers a full-document navigation,
57
+ * forcing a fresh server render in the new locale so every part updates. (Regular
58
+ * nav links can stay `<Link>` — the nav is identical within a language.)
59
+ *
39
60
  * @example
40
61
  * ```tsx
41
62
  * const links = getLanguageLinks(page.translation, {
42
63
  * currentLang: lang,
43
64
  * locales: flyo.state.locales,
44
65
  * });
66
+ * // Native <a> (full-document nav) — NOT next/link, so shared layout chrome
67
+ * // re-renders in the new locale.
45
68
  * // links.map(l => l.exists
46
69
  * // ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>
47
70
  * // : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>)
@@ -161,6 +184,81 @@ declare function initNitro({ accessToken, lang, locales, defaultLocale, baseUrl,
161
184
  serverCacheTtl?: number;
162
185
  clientCacheTtl?: number;
163
186
  }): FlyoInstance;
187
+ interface LanguageLinksStore {
188
+ /**
189
+ * Publish the links once. Later calls are ignored (first publish wins).
190
+ *
191
+ * The active page/entity route is the single writer per request: it publishes
192
+ * the resolved translations on success and a fallback before any `notFound()`.
193
+ * "First wins" then only guards against the route's render pass and its
194
+ * `generateMetadata` both publishing (they publish the same links) — no other
195
+ * caller races it.
196
+ */
197
+ publish(links: FlyoLanguageLink[]): void;
198
+ /** A promise that resolves when the links are published. */
199
+ read(): Promise<FlyoLanguageLink[]>;
200
+ }
201
+ /**
202
+ * Create a single deferred language-links channel: one writer, many readers,
203
+ * order-independent. Low-level — most code uses {@link publishLanguageLinks} /
204
+ * {@link readLanguageLinks}, which wrap this in a per-request `cache()`.
205
+ */
206
+ declare function createLanguageLinksStore(): LanguageLinksStore;
207
+ /**
208
+ * Publish the current route's language-switcher links into the request-scoped
209
+ * store, so a switcher rendered in shared chrome (e.g. a footer in the root
210
+ * layout) can read them via {@link readLanguageLinks}.
211
+ *
212
+ * The Flyo route helpers call this for you: `pageResolveRoute` (and therefore
213
+ * `nitroPageRoute`) publishes the page's links, and `nitroEntityRoute` /
214
+ * `nitroEntityGenerateMetadata` publish the entity's. Crucially they also
215
+ * publish a fallback on their `notFound()` paths, so a genuine 404 that renders
216
+ * `not-found.tsx` still settles the store — the footer never waits forever.
217
+ *
218
+ * **Do not publish from `not-found.tsx`.** In the App Router the root not-found
219
+ * boundary is rendered on *every* request, not only on real 404s, and it renders
220
+ * synchronously — ahead of a route's `await`ed CMS fetch. With a first-wins
221
+ * store, a publish there would settle the store with the fallback before the
222
+ * real route publishes, so pages that *do* have translations would show the
223
+ * home/fallback links. Let the route own the publish instead.
224
+ *
225
+ * You only need to call this yourself on a **custom route Flyo does not
226
+ * resolve** — a hand-written page that renders the shared switcher without going
227
+ * through the helpers above. Publish that route's real links, or a fallback via
228
+ * `getLanguageLinks(undefined, { currentLang, locales })`.
229
+ *
230
+ * Only the first call per request takes effect.
231
+ */
232
+ declare function publishLanguageLinks(links: FlyoLanguageLink[]): void;
233
+ /**
234
+ * Await the current route's language-switcher links from shared chrome — a
235
+ * footer, header, or any component in the root layout. Resolves with whatever
236
+ * the active page/entity route passed to {@link publishLanguageLinks}.
237
+ *
238
+ * Because it awaits, the reader suspends until the links are published, so it
239
+ * works no matter whether the layout or the page renders first. Wrap the reading
240
+ * component in `<Suspense>` so the rest of the layout can stream while it waits.
241
+ *
242
+ * @example
243
+ * ```tsx
244
+ * // components/LanguageSwitcher.tsx (server component)
245
+ * import { readLanguageLinks } from '@flyo/nitro-next/server';
246
+ *
247
+ * export async function LanguageSwitcher() {
248
+ * const links = await readLanguageLinks();
249
+ * return (
250
+ * <nav aria-label="Language">
251
+ * {links.map((l) =>
252
+ * l.exists
253
+ * ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>
254
+ * : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>,
255
+ * )}
256
+ * </nav>
257
+ * );
258
+ * }
259
+ * ```
260
+ */
261
+ declare function readLanguageLinks(): Promise<FlyoLanguageLink[]>;
164
262
  /**
165
263
  * NitroDebugInfo Component
166
264
  *
@@ -301,4 +399,4 @@ declare function nitroEntityGenerateMetadata<T = any>(flyo: FlyoInstance, option
301
399
  resolver: EntityResolver<T>;
302
400
  }): (props: EntityRouteParams<T>) => Promise<Metadata>;
303
401
 
304
- export { type EntityResolver, type FlyoInstance, type FlyoLanguageLink, NitroBlock, NitroDebugInfo, NitroEntityJsonLd, NitroPage, NitroSlot, type NitroState, getLanguageLinks, initNitro, nitroEntityGenerateMetadata, nitroEntityRoute, nitroPageGenerateMetadata, nitroPageGenerateStaticParams, nitroPageRoute };
402
+ export { type EntityResolver, type FlyoInstance, type FlyoLanguageLink, type LanguageLinksStore, NitroBlock, NitroDebugInfo, NitroEntityJsonLd, NitroPage, NitroSlot, type NitroState, createLanguageLinksStore, getLanguageLinks, initNitro, nitroEntityGenerateMetadata, nitroEntityRoute, nitroPageGenerateMetadata, nitroPageGenerateStaticParams, nitroPageRoute, publishLanguageLinks, readLanguageLinks };
package/dist/server.d.ts CHANGED
@@ -36,12 +36,35 @@ interface FlyoLanguageLink {
36
36
  * Pure — no React or server-only APIs, so it is safe to call from server *or*
37
37
  * client components.
38
38
  *
39
+ * This is a pure *data* helper — it maps a `translation[]` to switcher links and
40
+ * renders nothing. **The Flyo route helpers already call it for you and publish
41
+ * the result**, so page and entity routes need no switcher code at all; the
42
+ * footer just reads the store (see `publishLanguageLinks` / `readLanguageLinks`
43
+ * in `@flyo/nitro-next/server`). Call this directly only when you publish links
44
+ * by hand from a raw `translation[]` — e.g. on a custom route Flyo doesn't
45
+ * resolve:
46
+ *
47
+ * ```tsx
48
+ * getLanguageLinks(translations, { currentLang, locales });
49
+ * ```
50
+ *
51
+ * **Render each link as a native `<a>`, not `next/link`'s `<Link>`.** A language
52
+ * switch must refresh the shared chrome — the localized nav, footer and
53
+ * `<html lang>` — that lives in the root layout. In the Next.js App Router, soft
54
+ * (client-side) navigation re-renders only the page segment, *not* shared
55
+ * layouts, so a `<Link>` would leave that chrome in the previous language while
56
+ * only the page body updates. A plain `<a>` triggers a full-document navigation,
57
+ * forcing a fresh server render in the new locale so every part updates. (Regular
58
+ * nav links can stay `<Link>` — the nav is identical within a language.)
59
+ *
39
60
  * @example
40
61
  * ```tsx
41
62
  * const links = getLanguageLinks(page.translation, {
42
63
  * currentLang: lang,
43
64
  * locales: flyo.state.locales,
44
65
  * });
66
+ * // Native <a> (full-document nav) — NOT next/link, so shared layout chrome
67
+ * // re-renders in the new locale.
45
68
  * // links.map(l => l.exists
46
69
  * // ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>
47
70
  * // : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>)
@@ -161,6 +184,81 @@ declare function initNitro({ accessToken, lang, locales, defaultLocale, baseUrl,
161
184
  serverCacheTtl?: number;
162
185
  clientCacheTtl?: number;
163
186
  }): FlyoInstance;
187
+ interface LanguageLinksStore {
188
+ /**
189
+ * Publish the links once. Later calls are ignored (first publish wins).
190
+ *
191
+ * The active page/entity route is the single writer per request: it publishes
192
+ * the resolved translations on success and a fallback before any `notFound()`.
193
+ * "First wins" then only guards against the route's render pass and its
194
+ * `generateMetadata` both publishing (they publish the same links) — no other
195
+ * caller races it.
196
+ */
197
+ publish(links: FlyoLanguageLink[]): void;
198
+ /** A promise that resolves when the links are published. */
199
+ read(): Promise<FlyoLanguageLink[]>;
200
+ }
201
+ /**
202
+ * Create a single deferred language-links channel: one writer, many readers,
203
+ * order-independent. Low-level — most code uses {@link publishLanguageLinks} /
204
+ * {@link readLanguageLinks}, which wrap this in a per-request `cache()`.
205
+ */
206
+ declare function createLanguageLinksStore(): LanguageLinksStore;
207
+ /**
208
+ * Publish the current route's language-switcher links into the request-scoped
209
+ * store, so a switcher rendered in shared chrome (e.g. a footer in the root
210
+ * layout) can read them via {@link readLanguageLinks}.
211
+ *
212
+ * The Flyo route helpers call this for you: `pageResolveRoute` (and therefore
213
+ * `nitroPageRoute`) publishes the page's links, and `nitroEntityRoute` /
214
+ * `nitroEntityGenerateMetadata` publish the entity's. Crucially they also
215
+ * publish a fallback on their `notFound()` paths, so a genuine 404 that renders
216
+ * `not-found.tsx` still settles the store — the footer never waits forever.
217
+ *
218
+ * **Do not publish from `not-found.tsx`.** In the App Router the root not-found
219
+ * boundary is rendered on *every* request, not only on real 404s, and it renders
220
+ * synchronously — ahead of a route's `await`ed CMS fetch. With a first-wins
221
+ * store, a publish there would settle the store with the fallback before the
222
+ * real route publishes, so pages that *do* have translations would show the
223
+ * home/fallback links. Let the route own the publish instead.
224
+ *
225
+ * You only need to call this yourself on a **custom route Flyo does not
226
+ * resolve** — a hand-written page that renders the shared switcher without going
227
+ * through the helpers above. Publish that route's real links, or a fallback via
228
+ * `getLanguageLinks(undefined, { currentLang, locales })`.
229
+ *
230
+ * Only the first call per request takes effect.
231
+ */
232
+ declare function publishLanguageLinks(links: FlyoLanguageLink[]): void;
233
+ /**
234
+ * Await the current route's language-switcher links from shared chrome — a
235
+ * footer, header, or any component in the root layout. Resolves with whatever
236
+ * the active page/entity route passed to {@link publishLanguageLinks}.
237
+ *
238
+ * Because it awaits, the reader suspends until the links are published, so it
239
+ * works no matter whether the layout or the page renders first. Wrap the reading
240
+ * component in `<Suspense>` so the rest of the layout can stream while it waits.
241
+ *
242
+ * @example
243
+ * ```tsx
244
+ * // components/LanguageSwitcher.tsx (server component)
245
+ * import { readLanguageLinks } from '@flyo/nitro-next/server';
246
+ *
247
+ * export async function LanguageSwitcher() {
248
+ * const links = await readLanguageLinks();
249
+ * return (
250
+ * <nav aria-label="Language">
251
+ * {links.map((l) =>
252
+ * l.exists
253
+ * ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>
254
+ * : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>,
255
+ * )}
256
+ * </nav>
257
+ * );
258
+ * }
259
+ * ```
260
+ */
261
+ declare function readLanguageLinks(): Promise<FlyoLanguageLink[]>;
164
262
  /**
165
263
  * NitroDebugInfo Component
166
264
  *
@@ -301,4 +399,4 @@ declare function nitroEntityGenerateMetadata<T = any>(flyo: FlyoInstance, option
301
399
  resolver: EntityResolver<T>;
302
400
  }): (props: EntityRouteParams<T>) => Promise<Metadata>;
303
401
 
304
- export { type EntityResolver, type FlyoInstance, type FlyoLanguageLink, NitroBlock, NitroDebugInfo, NitroEntityJsonLd, NitroPage, NitroSlot, type NitroState, getLanguageLinks, initNitro, nitroEntityGenerateMetadata, nitroEntityRoute, nitroPageGenerateMetadata, nitroPageGenerateStaticParams, nitroPageRoute };
402
+ export { type EntityResolver, type FlyoInstance, type FlyoLanguageLink, type LanguageLinksStore, NitroBlock, NitroDebugInfo, NitroEntityJsonLd, NitroPage, NitroSlot, type NitroState, createLanguageLinksStore, getLanguageLinks, initNitro, nitroEntityGenerateMetadata, nitroEntityRoute, nitroPageGenerateMetadata, nitroPageGenerateStaticParams, nitroPageRoute, publishLanguageLinks, readLanguageLinks };
package/dist/server.js CHANGED
@@ -25,13 +25,16 @@ __export(server_exports, {
25
25
  NitroEntityJsonLd: () => NitroEntityJsonLd,
26
26
  NitroPage: () => NitroPage,
27
27
  NitroSlot: () => NitroSlot,
28
+ createLanguageLinksStore: () => createLanguageLinksStore,
28
29
  getLanguageLinks: () => getLanguageLinks,
29
30
  initNitro: () => initNitro,
30
31
  nitroEntityGenerateMetadata: () => nitroEntityGenerateMetadata,
31
32
  nitroEntityRoute: () => nitroEntityRoute,
32
33
  nitroPageGenerateMetadata: () => nitroPageGenerateMetadata,
33
34
  nitroPageGenerateStaticParams: () => nitroPageGenerateStaticParams,
34
- nitroPageRoute: () => nitroPageRoute
35
+ nitroPageRoute: () => nitroPageRoute,
36
+ publishLanguageLinks: () => publishLanguageLinks,
37
+ readLanguageLinks: () => readLanguageLinks
35
38
  });
36
39
  module.exports = __toCommonJS(server_exports);
37
40
  var import_react = require("react");
@@ -117,17 +120,22 @@ function initNitro({
117
120
  const path = segments.join("/");
118
121
  const firstSegment = segments[0];
119
122
  const lang2 = firstSegment && state.locales.includes(firstSegment) ? firstSegment : state.defaultLocale ?? void 0;
123
+ const publishNotFoundFallback = () => publishLanguageLinks(getLanguageLinks(void 0, { currentLang: lang2, locales: state.locales }));
120
124
  const cfg = await getNitroConfig(lang2);
121
125
  if (!cfg.pages?.includes(path)) {
126
+ publishNotFoundFallback();
122
127
  (0, import_navigation.notFound)();
123
128
  }
124
129
  const page = await new import_nitro_typescript.PagesApi(configuration).page({ slug: path, lang: lang2 }).catch((error) => {
125
130
  console.error("Error fetching page:", path, error);
131
+ publishNotFoundFallback();
126
132
  (0, import_navigation.notFound)();
127
133
  });
128
134
  if (!page) {
135
+ publishNotFoundFallback();
129
136
  (0, import_navigation.notFound)();
130
137
  }
138
+ publishLanguageLinks(getLanguageLinks(page.translation, { currentLang: lang2, locales: state.locales }));
131
139
  return { page, path, lang: lang2, cfg };
132
140
  });
133
141
  const sitemap = async () => {
@@ -168,6 +176,31 @@ function initNitro({
168
176
  sitemap
169
177
  };
170
178
  }
179
+ function createLanguageLinksStore() {
180
+ let resolve;
181
+ const promise = new Promise((r) => {
182
+ resolve = r;
183
+ });
184
+ let settled = false;
185
+ return {
186
+ publish(links) {
187
+ if (!settled) {
188
+ settled = true;
189
+ resolve(links);
190
+ }
191
+ },
192
+ read() {
193
+ return promise;
194
+ }
195
+ };
196
+ }
197
+ var languageLinksStore = (0, import_react.cache)(createLanguageLinksStore);
198
+ function publishLanguageLinks(links) {
199
+ languageLinksStore().publish(links);
200
+ }
201
+ function readLanguageLinks() {
202
+ return languageLinksStore().read();
203
+ }
171
204
  var readEnv = (key, fallback = "") => {
172
205
  const value = process.env[key];
173
206
  if (value !== void 0 && value !== "") {
@@ -192,12 +225,28 @@ function buildLanguageAlternates(translations, currentLang) {
192
225
  }
193
226
  return canonical ? { canonical, languages } : { languages };
194
227
  }
195
- function createCachedEntityResolver(resolver) {
228
+ function createCachedEntityResolver(flyo, resolver) {
196
229
  return (0, import_react.cache)(async ({ params }) => {
197
- const entity = await resolver(params);
230
+ const publishNotFoundFallback = async () => publishLanguageLinks(
231
+ getLanguageLinks(void 0, {
232
+ currentLang: await flyo.getRequestLocale(),
233
+ locales: flyo.state.locales
234
+ })
235
+ );
236
+ let entity;
237
+ try {
238
+ entity = await resolver(params);
239
+ } catch (error) {
240
+ await publishNotFoundFallback();
241
+ throw error;
242
+ }
198
243
  if (!entity) {
244
+ await publishNotFoundFallback();
199
245
  (0, import_navigation.notFound)();
200
246
  }
247
+ publishLanguageLinks(
248
+ getLanguageLinks(entity.translation, { currentLang: entity.language, locales: flyo.state.locales })
249
+ );
201
250
  return entity;
202
251
  });
203
252
  }
@@ -351,7 +400,7 @@ function nitroPageGenerateStaticParams(flyo) {
351
400
  };
352
401
  }
353
402
  function nitroEntityRoute(flyo, options) {
354
- const cachedResolver = createCachedEntityResolver(options.resolver);
403
+ const cachedResolver = createCachedEntityResolver(flyo, options.resolver);
355
404
  async function entityRoute(props) {
356
405
  const entity = await cachedResolver(props);
357
406
  if (options.render) {
@@ -362,7 +411,7 @@ function nitroEntityRoute(flyo, options) {
362
411
  return entityRoute;
363
412
  }
364
413
  function nitroEntityGenerateMetadata(flyo, options) {
365
- const cachedResolver = createCachedEntityResolver(options.resolver);
414
+ const cachedResolver = createCachedEntityResolver(flyo, options.resolver);
366
415
  return async (props) => {
367
416
  const entity = await cachedResolver(props);
368
417
  const title = entity.entity?.entity_title || "";
@@ -397,12 +446,15 @@ function nitroEntityGenerateMetadata(flyo, options) {
397
446
  NitroEntityJsonLd,
398
447
  NitroPage,
399
448
  NitroSlot,
449
+ createLanguageLinksStore,
400
450
  getLanguageLinks,
401
451
  initNitro,
402
452
  nitroEntityGenerateMetadata,
403
453
  nitroEntityRoute,
404
454
  nitroPageGenerateMetadata,
405
455
  nitroPageGenerateStaticParams,
406
- nitroPageRoute
456
+ nitroPageRoute,
457
+ publishLanguageLinks,
458
+ readLanguageLinks
407
459
  });
408
460
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.tsx","../src/i18n.ts"],"sourcesContent":["import { cache } from 'react';\nimport type { Metadata, MetadataRoute } from 'next';\nimport { notFound } from 'next/navigation';\nimport { headers } from 'next/headers';\nimport {\n Page,\n Block,\n Entity,\n ConfigApi,\n ConfigResponse,\n Configuration,\n PagesApi,\n EntitiesApi,\n SitemapApi,\n SearchApi,\n type Translation\n} from '@flyo/nitro-typescript';\n\n// Re-export the framework-agnostic language-links helper + types so they are\n// available from `@flyo/nitro-next/server` alongside the rest of the server API.\nexport { getLanguageLinks } from './i18n';\nexport type { FlyoLanguageLink } from './i18n';\nexport type { Translation } from '@flyo/nitro-typescript';\n\n/**\n * Read-only configuration state\n */\nexport interface NitroState {\n readonly accessToken: string;\n /** The single default language (back-compat). Prefer `defaultLocale` for i18n. */\n readonly lang: string | null;\n /** All locale shortcodes the site supports, e.g. `['de', 'en']`. Empty for single-language setups. */\n readonly locales: string[];\n /** The primary/default locale (no URL prefix). Matches `config.nitro.primary_language`. */\n readonly defaultLocale: string | null;\n readonly baseUrl: string | null;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly components: Record<string, any>;\n readonly showMissingComponentAlert: boolean;\n readonly liveEdit: boolean;\n readonly serverCacheTtl: number;\n readonly clientCacheTtl: number;\n}\n\n/**\n * Route params type for Next.js catch-all routes\n */\ntype RouteParams = {\n params: Promise<{ slug?: string[] }>;\n};\n\n/**\n * Generic route params type for entity routes\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype EntityRouteParams<T = any> = {\n params: Promise<T>;\n};\n\n/**\n * Entity resolver function type\n * Users provide this to resolve entities from their route params\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type EntityResolver<T = any> = (params: Promise<T>) => Promise<Entity>;\n\n/**\n * The Flyo instance returned by initNitro().\n * Carries all API access methods and configuration state.\n */\nexport interface FlyoInstance {\n /** Read-only configuration state */\n readonly state: NitroState;\n /**\n * Fetch and cache the Nitro CMS configuration (React-cached per requested locale).\n * When `lang` is omitted, the active request locale is used (from the `x-flyo-locale`\n * header set by the proxy), falling back to `defaultLocale`.\n */\n getNitroConfig(lang?: string): Promise<ConfigResponse>;\n /** Create a PagesApi client */\n getNitroPages(): PagesApi;\n /** Create an EntitiesApi client */\n getNitroEntities(): EntitiesApi;\n /** Create a SitemapApi client */\n getNitroSitemap(): SitemapApi;\n /** Create a SearchApi client */\n getNitroSearch(): SearchApi;\n /**\n * Resolve the active request locale from the `x-flyo-locale` header (set by the\n * proxy middleware), falling back to `defaultLocale`. Use it inside entity\n * resolvers or to set `<html lang>`.\n */\n getRequestLocale(): Promise<string | undefined>;\n /** Resolve a page from catch-all route params (React-cached per request) */\n pageResolveRoute(props: RouteParams): Promise<{ page: Page; path: string; lang: string | undefined; cfg: ConfigResponse }>;\n /** Generate a Next.js sitemap from Flyo Nitro content */\n sitemap(): Promise<MetadataRoute.Sitemap>;\n}\n\n/**\n * Initialize and return a FlyoInstance.\n *\n * Call once in your flyo.config.tsx and export the result.\n * The returned object carries all methods bound to the configuration,\n * eliminating the need for global state.\n *\n * @example\n * ```tsx\n * // flyo.config.tsx\n * import { initNitro } from '@flyo/nitro-next/server';\n *\n * export const flyo = initNitro({\n * accessToken: process.env.FLYO_ACCESS_TOKEN || '',\n * lang: 'en',\n * baseUrl: process.env.SITE_URL || 'http://localhost:3000',\n * liveEdit: process.env.FLYO_LIVE_EDIT === 'true',\n * components: { HeroBanner, Text },\n * });\n * ```\n */\nexport function initNitro({\n accessToken,\n lang,\n locales,\n defaultLocale,\n baseUrl,\n components,\n showMissingComponentAlert,\n liveEdit,\n serverCacheTtl,\n clientCacheTtl,\n}: {\n accessToken: string;\n lang?: string;\n /** All supported locale shortcodes, e.g. `['de', 'en']`. Enables per-request i18n. */\n locales?: string[];\n /** The primary/default locale (no URL prefix). Defaults to `lang`. Should match `config.nitro.primary_language`. */\n defaultLocale?: string;\n baseUrl?: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n components?: Record<string, any>;\n showMissingComponentAlert?: boolean;\n liveEdit?: boolean;\n serverCacheTtl?: number;\n clientCacheTtl?: number;\n}): FlyoInstance {\n const configuration = new Configuration({ apiKey: accessToken });\n\n const resolvedDefaultLocale = defaultLocale ?? lang ?? null;\n const resolvedLocales = locales ?? (resolvedDefaultLocale ? [resolvedDefaultLocale] : []);\n\n const state: NitroState = {\n accessToken,\n lang: lang ?? null,\n locales: resolvedLocales,\n defaultLocale: resolvedDefaultLocale,\n baseUrl: baseUrl ?? null,\n components: components ?? {},\n showMissingComponentAlert: showMissingComponentAlert ?? liveEdit ?? false,\n liveEdit: liveEdit ?? false,\n serverCacheTtl: serverCacheTtl ?? 1200,\n clientCacheTtl: clientCacheTtl ?? 900,\n };\n\n const getRequestLocale = async (): Promise<string | undefined> => {\n try {\n const requestHeaders = await headers();\n const headerLocale = requestHeaders.get('x-flyo-locale');\n if (headerLocale) {\n return headerLocale;\n }\n } catch {\n // `headers()` throws outside a request scope (build time, tests, …) — fall back below.\n }\n return state.defaultLocale ?? state.lang ?? undefined;\n };\n\n // React-cached per requested locale, so different languages don't collide on\n // one memoized value and the same locale is only fetched once per request.\n const fetchConfig = cache(async (lang?: string): Promise<ConfigResponse> => {\n const configApi = new ConfigApi(configuration);\n return configApi.config({ lang: lang ?? undefined });\n });\n\n const getNitroConfig = async (lang?: string): Promise<ConfigResponse> => {\n const useLang = lang ?? (await getRequestLocale());\n return fetchConfig(useLang);\n };\n\n const pageResolveRoute = cache(async ({ params }: RouteParams) => {\n const { slug } = await params;\n const segments = slug ?? [];\n // Pages are addressed by their full, globally-unique slug (locale prefix\n // included), which is exactly what the catch-all captures — pass it through.\n const path = segments.join('/');\n\n // Derive the active locale from the leading path segment when it is a\n // configured locale (matches the Laravel adapter's `request()->segment(1)`).\n const firstSegment = segments[0];\n const lang = firstSegment && state.locales.includes(firstSegment)\n ? firstSegment\n : (state.defaultLocale ?? undefined);\n\n const cfg = await getNitroConfig(lang);\n\n if (!cfg.pages?.includes(path)) {\n notFound();\n }\n\n const page = await new PagesApi(configuration)\n .page({ slug: path, lang })\n .catch((error: unknown) => {\n console.error('Error fetching page:', path, error);\n notFound();\n });\n\n if (!page) {\n notFound();\n }\n\n return { page, path, lang, cfg };\n });\n\n const sitemap = async (): Promise<MetadataRoute.Sitemap> => {\n const sitemapApi = new SitemapApi(configuration);\n const sitemapLang = state.lang ?? undefined;\n\n if (!state.baseUrl) {\n throw new Error('baseUrl is not configured. Please set it in initNitro().');\n }\n\n const items = await sitemapApi.sitemap({ lang: sitemapLang });\n const cleanBaseUrl = state.baseUrl.endsWith('/') ? state.baseUrl.slice(0, -1) : state.baseUrl;\n\n return items.map((item) => {\n let path = '';\n\n if (item.routes && typeof item.routes === 'object') {\n const routeValues = Object.values(item.routes);\n if (routeValues.length > 0) {\n path = routeValues[0];\n }\n }\n\n if (!path && item.entity_slug) {\n path = item.entity_slug;\n }\n\n const cleanPath = path && !path.startsWith('/') ? `/${path}` : path;\n\n return {\n url: `${cleanBaseUrl}${cleanPath}`,\n lastModified: new Date(),\n };\n });\n };\n\n return {\n state,\n getNitroConfig,\n getRequestLocale,\n getNitroPages: () => new PagesApi(configuration),\n getNitroEntities: () => new EntitiesApi(configuration),\n getNitroSitemap: () => new SitemapApi(configuration),\n getNitroSearch: () => new SearchApi(configuration),\n pageResolveRoute,\n sitemap,\n };\n}\n\n// ─── Helper ──────────────────────────────────────────────────────────────────\n\n/**\n * Helper function to read environment variables with fallback\n */\nconst readEnv = (key: string, fallback = \"\"): string => {\n const value = process.env[key];\n if (value !== undefined && value !== \"\") {\n return String(value);\n }\n return fallback;\n};\n\n/**\n * Build Next.js `alternates` (hreflang + canonical) from a page's or entity's\n * `translation[]`. Returns `undefined` when there are no linked translations.\n */\nfunction buildLanguageAlternates(\n translations: Translation[] | undefined,\n currentLang?: string,\n): Metadata['alternates'] | undefined {\n const languages: Record<string, string> = {};\n let canonical: string | undefined;\n\n for (const t of translations ?? []) {\n const shortcode = t.language?.shortcode;\n if (shortcode && t.href) {\n languages[shortcode] = t.href;\n if (currentLang && shortcode === currentLang) {\n canonical = t.href;\n }\n }\n }\n\n if (Object.keys(languages).length === 0) {\n return undefined;\n }\n\n return canonical ? { canonical, languages } : { languages };\n}\n\n/**\n * Internal helper to wrap and cache entity resolvers\n */\nfunction createCachedEntityResolver<T>(\n resolver: EntityResolver<T>\n): (props: EntityRouteParams<T>) => Promise<Entity> {\n return cache(async ({ params }: EntityRouteParams<T>) => {\n const entity = await resolver(params);\n\n if (!entity) {\n notFound();\n }\n\n return entity;\n });\n}\n\n// ─── Server Components ───────────────────────────────────────────────────────\n\n/**\n * NitroDebugInfo Component\n *\n * Async server component that outputs debug information as an HTML comment.\n * Resolves config internally from the flyo instance.\n *\n * @example\n * ```tsx\n * <NitroDebugInfo flyo={flyo} />\n * ```\n */\nexport async function NitroDebugInfo({ flyo }: { flyo: FlyoInstance }) {\n try {\n const config = await flyo.getNitroConfig();\n const { state } = flyo;\n\n const mode = readEnv(\"NODE_ENV\", \"-\");\n const vercelDeploymentId = readEnv(\"VERCEL_DEPLOYMENT_ID\", \"-\");\n const vercelGitCommitSha = readEnv(\"VERCEL_GIT_COMMIT_SHA\", \"-\");\n const version = readEnv(\"VERSION\", \"\");\n\n const tokenValue = state.accessToken || \"\";\n const token = typeof tokenValue === \"string\" ? tokenValue : \"\";\n const tokenType = token.startsWith(\"p-\")\n ? \"production\"\n : token.startsWith(\"d-\")\n ? \"develop\"\n : \"unknown\";\n\n const debug = state.liveEdit;\n\n const apiVersion = config.nitro?.version?.toString() || \"-\";\n const apiLastUpdate = config.nitro?.updated_at\n ? new Date(config.nitro.updated_at * 1000).toLocaleString(\"de-CH\", {\n day: \"2-digit\",\n month: \"2-digit\",\n year: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n })\n : \"-\";\n\n const debugInfoParts = [\n `liveedit:${debug}`,\n `env:${mode}`,\n `version:${apiVersion}`,\n `versiondate:${apiLastUpdate}`,\n `tokentype:${tokenType}`,\n `did:${vercelDeploymentId}`,\n `csha:${vercelGitCommitSha}`,\n ];\n\n if (version) {\n debugInfoParts.push(`release:${version}`);\n }\n\n const debugInfo = debugInfoParts.join(\" | \");\n\n return (\n <template dangerouslySetInnerHTML={{ __html: `<!-- ${debugInfo} -->` }} suppressHydrationWarning />\n );\n } catch {\n return <template dangerouslySetInnerHTML={{ __html: `<!-- nitro-debug: not initialized -->` }} suppressHydrationWarning />;\n }\n}\n\n/**\n * Renders a JSON-LD structured data script tag from an Entity's jsonld field.\n * Safely escapes HTML to prevent XSS.\n */\nexport function NitroEntityJsonLd({ entity }: { entity: Entity }) {\n if (!entity?.jsonld) {\n return null;\n }\n\n return (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{\n __html: JSON.stringify(entity.jsonld).replace(/</g, '\\\\u003c'),\n }}\n />\n );\n}\n\n/**\n * NitroPage renders all blocks from a Flyo page.\n */\nexport function NitroPage({\n page,\n flyo,\n}: {\n page: Page;\n flyo: FlyoInstance;\n}) {\n if (!page?.json || !Array.isArray(page.json)) {\n return null;\n }\n\n return (\n <>\n {page.json.map((block: Block, index: number) => (\n <NitroBlock\n key={block.uid || index}\n block={block}\n flyo={flyo}\n />\n ))}\n </>\n );\n}\n\n/**\n * NitroBlock renders a single block using the registered component.\n */\nexport function NitroBlock({\n block,\n flyo,\n}: {\n block: Block;\n flyo: FlyoInstance;\n}) {\n if (!block) {\n return null;\n }\n\n const Component = block.component ? flyo.state.components[block.component] : undefined;\n\n if (Component) {\n return <Component block={block} />;\n }\n\n if (flyo.state.showMissingComponentAlert) {\n return (\n <div style={{ border: '1px solid #fff', padding: '1rem', marginBottom: '1rem', backgroundColor: 'red' }}>\n Component <b>{block.component}</b> not found.\n </div>\n );\n }\n\n return null;\n}\n\n/**\n * NitroSlot renders nested blocks from a slot.\n * Used for recursive block rendering when blocks contain slots.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { NitroSlot } from '@flyo/nitro-next/server';\n *\n * export function MyComponent({ block }) {\n * return (\n * <div>\n * <NitroSlot slot={block.slots?.content} flyo={flyo} />\n * </div>\n * );\n * }\n * ```\n */\nexport function NitroSlot({\n slot,\n flyo,\n}: {\n slot?: {\n content?: Block[];\n };\n flyo: FlyoInstance;\n}) {\n if (!slot?.content || !Array.isArray(slot.content)) {\n return null;\n }\n\n return (\n <>\n {slot.content.map((block: Block, index: number) => (\n <NitroBlock\n key={block.uid || index}\n block={block}\n flyo={flyo}\n />\n ))}\n </>\n );\n}\n\n// ─── Factory Functions ───────────────────────────────────────────────────────\n\n/**\n * Create a page route handler for Nitro pages.\n * Returns a Next.js page component function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroPageRoute } from '@flyo/nitro-next/server';\n *\n * export default nitroPageRoute(flyo);\n * ```\n */\nexport function nitroPageRoute(flyo: FlyoInstance) {\n async function pageRoute(props: RouteParams) {\n const { page } = await flyo.pageResolveRoute(props);\n return <NitroPage page={page} flyo={flyo} />;\n }\n return pageRoute;\n}\n\n/**\n * Create a metadata generator for Nitro pages.\n * Returns a Next.js generateMetadata function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroPageGenerateMetadata } from '@flyo/nitro-next/server';\n *\n * export const generateMetadata = nitroPageGenerateMetadata(flyo);\n * ```\n */\nexport function nitroPageGenerateMetadata(flyo: FlyoInstance) {\n return async (props: RouteParams): Promise<Metadata> => {\n const { page, lang } = await flyo.pageResolveRoute(props);\n\n const meta = page.meta_json;\n const title = meta?.title || page.title || '';\n const description = meta?.description ?? '';\n const image = meta?.image ?? '';\n\n const ogImage = image ? `${image}/thumb/1200x630?format=jpg` : undefined;\n const twImage = image ? `${image}/thumb/1200x600?format=jpg` : undefined;\n\n const alternates = buildLanguageAlternates(page.translation, lang);\n\n return {\n title,\n description,\n ...(alternates ? { alternates } : {}),\n openGraph: {\n title,\n description,\n images: ogImage ? [ogImage] : [],\n type: 'website',\n },\n twitter: {\n card: 'summary_large_image',\n title,\n description,\n images: twImage ? [twImage] : [],\n },\n };\n };\n}\n\n/**\n * Create a static params generator for Nitro pages.\n * Returns a Next.js generateStaticParams function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroPageGenerateStaticParams } from '@flyo/nitro-next/server';\n *\n * export const generateStaticParams = nitroPageGenerateStaticParams(flyo);\n * ```\n */\nexport function nitroPageGenerateStaticParams(flyo: FlyoInstance) {\n return async () => {\n const cfg = await flyo.getNitroConfig();\n const pages = cfg.pages ?? [];\n\n return pages.map((path: string) => ({\n slug: path === '' ? undefined : path.split('/'),\n }));\n };\n}\n\n/**\n * Create an entity route handler with a custom resolver.\n * Returns a Next.js page component function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroEntityRoute } from '@flyo/nitro-next/server';\n *\n * const resolver = async (params) => {\n * const { slug } = await params;\n * return flyo.getNitroEntities().entityBySlug({ slug, typeId: 123 });\n * };\n *\n * export default nitroEntityRoute(flyo, {\n * resolver,\n * render: (entity) => <h1>{entity.entity?.entity_title}</h1>,\n * });\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function nitroEntityRoute<T = any>(\n flyo: FlyoInstance,\n options: {\n resolver: EntityResolver<T>;\n render?: (entity: Entity) => React.ReactNode;\n }\n) {\n const cachedResolver = createCachedEntityResolver(options.resolver);\n\n async function entityRoute(props: EntityRouteParams<T>) {\n const entity = await cachedResolver(props);\n\n if (options.render) {\n return options.render(entity);\n }\n\n return <div>{entity.entity?.entity_title}</div>;\n }\n return entityRoute;\n}\n\n/**\n * Create a metadata generator for entity detail pages.\n * Returns a Next.js generateMetadata function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroEntityGenerateMetadata } from '@flyo/nitro-next/server';\n *\n * export const generateMetadata = nitroEntityGenerateMetadata(flyo, { resolver });\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function nitroEntityGenerateMetadata<T = any>(\n flyo: FlyoInstance,\n options: {\n resolver: EntityResolver<T>;\n }\n) {\n const cachedResolver = createCachedEntityResolver(options.resolver);\n\n return async (props: EntityRouteParams<T>): Promise<Metadata> => {\n const entity = await cachedResolver(props);\n\n const title = entity.entity?.entity_title || '';\n const description = entity.entity?.entity_teaser ?? '';\n const image = entity.entity?.entity_image ?? '';\n\n const ogImage = image ? `${image}/thumb/1200x630?format=jpg` : undefined;\n const twImage = image ? `${image}/thumb/1200x600?format=jpg` : undefined;\n\n const alternates = buildLanguageAlternates(entity.translation, entity.language);\n\n return {\n title,\n description,\n ...(alternates ? { alternates } : {}),\n openGraph: {\n title,\n description,\n images: ogImage ? [ogImage] : [],\n type: 'website',\n },\n twitter: {\n card: 'summary_large_image',\n title,\n description,\n images: twImage ? [twImage] : [],\n },\n };\n };\n}","import type { Translation } from '@flyo/nitro-typescript';\n\n/**\n * A single language option for a language switcher, derived from a page's or\n * entity's `translation[]`. Framework-agnostic plain data — render it however\n * you like.\n */\nexport interface FlyoLanguageLink {\n /** Locale shortcode, e.g. `\"de\"`. */\n shortcode: string;\n /** Full language name from the CMS translation, e.g. `\"Deutsch\"` (when available). */\n name?: string;\n /** Fully-resolved localized href, or `null` when this locale has no linked translation. */\n href: string | null;\n /** Localized page/entity title, when available. */\n title?: string;\n /** `true` when this entry is the currently active locale. */\n isCurrent: boolean;\n /** `true` when a linked translation actually exists for this locale. */\n exists: boolean;\n}\n\n/**\n * Map a page's or entity's `translation[]` into a switcher-ready array of typed\n * links — the building block for a language switcher.\n *\n * Flyo only returns `translation` entries for languages that actually have a\n * translation. Pass `options.locales` (e.g. `flyo.state.locales`) to also get an\n * entry for every configured locale that is *missing* a translation — those come\n * back as `{ href: null, exists: false }` so you can render a fallback (a disabled\n * item, a link to the home page, …).\n *\n * Pure — no React or server-only APIs, so it is safe to call from server *or*\n * client components.\n *\n * @example\n * ```tsx\n * const links = getLanguageLinks(page.translation, {\n * currentLang: lang,\n * locales: flyo.state.locales,\n * });\n * // links.map(l => l.exists\n * // ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>\n * // : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>)\n * ```\n */\nexport function getLanguageLinks(\n translations: Translation[] | undefined,\n options?: { currentLang?: string; locales?: string[] },\n): FlyoLanguageLink[] {\n const currentLang = options?.currentLang;\n\n const byShortcode = new Map<string, Translation>();\n for (const t of translations ?? []) {\n const shortcode = t.language?.shortcode;\n if (shortcode) {\n byShortcode.set(shortcode, t);\n }\n }\n\n const toLink = (shortcode: string, t: Translation | undefined): FlyoLanguageLink => ({\n shortcode,\n name: t?.language?.name,\n href: t?.href ?? null,\n title: t?.title,\n isCurrent: shortcode === currentLang,\n exists: t?.href != null,\n });\n\n // When the full set of locales is known, emit an entry for every one so\n // callers can render fallbacks for languages that have no translation.\n if (options?.locales && options.locales.length > 0) {\n return options.locales.map((shortcode) => toLink(shortcode, byShortcode.get(shortcode)));\n }\n\n // Otherwise, just surface the translations that exist.\n return (translations ?? [])\n .filter((t) => t.language?.shortcode)\n .map((t) => toLink(t.language!.shortcode!, t));\n}\n"],"mappings":"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","names":["lang"]}
1
+ {"version":3,"sources":["../src/server.tsx","../src/i18n.ts"],"sourcesContent":["import { cache } from 'react';\nimport type { Metadata, MetadataRoute } from 'next';\nimport { notFound } from 'next/navigation';\nimport { headers } from 'next/headers';\nimport {\n Page,\n Block,\n Entity,\n ConfigApi,\n ConfigResponse,\n Configuration,\n PagesApi,\n EntitiesApi,\n SitemapApi,\n SearchApi,\n type Translation\n} from '@flyo/nitro-typescript';\n\n// Re-export the framework-agnostic language-links helper + types so they are\n// available from `@flyo/nitro-next/server` alongside the rest of the server API.\n// (Also imported locally below, for the request-scoped switcher store.)\nimport { getLanguageLinks, type FlyoLanguageLink } from './i18n';\nexport { getLanguageLinks };\nexport type { FlyoLanguageLink };\nexport type { Translation } from '@flyo/nitro-typescript';\n\n/**\n * Read-only configuration state\n */\nexport interface NitroState {\n readonly accessToken: string;\n /** The single default language (back-compat). Prefer `defaultLocale` for i18n. */\n readonly lang: string | null;\n /** All locale shortcodes the site supports, e.g. `['de', 'en']`. Empty for single-language setups. */\n readonly locales: string[];\n /** The primary/default locale (no URL prefix). Matches `config.nitro.primary_language`. */\n readonly defaultLocale: string | null;\n readonly baseUrl: string | null;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly components: Record<string, any>;\n readonly showMissingComponentAlert: boolean;\n readonly liveEdit: boolean;\n readonly serverCacheTtl: number;\n readonly clientCacheTtl: number;\n}\n\n/**\n * Route params type for Next.js catch-all routes\n */\ntype RouteParams = {\n params: Promise<{ slug?: string[] }>;\n};\n\n/**\n * Generic route params type for entity routes\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype EntityRouteParams<T = any> = {\n params: Promise<T>;\n};\n\n/**\n * Entity resolver function type\n * Users provide this to resolve entities from their route params\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type EntityResolver<T = any> = (params: Promise<T>) => Promise<Entity>;\n\n/**\n * The Flyo instance returned by initNitro().\n * Carries all API access methods and configuration state.\n */\nexport interface FlyoInstance {\n /** Read-only configuration state */\n readonly state: NitroState;\n /**\n * Fetch and cache the Nitro CMS configuration (React-cached per requested locale).\n * When `lang` is omitted, the active request locale is used (from the `x-flyo-locale`\n * header set by the proxy), falling back to `defaultLocale`.\n */\n getNitroConfig(lang?: string): Promise<ConfigResponse>;\n /** Create a PagesApi client */\n getNitroPages(): PagesApi;\n /** Create an EntitiesApi client */\n getNitroEntities(): EntitiesApi;\n /** Create a SitemapApi client */\n getNitroSitemap(): SitemapApi;\n /** Create a SearchApi client */\n getNitroSearch(): SearchApi;\n /**\n * Resolve the active request locale from the `x-flyo-locale` header (set by the\n * proxy middleware), falling back to `defaultLocale`. Use it inside entity\n * resolvers or to set `<html lang>`.\n */\n getRequestLocale(): Promise<string | undefined>;\n /** Resolve a page from catch-all route params (React-cached per request) */\n pageResolveRoute(props: RouteParams): Promise<{ page: Page; path: string; lang: string | undefined; cfg: ConfigResponse }>;\n /** Generate a Next.js sitemap from Flyo Nitro content */\n sitemap(): Promise<MetadataRoute.Sitemap>;\n}\n\n/**\n * Initialize and return a FlyoInstance.\n *\n * Call once in your flyo.config.tsx and export the result.\n * The returned object carries all methods bound to the configuration,\n * eliminating the need for global state.\n *\n * @example\n * ```tsx\n * // flyo.config.tsx\n * import { initNitro } from '@flyo/nitro-next/server';\n *\n * export const flyo = initNitro({\n * accessToken: process.env.FLYO_ACCESS_TOKEN || '',\n * lang: 'en',\n * baseUrl: process.env.SITE_URL || 'http://localhost:3000',\n * liveEdit: process.env.FLYO_LIVE_EDIT === 'true',\n * components: { HeroBanner, Text },\n * });\n * ```\n */\nexport function initNitro({\n accessToken,\n lang,\n locales,\n defaultLocale,\n baseUrl,\n components,\n showMissingComponentAlert,\n liveEdit,\n serverCacheTtl,\n clientCacheTtl,\n}: {\n accessToken: string;\n lang?: string;\n /** All supported locale shortcodes, e.g. `['de', 'en']`. Enables per-request i18n. */\n locales?: string[];\n /** The primary/default locale (no URL prefix). Defaults to `lang`. Should match `config.nitro.primary_language`. */\n defaultLocale?: string;\n baseUrl?: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n components?: Record<string, any>;\n showMissingComponentAlert?: boolean;\n liveEdit?: boolean;\n serverCacheTtl?: number;\n clientCacheTtl?: number;\n}): FlyoInstance {\n const configuration = new Configuration({ apiKey: accessToken });\n\n const resolvedDefaultLocale = defaultLocale ?? lang ?? null;\n const resolvedLocales = locales ?? (resolvedDefaultLocale ? [resolvedDefaultLocale] : []);\n\n const state: NitroState = {\n accessToken,\n lang: lang ?? null,\n locales: resolvedLocales,\n defaultLocale: resolvedDefaultLocale,\n baseUrl: baseUrl ?? null,\n components: components ?? {},\n showMissingComponentAlert: showMissingComponentAlert ?? liveEdit ?? false,\n liveEdit: liveEdit ?? false,\n serverCacheTtl: serverCacheTtl ?? 1200,\n clientCacheTtl: clientCacheTtl ?? 900,\n };\n\n const getRequestLocale = async (): Promise<string | undefined> => {\n try {\n const requestHeaders = await headers();\n const headerLocale = requestHeaders.get('x-flyo-locale');\n if (headerLocale) {\n return headerLocale;\n }\n } catch {\n // `headers()` throws outside a request scope (build time, tests, …) — fall back below.\n }\n return state.defaultLocale ?? state.lang ?? undefined;\n };\n\n // React-cached per requested locale, so different languages don't collide on\n // one memoized value and the same locale is only fetched once per request.\n const fetchConfig = cache(async (lang?: string): Promise<ConfigResponse> => {\n const configApi = new ConfigApi(configuration);\n return configApi.config({ lang: lang ?? undefined });\n });\n\n const getNitroConfig = async (lang?: string): Promise<ConfigResponse> => {\n const useLang = lang ?? (await getRequestLocale());\n return fetchConfig(useLang);\n };\n\n const pageResolveRoute = cache(async ({ params }: RouteParams) => {\n const { slug } = await params;\n const segments = slug ?? [];\n // Pages are addressed by their full, globally-unique slug (locale prefix\n // included), which is exactly what the catch-all captures — pass it through.\n const path = segments.join('/');\n\n // Derive the active locale from the leading path segment when it is a\n // configured locale (matches the Laravel adapter's `request()->segment(1)`).\n const firstSegment = segments[0];\n const lang = firstSegment && state.locales.includes(firstSegment)\n ? firstSegment\n : (state.defaultLocale ?? undefined);\n\n // Every `notFound()` below hands the request off to `not-found.tsx`, but this\n // route still owns the request-scoped language-links store (see the store\n // notes below). Publish a fallback — one disabled entry per configured\n // locale — *before* bailing, so a switcher in shared chrome awaiting\n // `readLanguageLinks()` resolves instead of hanging. Doing it here (and\n // never in `not-found.tsx`) also keeps the fallback from racing a real\n // route's links: the App Router renders the root `not-found.tsx` even on\n // 200s, so publishing there would poison pages that *do* have translations.\n const publishNotFoundFallback = () =>\n publishLanguageLinks(getLanguageLinks(undefined, { currentLang: lang, locales: state.locales }));\n\n const cfg = await getNitroConfig(lang);\n\n if (!cfg.pages?.includes(path)) {\n publishNotFoundFallback();\n notFound();\n }\n\n const page = await new PagesApi(configuration)\n .page({ slug: path, lang })\n .catch((error: unknown) => {\n console.error('Error fetching page:', path, error);\n publishNotFoundFallback();\n notFound();\n });\n\n if (!page) {\n publishNotFoundFallback();\n notFound();\n }\n\n // Publish this page's switcher links so a switcher in shared chrome (e.g. a\n // footer in the root layout) can read them via `readLanguageLinks()`.\n publishLanguageLinks(getLanguageLinks(page.translation, { currentLang: lang, locales: state.locales }));\n\n return { page, path, lang, cfg };\n });\n\n const sitemap = async (): Promise<MetadataRoute.Sitemap> => {\n const sitemapApi = new SitemapApi(configuration);\n const sitemapLang = state.lang ?? undefined;\n\n if (!state.baseUrl) {\n throw new Error('baseUrl is not configured. Please set it in initNitro().');\n }\n\n const items = await sitemapApi.sitemap({ lang: sitemapLang });\n const cleanBaseUrl = state.baseUrl.endsWith('/') ? state.baseUrl.slice(0, -1) : state.baseUrl;\n\n return items.map((item) => {\n let path = '';\n\n if (item.routes && typeof item.routes === 'object') {\n const routeValues = Object.values(item.routes);\n if (routeValues.length > 0) {\n path = routeValues[0];\n }\n }\n\n if (!path && item.entity_slug) {\n path = item.entity_slug;\n }\n\n const cleanPath = path && !path.startsWith('/') ? `/${path}` : path;\n\n return {\n url: `${cleanBaseUrl}${cleanPath}`,\n lastModified: new Date(),\n };\n });\n };\n\n return {\n state,\n getNitroConfig,\n getRequestLocale,\n getNitroPages: () => new PagesApi(configuration),\n getNitroEntities: () => new EntitiesApi(configuration),\n getNitroSitemap: () => new SitemapApi(configuration),\n getNitroSearch: () => new SearchApi(configuration),\n pageResolveRoute,\n sitemap,\n };\n}\n\n// ─── Request-scoped language-switcher store ──────────────────────────────────\n//\n// A page's / entity's `translation[]` is resolved deep in the route tree (in the\n// catch-all page route or an entity detail route). A language switcher, however,\n// usually lives in *shared chrome* — a footer, say — that is rendered by the\n// **root layout**. In the App Router the root layout is an *ancestor* of the\n// page, so it cannot receive the page's data as props: data only flows down.\n//\n// This store bridges that gap. The page/entity route **publishes** its resolved\n// links; the switcher in the footer **reads** them. It is a per-request deferred,\n// created once per request via React `cache()` (so the layout and the page see\n// the *same* instance), holding a promise the footer awaits.\n//\n// Why the reader awaits a promise instead of reading a plain value: the root\n// layout (and therefore its footer) renders *above*, and concurrently with, the\n// page — so a synchronous read could run before the page has resolved. Awaiting\n// suspends the footer until the links are published, regardless of render order.\n\nexport interface LanguageLinksStore {\n /**\n * Publish the links once. Later calls are ignored (first publish wins).\n *\n * The active page/entity route is the single writer per request: it publishes\n * the resolved translations on success and a fallback before any `notFound()`.\n * \"First wins\" then only guards against the route's render pass and its\n * `generateMetadata` both publishing (they publish the same links) — no other\n * caller races it.\n */\n publish(links: FlyoLanguageLink[]): void;\n /** A promise that resolves when the links are published. */\n read(): Promise<FlyoLanguageLink[]>;\n}\n\n/**\n * Create a single deferred language-links channel: one writer, many readers,\n * order-independent. Low-level — most code uses {@link publishLanguageLinks} /\n * {@link readLanguageLinks}, which wrap this in a per-request `cache()`.\n */\nexport function createLanguageLinksStore(): LanguageLinksStore {\n let resolve!: (links: FlyoLanguageLink[]) => void;\n const promise = new Promise<FlyoLanguageLink[]>((r) => {\n resolve = r;\n });\n let settled = false;\n return {\n publish(links: FlyoLanguageLink[]) {\n // First publish wins — a route resolves once per request, and both the\n // render pass and `generateMetadata` share this store, so guard against a\n // double resolve.\n if (!settled) {\n settled = true;\n resolve(links);\n }\n },\n read() {\n return promise;\n },\n };\n}\n\n// `cache()` gives one store per request, shared across the whole server\n// component tree for that request. A fresh request gets a fresh store.\nconst languageLinksStore = cache(createLanguageLinksStore);\n\n/**\n * Publish the current route's language-switcher links into the request-scoped\n * store, so a switcher rendered in shared chrome (e.g. a footer in the root\n * layout) can read them via {@link readLanguageLinks}.\n *\n * The Flyo route helpers call this for you: `pageResolveRoute` (and therefore\n * `nitroPageRoute`) publishes the page's links, and `nitroEntityRoute` /\n * `nitroEntityGenerateMetadata` publish the entity's. Crucially they also\n * publish a fallback on their `notFound()` paths, so a genuine 404 that renders\n * `not-found.tsx` still settles the store — the footer never waits forever.\n *\n * **Do not publish from `not-found.tsx`.** In the App Router the root not-found\n * boundary is rendered on *every* request, not only on real 404s, and it renders\n * synchronously — ahead of a route's `await`ed CMS fetch. With a first-wins\n * store, a publish there would settle the store with the fallback before the\n * real route publishes, so pages that *do* have translations would show the\n * home/fallback links. Let the route own the publish instead.\n *\n * You only need to call this yourself on a **custom route Flyo does not\n * resolve** — a hand-written page that renders the shared switcher without going\n * through the helpers above. Publish that route's real links, or a fallback via\n * `getLanguageLinks(undefined, { currentLang, locales })`.\n *\n * Only the first call per request takes effect.\n */\nexport function publishLanguageLinks(links: FlyoLanguageLink[]): void {\n languageLinksStore().publish(links);\n}\n\n/**\n * Await the current route's language-switcher links from shared chrome — a\n * footer, header, or any component in the root layout. Resolves with whatever\n * the active page/entity route passed to {@link publishLanguageLinks}.\n *\n * Because it awaits, the reader suspends until the links are published, so it\n * works no matter whether the layout or the page renders first. Wrap the reading\n * component in `<Suspense>` so the rest of the layout can stream while it waits.\n *\n * @example\n * ```tsx\n * // components/LanguageSwitcher.tsx (server component)\n * import { readLanguageLinks } from '@flyo/nitro-next/server';\n *\n * export async function LanguageSwitcher() {\n * const links = await readLanguageLinks();\n * return (\n * <nav aria-label=\"Language\">\n * {links.map((l) =>\n * l.exists\n * ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>\n * : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>,\n * )}\n * </nav>\n * );\n * }\n * ```\n */\nexport function readLanguageLinks(): Promise<FlyoLanguageLink[]> {\n return languageLinksStore().read();\n}\n\n// ─── Helper ──────────────────────────────────────────────────────────────────\n\n/**\n * Helper function to read environment variables with fallback\n */\nconst readEnv = (key: string, fallback = \"\"): string => {\n const value = process.env[key];\n if (value !== undefined && value !== \"\") {\n return String(value);\n }\n return fallback;\n};\n\n/**\n * Build Next.js `alternates` (hreflang + canonical) from a page's or entity's\n * `translation[]`. Returns `undefined` when there are no linked translations.\n */\nfunction buildLanguageAlternates(\n translations: Translation[] | undefined,\n currentLang?: string,\n): Metadata['alternates'] | undefined {\n const languages: Record<string, string> = {};\n let canonical: string | undefined;\n\n for (const t of translations ?? []) {\n const shortcode = t.language?.shortcode;\n if (shortcode && t.href) {\n languages[shortcode] = t.href;\n if (currentLang && shortcode === currentLang) {\n canonical = t.href;\n }\n }\n }\n\n if (Object.keys(languages).length === 0) {\n return undefined;\n }\n\n return canonical ? { canonical, languages } : { languages };\n}\n\n/**\n * Internal helper to wrap and cache entity resolvers.\n *\n * Also publishes the entity's language-switcher links into the request-scoped\n * store (see {@link publishLanguageLinks}) so a switcher in shared chrome can\n * read them via `readLanguageLinks()`.\n */\nfunction createCachedEntityResolver<T>(\n flyo: FlyoInstance,\n resolver: EntityResolver<T>\n): (props: EntityRouteParams<T>) => Promise<Entity> {\n return cache(async ({ params }: EntityRouteParams<T>) => {\n // As with `pageResolveRoute`, this resolver owns the request's language-links\n // store even when it bails. Publish a fallback before every not-found path\n // (a missing entity, a `notFound()` from the resolver, a failed CMS fetch)\n // so a switcher in shared chrome resolves instead of hanging — and never\n // publish from `not-found.tsx`, which the App Router also renders on 200s.\n const publishNotFoundFallback = async () =>\n publishLanguageLinks(\n getLanguageLinks(undefined, {\n currentLang: await flyo.getRequestLocale(),\n locales: flyo.state.locales,\n }),\n );\n\n let entity: Entity;\n try {\n entity = await resolver(params);\n } catch (error) {\n await publishNotFoundFallback();\n throw error;\n }\n\n if (!entity) {\n await publishNotFoundFallback();\n notFound();\n }\n\n publishLanguageLinks(\n getLanguageLinks(entity.translation, { currentLang: entity.language, locales: flyo.state.locales }),\n );\n\n return entity;\n });\n}\n\n// ─── Server Components ───────────────────────────────────────────────────────\n\n/**\n * NitroDebugInfo Component\n *\n * Async server component that outputs debug information as an HTML comment.\n * Resolves config internally from the flyo instance.\n *\n * @example\n * ```tsx\n * <NitroDebugInfo flyo={flyo} />\n * ```\n */\nexport async function NitroDebugInfo({ flyo }: { flyo: FlyoInstance }) {\n try {\n const config = await flyo.getNitroConfig();\n const { state } = flyo;\n\n const mode = readEnv(\"NODE_ENV\", \"-\");\n const vercelDeploymentId = readEnv(\"VERCEL_DEPLOYMENT_ID\", \"-\");\n const vercelGitCommitSha = readEnv(\"VERCEL_GIT_COMMIT_SHA\", \"-\");\n const version = readEnv(\"VERSION\", \"\");\n\n const tokenValue = state.accessToken || \"\";\n const token = typeof tokenValue === \"string\" ? tokenValue : \"\";\n const tokenType = token.startsWith(\"p-\")\n ? \"production\"\n : token.startsWith(\"d-\")\n ? \"develop\"\n : \"unknown\";\n\n const debug = state.liveEdit;\n\n const apiVersion = config.nitro?.version?.toString() || \"-\";\n const apiLastUpdate = config.nitro?.updated_at\n ? new Date(config.nitro.updated_at * 1000).toLocaleString(\"de-CH\", {\n day: \"2-digit\",\n month: \"2-digit\",\n year: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n })\n : \"-\";\n\n const debugInfoParts = [\n `liveedit:${debug}`,\n `env:${mode}`,\n `version:${apiVersion}`,\n `versiondate:${apiLastUpdate}`,\n `tokentype:${tokenType}`,\n `did:${vercelDeploymentId}`,\n `csha:${vercelGitCommitSha}`,\n ];\n\n if (version) {\n debugInfoParts.push(`release:${version}`);\n }\n\n const debugInfo = debugInfoParts.join(\" | \");\n\n return (\n <template dangerouslySetInnerHTML={{ __html: `<!-- ${debugInfo} -->` }} suppressHydrationWarning />\n );\n } catch {\n return <template dangerouslySetInnerHTML={{ __html: `<!-- nitro-debug: not initialized -->` }} suppressHydrationWarning />;\n }\n}\n\n/**\n * Renders a JSON-LD structured data script tag from an Entity's jsonld field.\n * Safely escapes HTML to prevent XSS.\n */\nexport function NitroEntityJsonLd({ entity }: { entity: Entity }) {\n if (!entity?.jsonld) {\n return null;\n }\n\n return (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{\n __html: JSON.stringify(entity.jsonld).replace(/</g, '\\\\u003c'),\n }}\n />\n );\n}\n\n/**\n * NitroPage renders all blocks from a Flyo page.\n */\nexport function NitroPage({\n page,\n flyo,\n}: {\n page: Page;\n flyo: FlyoInstance;\n}) {\n if (!page?.json || !Array.isArray(page.json)) {\n return null;\n }\n\n return (\n <>\n {page.json.map((block: Block, index: number) => (\n <NitroBlock\n key={block.uid || index}\n block={block}\n flyo={flyo}\n />\n ))}\n </>\n );\n}\n\n/**\n * NitroBlock renders a single block using the registered component.\n */\nexport function NitroBlock({\n block,\n flyo,\n}: {\n block: Block;\n flyo: FlyoInstance;\n}) {\n if (!block) {\n return null;\n }\n\n const Component = block.component ? flyo.state.components[block.component] : undefined;\n\n if (Component) {\n return <Component block={block} />;\n }\n\n if (flyo.state.showMissingComponentAlert) {\n return (\n <div style={{ border: '1px solid #fff', padding: '1rem', marginBottom: '1rem', backgroundColor: 'red' }}>\n Component <b>{block.component}</b> not found.\n </div>\n );\n }\n\n return null;\n}\n\n/**\n * NitroSlot renders nested blocks from a slot.\n * Used for recursive block rendering when blocks contain slots.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { NitroSlot } from '@flyo/nitro-next/server';\n *\n * export function MyComponent({ block }) {\n * return (\n * <div>\n * <NitroSlot slot={block.slots?.content} flyo={flyo} />\n * </div>\n * );\n * }\n * ```\n */\nexport function NitroSlot({\n slot,\n flyo,\n}: {\n slot?: {\n content?: Block[];\n };\n flyo: FlyoInstance;\n}) {\n if (!slot?.content || !Array.isArray(slot.content)) {\n return null;\n }\n\n return (\n <>\n {slot.content.map((block: Block, index: number) => (\n <NitroBlock\n key={block.uid || index}\n block={block}\n flyo={flyo}\n />\n ))}\n </>\n );\n}\n\n// ─── Factory Functions ───────────────────────────────────────────────────────\n\n/**\n * Create a page route handler for Nitro pages.\n * Returns a Next.js page component function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroPageRoute } from '@flyo/nitro-next/server';\n *\n * export default nitroPageRoute(flyo);\n * ```\n */\nexport function nitroPageRoute(flyo: FlyoInstance) {\n async function pageRoute(props: RouteParams) {\n const { page } = await flyo.pageResolveRoute(props);\n return <NitroPage page={page} flyo={flyo} />;\n }\n return pageRoute;\n}\n\n/**\n * Create a metadata generator for Nitro pages.\n * Returns a Next.js generateMetadata function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroPageGenerateMetadata } from '@flyo/nitro-next/server';\n *\n * export const generateMetadata = nitroPageGenerateMetadata(flyo);\n * ```\n */\nexport function nitroPageGenerateMetadata(flyo: FlyoInstance) {\n return async (props: RouteParams): Promise<Metadata> => {\n const { page, lang } = await flyo.pageResolveRoute(props);\n\n const meta = page.meta_json;\n const title = meta?.title || page.title || '';\n const description = meta?.description ?? '';\n const image = meta?.image ?? '';\n\n const ogImage = image ? `${image}/thumb/1200x630?format=jpg` : undefined;\n const twImage = image ? `${image}/thumb/1200x600?format=jpg` : undefined;\n\n const alternates = buildLanguageAlternates(page.translation, lang);\n\n return {\n title,\n description,\n ...(alternates ? { alternates } : {}),\n openGraph: {\n title,\n description,\n images: ogImage ? [ogImage] : [],\n type: 'website',\n },\n twitter: {\n card: 'summary_large_image',\n title,\n description,\n images: twImage ? [twImage] : [],\n },\n };\n };\n}\n\n/**\n * Create a static params generator for Nitro pages.\n * Returns a Next.js generateStaticParams function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroPageGenerateStaticParams } from '@flyo/nitro-next/server';\n *\n * export const generateStaticParams = nitroPageGenerateStaticParams(flyo);\n * ```\n */\nexport function nitroPageGenerateStaticParams(flyo: FlyoInstance) {\n return async () => {\n const cfg = await flyo.getNitroConfig();\n const pages = cfg.pages ?? [];\n\n return pages.map((path: string) => ({\n slug: path === '' ? undefined : path.split('/'),\n }));\n };\n}\n\n/**\n * Create an entity route handler with a custom resolver.\n * Returns a Next.js page component function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroEntityRoute } from '@flyo/nitro-next/server';\n *\n * const resolver = async (params) => {\n * const { slug } = await params;\n * return flyo.getNitroEntities().entityBySlug({ slug, typeId: 123 });\n * };\n *\n * export default nitroEntityRoute(flyo, {\n * resolver,\n * render: (entity) => <h1>{entity.entity?.entity_title}</h1>,\n * });\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function nitroEntityRoute<T = any>(\n flyo: FlyoInstance,\n options: {\n resolver: EntityResolver<T>;\n render?: (entity: Entity) => React.ReactNode;\n }\n) {\n const cachedResolver = createCachedEntityResolver(flyo, options.resolver);\n\n async function entityRoute(props: EntityRouteParams<T>) {\n const entity = await cachedResolver(props);\n\n if (options.render) {\n return options.render(entity);\n }\n\n return <div>{entity.entity?.entity_title}</div>;\n }\n return entityRoute;\n}\n\n/**\n * Create a metadata generator for entity detail pages.\n * Returns a Next.js generateMetadata function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroEntityGenerateMetadata } from '@flyo/nitro-next/server';\n *\n * export const generateMetadata = nitroEntityGenerateMetadata(flyo, { resolver });\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function nitroEntityGenerateMetadata<T = any>(\n flyo: FlyoInstance,\n options: {\n resolver: EntityResolver<T>;\n }\n) {\n const cachedResolver = createCachedEntityResolver(flyo, options.resolver);\n\n return async (props: EntityRouteParams<T>): Promise<Metadata> => {\n const entity = await cachedResolver(props);\n\n const title = entity.entity?.entity_title || '';\n const description = entity.entity?.entity_teaser ?? '';\n const image = entity.entity?.entity_image ?? '';\n\n const ogImage = image ? `${image}/thumb/1200x630?format=jpg` : undefined;\n const twImage = image ? `${image}/thumb/1200x600?format=jpg` : undefined;\n\n const alternates = buildLanguageAlternates(entity.translation, entity.language);\n\n return {\n title,\n description,\n ...(alternates ? { alternates } : {}),\n openGraph: {\n title,\n description,\n images: ogImage ? [ogImage] : [],\n type: 'website',\n },\n twitter: {\n card: 'summary_large_image',\n title,\n description,\n images: twImage ? [twImage] : [],\n },\n };\n };\n}","import type { Translation } from '@flyo/nitro-typescript';\n\n/**\n * A single language option for a language switcher, derived from a page's or\n * entity's `translation[]`. Framework-agnostic plain data — render it however\n * you like.\n */\nexport interface FlyoLanguageLink {\n /** Locale shortcode, e.g. `\"de\"`. */\n shortcode: string;\n /** Full language name from the CMS translation, e.g. `\"Deutsch\"` (when available). */\n name?: string;\n /** Fully-resolved localized href, or `null` when this locale has no linked translation. */\n href: string | null;\n /** Localized page/entity title, when available. */\n title?: string;\n /** `true` when this entry is the currently active locale. */\n isCurrent: boolean;\n /** `true` when a linked translation actually exists for this locale. */\n exists: boolean;\n}\n\n/**\n * Map a page's or entity's `translation[]` into a switcher-ready array of typed\n * links — the building block for a language switcher.\n *\n * Flyo only returns `translation` entries for languages that actually have a\n * translation. Pass `options.locales` (e.g. `flyo.state.locales`) to also get an\n * entry for every configured locale that is *missing* a translation — those come\n * back as `{ href: null, exists: false }` so you can render a fallback (a disabled\n * item, a link to the home page, …).\n *\n * Pure — no React or server-only APIs, so it is safe to call from server *or*\n * client components.\n *\n * This is a pure *data* helper — it maps a `translation[]` to switcher links and\n * renders nothing. **The Flyo route helpers already call it for you and publish\n * the result**, so page and entity routes need no switcher code at all; the\n * footer just reads the store (see `publishLanguageLinks` / `readLanguageLinks`\n * in `@flyo/nitro-next/server`). Call this directly only when you publish links\n * by hand from a raw `translation[]` — e.g. on a custom route Flyo doesn't\n * resolve:\n *\n * ```tsx\n * getLanguageLinks(translations, { currentLang, locales });\n * ```\n *\n * **Render each link as a native `<a>`, not `next/link`'s `<Link>`.** A language\n * switch must refresh the shared chrome — the localized nav, footer and\n * `<html lang>` — that lives in the root layout. In the Next.js App Router, soft\n * (client-side) navigation re-renders only the page segment, *not* shared\n * layouts, so a `<Link>` would leave that chrome in the previous language while\n * only the page body updates. A plain `<a>` triggers a full-document navigation,\n * forcing a fresh server render in the new locale so every part updates. (Regular\n * nav links can stay `<Link>` — the nav is identical within a language.)\n *\n * @example\n * ```tsx\n * const links = getLanguageLinks(page.translation, {\n * currentLang: lang,\n * locales: flyo.state.locales,\n * });\n * // Native <a> (full-document nav) — NOT next/link, so shared layout chrome\n * // re-renders in the new locale.\n * // links.map(l => l.exists\n * // ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>\n * // : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>)\n * ```\n */\nexport function getLanguageLinks(\n translations: Translation[] | undefined,\n options?: { currentLang?: string; locales?: string[] },\n): FlyoLanguageLink[] {\n const currentLang = options?.currentLang;\n\n const byShortcode = new Map<string, Translation>();\n for (const t of translations ?? []) {\n const shortcode = t.language?.shortcode;\n if (shortcode) {\n byShortcode.set(shortcode, t);\n }\n }\n\n const toLink = (shortcode: string, t: Translation | undefined): FlyoLanguageLink => ({\n shortcode,\n name: t?.language?.name,\n href: t?.href ?? null,\n title: t?.title,\n isCurrent: shortcode === currentLang,\n exists: t?.href != null,\n });\n\n // When the full set of locales is known, emit an entry for every one so\n // callers can render fallbacks for languages that have no translation.\n if (options?.locales && options.locales.length > 0) {\n return options.locales.map((shortcode) => toLink(shortcode, byShortcode.get(shortcode)));\n }\n\n // Otherwise, just surface the translations that exist.\n return (translations ?? [])\n .filter((t) => t.language?.shortcode)\n .map((t) => toLink(t.language!.shortcode!, t));\n}\n"],"mappings":"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","names":["lang"]}
package/dist/server.mjs CHANGED
@@ -89,17 +89,22 @@ function initNitro({
89
89
  const path = segments.join("/");
90
90
  const firstSegment = segments[0];
91
91
  const lang2 = firstSegment && state.locales.includes(firstSegment) ? firstSegment : state.defaultLocale ?? void 0;
92
+ const publishNotFoundFallback = () => publishLanguageLinks(getLanguageLinks(void 0, { currentLang: lang2, locales: state.locales }));
92
93
  const cfg = await getNitroConfig(lang2);
93
94
  if (!cfg.pages?.includes(path)) {
95
+ publishNotFoundFallback();
94
96
  notFound();
95
97
  }
96
98
  const page = await new PagesApi(configuration).page({ slug: path, lang: lang2 }).catch((error) => {
97
99
  console.error("Error fetching page:", path, error);
100
+ publishNotFoundFallback();
98
101
  notFound();
99
102
  });
100
103
  if (!page) {
104
+ publishNotFoundFallback();
101
105
  notFound();
102
106
  }
107
+ publishLanguageLinks(getLanguageLinks(page.translation, { currentLang: lang2, locales: state.locales }));
103
108
  return { page, path, lang: lang2, cfg };
104
109
  });
105
110
  const sitemap = async () => {
@@ -140,6 +145,31 @@ function initNitro({
140
145
  sitemap
141
146
  };
142
147
  }
148
+ function createLanguageLinksStore() {
149
+ let resolve;
150
+ const promise = new Promise((r) => {
151
+ resolve = r;
152
+ });
153
+ let settled = false;
154
+ return {
155
+ publish(links) {
156
+ if (!settled) {
157
+ settled = true;
158
+ resolve(links);
159
+ }
160
+ },
161
+ read() {
162
+ return promise;
163
+ }
164
+ };
165
+ }
166
+ var languageLinksStore = cache(createLanguageLinksStore);
167
+ function publishLanguageLinks(links) {
168
+ languageLinksStore().publish(links);
169
+ }
170
+ function readLanguageLinks() {
171
+ return languageLinksStore().read();
172
+ }
143
173
  var readEnv = (key, fallback = "") => {
144
174
  const value = process.env[key];
145
175
  if (value !== void 0 && value !== "") {
@@ -164,12 +194,28 @@ function buildLanguageAlternates(translations, currentLang) {
164
194
  }
165
195
  return canonical ? { canonical, languages } : { languages };
166
196
  }
167
- function createCachedEntityResolver(resolver) {
197
+ function createCachedEntityResolver(flyo, resolver) {
168
198
  return cache(async ({ params }) => {
169
- const entity = await resolver(params);
199
+ const publishNotFoundFallback = async () => publishLanguageLinks(
200
+ getLanguageLinks(void 0, {
201
+ currentLang: await flyo.getRequestLocale(),
202
+ locales: flyo.state.locales
203
+ })
204
+ );
205
+ let entity;
206
+ try {
207
+ entity = await resolver(params);
208
+ } catch (error) {
209
+ await publishNotFoundFallback();
210
+ throw error;
211
+ }
170
212
  if (!entity) {
213
+ await publishNotFoundFallback();
171
214
  notFound();
172
215
  }
216
+ publishLanguageLinks(
217
+ getLanguageLinks(entity.translation, { currentLang: entity.language, locales: flyo.state.locales })
218
+ );
173
219
  return entity;
174
220
  });
175
221
  }
@@ -323,7 +369,7 @@ function nitroPageGenerateStaticParams(flyo) {
323
369
  };
324
370
  }
325
371
  function nitroEntityRoute(flyo, options) {
326
- const cachedResolver = createCachedEntityResolver(options.resolver);
372
+ const cachedResolver = createCachedEntityResolver(flyo, options.resolver);
327
373
  async function entityRoute(props) {
328
374
  const entity = await cachedResolver(props);
329
375
  if (options.render) {
@@ -334,7 +380,7 @@ function nitroEntityRoute(flyo, options) {
334
380
  return entityRoute;
335
381
  }
336
382
  function nitroEntityGenerateMetadata(flyo, options) {
337
- const cachedResolver = createCachedEntityResolver(options.resolver);
383
+ const cachedResolver = createCachedEntityResolver(flyo, options.resolver);
338
384
  return async (props) => {
339
385
  const entity = await cachedResolver(props);
340
386
  const title = entity.entity?.entity_title || "";
@@ -368,12 +414,15 @@ export {
368
414
  NitroEntityJsonLd,
369
415
  NitroPage,
370
416
  NitroSlot,
417
+ createLanguageLinksStore,
371
418
  getLanguageLinks,
372
419
  initNitro,
373
420
  nitroEntityGenerateMetadata,
374
421
  nitroEntityRoute,
375
422
  nitroPageGenerateMetadata,
376
423
  nitroPageGenerateStaticParams,
377
- nitroPageRoute
424
+ nitroPageRoute,
425
+ publishLanguageLinks,
426
+ readLanguageLinks
378
427
  };
379
428
  //# sourceMappingURL=server.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.tsx","../src/i18n.ts"],"sourcesContent":["import { cache } from 'react';\nimport type { Metadata, MetadataRoute } from 'next';\nimport { notFound } from 'next/navigation';\nimport { headers } from 'next/headers';\nimport {\n Page,\n Block,\n Entity,\n ConfigApi,\n ConfigResponse,\n Configuration,\n PagesApi,\n EntitiesApi,\n SitemapApi,\n SearchApi,\n type Translation\n} from '@flyo/nitro-typescript';\n\n// Re-export the framework-agnostic language-links helper + types so they are\n// available from `@flyo/nitro-next/server` alongside the rest of the server API.\nexport { getLanguageLinks } from './i18n';\nexport type { FlyoLanguageLink } from './i18n';\nexport type { Translation } from '@flyo/nitro-typescript';\n\n/**\n * Read-only configuration state\n */\nexport interface NitroState {\n readonly accessToken: string;\n /** The single default language (back-compat). Prefer `defaultLocale` for i18n. */\n readonly lang: string | null;\n /** All locale shortcodes the site supports, e.g. `['de', 'en']`. Empty for single-language setups. */\n readonly locales: string[];\n /** The primary/default locale (no URL prefix). Matches `config.nitro.primary_language`. */\n readonly defaultLocale: string | null;\n readonly baseUrl: string | null;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly components: Record<string, any>;\n readonly showMissingComponentAlert: boolean;\n readonly liveEdit: boolean;\n readonly serverCacheTtl: number;\n readonly clientCacheTtl: number;\n}\n\n/**\n * Route params type for Next.js catch-all routes\n */\ntype RouteParams = {\n params: Promise<{ slug?: string[] }>;\n};\n\n/**\n * Generic route params type for entity routes\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype EntityRouteParams<T = any> = {\n params: Promise<T>;\n};\n\n/**\n * Entity resolver function type\n * Users provide this to resolve entities from their route params\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type EntityResolver<T = any> = (params: Promise<T>) => Promise<Entity>;\n\n/**\n * The Flyo instance returned by initNitro().\n * Carries all API access methods and configuration state.\n */\nexport interface FlyoInstance {\n /** Read-only configuration state */\n readonly state: NitroState;\n /**\n * Fetch and cache the Nitro CMS configuration (React-cached per requested locale).\n * When `lang` is omitted, the active request locale is used (from the `x-flyo-locale`\n * header set by the proxy), falling back to `defaultLocale`.\n */\n getNitroConfig(lang?: string): Promise<ConfigResponse>;\n /** Create a PagesApi client */\n getNitroPages(): PagesApi;\n /** Create an EntitiesApi client */\n getNitroEntities(): EntitiesApi;\n /** Create a SitemapApi client */\n getNitroSitemap(): SitemapApi;\n /** Create a SearchApi client */\n getNitroSearch(): SearchApi;\n /**\n * Resolve the active request locale from the `x-flyo-locale` header (set by the\n * proxy middleware), falling back to `defaultLocale`. Use it inside entity\n * resolvers or to set `<html lang>`.\n */\n getRequestLocale(): Promise<string | undefined>;\n /** Resolve a page from catch-all route params (React-cached per request) */\n pageResolveRoute(props: RouteParams): Promise<{ page: Page; path: string; lang: string | undefined; cfg: ConfigResponse }>;\n /** Generate a Next.js sitemap from Flyo Nitro content */\n sitemap(): Promise<MetadataRoute.Sitemap>;\n}\n\n/**\n * Initialize and return a FlyoInstance.\n *\n * Call once in your flyo.config.tsx and export the result.\n * The returned object carries all methods bound to the configuration,\n * eliminating the need for global state.\n *\n * @example\n * ```tsx\n * // flyo.config.tsx\n * import { initNitro } from '@flyo/nitro-next/server';\n *\n * export const flyo = initNitro({\n * accessToken: process.env.FLYO_ACCESS_TOKEN || '',\n * lang: 'en',\n * baseUrl: process.env.SITE_URL || 'http://localhost:3000',\n * liveEdit: process.env.FLYO_LIVE_EDIT === 'true',\n * components: { HeroBanner, Text },\n * });\n * ```\n */\nexport function initNitro({\n accessToken,\n lang,\n locales,\n defaultLocale,\n baseUrl,\n components,\n showMissingComponentAlert,\n liveEdit,\n serverCacheTtl,\n clientCacheTtl,\n}: {\n accessToken: string;\n lang?: string;\n /** All supported locale shortcodes, e.g. `['de', 'en']`. Enables per-request i18n. */\n locales?: string[];\n /** The primary/default locale (no URL prefix). Defaults to `lang`. Should match `config.nitro.primary_language`. */\n defaultLocale?: string;\n baseUrl?: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n components?: Record<string, any>;\n showMissingComponentAlert?: boolean;\n liveEdit?: boolean;\n serverCacheTtl?: number;\n clientCacheTtl?: number;\n}): FlyoInstance {\n const configuration = new Configuration({ apiKey: accessToken });\n\n const resolvedDefaultLocale = defaultLocale ?? lang ?? null;\n const resolvedLocales = locales ?? (resolvedDefaultLocale ? [resolvedDefaultLocale] : []);\n\n const state: NitroState = {\n accessToken,\n lang: lang ?? null,\n locales: resolvedLocales,\n defaultLocale: resolvedDefaultLocale,\n baseUrl: baseUrl ?? null,\n components: components ?? {},\n showMissingComponentAlert: showMissingComponentAlert ?? liveEdit ?? false,\n liveEdit: liveEdit ?? false,\n serverCacheTtl: serverCacheTtl ?? 1200,\n clientCacheTtl: clientCacheTtl ?? 900,\n };\n\n const getRequestLocale = async (): Promise<string | undefined> => {\n try {\n const requestHeaders = await headers();\n const headerLocale = requestHeaders.get('x-flyo-locale');\n if (headerLocale) {\n return headerLocale;\n }\n } catch {\n // `headers()` throws outside a request scope (build time, tests, …) — fall back below.\n }\n return state.defaultLocale ?? state.lang ?? undefined;\n };\n\n // React-cached per requested locale, so different languages don't collide on\n // one memoized value and the same locale is only fetched once per request.\n const fetchConfig = cache(async (lang?: string): Promise<ConfigResponse> => {\n const configApi = new ConfigApi(configuration);\n return configApi.config({ lang: lang ?? undefined });\n });\n\n const getNitroConfig = async (lang?: string): Promise<ConfigResponse> => {\n const useLang = lang ?? (await getRequestLocale());\n return fetchConfig(useLang);\n };\n\n const pageResolveRoute = cache(async ({ params }: RouteParams) => {\n const { slug } = await params;\n const segments = slug ?? [];\n // Pages are addressed by their full, globally-unique slug (locale prefix\n // included), which is exactly what the catch-all captures — pass it through.\n const path = segments.join('/');\n\n // Derive the active locale from the leading path segment when it is a\n // configured locale (matches the Laravel adapter's `request()->segment(1)`).\n const firstSegment = segments[0];\n const lang = firstSegment && state.locales.includes(firstSegment)\n ? firstSegment\n : (state.defaultLocale ?? undefined);\n\n const cfg = await getNitroConfig(lang);\n\n if (!cfg.pages?.includes(path)) {\n notFound();\n }\n\n const page = await new PagesApi(configuration)\n .page({ slug: path, lang })\n .catch((error: unknown) => {\n console.error('Error fetching page:', path, error);\n notFound();\n });\n\n if (!page) {\n notFound();\n }\n\n return { page, path, lang, cfg };\n });\n\n const sitemap = async (): Promise<MetadataRoute.Sitemap> => {\n const sitemapApi = new SitemapApi(configuration);\n const sitemapLang = state.lang ?? undefined;\n\n if (!state.baseUrl) {\n throw new Error('baseUrl is not configured. Please set it in initNitro().');\n }\n\n const items = await sitemapApi.sitemap({ lang: sitemapLang });\n const cleanBaseUrl = state.baseUrl.endsWith('/') ? state.baseUrl.slice(0, -1) : state.baseUrl;\n\n return items.map((item) => {\n let path = '';\n\n if (item.routes && typeof item.routes === 'object') {\n const routeValues = Object.values(item.routes);\n if (routeValues.length > 0) {\n path = routeValues[0];\n }\n }\n\n if (!path && item.entity_slug) {\n path = item.entity_slug;\n }\n\n const cleanPath = path && !path.startsWith('/') ? `/${path}` : path;\n\n return {\n url: `${cleanBaseUrl}${cleanPath}`,\n lastModified: new Date(),\n };\n });\n };\n\n return {\n state,\n getNitroConfig,\n getRequestLocale,\n getNitroPages: () => new PagesApi(configuration),\n getNitroEntities: () => new EntitiesApi(configuration),\n getNitroSitemap: () => new SitemapApi(configuration),\n getNitroSearch: () => new SearchApi(configuration),\n pageResolveRoute,\n sitemap,\n };\n}\n\n// ─── Helper ──────────────────────────────────────────────────────────────────\n\n/**\n * Helper function to read environment variables with fallback\n */\nconst readEnv = (key: string, fallback = \"\"): string => {\n const value = process.env[key];\n if (value !== undefined && value !== \"\") {\n return String(value);\n }\n return fallback;\n};\n\n/**\n * Build Next.js `alternates` (hreflang + canonical) from a page's or entity's\n * `translation[]`. Returns `undefined` when there are no linked translations.\n */\nfunction buildLanguageAlternates(\n translations: Translation[] | undefined,\n currentLang?: string,\n): Metadata['alternates'] | undefined {\n const languages: Record<string, string> = {};\n let canonical: string | undefined;\n\n for (const t of translations ?? []) {\n const shortcode = t.language?.shortcode;\n if (shortcode && t.href) {\n languages[shortcode] = t.href;\n if (currentLang && shortcode === currentLang) {\n canonical = t.href;\n }\n }\n }\n\n if (Object.keys(languages).length === 0) {\n return undefined;\n }\n\n return canonical ? { canonical, languages } : { languages };\n}\n\n/**\n * Internal helper to wrap and cache entity resolvers\n */\nfunction createCachedEntityResolver<T>(\n resolver: EntityResolver<T>\n): (props: EntityRouteParams<T>) => Promise<Entity> {\n return cache(async ({ params }: EntityRouteParams<T>) => {\n const entity = await resolver(params);\n\n if (!entity) {\n notFound();\n }\n\n return entity;\n });\n}\n\n// ─── Server Components ───────────────────────────────────────────────────────\n\n/**\n * NitroDebugInfo Component\n *\n * Async server component that outputs debug information as an HTML comment.\n * Resolves config internally from the flyo instance.\n *\n * @example\n * ```tsx\n * <NitroDebugInfo flyo={flyo} />\n * ```\n */\nexport async function NitroDebugInfo({ flyo }: { flyo: FlyoInstance }) {\n try {\n const config = await flyo.getNitroConfig();\n const { state } = flyo;\n\n const mode = readEnv(\"NODE_ENV\", \"-\");\n const vercelDeploymentId = readEnv(\"VERCEL_DEPLOYMENT_ID\", \"-\");\n const vercelGitCommitSha = readEnv(\"VERCEL_GIT_COMMIT_SHA\", \"-\");\n const version = readEnv(\"VERSION\", \"\");\n\n const tokenValue = state.accessToken || \"\";\n const token = typeof tokenValue === \"string\" ? tokenValue : \"\";\n const tokenType = token.startsWith(\"p-\")\n ? \"production\"\n : token.startsWith(\"d-\")\n ? \"develop\"\n : \"unknown\";\n\n const debug = state.liveEdit;\n\n const apiVersion = config.nitro?.version?.toString() || \"-\";\n const apiLastUpdate = config.nitro?.updated_at\n ? new Date(config.nitro.updated_at * 1000).toLocaleString(\"de-CH\", {\n day: \"2-digit\",\n month: \"2-digit\",\n year: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n })\n : \"-\";\n\n const debugInfoParts = [\n `liveedit:${debug}`,\n `env:${mode}`,\n `version:${apiVersion}`,\n `versiondate:${apiLastUpdate}`,\n `tokentype:${tokenType}`,\n `did:${vercelDeploymentId}`,\n `csha:${vercelGitCommitSha}`,\n ];\n\n if (version) {\n debugInfoParts.push(`release:${version}`);\n }\n\n const debugInfo = debugInfoParts.join(\" | \");\n\n return (\n <template dangerouslySetInnerHTML={{ __html: `<!-- ${debugInfo} -->` }} suppressHydrationWarning />\n );\n } catch {\n return <template dangerouslySetInnerHTML={{ __html: `<!-- nitro-debug: not initialized -->` }} suppressHydrationWarning />;\n }\n}\n\n/**\n * Renders a JSON-LD structured data script tag from an Entity's jsonld field.\n * Safely escapes HTML to prevent XSS.\n */\nexport function NitroEntityJsonLd({ entity }: { entity: Entity }) {\n if (!entity?.jsonld) {\n return null;\n }\n\n return (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{\n __html: JSON.stringify(entity.jsonld).replace(/</g, '\\\\u003c'),\n }}\n />\n );\n}\n\n/**\n * NitroPage renders all blocks from a Flyo page.\n */\nexport function NitroPage({\n page,\n flyo,\n}: {\n page: Page;\n flyo: FlyoInstance;\n}) {\n if (!page?.json || !Array.isArray(page.json)) {\n return null;\n }\n\n return (\n <>\n {page.json.map((block: Block, index: number) => (\n <NitroBlock\n key={block.uid || index}\n block={block}\n flyo={flyo}\n />\n ))}\n </>\n );\n}\n\n/**\n * NitroBlock renders a single block using the registered component.\n */\nexport function NitroBlock({\n block,\n flyo,\n}: {\n block: Block;\n flyo: FlyoInstance;\n}) {\n if (!block) {\n return null;\n }\n\n const Component = block.component ? flyo.state.components[block.component] : undefined;\n\n if (Component) {\n return <Component block={block} />;\n }\n\n if (flyo.state.showMissingComponentAlert) {\n return (\n <div style={{ border: '1px solid #fff', padding: '1rem', marginBottom: '1rem', backgroundColor: 'red' }}>\n Component <b>{block.component}</b> not found.\n </div>\n );\n }\n\n return null;\n}\n\n/**\n * NitroSlot renders nested blocks from a slot.\n * Used for recursive block rendering when blocks contain slots.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { NitroSlot } from '@flyo/nitro-next/server';\n *\n * export function MyComponent({ block }) {\n * return (\n * <div>\n * <NitroSlot slot={block.slots?.content} flyo={flyo} />\n * </div>\n * );\n * }\n * ```\n */\nexport function NitroSlot({\n slot,\n flyo,\n}: {\n slot?: {\n content?: Block[];\n };\n flyo: FlyoInstance;\n}) {\n if (!slot?.content || !Array.isArray(slot.content)) {\n return null;\n }\n\n return (\n <>\n {slot.content.map((block: Block, index: number) => (\n <NitroBlock\n key={block.uid || index}\n block={block}\n flyo={flyo}\n />\n ))}\n </>\n );\n}\n\n// ─── Factory Functions ───────────────────────────────────────────────────────\n\n/**\n * Create a page route handler for Nitro pages.\n * Returns a Next.js page component function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroPageRoute } from '@flyo/nitro-next/server';\n *\n * export default nitroPageRoute(flyo);\n * ```\n */\nexport function nitroPageRoute(flyo: FlyoInstance) {\n async function pageRoute(props: RouteParams) {\n const { page } = await flyo.pageResolveRoute(props);\n return <NitroPage page={page} flyo={flyo} />;\n }\n return pageRoute;\n}\n\n/**\n * Create a metadata generator for Nitro pages.\n * Returns a Next.js generateMetadata function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroPageGenerateMetadata } from '@flyo/nitro-next/server';\n *\n * export const generateMetadata = nitroPageGenerateMetadata(flyo);\n * ```\n */\nexport function nitroPageGenerateMetadata(flyo: FlyoInstance) {\n return async (props: RouteParams): Promise<Metadata> => {\n const { page, lang } = await flyo.pageResolveRoute(props);\n\n const meta = page.meta_json;\n const title = meta?.title || page.title || '';\n const description = meta?.description ?? '';\n const image = meta?.image ?? '';\n\n const ogImage = image ? `${image}/thumb/1200x630?format=jpg` : undefined;\n const twImage = image ? `${image}/thumb/1200x600?format=jpg` : undefined;\n\n const alternates = buildLanguageAlternates(page.translation, lang);\n\n return {\n title,\n description,\n ...(alternates ? { alternates } : {}),\n openGraph: {\n title,\n description,\n images: ogImage ? [ogImage] : [],\n type: 'website',\n },\n twitter: {\n card: 'summary_large_image',\n title,\n description,\n images: twImage ? [twImage] : [],\n },\n };\n };\n}\n\n/**\n * Create a static params generator for Nitro pages.\n * Returns a Next.js generateStaticParams function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroPageGenerateStaticParams } from '@flyo/nitro-next/server';\n *\n * export const generateStaticParams = nitroPageGenerateStaticParams(flyo);\n * ```\n */\nexport function nitroPageGenerateStaticParams(flyo: FlyoInstance) {\n return async () => {\n const cfg = await flyo.getNitroConfig();\n const pages = cfg.pages ?? [];\n\n return pages.map((path: string) => ({\n slug: path === '' ? undefined : path.split('/'),\n }));\n };\n}\n\n/**\n * Create an entity route handler with a custom resolver.\n * Returns a Next.js page component function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroEntityRoute } from '@flyo/nitro-next/server';\n *\n * const resolver = async (params) => {\n * const { slug } = await params;\n * return flyo.getNitroEntities().entityBySlug({ slug, typeId: 123 });\n * };\n *\n * export default nitroEntityRoute(flyo, {\n * resolver,\n * render: (entity) => <h1>{entity.entity?.entity_title}</h1>,\n * });\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function nitroEntityRoute<T = any>(\n flyo: FlyoInstance,\n options: {\n resolver: EntityResolver<T>;\n render?: (entity: Entity) => React.ReactNode;\n }\n) {\n const cachedResolver = createCachedEntityResolver(options.resolver);\n\n async function entityRoute(props: EntityRouteParams<T>) {\n const entity = await cachedResolver(props);\n\n if (options.render) {\n return options.render(entity);\n }\n\n return <div>{entity.entity?.entity_title}</div>;\n }\n return entityRoute;\n}\n\n/**\n * Create a metadata generator for entity detail pages.\n * Returns a Next.js generateMetadata function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroEntityGenerateMetadata } from '@flyo/nitro-next/server';\n *\n * export const generateMetadata = nitroEntityGenerateMetadata(flyo, { resolver });\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function nitroEntityGenerateMetadata<T = any>(\n flyo: FlyoInstance,\n options: {\n resolver: EntityResolver<T>;\n }\n) {\n const cachedResolver = createCachedEntityResolver(options.resolver);\n\n return async (props: EntityRouteParams<T>): Promise<Metadata> => {\n const entity = await cachedResolver(props);\n\n const title = entity.entity?.entity_title || '';\n const description = entity.entity?.entity_teaser ?? '';\n const image = entity.entity?.entity_image ?? '';\n\n const ogImage = image ? `${image}/thumb/1200x630?format=jpg` : undefined;\n const twImage = image ? `${image}/thumb/1200x600?format=jpg` : undefined;\n\n const alternates = buildLanguageAlternates(entity.translation, entity.language);\n\n return {\n title,\n description,\n ...(alternates ? { alternates } : {}),\n openGraph: {\n title,\n description,\n images: ogImage ? [ogImage] : [],\n type: 'website',\n },\n twitter: {\n card: 'summary_large_image',\n title,\n description,\n images: twImage ? [twImage] : [],\n },\n };\n };\n}","import type { Translation } from '@flyo/nitro-typescript';\n\n/**\n * A single language option for a language switcher, derived from a page's or\n * entity's `translation[]`. Framework-agnostic plain data — render it however\n * you like.\n */\nexport interface FlyoLanguageLink {\n /** Locale shortcode, e.g. `\"de\"`. */\n shortcode: string;\n /** Full language name from the CMS translation, e.g. `\"Deutsch\"` (when available). */\n name?: string;\n /** Fully-resolved localized href, or `null` when this locale has no linked translation. */\n href: string | null;\n /** Localized page/entity title, when available. */\n title?: string;\n /** `true` when this entry is the currently active locale. */\n isCurrent: boolean;\n /** `true` when a linked translation actually exists for this locale. */\n exists: boolean;\n}\n\n/**\n * Map a page's or entity's `translation[]` into a switcher-ready array of typed\n * links — the building block for a language switcher.\n *\n * Flyo only returns `translation` entries for languages that actually have a\n * translation. Pass `options.locales` (e.g. `flyo.state.locales`) to also get an\n * entry for every configured locale that is *missing* a translation — those come\n * back as `{ href: null, exists: false }` so you can render a fallback (a disabled\n * item, a link to the home page, …).\n *\n * Pure — no React or server-only APIs, so it is safe to call from server *or*\n * client components.\n *\n * @example\n * ```tsx\n * const links = getLanguageLinks(page.translation, {\n * currentLang: lang,\n * locales: flyo.state.locales,\n * });\n * // links.map(l => l.exists\n * // ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>\n * // : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>)\n * ```\n */\nexport function getLanguageLinks(\n translations: Translation[] | undefined,\n options?: { currentLang?: string; locales?: string[] },\n): FlyoLanguageLink[] {\n const currentLang = options?.currentLang;\n\n const byShortcode = new Map<string, Translation>();\n for (const t of translations ?? []) {\n const shortcode = t.language?.shortcode;\n if (shortcode) {\n byShortcode.set(shortcode, t);\n }\n }\n\n const toLink = (shortcode: string, t: Translation | undefined): FlyoLanguageLink => ({\n shortcode,\n name: t?.language?.name,\n href: t?.href ?? null,\n title: t?.title,\n isCurrent: shortcode === currentLang,\n exists: t?.href != null,\n });\n\n // When the full set of locales is known, emit an entry for every one so\n // callers can render fallbacks for languages that have no translation.\n if (options?.locales && options.locales.length > 0) {\n return options.locales.map((shortcode) => toLink(shortcode, byShortcode.get(shortcode)));\n }\n\n // Otherwise, just surface the translations that exist.\n return (translations ?? [])\n .filter((t) => t.language?.shortcode)\n .map((t) => toLink(t.language!.shortcode!, t));\n}\n"],"mappings":"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","names":["lang"]}
1
+ {"version":3,"sources":["../src/server.tsx","../src/i18n.ts"],"sourcesContent":["import { cache } from 'react';\nimport type { Metadata, MetadataRoute } from 'next';\nimport { notFound } from 'next/navigation';\nimport { headers } from 'next/headers';\nimport {\n Page,\n Block,\n Entity,\n ConfigApi,\n ConfigResponse,\n Configuration,\n PagesApi,\n EntitiesApi,\n SitemapApi,\n SearchApi,\n type Translation\n} from '@flyo/nitro-typescript';\n\n// Re-export the framework-agnostic language-links helper + types so they are\n// available from `@flyo/nitro-next/server` alongside the rest of the server API.\n// (Also imported locally below, for the request-scoped switcher store.)\nimport { getLanguageLinks, type FlyoLanguageLink } from './i18n';\nexport { getLanguageLinks };\nexport type { FlyoLanguageLink };\nexport type { Translation } from '@flyo/nitro-typescript';\n\n/**\n * Read-only configuration state\n */\nexport interface NitroState {\n readonly accessToken: string;\n /** The single default language (back-compat). Prefer `defaultLocale` for i18n. */\n readonly lang: string | null;\n /** All locale shortcodes the site supports, e.g. `['de', 'en']`. Empty for single-language setups. */\n readonly locales: string[];\n /** The primary/default locale (no URL prefix). Matches `config.nitro.primary_language`. */\n readonly defaultLocale: string | null;\n readonly baseUrl: string | null;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly components: Record<string, any>;\n readonly showMissingComponentAlert: boolean;\n readonly liveEdit: boolean;\n readonly serverCacheTtl: number;\n readonly clientCacheTtl: number;\n}\n\n/**\n * Route params type for Next.js catch-all routes\n */\ntype RouteParams = {\n params: Promise<{ slug?: string[] }>;\n};\n\n/**\n * Generic route params type for entity routes\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype EntityRouteParams<T = any> = {\n params: Promise<T>;\n};\n\n/**\n * Entity resolver function type\n * Users provide this to resolve entities from their route params\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type EntityResolver<T = any> = (params: Promise<T>) => Promise<Entity>;\n\n/**\n * The Flyo instance returned by initNitro().\n * Carries all API access methods and configuration state.\n */\nexport interface FlyoInstance {\n /** Read-only configuration state */\n readonly state: NitroState;\n /**\n * Fetch and cache the Nitro CMS configuration (React-cached per requested locale).\n * When `lang` is omitted, the active request locale is used (from the `x-flyo-locale`\n * header set by the proxy), falling back to `defaultLocale`.\n */\n getNitroConfig(lang?: string): Promise<ConfigResponse>;\n /** Create a PagesApi client */\n getNitroPages(): PagesApi;\n /** Create an EntitiesApi client */\n getNitroEntities(): EntitiesApi;\n /** Create a SitemapApi client */\n getNitroSitemap(): SitemapApi;\n /** Create a SearchApi client */\n getNitroSearch(): SearchApi;\n /**\n * Resolve the active request locale from the `x-flyo-locale` header (set by the\n * proxy middleware), falling back to `defaultLocale`. Use it inside entity\n * resolvers or to set `<html lang>`.\n */\n getRequestLocale(): Promise<string | undefined>;\n /** Resolve a page from catch-all route params (React-cached per request) */\n pageResolveRoute(props: RouteParams): Promise<{ page: Page; path: string; lang: string | undefined; cfg: ConfigResponse }>;\n /** Generate a Next.js sitemap from Flyo Nitro content */\n sitemap(): Promise<MetadataRoute.Sitemap>;\n}\n\n/**\n * Initialize and return a FlyoInstance.\n *\n * Call once in your flyo.config.tsx and export the result.\n * The returned object carries all methods bound to the configuration,\n * eliminating the need for global state.\n *\n * @example\n * ```tsx\n * // flyo.config.tsx\n * import { initNitro } from '@flyo/nitro-next/server';\n *\n * export const flyo = initNitro({\n * accessToken: process.env.FLYO_ACCESS_TOKEN || '',\n * lang: 'en',\n * baseUrl: process.env.SITE_URL || 'http://localhost:3000',\n * liveEdit: process.env.FLYO_LIVE_EDIT === 'true',\n * components: { HeroBanner, Text },\n * });\n * ```\n */\nexport function initNitro({\n accessToken,\n lang,\n locales,\n defaultLocale,\n baseUrl,\n components,\n showMissingComponentAlert,\n liveEdit,\n serverCacheTtl,\n clientCacheTtl,\n}: {\n accessToken: string;\n lang?: string;\n /** All supported locale shortcodes, e.g. `['de', 'en']`. Enables per-request i18n. */\n locales?: string[];\n /** The primary/default locale (no URL prefix). Defaults to `lang`. Should match `config.nitro.primary_language`. */\n defaultLocale?: string;\n baseUrl?: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n components?: Record<string, any>;\n showMissingComponentAlert?: boolean;\n liveEdit?: boolean;\n serverCacheTtl?: number;\n clientCacheTtl?: number;\n}): FlyoInstance {\n const configuration = new Configuration({ apiKey: accessToken });\n\n const resolvedDefaultLocale = defaultLocale ?? lang ?? null;\n const resolvedLocales = locales ?? (resolvedDefaultLocale ? [resolvedDefaultLocale] : []);\n\n const state: NitroState = {\n accessToken,\n lang: lang ?? null,\n locales: resolvedLocales,\n defaultLocale: resolvedDefaultLocale,\n baseUrl: baseUrl ?? null,\n components: components ?? {},\n showMissingComponentAlert: showMissingComponentAlert ?? liveEdit ?? false,\n liveEdit: liveEdit ?? false,\n serverCacheTtl: serverCacheTtl ?? 1200,\n clientCacheTtl: clientCacheTtl ?? 900,\n };\n\n const getRequestLocale = async (): Promise<string | undefined> => {\n try {\n const requestHeaders = await headers();\n const headerLocale = requestHeaders.get('x-flyo-locale');\n if (headerLocale) {\n return headerLocale;\n }\n } catch {\n // `headers()` throws outside a request scope (build time, tests, …) — fall back below.\n }\n return state.defaultLocale ?? state.lang ?? undefined;\n };\n\n // React-cached per requested locale, so different languages don't collide on\n // one memoized value and the same locale is only fetched once per request.\n const fetchConfig = cache(async (lang?: string): Promise<ConfigResponse> => {\n const configApi = new ConfigApi(configuration);\n return configApi.config({ lang: lang ?? undefined });\n });\n\n const getNitroConfig = async (lang?: string): Promise<ConfigResponse> => {\n const useLang = lang ?? (await getRequestLocale());\n return fetchConfig(useLang);\n };\n\n const pageResolveRoute = cache(async ({ params }: RouteParams) => {\n const { slug } = await params;\n const segments = slug ?? [];\n // Pages are addressed by their full, globally-unique slug (locale prefix\n // included), which is exactly what the catch-all captures — pass it through.\n const path = segments.join('/');\n\n // Derive the active locale from the leading path segment when it is a\n // configured locale (matches the Laravel adapter's `request()->segment(1)`).\n const firstSegment = segments[0];\n const lang = firstSegment && state.locales.includes(firstSegment)\n ? firstSegment\n : (state.defaultLocale ?? undefined);\n\n // Every `notFound()` below hands the request off to `not-found.tsx`, but this\n // route still owns the request-scoped language-links store (see the store\n // notes below). Publish a fallback — one disabled entry per configured\n // locale — *before* bailing, so a switcher in shared chrome awaiting\n // `readLanguageLinks()` resolves instead of hanging. Doing it here (and\n // never in `not-found.tsx`) also keeps the fallback from racing a real\n // route's links: the App Router renders the root `not-found.tsx` even on\n // 200s, so publishing there would poison pages that *do* have translations.\n const publishNotFoundFallback = () =>\n publishLanguageLinks(getLanguageLinks(undefined, { currentLang: lang, locales: state.locales }));\n\n const cfg = await getNitroConfig(lang);\n\n if (!cfg.pages?.includes(path)) {\n publishNotFoundFallback();\n notFound();\n }\n\n const page = await new PagesApi(configuration)\n .page({ slug: path, lang })\n .catch((error: unknown) => {\n console.error('Error fetching page:', path, error);\n publishNotFoundFallback();\n notFound();\n });\n\n if (!page) {\n publishNotFoundFallback();\n notFound();\n }\n\n // Publish this page's switcher links so a switcher in shared chrome (e.g. a\n // footer in the root layout) can read them via `readLanguageLinks()`.\n publishLanguageLinks(getLanguageLinks(page.translation, { currentLang: lang, locales: state.locales }));\n\n return { page, path, lang, cfg };\n });\n\n const sitemap = async (): Promise<MetadataRoute.Sitemap> => {\n const sitemapApi = new SitemapApi(configuration);\n const sitemapLang = state.lang ?? undefined;\n\n if (!state.baseUrl) {\n throw new Error('baseUrl is not configured. Please set it in initNitro().');\n }\n\n const items = await sitemapApi.sitemap({ lang: sitemapLang });\n const cleanBaseUrl = state.baseUrl.endsWith('/') ? state.baseUrl.slice(0, -1) : state.baseUrl;\n\n return items.map((item) => {\n let path = '';\n\n if (item.routes && typeof item.routes === 'object') {\n const routeValues = Object.values(item.routes);\n if (routeValues.length > 0) {\n path = routeValues[0];\n }\n }\n\n if (!path && item.entity_slug) {\n path = item.entity_slug;\n }\n\n const cleanPath = path && !path.startsWith('/') ? `/${path}` : path;\n\n return {\n url: `${cleanBaseUrl}${cleanPath}`,\n lastModified: new Date(),\n };\n });\n };\n\n return {\n state,\n getNitroConfig,\n getRequestLocale,\n getNitroPages: () => new PagesApi(configuration),\n getNitroEntities: () => new EntitiesApi(configuration),\n getNitroSitemap: () => new SitemapApi(configuration),\n getNitroSearch: () => new SearchApi(configuration),\n pageResolveRoute,\n sitemap,\n };\n}\n\n// ─── Request-scoped language-switcher store ──────────────────────────────────\n//\n// A page's / entity's `translation[]` is resolved deep in the route tree (in the\n// catch-all page route or an entity detail route). A language switcher, however,\n// usually lives in *shared chrome* — a footer, say — that is rendered by the\n// **root layout**. In the App Router the root layout is an *ancestor* of the\n// page, so it cannot receive the page's data as props: data only flows down.\n//\n// This store bridges that gap. The page/entity route **publishes** its resolved\n// links; the switcher in the footer **reads** them. It is a per-request deferred,\n// created once per request via React `cache()` (so the layout and the page see\n// the *same* instance), holding a promise the footer awaits.\n//\n// Why the reader awaits a promise instead of reading a plain value: the root\n// layout (and therefore its footer) renders *above*, and concurrently with, the\n// page — so a synchronous read could run before the page has resolved. Awaiting\n// suspends the footer until the links are published, regardless of render order.\n\nexport interface LanguageLinksStore {\n /**\n * Publish the links once. Later calls are ignored (first publish wins).\n *\n * The active page/entity route is the single writer per request: it publishes\n * the resolved translations on success and a fallback before any `notFound()`.\n * \"First wins\" then only guards against the route's render pass and its\n * `generateMetadata` both publishing (they publish the same links) — no other\n * caller races it.\n */\n publish(links: FlyoLanguageLink[]): void;\n /** A promise that resolves when the links are published. */\n read(): Promise<FlyoLanguageLink[]>;\n}\n\n/**\n * Create a single deferred language-links channel: one writer, many readers,\n * order-independent. Low-level — most code uses {@link publishLanguageLinks} /\n * {@link readLanguageLinks}, which wrap this in a per-request `cache()`.\n */\nexport function createLanguageLinksStore(): LanguageLinksStore {\n let resolve!: (links: FlyoLanguageLink[]) => void;\n const promise = new Promise<FlyoLanguageLink[]>((r) => {\n resolve = r;\n });\n let settled = false;\n return {\n publish(links: FlyoLanguageLink[]) {\n // First publish wins — a route resolves once per request, and both the\n // render pass and `generateMetadata` share this store, so guard against a\n // double resolve.\n if (!settled) {\n settled = true;\n resolve(links);\n }\n },\n read() {\n return promise;\n },\n };\n}\n\n// `cache()` gives one store per request, shared across the whole server\n// component tree for that request. A fresh request gets a fresh store.\nconst languageLinksStore = cache(createLanguageLinksStore);\n\n/**\n * Publish the current route's language-switcher links into the request-scoped\n * store, so a switcher rendered in shared chrome (e.g. a footer in the root\n * layout) can read them via {@link readLanguageLinks}.\n *\n * The Flyo route helpers call this for you: `pageResolveRoute` (and therefore\n * `nitroPageRoute`) publishes the page's links, and `nitroEntityRoute` /\n * `nitroEntityGenerateMetadata` publish the entity's. Crucially they also\n * publish a fallback on their `notFound()` paths, so a genuine 404 that renders\n * `not-found.tsx` still settles the store — the footer never waits forever.\n *\n * **Do not publish from `not-found.tsx`.** In the App Router the root not-found\n * boundary is rendered on *every* request, not only on real 404s, and it renders\n * synchronously — ahead of a route's `await`ed CMS fetch. With a first-wins\n * store, a publish there would settle the store with the fallback before the\n * real route publishes, so pages that *do* have translations would show the\n * home/fallback links. Let the route own the publish instead.\n *\n * You only need to call this yourself on a **custom route Flyo does not\n * resolve** — a hand-written page that renders the shared switcher without going\n * through the helpers above. Publish that route's real links, or a fallback via\n * `getLanguageLinks(undefined, { currentLang, locales })`.\n *\n * Only the first call per request takes effect.\n */\nexport function publishLanguageLinks(links: FlyoLanguageLink[]): void {\n languageLinksStore().publish(links);\n}\n\n/**\n * Await the current route's language-switcher links from shared chrome — a\n * footer, header, or any component in the root layout. Resolves with whatever\n * the active page/entity route passed to {@link publishLanguageLinks}.\n *\n * Because it awaits, the reader suspends until the links are published, so it\n * works no matter whether the layout or the page renders first. Wrap the reading\n * component in `<Suspense>` so the rest of the layout can stream while it waits.\n *\n * @example\n * ```tsx\n * // components/LanguageSwitcher.tsx (server component)\n * import { readLanguageLinks } from '@flyo/nitro-next/server';\n *\n * export async function LanguageSwitcher() {\n * const links = await readLanguageLinks();\n * return (\n * <nav aria-label=\"Language\">\n * {links.map((l) =>\n * l.exists\n * ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>\n * : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>,\n * )}\n * </nav>\n * );\n * }\n * ```\n */\nexport function readLanguageLinks(): Promise<FlyoLanguageLink[]> {\n return languageLinksStore().read();\n}\n\n// ─── Helper ──────────────────────────────────────────────────────────────────\n\n/**\n * Helper function to read environment variables with fallback\n */\nconst readEnv = (key: string, fallback = \"\"): string => {\n const value = process.env[key];\n if (value !== undefined && value !== \"\") {\n return String(value);\n }\n return fallback;\n};\n\n/**\n * Build Next.js `alternates` (hreflang + canonical) from a page's or entity's\n * `translation[]`. Returns `undefined` when there are no linked translations.\n */\nfunction buildLanguageAlternates(\n translations: Translation[] | undefined,\n currentLang?: string,\n): Metadata['alternates'] | undefined {\n const languages: Record<string, string> = {};\n let canonical: string | undefined;\n\n for (const t of translations ?? []) {\n const shortcode = t.language?.shortcode;\n if (shortcode && t.href) {\n languages[shortcode] = t.href;\n if (currentLang && shortcode === currentLang) {\n canonical = t.href;\n }\n }\n }\n\n if (Object.keys(languages).length === 0) {\n return undefined;\n }\n\n return canonical ? { canonical, languages } : { languages };\n}\n\n/**\n * Internal helper to wrap and cache entity resolvers.\n *\n * Also publishes the entity's language-switcher links into the request-scoped\n * store (see {@link publishLanguageLinks}) so a switcher in shared chrome can\n * read them via `readLanguageLinks()`.\n */\nfunction createCachedEntityResolver<T>(\n flyo: FlyoInstance,\n resolver: EntityResolver<T>\n): (props: EntityRouteParams<T>) => Promise<Entity> {\n return cache(async ({ params }: EntityRouteParams<T>) => {\n // As with `pageResolveRoute`, this resolver owns the request's language-links\n // store even when it bails. Publish a fallback before every not-found path\n // (a missing entity, a `notFound()` from the resolver, a failed CMS fetch)\n // so a switcher in shared chrome resolves instead of hanging — and never\n // publish from `not-found.tsx`, which the App Router also renders on 200s.\n const publishNotFoundFallback = async () =>\n publishLanguageLinks(\n getLanguageLinks(undefined, {\n currentLang: await flyo.getRequestLocale(),\n locales: flyo.state.locales,\n }),\n );\n\n let entity: Entity;\n try {\n entity = await resolver(params);\n } catch (error) {\n await publishNotFoundFallback();\n throw error;\n }\n\n if (!entity) {\n await publishNotFoundFallback();\n notFound();\n }\n\n publishLanguageLinks(\n getLanguageLinks(entity.translation, { currentLang: entity.language, locales: flyo.state.locales }),\n );\n\n return entity;\n });\n}\n\n// ─── Server Components ───────────────────────────────────────────────────────\n\n/**\n * NitroDebugInfo Component\n *\n * Async server component that outputs debug information as an HTML comment.\n * Resolves config internally from the flyo instance.\n *\n * @example\n * ```tsx\n * <NitroDebugInfo flyo={flyo} />\n * ```\n */\nexport async function NitroDebugInfo({ flyo }: { flyo: FlyoInstance }) {\n try {\n const config = await flyo.getNitroConfig();\n const { state } = flyo;\n\n const mode = readEnv(\"NODE_ENV\", \"-\");\n const vercelDeploymentId = readEnv(\"VERCEL_DEPLOYMENT_ID\", \"-\");\n const vercelGitCommitSha = readEnv(\"VERCEL_GIT_COMMIT_SHA\", \"-\");\n const version = readEnv(\"VERSION\", \"\");\n\n const tokenValue = state.accessToken || \"\";\n const token = typeof tokenValue === \"string\" ? tokenValue : \"\";\n const tokenType = token.startsWith(\"p-\")\n ? \"production\"\n : token.startsWith(\"d-\")\n ? \"develop\"\n : \"unknown\";\n\n const debug = state.liveEdit;\n\n const apiVersion = config.nitro?.version?.toString() || \"-\";\n const apiLastUpdate = config.nitro?.updated_at\n ? new Date(config.nitro.updated_at * 1000).toLocaleString(\"de-CH\", {\n day: \"2-digit\",\n month: \"2-digit\",\n year: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n })\n : \"-\";\n\n const debugInfoParts = [\n `liveedit:${debug}`,\n `env:${mode}`,\n `version:${apiVersion}`,\n `versiondate:${apiLastUpdate}`,\n `tokentype:${tokenType}`,\n `did:${vercelDeploymentId}`,\n `csha:${vercelGitCommitSha}`,\n ];\n\n if (version) {\n debugInfoParts.push(`release:${version}`);\n }\n\n const debugInfo = debugInfoParts.join(\" | \");\n\n return (\n <template dangerouslySetInnerHTML={{ __html: `<!-- ${debugInfo} -->` }} suppressHydrationWarning />\n );\n } catch {\n return <template dangerouslySetInnerHTML={{ __html: `<!-- nitro-debug: not initialized -->` }} suppressHydrationWarning />;\n }\n}\n\n/**\n * Renders a JSON-LD structured data script tag from an Entity's jsonld field.\n * Safely escapes HTML to prevent XSS.\n */\nexport function NitroEntityJsonLd({ entity }: { entity: Entity }) {\n if (!entity?.jsonld) {\n return null;\n }\n\n return (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{\n __html: JSON.stringify(entity.jsonld).replace(/</g, '\\\\u003c'),\n }}\n />\n );\n}\n\n/**\n * NitroPage renders all blocks from a Flyo page.\n */\nexport function NitroPage({\n page,\n flyo,\n}: {\n page: Page;\n flyo: FlyoInstance;\n}) {\n if (!page?.json || !Array.isArray(page.json)) {\n return null;\n }\n\n return (\n <>\n {page.json.map((block: Block, index: number) => (\n <NitroBlock\n key={block.uid || index}\n block={block}\n flyo={flyo}\n />\n ))}\n </>\n );\n}\n\n/**\n * NitroBlock renders a single block using the registered component.\n */\nexport function NitroBlock({\n block,\n flyo,\n}: {\n block: Block;\n flyo: FlyoInstance;\n}) {\n if (!block) {\n return null;\n }\n\n const Component = block.component ? flyo.state.components[block.component] : undefined;\n\n if (Component) {\n return <Component block={block} />;\n }\n\n if (flyo.state.showMissingComponentAlert) {\n return (\n <div style={{ border: '1px solid #fff', padding: '1rem', marginBottom: '1rem', backgroundColor: 'red' }}>\n Component <b>{block.component}</b> not found.\n </div>\n );\n }\n\n return null;\n}\n\n/**\n * NitroSlot renders nested blocks from a slot.\n * Used for recursive block rendering when blocks contain slots.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { NitroSlot } from '@flyo/nitro-next/server';\n *\n * export function MyComponent({ block }) {\n * return (\n * <div>\n * <NitroSlot slot={block.slots?.content} flyo={flyo} />\n * </div>\n * );\n * }\n * ```\n */\nexport function NitroSlot({\n slot,\n flyo,\n}: {\n slot?: {\n content?: Block[];\n };\n flyo: FlyoInstance;\n}) {\n if (!slot?.content || !Array.isArray(slot.content)) {\n return null;\n }\n\n return (\n <>\n {slot.content.map((block: Block, index: number) => (\n <NitroBlock\n key={block.uid || index}\n block={block}\n flyo={flyo}\n />\n ))}\n </>\n );\n}\n\n// ─── Factory Functions ───────────────────────────────────────────────────────\n\n/**\n * Create a page route handler for Nitro pages.\n * Returns a Next.js page component function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroPageRoute } from '@flyo/nitro-next/server';\n *\n * export default nitroPageRoute(flyo);\n * ```\n */\nexport function nitroPageRoute(flyo: FlyoInstance) {\n async function pageRoute(props: RouteParams) {\n const { page } = await flyo.pageResolveRoute(props);\n return <NitroPage page={page} flyo={flyo} />;\n }\n return pageRoute;\n}\n\n/**\n * Create a metadata generator for Nitro pages.\n * Returns a Next.js generateMetadata function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroPageGenerateMetadata } from '@flyo/nitro-next/server';\n *\n * export const generateMetadata = nitroPageGenerateMetadata(flyo);\n * ```\n */\nexport function nitroPageGenerateMetadata(flyo: FlyoInstance) {\n return async (props: RouteParams): Promise<Metadata> => {\n const { page, lang } = await flyo.pageResolveRoute(props);\n\n const meta = page.meta_json;\n const title = meta?.title || page.title || '';\n const description = meta?.description ?? '';\n const image = meta?.image ?? '';\n\n const ogImage = image ? `${image}/thumb/1200x630?format=jpg` : undefined;\n const twImage = image ? `${image}/thumb/1200x600?format=jpg` : undefined;\n\n const alternates = buildLanguageAlternates(page.translation, lang);\n\n return {\n title,\n description,\n ...(alternates ? { alternates } : {}),\n openGraph: {\n title,\n description,\n images: ogImage ? [ogImage] : [],\n type: 'website',\n },\n twitter: {\n card: 'summary_large_image',\n title,\n description,\n images: twImage ? [twImage] : [],\n },\n };\n };\n}\n\n/**\n * Create a static params generator for Nitro pages.\n * Returns a Next.js generateStaticParams function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroPageGenerateStaticParams } from '@flyo/nitro-next/server';\n *\n * export const generateStaticParams = nitroPageGenerateStaticParams(flyo);\n * ```\n */\nexport function nitroPageGenerateStaticParams(flyo: FlyoInstance) {\n return async () => {\n const cfg = await flyo.getNitroConfig();\n const pages = cfg.pages ?? [];\n\n return pages.map((path: string) => ({\n slug: path === '' ? undefined : path.split('/'),\n }));\n };\n}\n\n/**\n * Create an entity route handler with a custom resolver.\n * Returns a Next.js page component function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroEntityRoute } from '@flyo/nitro-next/server';\n *\n * const resolver = async (params) => {\n * const { slug } = await params;\n * return flyo.getNitroEntities().entityBySlug({ slug, typeId: 123 });\n * };\n *\n * export default nitroEntityRoute(flyo, {\n * resolver,\n * render: (entity) => <h1>{entity.entity?.entity_title}</h1>,\n * });\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function nitroEntityRoute<T = any>(\n flyo: FlyoInstance,\n options: {\n resolver: EntityResolver<T>;\n render?: (entity: Entity) => React.ReactNode;\n }\n) {\n const cachedResolver = createCachedEntityResolver(flyo, options.resolver);\n\n async function entityRoute(props: EntityRouteParams<T>) {\n const entity = await cachedResolver(props);\n\n if (options.render) {\n return options.render(entity);\n }\n\n return <div>{entity.entity?.entity_title}</div>;\n }\n return entityRoute;\n}\n\n/**\n * Create a metadata generator for entity detail pages.\n * Returns a Next.js generateMetadata function.\n *\n * @example\n * ```tsx\n * import { flyo } from '@/flyo.config';\n * import { nitroEntityGenerateMetadata } from '@flyo/nitro-next/server';\n *\n * export const generateMetadata = nitroEntityGenerateMetadata(flyo, { resolver });\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function nitroEntityGenerateMetadata<T = any>(\n flyo: FlyoInstance,\n options: {\n resolver: EntityResolver<T>;\n }\n) {\n const cachedResolver = createCachedEntityResolver(flyo, options.resolver);\n\n return async (props: EntityRouteParams<T>): Promise<Metadata> => {\n const entity = await cachedResolver(props);\n\n const title = entity.entity?.entity_title || '';\n const description = entity.entity?.entity_teaser ?? '';\n const image = entity.entity?.entity_image ?? '';\n\n const ogImage = image ? `${image}/thumb/1200x630?format=jpg` : undefined;\n const twImage = image ? `${image}/thumb/1200x600?format=jpg` : undefined;\n\n const alternates = buildLanguageAlternates(entity.translation, entity.language);\n\n return {\n title,\n description,\n ...(alternates ? { alternates } : {}),\n openGraph: {\n title,\n description,\n images: ogImage ? [ogImage] : [],\n type: 'website',\n },\n twitter: {\n card: 'summary_large_image',\n title,\n description,\n images: twImage ? [twImage] : [],\n },\n };\n };\n}","import type { Translation } from '@flyo/nitro-typescript';\n\n/**\n * A single language option for a language switcher, derived from a page's or\n * entity's `translation[]`. Framework-agnostic plain data — render it however\n * you like.\n */\nexport interface FlyoLanguageLink {\n /** Locale shortcode, e.g. `\"de\"`. */\n shortcode: string;\n /** Full language name from the CMS translation, e.g. `\"Deutsch\"` (when available). */\n name?: string;\n /** Fully-resolved localized href, or `null` when this locale has no linked translation. */\n href: string | null;\n /** Localized page/entity title, when available. */\n title?: string;\n /** `true` when this entry is the currently active locale. */\n isCurrent: boolean;\n /** `true` when a linked translation actually exists for this locale. */\n exists: boolean;\n}\n\n/**\n * Map a page's or entity's `translation[]` into a switcher-ready array of typed\n * links — the building block for a language switcher.\n *\n * Flyo only returns `translation` entries for languages that actually have a\n * translation. Pass `options.locales` (e.g. `flyo.state.locales`) to also get an\n * entry for every configured locale that is *missing* a translation — those come\n * back as `{ href: null, exists: false }` so you can render a fallback (a disabled\n * item, a link to the home page, …).\n *\n * Pure — no React or server-only APIs, so it is safe to call from server *or*\n * client components.\n *\n * This is a pure *data* helper — it maps a `translation[]` to switcher links and\n * renders nothing. **The Flyo route helpers already call it for you and publish\n * the result**, so page and entity routes need no switcher code at all; the\n * footer just reads the store (see `publishLanguageLinks` / `readLanguageLinks`\n * in `@flyo/nitro-next/server`). Call this directly only when you publish links\n * by hand from a raw `translation[]` — e.g. on a custom route Flyo doesn't\n * resolve:\n *\n * ```tsx\n * getLanguageLinks(translations, { currentLang, locales });\n * ```\n *\n * **Render each link as a native `<a>`, not `next/link`'s `<Link>`.** A language\n * switch must refresh the shared chrome — the localized nav, footer and\n * `<html lang>` — that lives in the root layout. In the Next.js App Router, soft\n * (client-side) navigation re-renders only the page segment, *not* shared\n * layouts, so a `<Link>` would leave that chrome in the previous language while\n * only the page body updates. A plain `<a>` triggers a full-document navigation,\n * forcing a fresh server render in the new locale so every part updates. (Regular\n * nav links can stay `<Link>` — the nav is identical within a language.)\n *\n * @example\n * ```tsx\n * const links = getLanguageLinks(page.translation, {\n * currentLang: lang,\n * locales: flyo.state.locales,\n * });\n * // Native <a> (full-document nav) — NOT next/link, so shared layout chrome\n * // re-renders in the new locale.\n * // links.map(l => l.exists\n * // ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>\n * // : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>)\n * ```\n */\nexport function getLanguageLinks(\n translations: Translation[] | undefined,\n options?: { currentLang?: string; locales?: string[] },\n): FlyoLanguageLink[] {\n const currentLang = options?.currentLang;\n\n const byShortcode = new Map<string, Translation>();\n for (const t of translations ?? []) {\n const shortcode = t.language?.shortcode;\n if (shortcode) {\n byShortcode.set(shortcode, t);\n }\n }\n\n const toLink = (shortcode: string, t: Translation | undefined): FlyoLanguageLink => ({\n shortcode,\n name: t?.language?.name,\n href: t?.href ?? null,\n title: t?.title,\n isCurrent: shortcode === currentLang,\n exists: t?.href != null,\n });\n\n // When the full set of locales is known, emit an entry for every one so\n // callers can render fallbacks for languages that have no translation.\n if (options?.locales && options.locales.length > 0) {\n return options.locales.map((shortcode) => toLink(shortcode, byShortcode.get(shortcode)));\n }\n\n // Otherwise, just surface the translations that exist.\n return (translations ?? [])\n .filter((t) => t.language?.shortcode)\n .map((t) => toLink(t.language!.shortcode!, t));\n}\n"],"mappings":";AAAA,SAAS,aAAa;AAEtB,SAAS,gBAAgB;AACzB,SAAS,eAAe;AACxB;AAAA,EAIE;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;;;ACqDA,SAAS,iBACd,cACA,SACoB;AACpB,QAAM,cAAc,SAAS;AAE7B,QAAM,cAAc,oBAAI,IAAyB;AACjD,aAAW,KAAK,gBAAgB,CAAC,GAAG;AAClC,UAAM,YAAY,EAAE,UAAU;AAC9B,QAAI,WAAW;AACb,kBAAY,IAAI,WAAW,CAAC;AAAA,IAC9B;AAAA,EACF;AAEA,QAAM,SAAS,CAAC,WAAmB,OAAkD;AAAA,IACnF;AAAA,IACA,MAAM,GAAG,UAAU;AAAA,IACnB,MAAM,GAAG,QAAQ;AAAA,IACjB,OAAO,GAAG;AAAA,IACV,WAAW,cAAc;AAAA,IACzB,QAAQ,GAAG,QAAQ;AAAA,EACrB;AAIA,MAAI,SAAS,WAAW,QAAQ,QAAQ,SAAS,GAAG;AAClD,WAAO,QAAQ,QAAQ,IAAI,CAAC,cAAc,OAAO,WAAW,YAAY,IAAI,SAAS,CAAC,CAAC;AAAA,EACzF;AAGA,UAAQ,gBAAgB,CAAC,GACtB,OAAO,CAAC,MAAM,EAAE,UAAU,SAAS,EACnC,IAAI,CAAC,MAAM,OAAO,EAAE,SAAU,WAAY,CAAC,CAAC;AACjD;;;AD6cM,SAyCF,UAzCE,KA2EA,YA3EA;AAzbC,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAciB;AACf,QAAM,gBAAgB,IAAI,cAAc,EAAE,QAAQ,YAAY,CAAC;AAE/D,QAAM,wBAAwB,iBAAiB,QAAQ;AACvD,QAAM,kBAAkB,YAAY,wBAAwB,CAAC,qBAAqB,IAAI,CAAC;AAEvF,QAAM,QAAoB;AAAA,IACxB;AAAA,IACA,MAAM,QAAQ;AAAA,IACd,SAAS;AAAA,IACT,eAAe;AAAA,IACf,SAAS,WAAW;AAAA,IACpB,YAAY,cAAc,CAAC;AAAA,IAC3B,2BAA2B,6BAA6B,YAAY;AAAA,IACpE,UAAU,YAAY;AAAA,IACtB,gBAAgB,kBAAkB;AAAA,IAClC,gBAAgB,kBAAkB;AAAA,EACpC;AAEA,QAAM,mBAAmB,YAAyC;AAChE,QAAI;AACF,YAAM,iBAAiB,MAAM,QAAQ;AACrC,YAAM,eAAe,eAAe,IAAI,eAAe;AACvD,UAAI,cAAc;AAChB,eAAO;AAAA,MACT;AAAA,IACF,QAAQ;AAAA,IAER;AACA,WAAO,MAAM,iBAAiB,MAAM,QAAQ;AAAA,EAC9C;AAIA,QAAM,cAAc,MAAM,OAAOA,UAA2C;AAC1E,UAAM,YAAY,IAAI,UAAU,aAAa;AAC7C,WAAO,UAAU,OAAO,EAAE,MAAMA,SAAQ,OAAU,CAAC;AAAA,EACrD,CAAC;AAED,QAAM,iBAAiB,OAAOA,UAA2C;AACvE,UAAM,UAAUA,SAAS,MAAM,iBAAiB;AAChD,WAAO,YAAY,OAAO;AAAA,EAC5B;AAEA,QAAM,mBAAmB,MAAM,OAAO,EAAE,OAAO,MAAmB;AAChE,UAAM,EAAE,KAAK,IAAI,MAAM;AACvB,UAAM,WAAW,QAAQ,CAAC;AAG1B,UAAM,OAAO,SAAS,KAAK,GAAG;AAI9B,UAAM,eAAe,SAAS,CAAC;AAC/B,UAAMA,QAAO,gBAAgB,MAAM,QAAQ,SAAS,YAAY,IAC5D,eACC,MAAM,iBAAiB;AAU5B,UAAM,0BAA0B,MAC9B,qBAAqB,iBAAiB,QAAW,EAAE,aAAaA,OAAM,SAAS,MAAM,QAAQ,CAAC,CAAC;AAEjG,UAAM,MAAM,MAAM,eAAeA,KAAI;AAErC,QAAI,CAAC,IAAI,OAAO,SAAS,IAAI,GAAG;AAC9B,8BAAwB;AACxB,eAAS;AAAA,IACX;AAEA,UAAM,OAAO,MAAM,IAAI,SAAS,aAAa,EAC1C,KAAK,EAAE,MAAM,MAAM,MAAAA,MAAK,CAAC,EACzB,MAAM,CAAC,UAAmB;AACzB,cAAQ,MAAM,wBAAwB,MAAM,KAAK;AACjD,8BAAwB;AACxB,eAAS;AAAA,IACX,CAAC;AAEH,QAAI,CAAC,MAAM;AACT,8BAAwB;AACxB,eAAS;AAAA,IACX;AAIA,yBAAqB,iBAAiB,KAAK,aAAa,EAAE,aAAaA,OAAM,SAAS,MAAM,QAAQ,CAAC,CAAC;AAEtG,WAAO,EAAE,MAAM,MAAM,MAAAA,OAAM,IAAI;AAAA,EACjC,CAAC;AAED,QAAM,UAAU,YAA4C;AAC1D,UAAM,aAAa,IAAI,WAAW,aAAa;AAC/C,UAAM,cAAc,MAAM,QAAQ;AAElC,QAAI,CAAC,MAAM,SAAS;AAClB,YAAM,IAAI,MAAM,0DAA0D;AAAA,IAC5E;AAEA,UAAM,QAAQ,MAAM,WAAW,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC5D,UAAM,eAAe,MAAM,QAAQ,SAAS,GAAG,IAAI,MAAM,QAAQ,MAAM,GAAG,EAAE,IAAI,MAAM;AAEtF,WAAO,MAAM,IAAI,CAAC,SAAS;AACzB,UAAI,OAAO;AAEX,UAAI,KAAK,UAAU,OAAO,KAAK,WAAW,UAAU;AAClD,cAAM,cAAc,OAAO,OAAO,KAAK,MAAM;AAC7C,YAAI,YAAY,SAAS,GAAG;AAC1B,iBAAO,YAAY,CAAC;AAAA,QACtB;AAAA,MACF;AAEA,UAAI,CAAC,QAAQ,KAAK,aAAa;AAC7B,eAAO,KAAK;AAAA,MACd;AAEA,YAAM,YAAY,QAAQ,CAAC,KAAK,WAAW,GAAG,IAAI,IAAI,IAAI,KAAK;AAE/D,aAAO;AAAA,QACL,KAAK,GAAG,YAAY,GAAG,SAAS;AAAA,QAChC,cAAc,oBAAI,KAAK;AAAA,MACzB;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe,MAAM,IAAI,SAAS,aAAa;AAAA,IAC/C,kBAAkB,MAAM,IAAI,YAAY,aAAa;AAAA,IACrD,iBAAiB,MAAM,IAAI,WAAW,aAAa;AAAA,IACnD,gBAAgB,MAAM,IAAI,UAAU,aAAa;AAAA,IACjD;AAAA,IACA;AAAA,EACF;AACF;AAwCO,SAAS,2BAA+C;AAC7D,MAAI;AACJ,QAAM,UAAU,IAAI,QAA4B,CAAC,MAAM;AACrD,cAAU;AAAA,EACZ,CAAC;AACD,MAAI,UAAU;AACd,SAAO;AAAA,IACL,QAAQ,OAA2B;AAIjC,UAAI,CAAC,SAAS;AACZ,kBAAU;AACV,gBAAQ,KAAK;AAAA,MACf;AAAA,IACF;AAAA,IACA,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAIA,IAAM,qBAAqB,MAAM,wBAAwB;AA2BlD,SAAS,qBAAqB,OAAiC;AACpE,qBAAmB,EAAE,QAAQ,KAAK;AACpC;AA8BO,SAAS,oBAAiD;AAC/D,SAAO,mBAAmB,EAAE,KAAK;AACnC;AAOA,IAAM,UAAU,CAAC,KAAa,WAAW,OAAe;AACtD,QAAM,QAAQ,QAAQ,IAAI,GAAG;AAC7B,MAAI,UAAU,UAAa,UAAU,IAAI;AACvC,WAAO,OAAO,KAAK;AAAA,EACrB;AACA,SAAO;AACT;AAMA,SAAS,wBACP,cACA,aACoC;AACpC,QAAM,YAAoC,CAAC;AAC3C,MAAI;AAEJ,aAAW,KAAK,gBAAgB,CAAC,GAAG;AAClC,UAAM,YAAY,EAAE,UAAU;AAC9B,QAAI,aAAa,EAAE,MAAM;AACvB,gBAAU,SAAS,IAAI,EAAE;AACzB,UAAI,eAAe,cAAc,aAAa;AAC5C,oBAAY,EAAE;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AAEA,MAAI,OAAO,KAAK,SAAS,EAAE,WAAW,GAAG;AACvC,WAAO;AAAA,EACT;AAEA,SAAO,YAAY,EAAE,WAAW,UAAU,IAAI,EAAE,UAAU;AAC5D;AASA,SAAS,2BACP,MACA,UACkD;AAClD,SAAO,MAAM,OAAO,EAAE,OAAO,MAA4B;AAMvD,UAAM,0BAA0B,YAC9B;AAAA,MACE,iBAAiB,QAAW;AAAA,QAC1B,aAAa,MAAM,KAAK,iBAAiB;AAAA,QACzC,SAAS,KAAK,MAAM;AAAA,MACtB,CAAC;AAAA,IACH;AAEF,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,SAAS,MAAM;AAAA,IAChC,SAAS,OAAO;AACd,YAAM,wBAAwB;AAC9B,YAAM;AAAA,IACR;AAEA,QAAI,CAAC,QAAQ;AACX,YAAM,wBAAwB;AAC9B,eAAS;AAAA,IACX;AAEA;AAAA,MACE,iBAAiB,OAAO,aAAa,EAAE,aAAa,OAAO,UAAU,SAAS,KAAK,MAAM,QAAQ,CAAC;AAAA,IACpG;AAEA,WAAO;AAAA,EACT,CAAC;AACH;AAeA,eAAsB,eAAe,EAAE,KAAK,GAA2B;AACrE,MAAI;AACF,UAAM,SAAS,MAAM,KAAK,eAAe;AACzC,UAAM,EAAE,MAAM,IAAI;AAElB,UAAM,OAAO,QAAQ,YAAY,GAAG;AACpC,UAAM,qBAAqB,QAAQ,wBAAwB,GAAG;AAC9D,UAAM,qBAAqB,QAAQ,yBAAyB,GAAG;AAC/D,UAAM,UAAU,QAAQ,WAAW,EAAE;AAErC,UAAM,aAAa,MAAM,eAAe;AACxC,UAAM,QAAQ,OAAO,eAAe,WAAW,aAAa;AAC5D,UAAM,YAAY,MAAM,WAAW,IAAI,IACnC,eACA,MAAM,WAAW,IAAI,IACrB,YACA;AAEJ,UAAM,QAAQ,MAAM;AAEpB,UAAM,aAAa,OAAO,OAAO,SAAS,SAAS,KAAK;AACxD,UAAM,gBAAgB,OAAO,OAAO,aAChC,IAAI,KAAK,OAAO,MAAM,aAAa,GAAI,EAAE,eAAe,SAAS;AAAA,MAC/D,KAAK;AAAA,MACL,OAAO;AAAA,MACP,MAAM;AAAA,MACN,MAAM;AAAA,MACN,QAAQ;AAAA,IACV,CAAC,IACD;AAEJ,UAAM,iBAAiB;AAAA,MACrB,YAAY,KAAK;AAAA,MACjB,OAAO,IAAI;AAAA,MACX,WAAW,UAAU;AAAA,MACrB,eAAe,aAAa;AAAA,MAC5B,aAAa,SAAS;AAAA,MACtB,OAAO,kBAAkB;AAAA,MACzB,QAAQ,kBAAkB;AAAA,IAC5B;AAEA,QAAI,SAAS;AACX,qBAAe,KAAK,WAAW,OAAO,EAAE;AAAA,IAC1C;AAEA,UAAM,YAAY,eAAe,KAAK,KAAK;AAE3C,WACE,oBAAC,cAAS,yBAAyB,EAAE,QAAQ,QAAQ,SAAS,OAAO,GAAG,0BAAwB,MAAC;AAAA,EAErG,QAAQ;AACN,WAAO,oBAAC,cAAS,yBAAyB,EAAE,QAAQ,wCAAwC,GAAG,0BAAwB,MAAC;AAAA,EAC1H;AACF;AAMO,SAAS,kBAAkB,EAAE,OAAO,GAAuB;AAChE,MAAI,CAAC,QAAQ,QAAQ;AACnB,WAAO;AAAA,EACT;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,yBAAyB;AAAA,QACvB,QAAQ,KAAK,UAAU,OAAO,MAAM,EAAE,QAAQ,MAAM,SAAS;AAAA,MAC/D;AAAA;AAAA,EACF;AAEJ;AAKO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AACF,GAGG;AACD,MAAI,CAAC,MAAM,QAAQ,CAAC,MAAM,QAAQ,KAAK,IAAI,GAAG;AAC5C,WAAO;AAAA,EACT;AAEA,SACE,gCACG,eAAK,KAAK,IAAI,CAAC,OAAc,UAC5B;AAAA,IAAC;AAAA;AAAA,MAEC;AAAA,MACA;AAAA;AAAA,IAFK,MAAM,OAAO;AAAA,EAGpB,CACD,GACH;AAEJ;AAKO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AACF,GAGG;AACD,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,MAAM,YAAY,KAAK,MAAM,WAAW,MAAM,SAAS,IAAI;AAE7E,MAAI,WAAW;AACb,WAAO,oBAAC,aAAU,OAAc;AAAA,EAClC;AAEA,MAAI,KAAK,MAAM,2BAA2B;AACxC,WACE,qBAAC,SAAI,OAAO,EAAE,QAAQ,kBAAkB,SAAS,QAAQ,cAAc,QAAQ,iBAAiB,MAAM,GAAG;AAAA;AAAA,MAC7F,oBAAC,OAAG,gBAAM,WAAU;AAAA,MAAI;AAAA,OACpC;AAAA,EAEJ;AAEA,SAAO;AACT;AAoBO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AACF,GAKG;AACD,MAAI,CAAC,MAAM,WAAW,CAAC,MAAM,QAAQ,KAAK,OAAO,GAAG;AAClD,WAAO;AAAA,EACT;AAEA,SACE,gCACG,eAAK,QAAQ,IAAI,CAAC,OAAc,UAC/B;AAAA,IAAC;AAAA;AAAA,MAEC;AAAA,MACA;AAAA;AAAA,IAFK,MAAM,OAAO;AAAA,EAGpB,CACD,GACH;AAEJ;AAgBO,SAAS,eAAe,MAAoB;AACjD,iBAAe,UAAU,OAAoB;AAC3C,UAAM,EAAE,KAAK,IAAI,MAAM,KAAK,iBAAiB,KAAK;AAClD,WAAO,oBAAC,aAAU,MAAY,MAAY;AAAA,EAC5C;AACA,SAAO;AACT;AAcO,SAAS,0BAA0B,MAAoB;AAC5D,SAAO,OAAO,UAA0C;AACtD,UAAM,EAAE,MAAM,KAAK,IAAI,MAAM,KAAK,iBAAiB,KAAK;AAExD,UAAM,OAAO,KAAK;AAClB,UAAM,QAAQ,MAAM,SAAS,KAAK,SAAS;AAC3C,UAAM,cAAc,MAAM,eAAe;AACzC,UAAM,QAAQ,MAAM,SAAS;AAE7B,UAAM,UAAU,QAAQ,GAAG,KAAK,+BAA+B;AAC/D,UAAM,UAAU,QAAQ,GAAG,KAAK,+BAA+B;AAE/D,UAAM,aAAa,wBAAwB,KAAK,aAAa,IAAI;AAEjE,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;AAAA,MACnC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,QAAQ,UAAU,CAAC,OAAO,IAAI,CAAC;AAAA,QAC/B,MAAM;AAAA,MACR;AAAA,MACA,SAAS;AAAA,QACP,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,QAAQ,UAAU,CAAC,OAAO,IAAI,CAAC;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AACF;AAcO,SAAS,8BAA8B,MAAoB;AAChE,SAAO,YAAY;AACjB,UAAM,MAAM,MAAM,KAAK,eAAe;AACtC,UAAM,QAAQ,IAAI,SAAS,CAAC;AAE5B,WAAO,MAAM,IAAI,CAAC,UAAkB;AAAA,MAClC,MAAM,SAAS,KAAK,SAAY,KAAK,MAAM,GAAG;AAAA,IAChD,EAAE;AAAA,EACJ;AACF;AAuBO,SAAS,iBACd,MACA,SAIA;AACA,QAAM,iBAAiB,2BAA2B,MAAM,QAAQ,QAAQ;AAExE,iBAAe,YAAY,OAA6B;AACtD,UAAM,SAAS,MAAM,eAAe,KAAK;AAEzC,QAAI,QAAQ,QAAQ;AAClB,aAAO,QAAQ,OAAO,MAAM;AAAA,IAC9B;AAEA,WAAO,oBAAC,SAAK,iBAAO,QAAQ,cAAa;AAAA,EAC3C;AACA,SAAO;AACT;AAeO,SAAS,4BACd,MACA,SAGA;AACA,QAAM,iBAAiB,2BAA2B,MAAM,QAAQ,QAAQ;AAExE,SAAO,OAAO,UAAmD;AAC/D,UAAM,SAAS,MAAM,eAAe,KAAK;AAEzC,UAAM,QAAQ,OAAO,QAAQ,gBAAgB;AAC7C,UAAM,cAAc,OAAO,QAAQ,iBAAiB;AACpD,UAAM,QAAQ,OAAO,QAAQ,gBAAgB;AAE7C,UAAM,UAAU,QAAQ,GAAG,KAAK,+BAA+B;AAC/D,UAAM,UAAU,QAAQ,GAAG,KAAK,+BAA+B;AAE/D,UAAM,aAAa,wBAAwB,OAAO,aAAa,OAAO,QAAQ;AAE9E,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;AAAA,MACnC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,QAAQ,UAAU,CAAC,OAAO,IAAI,CAAC;AAAA,QAC/B,MAAM;AAAA,MACR;AAAA,MACA,SAAS;AAAA,QACP,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,QAAQ,UAAU,CAAC,OAAO,IAAI,CAAC;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AACF;","names":["lang"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flyo/nitro-next",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "Connecting Flyo Headless Content Hub into your Next.js project.",
5
5
  "homepage": "https://dev.flyo.cloud/nitro",
6
6
  "keywords": [