@blinkk/root-cms 2.2.1-alpha.7 → 2.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js CHANGED
@@ -83,7 +83,7 @@ function convertOneOfTypes(collection) {
83
83
  // package.json
84
84
  var package_default = {
85
85
  name: "@blinkk/root-cms",
86
- version: "2.2.1-alpha.7",
86
+ version: "2.2.3",
87
87
  author: "s@blinkk.com",
88
88
  license: "MIT",
89
89
  engines: {
@@ -232,7 +232,7 @@ var package_default = {
232
232
  yjs: "13.6.27"
233
233
  },
234
234
  peerDependencies: {
235
- "@blinkk/root": "2.2.1-alpha.7",
235
+ "@blinkk/root": "2.2.3",
236
236
  "firebase-admin": ">=11",
237
237
  "firebase-functions": ">=4",
238
238
  preact: ">=10",
@@ -861,4 +861,4 @@ declare class BatchResponse {
861
861
  private getTranslationsMap;
862
862
  }
863
863
 
864
- export { cmsPlugin as $, type Action as A, type BatchRequestOptions as B, BatchResponse as C, type Doc as D, type Locale as E, type SourceString as F, type GetDocOptions as G, type HttpMethod as H, type TranslatedString as I, type TranslationsDocMode as J, type TranslationsLocaleDocHashMap as K, type LoadTranslationsOptions as L, type TranslationsLocaleDocEntry as M, type MultiLocaleTranslationsMap as N, type SingleLocaleTranslationsMap as O, TranslationsManager as P, buildTranslationsDbPath as Q, type Release as R, type SetDocOptions as S, type TranslationsMap as T, type UserRole as U, buildTranslationsLocaleDocDbPath as V, type CMSUser as W, type CMSAIConfig as X, type CMSSidebarTool as Y, type CMSPluginOptions as Z, type CMSPlugin as _, type LocaleTranslations as a, type DocMode as b, type DataSource as c, type DataSourceData as d, type DataSourceMode as e, type SaveDraftOptions as f, type ListDocsOptions as g, type GetCountOptions as h, type Translation as i, type ListActionsOptions as j, RootCMSClient as k, isRichTextData as l, getCmsPlugin as m, marshalData as n, normalizeData as o, type ArrayObject as p, marshalArray as q, unmarshalArray as r, translationsForLocale as s, toArrayObject as t, unmarshalData as u, parseDocId as v, type BatchRequestQuery as w, type BatchRequestQueryOptions as x, type TranslationsDoc as y, BatchRequest as z };
864
+ export { cmsPlugin as $, type Action as A, type BatchRequestOptions as B, BatchResponse as C, type DocMode as D, type Locale as E, type SourceString as F, type GetDocOptions as G, type HttpMethod as H, type TranslatedString as I, type TranslationsDocMode as J, type TranslationsLocaleDocHashMap as K, type LoadTranslationsOptions as L, type TranslationsLocaleDocEntry as M, type MultiLocaleTranslationsMap as N, type SingleLocaleTranslationsMap as O, TranslationsManager as P, buildTranslationsDbPath as Q, RootCMSClient as R, type SetDocOptions as S, type TranslationsMap as T, type UserRole as U, buildTranslationsLocaleDocDbPath as V, type CMSUser as W, type CMSAIConfig as X, type CMSSidebarTool as Y, type CMSPluginOptions as Z, type CMSPlugin as _, type LocaleTranslations as a, type Doc as b, type DataSource as c, type DataSourceData as d, type DataSourceMode as e, type SaveDraftOptions as f, type ListDocsOptions as g, type GetCountOptions as h, type Translation as i, type Release as j, type ListActionsOptions as k, isRichTextData as l, getCmsPlugin as m, marshalData as n, normalizeData as o, type ArrayObject as p, marshalArray as q, unmarshalArray as r, translationsForLocale as s, toArrayObject as t, unmarshalData as u, parseDocId as v, type BatchRequestQuery as w, type BatchRequestQueryOptions as x, type TranslationsDoc as y, BatchRequest as z };
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import '@blinkk/root';
2
2
  import 'firebase-admin/app';
3
3
  import 'firebase-admin/firestore';
4
- export { A as Action, p as ArrayObject, z as BatchRequest, B as BatchRequestOptions, w as BatchRequestQuery, x as BatchRequestQueryOptions, C as BatchResponse, c as DataSource, d as DataSourceData, e as DataSourceMode, D as Doc, b as DocMode, h as GetCountOptions, G as GetDocOptions, H as HttpMethod, j as ListActionsOptions, g as ListDocsOptions, L as LoadTranslationsOptions, a as LocaleTranslations, R as Release, k as RootCMSClient, f as SaveDraftOptions, S as SetDocOptions, i as Translation, y as TranslationsDoc, T as TranslationsMap, U as UserRole, m as getCmsPlugin, l as isRichTextData, q as marshalArray, n as marshalData, o as normalizeData, v as parseDocId, t as toArrayObject, s as translationsForLocale, r as unmarshalArray, u as unmarshalData } from './client-BKO9Ikqy.js';
4
+ export { A as Action, p as ArrayObject, z as BatchRequest, B as BatchRequestOptions, w as BatchRequestQuery, x as BatchRequestQueryOptions, C as BatchResponse, c as DataSource, d as DataSourceData, e as DataSourceMode, b as Doc, D as DocMode, h as GetCountOptions, G as GetDocOptions, H as HttpMethod, k as ListActionsOptions, g as ListDocsOptions, L as LoadTranslationsOptions, a as LocaleTranslations, j as Release, R as RootCMSClient, f as SaveDraftOptions, S as SetDocOptions, i as Translation, y as TranslationsDoc, T as TranslationsMap, U as UserRole, m as getCmsPlugin, l as isRichTextData, q as marshalArray, n as marshalData, o as normalizeData, v as parseDocId, t as toArrayObject, s as translationsForLocale, r as unmarshalArray, u as unmarshalData } from './client-Rka_a6jc.js';
package/dist/core.d.ts CHANGED
@@ -1,10 +1,194 @@
1
- import { L as LoadTranslationsOptions, T as TranslationsMap, a as LocaleTranslations } from './client-BKO9Ikqy.js';
2
- export { A as Action, p as ArrayObject, z as BatchRequest, B as BatchRequestOptions, w as BatchRequestQuery, x as BatchRequestQueryOptions, C as BatchResponse, c as DataSource, d as DataSourceData, e as DataSourceMode, D as Doc, b as DocMode, h as GetCountOptions, G as GetDocOptions, H as HttpMethod, j as ListActionsOptions, g as ListDocsOptions, E as Locale, N as MultiLocaleTranslationsMap, R as Release, k as RootCMSClient, f as SaveDraftOptions, S as SetDocOptions, O as SingleLocaleTranslationsMap, F as SourceString, I as TranslatedString, i as Translation, y as TranslationsDoc, J as TranslationsDocMode, M as TranslationsLocaleDocEntry, K as TranslationsLocaleDocHashMap, P as TranslationsManager, U as UserRole, Q as buildTranslationsDbPath, V as buildTranslationsLocaleDocDbPath, m as getCmsPlugin, l as isRichTextData, q as marshalArray, n as marshalData, o as normalizeData, v as parseDocId, t as toArrayObject, s as translationsForLocale, r as unmarshalArray, u as unmarshalData } from './client-BKO9Ikqy.js';
3
- import { RootConfig } from '@blinkk/root';
1
+ import { R as RootCMSClient, D as DocMode, L as LoadTranslationsOptions, T as TranslationsMap, a as LocaleTranslations } from './client-Rka_a6jc.js';
2
+ export { A as Action, p as ArrayObject, z as BatchRequest, B as BatchRequestOptions, w as BatchRequestQuery, x as BatchRequestQueryOptions, C as BatchResponse, c as DataSource, d as DataSourceData, e as DataSourceMode, b as Doc, h as GetCountOptions, G as GetDocOptions, H as HttpMethod, k as ListActionsOptions, g as ListDocsOptions, E as Locale, N as MultiLocaleTranslationsMap, j as Release, f as SaveDraftOptions, S as SetDocOptions, O as SingleLocaleTranslationsMap, F as SourceString, I as TranslatedString, i as Translation, y as TranslationsDoc, J as TranslationsDocMode, M as TranslationsLocaleDocEntry, K as TranslationsLocaleDocHashMap, P as TranslationsManager, U as UserRole, Q as buildTranslationsDbPath, V as buildTranslationsLocaleDocDbPath, m as getCmsPlugin, l as isRichTextData, q as marshalArray, n as marshalData, o as normalizeData, v as parseDocId, t as toArrayObject, s as translationsForLocale, r as unmarshalArray, u as unmarshalData } from './client-Rka_a6jc.js';
3
+ import { Request, RootConfig, Response, RouteParams, GetStaticProps, GetStaticPaths } from '@blinkk/root';
4
4
  import { Query } from 'firebase-admin/firestore';
5
- export { s as schema } from './schema-FB6Ck9sN.js';
5
+ export { s as schema } from './schema-KYFRsBf5.js';
6
6
  import 'firebase-admin/app';
7
7
 
8
+ /**
9
+ * Utility for creating Root filesystem routes that are connected to a CMS doc.
10
+ *
11
+ * Usage:
12
+ *
13
+ * ```
14
+ * // routes/blog/[slug].tsx
15
+ * import {createRoute} from '@blinkk/root-cms';
16
+ *
17
+ * export default function Page(props) { ... }
18
+ *
19
+ * export const {handle} = createRoute({collection: 'BlogPosts'});
20
+ * ```
21
+ *
22
+ * For SSG enabled sites, add `{ssg: true}`:
23
+ *
24
+ * ```
25
+ * // routes/blog/[slug].tsx
26
+ * import {createRoute} from '@blinkk/root-cms';
27
+ *
28
+ * export default function Page(props) { ... }
29
+ *
30
+ * export const {getStaticProps, getStaticPaths} = createRoute({collection: 'BlogPosts'});
31
+ * ```
32
+ */
33
+
34
+ interface RootCMSDoc<Fields = any> {
35
+ /** The id of the doc, e.g. "Pages/foo-bar". */
36
+ id: string;
37
+ /** The collection id of the doc, e.g. "Pages". */
38
+ collection: string;
39
+ /** The slug of the doc, e.g. "foo-bar". */
40
+ slug: string;
41
+ /** System-level metadata. */
42
+ sys: {
43
+ createdAt: number;
44
+ createdBy: string;
45
+ modifiedAt: number;
46
+ modifiedBy: string;
47
+ firstPublishedAt?: number;
48
+ firstPublishedBy?: string;
49
+ publishedAt?: number;
50
+ publishedBy?: string;
51
+ locales?: string[];
52
+ };
53
+ /** User-entered field values from the CMS. */
54
+ fields?: Fields;
55
+ }
56
+ type RouteRequest = Request & {
57
+ rootConfig: RootConfig;
58
+ cmsClient: RootCMSClient;
59
+ };
60
+ type RouteResponse = Response;
61
+ interface RouteContext {
62
+ /**
63
+ * HTTP request object. Only available in SSR mode.
64
+ */
65
+ req?: RouteRequest;
66
+ /**
67
+ * The slug of the page being requested.
68
+ */
69
+ slug: string;
70
+ /**
71
+ * Doc publishing mode.
72
+ */
73
+ mode: DocMode;
74
+ /**
75
+ * Client for interacting with Root CMS data.
76
+ */
77
+ cmsClient: RootCMSClient;
78
+ /**
79
+ * URL param map from filesystem routing.
80
+ */
81
+ params: RouteParams;
82
+ }
83
+ interface Route {
84
+ /**
85
+ * SSR handler.
86
+ */
87
+ handle: (req: RouteRequest, res: RouteResponse) => Promise<void>;
88
+ /**
89
+ * SSG handler props handler, enabled with `{ssg: true}`.
90
+ */
91
+ getStaticProps?: GetStaticProps;
92
+ /**
93
+ * SSG path params provider, enabled with `{ssg: true}`.
94
+ */
95
+ getStaticPaths?: GetStaticPaths;
96
+ }
97
+ interface CreateRouteOptions {
98
+ /**
99
+ * Collection mapped to the route.
100
+ */
101
+ collection: string;
102
+ /**
103
+ * Route param name used for the slug. Used for dynamic routes, e.g.
104
+ * `[...slug].tsx`. Defaults to "slug".
105
+ */
106
+ slugParam?: string;
107
+ /**
108
+ * Format pattern for slugs that use multiple param values to form the slug,
109
+ * e.g. for experiments you might have something like:
110
+ *
111
+ * Route: routes/ex/[experimentId]/[...page].tsx
112
+ * Doc ID: ExperimentPages/1234--about--foo
113
+ * URL path: /ex/1234/about/foo/
114
+ *
115
+ * To grab the correct doc, use `{slugFormat: '[experimentId]/[page]'}`.
116
+ *
117
+ */
118
+ slugFormat?: string;
119
+ /**
120
+ * Slug to use for the route. Used for non-dynamic, single-document routes.
121
+ */
122
+ slug?: string;
123
+ /**
124
+ * Callback function that returns a map of Promises that contain fetched data.
125
+ * Once the promise is resolved, the values are injected into page's props
126
+ * for rendering.
127
+ */
128
+ fetchData?: (context: RouteContext) => Record<string, Promise<any>>;
129
+ /**
130
+ * Hook that's called when the doc is not found. If not provided, the default
131
+ * 404 handler will be called.
132
+ */
133
+ notFoundHook?: (req: Request, res: Response) => void | Promise<void>;
134
+ /**
135
+ * Hook for amending any props values before being passed to the page
136
+ * component.
137
+ */
138
+ preRenderHook?: (props: any, context: RouteContext) => any | Promise<any>;
139
+ /**
140
+ * Hook for setting any response headers.
141
+ */
142
+ setResponseHeaders?: (req: Request, res: Response) => void;
143
+ /**
144
+ * Translations configuration.
145
+ */
146
+ translations?: (context: RouteContext) => {
147
+ tags?: string[];
148
+ };
149
+ /**
150
+ * Sets Cache-Control header to `private`.
151
+ */
152
+ disableCacheControl?: boolean;
153
+ /**
154
+ * Enables SSG mode for sites that serve on SCS or other static servers.
155
+ */
156
+ ssg?: boolean;
157
+ /**
158
+ * Overrides the "mode" (draft vs published) for SSG builds. Primarily
159
+ * intended for testing prior to launches.
160
+ */
161
+ ssgMode?: DocMode;
162
+ /**
163
+ * Whether the route should only be available with ?preview=true.
164
+ */
165
+ previewOnly?: boolean;
166
+ /**
167
+ * Overrides the default locale for the route.
168
+ */
169
+ defaultLocale?: string;
170
+ }
171
+ /**
172
+ * Utility for creating Root filesystem routes that are connected to a CMS doc.
173
+ */
174
+ declare function createRoute(options: CreateRouteOptions): Route;
175
+ declare function resolvePromisesMap(promisesMap: Record<string, Promise<any>>): Promise<Record<string, any>>;
176
+ /**
177
+ * Returns the first query param value in a given request.
178
+ *
179
+ * For example, for a URL like `/?foo=bar&foo=baz`, calling
180
+ * `getFirstQueryParam(req, 'foo')` would return `"bar"`.
181
+ */
182
+ declare function getFirstQueryParam(req: Request, key: string): string | null;
183
+ /**
184
+ * Issues an HTTP redirect, preserving any query params from the original req.
185
+ */
186
+ declare function redirectWithQuery(req: Request, res: Response, redirectCode: number, redirectPath: string): void;
187
+ /**
188
+ * Returns the CMS document mode associated with the request.
189
+ */
190
+ declare function getMode(req: Request): Promise<DocMode>;
191
+
8
192
  /** @deprecated Use client.ts instead. */
9
193
 
10
194
  /**
@@ -50,4 +234,4 @@ declare function loadTranslations(rootConfig: RootConfig, options?: LoadTranslat
50
234
  */
51
235
  declare function loadTranslationsForLocale(rootConfig: RootConfig, locale: string, options?: LoadTranslationsOptions): Promise<LocaleTranslations>;
52
236
 
53
- export { LoadTranslationsOptions, LocaleTranslations, TranslationsMap, getDoc, listDocs, loadTranslations, loadTranslationsForLocale, numDocs, publishScheduledDocs };
237
+ export { type CreateRouteOptions, DocMode, LoadTranslationsOptions, LocaleTranslations, RootCMSClient, type RootCMSDoc, type Route, type RouteContext, type RouteRequest, type RouteResponse, TranslationsMap, createRoute, getDoc, getFirstQueryParam, getMode, listDocs, loadTranslations, loadTranslationsForLocale, numDocs, publishScheduledDocs, redirectWithQuery, resolvePromisesMap };
package/dist/core.js CHANGED
@@ -1512,6 +1512,288 @@ var BatchResponse = class {
1512
1512
  }
1513
1513
  };
1514
1514
 
1515
+ // core/route.ts
1516
+ import {
1517
+ replaceParams
1518
+ } from "@blinkk/root";
1519
+ function createRoute(options) {
1520
+ let cmsClient;
1521
+ function getSlug(params) {
1522
+ if (options.slug) {
1523
+ return options.slug;
1524
+ }
1525
+ if (options.slugFormat) {
1526
+ return replaceParams(options.slugFormat, params);
1527
+ }
1528
+ const slugParam = options.slugParam || "slug";
1529
+ const slug = params[slugParam] || "index";
1530
+ return slug;
1531
+ }
1532
+ async function fetchData(fetchOptions) {
1533
+ if (!options.fetchData) {
1534
+ return {};
1535
+ }
1536
+ const promisesMap = options.fetchData(fetchOptions);
1537
+ return resolvePromisesMap(promisesMap);
1538
+ }
1539
+ async function generateProps(routeContext, locale) {
1540
+ const { slug, mode } = routeContext;
1541
+ const translationsTags = [
1542
+ "common",
1543
+ `${options.collection}/${slug.replaceAll("/", "--")}`
1544
+ ];
1545
+ if (options.translations) {
1546
+ const tags = options.translations(routeContext)?.tags || [];
1547
+ translationsTags.push(...tags);
1548
+ }
1549
+ const [doc, translationsMap, data] = await Promise.all([
1550
+ cmsClient.getDoc(options.collection, slug, {
1551
+ mode
1552
+ }),
1553
+ cmsClient.loadTranslations({ tags: translationsTags }),
1554
+ fetchData(routeContext)
1555
+ ]);
1556
+ if (!doc) {
1557
+ return { notFound: true };
1558
+ }
1559
+ const docLocales = doc.sys.locales || [];
1560
+ if (!docLocales.includes(locale)) {
1561
+ return { notFound: true };
1562
+ }
1563
+ const translations = translationsForLocale(translationsMap, locale);
1564
+ let props = { ...data, locale, mode, slug, doc };
1565
+ if (options.preRenderHook) {
1566
+ props = await options.preRenderHook(props, routeContext);
1567
+ }
1568
+ return { props, locale, translations };
1569
+ }
1570
+ const route = {
1571
+ // SSR handler.
1572
+ handle: async (req, res) => {
1573
+ if (!cmsClient) {
1574
+ cmsClient = new RootCMSClient(req.rootConfig);
1575
+ }
1576
+ req.cmsClient = cmsClient;
1577
+ const ctx = req.handlerContext;
1578
+ const slug = getSlug(ctx.params);
1579
+ if (slug.includes(".") || slug.includes("--")) {
1580
+ res.setHeader("cache-control", "private");
1581
+ return ctx.render404();
1582
+ }
1583
+ const mode = await getMode(req);
1584
+ const siteLocales = req.rootConfig?.i18n?.locales || ["en"];
1585
+ if (options.previewOnly && mode !== "draft") {
1586
+ return ctx.render404();
1587
+ }
1588
+ const routeContext = {
1589
+ req,
1590
+ slug,
1591
+ mode,
1592
+ cmsClient,
1593
+ params: ctx.params
1594
+ };
1595
+ const translationsTags = [
1596
+ "common",
1597
+ `${options.collection}/${slug.replaceAll("/", "--")}`
1598
+ ];
1599
+ if (options.translations) {
1600
+ const tags = options.translations(routeContext)?.tags || [];
1601
+ translationsTags.push(...tags);
1602
+ }
1603
+ const [doc, translationsMap, data] = await Promise.all([
1604
+ cmsClient.getDoc(options.collection, slug, {
1605
+ mode
1606
+ }),
1607
+ // Only load translations for sites that have >1 locale configured.
1608
+ siteLocales.length > 1 ? cmsClient.loadTranslations({ tags: translationsTags }) : Promise.resolve({}),
1609
+ fetchData(routeContext)
1610
+ ]);
1611
+ if (!doc) {
1612
+ if (options.notFoundHook) {
1613
+ await options.notFoundHook(req, res);
1614
+ return;
1615
+ }
1616
+ res.setHeader("cache-control", "private");
1617
+ return ctx.render404();
1618
+ }
1619
+ const hl = getFirstQueryParam(req, "hl");
1620
+ let country = getFirstQueryParam(req, "gl") || req.get("x-country-code") || req.get("x-appengine-country") || "";
1621
+ if (country) {
1622
+ country = country.toUpperCase();
1623
+ }
1624
+ function getFallbackLocale(docLocales2) {
1625
+ const localesMap = {};
1626
+ docLocales2.forEach((docLocale) => {
1627
+ const lowerLocale = docLocale.toLowerCase();
1628
+ localesMap[lowerLocale] = docLocale;
1629
+ });
1630
+ if (hl === "fr") {
1631
+ if (country === "CA" && "fr-ca" in localesMap) {
1632
+ return localesMap["fr-ca"];
1633
+ }
1634
+ if (country === "FR" && "fr-fr" in localesMap) {
1635
+ return localesMap["fr-fr"];
1636
+ }
1637
+ }
1638
+ if (hl === "pt") {
1639
+ if (country === "BR" && "pt-br" in localesMap) {
1640
+ return localesMap["pt-br"];
1641
+ }
1642
+ if (country === "PT" && "pt-pt" in localesMap) {
1643
+ return localesMap["pt-pt"];
1644
+ }
1645
+ }
1646
+ const preferredLocale = ctx.getPreferredLocale(docLocales2);
1647
+ if (preferredLocale) {
1648
+ const normalizedLocale = localesMap[preferredLocale] || preferredLocale;
1649
+ if (preferredLocale === "en") {
1650
+ if (["AU", "CA", "IN", "MY"].includes(country)) {
1651
+ if (localesMap[`en-${country.toLowerCase()}`]) {
1652
+ return localesMap[`en-${country.toLowerCase()}`];
1653
+ }
1654
+ if (localesMap["en-gb"]) {
1655
+ return localesMap["en-gb"];
1656
+ }
1657
+ }
1658
+ }
1659
+ return normalizedLocale;
1660
+ }
1661
+ const defaultLocale = options.defaultLocale || req.rootConfig?.i18n?.defaultLocale || "en";
1662
+ if (docLocales2.includes(defaultLocale)) {
1663
+ return defaultLocale;
1664
+ }
1665
+ return docLocales2[0];
1666
+ }
1667
+ const sys = doc.sys;
1668
+ const docLocales = sys.locales || ["en"];
1669
+ let locale = ctx.route.locale;
1670
+ if (ctx.route.isDefaultLocale) {
1671
+ locale = getFallbackLocale(docLocales);
1672
+ } else {
1673
+ if (!docLocales.includes(locale)) {
1674
+ if (options.notFoundHook) {
1675
+ await options.notFoundHook(req, res);
1676
+ return;
1677
+ }
1678
+ res.setHeader("cache-control", "private");
1679
+ return ctx.render404();
1680
+ }
1681
+ }
1682
+ const translations = translationsForLocale(translationsMap, locale);
1683
+ let props = { ...data, req, locale, mode, slug, doc, country };
1684
+ if (options.preRenderHook) {
1685
+ props = await options.preRenderHook(props, routeContext);
1686
+ }
1687
+ if (props.$redirect) {
1688
+ const redirectCode = props.$redirectCode || 302;
1689
+ console.log(`redirecting to: ${props.$redirect} (${redirectCode})`);
1690
+ redirectWithQuery(req, res, redirectCode, props.$redirect);
1691
+ return;
1692
+ }
1693
+ if (options.disableCacheControl) {
1694
+ res.setHeader("cache-control", "private");
1695
+ } else if (mode === "published") {
1696
+ res.setHeader("cache-control", "public, max-age=15, s-maxage=30");
1697
+ if (ctx.route.isDefaultLocale) {
1698
+ res.setHeader(
1699
+ "vary",
1700
+ "accept-language,x-appengine-country,x-country-code"
1701
+ );
1702
+ }
1703
+ }
1704
+ if (options.setResponseHeaders) {
1705
+ options.setResponseHeaders(req, res);
1706
+ }
1707
+ return ctx.render(props, { locale, translations });
1708
+ }
1709
+ };
1710
+ if (options.ssg) {
1711
+ route.getStaticPaths = async (ctx) => {
1712
+ if (!options.slugParam) {
1713
+ return { paths: [] };
1714
+ }
1715
+ if (!cmsClient) {
1716
+ cmsClient = new RootCMSClient(ctx.rootConfig);
1717
+ }
1718
+ const mode = options.ssgMode || "published";
1719
+ const res = await cmsClient.listDocs(options.collection, {
1720
+ mode
1721
+ });
1722
+ const ssgPaths = [];
1723
+ const slugParam = options.slugParam || "slug";
1724
+ res.docs.forEach((doc) => {
1725
+ const params = {};
1726
+ params[slugParam] = doc.slug.replaceAll("--", "/");
1727
+ ssgPaths.push({ params });
1728
+ });
1729
+ return { paths: ssgPaths };
1730
+ };
1731
+ route.getStaticProps = async (ctx) => {
1732
+ if (!cmsClient) {
1733
+ cmsClient = new RootCMSClient(ctx.rootConfig);
1734
+ }
1735
+ const slug = getSlug(ctx.params);
1736
+ const mode = options.ssgMode || "published";
1737
+ const routeContext = {
1738
+ req: void 0,
1739
+ slug,
1740
+ mode,
1741
+ cmsClient,
1742
+ params: ctx.params
1743
+ };
1744
+ return generateProps(routeContext, ctx.params.$locale);
1745
+ };
1746
+ }
1747
+ return route;
1748
+ }
1749
+ async function resolvePromisesMap(promisesMap) {
1750
+ const keys = Object.keys(promisesMap);
1751
+ const promises = Object.values(promisesMap);
1752
+ const results = await Promise.all(promises);
1753
+ const resultMap = {};
1754
+ keys.forEach((key, index) => {
1755
+ resultMap[key] = results[index];
1756
+ });
1757
+ return resultMap;
1758
+ }
1759
+ function getFirstQueryParam(req, key) {
1760
+ const val = req.query[key];
1761
+ if (val === null || val === void 0) {
1762
+ return null;
1763
+ }
1764
+ if (Array.isArray(val)) {
1765
+ if (val.length === 0) {
1766
+ return null;
1767
+ }
1768
+ return String(val[0]);
1769
+ }
1770
+ return String(val);
1771
+ }
1772
+ function redirectWithQuery(req, res, redirectCode, redirectPath) {
1773
+ if (!redirectPath.startsWith("/")) {
1774
+ res.redirect(redirectCode, redirectPath);
1775
+ return;
1776
+ }
1777
+ const queryStr = getQueryStr(req);
1778
+ const redirectUrl = queryStr ? `${redirectPath}?${queryStr}` : redirectPath;
1779
+ res.redirect(redirectCode, redirectUrl);
1780
+ }
1781
+ function getQueryStr(req) {
1782
+ const qIndex = req.originalUrl.indexOf("?");
1783
+ if (qIndex === -1) {
1784
+ return "";
1785
+ }
1786
+ return req.originalUrl.slice(qIndex + 1);
1787
+ }
1788
+ async function getMode(req) {
1789
+ const isPreview = String(req.query.preview) === "true";
1790
+ let mode = isPreview ? "draft" : "published";
1791
+ if (isPreview && req.query.mode === "published") {
1792
+ mode = "published";
1793
+ }
1794
+ return mode;
1795
+ }
1796
+
1515
1797
  // core/runtime.ts
1516
1798
  import { FieldValue as FieldValue3 } from "firebase-admin/firestore";
1517
1799
  async function getDoc(rootConfig, collectionId, slug, options) {
@@ -1765,8 +2047,11 @@ export {
1765
2047
  TranslationsManager,
1766
2048
  buildTranslationsDbPath,
1767
2049
  buildTranslationsLocaleDocDbPath,
2050
+ createRoute,
1768
2051
  getCmsPlugin,
1769
2052
  getDoc,
2053
+ getFirstQueryParam,
2054
+ getMode,
1770
2055
  isRichTextData,
1771
2056
  listDocs,
1772
2057
  loadTranslations,
@@ -1777,6 +2062,8 @@ export {
1777
2062
  numDocs,
1778
2063
  parseDocId,
1779
2064
  publishScheduledDocs,
2065
+ redirectWithQuery,
2066
+ resolvePromisesMap,
1780
2067
  schema_exports as schema,
1781
2068
  toArrayObject,
1782
2069
  translationsForLocale,
package/dist/plugin.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import '@blinkk/root';
2
2
  import 'firebase-admin/app';
3
3
  import 'firebase-admin/firestore';
4
- export { X as CMSAIConfig, _ as CMSPlugin, Z as CMSPluginOptions, Y as CMSSidebarTool, W as CMSUser, $ as cmsPlugin } from './client-BKO9Ikqy.js';
4
+ export { X as CMSAIConfig, _ as CMSPlugin, Z as CMSPluginOptions, Y as CMSSidebarTool, W as CMSUser, $ as cmsPlugin } from './client-Rka_a6jc.js';
package/dist/plugin.js CHANGED
@@ -397,6 +397,51 @@ import { genkit } from "genkit";
397
397
  import { logger } from "genkit/logging";
398
398
  logger.setLogLevel("warn");
399
399
  var DEFAULT_MODEL = "vertexai/gemini-2.5-flash";
400
+ async function summarizeDiff(cmsClient, options) {
401
+ const cmsPluginOptions = cmsClient.cmsPlugin.getConfig();
402
+ const firebaseConfig = cmsPluginOptions.firebaseConfig;
403
+ const model = (typeof cmsPluginOptions.experiments?.ai === "object" ? cmsPluginOptions.experiments.ai.model : void 0) || DEFAULT_MODEL;
404
+ const ai = genkit({
405
+ plugins: [
406
+ vertexAI({
407
+ projectId: firebaseConfig.projectId,
408
+ location: firebaseConfig.location || "us-central1"
409
+ })
410
+ ]
411
+ });
412
+ const beforeJson = JSON.stringify(options.before ?? null, null, 2);
413
+ const afterJson = JSON.stringify(options.after ?? null, null, 2);
414
+ const systemPrompt = [
415
+ "You are an assistant that summarizes changes made to CMS documents stored as JSON.",
416
+ "Provide a concise description of the most important updates using short bullet points.",
417
+ 'If there are no meaningful differences, respond with "No significant changes."',
418
+ `Focus on just the content changes, ignore insignificant changes to richtext blocks and structure, such as updates to the richtext block's "timestamp" and "version" fields.`
419
+ ].join("\n");
420
+ const diffPrompt = [
421
+ "Previous version JSON:",
422
+ "```json",
423
+ beforeJson,
424
+ "```",
425
+ "",
426
+ "Updated version JSON:",
427
+ "```json",
428
+ afterJson,
429
+ "```",
430
+ "",
431
+ "Summarize the differences between the two payloads."
432
+ ].join("\n");
433
+ const res = await ai.generate({
434
+ model,
435
+ messages: [
436
+ {
437
+ role: "system",
438
+ content: [{ text: systemPrompt }]
439
+ }
440
+ ],
441
+ prompt: [{ text: diffPrompt }]
442
+ });
443
+ return res.text?.trim() || "";
444
+ }
400
445
  var Chat = class {
401
446
  constructor(chatClient, id, options) {
402
447
  this.chatClient = chatClient;
@@ -2402,6 +2447,36 @@ function api(server, options) {
2402
2447
  res.status(500).json({ success: false, error: "UNKNOWN" });
2403
2448
  }
2404
2449
  });
2450
+ server.use("/cms/api/ai.diff", async (req, res) => {
2451
+ if (req.method !== "POST" || !String(req.get("content-type")).startsWith("application/json")) {
2452
+ res.status(400).json({ success: false, error: "BAD_REQUEST" });
2453
+ return;
2454
+ }
2455
+ if (!req.user?.email) {
2456
+ res.status(401).json({ success: false, error: "UNAUTHORIZED" });
2457
+ return;
2458
+ }
2459
+ const reqBody = req.body || {};
2460
+ if (!Object.prototype.hasOwnProperty.call(reqBody, "after")) {
2461
+ res.status(400).json({
2462
+ success: false,
2463
+ error: "MISSING_REQUIRED_FIELD",
2464
+ field: "after"
2465
+ });
2466
+ return;
2467
+ }
2468
+ try {
2469
+ const cmsClient = new RootCMSClient(req.rootConfig);
2470
+ const summary = await summarizeDiff(cmsClient, {
2471
+ before: reqBody.before ?? null,
2472
+ after: reqBody.after ?? null
2473
+ });
2474
+ res.status(200).json({ success: true, summary });
2475
+ } catch (err) {
2476
+ console.error(err.stack || err);
2477
+ res.status(500).json({ success: false, error: "UNKNOWN" });
2478
+ }
2479
+ });
2405
2480
  server.use("/cms/api/ai.list_chats", async (req, res) => {
2406
2481
  if (req.method !== "POST" || !String(req.get("content-type")).startsWith("application/json")) {
2407
2482
  res.status(400).json({ success: false, error: "BAD_REQUEST" });
@@ -2428,7 +2503,7 @@ function api(server, options) {
2428
2503
  // package.json
2429
2504
  var package_default = {
2430
2505
  name: "@blinkk/root-cms",
2431
- version: "2.2.1-alpha.7",
2506
+ version: "2.2.3",
2432
2507
  author: "s@blinkk.com",
2433
2508
  license: "MIT",
2434
2509
  engines: {
@@ -2577,7 +2652,7 @@ var package_default = {
2577
2652
  yjs: "13.6.27"
2578
2653
  },
2579
2654
  peerDependencies: {
2580
- "@blinkk/root": "2.2.1-alpha.7",
2655
+ "@blinkk/root": "2.2.3",
2581
2656
  "firebase-admin": ">=11",
2582
2657
  "firebase-functions": ">=4",
2583
2658
  preact: ">=10",
package/dist/project.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { S as Schema, C as Collection } from './schema-FB6Ck9sN.js';
1
+ import { S as Schema, C as Collection } from './schema-KYFRsBf5.js';
2
2
 
3
3
  /**
4
4
  * Loads various files or configurations from the project.
@@ -240,9 +240,9 @@ type Collection = SchemaWithTypes & {
240
240
  */
241
241
  preview?: {
242
242
  /** The field that provides the document title. */
243
- title?: string;
243
+ title?: string | string[];
244
244
  /** The field that provides the document image. */
245
- image?: string;
245
+ image?: string | string[];
246
246
  /** A fallback image to display when the document image field is empty. */
247
247
  defaultImage?: {
248
248
  src: string;