@levo-so/site-runtime 0.1.113 → 0.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.
@@ -86,8 +86,12 @@ export declare const useLevoRecord: <T = Record<string, unknown>>(config: ILevoR
86
86
  * from the route `loader` so SSR renders fully-populated HTML. Builds its own client from
87
87
  * the site config (loaders run outside React, so `useLevoSite` isn't available). Record
88
88
  * bindings pull their slug from the route `params` (`$slug`). Resilient: a single failed
89
- * source resolves to a cached error so it doesn't blank the whole page — the hook surfaces
90
- * that binding's error while the rest render.
89
+ * source resolves to null so it doesn't blank the whole page — the hook surfaces that
90
+ * binding's error while the rest render.
91
+ *
92
+ * Returns the fetched values keyed by source key (records as the record object, lists as
93
+ * `{data, meta}`). The route loader returns this map, making it the route's `loaderData` —
94
+ * which `levoHead`'s dynamic config reads for per-record titles/descriptions/share images.
91
95
  */
92
96
  export declare const prefetchLevoSources: ({ queryClient, config, sources, params, }: {
93
97
  queryClient: IQueryClientLike;
@@ -97,5 +101,5 @@ export declare const prefetchLevoSources: ({ queryClient, config, sources, param
97
101
  sources: ILevoDataSources | undefined;
98
102
  /** Route params; record bindings read `params.slug`. */
99
103
  params?: Record<string, string>;
100
- }) => Promise<void>;
104
+ }) => Promise<Record<string, unknown>>;
101
105
  //# sourceMappingURL=index.d.ts.map
@@ -61,19 +61,21 @@ var i = (e) => [
61
61
  };
62
62
  }, u = async ({ queryClient: t, config: n, sources: r, params: c }) => {
63
63
  let l = e(n);
64
- !l || !r || await Promise.all(Object.values(r).map((e) => {
65
- if (e.mode === "record") {
66
- let n = c?.slug;
67
- return n ? t.ensureQueryData({
68
- queryKey: a(e, n),
69
- queryFn: () => s(l, e, n)
70
- }).catch(() => void 0) : Promise.resolve();
64
+ if (!l || !r) return {};
65
+ let u = await Promise.all(Object.entries(r).map(async ([e, n]) => {
66
+ if (n.mode === "record") {
67
+ let r = c?.slug;
68
+ return r ? [e, await t.ensureQueryData({
69
+ queryKey: a(n, r),
70
+ queryFn: () => s(l, n, r)
71
+ }).catch(() => null)] : [e, null];
71
72
  }
72
- return t.ensureQueryData({
73
- queryKey: i(e),
74
- queryFn: () => o(l, e)
75
- }).catch(() => void 0);
73
+ return [e, await t.ensureQueryData({
74
+ queryKey: i(n),
75
+ queryFn: () => o(l, n)
76
+ }).catch(() => null)];
76
77
  }));
78
+ return Object.fromEntries(u);
77
79
  };
78
80
  //#endregion
79
81
  export { u as prefetchLevoSources, l as useLevoRecord, c as useLevoSource };
package/dist/index.d.ts CHANGED
@@ -23,4 +23,6 @@ export type { ILevoSiteConfig } from './LevoSiteProvider';
23
23
  export { LevoSiteProvider, useLevoInsights, useLevoSite } from './LevoSiteProvider';
24
24
  export type { IComposedHead, IPageMeta, ISiteMeta } from './pageHead';
25
25
  export { PageHead, pageHead, parseRawHead } from './pageHead';
26
+ export type { ILevoDynamicHead, IPlatformMetaFile, IPlatformPageMeta, ISiteJson, } from './scaffold';
27
+ export { createLevoHead, createLevoLoader, siteConfigFromJson } from './scaffold';
26
28
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -3,4 +3,5 @@ import { LevoSiteProvider as t, useLevoInsights as n, useLevoSite as r } from ".
3
3
  import { prefetchLevoSources as i, useLevoRecord as a, useLevoSource as o } from "./data/index.js";
4
4
  import { Inspector as s } from "./inspector/index.js";
5
5
  import { PageHead as c, pageHead as l, parseRawHead as u } from "./pageHead.js";
6
- export { s as Inspector, t as LevoSiteProvider, c as PageHead, e as createSiteClient, l as pageHead, u as parseRawHead, i as prefetchLevoSources, n as useLevoInsights, a as useLevoRecord, r as useLevoSite, o as useLevoSource };
6
+ import { createLevoHead as d, createLevoLoader as f, siteConfigFromJson as p } from "./scaffold.js";
7
+ export { s as Inspector, t as LevoSiteProvider, c as PageHead, d as createLevoHead, f as createLevoLoader, e as createSiteClient, l as pageHead, u as parseRawHead, i as prefetchLevoSources, p as siteConfigFromJson, n as useLevoInsights, a as useLevoRecord, r as useLevoSite, o as useLevoSource };
@@ -1,5 +1,5 @@
1
- import { IStagedEntry } from './protocol';
2
1
  import { IOriginalsStash } from './originalsStash';
2
+ import { IStagedEntry } from './protocol';
3
3
  export type IStagedOverrides = ReturnType<typeof createStagedOverrides>;
4
4
  export declare const createStagedOverrides: (deps: {
5
5
  originals: IOriginalsStash;
@@ -10,6 +10,8 @@ export interface IPageMeta {
10
10
  title?: string;
11
11
  description?: string;
12
12
  image?: string;
13
+ /** Absolute share URL. Falls back to `canonical` when unset. */
14
+ url?: string;
13
15
  /** Defaults to "website" when any og field is present. */
14
16
  type?: string;
15
17
  };
package/dist/pageHead.js CHANGED
@@ -44,8 +44,8 @@ var r = (e) => JSON.stringify(e).replace(/<\//g, "<\\/"), i = (e) => {
44
44
  let f = {
45
45
  ...t.og,
46
46
  ...e.og
47
- }, p = f.title ?? i ?? o, m = f.description ?? l;
48
- (p || m || f.image) && (n.meta.push({
47
+ }, p = f.title ?? i ?? o, m = f.description ?? l, h = !!(p || m || f.image), g = f.url ?? d;
48
+ h && (n.meta.push({
49
49
  property: "og:type",
50
50
  content: f.type ?? "website"
51
51
  }), p && n.meta.push({
@@ -57,26 +57,29 @@ var r = (e) => JSON.stringify(e).replace(/<\//g, "<\\/"), i = (e) => {
57
57
  }), f.image && n.meta.push({
58
58
  property: "og:image",
59
59
  content: f.image
60
+ }), g && n.meta.push({
61
+ property: "og:url",
62
+ content: g
60
63
  }));
61
- let h = {
64
+ let _ = {
62
65
  ...t.twitter,
63
66
  ...e.twitter
64
- }, g = h.title ?? p, _ = h.description ?? m, v = h.image ?? f.image;
65
- (g || _ || v) && (n.meta.push({
67
+ }, v = _.title ?? p, y = _.description ?? m, b = _.image ?? f.image;
68
+ (v || y || b) && (n.meta.push({
66
69
  name: "twitter:card",
67
- content: h.card ?? "summary_large_image"
68
- }), g && n.meta.push({
70
+ content: _.card ?? "summary_large_image"
71
+ }), v && n.meta.push({
69
72
  name: "twitter:title",
70
- content: g
71
- }), _ && n.meta.push({
73
+ content: v
74
+ }), y && n.meta.push({
72
75
  name: "twitter:description",
73
- content: _
74
- }), v && n.meta.push({
76
+ content: y
77
+ }), b && n.meta.push({
75
78
  name: "twitter:image",
76
- content: v
79
+ content: b
77
80
  }));
78
- let y = e.ldJson ? Array.isArray(e.ldJson) ? e.ldJson : [e.ldJson] : [];
79
- for (let e of y) n.scripts.push({
81
+ let x = e.ldJson ? Array.isArray(e.ldJson) ? e.ldJson : [e.ldJson] : [];
82
+ for (let e of x) n.scripts.push({
80
83
  type: "application/ld+json",
81
84
  children: r(e)
82
85
  });
@@ -0,0 +1,110 @@
1
+ import { ILevoSiteConfig } from './client';
2
+ import { ILevoDataSources, IQueryClientLike } from './data';
3
+ import { IComposedHead } from './pageHead';
4
+ /** The platform's per-page SEO object — the `meta` key of `meta/<route>.json`. */
5
+ export interface IPlatformPageMeta {
6
+ meta_title?: string;
7
+ meta_description?: string;
8
+ keywords?: string;
9
+ og_title?: string;
10
+ og_description?: string;
11
+ /** Plain URL string, or the platform media object (older rows) — `.location` is read. */
12
+ og_image?: string | {
13
+ location?: string;
14
+ } | null;
15
+ og_url?: string;
16
+ canonical?: string;
17
+ twitter_card?: string;
18
+ /** JSON-LD structured data — rendered as an `application/ld+json` script tag. */
19
+ ld_json?: Record<string, unknown> | Array<Record<string, unknown>>;
20
+ /** Page-scoped raw head snippet — script/meta/link tags only. */
21
+ head_code?: string;
22
+ disable_indexing?: boolean;
23
+ robots?: string;
24
+ }
25
+ /** The full `meta/<route>.json` shape: page identity (platform-owned) + the SEO object. */
26
+ export interface IPlatformMetaFile {
27
+ title?: string;
28
+ description?: string;
29
+ meta?: IPlatformPageMeta;
30
+ }
31
+ /** The slice of the project's root `site.json` the runtime reads. */
32
+ export interface ISiteJson {
33
+ id?: string;
34
+ workspace_id?: string;
35
+ name?: string;
36
+ description?: string;
37
+ /** The site's live URL host (with or without protocol) — feeds auto-canonicals. */
38
+ domain?: string;
39
+ settings?: {
40
+ head_code?: string;
41
+ };
42
+ levo?: {
43
+ api_url?: string;
44
+ insights_url?: string | null;
45
+ app_mode?: string;
46
+ };
47
+ }
48
+ /**
49
+ * Per-record head config for a dynamic (`$slug`) route — `levoHead`'s optional second
50
+ * argument. A static literal (AI/editor-written, next to the route's meta import); each
51
+ * field names a FIELD on the loaded record (the `dataSources` entry named by `source`).
52
+ * At request time the record's values override the static meta file per-field, so every
53
+ * slug ships its own title, description, and share image.
54
+ */
55
+ export interface ILevoDynamicHead {
56
+ /** Key in the page's `dataSources` whose loaded record feeds the head. */
57
+ source: string;
58
+ /** Record field holding the page title (the site name is appended automatically). */
59
+ title?: string;
60
+ /** Record field holding the description. */
61
+ description?: string;
62
+ /** Record field holding the share image (platform image object or URL). */
63
+ image?: string;
64
+ }
65
+ /**
66
+ * The slice of the router's head-fn context we read. Structural on purpose (like
67
+ * IQueryClientLike) so the package never depends on the router's types: `loaderData` is
68
+ * the loader's return value — `prefetchLevoSources`' source-keyed map — and
69
+ * `match.pathname` is the request path. Both read defensively; absent = skipped.
70
+ */
71
+ interface IHeadCtx {
72
+ loaderData?: unknown;
73
+ match?: {
74
+ pathname?: string;
75
+ };
76
+ }
77
+ /**
78
+ * The site's Levo client config from its `site.json`. The platform stamps that file at
79
+ * provision and re-syncs it on settings saves; the `?.` on `levo` is load-bearing — a
80
+ * site provisioned before the block shipped degrades to an inert client, never a crash.
81
+ */
82
+ export declare const siteConfigFromJson: (site: ISiteJson) => ILevoSiteConfig;
83
+ /**
84
+ * Build the site's `levoHead` from its `site.json`. Each route passes its OWN imported
85
+ * meta file (`head: levoHead(meta)`) — no path matching anywhere; the import IS the
86
+ * pairing. The argument default lets a meta-less route render pure site defaults.
87
+ *
88
+ * Dynamic (`$slug`) routes pass a second argument naming the record fields that feed the
89
+ * head (`head: levoHead(meta, { source: "post", title: "title", … })`), so every slug
90
+ * gets its own title/description/share image instead of the route's shared static meta.
91
+ * Pages that set no `canonical` get a self-referencing one derived from the site domain
92
+ * + request path (og:url follows it in pageHead) — per-slug correct on dynamic routes.
93
+ */
94
+ export declare const createLevoHead: (site: ISiteJson) => (meta?: IPlatformMetaFile, dynamic?: ILevoDynamicHead) => (ctx?: IHeadCtx) => IComposedHead;
95
+ /**
96
+ * Build the one-line route loader (`loader: levoLoader(dataSources)`): prefetch the
97
+ * page's declared data sources on the server so SSR ships fully-rendered HTML, and
98
+ * return them keyed by source key — that map is the route's `loaderData`, which
99
+ * `levoHead`'s dynamic config reads for per-record heads. An empty `dataSources` ({})
100
+ * resolves to an empty map. Context is typed structurally (just `queryClient`) so the
101
+ * scaffold's router types never enter the package.
102
+ */
103
+ export declare const createLevoLoader: (config: ILevoSiteConfig) => (sources: ILevoDataSources) => ({ context, params, }: {
104
+ context: {
105
+ queryClient: IQueryClientLike;
106
+ };
107
+ params?: Record<string, string>;
108
+ }) => Promise<Record<string, unknown>>;
109
+ export {};
110
+ //# sourceMappingURL=scaffold.d.ts.map
@@ -0,0 +1,64 @@
1
+ import { prefetchLevoSources as e } from "./data/index.js";
2
+ import { pageHead as t } from "./pageHead.js";
3
+ //#region src/scaffold.ts
4
+ var n = (e) => typeof e == "string" && e.trim() ? e : void 0, r = (e) => ({
5
+ apiUrl: e.levo?.api_url || "",
6
+ workspace: e.workspace_id || "",
7
+ siteId: e.id || "",
8
+ insightsUrl: e.levo?.insights_url || null,
9
+ appMode: e.levo?.app_mode || "production"
10
+ }), i = (e) => typeof e == "string" ? e : e?.location ?? void 0, a = (e, t) => {
11
+ let n = e.meta ?? {}, r = e.title && t ? `${e.title} | ${t}` : e.title || t || void 0;
12
+ return {
13
+ title: n.meta_title || r,
14
+ description: n.meta_description || void 0,
15
+ canonical: n.canonical || void 0,
16
+ robots: n.disable_indexing ? "noindex,nofollow" : n.robots || void 0,
17
+ og: {
18
+ title: n.og_title || void 0,
19
+ description: n.og_description || void 0,
20
+ image: i(n.og_image),
21
+ url: n.og_url || void 0
22
+ },
23
+ twitter: { card: n.twitter_card || void 0 },
24
+ ldJson: n.ld_json || void 0,
25
+ customHead: n.head_code || void 0
26
+ };
27
+ }, o = (e) => ({
28
+ description: e.description || void 0,
29
+ customHead: e.settings?.head_code || void 0
30
+ }), s = (e) => (r = {}, s) => (c) => {
31
+ let l = a(r, e.name);
32
+ if (s) {
33
+ let t = c?.loaderData?.[s.source];
34
+ if (t) {
35
+ let r = s.title ? n(t[s.title]) : void 0;
36
+ r && (l.title = e.name ? `${r} | ${e.name}` : r, l.og = {
37
+ ...l.og,
38
+ title: r
39
+ });
40
+ let a = s.description ? n(t[s.description]) : void 0;
41
+ a && (l.description = a, l.og = {
42
+ ...l.og,
43
+ description: a
44
+ });
45
+ let o = s.image ? i(t[s.image]) : void 0;
46
+ o && (l.og = {
47
+ ...l.og,
48
+ image: o
49
+ });
50
+ }
51
+ }
52
+ if (!l.canonical) {
53
+ let t = c?.match?.pathname, n = (e.domain || "").trim();
54
+ n && t?.startsWith("/") && (l.canonical = (/^https?:\/\//.test(n) ? n : `https://${n}`).replace(/\/+$/, "") + t);
55
+ }
56
+ return t(l, o(e));
57
+ }, c = (t) => (n) => ({ context: r, params: i }) => e({
58
+ queryClient: r.queryClient,
59
+ config: t,
60
+ sources: n,
61
+ params: i
62
+ });
63
+ //#endregion
64
+ export { s as createLevoHead, c as createLevoLoader, r as siteConfigFromJson };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levo-so/site-runtime",
3
- "version": "0.1.113",
3
+ "version": "0.2.0",
4
4
  "description": "Runtime for Levo Studio v3 sites — the editor preview inspector, page head metadata, and the Levo data client.",
5
5
  "type": "module",
6
6
  "publishConfig": {