@paroicms/site-generator-plugin 1.0.1 → 1.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.
@@ -1,4 +1,4 @@
1
- import { getPartTypeByName, getRegularDocumentTypeByName, getRoutingDocumentTypeByName, } from "@paroicms/internal-anywhere-lib";
1
+ import { getPartTypeByName, getRegularDocumentTypeByName, getRoutingDocumentTypeByName, isRootOfSubRoutingCluster, } from "@paroicms/internal-anywhere-lib";
2
2
  import { updateGeneratedSiteStepSetAsCompleted, } from "../../db/db-write.queries.js";
3
3
  import { createTaskCollector } from "../lib/tasks.js";
4
4
  import { createGeneratedContentReport } from "./content-report.js";
@@ -58,10 +58,40 @@ function fillRoutingDocumentAndAddChildren(ctx, tasks, report, nodeOptions) {
58
58
  }
59
59
  for (const typeName of nodeType.regularChildren ?? []) {
60
60
  const childType = getRegularDocumentTypeByName(siteSchema, typeName);
61
- tasks.add(() => addRegularDocuments(ctx, report, {
62
- parentNodeId: clusterNode.nodeId,
63
- nodeType: childType,
64
- documentType: childType,
65
- }));
61
+ if (isRootOfSubRoutingCluster(childType)) {
62
+ // Each created instance is the root of its own sub routing cluster (auto-created
63
+ // synchronously by `createDocument`). Load each sub-cluster and recurse to fill it.
64
+ tasks.add(async () => {
65
+ const created = await addRegularDocuments(ctx, report, {
66
+ parentNodeId: clusterNode.nodeId,
67
+ nodeType: childType,
68
+ documentType: childType,
69
+ });
70
+ for (const doc of created) {
71
+ const subCluster = await ctx.siteConnector.loadRoutingClusterFromNode({
72
+ nodeId: doc.nodeId,
73
+ typeName: childType.typeName,
74
+ });
75
+ for (const routingTypeName of childType.routingChildren ?? []) {
76
+ const childNode = subCluster.children?.[routingTypeName];
77
+ if (!childNode)
78
+ continue;
79
+ fillRoutingDocumentAndAddChildren(ctx, tasks, report, {
80
+ clusterNode: childNode,
81
+ nodeType: getRoutingDocumentTypeByName(siteSchema, routingTypeName),
82
+ });
83
+ }
84
+ }
85
+ });
86
+ }
87
+ else {
88
+ tasks.add(async () => {
89
+ await addRegularDocuments(ctx, report, {
90
+ parentNodeId: clusterNode.nodeId,
91
+ nodeType: childType,
92
+ documentType: childType,
93
+ });
94
+ });
95
+ }
66
96
  }
67
97
  }
@@ -1,5 +1,5 @@
1
1
  import { encodeLNodeId, } from "@paroicms/public-anywhere-lib";
2
- import { getHandleOfFeaturedImage, getHandleOfFieldOnNode } from "@paroicms/public-server-lib";
2
+ import { getHandleOfFeaturedImage, getHandleOfFieldOnNode, } from "@paroicms/public-server-lib";
3
3
  import { dedupMessages, getTermTypeNames } from "./content-helpers.js";
4
4
  import { generateFieldSetContent, generateMultipleFieldSetContents, } from "./generate-fake-content.js";
