@plitzi/sdk-server 0.33.2 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +283 -8
  3. package/dist/actions.d.ts +62 -0
  4. package/dist/actions.js +12 -0
  5. package/dist/adapters/cloudAdapters.d.ts +72 -0
  6. package/dist/adapters/cloudAdapters.js +303 -0
  7. package/dist/adapters/space/index.d.ts +93 -0
  8. package/dist/adapters/space/index.js +122 -0
  9. package/dist/adapters/space/resolvers.d.ts +69 -0
  10. package/dist/adapters/space/resolvers.js +79 -0
  11. package/dist/adapters/space/space.test.d.ts +1 -0
  12. package/dist/adapters/space/types.d.ts +64 -0
  13. package/dist/adapters/space/types.js +41 -0
  14. package/dist/auth.d.ts +50 -0
  15. package/dist/auth.js +19 -0
  16. package/dist/core/auth/api.d.ts +400 -11
  17. package/dist/core/auth/api.js +1044 -50
  18. package/dist/core/auth/createAuth.d.ts +135 -30
  19. package/dist/core/auth/createAuth.js +96 -24
  20. package/dist/core/auth/credentials.d.ts +9 -3
  21. package/dist/core/auth/credentials.js +10 -4
  22. package/dist/core/auth/csrf.d.ts +89 -0
  23. package/dist/core/auth/csrf.js +234 -0
  24. package/dist/core/auth/csrf.test.d.ts +1 -0
  25. package/dist/core/auth/identity.d.ts +6 -0
  26. package/dist/core/auth/identity.js +37 -8
  27. package/dist/core/auth/lifecycle.test.d.ts +1 -0
  28. package/dist/core/auth/oauth/index.d.ts +8 -0
  29. package/dist/core/auth/oauth/index.js +13 -15
  30. package/dist/core/auth/passwords.d.ts +15 -0
  31. package/dist/core/auth/passwords.js +32 -0
  32. package/dist/core/auth/redirects.d.ts +18 -0
  33. package/dist/core/auth/redirects.js +31 -0
  34. package/dist/core/auth/redirects.test.d.ts +1 -0
  35. package/dist/core/auth/routes.d.ts +4 -2
  36. package/dist/core/auth/routes.js +234 -9
  37. package/dist/core/auth/session.d.ts +11 -0
  38. package/dist/core/auth/session.js +1 -1
  39. package/dist/core/auth/spaceTokens.d.ts +38 -2
  40. package/dist/core/auth/spaceTokens.js +54 -2
  41. package/dist/core/auth/throttle.d.ts +5 -0
  42. package/dist/core/auth/throttle.js +81 -0
  43. package/dist/core/auth/throttle.test.d.ts +1 -0
  44. package/dist/core/auth/tokens.d.ts +83 -12
  45. package/dist/core/auth/tokens.js +51 -9
  46. package/dist/core/auth/totp.d.ts +48 -0
  47. package/dist/core/auth/totp.js +118 -0
  48. package/dist/core/auth/totp.test.d.ts +1 -0
  49. package/dist/core/createServer.d.ts +0 -6
  50. package/dist/core/createServer.js +35 -3
  51. package/dist/core/fontStore.d.ts +33 -0
  52. package/dist/core/fontStore.js +53 -0
  53. package/dist/core/fontStore.test.d.ts +1 -0
  54. package/dist/core/handlers/authMiddleware.js +1 -1
  55. package/dist/core/handlers/authRouteHandlers.d.ts +9 -1
  56. package/dist/core/handlers/authRouteHandlers.js +4 -3
  57. package/dist/core/handlers/csrfMiddleware.d.ts +20 -0
  58. package/dist/core/handlers/csrfMiddleware.js +32 -0
  59. package/dist/core/handlers/oauthRouteHandlers.d.ts +46 -0
  60. package/dist/core/handlers/oauthRouteHandlers.js +159 -0
  61. package/dist/core/handlers/socialRouteHandlers.d.ts +40 -0
  62. package/dist/core/handlers/socialRouteHandlers.js +129 -0
  63. package/dist/core/handlers/socialRouteHandlers.test.d.ts +1 -0
  64. package/dist/core/handlers/types.d.ts +22 -5
  65. package/dist/core/health.d.ts +11 -1
  66. package/dist/core/health.js +27 -3
  67. package/dist/core/http/dispatcher.d.ts +1 -1
  68. package/dist/core/http/dispatcher.js +20 -1
  69. package/dist/core/http/stages/authApi.js +1 -1
  70. package/dist/core/http/stages/fontAssets.d.ts +9 -0
  71. package/dist/core/http/stages/fontAssets.js +27 -0
  72. package/dist/core/http/stages/oauth.d.ts +19 -0
  73. package/dist/core/http/stages/oauth.js +118 -0
  74. package/dist/core/http/types.d.ts +12 -0
  75. package/dist/core/oauth/authorize.d.ts +22 -0
  76. package/dist/core/oauth/authorize.js +254 -0
  77. package/dist/core/oauth/authorize.test.d.ts +1 -0
  78. package/dist/core/oauth/challenge.d.ts +11 -0
  79. package/dist/core/oauth/challenge.js +33 -0
  80. package/dist/core/oauth/consentPage.d.ts +12 -0
  81. package/dist/core/oauth/consentPage.js +131 -0
  82. package/dist/core/oauth/metadata.d.ts +41 -0
  83. package/dist/core/oauth/metadata.js +71 -0
  84. package/dist/core/oauth/params.d.ts +5 -0
  85. package/dist/core/oauth/params.js +5 -0
  86. package/dist/core/oauth/pkce.d.ts +6 -0
  87. package/dist/core/oauth/pkce.js +15 -0
  88. package/dist/core/oauth/records.d.ts +72 -0
  89. package/dist/core/oauth/records.js +49 -0
  90. package/dist/core/oauth/register.d.ts +5 -0
  91. package/dist/core/oauth/register.js +55 -0
  92. package/dist/core/oauth/respond.d.ts +21 -0
  93. package/dist/core/oauth/respond.js +59 -0
  94. package/dist/core/oauth/token.d.ts +14 -0
  95. package/dist/core/oauth/token.js +130 -0
  96. package/dist/core/previewToken.d.ts +20 -0
  97. package/dist/core/previewToken.js +29 -1
  98. package/dist/core/server/pageServer.js +4 -1
  99. package/dist/core/services/action.d.ts +6 -4
  100. package/dist/core/services/action.js +103 -10
  101. package/dist/core/services/registry.js +2 -0
  102. package/dist/core/services/resolve.d.ts +8 -0
  103. package/dist/core/services/resolve.js +12 -1
  104. package/dist/core/staticFiles.js +1 -2
  105. package/dist/handlers.d.ts +7 -1
  106. package/dist/handlers.js +4 -1
  107. package/dist/helpers/buildResponseHelpers.d.ts +8 -0
  108. package/dist/helpers/buildResponseHelpers.js +9 -1
  109. package/dist/helpers/buildServerInfo.d.ts +8 -3
  110. package/dist/helpers/buildServerInfo.js +18 -14
  111. package/dist/helpers/cache/keys.d.ts +13 -0
  112. package/dist/helpers/cache/keys.js +12 -1
  113. package/dist/helpers/onAbort.d.ts +12 -0
  114. package/dist/helpers/onAbort.js +22 -0
  115. package/dist/helpers/onAbort.test.d.ts +1 -0
  116. package/dist/helpers/outboundGuard.d.ts +27 -0
  117. package/dist/helpers/outboundGuard.js +49 -0
  118. package/dist/helpers/outboundGuard.test.d.ts +1 -0
  119. package/dist/helpers/serverLog.d.ts +6 -3
  120. package/dist/helpers/serverLog.js +20 -3
  121. package/dist/index.d.ts +6 -0
  122. package/dist/index.js +9 -4
  123. package/dist/kernel.d.ts +5 -22
  124. package/dist/kernel.js +1 -14
  125. package/dist/modules/actions/{handler.js → connectorWrite.js} +5 -2
  126. package/dist/modules/actions/connectorWrite.test.d.ts +1 -0
  127. package/dist/modules/actions/index.d.ts +40 -0
  128. package/dist/modules/actions/index.js +46 -0
  129. package/dist/modules/actions/moduleFor.d.ts +3 -0
  130. package/dist/modules/actions/moduleFor.js +35 -0
  131. package/dist/modules/actions/moduleFor.test.d.ts +1 -0
  132. package/dist/modules/actions/runtime/check.d.ts +24 -0
  133. package/dist/modules/actions/runtime/check.js +111 -0
  134. package/dist/modules/actions/runtime/check.test.d.ts +1 -0
  135. package/dist/modules/actions/runtime/errors.d.ts +11 -0
  136. package/dist/modules/actions/runtime/errors.js +17 -0
  137. package/dist/modules/actions/runtime/guards.d.ts +126 -0
  138. package/dist/modules/actions/runtime/guards.js +246 -0
  139. package/dist/modules/actions/runtime/guards.test.d.ts +1 -0
  140. package/dist/modules/actions/runtime/kvStore.d.ts +27 -0
  141. package/dist/modules/actions/runtime/kvStore.js +41 -0
  142. package/dist/modules/actions/runtime/kvStore.test.d.ts +1 -0
  143. package/dist/modules/actions/runtime/limits.d.ts +11 -0
  144. package/dist/modules/actions/runtime/limits.js +35 -0
  145. package/dist/modules/actions/runtime/memoryKv.d.ts +2 -0
  146. package/dist/modules/actions/runtime/memoryKv.js +77 -0
  147. package/dist/modules/actions/runtime/namespaceKv.d.ts +9 -0
  148. package/dist/modules/actions/runtime/namespaceKv.js +19 -0
  149. package/dist/modules/actions/runtime/precheck.d.ts +27 -0
  150. package/dist/modules/actions/runtime/precheck.js +45 -0
  151. package/dist/modules/actions/runtime/renderResolver.d.ts +15 -0
  152. package/dist/modules/actions/runtime/renderResolver.js +130 -0
  153. package/dist/modules/actions/runtime/renderResolver.test.d.ts +1 -0
  154. package/dist/modules/actions/runtime/renderShare.d.ts +29 -0
  155. package/dist/modules/actions/runtime/renderShare.js +36 -0
  156. package/dist/modules/actions/runtime/renderShare.test.d.ts +1 -0
  157. package/dist/modules/actions/runtime/report.d.ts +14 -0
  158. package/dist/modules/actions/runtime/report.js +22 -0
  159. package/dist/modules/actions/runtime/runAction.d.ts +12 -0
  160. package/dist/modules/actions/runtime/runAction.js +405 -0
  161. package/dist/modules/actions/runtime/runAction.test.d.ts +1 -0
  162. package/dist/modules/actions/runtime/runLogger.d.ts +31 -0
  163. package/dist/modules/actions/runtime/runLogger.js +66 -0
  164. package/dist/modules/actions/runtime/runLogger.test.d.ts +1 -0
  165. package/dist/modules/actions/runtime/schedule.d.ts +35 -0
  166. package/dist/modules/actions/runtime/schedule.js +76 -0
  167. package/dist/modules/actions/runtime/schedule.test.d.ts +1 -0
  168. package/dist/modules/actions/runtime/scope.d.ts +40 -0
  169. package/dist/modules/actions/runtime/scope.js +103 -0
  170. package/dist/modules/actions/runtime/triggers.d.ts +12 -0
  171. package/dist/modules/actions/runtime/triggers.js +14 -0
  172. package/dist/modules/actions/taskCatalog.d.ts +26 -0
  173. package/dist/modules/actions/taskCatalog.js +24 -0
  174. package/dist/modules/actions/taskCatalog.test.d.ts +1 -0
  175. package/dist/modules/actions/tasks/auth.d.ts +2 -0
  176. package/dist/modules/actions/tasks/auth.js +40 -0
  177. package/dist/modules/actions/tasks/builtins.d.ts +8 -0
  178. package/dist/modules/actions/tasks/builtins.js +24 -0
  179. package/dist/modules/actions/tasks/connector.d.ts +2 -0
  180. package/dist/modules/actions/tasks/connector.js +141 -0
  181. package/dist/modules/actions/tasks/db.d.ts +2 -0
  182. package/dist/modules/actions/tasks/db.js +59 -0
  183. package/dist/modules/actions/tasks/db.test.d.ts +1 -0
  184. package/dist/modules/actions/tasks/flow.d.ts +3 -0
  185. package/dist/modules/actions/tasks/flow.js +109 -0
  186. package/dist/modules/actions/tasks/helpers.d.ts +16 -0
  187. package/dist/modules/actions/tasks/helpers.js +30 -0
  188. package/dist/modules/actions/tasks/http.d.ts +2 -0
  189. package/dist/modules/actions/tasks/http.js +111 -0
  190. package/dist/modules/actions/tasks/http.test.d.ts +1 -0
  191. package/dist/modules/actions/tasks/kv.d.ts +2 -0
  192. package/dist/modules/actions/tasks/kv.js +102 -0
  193. package/dist/modules/actions/tasks/kv.test.d.ts +1 -0
  194. package/dist/modules/actions/tasks/registry.d.ts +10 -0
  195. package/dist/modules/actions/tasks/registry.js +36 -0
  196. package/dist/modules/actions/tasks/transform.d.ts +2 -0
  197. package/dist/modules/actions/tasks/transform.js +34 -0
  198. package/dist/modules/actions/transport/callHandler.d.ts +24 -0
  199. package/dist/modules/actions/transport/callHandler.js +233 -0
  200. package/dist/modules/actions/transport/callHandler.test.d.ts +1 -0
  201. package/dist/modules/actions/transport/cancelHandler.d.ts +23 -0
  202. package/dist/modules/actions/transport/cancelHandler.js +23 -0
  203. package/dist/modules/actions/transport/catalogHandler.d.ts +18 -0
  204. package/dist/modules/actions/transport/catalogHandler.js +21 -0
  205. package/dist/modules/actions/transport/stream.d.ts +23 -0
  206. package/dist/modules/actions/transport/stream.js +100 -0
  207. package/dist/modules/actions/transport/verifySignature.d.ts +16 -0
  208. package/dist/modules/actions/transport/verifySignature.js +72 -0
  209. package/dist/modules/actions/transport/webhookHandler.d.ts +25 -0
  210. package/dist/modules/actions/transport/webhookHandler.js +206 -0
  211. package/dist/modules/actions/transport/webhookHandler.test.d.ts +1 -0
  212. package/dist/modules/actions/types.d.ts +234 -0
  213. package/dist/modules/connectors/engine.js +4 -1
  214. package/dist/modules/connectors/resolver.d.ts +10 -2
  215. package/dist/modules/connectors/resolver.js +8 -3
  216. package/dist/modules/mysql/accounts.d.ts +5 -0
  217. package/dist/modules/mysql/accounts.js +365 -0
  218. package/dist/modules/mysql/accounts.test.d.ts +1 -0
  219. package/dist/modules/mysql/admin.d.ts +58 -0
  220. package/dist/modules/mysql/admin.js +133 -0
  221. package/dist/modules/mysql/config.d.ts +82 -0
  222. package/dist/modules/mysql/config.js +30 -0
  223. package/dist/modules/mysql/identities.d.ts +35 -0
  224. package/dist/modules/mysql/identities.js +43 -0
  225. package/dist/modules/mysql/index.d.ts +67 -0
  226. package/dist/modules/mysql/index.js +62 -0
  227. package/dist/modules/mysql/pool.d.ts +3 -0
  228. package/dist/modules/mysql/pool.js +88 -0
  229. package/dist/modules/mysql/query.d.ts +12 -0
  230. package/dist/modules/mysql/query.js +21 -0
  231. package/dist/modules/mysql/schema.d.ts +43 -0
  232. package/dist/modules/mysql/schema.js +340 -0
  233. package/dist/modules/mysql/schema.test.d.ts +1 -0
  234. package/dist/modules/mysql/sessions.d.ts +33 -0
  235. package/dist/modules/mysql/sessions.js +46 -0
  236. package/dist/modules/mysql/spaceTokens.d.ts +11 -0
  237. package/dist/modules/mysql/spaceTokens.js +59 -0
  238. package/dist/modules/mysql/store.integration.test.d.ts +1 -0
  239. package/dist/modules/rsc/connectorRscData.d.ts +25 -0
  240. package/dist/modules/rsc/connectorRscData.js +51 -0
  241. package/dist/modules/rsc/connectorRscData.test.d.ts +1 -0
  242. package/dist/modules/rsc/handler.js +16 -3
  243. package/dist/modules/rsc/matchRscPage.d.ts +14 -0
  244. package/dist/modules/rsc/matchRscPage.js +26 -0
  245. package/dist/modules/rsc/resolveRscData.d.ts +14 -1
  246. package/dist/modules/rsc/resolveRscData.js +23 -34
  247. package/dist/modules/ssr/Component.d.ts +6 -2
  248. package/dist/modules/ssr/Component.js +3 -1
  249. package/dist/modules/ssr/draftSession.test.d.ts +1 -0
  250. package/dist/modules/ssr/handler.js +15 -2
  251. package/dist/modules/ssr/loadPluginComponents.js +22 -5
  252. package/dist/modules/ssr/prepareRender.js +87 -8
  253. package/dist/modules/ssr/prepareRender.test.d.ts +1 -0
  254. package/dist/modules/ssr/preview.d.ts +21 -8
  255. package/dist/modules/ssr/preview.js +38 -14
  256. package/dist/modules/ssr/registerExternalPlugins.js +2 -2
  257. package/dist/modules/ssr/resolvePageSeo.d.ts +18 -0
  258. package/dist/modules/ssr/resolvePageSeo.js +26 -0
  259. package/dist/modules/ssr/resolvePageSeo.test.d.ts +1 -0
  260. package/dist/modules/ssr/template.test.d.ts +1 -0
  261. package/dist/modules/ssr/views/template.ejs +71 -5
  262. package/dist/mysql.d.ts +26 -0
  263. package/dist/mysql.js +4 -0
  264. package/dist/oauth.d.ts +24 -0
  265. package/dist/oauth.js +10 -0
  266. package/dist/plugins/compile.d.ts +1 -0
  267. package/dist/plugins/compile.js +37 -3
  268. package/dist/plugins/manager.d.ts +40 -0
  269. package/dist/plugins/manager.js +113 -13
  270. package/dist/plugins/manager.test.d.ts +1 -0
  271. package/package.json +43 -14
  272. package/skills/plitzi-authoring/SKILL.md +259 -0
  273. /package/dist/{modules/actions/handler.test.d.ts → adapters/cloudAdapters.test.d.ts} +0 -0
  274. /package/dist/modules/actions/{handler.d.ts → connectorWrite.d.ts} +0 -0
