@paroicms/site-generator-plugin 0.3.0 → 0.5.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,5 @@
1
1
  import { boolVal, isObj, strVal, strValOrUndef } from "@paroi/data-formatters-lib";
2
+ import { ApiError } from "@paroicms/public-server-lib";
2
3
  export function formatGeneratorPluginConfiguration(pluginConf) {
3
4
  const anthropicApiKey = strValOrUndef(pluginConf.anthropicApiKey, { varName: "anthropicApiKey" });
4
5
  const mistralApiKey = strValOrUndef(pluginConf.mistralApiKey, { varName: "mistralApiKey" });
@@ -11,27 +12,43 @@ export function formatGeneratorPluginConfiguration(pluginConf) {
11
12
  }
12
13
  export function formatGeneratorCommand(data) {
13
14
  if (!isObj(data))
14
- throw new Error("Invalid input");
15
+ throw new ApiError("Invalid input", 400);
15
16
  const command = strVal(data.command, { varName: "command" });
17
+ if (command === "newSession") {
18
+ return {
19
+ command: "newSession",
20
+ recaptchaToken: strVal(data.recaptchaToken, { varName: "recaptchaToken" }),
21
+ };
22
+ }
16
23
  if (command === "createSiteSchema") {
17
24
  return {
18
25
  command: "createSiteSchema",
19
- prompt: strVal(data.prompt, { varName: "prompt" }),
26
+ sessionToken: strVal(data.sessionToken, { varName: "sessionToken" }),
27
+ prompt: formatPrompt(data.prompt),
20
28
  };
21
29
  }
22
30
  if (command === "updateSiteSchema") {
23
31
  return {
24
32
  command: "updateSiteSchema",
25
- prompt: strVal(data.prompt, { varName: "prompt" }),
33
+ sessionToken: strVal(data.sessionToken, { varName: "sessionToken" }),
34
+ prompt: formatPrompt(data.prompt),
26
35
  generatedSchema: data.generatedSchema,
27
36
  };
28
37
  }
29
38
  if (command === "generateSite") {
30
39
  return {
31
40
  command: "generateSite",
41
+ sessionToken: strVal(data.sessionToken, { varName: "sessionToken" }),
32
42
  generatedSchema: data.generatedSchema,
33
43
  withFakeContent: boolVal(data.withFakeContent, { varName: "withFakeContent" }),
34
44
  };
35
45
  }
36
- throw new Error(`Invalid command: ${command}`);
46
+ throw new ApiError(`Invalid command: ${command}`, 400);
47
+ }
48
+ function formatPrompt(val) {
49
+ const prompt = strVal(val, { varName: "prompt" });
50
+ if (prompt.length > 5000) {
51
+ throw new ApiError("Prompt is too long", 400);
52
+ }
53
+ return prompt;
37
54
  }
@@ -1,8 +1,16 @@
1
1
  import { invokeMessageGuard } from "./llm-queries/invoke-message-guard.js";
2
2
  import { invokeNewSiteAnalysis } from "./llm-queries/invoke-new-site-analysis.js";
3
3
  import { invokeUpdateSiteSchema } from "./llm-queries/invoke-update-site-schema.js";
4
+ import { newSession, verifySessionToken } from "./session/generator-session.js";
4
5
  import { generateSite } from "./site-generator/site-generator.js";
5
6
  export async function executeCommand(ctx, input) {
7
+ if (input.command === "newSession") {
8
+ return {
9
+ success: true,
10
+ result: await newSession(ctx, input),
11
+ };
12
+ }
13
+ verifySessionToken(input.sessionToken);
6
14
  if (input.command === "createSiteSchema") {
7
15
  const errorResponse = await invokeMessageGuard(ctx, input);
8
16
  if (errorResponse)
@@ -13,7 +21,9 @@ export async function executeCommand(ctx, input) {
13
21
  };
14
22
  }
15
23
  if (input.command === "updateSiteSchema") {
16
- const errorResponse = await invokeMessageGuard(ctx, input);
24
+ const errorResponse = await invokeMessageGuard(ctx, input, {
25
+ skipOutOfScopeCheck: true,
26
+ });
17
27
  if (errorResponse)
18
28
  return errorResponse;
19
29
  return {
@@ -37,7 +37,7 @@ export function parseLlmResponseAsList(llmResponse, outputTags, options = {}) {
37
37
  }
38
38
  current = {};
39
39
  }
40
- if (rawTag.content === "") {
40
+ if (rawTag.content.trim() === "") {
41
41
  if (!outputTag.optional) {
42
42
  const message = `Empty tag <${outputTag.tagName}>`;
43
43
  if (!tolerateErrors)
@@ -6,7 +6,7 @@ const guardPrompt = await createPromptTemplate({
6
6
  fileName: "message-guard.md",
7
7
  });
8
8
  const invalidCauses = new Set(["rude", "malicious", "outOfScope", "technicalLimits", "noSense"]);
9
- export async function invokeMessageGuard(ctx, input) {
9
+ export async function invokeMessageGuard(ctx, input, { skipOutOfScopeCheck = false } = {}) {
10
10
  const debugName = "guard";
11
11
  const llmInput = {
12
12
  message: input.prompt,
@@ -27,6 +27,12 @@ export async function invokeMessageGuard(ctx, input) {
27
27
  },
28
28
  ]);
29
29
  const report = formatMessageGuardReport(rawReport.guard);
30
+ if (report.causes && skipOutOfScopeCheck) {
31
+ report.causes = report.causes.filter((cause) => cause !== "outOfScope");
32
+ if (report.causes.length === 0) {
33
+ report.valid = true;
34
+ }
35
+ }
30
36
  if (report.valid)
31
37
  return;
32
38
  return {
@@ -28,6 +28,7 @@ export async function invokeNewSiteAnalysis(ctx, input) {
28
28
  siteTitle,
29
29
  siteSchema,
30
30
  l10n,
31
+ changed: true,
31
32
  explanation,
32
33
  };
33
34
  }
@@ -44,7 +45,8 @@ export async function invokeNewSiteAnalysis(ctx, input) {
44
45
  siteTitle,
45
46
  siteSchema: updated.siteSchema,
46
47
  l10n: updated.l10n,
47
- explanation: `${explanation}\n\n${updated.explanation}`,
48
+ changed: true,
49
+ explanation: updated.changed ? `${explanation}\n\n${updated.explanation}` : explanation,
48
50
  };
49
51
  }
50
52
  async function invokeAnalysisStep1(ctx, input) {
@@ -53,12 +55,12 @@ async function invokeAnalysisStep1(ctx, input) {
53
55
  message: input.prompt,
54
56
  siteSchemaTsDefs: getSiteSchemaTsDefs(),
55
57
  };
56
- const debug = await debugLlmOutput(ctx, debugName, ctx.goodModelName, {
58
+ const debug = await debugLlmOutput(ctx, debugName, ctx.bestModelName, {
57
59
  message: llmInput.message,
58
60
  });
59
61
  let llmMessageContent = debug.storedContent;
60
62
  if (!llmMessageContent) {
61
- const llmMessage = await analyzePrompt.pipe(ctx.goodModel).invoke(llmInput);
63
+ const llmMessage = await analyzePrompt.pipe(ctx.bestModel).invoke(llmInput);
62
64
  llmMessageContent = await debug.getMessageContent(llmMessage);
63
65
  }
64
66
  const rawAnalysis = parseLlmResponseAsProperties(llmMessageContent, [
@@ -13,6 +13,7 @@ export async function invokeUpdateSiteSchema(ctx, input) {
13
13
  // no changes
14
14
  return {
15
15
  ...input.generatedSchema,
16
+ changed: false,
16
17
  explanation: task.explanation,
17
18
  };
18
19
  }
@@ -22,6 +23,7 @@ export async function invokeUpdateSiteSchema(ctx, input) {
22
23
  });
23
24
  return {
24
25
  ...genSchema,
26
+ changed: true,
25
27
  explanation: task.explanation,
26
28
  };
27
29
  }
@@ -29,6 +31,7 @@ async function invokeUpdateSiteSchemaStep1(ctx, input) {
29
31
  const debugName = "update-step1";
30
32
  const llmInput = {
31
33
  siteSchemaTsDefs: getSiteSchemaTsDefs(),
34
+ predefinedFields: JSON.stringify(getPredefinedFields(), undefined, 2),
32
35
  siteSchemaJson: JSON.stringify(input.generatedSchema.siteSchema, undefined, 2),
33
36
  l10nJson: JSON.stringify(input.generatedSchema.l10n, undefined, 2),
34
37
  updateMessage: input.prompt,
@@ -66,29 +69,38 @@ async function invokeUpdateSiteSchemaStep2(ctx, input) {
66
69
  l10nJson: JSON.stringify(input.generatedSchema.l10n, undefined, 2),
67
70
  taskDetailsMd: input.taskDetailsMd,
68
71
  };
69
- const debug = await debugLlmOutput(ctx, debugName, ctx.goodModelName, {
72
+ const debug = await debugLlmOutput(ctx, debugName, ctx.bestModelName, {
70
73
  taskDetailsMd: llmInput.taskDetailsMd,
71
74
  siteSchemaJson: llmInput.siteSchemaJson,
72
75
  l10nJson: llmInput.l10nJson,
73
76
  });
74
77
  let llmMessageContent = debug.storedContent;
75
78
  if (!llmMessageContent) {
76
- const llmMessage = await prompt2Tpl.pipe(ctx.goodModel).invoke(llmInput);
79
+ const llmMessage = await prompt2Tpl.pipe(ctx.bestModel).invoke(llmInput);
77
80
  llmMessageContent = await debug.getMessageContent(llmMessage);
78
81
  }
79
- return {
82
+ const parsed = parseLlmResponseAsProperties(llmMessageContent, [
83
+ {
84
+ tagName: "updated_site_schema_json",
85
+ key: "siteSchema",
86
+ format: "json",
87
+ optional: true,
88
+ },
89
+ {
90
+ tagName: "updated_l10n_json",
91
+ key: "l10n",
92
+ format: "json",
93
+ optional: true,
94
+ },
95
+ ]);
96
+ const result = {
80
97
  ...input.generatedSchema,
81
- ...parseLlmResponseAsProperties(llmMessageContent, [
82
- {
83
- tagName: "updated_site_schema_json",
84
- key: "siteSchema",
85
- format: "json",
86
- },
87
- {
88
- tagName: "updated_labels_json",
89
- key: "l10n",
90
- format: "json",
91
- },
92
- ]),
93
98
  };
99
+ if (parsed.siteSchema) {
100
+ result.siteSchema = parsed.siteSchema;
101
+ }
102
+ if (parsed.l10n) {
103
+ result.l10n = parsed.l10n;
104
+ }
105
+ return result;
94
106
  }
@@ -0,0 +1,33 @@
1
+ import { ApiError } from "@paroicms/public-server-lib";
2
+ import { nanoid } from "nanoid";
3
+ const { sign, verify } = (await import("jsonwebtoken")).default;
4
+ const JWT_SECRET = "init"; // FIXME: Hardcoded JWT secret as specified
5
+ export async function newSession(ctx, command) {
6
+ const { service } = ctx;
7
+ const { recaptchaToken } = command;
8
+ const isValid = await service.validateRecaptchaResponse(recaptchaToken);
9
+ if (!isValid)
10
+ throw new ApiError("Invalid reCAPTCHA token", 400);
11
+ const sessionId = nanoid();
12
+ const token = sign({ sessionId }, JWT_SECRET, { expiresIn: "24h" });
13
+ return { token };
14
+ }
15
+ /**
16
+ * Verifies a session token and returns the session ID if valid
17
+ */
18
+ export function verifySessionToken(token) {
19
+ let payload;
20
+ try {
21
+ payload = verify(token, JWT_SECRET);
22
+ }
23
+ catch (error) {
24
+ if (error.name === "TokenExpiredError")
25
+ throw new ApiError("Session token expired", 401);
26
+ if (error.name === "JsonWebTokenError")
27
+ throw new ApiError("Invalid session token", 401);
28
+ throw error;
29
+ }
30
+ if (!payload || !payload.sessionId)
31
+ throw new ApiError("Invalid session token", 401);
32
+ return payload.sessionId;
33
+ }
@@ -63,7 +63,6 @@ function templateOfSpecialDocument(ctx, documentType) {
63
63
  <div class="TextWidth Pt">
64
64
  <div
65
65
  data-effect="paContactForm"
66
- data-recaptcha-key="{{ site.recaptchaKey }}"
67
66
  data-home-url="{{ site.home.url }}"></div>
68
67
  </div>
69
68
  </div>`;
@@ -5,15 +5,14 @@ import { join } from "node:path";
5
5
  import { fillSiteWithFakeContent } from "../fake-content-generator.ts/create-database-with-fake-content.js";
6
6
  import { createTheme } from "./theme-creator.js";
7
7
  export async function generateSite(ctx, input) {
8
- const { service, logger, pluginConf: { packName }, } = ctx;
8
+ const { service, logger, packConf } = ctx;
9
+ const { sitesDir, packName } = packConf;
9
10
  const { generatedSchema: { l10n, siteSchema, siteTitle }, withFakeContent, } = input;
10
- const siteId = randomUUID();
11
- logger.info(`Generating site: ${siteId}…`);
12
- const packConf = service.connector.getSitePackConf(packName);
13
- const { sitesDir } = packConf;
14
11
  if (!sitesDir) {
15
12
  throw new Error(`Site-generator plugin can generate sites only for pack with "sitesDir", but pack "${packName}" doesn't have it`);
16
13
  }
14
+ const siteId = randomUUID();
15
+ logger.info(`Generating site: ${siteId}…`);
17
16
  const siteDir = join(sitesDir, siteId);
18
17
  await mkdir(siteDir);
19
18
  await writeFile(join(siteDir, "site-schema.json"), JSON.stringify(siteSchema, null, 2), "utf-8");
@@ -2,7 +2,7 @@ import { ChatAnthropic } from "@langchain/anthropic";
2
2
  import { ChatMistralAI } from "@langchain/mistralai";
3
3
  import { strVal } from "@paroi/data-formatters-lib";
4
4
  import { pathExists } from "@paroicms/internal-server-lib";
5
- import { escapeHtml } from "@paroicms/public-server-lib";
5
+ import { ApiError, escapeHtml } from "@paroicms/public-server-lib";
6
6
  import { readFileSync } from "node:fs";
7
7
  import { dirname, join } from "node:path";
8
8
  import { projectDir } from "./context.js";
@@ -11,12 +11,13 @@ import { executeCommand } from "./generator/actions.js";
11
11
  import { initializeImageNames } from "./generator/lib/images-lib.js";
12
12
  const packageDir = dirname(projectDir);
13
13
  const version = strVal(JSON.parse(readFileSync(join(packageDir, "package.json"), "utf-8")).version);
14
+ const SLUG = "site-generator";
14
15
  await initializeImageNames();
15
16
  const plugin = {
16
17
  version,
17
- slug: "site-generator",
18
+ slug: SLUG,
18
19
  async siteInit(service) {
19
- const pluginConf = formatGeneratorPluginConfiguration(service.pluginConf);
20
+ const pluginConf = formatGeneratorPluginConfiguration(service.configuration);
20
21
  let debugDir = pluginConf.debugDir;
21
22
  if (debugDir) {
22
23
  if (!(await pathExists(debugDir))) {
@@ -24,11 +25,23 @@ const plugin = {
24
25
  debugDir = undefined;
25
26
  }
26
27
  }
28
+ const bestModel = new ChatAnthropic({
29
+ modelName: "claude-3-7-sonnet-20250219",
30
+ anthropicApiKey: pluginConf.anthropicApiKey,
31
+ temperature: 0.1,
32
+ maxTokens: 4096,
33
+ clientOptions: {
34
+ timeout: 60_000,
35
+ },
36
+ });
27
37
  const goodModel = new ChatAnthropic({
28
38
  modelName: "claude-3-7-sonnet-20250219",
29
39
  anthropicApiKey: pluginConf.anthropicApiKey,
30
- temperature: 0.2,
31
- // maxTokens: 50_000,
40
+ temperature: 0.1,
41
+ maxTokens: 4096,
42
+ clientOptions: {
43
+ timeout: 20_000,
44
+ },
32
45
  });
33
46
  // const cheapModel = new ChatAnthropic({
34
47
  // modelName: "claude-3-5-haiku-20241022",
@@ -43,16 +56,26 @@ const plugin = {
43
56
  maxTokens: 50_000,
44
57
  });
45
58
  service.setPublicAssetsDirectory(join(packageDir, "gen-front", "dist"));
46
- service.addHeadTag(`<link rel="stylesheet" href="${escapeHtml(`${service.pluginAssetsUrl}/gen-front.css`)}">`, `<script type="module" src="${escapeHtml(`${service.pluginAssetsUrl}/gen-front.mjs`)}"></script>`);
47
- service.setPublicApiHandler(async (service, req, res, relativePath) => {
59
+ const scriptAttr = [
60
+ ["type", "module"],
61
+ ["src", `${service.pluginAssetsUrl}/gen-front.mjs`],
62
+ ["class", `js-script-${SLUG}`],
63
+ ["data-google-recaptcha-key", service.configuration.googleRecaptchaKey],
64
+ ];
65
+ service.addHeadTag(`<link rel="stylesheet" href="${escapeHtml(`${service.pluginAssetsUrl}/gen-front.css`)}">`, `<script ${scriptAttr.map(([key, val]) => `${key}="${escapeHtml(val)}"`).join(" ")}></script>`);
66
+ service.setPublicApiHandler(async (service, httpContext, relativePath) => {
67
+ const { req, res } = httpContext;
48
68
  if (relativePath !== "") {
49
69
  res.status(404).send({ status: 404 });
50
70
  return;
51
71
  }
52
72
  const ctx = {
53
73
  pluginConf,
74
+ packConf: service.connector.getSitePackConf(pluginConf.packName),
54
75
  service,
55
76
  logger: service.logger,
77
+ bestModel,
78
+ bestModelName: bestModel.model,
56
79
  goodModel,
57
80
  goodModelName: goodModel.model,
58
81
  cheapModel,
@@ -76,9 +99,18 @@ const plugin = {
76
99
  res.send(buf);
77
100
  }
78
101
  catch (error) {
79
- service.logger.error("Error while executing command:", error);
80
- const response = { success: false };
81
- res.status(500).send(response);
102
+ service.logger.error("Error executing command:", error);
103
+ if (error instanceof ApiError) {
104
+ const response = {
105
+ success: false,
106
+ userMessage: error.message,
107
+ };
108
+ res.status(error.status).send(response);
109
+ }
110
+ else {
111
+ const response = { success: false };
112
+ res.status(500).send(response);
113
+ }
82
114
  }
83
115
  });
84
116
  },
@@ -58,6 +58,7 @@ Guidelines for creating the hierarchical bullet list:
58
58
  - Each distinct node type must have a unique name through the whole tree structure.
59
59
  - Reusing the same node type several times (for recursivity etc.):
60
60
  - When the same node type is reused as a child in several places, then write its children only on the first occurence.
61
+ - 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.
61
62
 
62
63
  Here's an example of a correct output:
63
64
 
@@ -207,7 +208,7 @@ Then, provide the mardown representing the website tree structure (step 2) withi
207
208
 
208
209
  Provide the node types properties (step 3) within <dictionary_yaml> tags.
209
210
 
210
- Also, write a short explanation of your choices regarding the 3 steps, within <explanation_md> tags. Write the explanation in markdown and in the website description language. Your explanation message will be shown to the end-user.
211
+ Also, write a short explanation of your choices regarding the 3 steps, within <explanation_md> tags. Write the explanation in markdown and in the website description language. Your explanation message will be shown to the end-user as a message in a chat. The user will also see the website structure and its properties, so don't repeat descriptions, don't describe choices when they're obvious. Be as succinct as possible. If there is nothing valuable to say, then let this tag empty.
211
212
 
212
213
  Finally, extract and rephrase within <unused_information_md> tags the information from the site description that you weren't able to use in the previous elements, in the form of a short prompt, in markdown format. Do not invent anything. Do not imagine anything. If you used every valuable information, then let this tag empty.
213
214
 
@@ -32,6 +32,7 @@ Guidelines for creating the dictionnary YAML:
32
32
  - Default values:
33
33
  - By default, for most of node types, if you are not sure about what could be the best fields, then remember that a document is a webpage and just use a `[htmlContent]`.
34
34
  - Except if there are specific instructions in the website description, here is the default value for the `_site` node type: `["logo", "footerMention"]`.
35
+ - Gallery of medias: there is a predefined field named `"gallery"`. It contains a list of medias. The theme can render it as a carousel, a slider, an image gallery, a slideshow, etc.
35
36
 
36
37
  Here is an example of expected output:
37
38
 
@@ -63,7 +63,7 @@
63
63
  "fieldName": "gallery",
64
64
  "dataType": "gallery",
65
65
  "localized": false,
66
- "description": "A collection of images. It can be used to create a gallery, a slider or a carousel."
66
+ "description": "A collection of medias. It can be rendered as a carousel, a slider, a slideshow, an image gallery etc."
67
67
  },
68
68
  {
69
69
  "fieldName": "image",
@@ -1,12 +1,22 @@
1
1
  You are tasked with modifying a JSON object of a _site schema_, based on a given TypeScript type definition and an update message, and its localized labels. Follow these steps carefully:
2
2
 
3
- 1. Review the TypeScript type definition of the JSON structure:
3
+ # 1. Review the TypeScript type definition of the JSON structure:
4
4
 
5
5
  <site_schema_ts_defs>
6
6
  {siteSchemaTsDefs}
7
7
  </site_schema_ts_defs>
8
8
 
9
- 2. Examine the current JSON data, which conforms to the `JtSiteSchema` type:
9
+ # 2. About fields
10
+
11
+ Fields can be either predefined fields or custom fields. Take a look at the predefined fields provided by the CMS:
12
+
13
+ <predefined_fields_json>
14
+ {predefinedFields}
15
+ </predefined_fields_json>
16
+
17
+ # 3. The current site schema
18
+
19
+ Examine the current JSON data, which conforms to the `JtSiteSchema` type:
10
20
 
11
21
  <site_schema_json>
12
22
  {siteSchemaJson}
@@ -18,17 +28,17 @@ Also, the attached localized labels:
18
28
  {l10nJson}
19
29
  </localized_labels_json>
20
30
 
21
- 3. Read the user message that describes the required changes:
31
+ # 4. Read the user message that describes the required changes:
22
32
 
23
33
  <user_message>
24
34
  {updateMessage}
25
35
  </user_message>
26
36
 
27
- 4. When there is nothing to change
37
+ # 5. When there is nothing to change
28
38
 
29
39
  Evaluate wether the current schema already implements the update message. If there's nothing to do, provide an empty <task_details_md> tags and you're done.
30
40
 
31
- 5. When there are changes to do
41
+ # 6. When there are changes to do
32
42
 
33
43
  Otherwise, if there is something to change, then rewrite the user's request as a detailed sequence of operations to be carried out one after the other.
34
44
 
@@ -40,6 +50,8 @@ Guidelines for reformulating into a detailed sequence of operations:
40
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.
41
51
  - Describe each operation concisely in a few words.
42
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
+ - 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
+ - Never use the same field twice in a list of fields.
43
55
 
44
56
  Be the the less creative as possible. Follow the instructions. In particular:
45
57
 
@@ -50,8 +62,8 @@ Be the the less creative as possible. Follow the instructions. In particular:
50
62
 
51
63
  Provide the bullet list in Markdown within <task_details_md> tags.
52
64
 
53
- 6. Write an explanation
65
+ # 6. Write an explanation
54
66
 
55
- Also, write a short explanation of your choices, within <explanation_md> tags. Write the explanation in markdown and in the language of the update message. Your explanation message will be shown to the end-user.
67
+ Also, write a short explanation of your choices, within <explanation_md> tags. Write the explanation in markdown and in the language of the update message. Your explanation message will be shown to the end-user as a message in a chat. Don't describe choices when they're obvious. Be as succinct as possible.
56
68
 
57
69
  Do not include any other explanation or commentary outside these tags.
@@ -16,7 +16,7 @@ Here are the predefined fields provided by ParoiCMS:
16
16
  {predefinedFields}
17
17
  </predefined_fields_json>
18
18
 
19
- Here is an example of an object for describing an **HTML** field type:
19
+ Here is an example of an object for describing a custom **HTML** field type:
20
20
 
21
21
  <field_type_example>
22
22
  {{
@@ -29,7 +29,7 @@ Here is an example of an object for describing an **HTML** field type:
29
29
  }}
30
30
  </field_type_example>
31
31
 
32
- Then, in the locales, the translations must be in a sub-key `fields` of the node type:
32
+ Then, in the locales, the translations of custom field types must be in a sub-key `fields` of the node type:
33
33
 
34
34
  <l10n_example>
35
35
  {{
@@ -48,6 +48,11 @@ Then, in the locales, the translations must be in a sub-key `fields` of the node
48
48
  }}
49
49
  </l10n_example>
50
50
 
51
+ Important:
52
+
53
+ - Never add locales for a predefined field.
54
+ - Never add an unknown predefined field.
55
+
51
56
  # 3. Examine the current JSON data, which conforms to the `JtSiteSchema` type:
52
57
 
53
58
  <site_schema_json>
@@ -74,4 +79,8 @@ Also, the attached locales:
74
79
 
75
80
  # 6. Output
76
81
 
77
- Provide the updated site schema in JSON within <updated_site_schema_json> tags, and the new localized labels within <updated_labels_json> Do not write any explanation or commentary outside these tags.
82
+ If there is a change in the site schema, then provide the updated site schema in JSON within <updated_site_schema_json> tags. Otherwise, let this tag empty.
83
+
84
+ If there is a change in the locales, then provide the updated attached locales within <updated_l10n_json>. Otherwise, let this tag empty.
85
+
86
+ Do not write any explanation or commentary outside these tags.