5
5
  export async function updateRoutingDocument(ctx, report, nodeOptions) {
@@ -42,7 +42,7 @@ export async function addRegularDocuments(ctx, report, nodeOptions) {
42
42
  // Special case: author terms are created without LLM
43
43
  if (nodeOptions.nodeType.typeName === "author") {
44
44
  await addSingleAuthor(ctx, report, nodeOptions);
45
- return;
45
+ return [];
46
46
  }
47
47
  ctx.logger.debug(`[TASK] Adding regular documents "${nodeOptions.nodeType.typeName}"…`);
48
48
  const { parentNodeId, nodeType, documentType } = nodeOptions;
@@ -76,6 +76,7 @@ export async function addRegularDocuments(ctx, report, nodeOptions) {
76
76
  if (errorMessages.length > 0) {
77
77
  ctx.logger.warn(`Error generating content for ${nodeType.typeName}:\n - ${errorMessages.join("\n - ")}`);
78
78
  }
79
+ const createdInfos = [];
79
80
  for (const content of list) {
80
81
  const [firstLanguage, ...otherLanguages] = siteSchema.languages;
81
82
  const parentLNodeId = encodeLNodeId({ nodeId: parentNodeId, language: firstLanguage });
@@ -87,6 +88,7 @@ export async function addRegularDocuments(ctx, report, nodeOptions) {
87
88
  values: content.fields,
88
89
  });
89
90
  const nodeId = info.nodeId;
91
+ createdInfos.push(info);
90
92
  await uploadMediaEntries(ctx, nodeId, content.medias);
91
93
  report.addId({
92
94
  typeName: nodeType.typeName,
@@ -109,6 +111,7 @@ export async function addRegularDocuments(ctx, report, nodeOptions) {
109
111
  });
110
112
  }
111
113
  }
114
+ return createdInfos;
112
115
  }
113
116
  const HERO_NAMES = [
114
117
  "Mario",
@@ -56,7 +56,7 @@ export async function invokeGenerateFakeContent(ctx, input, outputTags, options)
56
56
  llmTaskName,
57
57
  temperature: 0.1,
58
58
  maxTokens: 50_000,
59
- timeoutMs: 60_000,
59
+ timeoutMs: 120_000,
60
60
  });
61
61
  llmOutput = await debug.getMessageContents(results);
62
62
  }
@@ -68,9 +68,6 @@ function appendChildToParentNode(child, parent) {
68
68
  parent.children.push(child);
69
69
  }
70
70
  else if (parent.kind === "regularDocument") {
71
- if (child.kind === "routingDocument") {
72
- throw new Error(`Regular document type "${parent.typeName}" cannot have ${child.kind} child "${child.typeName}"`);
73
- }
74
71
  parent.children ??= [];
75
72
  parent.children.push(child);
76
73
  }
@@ -131,7 +128,8 @@ export function parseLine(input, index) {
131
128
  };
132
129
  }
133
130
  // list of `pageSection` (parts), list name: `partSections`