@@ -10,6 +10,14 @@ export type RscResolveContext = {
10
10
  spaceId: number;
11
11
  environment: Environment;
12
12
  user: SSRUser | undefined;
13
+ /**
14
+ * Aborted when this element's budget runs out, so a resolver can stop the work nobody is waiting for.
15
+ *
16
+ * The timeout below only ever decided when to STOP WAITING — the losing side of a race keeps running — so a
17
+ * provider that took longer than its budget went on holding a run slot and an outbound connection for a page
18
+ * that had already been sent. What is being cancelled is not the request, it is the work behind it.
19
+ */
20
+ signal: AbortSignal;
13
21
  };
14
22
  /** Produces the data slice for a single server element. Returning undefined leaves the element out of the payload. */
15
23
  export type RscElementResolver = (context: RscResolveContext) => Promise<unknown>;
@@ -22,7 +30,12 @@ export type ResolveRscDataOptions = {
22
30
  /** Restricts resolution to these element ids (partial refresh). Undefined resolves every server element. */
23
31
  ids?: string[];
24
32
  resolveElement: RscElementResolver;
25
- /** Per-element budget. One slow provider must not hold the whole payload. */
33
+ /**
34
+ * Per-element budget: one slow provider must not hold the whole payload, and must not go on working once it
35
+ * has. It is the PAGE's ceiling and it wins over the producer's own — an action allowed ten seconds of its own
36
+ * is still cut off here, because a section is worth waiting for only as long as the visitor is. Configurable
37
+ * per deployment as `rsc.elementTimeoutMs`.
38
+ */
26
39
  timeoutMs?: number;
27
40
  };
