@paroicms/site-generator-plugin 0.26.1 → 0.27.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.
@@ -56,7 +56,9 @@ function createIdPicker(ids, childNodeIds) {
56
56
  const nodeIds = new Set();
57
57
  for (const language of Object.keys(ids)) {
58
58
  const found = getNodeIdsByLanguageAndType(language, typeName);
59
- found.forEach((id) => nodeIds.add(id));
59
+ for (const id of found) {
60
+ nodeIds.add(id);
61
+ }
60
62
  }
61
63
  return Array.from(nodeIds);
62
64
  };
@@ -1,13 +1,14 @@
1
- import markdownToDelta from "markdown-to-quill-delta";
1
+ import { convertMarkdownToTiptap } from "@paroicms/markdown-to-tiptap-json";
2
2
  import { getRandomImagePath } from "../lib/images-lib.js";
3
3
  export function createNodeContents(options) {
4
- const { nodeType, count, generatedContents, outputTags, language } = options;
4
+ const { ctx, nodeType, count, generatedContents, outputTags, language } = options;
5
5
  const result = [];
6
6
  for (let i = 0; i < count; ++i) {
7
7
  const generatedContent = generatedContents ? generatedContents[i] : {};
8
8
  if (generatedContent === undefined)
9
9
  break; // The LLM produced less than expected
10
10
  const nodeContent = createNodeContent({
11
+ ctx,
11
12
  nodeType,
12
13
  language,
13
14
  generatedContent,
@@ -18,11 +19,11 @@ export function createNodeContents(options) {
18
19
  return result;
19
20
  }
20
21
  function createNodeContent(options) {
21
- const { nodeType, language, generatedContent, outputTags } = options;
22
+ const { ctx, nodeType, language, generatedContent, outputTags } = options;
22
23
  const fieldTypes = nodeType.fields ?? [];
23
24
  if (nodeType.kind === "document") {
24
25
  const { title, ...generatedFields } = generatedContent;
25
- const fields = toRiFieldSetContent(generatedFields, outputTags, fieldTypes, language);
26
+ const fields = toRiFieldSetContent(ctx, generatedFields, outputTags, fieldTypes, language);
26
27
  const featuredImage = nodeType.withFeaturedImage
27
28
  ? {
28
29
  file: getRandomImagePath(),
@@ -34,16 +35,17 @@ function createNodeContent(options) {
34
35
  fields,
35
36
  };
36
37
  }
37
- const fields = toRiFieldSetContent(generatedContent, outputTags, fieldTypes, language);
38
+ const fields = toRiFieldSetContent(ctx, generatedContent, outputTags, fieldTypes, language);
38
39
  return { fields };
39
40
  }
40
- function toRiFieldSetContent(content, outputTags, fieldTypes, language) {
41
+ function toRiFieldSetContent(ctx, content, outputTags, fieldTypes, language) {
41
42
  const result = {};
42
43
  for (const fieldType of fieldTypes) {
43
44
  const isMarkdown = outputTags.find((tag) => tag.key === fieldType.name)?.format === "markdown";
44
45
  const localized = fieldType.localized;
45
46
  const value = content[fieldType.name];
46
47
  const fieldContent = toRiFieldContent({
48
+ ctx,
47
49
  fieldType,
48
50
  generatedValue: value,
49
51
  localized,
@@ -57,7 +59,7 @@ function toRiFieldSetContent(content, outputTags, fieldTypes, language) {
57
59
  return result;
58
60
  }
59
61
  function toRiFieldContent(options) {
60
- const { fieldType, generatedValue, localized, language, isMarkdown } = options;
62
+ const { ctx, fieldType, generatedValue, localized, language, isMarkdown } = options;
61
63
  if (generatedValue !== undefined && localized) {
62
64
  if (fieldType.dataType === "string") {
63
65
  return {
@@ -66,8 +68,11 @@ function toRiFieldContent(options) {
66
68
  value: generatedValue,
67
69
  };
68
70
  }
69
- if (fieldType.dataType === "json" && fieldType.renderAs === "html" && isMarkdown) {
70
- return toRiQuillDeltaContent(generatedValue);
71
+ if (fieldType.dataType === "json" &&
72
+ fieldType.renderAs === "html" &&
73
+ fieldType.pluginName === "@paroicms/tiptap-editor-plugin" &&
74
+ isMarkdown) {
75
+ return toTiptapRiJsonContent(ctx, generatedValue);
71
76
  }
72
77
  }
73
78
  if (fieldType.storedAs === "mediaHandle") {
@@ -126,18 +131,17 @@ function toRiFieldContent(options) {
126
131
  if (fieldType.storedAs === "labeling")
127
132
  return; // TODO: Generate values for labeling fields
128
133
  }
129
- function toRiQuillDeltaContent(content) {
134
+ function toTiptapRiJsonContent(ctx, content) {
130
135
  return {
131
136
  dataType: "json",
132
137
  localized: true,
133
- value: Object.fromEntries(Object.entries(content).map(([language, text]) => [
134
- language,
135
- {
136
- j: {
137
- ops: markdownToDelta(text),
138
- },
139
- },
140
- ])),
138
+ value: Object.fromEntries(Object.entries(content).map(([language, text]) => {
139
+ const { result, issues } = convertMarkdownToTiptap(text);
140
+ if (issues && issues.length > 0) {
141
+ ctx.logger.warn(`Markdown conversion issues for language ${language}:`, issues);
142
+ }
143
+ return [language, { j: result }];
144
+ })),
141
145
  };
142
146
  }
143
147
  function getDefaultStringValueForField(fieldName) {
@@ -147,18 +151,17 @@ function getDefaultStringValueForField(fieldName) {
147
151
  return JSON.stringify(["0123456789", "9876543210"]);
148
152
  if (fieldName === "updateDateTime")
149
153
  return new Date().toISOString();
150
- return undefined;
154
+ return;
151
155
  }
152
- export function generateLocalizedFooterMention(siteSchema) {
156
+ export function generateLocalizedFooterMention(ctx, siteSchema) {
153
157
  const { languages } = siteSchema;
154
- return Object.fromEntries(languages.map((language) => [
155
- language,
156
- {
157
- j: {
158
- ops: markdownToDelta(getPoweredByLabel(language)),
159
- },
160
- },
161
- ]));
158
+ return Object.fromEntries(languages.map((language) => {
159
+ const { result, issues } = convertMarkdownToTiptap(getPoweredByLabel(language));
160
+ if (issues && issues.length > 0) {
161
+ ctx.logger.warn(`Markdown conversion issues for footer mention (${language}):`, issues);
162
+ }
163
+ return [language, { j: result }];
164
+ }));
162
165
  }
163
166
  function getPoweredByLabel(language) {
164
167
  const translations = {
@@ -54,7 +54,7 @@ export async function updateSiteFields(ctx, _report, options) {
54
54
  {
55
55
  dataType: "json",
56
56
  localized: true,
57
- value: generateLocalizedFooterMention(siteSchema),
57
+ value: generateLocalizedFooterMention(ctx, siteSchema),
58
58
  },
59
59
  ]);
60
60
  }
@@ -63,6 +63,7 @@ export async function generateMultipleFieldSetContents(ctx, options, report) {
63
63
  }
64
64
  report.add(count, output?.llmReport);
65
65
  return createNodeContents({
66
+ ctx,
66
67
  nodeType,
67
68
  count,
68
69
  generatedContents: output?.contents,
@@ -164,11 +164,11 @@ function fixSiteSchema(siteSchema) {
164
164
  for (const nodeType of siteSchema.nodeTypes ?? []) {
165
165
  if (!nodeType.fields)
166
166
  continue;
167
- // Add Quill plugin to fields with renderAs "html"
167
+ // Add Tiptap plugin to fields with renderAs "html"
168
168
  for (const field of nodeType.fields) {
169
169
  if (typeof field !== "string" && field.renderAs === "html") {
170
170
  field.dataType = "json";
171
- field.plugin = "@paroicms/quill-editor-plugin";
171
+ field.plugin = "@paroicms/tiptap-editor-plugin";
172
172
  }
173
173
  }
174
174
  // Move labeling fields to the beginning of the fields array
@@ -159,20 +159,19 @@ function templateOfRegularDocumentCards(ctx, parentType, parentIdKeyProvider, {
159
159
  function templateOfFields(ctx, fields, { parentKey }) {
160
160
  if (!fields || fields.length === 0)
161
161
  return;
162
- const fieldTemplates = fields.map((fieldOrName) => templateOfField(ctx, fieldOrName, parentKey));
162
+ const fieldTemplates = fields.map((fieldOrQualifiedName) => templateOfField(ctx, fieldOrQualifiedName, parentKey));
163
163
  return fieldTemplates.join("\n");
164
164
  }
165
- function templateOfField(ctx, fieldOrName, parentKey) {
166
- const fieldName = typeof fieldOrName === "string" ? fieldOrName : fieldOrName.name;
167
- const { dataType, renderAs } = typeof fieldOrName === "string"
168
- ? getPredefinedDataType(ctx, fieldName)
169
- : fieldOrName.storedAs === "labeling"
170
- ? { dataType: "labeling" }
171
- : fieldOrName;
165
+ function templateOfField(ctx, fieldOrQualifiedName, parentKey) {
166
+ const { dataType, renderAs, name } = typeof fieldOrQualifiedName === "string"
167
+ ? getPredefinedDataType(ctx, fieldOrQualifiedName)
168
+ : fieldOrQualifiedName.storedAs === "labeling"
169
+ ? { dataType: "labeling", renderAs: undefined, name: fieldOrQualifiedName.name }
170
+ : fieldOrQualifiedName;
172
171
  if (dataType === "labeling") {
173
- return `{% if ${parentKey}.${fieldName} %}
172
+ return `{% if ${parentKey}.${name} %}
174
173
  <div class="Field">
175
- {% for tag in ${parentKey}.${fieldName} %}
174
+ {% for tag in ${parentKey}.${name} %}
176
175
  {% if tag.inRightLanguage %}
177
176
  <a class="Label" href="{{ tag.url }}">{{ tag.title }}</a>
178
177
  {% else %}
@@ -183,20 +182,20 @@ function templateOfField(ctx, fieldOrName, parentKey) {
183
182
  {% endif %}`;
184
183
  }
185
184
  if (renderAs === "html") {
186
- return `<div class="Field Text">{{ ${parentKey}.${fieldName} | raw }}</div>`;
185
+ return `<div class="Field Text">{{ ${parentKey}.${name} | raw }}</div>`;
187
186
  }
188
187
  if (dataType === "date") {
189
- return `<div class="Field">{{ ${parentKey}.${fieldName} | formatDate: "short" }}</div>`;
188
+ return `<div class="Field">{{ ${parentKey}.${name} | formatDate: "short" }}</div>`;
190
189
  }
191
190
  if (dataType === "dateTime") {
192
- return `<div class="Field">{{ ${parentKey}.${fieldName} | formatDate: "long" }}</div>`;
191
+ return `<div class="Field">{{ ${parentKey}.${name} | formatDate: "long" }}</div>`;
193
192
  }
194
193
  if (dataType === "json") {
195
- return `<div class="Field">{{ ${parentKey}.${fieldName} | json }}</div>`;
194
+ return `<div class="Field">{{ ${parentKey}.${name} | json }}</div>`;
196
195
  }
197
196
  if (dataType === "gallery") {
198
197
  return `<div class="Field">
199
- {% for media in ${parentKey}.${fieldName} %}
198
+ {% for media in ${parentKey}.${name} %}
200
199
  {% set im = image(media, resize: "150x150") %}
201
200
  <img
202
201
  class="Field-img"
@@ -211,7 +210,7 @@ function templateOfField(ctx, fieldOrName, parentKey) {
211
210
  </div>`;
212
211
  }
213
212
  if (dataType === "media") {
214
- const mediaKey = `${parentKey}.${fieldName}`;
213
+ const mediaKey = `${parentKey}.${name}`;
215
214
  return `{% set im = image(${mediaKey}, resize: "x250x") %}
216
215
  <div class="Field">
217
216
  <img
@@ -225,10 +224,10 @@ function templateOfField(ctx, fieldOrName, parentKey) {
225
224
  >
226
225
  </div>`;
227
226
  }
228
- if (fieldName === "title") {
229
- return `<h2>{{ ${parentKey}.${fieldName} }}</h2>`;
227
+ if (name === "title") {
228
+ return `<h2>{{ ${parentKey}.${name} }}</h2>`;
230
229
  }
231
- return `<div class="Field">{{ ${parentKey}.${fieldName} }}</div>`;
230
+ return `<div class="Field">{{ ${parentKey}.${name} }}</div>`;
232
231
  }
233
232
  function templateOfPicture({ imageKey }) {
234
233
  return `{% if ${imageKey} %}
@@ -13,7 +13,7 @@ export function templatesOfLabeledList(ctx, documentType) {
13
13
  sameClusterAs: labeledDocument.taxonomyType,
14
14
  });
15
15
  if (!parentRoutingKey)
16
- return;
16
+ return undefined;
17
17
  const labeledListKey = `labeled${upperCaseFirstLetter(labeledDocument.parentDocumentType.typeName)}`;
18
18
  const cardTemplateName = `${camelToKebabCase(labeledDocument.documentType.typeName)}-card.public`;
19
19
  addLiquidFile("partials", `${cardTemplateName}.liquid`, templateOfDocumentCard(ctx, "doc", { parentType: labeledDocument.parentDocumentType }), { skipIfExists: true });
@@ -81,8 +81,8 @@ function getPackageJsonContent(options) {
81
81
  "@paroicms/internal-link-plugin": "*",
82
82
  "@paroicms/list-field-plugin": "*",
83
83
  "@paroicms/public-menu-plugin": "*",
84
- "@paroicms/quill-editor-plugin": "*",
85
- "@paroicms/video-plugin": "*",
84
+ "@paroicms/tiptap-editor-plugin": "*",
85
+ "@paroicms/platform-video-plugin": "*",
86
86
  "@paroicms/server": "*",
87
87
  },
88
88
  devDependencies: {
@@ -8,13 +8,15 @@ export function indent(template, level, { skipFirst = false } = {}) {
8
8
  // For firstLine true, indent all lines.
9
9
  return template.replace(/^/gm, indentStr);
10
10
  }
11
- export function getPredefinedDataType(ctx, fieldName) {
12
- const predefinedField = ctx.predefinedFields.get(fieldName);
13
- if (!predefinedField)
14
- return { dataType: "string", renderAs: undefined };
11
+ export function getPredefinedDataType(ctx, qualifiedFieldName) {
12
+ const predefinedField = ctx.predefinedFields.get(qualifiedFieldName);
13
+ if (!predefinedField) {
14
+ throw new Error(`Predefined field not found: "${qualifiedFieldName}"`);
15
+ }
15
16
  return {
16
17
  dataType: predefinedField.dataType,
17
18
  renderAs: predefinedField.renderAs,
19
+ name: predefinedField.fieldName,
18
20
  };
19
21
  }
20
22
  export function localizedLabelTemplate(ctx, label) {
@@ -6,11 +6,16 @@ export function createThemeCreatorContext(siteSchema) {
6
6
  const otherFiles = new Map();
7
7
  const issues = [];
8
8
  const getParentTypes = makeGetParentTypes(siteSchema);
9
+ const predefinedFieldsWithFieldName = getPredefinedFields().map((f) => ({
10
+ ...f,
11
+ fieldName: extractFieldName(f.qualifiedFieldName),
12
+ }));
13
+ const predefinedFields = new Map(predefinedFieldsWithFieldName.map((f) => [f.qualifiedFieldName, f]));
9
14
  return {
10
15
  siteSchema,
11
16
  labeledDocuments: getLabeledDocuments(siteSchema, getParentTypes),
12
17
  getParentTypes,
13
- predefinedFields: new Map(getPredefinedFields().map((f) => [f.fieldName, f])),
18
+ predefinedFields,
14
19
  setLocalizedLabel(label) {
15
20
  for (const language of languages) {
16
21
  const value = label[language];
@@ -107,3 +112,7 @@ function getLabeledDocuments(siteSchema, getParentTypes) {
107
112
  }
108
113
  return result;
109
114
  }
115
+ function extractFieldName(qualifiedFieldName) {
116
+ const bracketIndex = qualifiedFieldName.indexOf("[");
117
+ return bracketIndex === -1 ? qualifiedFieldName : qualifiedFieldName.slice(0, bracketIndex);
118
+ }
@@ -10,8 +10,8 @@ export function createSiteSchemaFromAnalysis(analysis) {
10
10
  "@paroicms/internal-link-plugin",
11
11
  "@paroicms/list-field-plugin",
12
12
  "@paroicms/public-menu-plugin",
13
- "@paroicms/quill-editor-plugin",
14
- "@paroicms/video-plugin",
13
+ "@paroicms/tiptap-editor-plugin",
14
+ "@paroicms/platform-video-plugin",
15
15
  ],
16
16
  nodeTypes: [
17
17
  {
@@ -136,7 +136,9 @@ function walkTree(saNode, parentNode, ctx) {
136
136
  throw new Error(`Unexpected node kind: "${saNode.kind}"`);
137
137
  }
138
138
  }
139
- saNode.children?.forEach((child) => walkTree(child, nodeType, ctx));
139
+ for (const child of saNode.children ?? []) {
140
+ walkTree(child, nodeType, ctx);
141
+ }
140
142
  }
141
143
  function appendAsRoutingChild(options) {
142
144
  const { nodeType, typeName, parentNode } = options;
@@ -172,7 +174,9 @@ function appendAsChildRegularDocument(options, ctx) {
172
174
  parentNode.regularChildrenSorting = saType.temporal ? "publishDate desc" : "title asc";
173
175
  }
174
176
  parentNode.regularChildren.push(typeName);
175
- children?.forEach((child) => walkTree(child, nodeType, ctx));
177
+ for (const child of children ?? []) {
178
+ walkTree(child, nodeType, ctx);
179
+ }
176
180
  }
177
181
  function appendAsChildPart(options, ctx) {
178
182
  const { nodeType, typeName, parentNode, children, listName } = options;
@@ -202,7 +206,9 @@ function appendAsChildPart(options, ctx) {
202
206
  else {
203
207
  throw new Error(`Unexpected kind: "${parentNode.kind}"`);
204
208
  }
205
- children?.forEach((child) => walkTree(child, nodeType, ctx));
209
+ for (const child of children ?? []) {
210
+ walkTree(child, nodeType, ctx);
211
+ }
206
212
  }
207
213
  export function toNodeType(typeName, saType) {
208
214
  if (saType.kind === "part")
@@ -30,21 +30,21 @@ Guidelines for creating the dictionnary YAML:
30
30
  - Documents and parts already contains a _publish date_ and a _draft_ flag.
31
31
  - A document already contains a _title_ and a _featured image_, but a part doesn't. That's why the predefined `title` field can be used on part types when needed.
32
32
  - Default values:
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[@paroicms/quill-editor-plugin]]`.
34
- - Except if there are specific instructions in the website description, here is the default value for the `_site` node type: `["logo", "footerMention[@paroicms/quill-editor-plugin]"]`.
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[@paroicms/tiptap-editor-plugin]]`.
34
+ - Except if there are specific instructions in the website description, here is the default value for the `_site` node type: `["logo", "footerMention[@paroicms/tiptap-editor-plugin]"]`.
35
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.
36
36
  - This task is about predefined fields only. Custom fields will be added in a further step.
37
37
 
38
38
  Here is an example of expected output:
39
39
 
40
40
  <correct_example>
41
- _site: [logo, "footerMention[@paroicms/quill-editor-plugin]"]
42
- home: ["htmlContent[@paroicms/quill-editor-plugin]"]
41
+ _site: [logo, "footerMention[@paroicms/tiptap-editor-plugin]"]
42
+ home: ["htmlContent[@paroicms/tiptap-editor-plugin]"]
43
43
  searchPage: []
44
- contactPage: ["introduction[@paroicms/quill-editor-plugin]"]
45
- regularDocumentExemple1: [gallery, "htmlContent[@paroicms/quill-editor-plugin]"]
46
- partExemple2: [title, image, "htmlContent[@paroicms/quill-editor-plugin]"]
47
- aBlogPostExample: ["leadParagraph[@paroicms/quill-editor-plugin]", "htmlContent[@paroicms/quill-editor-plugin]"]
44
+ contactPage: ["introduction[@paroicms/tiptap-editor-plugin]"]
45
+ regularDocumentExemple1: [gallery, "htmlContent[@paroicms/tiptap-editor-plugin]"]
46
+ partExemple2: [title, image, "htmlContent[@paroicms/tiptap-editor-plugin]"]
47
+ aBlogPostExample: ["leadParagraph[@paroicms/tiptap-editor-plugin]", "htmlContent[@paroicms/tiptap-editor-plugin]"]
48
48
  </correct_example>
49
49
 
50
50
  Provide the YAML result within <yaml_result> tags. Do not write any explanation or commentary outside the tags.
@@ -1,6 +1,6 @@
1
1
  [
2
2
  {
3
- "qualifiedFieldName": "leadParagraph[@paroicms/quill-editor-plugin]",
3
+ "qualifiedFieldName": "leadParagraph[@paroicms/tiptap-editor-plugin]",
4
4
  "localized": true,
5
5
  "dataType": "json",
6
6
  "renderAs": "html",
@@ -8,7 +8,7 @@
8
8
  "description": "Lead paragraph, or \"chapo\", of a post (HTML)"
9
9
  },
10
10
  {
11
- "qualifiedFieldName": "htmlContent[@paroicms/quill-editor-plugin]",
11
+ "qualifiedFieldName": "htmlContent[@paroicms/tiptap-editor-plugin]",
12
12
  "localized": true,
13
13
  "dataType": "json",
14
14
  "renderAs": "html",
@@ -16,7 +16,7 @@
16
16
  "description": "HTML Content"
17
17
  },
18
18
  {
19
- "qualifiedFieldName": "introduction[@paroicms/quill-editor-plugin]",
19
+ "qualifiedFieldName": "introduction[@paroicms/tiptap-editor-plugin]",
20
20
  "localized": true,
21
21
  "dataType": "json",
22
22
  "renderAs": "html",
@@ -24,7 +24,7 @@
24
24
  "description": "A short introduction (HTML)"
25
25
  },
26
26
  {
27
- "qualifiedFieldName": "footerMention[@paroicms/quill-editor-plugin]",
27
+ "qualifiedFieldName": "footerMention[@paroicms/tiptap-editor-plugin]",
28
28
  "dataType": "json",
29
29
  "renderAs": "html",
30
30
  "normalizeTypography": true,
@@ -107,13 +107,13 @@
107
107
  "description": "A list of phone numbers"
108
108
  },
109
109
  {
110
- "qualifiedFieldName": "video[@paroicms/video-plugin]",
110
+ "qualifiedFieldName": "video[@paroicms/platform-video-plugin]",
111
111
  "localized": false,
112
112
  "dataType": "string",
113
113
  "description": "A YouTube video"
114
114
  },
115
115
  {
116
- "qualifiedFieldName": "oneLanguageVideo[@paroicms/video-plugin]",
116
+ "qualifiedFieldName": "oneLanguageVideo[@paroicms/platform-video-plugin]",
117
117
  "localized": true,
118
118
  "dataType": "string",
119
119
  "description": "A YouTube video, but different for each language"
@@ -1,6 +1,6 @@
1
1
  You are tasked with modifying a JSON object based on a given TypeScript type definition and an update message. 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}}
@@ -26,7 +26,7 @@ Here is an example of an object for describing a custom **HTML** field type:
26
26
  "dataType": "json",
27
27
  "renderAs": "html",
28
28
  "useAsExcerpt": 1,
29
- "plugin": "@paroicms/quill-editor-plugin"
29
+ "plugin": "@paroicms/tiptap-editor-plugin"
30
30
  }
31
31
  </field_type_example>
32
32
 
@@ -89,7 +89,7 @@ A labeling field lets the user assign taxonomy terms to a document (or part).
89
89
 
90
90
  Most of the time, the field name will be the same as the taxonomy type name.
91
91
 
92
- # 4. Examine the current JSON data, which conforms to the `JtSiteSchema` type:
92
+ # 4. Examine the current JSON data, which conforms to the `JtSiteSchema` type
93
93
 
94
94
  <site_schema_json>
95
95
  {{siteSchemaJson}}
@@ -101,7 +101,7 @@ Also, the attached locales:
101
101
  {{l10nJson}}
102
102
  </l10n_json>
103
103
 
104
- # 5. Now, here is what to do:
104
+ # 5. Now, here is what to do
105
105
 
106
106
  <user_request>
107
107
  {{taskDetailsMd}}