@flyo/nitro-next 2.1.0 → 2.2.0
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 +23 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.js.map +1 -1
- package/dist/client.mjs.map +1 -1
- package/dist/server.d.mts +81 -1
- package/dist/server.d.ts +81 -1
- package/dist/server.js +40 -5
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +36 -4
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
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>)
|
package/dist/client.js.map
CHANGED
|
@@ -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/client.mjs.map
CHANGED
|
@@ -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,63 @@ declare function initNitro({ accessToken, lang, locales, defaultLocale, baseUrl,
|
|
|
161
184
|
serverCacheTtl?: number;
|
|
162
185
|
clientCacheTtl?: number;
|
|
163
186
|
}): FlyoInstance;
|
|
187
|
+
interface LanguageLinksStore {
|
|
188
|
+
/** Publish the links once. Later calls are ignored (first publish wins). */
|
|
189
|
+
publish(links: FlyoLanguageLink[]): void;
|
|
190
|
+
/** A promise that resolves when the links are published. */
|
|
191
|
+
read(): Promise<FlyoLanguageLink[]>;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Create a single deferred language-links channel: one writer, many readers,
|
|
195
|
+
* order-independent. Low-level — most code uses {@link publishLanguageLinks} /
|
|
196
|
+
* {@link readLanguageLinks}, which wrap this in a per-request `cache()`.
|
|
197
|
+
*/
|
|
198
|
+
declare function createLanguageLinksStore(): LanguageLinksStore;
|
|
199
|
+
/**
|
|
200
|
+
* Publish the current route's language-switcher links into the request-scoped
|
|
201
|
+
* store, so a switcher rendered in shared chrome (e.g. a footer in the root
|
|
202
|
+
* layout) can read them via {@link readLanguageLinks}.
|
|
203
|
+
*
|
|
204
|
+
* The Flyo route helpers call this for you: `pageResolveRoute` (and therefore
|
|
205
|
+
* `nitroPageRoute`) publishes the page's links, and `nitroEntityRoute` /
|
|
206
|
+
* `nitroEntityGenerateMetadata` publish the entity's. You only need to call it
|
|
207
|
+
* yourself on routes that render the shared switcher **without** going through
|
|
208
|
+
* those helpers — a custom page, or `not-found.tsx` — where you would otherwise
|
|
209
|
+
* leave the footer waiting forever. Pass a fallback there, e.g.
|
|
210
|
+
* `publishLanguageLinks(getLanguageLinks(undefined, { currentLang, locales }))`.
|
|
211
|
+
*
|
|
212
|
+
* Only the first call per request takes effect.
|
|
213
|
+
*/
|
|
214
|
+
declare function publishLanguageLinks(links: FlyoLanguageLink[]): void;
|
|
215
|
+
/**
|
|
216
|
+
* Await the current route's language-switcher links from shared chrome — a
|
|
217
|
+
* footer, header, or any component in the root layout. Resolves with whatever
|
|
218
|
+
* the active page/entity route passed to {@link publishLanguageLinks}.
|
|
219
|
+
*
|
|
220
|
+
* Because it awaits, the reader suspends until the links are published, so it
|
|
221
|
+
* works no matter whether the layout or the page renders first. Wrap the reading
|
|
222
|
+
* component in `<Suspense>` so the rest of the layout can stream while it waits.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```tsx
|
|
226
|
+
* // components/LanguageSwitcher.tsx (server component)
|
|
227
|
+
* import { readLanguageLinks } from '@flyo/nitro-next/server';
|
|
228
|
+
*
|
|
229
|
+
* export async function LanguageSwitcher() {
|
|
230
|
+
* const links = await readLanguageLinks();
|
|
231
|
+
* return (
|
|
232
|
+
* <nav aria-label="Language">
|
|
233
|
+
* {links.map((l) =>
|
|
234
|
+
* l.exists
|
|
235
|
+
* ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>
|
|
236
|
+
* : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>,
|
|
237
|
+
* )}
|
|
238
|
+
* </nav>
|
|
239
|
+
* );
|
|
240
|
+
* }
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
declare function readLanguageLinks(): Promise<FlyoLanguageLink[]>;
|
|
164
244
|
/**
|
|
165
245
|
* NitroDebugInfo Component
|
|
166
246
|
*
|
|
@@ -301,4 +381,4 @@ declare function nitroEntityGenerateMetadata<T = any>(flyo: FlyoInstance, option
|
|
|
301
381
|
resolver: EntityResolver<T>;
|
|
302
382
|
}): (props: EntityRouteParams<T>) => Promise<Metadata>;
|
|
303
383
|
|
|
304
|
-
export { type EntityResolver, type FlyoInstance, type FlyoLanguageLink, NitroBlock, NitroDebugInfo, NitroEntityJsonLd, NitroPage, NitroSlot, type NitroState, getLanguageLinks, initNitro, nitroEntityGenerateMetadata, nitroEntityRoute, nitroPageGenerateMetadata, nitroPageGenerateStaticParams, nitroPageRoute };
|
|
384
|
+
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,63 @@ declare function initNitro({ accessToken, lang, locales, defaultLocale, baseUrl,
|
|
|
161
184
|
serverCacheTtl?: number;
|
|
162
185
|
clientCacheTtl?: number;
|
|
163
186
|
}): FlyoInstance;
|
|
187
|
+
interface LanguageLinksStore {
|
|
188
|
+
/** Publish the links once. Later calls are ignored (first publish wins). */
|
|
189
|
+
publish(links: FlyoLanguageLink[]): void;
|
|
190
|
+
/** A promise that resolves when the links are published. */
|
|
191
|
+
read(): Promise<FlyoLanguageLink[]>;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Create a single deferred language-links channel: one writer, many readers,
|
|
195
|
+
* order-independent. Low-level — most code uses {@link publishLanguageLinks} /
|
|
196
|
+
* {@link readLanguageLinks}, which wrap this in a per-request `cache()`.
|
|
197
|
+
*/
|
|
198
|
+
declare function createLanguageLinksStore(): LanguageLinksStore;
|
|
199
|
+
/**
|
|
200
|
+
* Publish the current route's language-switcher links into the request-scoped
|
|
201
|
+
* store, so a switcher rendered in shared chrome (e.g. a footer in the root
|
|
202
|
+
* layout) can read them via {@link readLanguageLinks}.
|
|
203
|
+
*
|
|
204
|
+
* The Flyo route helpers call this for you: `pageResolveRoute` (and therefore
|
|
205
|
+
* `nitroPageRoute`) publishes the page's links, and `nitroEntityRoute` /
|
|
206
|
+
* `nitroEntityGenerateMetadata` publish the entity's. You only need to call it
|
|
207
|
+
* yourself on routes that render the shared switcher **without** going through
|
|
208
|
+
* those helpers — a custom page, or `not-found.tsx` — where you would otherwise
|
|
209
|
+
* leave the footer waiting forever. Pass a fallback there, e.g.
|
|
210
|
+
* `publishLanguageLinks(getLanguageLinks(undefined, { currentLang, locales }))`.
|
|
211
|
+
*
|
|
212
|
+
* Only the first call per request takes effect.
|
|
213
|
+
*/
|
|
214
|
+
declare function publishLanguageLinks(links: FlyoLanguageLink[]): void;
|
|
215
|
+
/**
|
|
216
|
+
* Await the current route's language-switcher links from shared chrome — a
|
|
217
|
+
* footer, header, or any component in the root layout. Resolves with whatever
|
|
218
|
+
* the active page/entity route passed to {@link publishLanguageLinks}.
|
|
219
|
+
*
|
|
220
|
+
* Because it awaits, the reader suspends until the links are published, so it
|
|
221
|
+
* works no matter whether the layout or the page renders first. Wrap the reading
|
|
222
|
+
* component in `<Suspense>` so the rest of the layout can stream while it waits.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```tsx
|
|
226
|
+
* // components/LanguageSwitcher.tsx (server component)
|
|
227
|
+
* import { readLanguageLinks } from '@flyo/nitro-next/server';
|
|
228
|
+
*
|
|
229
|
+
* export async function LanguageSwitcher() {
|
|
230
|
+
* const links = await readLanguageLinks();
|
|
231
|
+
* return (
|
|
232
|
+
* <nav aria-label="Language">
|
|
233
|
+
* {links.map((l) =>
|
|
234
|
+
* l.exists
|
|
235
|
+
* ? <a key={l.shortcode} href={l.href!} aria-current={l.isCurrent || undefined}>{l.name ?? l.shortcode}</a>
|
|
236
|
+
* : <span key={l.shortcode} aria-disabled>{l.shortcode}</span>,
|
|
237
|
+
* )}
|
|
238
|
+
* </nav>
|
|
239
|
+
* );
|
|
240
|
+
* }
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
declare function readLanguageLinks(): Promise<FlyoLanguageLink[]>;
|
|
164
244
|
/**
|
|
165
245
|
* NitroDebugInfo Component
|
|
166
246
|
*
|
|
@@ -301,4 +381,4 @@ declare function nitroEntityGenerateMetadata<T = any>(flyo: FlyoInstance, option
|
|
|
301
381
|
resolver: EntityResolver<T>;
|
|
302
382
|
}): (props: EntityRouteParams<T>) => Promise<Metadata>;
|
|
303
383
|
|
|
304
|
-
export { type EntityResolver, type FlyoInstance, type FlyoLanguageLink, NitroBlock, NitroDebugInfo, NitroEntityJsonLd, NitroPage, NitroSlot, type NitroState, getLanguageLinks, initNitro, nitroEntityGenerateMetadata, nitroEntityRoute, nitroPageGenerateMetadata, nitroPageGenerateStaticParams, nitroPageRoute };
|
|
384
|
+
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");
|
|
@@ -128,6 +131,7 @@ function initNitro({
|
|
|
128
131
|
if (!page) {
|
|
129
132
|
(0, import_navigation.notFound)();
|
|
130
133
|
}
|
|
134
|
+
publishLanguageLinks(getLanguageLinks(page.translation, { currentLang: lang2, locales: state.locales }));
|
|
131
135
|
return { page, path, lang: lang2, cfg };
|
|
132
136
|
});
|
|
133
137
|
const sitemap = async () => {
|
|
@@ -168,6 +172,31 @@ function initNitro({
|
|
|
168
172
|
sitemap
|
|
169
173
|
};
|
|
170
174
|
}
|
|
175
|
+
function createLanguageLinksStore() {
|
|
176
|
+
let resolve;
|
|
177
|
+
const promise = new Promise((r) => {
|
|
178
|
+
resolve = r;
|
|
179
|
+
});
|
|
180
|
+
let settled = false;
|
|
181
|
+
return {
|
|
182
|
+
publish(links) {
|
|
183
|
+
if (!settled) {
|
|
184
|
+
settled = true;
|
|
185
|
+
resolve(links);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
read() {
|
|
189
|
+
return promise;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
var languageLinksStore = (0, import_react.cache)(createLanguageLinksStore);
|
|
194
|
+
function publishLanguageLinks(links) {
|
|
195
|
+
languageLinksStore().publish(links);
|
|
196
|
+
}
|
|
197
|
+
function readLanguageLinks() {
|
|
198
|
+
return languageLinksStore().read();
|
|
199
|
+
}
|
|
171
200
|
var readEnv = (key, fallback = "") => {
|
|
172
201
|
const value = process.env[key];
|
|
173
202
|
if (value !== void 0 && value !== "") {
|
|
@@ -192,12 +221,15 @@ function buildLanguageAlternates(translations, currentLang) {
|
|
|
192
221
|
}
|
|
193
222
|
return canonical ? { canonical, languages } : { languages };
|
|
194
223
|
}
|
|
195
|
-
function createCachedEntityResolver(resolver) {
|
|
224
|
+
function createCachedEntityResolver(flyo, resolver) {
|
|
196
225
|
return (0, import_react.cache)(async ({ params }) => {
|
|
197
226
|
const entity = await resolver(params);
|
|
198
227
|
if (!entity) {
|
|
199
228
|
(0, import_navigation.notFound)();
|
|
200
229
|
}
|
|
230
|
+
publishLanguageLinks(
|
|
231
|
+
getLanguageLinks(entity.translation, { currentLang: entity.language, locales: flyo.state.locales })
|
|
232
|
+
);
|
|
201
233
|
return entity;
|
|
202
234
|
});
|
|
203
235
|
}
|
|
@@ -351,7 +383,7 @@ function nitroPageGenerateStaticParams(flyo) {
|
|
|
351
383
|
};
|
|
352
384
|
}
|
|
353
385
|
function nitroEntityRoute(flyo, options) {
|
|
354
|
-
const cachedResolver = createCachedEntityResolver(options.resolver);
|
|
386
|
+
const cachedResolver = createCachedEntityResolver(flyo, options.resolver);
|
|
355
387
|
async function entityRoute(props) {
|
|
356
388
|
const entity = await cachedResolver(props);
|
|
357
389
|
if (options.render) {
|
|
@@ -362,7 +394,7 @@ function nitroEntityRoute(flyo, options) {
|
|
|
362
394
|
return entityRoute;
|
|
363
395
|
}
|
|
364
396
|
function nitroEntityGenerateMetadata(flyo, options) {
|
|
365
|
-
const cachedResolver = createCachedEntityResolver(options.resolver);
|
|
397
|
+
const cachedResolver = createCachedEntityResolver(flyo, options.resolver);
|
|
366
398
|
return async (props) => {
|
|
367
399
|
const entity = await cachedResolver(props);
|
|
368
400
|
const title = entity.entity?.entity_title || "";
|
|
@@ -397,12 +429,15 @@ function nitroEntityGenerateMetadata(flyo, options) {
|
|
|
397
429
|
NitroEntityJsonLd,
|
|
398
430
|
NitroPage,
|
|
399
431
|
NitroSlot,
|
|
432
|
+
createLanguageLinksStore,
|
|
400
433
|
getLanguageLinks,
|
|
401
434
|
initNitro,
|
|
402
435
|
nitroEntityGenerateMetadata,
|
|
403
436
|
nitroEntityRoute,
|
|
404
437
|
nitroPageGenerateMetadata,
|
|
405
438
|
nitroPageGenerateStaticParams,
|
|
406
|
-
nitroPageRoute
|
|
439
|
+
nitroPageRoute,
|
|
440
|
+
publishLanguageLinks,
|
|
441
|
+
readLanguageLinks
|
|
407
442
|
});
|
|
408
443
|
//# sourceMappingURL=server.js.map
|
package/dist/server.js.map
CHANGED
|
@@ -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 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 // 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 /** Publish the links once. Later calls are ignored (first publish wins). */\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. You only need to call it\n * yourself on routes that render the shared switcher **without** going through\n * those helpers — a custom page, or `not-found.tsx` — where you would otherwise\n * leave the footer waiting forever. Pass a fallback there, e.g.\n * `publishLanguageLinks(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 const entity = await resolver(params);\n\n if (!entity) {\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
|
@@ -100,6 +100,7 @@ function initNitro({
|
|
|
100
100
|
if (!page) {
|
|
101
101
|
notFound();
|
|
102
102
|
}
|
|
103
|
+
publishLanguageLinks(getLanguageLinks(page.translation, { currentLang: lang2, locales: state.locales }));
|
|
103
104
|
return { page, path, lang: lang2, cfg };
|
|
104
105
|
});
|
|
105
106
|
const sitemap = async () => {
|
|
@@ -140,6 +141,31 @@ function initNitro({
|
|
|
140
141
|
sitemap
|
|
141
142
|
};
|
|
142
143
|
}
|
|
144
|
+
function createLanguageLinksStore() {
|
|
145
|
+
let resolve;
|
|
146
|
+
const promise = new Promise((r) => {
|
|
147
|
+
resolve = r;
|
|
148
|
+
});
|
|
149
|
+
let settled = false;
|
|
150
|
+
return {
|
|
151
|
+
publish(links) {
|
|
152
|
+
if (!settled) {
|
|
153
|
+
settled = true;
|
|
154
|
+
resolve(links);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
read() {
|
|
158
|
+
return promise;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
var languageLinksStore = cache(createLanguageLinksStore);
|
|
163
|
+
function publishLanguageLinks(links) {
|
|
164
|
+
languageLinksStore().publish(links);
|
|
165
|
+
}
|
|
166
|
+
function readLanguageLinks() {
|
|
167
|
+
return languageLinksStore().read();
|
|
168
|
+
}
|
|
143
169
|
var readEnv = (key, fallback = "") => {
|
|
144
170
|
const value = process.env[key];
|
|
145
171
|
if (value !== void 0 && value !== "") {
|
|
@@ -164,12 +190,15 @@ function buildLanguageAlternates(translations, currentLang) {
|
|
|
164
190
|
}
|
|
165
191
|
return canonical ? { canonical, languages } : { languages };
|
|
166
192
|
}
|
|
167
|
-
function createCachedEntityResolver(resolver) {
|
|
193
|
+
function createCachedEntityResolver(flyo, resolver) {
|
|
168
194
|
return cache(async ({ params }) => {
|
|
169
195
|
const entity = await resolver(params);
|
|
170
196
|
if (!entity) {
|
|
171
197
|
notFound();
|
|
172
198
|
}
|
|
199
|
+
publishLanguageLinks(
|
|
200
|
+
getLanguageLinks(entity.translation, { currentLang: entity.language, locales: flyo.state.locales })
|
|
201
|
+
);
|
|
173
202
|
return entity;
|
|
174
203
|
});
|
|
175
204
|
}
|
|
@@ -323,7 +352,7 @@ function nitroPageGenerateStaticParams(flyo) {
|
|
|
323
352
|
};
|
|
324
353
|
}
|
|
325
354
|
function nitroEntityRoute(flyo, options) {
|
|
326
|
-
const cachedResolver = createCachedEntityResolver(options.resolver);
|
|
355
|
+
const cachedResolver = createCachedEntityResolver(flyo, options.resolver);
|
|
327
356
|
async function entityRoute(props) {
|
|
328
357
|
const entity = await cachedResolver(props);
|
|
329
358
|
if (options.render) {
|
|
@@ -334,7 +363,7 @@ function nitroEntityRoute(flyo, options) {
|
|
|
334
363
|
return entityRoute;
|
|
335
364
|
}
|
|
336
365
|
function nitroEntityGenerateMetadata(flyo, options) {
|
|
337
|
-
const cachedResolver = createCachedEntityResolver(options.resolver);
|
|
366
|
+
const cachedResolver = createCachedEntityResolver(flyo, options.resolver);
|
|
338
367
|
return async (props) => {
|
|
339
368
|
const entity = await cachedResolver(props);
|
|
340
369
|
const title = entity.entity?.entity_title || "";
|
|
@@ -368,12 +397,15 @@ export {
|
|
|
368
397
|
NitroEntityJsonLd,
|
|
369
398
|
NitroPage,
|
|
370
399
|
NitroSlot,
|
|
400
|
+
createLanguageLinksStore,
|
|
371
401
|
getLanguageLinks,
|
|
372
402
|
initNitro,
|
|
373
403
|
nitroEntityGenerateMetadata,
|
|
374
404
|
nitroEntityRoute,
|
|
375
405
|
nitroPageGenerateMetadata,
|
|
376
406
|
nitroPageGenerateStaticParams,
|
|
377
|
-
nitroPageRoute
|
|
407
|
+
nitroPageRoute,
|
|
408
|
+
publishLanguageLinks,
|
|
409
|
+
readLanguageLinks
|
|
378
410
|
};
|
|
379
411
|
//# sourceMappingURL=server.mjs.map
|
package/dist/server.mjs.map
CHANGED
|
@@ -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 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 // 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 /** Publish the links once. Later calls are ignored (first publish wins). */\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. You only need to call it\n * yourself on routes that render the shared switcher **without** going through\n * those helpers — a custom page, or `not-found.tsx` — where you would otherwise\n * leave the footer waiting forever. Pass a fallback there, e.g.\n * `publishLanguageLinks(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 const entity = await resolver(params);\n\n if (!entity) {\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"]}
|