28
41
  /**
@@ -1,28 +1,23 @@
1
- import { getPaths, matchRoutePath } from "@plitzi/sdk-shared/navigation";
1
+ import { matchRscPage } from "./matchRscPage.js";
2
+ import { collectServerElements } from "@plitzi/sdk-shared/schema/serverElements";
2
3
  //#region src/modules/rsc/resolveRscData.ts
3
4
  var DEFAULT_ELEMENT_TIMEOUT_MS = 5e3;
4
- /** Depth-first walk of a page subtree. Iterative to stay safe on deeply nested schemas. */
5
- var collectSubtree = (flat, rootId) => {
6
- const collected = [];
7
- const seen = /* @__PURE__ */ new Set();
8
- const pending = [rootId];
9
- while (pending.length > 0) {
10
- const id = pending.pop();
11
- if (id === void 0 || seen.has(id)) continue;
12
- seen.add(id);
13
- const element = flat[id];
14
- if (!element) continue;
15
- collected.push(element);
16
- const { items } = element.definition;
17
- if (items) pending.push(...items);
18
- }
19
- return collected;
20
- };
21
- var withTimeout = async (promise, timeoutMs, label) => {
5
+ /**
6
+ * Runs one element's resolution against its budget, and CANCELS it when the budget is gone.
7
+ *
8
+ * The race decides what the payload waits for; the abort decides what the server keeps doing. Racing alone left
9
+ * the loser running to its own timeout — for actions, five more seconds of a held slot and an in-flight request
10
+ * per element, for a page that was answered without it.
11
+ */
12
+ var withBudget = async (run, timeoutMs, label) => {
13
+ const controller = new AbortController();
22
14
  let timer;
23
15
  try {
24
- return await Promise.race([promise, new Promise((_resolve, reject) => {
25
- timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`RSC resolution timed out after ${timeoutMs}ms for ${label}`)), timeoutMs);
16
+ return await Promise.race([run(controller.signal), new Promise((_resolve, reject) => {
17
+ timer = setTimeout(() => {
18
+ controller.abort();
19
+ reject(/* @__PURE__ */ new Error(`RSC resolution timed out after ${timeoutMs}ms for ${label}`));
20
+ }, timeoutMs);
26
21
  })]);
27
22
  } finally {
28
23
  if (timer) clearTimeout(timer);
@@ -37,21 +32,14 @@ var withTimeout = async (promise, timeoutMs, label) => {
37
32
  */
38
33
  var resolveRscData = async ({ schema, req, spaceId, environment, user, ids, resolveElement, timeoutMs = DEFAULT_ELEMENT_TIMEOUT_MS }) => {
39
34
  if (schema.rsc?.enabled === false) return {};
40
- const pages = schema.pages.reduce((acum, pageId) => {
41
- const page = schema.flat[pageId];
42
- if (page) acum[pageId] = page;
43
- return acum;
44
- }, {});
45
- const paths = getPaths(pages, schema.pageFolders, !!user);
46
- const { pageId, pathMatch } = matchRoutePath(paths, req.path, !!user);
47
- if (!pageId) return { serverData: {} };
48
- const routeParams = pathMatch?.params ?? {};
49
- const requested = ids ? new Set(ids) : void 0;
50
- const targets = collectSubtree(schema.flat, pageId).filter((element) => element.definition.runtime === "server" && (!requested || requested.has(element.id)));
35
+ const match = matchRscPage(schema, req.path, user);
36
+ if (!match) return { serverData: {} };
37
+ const { pageId, routeParams } = match;
38
+ const targets = collectServerElements(schema, pageId, ids);
51
39
  if (targets.length === 0) return { serverData: {} };
52
40
  return { serverData: (await Promise.allSettled(targets.map(async (element) => ({
53
41
  id: element.id,
54
- data: await withTimeout(resolveElement({
42
+ data: await withBudget((signal) => resolveElement({
55
43
  element,
56
44
  flat: schema.flat,
57
45
  routeParams,
@@ -59,7 +47,8 @@ var resolveRscData = async ({ schema, req, spaceId, environment, user, ids, reso
59
47
  req,
60
48
  spaceId,
61
49
  environment,
62
- user
50
+ user,
51
+ signal
63
52
  }), timeoutMs, `element ${element.id}`)
64
53
  })))).reduce((acum, result, index) => {
65
54
  if (result.status === "rejected") {
@@ -1,4 +1,4 @@
1
- import { OfflineDataRaw, Environment, RenderMode, Server, SSRPlugin, SSRRenderResult } from '@plitzi/sdk-shared';
1
+ import { OfflineDataRaw, Environment, RenderMode, Server, SSRPlugin, SSRRenderResult, Theme } from '@plitzi/sdk-shared';
2
2
  export type ComponentProps = {
3
3
  server: Partial<Server>;
4
4
  renderMode?: Extract<RenderMode, 'raw'>;
@@ -12,6 +12,10 @@ export type ComponentProps = {
12
12
  /** Forced on when the metering adapter degrades this render; otherwise left to the SDK's own default so the
13
13
  * markup here matches what the browser hydrates with. */
14
14
  branding?: boolean;
15
+ /** The same degraded render, as the reason: the account behind this space is over its quota. */
16
+ overQuota?: boolean;
17
+ /** The theme this document was rendered with, from the visitor's cookie. See `prepareRender`. */
18
+ theme?: Theme;
15
19
  };
16
- declare const Component: ({ server, renderMode, previewMode, offlineData, environment, plugins, ssrResult, sdkDevToolsStylePath, debugMode, branding }: ComponentProps) => import("react").JSX.Element;
20
+ declare const Component: ({ server, renderMode, previewMode, offlineData, environment, plugins, ssrResult, sdkDevToolsStylePath, debugMode, branding, overQuota, theme }: ComponentProps) => import("react").JSX.Element;
17
21
  export default Component;
@@ -1,7 +1,7 @@
1
1
  import PlitziSdk from "@plitzi/plitzi-sdk";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  //#region src/modules/ssr/Component.tsx
4
- var Component = ({ server, renderMode = "raw", previewMode = true, offlineData, environment = "main", plugins, ssrResult, sdkDevToolsStylePath, debugMode = false, branding }) => {
4
+ var Component = ({ server, renderMode = "raw", previewMode = true, offlineData, environment = "main", plugins, ssrResult, sdkDevToolsStylePath, debugMode = false, branding, overQuota, theme }) => {
5
5
  const serverWithResult = {
6
6
  ...server,
7
7
  ssr: {
@@ -19,6 +19,8 @@ var Component = ({ server, renderMode = "raw", previewMode = true, offlineData,
19
19
  sdkDevToolsStylePath,
20
20
  debugMode,
21
21
  ...branding === void 0 ? {} : { branding },
22
+ ...overQuota === void 0 ? {} : { overQuota },
23
+ ...theme === void 0 ? {} : { theme },
22
24
  children: plugins && Object.keys(plugins).map((key) => /* @__PURE__ */ jsx(PlitziSdk.Plugin, {
23
25
  renderType: key,
24
26
  component: plugins[key].component,
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,5 @@
1
1
  import { buildHtmlCacheKey } from "../../helpers/cache/keys.js";
2
- import { takeDraftOverride } from "./preview.js";
2
+ import { draftSessionCookie, takeDraftOverride } from "./preview.js";
3
3
  import { applySSRResult } from "./applySSRResult.js";
4
4
  import { buildBody } from "./buildBody.js";
5
5
  import { streamBody } from "./streamBody.js";
@@ -24,7 +24,20 @@ var renderSSR = async (req, res, config, renderFn, pluginManager, caches) => {
24
24
  const { environment = "main", spaceId = 1, revision = 0 } = req.ctx.spaceDeployment || {};
25
25
  const devMode = config.devMode ?? false;
26
26
  res.setHeader("Content-Type", "text/html; charset=utf-8");
27
- const offlineDataOverride = await takeDraftOverride(req, config);
27
+ const draft = await takeDraftOverride(req, config);
28
+ const offlineDataOverride = draft?.data;
29
+ if (draft) {
30
+ /**
31
+ * Unsaved work, told not to travel.
32
+ *
33
+ * `no-store` because every layer between here and the browser would otherwise be free to hand this render to
34
+ * somebody else, and `noindex` because a draft URL that reaches a crawler puts unpublished copy in a search
35
+ * result. Neither is a nicety: the whole point of a draft is that it is not the site yet.
36
+ */
37
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
38
+ res.setHeader("X-Robots-Tag", "noindex, nofollow");
39
+ if (draft.reusable) res.setHeader("Set-Cookie", draftSessionCookie(req, draft.token, config.preview?.sessionTtlMs ?? 9e5));
40
+ }
28
41
  const htmlCache = environment !== "main" && !offlineDataOverride ? caches.html : void 0;
29
42
  const cacheKey = htmlCache ? buildHtmlCacheKey(req.ctx.user?.token, spaceId, environment, revision, req) : void 0;
30
43
  if (htmlCache && cacheKey) {
@@ -1,7 +1,23 @@
1
1
  import { pathToFileURL } from "node:url";
2
2
  //#region src/modules/ssr/loadPluginComponents.ts
3
- /** Module-level cache: absolute filePath → loaded React component. */
3
+ /** Module-level cache: the bundle's stamped identity → loaded React component. */
4
4
  var componentCache = /* @__PURE__ */ new Map();
5
+ /**
6
+ * When the bundle behind an entry was built, taken from the URL the page is told to load.
7
+ *
8
+ * The manager stamps that URL for the browser's benefit — the assets are served `immutable`, so a rebuilt plugin
9
+ * needs a new URL to reach anyone — and the server needs exactly the same fact for exactly the same reason: Node's
10
+ * ESM registry caches a module by URL for the life of the process, and a rebuild lands back on the same path. Reading
11
+ * the stamp the manager already publishes keeps the two sides on one answer instead of inventing a second one.
12
+ *
13
+ * Without it a dev server renders the component it imported when it started while the browser loads the rebuilt one,
14
+ * and the two disagree about the markup — which React reports as a hydration mismatch and answers by throwing away
15
+ * the whole tree, taking the panels beside it with it.
16
+ */
17
+ var stampOf = (entry) => {
18
+ const at = entry.js?.indexOf("?v=") ?? -1;
19
+ return at === -1 ? "" : entry.js.slice(at + 3);
20
+ };
5
21
  /** Bumped on invalidation and appended to the import URL. Node's ESM registry caches a module by URL for the
6
22
  * life of the process — clearing the Map above is not enough, since a rebuilt plugin lands back on the same
7
23
  * path and `import()` would hand back the module loaded before it. A changed URL is the only way to re-read.
@@ -9,7 +25,7 @@ var componentCache = /* @__PURE__ */ new Map();
9
25
  * subsequent one hits Node's cache. Superseded generations stay resident: the registry has no eviction, which
10
26
  * is the price of reloading at all and why this is driven by explicit invalidation rather than per-request. */
11
27
  var generation = 0;
12
- var importUrl = (filePath) => generation === 0 ? filePath : `${pathToFileURL(filePath).href}?g=${generation}`;
28
+ var importUrl = (filePath, stamp) => generation === 0 && !stamp ? filePath : `${pathToFileURL(filePath).href}?v=${stamp}&g=${generation}`;
13
29
  /**
14
30
  * Plugins that failed to import (e.g. browser-only code like `document`).
15
31
  * These are permanently skipped on the server and fall through to client-side rendering.
@@ -33,11 +49,12 @@ var loadPluginComponents = async (entries, inlineComponents) => {
33
49
  await Promise.all(entries.filter((e) => e.filePath).map(async (e) => {
34
50
  const filePath = e.filePath;
35
51
  if (failedImports.has(filePath)) return;
36
- let component = componentCache.get(filePath);
52
+ const cacheKey = importUrl(filePath, stampOf(e));
53
+ let component = componentCache.get(cacheKey);
37
54
  if (!component) try {
38
- const mod = await import(importUrl(filePath));
55
+ const mod = await import(cacheKey);
39
56
  component = mod.default ?? mod;
40
- componentCache.set(filePath, component);
57
+ componentCache.set(cacheKey, component);
41
58
  } catch (err) {
42
59
  console.warn(`[SSR] Plugin "${e.keyName}" cannot be imported server-side, falling back to client rendering:`, err.message);
43
60
  failedImports.add(filePath);
@@ -1,13 +1,23 @@
1
+ import { matchRscPage } from "../rsc/matchRscPage.js";
1
2
  import { buildOfflineDataCacheKey } from "../../helpers/cache/keys.js";
2
3
  import { loadPluginComponents } from "./loadPluginComponents.js";
4
+ import "../../core/previewToken.js";
5
+ import { resolveActionEndpoint, resolveRscEndpoint } from "../../core/services/resolve.js";
3
6
  import { createOfflineDataLoader } from "../../helpers/offlineDataLoader.js";
4
7
  import { registerExternalPlugins } from "./registerExternalPlugins.js";
8
+ import { resolvePageSeo } from "./resolvePageSeo.js";
5
9
  import { sdkAssetVersion } from "../../core/sdkAssets.js";
6
10
  import { buildServerInfo } from "../../helpers/buildServerInfo.js";
7
11
  import { escapeJson } from "../../helpers/escapeJson.js";
8
12
  import { readCookie } from "../../helpers/readCookie.js";
9
13
  import { resolveDebugMode } from "../../helpers/resolveDebugMode.js";
14
+ import { hasServerElements } from "@plitzi/sdk-shared/schema/serverElements";
15
+ import { themeFromCookies } from "@plitzi/sdk-shared/theme";
16
+ import { debugCookieName } from "@plitzi/sdk-shared/devTools";
17
+ import { fontUrlResolver, fontsToHead } from "@plitzi/sdk-shared/style";
10
18
  //#region src/modules/ssr/prepareRender.tsx
19
+ /** Last resort only: used for a page that declares no SEO title and a deployment that supplies none either. */
20
+ var DEFAULT_TITLE = "Plitzi App";
11
21
  var prepareRender = async (req, config, spaceId, environment, revision, pluginManager, offlineDataCache, metrics, offlineDataOverride) => {
12
22
  const m = (name, fn) => metrics ? metrics.measure(name, fn) : Promise.resolve(fn());
13
23
  const offlineCacheKey = environment !== "main" ? buildOfflineDataCacheKey(spaceId, environment, revision) : void 0;
@@ -17,18 +27,63 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
17
27
  if (cachedOfflineStr) return Promise.resolve(JSON.parse(cachedOfflineStr));
18
28
  return m("schema", () => config.adapters.getOfflineData(spaceId, environment, revision));
19
29
  });
20
- const [offlineData, server] = await Promise.all([loadOfflineData(), m("rsc", () => buildServerInfo(req, config, loadOfflineData))]);
30
+ const offlineData = await loadOfflineData();
31
+ const rscPath = resolveRscEndpoint(config);
32
+ const schema = offlineData?.schema;
33
+ const pageMatch = schema !== void 0 ? matchRscPage(schema, req.path, req.ctx.user) : void 0;
34
+ const rscData = rscPath !== void 0 && schema !== void 0 && schema.rsc?.enabled !== false && pageMatch !== void 0 && hasServerElements(schema, pageMatch.pageId) && config.adapters.getRscData ? await m("rsc", () => config.adapters.getRscData?.({
35
+ req,
36
+ spaceId: req.ctx.spaceDeployment?.spaceId ?? spaceId,
37
+ environment: req.ctx.spaceDeployment?.environment ?? environment,
38
+ revision: req.ctx.spaceDeployment?.revision ?? revision,
39
+ user: req.ctx.user,
40
+ loadOfflineData
41
+ })) : rscPath ? { serverData: {} } : void 0;
42
+ const pageSeo = resolvePageSeo(schema, pageMatch?.pageId);
43
+ const server = buildServerInfo(req, config, {
44
+ rscPath,
45
+ rscData,
46
+ actionPath: resolveActionEndpoint(config)
47
+ });
21
48
  if (offlineDataOverride === void 0 && !cachedOfflineStr && offlineCacheKey && offlineData !== void 0) offlineDataCache?.set(offlineCacheKey, JSON.stringify(offlineData));
22
49
  const version = config.assetVersion ?? sdkAssetVersion();
23
50
  const v = version ? `?v=${version}` : "";
24
51
  const sdkDevToolsStylePath = `/sdk-assets/plitzi-sdk-devtools.css${v}`;
25
- const debugMode = resolveDebugMode(config.debugMode ?? config.devMode, readCookie(req.headers.cookie, "plitzi_debug"));
52
+ /**
53
+ * Two facts, and they have to leave this server separately.
54
+ *
55
+ * `debugAuthorized` is the deployment's decision and is what the client bootstrap is handed, because on the
56
+ * client that argument is what arms the shortcut and the "currently hidden" console hint. Collapsing the
57
+ * cookie into it — which is what this used to send — made hiding the panel on an SSR page permanent: the page
58
+ * came back authorizing nothing, so the shortcut was dead and nothing on screen or in the console said why.
59
+ *
60
+ * `debugRendered` is what this particular render draws, preference included. The client derives the same
61
+ * product from the same cookie on its first pass, so the markup it hydrates matches.
62
+ */
63
+ const debugAuthorized = !Boolean(req.query["__pt"]) && Boolean(config.debugMode ?? config.devMode);
64
+ const debugRendered = resolveDebugMode(debugAuthorized, readCookie(req.headers.cookie, debugCookieName(req.headers.host)));
26
65
  const { degrade, analytics } = req.ctx.meter ?? {};
27
66
  const clientAnalytics = analytics ? {
28
67
  ...analytics,
29
68
  firstViewCounted: true
30
69
  } : void 0;
31
70
  const branding = degrade ? true : void 0;
71
+ const overQuota = degrade ? true : void 0;
72
+ /**
73
+ * The visitor's theme, from the cookie the SDK writes it to.
74
+ *
75
+ * This is the whole reason it is a cookie and not web storage: read here, the class goes on `<html>` in the
76
+ * document the server sends, so the page arrives already painted in the right theme — no blocking script in the
77
+ * head, and no first paint in the other theme for the SDK to correct four hundred milliseconds later.
78
+ *
79
+ * It travels to the browser too. The class alone would settle what the page LOOKS like, but a space can bind to
80
+ * `{{ theme.resolved }}` or gate a rule on the scheme, and a client that started at `system` while the server
81
+ * rendered `dark` would hydrate different markup and throw away the tree.
82
+ *
83
+ * Absent — a first visit — is not a problem to solve: nothing is stamped, `system` is what the provider starts
84
+ * at, and the stylesheet's media queries answer, which is exactly right.
85
+ */
86
+ const theme = themeFromCookies(req.headers.cookie);
32
87
  const offlineDataStr = escapeJson(JSON.stringify({
33
88
  offlineData,
34
89
  offlineMode: true,
@@ -36,8 +91,10 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
36
91
  renderMode: "raw",
37
92
  server,
38
93
  sdkDevToolsStylePath,
94
+ ...theme ? { theme } : {},
39
95
  ...clientAnalytics ? { analytics: clientAnalytics } : {},
40
- ...branding ? { branding } : {}
96
+ ...branding ? { branding } : {},
97
+ ...overQuota ? { overQuota } : {}
41
98
  }));
42
99
  const pluginNames = req.ctx.spaceDeployment?.pluginNames ?? [];
43
100
  const pluginSources = req.ctx.spaceDeployment?.pluginSources;
@@ -55,7 +112,10 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
55
112
  ];
56
113
  const entries = allPluginNames.length > 0 ? await pluginManager.getEntries(allPluginNames) : [];
57
114
  const pluginComponents = await m("plugins", () => loadPluginComponents(entries, pluginManager.getComponents()));
58
- const templatePlugins = entries.length > 0 ? entries : req.ctx.spaceDeployment?.templateProps?.plugins;
115
+ const templatePlugins = (entries.length > 0 ? entries : req.ctx.spaceDeployment?.templateProps?.plugins)?.map((entry) => ({
116
+ ...entry,
117
+ ssr: entry.keyName in pluginComponents
118
+ }));
59
119
  const vendorJs = (config.devMode ? "/sdk-assets/plitzi-sdk-dev-vendor.js" : "/sdk-assets/plitzi-sdk-vendor.js") + v;
60
120
  return {
61
121
  componentProps: {
@@ -63,13 +123,15 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
63
123
  offlineData,
64
124
  server,
65
125
  environment: req.ctx.spaceDeployment?.environment ?? environment,
66
- debugMode,
126
+ debugMode: debugRendered,
67
127
  sdkDevToolsStylePath,
68
- branding
128
+ branding,
129
+ overQuota,
130
+ theme
69
131
  },
70
132
  entries,
71
133
  templateParams: {
72
- title: "Plitzi App",
134
+ title: DEFAULT_TITLE,
73
135
  jsPath: `/sdk-assets/plitzi-sdk.js${v}`,
74
136
  cssPath: `/sdk-assets/plitzi-sdk.css${v}`,
75
137
  react: vendorJs,
@@ -77,9 +139,26 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
77
139
  reactDom: vendorJs,
78
140
  reactDomClient: vendorJs,
79
141
  reactCompilerRuntime: vendorJs,
142
+ /**
143
+ * The class this document wears, straight onto `<html>` — see `theme` above.
144
+ *
145
+ * `system` and "never chose" both write nothing, and the silence is the mechanism: the stylesheet's
146
+ * `prefers-color-scheme` queries are guarded on the absence of these classes, so stamping one would freeze the
147
+ * page against the machine it is running on.
148
+ */
149
+ themeClass: theme && theme !== "system" ? theme : void 0,
150
+ /**
151
+ * The space's own families, requested by the document itself.
152
+ *
153
+ * Ahead of the deployment's `templateProps` because a deployment cannot know them: they are a fact about the
154
+ * space's style document, and a page whose text is laid out in a face the browser has not been asked for is
155
+ * a page that renders in a fallback and then reflows.
156
+ */
157
+ fonts: fontsToHead(offlineData?.style.fonts ?? [], fontUrlResolver(config.fonts?.baseUrl)),
80
158
  ...req.ctx.spaceDeployment?.templateProps,
159
+ ...pageSeo,
81
160
  plugins: templatePlugins,
82
- debugMode,
161
+ debugMode: debugAuthorized,
83
162
  ssrOnly: config.ssrOnly === true,
84
163
  offlineData: offlineDataStr
85
164
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,10 +1,23 @@
1
1
  import { DraftStore, OfflineDataRaw, SSRRequest, SSRServerConfig } from '@plitzi/sdk-shared';
2
- /** The READ side of draft-preview: a render resolving a stashed draft from its one-shot token. The write side —
3
- * applying unsaved operations and rendering the result — lives in `@plitzi/sdk-mcp`, which is what knows how to
4
- * interpret an operation; a page-only deployment keeps this much and never loads that. */
5
- /** In-memory one-shot draft store — the default when the consumer injects none. Fine for a single replica;
6
- * a multi-replica deployment must inject a shared store so a preview URL resolves on any replica. */
2
+ /** The READ side of draft-preview: a render resolving a stashed draft from its token. The write side — applying
3
+ * unsaved operations and rendering the result — lives in `@plitzi/sdk-mcp`, which is what knows how to interpret an
4
+ * operation; a page-only deployment keeps this much and never loads that. */
5
+ /** In-memory draft store — the default when the consumer injects none. Fine for a single replica; a multi-replica
6
+ * deployment must inject a shared store so a preview URL resolves on any replica. */
7
7
  export declare const createMemoryDraftStore: () => DraftStore;
8
- /** Consume the draft override for a render, if the request carries a valid preview token. Returns undefined on
9
- * a normal request (renders persisted state) or when the token is unknown/expired (one-shot / TTL). */
10
- export declare const takeDraftOverride: (req: SSRRequest, config: SSRServerConfig) => Promise<OfflineDataRaw | undefined>;
8
+ /** What a draft render has to say about itself, so nothing downstream has to ask the store a second time. */
9
+ export type DraftOverride = {
10
+ data: OfflineDataRaw;
11
+ token: string;
12
+ /** Whether the token survived the read — which is what decides if the visit is worth remembering in a cookie. */
13
+ reusable: boolean;
14
+ };
15
+ /**
16
+ * Resolve the draft this request is looking at, if any.
17
+ *
18
+ * Returns undefined on a normal request (renders persisted state) and on a token the store no longer has — expired,
19
+ * dropped, or a one-shot that has already been rendered once.
20
+ */
21
+ export declare const takeDraftOverride: (req: SSRRequest, config: SSRServerConfig) => Promise<DraftOverride | undefined>;
22
+ /** The `Set-Cookie` that carries a draft session through the rest of the visit. */
23
+ export declare const draftSessionCookie: (req: SSRRequest, token: string, ttlMs: number) => string;
@@ -1,33 +1,57 @@
1
- import { PREVIEW_TOKEN_PARAM } from "../../core/previewToken.js";
1
+ import { PREVIEW_COOKIE, readDraftToken } from "../../core/previewToken.js";
2
2
  //#region src/modules/ssr/preview.ts
3
- /** The READ side of draft-preview: a render resolving a stashed draft from its one-shot token. The write side —
4
- * applying unsaved operations and rendering the result — lives in `@plitzi/sdk-mcp`, which is what knows how to
5
- * interpret an operation; a page-only deployment keeps this much and never loads that. */
6
- /** In-memory one-shot draft store — the default when the consumer injects none. Fine for a single replica;
7
- * a multi-replica deployment must inject a shared store so a preview URL resolves on any replica. */
3
+ /** The READ side of draft-preview: a render resolving a stashed draft from its token. The write side — applying
4
+ * unsaved operations and rendering the result — lives in `@plitzi/sdk-mcp`, which is what knows how to interpret an
5
+ * operation; a page-only deployment keeps this much and never loads that. */
6
+ /** In-memory draft store — the default when the consumer injects none. Fine for a single replica; a multi-replica
7
+ * deployment must inject a shared store so a preview URL resolves on any replica. */
8
8
  var createMemoryDraftStore = () => {
9
9
  const store = /* @__PURE__ */ new Map();
10
10
  return {
11
- put(token, data, ttlMs) {
11
+ put(token, data, { ttlMs, reusable = false }) {
12
12
  store.set(token, {
13
13
  data,
14
- expiresAt: Date.now() + ttlMs
14
+ expiresAt: Date.now() + ttlMs,
15
+ reusable
15
16
  });
16
17
  },
17
18
  take(token) {
18
19
  const hit = store.get(token);
19
20
  if (!hit) return;
21
+ if (!hit.reusable || hit.expiresAt <= Date.now()) store.delete(token);
22
+ return hit.expiresAt > Date.now() ? {
23
+ data: hit.data,
24
+ reusable: hit.reusable
25
+ } : void 0;
26
+ },
27
+ drop(token) {
20
28
  store.delete(token);
21
- return hit.expiresAt > Date.now() ? hit.data : void 0;
22
29
  }
23
30
  };
24
31
  };
25
- /** Consume the draft override for a render, if the request carries a valid preview token. Returns undefined on
26
- * a normal request (renders persisted state) or when the token is unknown/expired (one-shot / TTL). */
32
+ /**
33
+ * Resolve the draft this request is looking at, if any.
34
+ *
35
+ * Returns undefined on a normal request (renders persisted state) and on a token the store no longer has — expired,
36
+ * dropped, or a one-shot that has already been rendered once.
37
+ */
27
38
  var takeDraftOverride = async (req, config) => {
28
- const token = req.query[PREVIEW_TOKEN_PARAM];
39
+ const token = readDraftToken(req);
29
40
  if (!token || !config.draftStore) return;
30
- return config.draftStore.take(token);
41
+ const entry = await config.draftStore.take(token);
42
+ return entry ? {
43
+ ...entry,
44
+ token
45
+ } : void 0;
31
46
  };
47
+ /** The `Set-Cookie` that carries a draft session through the rest of the visit. */
48
+ var draftSessionCookie = (req, token, ttlMs) => [
49
+ `${PREVIEW_COOKIE}=${encodeURIComponent(token)}`,
50
+ "Path=/",
51
+ "HttpOnly",
52
+ "SameSite=Lax",
53
+ `Max-Age=${Math.max(0, Math.floor(ttlMs / 1e3))}`,
54
+ ...req.protocol === "https" ? ["Secure"] : []
55
+ ].join("; ");
32
56
  //#endregion
33
- export { createMemoryDraftStore, takeDraftOverride };
57
+ export { createMemoryDraftStore, draftSessionCookie, takeDraftOverride };
@@ -1,6 +1,6 @@
1
+ import crypto from "node:crypto";
1
2
  import path from "node:path";
2
3
  import fs from "node:fs/promises";
3
- import crypto from "node:crypto";
4
4
  //#region src/modules/ssr/registerExternalPlugins.ts
5
5
  var MANIFEST_TTL_MS = 6e5;
6
6
  var manifestCache = /* @__PURE__ */ new Map();
@@ -86,7 +86,7 @@ var registerPlugin = async (pluginManager, plugin) => {
86
86
  const source = {
87
87
  js: jsUrl,
88
88
  css: findAsset(manifest, "style", plugin.resource),
89
- action: "download",
89
+ action: "cdn",
90
90
  version: manifest.version
91
91
  };
92
92
  return pluginManager.ensure(plugin.type, source);
@@ -0,0 +1,18 @@
1
+ import { Schema } from '@plitzi/sdk-shared';
2
+ export type PageSeo = {
3
+ title?: string;
4
+ description?: string;
5
+ };
6
+ /**
7
+ * The title and description the addressed page declares for itself.
8
+ *
9
+ * The page element has carried `seoEnabled` / `seoPageTitle` / `seoPageDescription` all along — the builder edits
10
+ * them and the client renders them into the head through Helmet — but the SSR document was built from a constant,
11
+ * so every server-rendered page arrived titled the same. That is the half a crawler and a link preview see, which
12
+ * is the half that matters: they read the document as delivered and never run the client.
13
+ *
14
+ * `seoEnabled: false` returns nothing rather than a default, so the deployment's own title stays in charge of a
15
+ * page that opted out. Blank strings are treated as absent for the same reason — the builder writes one for a
16
+ * field the author cleared, and an empty <title> is worse than a generic one.
17
+ */
18
+ export declare const resolvePageSeo: (schema: Schema | undefined, pageId: string | undefined) => PageSeo;
@@ -0,0 +1,26 @@
1
+ //#region src/modules/ssr/resolvePageSeo.ts
2
+ /**
3
+ * The title and description the addressed page declares for itself.
4
+ *
5
+ * The page element has carried `seoEnabled` / `seoPageTitle` / `seoPageDescription` all along — the builder edits
6
+ * them and the client renders them into the head through Helmet — but the SSR document was built from a constant,
7
+ * so every server-rendered page arrived titled the same. That is the half a crawler and a link preview see, which
8
+ * is the half that matters: they read the document as delivered and never run the client.
9
+ *
10
+ * `seoEnabled: false` returns nothing rather than a default, so the deployment's own title stays in charge of a
11
+ * page that opted out. Blank strings are treated as absent for the same reason — the builder writes one for a
12
+ * field the author cleared, and an empty <title> is worse than a generic one.
13
+ */
14
+ var resolvePageSeo = (schema, pageId) => {
15
+ if (!schema || !pageId) return {};
16
+ const page = schema.flat[pageId];
17
+ if (!page?.attributes.seoEnabled) return {};
18
+ const title = page.attributes.seoPageTitle?.trim();
19
+ const description = page.attributes.seoPageDescription?.trim();
20
+ return {
21
+ ...title ? { title } : {},
22
+ ...description ? { description } : {}
23
+ };
24
+ };
25
+ //#endregion
26
+ export { resolvePageSeo };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};