@lynkow/next 0.1.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/LICENSE +29 -0
- package/README.md +257 -0
- package/dist/chunk-4QTGOTFV.js +95 -0
- package/dist/chunk-4QTGOTFV.js.map +1 -0
- package/dist/chunk-XNN4SECG.js +55 -0
- package/dist/chunk-XNN4SECG.js.map +1 -0
- package/dist/client/index.d.ts +152 -0
- package/dist/client/index.js +170 -0
- package/dist/client/index.js.map +1 -0
- package/dist/config-CwDV7jFS.d.ts +80 -0
- package/dist/image-loader.d.ts +34 -0
- package/dist/image-loader.js +21 -0
- package/dist/image-loader.js.map +1 -0
- package/dist/index.d.ts +216 -0
- package/dist/index.js +169 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy/index.d.ts +236 -0
- package/dist/proxy/index.js +219 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/server/index.d.ts +196 -0
- package/dist/server/index.js +263 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +85 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// src/visual-editor/provider.tsx
|
|
4
|
+
import { useState, useEffect, useRef } from "react";
|
|
5
|
+
import { initVisualEditor } from "lynkow/visual-editor";
|
|
6
|
+
|
|
7
|
+
// src/visual-editor/context.ts
|
|
8
|
+
import { createContext } from "react";
|
|
9
|
+
var LynkowVisualEditorContext = createContext({
|
|
10
|
+
instance: null,
|
|
11
|
+
isPreviewMode: false,
|
|
12
|
+
blocksData: {}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// src/visual-editor/provider.tsx
|
|
16
|
+
import { jsx } from "react/jsx-runtime";
|
|
17
|
+
function LynkowVisualEditor({ children, ...config }) {
|
|
18
|
+
const [blocksData, setBlocksData] = useState({});
|
|
19
|
+
const [isPreviewMode, setIsPreviewMode] = useState(false);
|
|
20
|
+
const instanceRef = useRef(null);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (typeof window === "undefined") return;
|
|
23
|
+
const instance = initVisualEditor(config);
|
|
24
|
+
instanceRef.current = instance;
|
|
25
|
+
if (instance.isPreviewMode()) {
|
|
26
|
+
setIsPreviewMode(true);
|
|
27
|
+
const unsubscribe = instance.onDataChange(
|
|
28
|
+
(blockSlug, _fieldKey, _fieldPath, _value, allData) => {
|
|
29
|
+
setBlocksData((prev) => ({
|
|
30
|
+
...prev,
|
|
31
|
+
[blockSlug]: allData
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
return () => {
|
|
36
|
+
unsubscribe();
|
|
37
|
+
instance.destroy();
|
|
38
|
+
instanceRef.current = null;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return () => {
|
|
42
|
+
instance.destroy();
|
|
43
|
+
instanceRef.current = null;
|
|
44
|
+
};
|
|
45
|
+
}, [config.cmsOrigin]);
|
|
46
|
+
const contextValue = {
|
|
47
|
+
instance: instanceRef.current,
|
|
48
|
+
isPreviewMode,
|
|
49
|
+
blocksData
|
|
50
|
+
};
|
|
51
|
+
return /* @__PURE__ */ jsx(LynkowVisualEditorContext.Provider, { value: contextValue, children });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/visual-editor/hooks.ts
|
|
55
|
+
import { useContext, useMemo } from "react";
|
|
56
|
+
function useBlockData(blockSlug, initialData) {
|
|
57
|
+
const { isPreviewMode, blocksData } = useContext(LynkowVisualEditorContext);
|
|
58
|
+
return useMemo(() => {
|
|
59
|
+
if (!isPreviewMode) {
|
|
60
|
+
return initialData;
|
|
61
|
+
}
|
|
62
|
+
const previewData = blocksData[blockSlug];
|
|
63
|
+
if (!previewData) {
|
|
64
|
+
return initialData;
|
|
65
|
+
}
|
|
66
|
+
return { ...initialData, ...previewData };
|
|
67
|
+
}, [isPreviewMode, blocksData, blockSlug, initialData]);
|
|
68
|
+
}
|
|
69
|
+
function useLynkowField(blockSlug, fieldKey, initialValue) {
|
|
70
|
+
const { isPreviewMode, blocksData } = useContext(LynkowVisualEditorContext);
|
|
71
|
+
return useMemo(() => {
|
|
72
|
+
if (!isPreviewMode) {
|
|
73
|
+
return initialValue;
|
|
74
|
+
}
|
|
75
|
+
const blockData = blocksData[blockSlug];
|
|
76
|
+
if (!blockData || !(fieldKey in blockData)) {
|
|
77
|
+
return initialValue;
|
|
78
|
+
}
|
|
79
|
+
return blockData[fieldKey];
|
|
80
|
+
}, [isPreviewMode, blocksData, blockSlug, fieldKey, initialValue]);
|
|
81
|
+
}
|
|
82
|
+
function useIsPreviewMode() {
|
|
83
|
+
const { isPreviewMode } = useContext(LynkowVisualEditorContext);
|
|
84
|
+
return isPreviewMode;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/visual-editor/editable.tsx
|
|
88
|
+
import { createContext as createContext2, useContext as useContext2 } from "react";
|
|
89
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
90
|
+
var BlockSlugContext = createContext2(null);
|
|
91
|
+
function EditableBlock({
|
|
92
|
+
slug,
|
|
93
|
+
as: As = "div",
|
|
94
|
+
className,
|
|
95
|
+
children
|
|
96
|
+
}) {
|
|
97
|
+
return /* @__PURE__ */ jsx2(BlockSlugContext.Provider, { value: slug, children: /* @__PURE__ */ jsx2(As, { "data-lynkow-block": slug, className, children }) });
|
|
98
|
+
}
|
|
99
|
+
function Editable({
|
|
100
|
+
field,
|
|
101
|
+
label,
|
|
102
|
+
type = "text",
|
|
103
|
+
as: As = "span",
|
|
104
|
+
className,
|
|
105
|
+
children,
|
|
106
|
+
html
|
|
107
|
+
}) {
|
|
108
|
+
const slug = useContext2(BlockSlugContext);
|
|
109
|
+
if (slug === null && process.env.NODE_ENV !== "production") {
|
|
110
|
+
console.error(
|
|
111
|
+
`[lynkow] <Editable field="${field}"> has no <EditableBlock> ancestor, so the editor's scanner will skip it.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const initial = html !== void 0 ? html : children;
|
|
115
|
+
const value = useLynkowField(slug ?? "", field, initial);
|
|
116
|
+
const markers = {
|
|
117
|
+
"data-lynkow-field": field,
|
|
118
|
+
"data-lynkow-type": type,
|
|
119
|
+
"data-lynkow-label": label ?? field
|
|
120
|
+
};
|
|
121
|
+
if (html !== void 0) {
|
|
122
|
+
return /* @__PURE__ */ jsx2(As, { ...markers, className, dangerouslySetInnerHTML: { __html: value } });
|
|
123
|
+
}
|
|
124
|
+
return /* @__PURE__ */ jsx2(As, { ...markers, className, children: value });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/analytics/tracker.tsx
|
|
128
|
+
import { useEffect as useEffect2 } from "react";
|
|
129
|
+
var TRACKER_ID = "lynkow-tracker";
|
|
130
|
+
function trackerAllowed(consent) {
|
|
131
|
+
switch (consent.mode) {
|
|
132
|
+
case null:
|
|
133
|
+
case "notice-only":
|
|
134
|
+
return true;
|
|
135
|
+
case "opt-out":
|
|
136
|
+
return consent.state !== "denied";
|
|
137
|
+
case "opt-in":
|
|
138
|
+
return consent.state === "granted";
|
|
139
|
+
default:
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function LynkowTracker({ config, consent }) {
|
|
144
|
+
const allowed = trackerAllowed(consent);
|
|
145
|
+
const { mode } = consent;
|
|
146
|
+
const { siteId, apiUrl } = config;
|
|
147
|
+
useEffect2(() => {
|
|
148
|
+
if (!allowed || document.getElementById(TRACKER_ID)) return;
|
|
149
|
+
const script = document.createElement("script");
|
|
150
|
+
script.id = TRACKER_ID;
|
|
151
|
+
script.src = `${apiUrl}/analytics/tracker.js`;
|
|
152
|
+
script.async = true;
|
|
153
|
+
script.dataset["siteId"] = siteId;
|
|
154
|
+
script.dataset["apiUrl"] = apiUrl;
|
|
155
|
+
if (mode) script.dataset["consentMode"] = mode;
|
|
156
|
+
document.body.appendChild(script);
|
|
157
|
+
}, [allowed, mode, siteId, apiUrl]);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
Editable,
|
|
162
|
+
EditableBlock,
|
|
163
|
+
LynkowTracker,
|
|
164
|
+
LynkowVisualEditor,
|
|
165
|
+
trackerAllowed,
|
|
166
|
+
useBlockData,
|
|
167
|
+
useIsPreviewMode,
|
|
168
|
+
useLynkowField
|
|
169
|
+
};
|
|
170
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/visual-editor/provider.tsx","../../src/visual-editor/context.ts","../../src/visual-editor/hooks.ts","../../src/visual-editor/editable.tsx","../../src/analytics/tracker.tsx"],"sourcesContent":["import React, { useState, useEffect, useRef } from 'react'\nimport { initVisualEditor, type VisualEditorConfig, type VisualEditorInstance } from 'lynkow/visual-editor'\nimport { LynkowVisualEditorContext } from './context.js'\n\n/**\n * Props of {@link LynkowVisualEditor}.\n *\n * Extends {@link VisualEditorConfig}, so `cmsOrigin` is documented once, on the\n * config the core reads, and means the same thing here.\n */\nexport interface LynkowVisualEditorProps extends VisualEditorConfig {\n /** Your application. Rendered unchanged; the provider adds no wrapper element. */\n children: React.ReactNode\n}\n\nexport function LynkowVisualEditor({ children, ...config }: LynkowVisualEditorProps) {\n const [blocksData, setBlocksData] = useState<Record<string, Record<string, any>>>({})\n const [isPreviewMode, setIsPreviewMode] = useState(false)\n const instanceRef = useRef<VisualEditorInstance | null>(null)\n\n useEffect(() => {\n if (typeof window === 'undefined') return\n\n const instance = initVisualEditor(config)\n instanceRef.current = instance\n\n if (instance.isPreviewMode()) {\n setIsPreviewMode(true)\n\n const unsubscribe = instance.onDataChange(\n (blockSlug, _fieldKey, _fieldPath, _value, allData) => {\n setBlocksData((prev) => ({\n ...prev,\n [blockSlug]: allData,\n }))\n }\n )\n\n return () => {\n unsubscribe()\n instance.destroy()\n instanceRef.current = null\n }\n }\n\n return () => {\n instance.destroy()\n instanceRef.current = null\n }\n // Keyed on the field, never on `config`: the rest object is a fresh identity\n // every render, which would tear the editor down and rebuild it each time.\n }, [config.cmsOrigin])\n\n const contextValue = {\n instance: instanceRef.current,\n isPreviewMode,\n blocksData,\n }\n\n return (\n <LynkowVisualEditorContext.Provider value={contextValue}>\n {children}\n </LynkowVisualEditorContext.Provider>\n )\n}\n","import { createContext } from 'react'\nimport type { VisualEditorInstance } from 'lynkow/visual-editor'\n\nexport interface LynkowVisualEditorContextValue {\n instance: VisualEditorInstance | null\n isPreviewMode: boolean\n blocksData: Record<string, Record<string, any>>\n}\n\nexport const LynkowVisualEditorContext = createContext<LynkowVisualEditorContextValue>({\n instance: null,\n isPreviewMode: false,\n blocksData: {},\n})\n","import { useContext, useMemo } from 'react'\nimport { LynkowVisualEditorContext } from './context.js'\n\nexport function useBlockData<T extends Record<string, any>>(\n blockSlug: string,\n initialData: T\n): T {\n const { isPreviewMode, blocksData } = useContext(LynkowVisualEditorContext)\n\n return useMemo(() => {\n if (!isPreviewMode) {\n return initialData\n }\n\n const previewData = blocksData[blockSlug]\n if (!previewData) {\n return initialData\n }\n\n return { ...initialData, ...previewData } as T\n }, [isPreviewMode, blocksData, blockSlug, initialData])\n}\n\nexport function useLynkowField<T>(blockSlug: string, fieldKey: string, initialValue: T): T {\n const { isPreviewMode, blocksData } = useContext(LynkowVisualEditorContext)\n\n return useMemo(() => {\n if (!isPreviewMode) {\n return initialValue\n }\n\n const blockData = blocksData[blockSlug]\n if (!blockData || !(fieldKey in blockData)) {\n return initialValue\n }\n\n return blockData[fieldKey] as T\n }, [isPreviewMode, blocksData, blockSlug, fieldKey, initialValue])\n}\n\nexport function useIsPreviewMode(): boolean {\n const { isPreviewMode } = useContext(LynkowVisualEditorContext)\n return isPreviewMode\n}\n","import { createContext, useContext, type ElementType, type ReactNode } from 'react'\nimport { useLynkowField } from './hooks.js'\n\n/**\n * The two markers the SDK's DOM scanner looks for, and the contract between\n * them.\n *\n * On activation the editor runs `document.querySelectorAll('[data-lynkow-block]')`\n * and, inside each match, `querySelectorAll('[data-lynkow-field]')`. A field\n * that is not a DESCENDANT of a block is invisible to it, which is why the slug\n * travels through context rather than being repeated on every field: the two\n * cannot drift apart if only one of them names it.\n *\n * Three consequences of that scan being a ONE-OFF at activation:\n *\n * - **An empty field cannot be discovered from the page.** A site that hides\n * a section whose text is blank leaves no element carrying the marker in\n * the DOM, so the editor never learns the field exists. It is still\n * editable from the dashboard's own field list; only the click-on-the-page\n * path is missing.\n * - **The CMS has the last word.** Hovering is gated on\n * `getEditableFields(slug).includes(key)`, sent by the admin at handshake.\n * A field marked here that the site's block schema does not declare is\n * ignored rather than broken.\n * - **Inline editing reads `textContent`.** The editor sets `contentEditable`\n * on the marked element, so mark the element that holds the text and\n * nothing else. Wrapping a whole section would make the editor overwrite\n * the section with a flat string on the first keystroke.\n */\nconst BlockSlugContext = createContext<string | null>(null)\n\n/**\n * Names the site block this subtree renders.\n *\n * `as` exists so this adds no DOM node: pass the element the block already had\n * (`as=\"header\"`, `as=\"section\"`) rather than nesting a div inside it.\n */\nexport function EditableBlock({\n slug,\n as: As = 'div',\n className,\n children,\n}: {\n slug: string\n as?: ElementType\n className?: string\n children: ReactNode\n}) {\n return (\n <BlockSlugContext.Provider value={slug}>\n <As data-lynkow-block={slug} className={className}>\n {children}\n </As>\n </BlockSlugContext.Provider>\n )\n}\n\ntype EditableCommon = {\n /** The field key, exactly as the site block schema spells it. */\n field: string\n /** What the editor's badge shows. Defaults to the field key. */\n label?: string\n /** Picks the badge icon and tells the admin which control to open. */\n type?: 'text' | 'richtext' | 'url' | 'image' | 'number' | 'date' | 'select' | 'boolean'\n as?: ElementType\n className?: string\n}\n\n/**\n * `children` for a plain text field, `html` for a richtext one. Never both:\n * a richtext field is injected rather than rendered, so the two cannot share a\n * code path without also sharing the injection.\n */\ntype EditableProps =\n | (EditableCommon & { children: string; html?: never })\n | (EditableCommon & { html: string; children?: never })\n\n/**\n * One editable field.\n *\n * The value passed in is the SERVER-rendered one and stays the value in\n * production: `useLynkowField` returns its third argument whenever the page is\n * not inside the editor. Inside it, the hook returns whatever the admin has\n * typed, so a change made in the dashboard's side panel appears here without a\n * reload.\n *\n * FLAT KEYS ONLY. The hook indexes the block data with `key in data`, so a\n * repeater row (`services[0].title`) has no addressable key and cannot be\n * marked. Those fields stay editable from the dashboard; they are simply not\n * clickable on the page. Marking the repeater's container would be worse than\n * not marking it, because inline editing would flatten the whole list to text.\n */\nexport function Editable({\n field,\n label,\n type = 'text',\n as: As = 'span',\n className,\n children,\n html,\n}: EditableProps) {\n const slug = useContext(BlockSlugContext)\n\n // Dot access on purpose: bundlers replace `process.env.NODE_ENV` as text.\n if (slug === null && (process.env as { NODE_ENV?: string }).NODE_ENV !== 'production') {\n console.error(\n `[lynkow] <Editable field=\"${field}\"> has no <EditableBlock> ancestor, so the editor's scanner will skip it.`\n )\n }\n\n const initial = html !== undefined ? html : children\n const value = useLynkowField<string>(slug ?? '', field, initial)\n\n const markers = {\n 'data-lynkow-field': field,\n 'data-lynkow-type': type,\n 'data-lynkow-label': label ?? field,\n }\n\n if (html !== undefined) {\n // Same trust boundary as an article body: richtext arrives as HTML the API\n // sanitized server side. In preview the string comes from the admin over\n // PostMessage instead, which is the same author through a different pipe.\n return <As {...markers} className={className} dangerouslySetInnerHTML={{ __html: value }} />\n }\n\n return (\n <As {...markers} className={className}>\n {value}\n </As>\n )\n}\n","import { useEffect } from 'react'\nimport type { LynkowNextConfig } from '../config.js'\n\n/**\n * What the site's consent state lets the analytics tracker do.\n *\n * - `mode: null`: the site has no consent gate, because the banner is off in\n * the admin or the consent code was removed. Loading the tracker is not this\n * component's decision to refuse.\n * - `notice-only`: the banner informs rather than asks, so the tracker loads.\n * - `opt-out`: the tracker loads unless the visitor refused.\n * - `opt-in`: the tracker loads only once the visitor granted analytics.\n * `unavailable` means the consent configuration could not be read, so there\n * is no choice to read against: the tracker never loads from here then.\n *\n * A mode outside these four never loads it: a value this package does not know\n * cannot be read as consent.\n *\n * This decides whether the script is ADDED. Once it runs, the tracker follows\n * later answers through Lynkow's own consent banner, which it listens to; a\n * refusal given through another banner reaches it on the next full page load.\n */\nexport type TrackerConsent =\n | { mode: null; state: 'not-required' }\n | { mode: 'opt-out' | 'notice-only'; state: 'pending' | 'granted' | 'denied' }\n | { mode: 'opt-in'; state: 'pending' | 'granted' | 'denied' | 'unavailable' }\n\n/** Props of {@link LynkowTracker}. */\nexport interface LynkowTrackerProps {\n config: Pick<LynkowNextConfig, 'siteId' | 'apiUrl'>\n consent: TrackerConsent\n}\n\n/** The id the SDK's loader gives the tracker and looks for before adding one. */\nconst TRACKER_ID = 'lynkow-tracker'\n\n/** Whether the tracker may load under this consent state. */\nexport function trackerAllowed(consent: TrackerConsent): boolean {\n switch (consent.mode) {\n case null:\n case 'notice-only':\n return true\n case 'opt-out':\n return consent.state !== 'denied'\n case 'opt-in':\n return consent.state === 'granted'\n default:\n return false\n }\n}\n\n/**\n * Loads Lynkow's analytics tracker when the consent state allows it, unless a\n * copy is already on the page.\n *\n * The tracker is a self-contained script the API serves. It records pageviews,\n * client-side navigations included, scroll depth, engaged time, outbound and\n * file links, rage and dead clicks, form events, Core Web Vitals and click\n * heatmaps on its own.\n *\n * ONE TRACKER, NEVER TWO. A browser client of the SDK loads `tracker.js` on its\n * own, under the id `lynkow-tracker`, and two copies each send every event.\n * This component therefore injects the script only when no copy exists, under\n * that same id, which the SDK's loader checks and waits on instead of adding\n * its own.\n *\n * The consent mode is passed to the script as `data-consent-mode`. Without it\n * the tracker works the mode out from the page, and on a site whose banner is\n * not the SDK's own it finds none and tracks before the visitor answers.\n *\n * Renders nothing.\n */\nexport function LynkowTracker({ config, consent }: LynkowTrackerProps): null {\n const allowed = trackerAllowed(consent)\n const { mode } = consent\n const { siteId, apiUrl } = config\n\n useEffect(() => {\n if (!allowed || document.getElementById(TRACKER_ID)) return\n\n const script = document.createElement('script')\n script.id = TRACKER_ID\n script.src = `${apiUrl}/analytics/tracker.js`\n script.async = true\n script.dataset['siteId'] = siteId\n script.dataset['apiUrl'] = apiUrl\n if (mode) script.dataset['consentMode'] = mode\n document.body.appendChild(script)\n }, [allowed, mode, siteId, apiUrl])\n\n return null\n}\n"],"mappings":"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","names":["createContext","useContext","jsx","createContext","useContext","useEffect"]}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The configuration a Lynkow site hands this adapter, and the one function
|
|
3
|
+
* that validates it.
|
|
4
|
+
*
|
|
5
|
+
* Nothing in this package reads `process.env`. Next inlines `NEXT_PUBLIC_*`
|
|
6
|
+
* variables at build time by textual substitution, so only a literal
|
|
7
|
+
* `process.env.NEXT_PUBLIC_X` written in the application's own source reaches
|
|
8
|
+
* the browser bundle, and a library cannot spell out names an application
|
|
9
|
+
* chose. The application reads its environment and passes the values here.
|
|
10
|
+
*
|
|
11
|
+
* Everything in the resolved configuration is public, because it reaches
|
|
12
|
+
* client bundles. A server secret, such as the webhook signing secret, never
|
|
13
|
+
* belongs in it.
|
|
14
|
+
*/
|
|
15
|
+
/** What an application passes to {@link resolveConfig}, typically straight from its environment. */
|
|
16
|
+
interface LynkowNextConfigInput {
|
|
17
|
+
/** The site's UUID, shown in the Lynkow admin and returned by the MCP `list_sites` tool. */
|
|
18
|
+
siteId: string | undefined;
|
|
19
|
+
/** The public origin of the site, such as `https://www.example.com`. No path, query or fragment. */
|
|
20
|
+
siteUrl: string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* The locales the site serves, as an array or a comma-separated list, in the
|
|
23
|
+
* order they should be advertised. Must mirror the site's enabled locales.
|
|
24
|
+
*/
|
|
25
|
+
locales: string | readonly string[] | undefined;
|
|
26
|
+
/** The default locale. Defaults to the first of `locales`. */
|
|
27
|
+
defaultLocale?: string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Whether the default locale carries its prefix in public URLs. `true`, the
|
|
30
|
+
* default, serves `/en/about` and `/fr/a-propos`; `false` serves `/about` and
|
|
31
|
+
* `/fr/a-propos`. The strings `"true"` and `"false"` are accepted too, so an
|
|
32
|
+
* environment variable can be passed as it is.
|
|
33
|
+
*/
|
|
34
|
+
prefixDefaultLocale?: boolean | string | undefined;
|
|
35
|
+
/** The Lynkow API origin. Defaults to `https://api.lynkow.com`. */
|
|
36
|
+
apiUrl?: string | undefined;
|
|
37
|
+
/** The site's publishable key (`lkw_pk_...`), which storefront reads need. */
|
|
38
|
+
publishableKey?: string | undefined;
|
|
39
|
+
}
|
|
40
|
+
/** A validated configuration, frozen. Every field is safe to ship to the browser. */
|
|
41
|
+
interface LynkowNextConfig {
|
|
42
|
+
readonly siteId: string;
|
|
43
|
+
/** The site's origin, with no trailing slash, so `${siteUrl}${path}` is always well formed. */
|
|
44
|
+
readonly siteUrl: string;
|
|
45
|
+
/** The API origin, with no trailing slash. */
|
|
46
|
+
readonly apiUrl: string;
|
|
47
|
+
readonly publishableKey: string | undefined;
|
|
48
|
+
/** The locales as configured, in their configured case. URLs carry them lowercased. */
|
|
49
|
+
readonly locales: readonly string[];
|
|
50
|
+
/** One of `locales`, in its configured case. */
|
|
51
|
+
readonly defaultLocale: string;
|
|
52
|
+
readonly prefixDefaultLocale: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Names to use for each field in error messages, such as the environment
|
|
56
|
+
* variable the application reads it from.
|
|
57
|
+
*/
|
|
58
|
+
type LynkowNextConfigLabels = Partial<Record<keyof LynkowNextConfigInput, string>>;
|
|
59
|
+
/**
|
|
60
|
+
* Validates a configuration and fills in its defaults.
|
|
61
|
+
*
|
|
62
|
+
* It throws from a call the application made, never while this module is
|
|
63
|
+
* imported, so a site owner gets an error they can read rather than a stack
|
|
64
|
+
* trace out of a dependency's import graph.
|
|
65
|
+
*
|
|
66
|
+
* @param input - The values, typically read from the application's environment
|
|
67
|
+
* @param labels - Optional names for the fields in error messages, such as
|
|
68
|
+
* `{ siteId: 'NEXT_PUBLIC_LYNKOW_SITE_ID' }`
|
|
69
|
+
* @throws {TypeError} Listing every problem found, one per line
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* export const config = resolveConfig({
|
|
73
|
+
* siteId: process.env.NEXT_PUBLIC_LYNKOW_SITE_ID,
|
|
74
|
+
* siteUrl: process.env.NEXT_PUBLIC_SITE_URL,
|
|
75
|
+
* locales: process.env.NEXT_PUBLIC_LOCALES,
|
|
76
|
+
* })
|
|
77
|
+
*/
|
|
78
|
+
declare function resolveConfig(input: LynkowNextConfigInput, labels?: LynkowNextConfigLabels): LynkowNextConfig;
|
|
79
|
+
|
|
80
|
+
export { type LynkowNextConfig as L, type LynkowNextConfigInput as a, type LynkowNextConfigLabels as b, resolveConfig as r };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** What `next/image` passes to a custom loader. */
|
|
2
|
+
interface ImageLoaderParams {
|
|
3
|
+
src: string;
|
|
4
|
+
width: number;
|
|
5
|
+
quality?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A `next/image` loader that resizes Lynkow media on the Lynkow CDN.
|
|
9
|
+
*
|
|
10
|
+
* Next's own optimizer downloads the original, resizes it on the app server
|
|
11
|
+
* and caches the result on that server's disk. Lynkow already resizes at the
|
|
12
|
+
* CDN edge, close to the visitor and cached there, so with this loader the app
|
|
13
|
+
* server never touches an image. Everything else about `next/image` stays:
|
|
14
|
+
* lazy loading, `priority`, reserved space, and the srcset Next builds by
|
|
15
|
+
* calling the loader once per candidate width.
|
|
16
|
+
*
|
|
17
|
+
* A relative URL, a file in `/public`, comes back unchanged: Lynkow media is
|
|
18
|
+
* always absolute, and the SDK recognises it by its path alone, so a local
|
|
19
|
+
* `/sites/logo.png` would otherwise be sent to a resizing endpoint this server
|
|
20
|
+
* does not have. An absolute URL whose path is not Lynkow media comes back
|
|
21
|
+
* unchanged too.
|
|
22
|
+
*
|
|
23
|
+
* Next wants a FILE for a custom loader, so re-export it from one:
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // src/image-loader.ts
|
|
27
|
+
* export { default } from '@lynkow/next/image-loader'
|
|
28
|
+
*
|
|
29
|
+
* // next.config.ts
|
|
30
|
+
* images: { loader: 'custom', loaderFile: './src/image-loader.ts' }
|
|
31
|
+
*/
|
|
32
|
+
declare function lynkowImageLoader({ src, width, quality }: ImageLoaderParams): string;
|
|
33
|
+
|
|
34
|
+
export { type ImageLoaderParams, lynkowImageLoader as default, lynkowImageLoader };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { MediaHelperService } from 'lynkow';
|
|
2
|
+
|
|
3
|
+
// src/image-loader.ts
|
|
4
|
+
var media = new MediaHelperService();
|
|
5
|
+
function lynkowImageLoader({ src, width, quality }) {
|
|
6
|
+
if (!/^https?:\/\//i.test(src)) return src;
|
|
7
|
+
return media.transform(src, {
|
|
8
|
+
w: width,
|
|
9
|
+
// `scale-down` never upscales, so a small original stays small rather than
|
|
10
|
+
// being blown up to the layout's widest candidate.
|
|
11
|
+
fit: "scale-down",
|
|
12
|
+
// The CDN negotiates the format from `Accept`, so a modern browser gets
|
|
13
|
+
// WebP or AVIF without the application knowing which.
|
|
14
|
+
format: "auto",
|
|
15
|
+
quality: quality ?? 80
|
|
16
|
+
}) || src;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { lynkowImageLoader as default, lynkowImageLoader };
|
|
20
|
+
//# sourceMappingURL=image-loader.js.map
|
|
21
|
+
//# sourceMappingURL=image-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/image-loader.ts"],"names":[],"mappings":";;;AAGA,IAAM,KAAA,GAAQ,IAAI,kBAAA,EAAmB;AAkCtB,SAAR,iBAAA,CAAmC,EAAE,GAAA,EAAK,KAAA,EAAO,SAAQ,EAA8B;AAC5F,EAAA,IAAI,CAAC,eAAA,CAAgB,IAAA,CAAK,GAAG,GAAG,OAAO,GAAA;AACvC,EAAA,OACE,KAAA,CAAM,UAAU,GAAA,EAAK;AAAA,IACnB,CAAA,EAAG,KAAA;AAAA;AAAA;AAAA,IAGH,GAAA,EAAK,YAAA;AAAA;AAAA;AAAA,IAGL,MAAA,EAAQ,MAAA;AAAA,IACR,SAAS,OAAA,IAAW;AAAA,GACrB,CAAA,IAAK,GAAA;AAEV","file":"image-loader.js","sourcesContent":["import { MediaHelperService } from 'lynkow'\n\n/** Stateless and pure: it only rewrites a URL, so one instance serves every call. */\nconst media = new MediaHelperService()\n\n/** What `next/image` passes to a custom loader. */\nexport interface ImageLoaderParams {\n src: string\n width: number\n quality?: number\n}\n\n/**\n * A `next/image` loader that resizes Lynkow media on the Lynkow CDN.\n *\n * Next's own optimizer downloads the original, resizes it on the app server\n * and caches the result on that server's disk. Lynkow already resizes at the\n * CDN edge, close to the visitor and cached there, so with this loader the app\n * server never touches an image. Everything else about `next/image` stays:\n * lazy loading, `priority`, reserved space, and the srcset Next builds by\n * calling the loader once per candidate width.\n *\n * A relative URL, a file in `/public`, comes back unchanged: Lynkow media is\n * always absolute, and the SDK recognises it by its path alone, so a local\n * `/sites/logo.png` would otherwise be sent to a resizing endpoint this server\n * does not have. An absolute URL whose path is not Lynkow media comes back\n * unchanged too.\n *\n * Next wants a FILE for a custom loader, so re-export it from one:\n *\n * @example\n * // src/image-loader.ts\n * export { default } from '@lynkow/next/image-loader'\n *\n * // next.config.ts\n * images: { loader: 'custom', loaderFile: './src/image-loader.ts' }\n */\nexport default function lynkowImageLoader({ src, width, quality }: ImageLoaderParams): string {\n if (!/^https?:\\/\\//i.test(src)) return src\n return (\n media.transform(src, {\n w: width,\n // `scale-down` never upscales, so a small original stays small rather than\n // being blown up to the layout's widest candidate.\n fit: 'scale-down',\n // The CDN negotiates the format from `Accept`, so a modern browser gets\n // WebP or AVIF without the application knowing which.\n format: 'auto',\n quality: quality ?? 80,\n }) || src\n )\n}\n\nexport { lynkowImageLoader }\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { L as LynkowNextConfig } from './config-CwDV7jFS.js';
|
|
2
|
+
export { a as LynkowNextConfigInput, b as LynkowNextConfigLabels, r as resolveConfig } from './config-CwDV7jFS.js';
|
|
3
|
+
import { BaseRequestOptions, Client, AnalyticsEventType } from 'lynkow';
|
|
4
|
+
export { AnalyticsEventType } from 'lynkow';
|
|
5
|
+
|
|
6
|
+
/** An href that names its own scheme (`https:`, `mailto:`), or a protocol-relative one. */
|
|
7
|
+
declare const ABSOLUTE_HREF: RegExp;
|
|
8
|
+
/**
|
|
9
|
+
* The one place a public path gets its locale, bound to a configuration.
|
|
10
|
+
*
|
|
11
|
+
* Every localized path of a site goes through {@link LocalePaths.localePath}
|
|
12
|
+
* or {@link LocalePaths.fromApiPath}. That is what makes `prefixDefaultLocale`
|
|
13
|
+
* hold everywhere: a path built by hand as `/${locale}/...` is right with the
|
|
14
|
+
* flag on and wrong with it off.
|
|
15
|
+
*/
|
|
16
|
+
interface LocalePaths {
|
|
17
|
+
readonly locales: readonly string[];
|
|
18
|
+
readonly defaultLocale: string;
|
|
19
|
+
readonly prefixDefaultLocale: boolean;
|
|
20
|
+
/** The configured spelling of a locale, matched case-insensitively, or null. */
|
|
21
|
+
toLocale(value: string | null | undefined): string | null;
|
|
22
|
+
/** Whether a value names a configured locale, in any case. */
|
|
23
|
+
isLocale(value: string | null | undefined): value is string;
|
|
24
|
+
/**
|
|
25
|
+
* The locale a path belongs to. A path whose first segment names a locale
|
|
26
|
+
* belongs to it. Any other path belongs to the default locale when the
|
|
27
|
+
* default locale is served unprefixed, and to none otherwise. That includes
|
|
28
|
+
* paths that are not pages at all, such as `/_next/...`, so a caller that
|
|
29
|
+
* routes requests filters those first.
|
|
30
|
+
*/
|
|
31
|
+
localeFromPath(pathname: string): string | null;
|
|
32
|
+
/**
|
|
33
|
+
* The public path of `path` in `locale`: `/fr/blog`, or `/blog` for the
|
|
34
|
+
* default locale when it is served unprefixed. The locale segment is written
|
|
35
|
+
* lowercased, as the API writes it. A query or fragment is kept. A leading
|
|
36
|
+
* run of slashes or backslashes collapses to one, so `//evil.example` stays a
|
|
37
|
+
* path once a default locale is stripped. Dot segments are left as written:
|
|
38
|
+
* `/.//evil.example` resolves to `//evil.example` in `new URL()`, so a caller
|
|
39
|
+
* that resolves the result checks the resolved path, as the proxy does.
|
|
40
|
+
*
|
|
41
|
+
* @throws {RangeError} When `locale` is not a configured locale.
|
|
42
|
+
*/
|
|
43
|
+
localePath(locale: string, path?: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* The public path of a path the API returned.
|
|
46
|
+
*
|
|
47
|
+
* On a multilingual site the API prefixes every path with its row's locale,
|
|
48
|
+
* the default one included, so `/en/blog/post` is mapped to `/blog/post`
|
|
49
|
+
* when the default locale is served unprefixed. A path with no locale
|
|
50
|
+
* segment, as on a single-locale site, belongs to `locale`. Anything that is
|
|
51
|
+
* not a root-relative path is returned unchanged.
|
|
52
|
+
*/
|
|
53
|
+
fromApiPath(path: string, locale: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* An href from CMS content, made to lead to `locale`.
|
|
56
|
+
*
|
|
57
|
+
* Navigation blocks store locale-agnostic hrefs (`/blog`, `/contact`), which
|
|
58
|
+
* is right for an editor, so the locale is added at render: without it every
|
|
59
|
+
* internal link goes through a redirect. An href that already names a locale
|
|
60
|
+
* keeps it, normalized like {@link fromApiPath}. Absolute URLs, `mailto:` and
|
|
61
|
+
* `tel:` links, anchors, query-only and relative hrefs are left untouched.
|
|
62
|
+
*/
|
|
63
|
+
localizeHref(href: string, locale: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* The locale a link leads to on this site, or null when it leads elsewhere:
|
|
66
|
+
* another origin, a `mailto:`, an anchor, a relative path. An absolute URL
|
|
67
|
+
* on the site's own origin counts as this site, which catches an editor's
|
|
68
|
+
* pasted `https://<site>/fr/...`.
|
|
69
|
+
*/
|
|
70
|
+
hrefLocale(href: string): string | null;
|
|
71
|
+
/**
|
|
72
|
+
* The best match between an `Accept-Language` header and the configured
|
|
73
|
+
* locales, falling back to the default locale rather than to a 404.
|
|
74
|
+
*/
|
|
75
|
+
negotiateLocale(acceptLanguage: string | null | undefined): string;
|
|
76
|
+
/** `path` on the site's origin. */
|
|
77
|
+
absoluteUrl(path: string): string;
|
|
78
|
+
}
|
|
79
|
+
/** Binds the locale path helpers to a configuration from {@link resolveConfig}. */
|
|
80
|
+
declare function createLocalePaths(config: LynkowNextConfig): LocalePaths;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The caching strategy of a Lynkow site on Next, in one file.
|
|
84
|
+
*
|
|
85
|
+
* Next 15 changed `fetch()` to be uncached by default, so nothing here is
|
|
86
|
+
* inherited: a read that does not pass one of these option bags hits the Lynkow
|
|
87
|
+
* API on every single request. Every data read in this app therefore goes
|
|
88
|
+
* through `cached()`. The durations are defaults tuned for a content site; a
|
|
89
|
+
* site that wants others passes its own number to `cached()`.
|
|
90
|
+
*
|
|
91
|
+
* Two mechanisms, deliberately combined:
|
|
92
|
+
*
|
|
93
|
+
* - `revalidate` is the ceiling. Even with no webhook wired, content is never
|
|
94
|
+
* more than this many seconds stale.
|
|
95
|
+
* - `tags` are the floor. The revalidation webhook calls `revalidateTag()` so
|
|
96
|
+
* an edit in the admin appears in seconds rather than minutes.
|
|
97
|
+
*
|
|
98
|
+
* A tag is only useful if the webhook actually sends the matching event, so the
|
|
99
|
+
* names here and the ones the revalidation handler maps MUST stay in step.
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
/** Seconds. Tuned for a content site: rarely-changing structure lives longer. */
|
|
103
|
+
declare const REVALIDATE: {
|
|
104
|
+
/** Site config and global blocks: header, footer, branding. */
|
|
105
|
+
readonly site: 3600;
|
|
106
|
+
/** Structural pages driven by site blocks. */
|
|
107
|
+
readonly page: 300;
|
|
108
|
+
/** Blog articles, categories, tags. */
|
|
109
|
+
readonly blog: 300;
|
|
110
|
+
/** Approved reviews. */
|
|
111
|
+
readonly reviews: 900;
|
|
112
|
+
/** Form schemas. */
|
|
113
|
+
readonly forms: 3600;
|
|
114
|
+
/** The cookie consent banner configuration. */
|
|
115
|
+
readonly consent: 3600;
|
|
116
|
+
/** sitemap.xml, robots.txt, llms.txt. */
|
|
117
|
+
readonly seo: 3600;
|
|
118
|
+
/** The path table used for static generation and redirect matching. */
|
|
119
|
+
readonly paths: 600;
|
|
120
|
+
/**
|
|
121
|
+
* One search result page.
|
|
122
|
+
*
|
|
123
|
+
* Short on purpose. Next keys its fetch cache on the full URL, so every
|
|
124
|
+
* distinct query is its own entry and a long ceiling would pin a large,
|
|
125
|
+
* unbounded set of them. The tag is what actually keeps results honest: a
|
|
126
|
+
* publish evicts every cached query at once.
|
|
127
|
+
*/
|
|
128
|
+
readonly search: 60;
|
|
129
|
+
};
|
|
130
|
+
declare const TAGS: {
|
|
131
|
+
readonly site: "lynkow:site";
|
|
132
|
+
readonly pages: "lynkow:pages";
|
|
133
|
+
readonly page: (slug: string) => string;
|
|
134
|
+
readonly contents: "lynkow:contents";
|
|
135
|
+
readonly content: (slug: string) => string;
|
|
136
|
+
readonly categories: "lynkow:categories";
|
|
137
|
+
readonly tags: "lynkow:tags";
|
|
138
|
+
readonly reviews: "lynkow:reviews";
|
|
139
|
+
readonly forms: "lynkow:forms";
|
|
140
|
+
readonly form: (slug: string) => string;
|
|
141
|
+
readonly consent: "lynkow:consent";
|
|
142
|
+
readonly seo: "lynkow:seo";
|
|
143
|
+
readonly paths: "lynkow:paths";
|
|
144
|
+
readonly search: "lynkow:search";
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Build the per-request options that put one Lynkow read into the Next cache.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* const page = await lynkow(locale).pages.getBySlug('home', cached([TAGS.page('home')], REVALIDATE.page))
|
|
151
|
+
*/
|
|
152
|
+
declare function cached(tags: string[], revalidate: number): BaseRequestOptions;
|
|
153
|
+
/**
|
|
154
|
+
* For a read that must never be served from cache: a preview render, or a
|
|
155
|
+
* form schema fetched at submit time.
|
|
156
|
+
*/
|
|
157
|
+
declare function uncached(): BaseRequestOptions;
|
|
158
|
+
|
|
159
|
+
/** Options of the client {@link LynkowClients} returns. */
|
|
160
|
+
interface ClientOptions {
|
|
161
|
+
/**
|
|
162
|
+
* `false` returns a client that sends each read once instead of retrying a
|
|
163
|
+
* 429 or a 503. Defaults to `true`.
|
|
164
|
+
*/
|
|
165
|
+
retry?: boolean;
|
|
166
|
+
}
|
|
167
|
+
/** Returns the Lynkow client of a locale, the default one when none is given. */
|
|
168
|
+
type LynkowClients = (locale?: string, options?: ClientOptions) => Client;
|
|
169
|
+
/**
|
|
170
|
+
* One Lynkow client per locale, reused across requests.
|
|
171
|
+
*
|
|
172
|
+
* The SDK's own cache stays OFF. On the server it is a process-global
|
|
173
|
+
* unbounded `Map`, which would duplicate and outlive Next's data cache without
|
|
174
|
+
* any of its invalidation. Caching is Next's job, expressed per read with
|
|
175
|
+
* `cached()`.
|
|
176
|
+
*
|
|
177
|
+
* `{ retry: false }` gives a second client for the locale, because the SDK's
|
|
178
|
+
* retry is a client setting rather than a per-read one. Only a read that
|
|
179
|
+
* already knows what to do when it fails should ask for it.
|
|
180
|
+
*
|
|
181
|
+
* FOR READS THAT ARE THE SAME FOR EVERY VISITOR. The clients are shared by
|
|
182
|
+
* every request the process serves, and the SDK keeps a buyer's state on the
|
|
183
|
+
* client instance: the guest cart token and the signed-in customer session.
|
|
184
|
+
* Calling `cart` or `customers` on one of these from server code would hand
|
|
185
|
+
* one visitor's cart or session to the next. Buyer flows run in the browser,
|
|
186
|
+
* or on a client created for that one request.
|
|
187
|
+
*
|
|
188
|
+
* A locale outside the configuration throws a `RangeError` instead of creating
|
|
189
|
+
* a client. The clients live as long as the process, so a locale taken from a
|
|
190
|
+
* URL and passed through unchecked would grow that set without bound. The
|
|
191
|
+
* locale is matched case-insensitively and the client gets its configured
|
|
192
|
+
* spelling.
|
|
193
|
+
*/
|
|
194
|
+
declare function createClients(config: LynkowNextConfig): LynkowClients;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Sends one custom event through the tracker already on the page.
|
|
198
|
+
*
|
|
199
|
+
* `type` is a CLOSED enum of mechanical interactions, the SDK's
|
|
200
|
+
* `AnalyticsEventType`: there is no free-form event name. A value outside it is
|
|
201
|
+
* a compile error rather than a silently dropped row, since the collector
|
|
202
|
+
* answers `204` to a payload it rejects.
|
|
203
|
+
*
|
|
204
|
+
* Returns `false` when no tracker has loaded, which is never an error: on an
|
|
205
|
+
* `opt-in` site none loads before the visitor accepts analytics. `true` means
|
|
206
|
+
* the event was handed to the tracker, which adds the session id, the visitor
|
|
207
|
+
* id and the timestamp, and drops the event itself when the visitor refused on
|
|
208
|
+
* an `opt-in` or `opt-out` site.
|
|
209
|
+
*
|
|
210
|
+
* The tracker already records pageviews, scroll, clicks, form events and Core
|
|
211
|
+
* Web Vitals. Reach for this only for an interaction that escapes it, such as a
|
|
212
|
+
* form submitted through `fetch` without a real `submit` event.
|
|
213
|
+
*/
|
|
214
|
+
declare function trackEvent(type: AnalyticsEventType, data?: Record<string, unknown>): boolean;
|
|
215
|
+
|
|
216
|
+
export { ABSOLUTE_HREF, type ClientOptions, type LocalePaths, type LynkowClients, LynkowNextConfig, REVALIDATE, TAGS, cached, createClients, createLocalePaths, trackEvent, uncached };
|