134
- const partMatch = cleanLine.match(/^list of `([^`]+)` \(parts\),? list name: `([^`]+)`$/);
131
+ // The list name backticks are optional: the LLM is inconsistent about wrapping it.
132
+ const partMatch = cleanLine.match(/^list of `([^`]+)` \(parts\),? list name: `?([^`]+?)`?$/);
135
133
  if (partMatch) {
136
134
  return {
137
135
  kind: "part",
@@ -9,7 +9,7 @@ import { parseMarkdownBulletedList } from "../lib/markdown-bulleted-list-parser.
9
9
  import { parseLlmResponseAsProperties } from "../lib/parse-llm-response.js";
10
10
  import { safeCallStep } from "../lib/session-utils.js";
11
11
  import { createL10n } from "../site-schema-generator/create-l10n.js";
12
- import { createSiteSchemaFromAnalysis } from "../site-schema-generator/create-site-schema.js";
12
+ import { createSiteSchemaFromAnalysis, ensureSubClusterAutoCreate, } from "../site-schema-generator/create-site-schema.js";
13
13
  import { invokeUpdateSiteSchema } from "./invoke-update-site-schema.js";
14
14
  export const analyzePrompt = await createPromptTemplate({
15
15
  filename: "initial-1-analysis.md",
@@ -37,6 +37,7 @@ export async function invokeInitialAnalysis(ctx, stepHandle, input) {
37
37
  });
38
38
  const { unusedInformation: unusedInformation2, llmReport: llmReport2 } = await invokeAnalysisStep2(ctx, { prompt: createUnusedInformationPrompt(unusedInformation, analysis) ?? "" }, siteSchema, stepHandle);
39
39
  reorderSiteSchemaNodeTypes(siteSchema);
40
+ ensureSubClusterAutoCreate(siteSchema);
40
41
  const l10n = createL10n(analysis, siteSchema);
41
42
  const siteTitle = {
42
43
  [analysis.siteProperties.language]: analysis.siteProperties.title,
@@ -79,7 +80,7 @@ async function invokeAnalysisStep1(ctx, input, stepHandle) {
79
80
  const { messageContent, report } = await invokeClaude(ctx, {
80
81
  llmTaskName,
81
82
  prompt,
82
- maxTokens: 4_000,
83
+ maxTokens: 10_000,
83
84
  temperature: 0.1,
84
85
  systemInstruction: "beSmart",
85
86
  });
@@ -146,7 +147,7 @@ siteSchema, stepHandle) {
146
147
  const { messageContent, report } = await invokeClaude(ctx, {
147
148
  llmTaskName,
148
149
  prompt,
149
- maxTokens: 700,
150
+ maxTokens: 1_000,
150
151
  temperature: 0.1,
151
152
  systemInstruction: "beFast",
152
153
  });
@@ -5,6 +5,7 @@ import { createPromptTemplate, getPredefinedFields, getSiteSchemaTsDefs, } from
5
5
  import { debugLlmOutput } from "../lib/debug-utils.js";
6
6
  import { parseLlmResponseAsProperties } from "../lib/parse-llm-response.js";
7
7
  import { safeCallStep } from "../lib/session-utils.js";
8
+ import { ensureSubClusterAutoCreate } from "../site-schema-generator/create-site-schema.js";
8
9
  const prompt1Tpl = await createPromptTemplate({
9
10
  filename: "update-site-schema-1-write-details.md",
10
11
  });
@@ -56,6 +57,7 @@ export async function invokeUpdateSiteSchema(ctx, stepHandle, input, { asRemaini
56
57
  : asRemainingOf.explanation;
57
58
  }
58
59
  }
60
+ ensureSubClusterAutoCreate(completedValues.siteSchema);
59
61
  await saveCompletedSchemaStep(ctx, stepHandle, completedValues);
60
62
  }
61
63
  async function invokeUpdateSiteSchemaStep1(ctx, input, stepHandle) {
@@ -80,7 +82,7 @@ async function invokeUpdateSiteSchemaStep1(ctx, input, stepHandle) {
80
82
  const { messageContent, report } = await invokeClaude(ctx, {
81
83
  llmTaskName,
82
84
  prompt: message,
83
- maxTokens: 1_500,
85
+ maxTokens: 3_000,
84
86
  temperature: 0.1,
85
87
  systemInstruction: "beSmart",
86
88
  });
@@ -126,7 +128,7 @@ async function invokeUpdateSiteSchemaStep2(ctx, input, stepHandle) {
126
128
  const { messageContent, report } = await invokeClaude(ctx, {
127
129
  llmTaskName,
128
130
  prompt: message,
129
- maxTokens: 7_000,
131
+ maxTokens: 15_000,
130
132
  temperature: 0.1,
131
133
  systemInstruction: "beSmart",
132
134
  });
@@ -46,11 +46,27 @@ function getRoutingKeyInCluster(ctx, documentType, options) {
46
46
  return found;
47
47
  }
48
48
  }
49
- const clusterRootTypeName = "home"; // limitation here: only works for the "home" cluster
49
+ // Resolve the cluster root of the taxonomy by walking ancestors via the parent map: the nearest
50
+ // ancestor that is "home" or a regular document carrying routing children.
51
+ function findClusterRoot(startTypeName) {
52
+ let current = getNodeType(startTypeName);
53
+ while (current && current.kind === "document") {
54
+ if (current.typeName === "home")
55
+ return "home";
56
+ if (current.documentKind === "regular" && (current.routingChildren?.length ?? 0) > 0) {
57
+ return current.typeName;
58
+ }
59
+ current = ctx.getParentTypes(current)[0];
60
+ }
61
+ return undefined;
62
+ }
63
+ const clusterRootTypeName = findClusterRoot(options.sameClusterAs.typeName);
64
+ if (!clusterRootTypeName)
65
+ return;
50
66
  const targetPath = findPath(documentType.typeName, clusterRootTypeName, []);
51
67
  const sameClusterPath = findPath(options.sameClusterAs.typeName, clusterRootTypeName, []);
52
68
  if (!targetPath || !sameClusterPath)
53
69
  return;
54
- targetPath.shift(); // remove leading 'home'
70
+ targetPath.shift(); // remove the leading cluster root (path is cluster-root-relative at runtime)
55
71
  return `doc.cluster.routing.${targetPath.join(".")}`;
56
72
  }
@@ -48,11 +48,12 @@ export async function generateSite(ctx, input) {
48
48
  version: "0.0.0",
49
49
  });
50
50
  const siteConnector = service.getUnsafeSiteConnector({ fqdn: regSite.fqdn });
51
+ const password = Math.random().toString(36).substring(2, 6); // 4 random lowercase characters
51
52
  const account = {
52
53
  kind: "local",
53
54
  email: `${siteId}@yopmail.com`,
54
55
  name: "Admin",
55
- password: Math.random().toString(36).substring(2, 6), // 4 random lowercase characters,
56
+ password,
56
57
  roles: ["admin"],
57
58
  };
58
59
  await siteConnector.createAccount(account, { asContactEmail: true });
@@ -63,7 +64,7 @@ export async function generateSite(ctx, input) {
63
64
  siteUrl,
64
65
  localizedValues,
65
66
  loginEmail: account.email,
66
- loginPassword: account.password,
67
+ loginPassword: password,
67
68
  };
68
69
  await saveGeneratedSiteStep(ctx, stepHandle, values);
69
70
  if (withSampleData) {
@@ -91,7 +92,7 @@ function getPackageJsonContent(options) {
91
92
  private: true,
92
93
  type: "module",
93
94
  scripts: {
94
- start: "paroicms | npm run _pino-pretty",
95
+ start: "paroicms-server | npm run _pino-pretty",
95
96
  "start:dev": "nodemon --watch './site-schema*.json' --watch ./config.json",
96
97
  clear: "rimraf theme/assets/bundle.css",
97
98
  build: "postcss theme/assets/css/index.css -o theme/assets/bundle.css",
@@ -104,7 +105,7 @@ function getPackageJsonContent(options) {
104
105
  "@paroicms/content-loading-plugin": "*",
105
106
  "@paroicms/internal-link-plugin": "*",
106
107
  "@paroicms/list-field-plugin": "*",
107
- "@paroicms/mcp-plugin": "*",
108
+ "@paroicms/api-plugin": "*",
108
109
  "@paroicms/public-menu-plugin": "*",
109
110
  "@paroicms/tiptap-editor-plugin": "*",
110
111
  "@paroicms/platform-video-plugin": "*",
@@ -67,12 +67,12 @@ function makeGetParentTypes(siteSchema) {
67
67
  for (const type of nodeTypes) {
68
68
  if (type.kind !== "document")
69
69
  continue;
70
- if (type.documentKind === "routing") {
71
- for (const childName of type.routingChildren ?? []) {
72
- const list = parentsByChild.get(childName) ?? [];
73
- list.push(type);
74
- parentsByChild.set(childName, list);
75
- }
70
+ // Register routing-child → parent links for every document kind, so a routing document nested
71
+ // under a regular parent (sub routing cluster root) keeps its parent link.
72
+ for (const childName of type.routingChildren ?? []) {
73
+ const list = parentsByChild.get(childName) ?? [];
74
+ list.push(type);
75
+ parentsByChild.set(childName, list);
76
76
  }
77
77
  for (const childName of type.regularChildren ?? []) {
78
78
  const list = parentsByChild.get(childName) ?? [];
@@ -9,7 +9,7 @@ export function createSiteSchemaFromAnalysis(analysis) {
9
9
  "@paroicms/content-loading-plugin",
10
10
  "@paroicms/internal-link-plugin",
11
11
  "@paroicms/list-field-plugin",
12
- "@paroicms/mcp-plugin",
12
+ "@paroicms/api-plugin",
13
13
  "@paroicms/platform-video-plugin",
14
14
  "@paroicms/public-menu-plugin",
15
15
  "@paroicms/tiptap-editor-plugin",
@@ -24,6 +24,20 @@ export function createSiteSchemaFromAnalysis(analysis) {
24
24
  };
25
25
  return siteSchema;
26
26
  }
27
+ /**
28
+ * Ensures every regular document that is the root of a sub routing cluster (i.e. has routing
29
+ * children) carries `cluster: { autoCreate: true }`. The LLM schema-update step sometimes strips
30
+ * this flag; without it the runtime would not auto-create the sub-cluster nodes.
31
+ */
32
+ export function ensureSubClusterAutoCreate(siteSchema) {
33
+ for (const nodeType of siteSchema.nodeTypes ?? []) {
34
+ if (nodeType.kind !== "document" || nodeType.documentKind !== "regular")
35
+ continue;
36
+ if ((nodeType.routingChildren?.length ?? 0) === 0)
37
+ continue;
38
+ nodeType.cluster ??= { autoCreate: true };
39
+ }
40
+ }
27
41
  function toNodeTypes(analysis) {
28
42
  const nodeTypes = new Map(Object.entries(analysis.dictionary)
29
43
  .map(([typeName, saType]) => [typeName, toNodeType(typeName, saType)])
@@ -69,7 +83,6 @@ function walkTree(saNode, parentNode, ctx) {
69
83
  nodeType,
70
84
  typeName,
71
85
  parentNode,
72
- children: undefined,
73
86
  }, ctx);
74
87
  }
75
88
  else {
@@ -84,7 +97,6 @@ function walkTree(saNode, parentNode, ctx) {
84
97
  nodeType,
85
98
  typeName,
86
99
  parentNode,
87
- children: undefined,
88
100
  listName: saNode.listName,
89
101
  }, ctx);
90
102
  }
@@ -117,7 +129,6 @@ function walkTree(saNode, parentNode, ctx) {
117
129
  nodeType,
118
130
  typeName,
119
131
  parentNode,
120
- children: saNode.children,
121
132
  }, ctx);
122
133
  }
123
134
  else if (saNode.kind === "part") {
@@ -130,7 +141,6 @@ function walkTree(saNode, parentNode, ctx) {
130
141
  nodeType,
131
142
  typeName,
132
143
  parentNode,
133
- children: saNode.children,
134
144
  listName: saNode.listName,
135
145
  }, ctx);
136
146
  }
@@ -153,14 +163,15 @@ function appendAsRoutingChild(options) {
153
163
  if (parentNode.kind !== "document") {
154
164
  throw new Error(`In the tree, "${typeName}" is used as a routing document but its parent is a "${parentNode.kind}"`);
155
165
  }
156
- if (parentNode.documentKind !== "routing") {
157
- throw new Error(`Due to a technical limitation, routing document "${typeName}" can't be used as child of "${parentNode.documentKind}"`);
158
- }
166
+ // A regular document with routing children is the root of a sub routing cluster: flag it so the
167
+ // runtime auto-creates the sub-cluster nodes (consistent with how the "home" cluster behaves).
168
+ if (parentNode.documentKind === "regular")
169
+ parentNode.cluster ??= { autoCreate: true };
159
170
  parentNode.routingChildren ??= [];
160
171
  parentNode.routingChildren.push(typeName);
161
172
  }
162
173
  function appendAsChildRegularDocument(options, ctx) {
163
- const { nodeType, typeName, parentNode, children } = options;
174
+ const { nodeType, typeName, parentNode } = options;
164
175
  const saType = ctx.analysis.dictionary[typeName];
165
176
  if (nodeType.kind !== "document") {
166
177
  throw new Error(`In the tree, "${typeName}" is used as a regular document but it is a "${nodeType.kind}"`);
@@ -176,12 +187,9 @@ function appendAsChildRegularDocument(options, ctx) {
176
187
  parentNode.regularChildrenSorting = saType.temporal ? "publishDate desc" : "title asc";
177
188
  }
178
189
  parentNode.regularChildren.push(typeName);
179
- for (const child of children ?? []) {
180
- walkTree(child, nodeType, ctx);
181
- }
182
190
  }
183
191
  function appendAsChildPart(options, ctx) {
184
- const { nodeType, typeName, parentNode, children, listName } = options;
192
+ const { nodeType, typeName, parentNode, listName } = options;
185
193
  const saType = ctx.analysis.dictionary[typeName];
186
194
  if (nodeType.kind !== "part") {
187
195
  throw new Error(`In the tree, "${typeName}" is used as a part but it is a "${nodeType.kind}"`);
@@ -208,9 +216,6 @@ function appendAsChildPart(options, ctx) {
208
216
  else {
209
217
  throw new Error(`Unexpected kind: "${parentNode.kind}"`);
210
218
  }
211
- for (const child of children ?? []) {
212
- walkTree(child, nodeType, ctx);
213
- }
214
219
  }
215
220
  function toNodeType(typeName, saType) {
216
221
  if (saType.kind === "part")
@@ -53,7 +53,7 @@ Guidelines for creating the hierarchical bullet list:
53
53
  - Start with a **bullet** (*).
54
54
  - Write `"list of"`, and then the **type name** within backquotes.
55
55
  - Append `(parts)`.
56
- - Add a coma, then write: `"list name:"` and append an identifier for the list name.
56
+ - Add a coma, then write: `"list name:"` and append an identifier for the list name, **within backquotes**.
57
57
  - Avoid recursivity. But if you can't avoid reusing a node type, then write its children only on the first occurence.
58
58
  - On this task, do not focus on fields. If the website description provide fields then you will report them in the unused information. In particular, a collection of medias (a carousel, a slider, an image gallery, a slideshow, etc.) is a type of field.
59
59
  - Don't try to provide a comment mechanism when the user requests a blog. Blog comments do not belong to the hierarchy of node types.
@@ -115,6 +115,8 @@ Here's an example of correct output using parts, and with the default contact an
115
115
  - `searchPage` (routing document)
116
116
  </correct_example>
117
117
 
118
+ A routing document **can** be a child of a regular document. This is a valid and supported structure: each instance of the regular document becomes the root of its own nested set of sections (a _sub routing cluster_). Keep such nesting when the website description implies it.
119
+
118
120
  ### Keep it simple
119
121
 
120
122
  When you have a choice, keep it simple. If the user lets you invent the site's structure, limit the site's sections to **2 main entries**. For example, a “news” routing document with a list of articles and categories, and a list of pages on the site's theme. Plus the usual contact and search pages.
@@ -50,6 +50,7 @@ Guidelines for reformulating into a detailed sequence of operations:
50
50
  - If the user's request is simple and doesn't need to be separated into several operations, write a list with a single operation.
51
51
  - Describe each operation concisely in a few words.
52
52
  - Make sure that the tree structure of node types is not broken. The `site` node is separate. Then, the root of the tree structure is the `home` routing document. Every other routing document type, regular document type, part type, must be the child of another node type.
53
+ - A routing document type **can** be a child of a regular document type: this is a valid _sub routing cluster_ (each instance of the regular document is the root of its own nested sections). Preserve such nesting. A regular document type that has `routingChildren` can carry `cluster: { autoCreate: true }`.
53
54
  - Prefer predefined fields where you can. In particular, you can use the `"gallery"` field when the user description is a synonym (carousel, slider, slideshow, image gallery).
54
55
  - Never use the same field twice in a list of fields.
55
56
 
@@ -129,6 +129,7 @@ Also, the attached locales:
129
129
  - You are allowed to be proactive, but only when the user asks you to do something.
130
130
  - Remember to adhere strictly to the TypeScript typing when making changes. If the update message requests changes that would violate the typing, then prioritize maintaining the correct structure over making those specific changes.
131
131
  - Field choice for listing pages: For new routing documents that have `regularChildren` (like blog index, products index, etc.), prefer `introduction[@paroicms/tiptap-editor-plugin]` instead of `htmlContent[@paroicms/tiptap-editor-plugin]`. These pages display a list of child items, so they only need a short introduction, not full HTML content.
132
+ - Preserve _sub routing clusters_: a regular document type may have a routing document type among its `routingChildren`. Keep that nesting intact. A regular document type that has `routingChildren` can carry `cluster: { autoCreate: true }`.
132
133
 
133
134
  # 8. Output
134
135