fastmode-mcp 1.0.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.
- package/README.md +561 -0
- package/bin/run.js +50 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +802 -0
- package/dist/lib/api-client.d.ts +81 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +237 -0
- package/dist/lib/auth-state.d.ts +13 -0
- package/dist/lib/auth-state.d.ts.map +1 -0
- package/dist/lib/auth-state.js +24 -0
- package/dist/lib/context-fetcher.d.ts +67 -0
- package/dist/lib/context-fetcher.d.ts.map +1 -0
- package/dist/lib/context-fetcher.js +190 -0
- package/dist/lib/credentials.d.ts +52 -0
- package/dist/lib/credentials.d.ts.map +1 -0
- package/dist/lib/credentials.js +196 -0
- package/dist/lib/device-flow.d.ts +14 -0
- package/dist/lib/device-flow.d.ts.map +1 -0
- package/dist/lib/device-flow.js +244 -0
- package/dist/tools/cms-items.d.ts +56 -0
- package/dist/tools/cms-items.d.ts.map +1 -0
- package/dist/tools/cms-items.js +376 -0
- package/dist/tools/create-site.d.ts +9 -0
- package/dist/tools/create-site.d.ts.map +1 -0
- package/dist/tools/create-site.js +202 -0
- package/dist/tools/deploy-package.d.ts +9 -0
- package/dist/tools/deploy-package.d.ts.map +1 -0
- package/dist/tools/deploy-package.js +434 -0
- package/dist/tools/generate-samples.d.ts +19 -0
- package/dist/tools/generate-samples.d.ts.map +1 -0
- package/dist/tools/generate-samples.js +272 -0
- package/dist/tools/get-conversion-guide.d.ts +7 -0
- package/dist/tools/get-conversion-guide.d.ts.map +1 -0
- package/dist/tools/get-conversion-guide.js +1323 -0
- package/dist/tools/get-example.d.ts +7 -0
- package/dist/tools/get-example.d.ts.map +1 -0
- package/dist/tools/get-example.js +1568 -0
- package/dist/tools/get-field-types.d.ts +30 -0
- package/dist/tools/get-field-types.d.ts.map +1 -0
- package/dist/tools/get-field-types.js +154 -0
- package/dist/tools/get-schema.d.ts +5 -0
- package/dist/tools/get-schema.d.ts.map +1 -0
- package/dist/tools/get-schema.js +320 -0
- package/dist/tools/get-started.d.ts +21 -0
- package/dist/tools/get-started.d.ts.map +1 -0
- package/dist/tools/get-started.js +624 -0
- package/dist/tools/get-tenant-schema.d.ts +18 -0
- package/dist/tools/get-tenant-schema.d.ts.map +1 -0
- package/dist/tools/get-tenant-schema.js +158 -0
- package/dist/tools/list-projects.d.ts +5 -0
- package/dist/tools/list-projects.d.ts.map +1 -0
- package/dist/tools/list-projects.js +101 -0
- package/dist/tools/sync-schema.d.ts +41 -0
- package/dist/tools/sync-schema.d.ts.map +1 -0
- package/dist/tools/sync-schema.js +483 -0
- package/dist/tools/validate-manifest.d.ts +5 -0
- package/dist/tools/validate-manifest.d.ts.map +1 -0
- package/dist/tools/validate-manifest.js +311 -0
- package/dist/tools/validate-package.d.ts +5 -0
- package/dist/tools/validate-package.d.ts.map +1 -0
- package/dist/tools/validate-package.js +337 -0
- package/dist/tools/validate-template.d.ts +12 -0
- package/dist/tools/validate-template.d.ts.map +1 -0
- package/dist/tools/validate-template.js +790 -0
- package/package.json +54 -0
- package/scripts/postinstall.js +129 -0
|
@@ -0,0 +1,1568 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getExample = getExample;
|
|
4
|
+
const EXAMPLES = {
|
|
5
|
+
manifest_basic: `# Basic manifest.json
|
|
6
|
+
|
|
7
|
+
A minimal manifest with static pages only:
|
|
8
|
+
|
|
9
|
+
\`\`\`json
|
|
10
|
+
{
|
|
11
|
+
"pages": [
|
|
12
|
+
{
|
|
13
|
+
"path": "/",
|
|
14
|
+
"file": "pages/index.html",
|
|
15
|
+
"title": "Home",
|
|
16
|
+
"editable": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "/about",
|
|
20
|
+
"file": "pages/about.html",
|
|
21
|
+
"title": "About Us",
|
|
22
|
+
"editable": true
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "/contact",
|
|
26
|
+
"file": "pages/contact.html",
|
|
27
|
+
"title": "Contact",
|
|
28
|
+
"editable": true
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"defaultHeadHtml": "<link href='https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap' rel='stylesheet'>"
|
|
32
|
+
}
|
|
33
|
+
\`\`\`
|
|
34
|
+
|
|
35
|
+
**Key points:**
|
|
36
|
+
- Each page has a unique \`path\` (the URL)
|
|
37
|
+
- \`file\` points to the HTML file in pages/ folder
|
|
38
|
+
- \`editable: true\` allows inline editing in the CMS
|
|
39
|
+
- \`defaultHeadHtml\` is injected into all pages`,
|
|
40
|
+
manifest_custom_paths: `# manifest.json with CMS Collections
|
|
41
|
+
|
|
42
|
+
Configure templates for your CMS collections:
|
|
43
|
+
|
|
44
|
+
\`\`\`json
|
|
45
|
+
{
|
|
46
|
+
"pages": [
|
|
47
|
+
{
|
|
48
|
+
"path": "/",
|
|
49
|
+
"file": "pages/index.html",
|
|
50
|
+
"title": "Home",
|
|
51
|
+
"editable": true
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"path": "/about",
|
|
55
|
+
"file": "pages/about.html",
|
|
56
|
+
"title": "About",
|
|
57
|
+
"editable": true
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"cmsTemplates": {
|
|
61
|
+
"postsIndex": "templates/posts_index.html",
|
|
62
|
+
"postsIndexPath": "/blog",
|
|
63
|
+
"postsDetail": "templates/posts_detail.html",
|
|
64
|
+
"postsDetailPath": "/blog",
|
|
65
|
+
"teamIndex": "templates/team.html",
|
|
66
|
+
"teamIndexPath": "/team",
|
|
67
|
+
"servicesIndex": "templates/services.html",
|
|
68
|
+
"servicesIndexPath": "/services",
|
|
69
|
+
"servicesDetail": "templates/service-detail.html",
|
|
70
|
+
"servicesDetailPath": "/services"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
\`\`\`
|
|
74
|
+
|
|
75
|
+
**Path Configuration Pattern:**
|
|
76
|
+
- \`{collectionSlug}Index\` - Template file for listing page
|
|
77
|
+
- \`{collectionSlug}IndexPath\` - URL for listing page
|
|
78
|
+
- \`{collectionSlug}Detail\` - Template file for detail page
|
|
79
|
+
- \`{collectionSlug}DetailPath\` - URL base for detail pages`,
|
|
80
|
+
manifest_minimal_with_ui: `# Minimal manifest.json (Configure Templates via Settings UI)
|
|
81
|
+
|
|
82
|
+
When you want to upload pages first and configure CMS templates later via the Settings UI:
|
|
83
|
+
|
|
84
|
+
\`\`\`json
|
|
85
|
+
{
|
|
86
|
+
"pages": [
|
|
87
|
+
{
|
|
88
|
+
"path": "/",
|
|
89
|
+
"file": "pages/index.html",
|
|
90
|
+
"title": "Home",
|
|
91
|
+
"editable": true
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"path": "/about",
|
|
95
|
+
"file": "pages/about.html",
|
|
96
|
+
"title": "About Us",
|
|
97
|
+
"editable": true
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"path": "/blog",
|
|
101
|
+
"file": "pages/blog.html",
|
|
102
|
+
"title": "Blog",
|
|
103
|
+
"editable": true
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"path": "/blog-post",
|
|
107
|
+
"file": "pages/blog-post.html",
|
|
108
|
+
"title": "Blog Post Template",
|
|
109
|
+
"editable": true
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
\`\`\`
|
|
114
|
+
|
|
115
|
+
**After uploading this package:**
|
|
116
|
+
1. Go to Dashboard → Settings → CMS Templates
|
|
117
|
+
2. Configure template mappings for your collections
|
|
118
|
+
3. Set URL paths
|
|
119
|
+
|
|
120
|
+
**Benefits of this approach:**
|
|
121
|
+
- Faster initial upload - no need to configure cmsTemplates in JSON
|
|
122
|
+
- Visual UI makes it easier to understand the configuration
|
|
123
|
+
- Can change template mappings without re-uploading the package`,
|
|
124
|
+
blog_index_template: `# Blog/Posts Index Template
|
|
125
|
+
|
|
126
|
+
Lists all posts with featured section (using a "posts" collection):
|
|
127
|
+
|
|
128
|
+
\`\`\`html
|
|
129
|
+
<main class="blog-page">
|
|
130
|
+
<header class="page-header">
|
|
131
|
+
<h1 data-edit-key="blog-page-title">Our Blog</h1>
|
|
132
|
+
<p data-edit-key="blog-page-intro">Latest articles and insights</p>
|
|
133
|
+
</header>
|
|
134
|
+
|
|
135
|
+
<!-- Featured Post (first featured article) -->
|
|
136
|
+
{{#each posts featured=true limit=1}}
|
|
137
|
+
<article class="featured-post">
|
|
138
|
+
{{#if image}}
|
|
139
|
+
<img src="{{image}}" alt="{{name}}" class="featured-image">
|
|
140
|
+
{{/if}}
|
|
141
|
+
<div class="featured-content">
|
|
142
|
+
<h2><a href="{{url}}">{{name}}</a></h2>
|
|
143
|
+
<p class="excerpt">{{summary}}</p>
|
|
144
|
+
<div class="meta">
|
|
145
|
+
{{#if author}}
|
|
146
|
+
<span class="author">By {{author.name}}</span>
|
|
147
|
+
{{/if}}
|
|
148
|
+
<time>{{publishedAt}}</time>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</article>
|
|
152
|
+
{{/each}}
|
|
153
|
+
|
|
154
|
+
<!-- All Posts Grid -->
|
|
155
|
+
<div class="posts-grid">
|
|
156
|
+
{{#each posts limit=12 sort="publishedAt" order="desc"}}
|
|
157
|
+
<article class="post-card">
|
|
158
|
+
{{#if thumbnail}}
|
|
159
|
+
<img src="{{thumbnail}}" alt="{{name}}">
|
|
160
|
+
{{else}}
|
|
161
|
+
{{#if image}}
|
|
162
|
+
<img src="{{image}}" alt="{{name}}">
|
|
163
|
+
{{/if}}
|
|
164
|
+
{{/if}}
|
|
165
|
+
<div class="card-content">
|
|
166
|
+
<h3><a href="{{url}}">{{name}}</a></h3>
|
|
167
|
+
<p>{{summary}}</p>
|
|
168
|
+
{{#if author}}
|
|
169
|
+
<span class="byline">{{author.name}}</span>
|
|
170
|
+
{{/if}}
|
|
171
|
+
</div>
|
|
172
|
+
</article>
|
|
173
|
+
{{/each}}
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{{#unless posts}}
|
|
177
|
+
<p>No posts yet. Check back soon!</p>
|
|
178
|
+
{{/unless}}
|
|
179
|
+
</main>
|
|
180
|
+
\`\`\`
|
|
181
|
+
|
|
182
|
+
**Key patterns:**
|
|
183
|
+
- \`{{#each posts featured=true limit=1}}\` for featured post
|
|
184
|
+
- \`{{#each posts limit=12 sort="publishedAt" order="desc"}}\` for post grid
|
|
185
|
+
- Always wrap images in \`{{#if image}}\`
|
|
186
|
+
- Use \`{{url}}\` for links to post detail page`,
|
|
187
|
+
blog_post_template: `# Blog Post Detail Template
|
|
188
|
+
|
|
189
|
+
Single post page with author information (using a "posts" collection with author relation):
|
|
190
|
+
|
|
191
|
+
\`\`\`html
|
|
192
|
+
<article class="blog-post">
|
|
193
|
+
{{#if image}}
|
|
194
|
+
<img src="{{image}}" alt="{{name}}" class="hero-image">
|
|
195
|
+
{{/if}}
|
|
196
|
+
|
|
197
|
+
<header class="post-header">
|
|
198
|
+
<h1>{{name}}</h1>
|
|
199
|
+
|
|
200
|
+
<div class="post-meta">
|
|
201
|
+
{{#if author}}
|
|
202
|
+
<div class="author-info">
|
|
203
|
+
{{#if author.photo}}
|
|
204
|
+
<img src="{{author.photo}}" alt="{{author.name}}" class="author-avatar">
|
|
205
|
+
{{/if}}
|
|
206
|
+
<a href="{{author.url}}" class="author-name">{{author.name}}</a>
|
|
207
|
+
</div>
|
|
208
|
+
{{/if}}
|
|
209
|
+
<time datetime="{{publishedAt}}">{{publishedAt}}</time>
|
|
210
|
+
</div>
|
|
211
|
+
</header>
|
|
212
|
+
|
|
213
|
+
{{#if summary}}
|
|
214
|
+
<p class="lead">{{summary}}</p>
|
|
215
|
+
{{/if}}
|
|
216
|
+
|
|
217
|
+
<div class="post-content">
|
|
218
|
+
{{{body}}}
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
{{#if author}}
|
|
222
|
+
<aside class="author-box">
|
|
223
|
+
{{#if author.photo}}
|
|
224
|
+
<img src="{{author.photo}}" alt="{{author.name}}">
|
|
225
|
+
{{/if}}
|
|
226
|
+
<div class="author-details">
|
|
227
|
+
<h3>About <a href="{{author.url}}">{{author.name}}</a></h3>
|
|
228
|
+
{{{author.bio}}}
|
|
229
|
+
</div>
|
|
230
|
+
</aside>
|
|
231
|
+
{{/if}}
|
|
232
|
+
</article>
|
|
233
|
+
\`\`\`
|
|
234
|
+
|
|
235
|
+
**IMPORTANT:**
|
|
236
|
+
- \`{{{body}}}\` uses TRIPLE braces (contains HTML)
|
|
237
|
+
- \`{{{author.bio}}}\` uses TRIPLE braces (contains HTML)
|
|
238
|
+
- Everything else uses double braces`,
|
|
239
|
+
team_template: `# Team Members Template
|
|
240
|
+
|
|
241
|
+
Using a "team" collection with name, role, photo, bio, order fields:
|
|
242
|
+
|
|
243
|
+
\`\`\`html
|
|
244
|
+
<section class="team-section">
|
|
245
|
+
<header class="section-header">
|
|
246
|
+
<h1 data-edit-key="team-page-title">Our Team</h1>
|
|
247
|
+
<p data-edit-key="team-page-intro">Meet the people behind our success</p>
|
|
248
|
+
</header>
|
|
249
|
+
|
|
250
|
+
<div class="team-grid">
|
|
251
|
+
{{#each team sort="order" order="asc"}}
|
|
252
|
+
<div class="team-member">
|
|
253
|
+
{{#if photo}}
|
|
254
|
+
<img src="{{photo}}" alt="{{name}}" class="member-photo">
|
|
255
|
+
{{/if}}
|
|
256
|
+
<h3>{{name}}</h3>
|
|
257
|
+
{{#if role}}
|
|
258
|
+
<p class="role">{{role}}</p>
|
|
259
|
+
{{/if}}
|
|
260
|
+
{{#if bio}}
|
|
261
|
+
<div class="bio">{{{bio}}}</div>
|
|
262
|
+
{{/if}}
|
|
263
|
+
{{#if email}}
|
|
264
|
+
<a href="mailto:{{email}}" class="email">{{email}}</a>
|
|
265
|
+
{{/if}}
|
|
266
|
+
</div>
|
|
267
|
+
{{/each}}
|
|
268
|
+
</div>
|
|
269
|
+
</section>
|
|
270
|
+
\`\`\`
|
|
271
|
+
|
|
272
|
+
**Key points:**
|
|
273
|
+
- \`sort="order" order="asc"\` maintains manual ordering
|
|
274
|
+
- \`{{{bio}}}\` uses triple braces for HTML content`,
|
|
275
|
+
downloads_template: `# Downloads/Resources Template
|
|
276
|
+
|
|
277
|
+
Using a "downloads" collection with name, description, fileUrl, category fields:
|
|
278
|
+
|
|
279
|
+
\`\`\`html
|
|
280
|
+
<section class="downloads-section">
|
|
281
|
+
<header class="section-header">
|
|
282
|
+
<h1 data-edit-key="downloads-title">Resources</h1>
|
|
283
|
+
<p data-edit-key="downloads-intro">Download our guides and materials</p>
|
|
284
|
+
</header>
|
|
285
|
+
|
|
286
|
+
<div class="downloads-list">
|
|
287
|
+
{{#each downloads sort="order" order="asc"}}
|
|
288
|
+
<div class="download-item">
|
|
289
|
+
<div class="download-info">
|
|
290
|
+
<h3>{{name}}</h3>
|
|
291
|
+
{{#if description}}
|
|
292
|
+
<p>{{description}}</p>
|
|
293
|
+
{{/if}}
|
|
294
|
+
{{#if category}}
|
|
295
|
+
<span class="category">{{category}}</span>
|
|
296
|
+
{{/if}}
|
|
297
|
+
</div>
|
|
298
|
+
<a href="{{fileUrl}}" class="download-btn" download>
|
|
299
|
+
Download
|
|
300
|
+
</a>
|
|
301
|
+
</div>
|
|
302
|
+
{{/each}}
|
|
303
|
+
</div>
|
|
304
|
+
</section>
|
|
305
|
+
\`\`\``,
|
|
306
|
+
authors_template: `# Authors Index Template
|
|
307
|
+
|
|
308
|
+
Using an "authors" collection:
|
|
309
|
+
|
|
310
|
+
\`\`\`html
|
|
311
|
+
<section class="authors-section">
|
|
312
|
+
<header class="section-header">
|
|
313
|
+
<h1 data-edit-key="authors-title">Our Authors</h1>
|
|
314
|
+
</header>
|
|
315
|
+
|
|
316
|
+
<div class="authors-grid">
|
|
317
|
+
{{#each authors}}
|
|
318
|
+
<a href="{{url}}" class="author-card">
|
|
319
|
+
{{#if photo}}
|
|
320
|
+
<img src="{{photo}}" alt="{{name}}" class="author-photo">
|
|
321
|
+
{{/if}}
|
|
322
|
+
<h3>{{name}}</h3>
|
|
323
|
+
{{#if bio}}
|
|
324
|
+
<p class="bio-preview">{{{bio}}}</p>
|
|
325
|
+
{{/if}}
|
|
326
|
+
</a>
|
|
327
|
+
{{/each}}
|
|
328
|
+
</div>
|
|
329
|
+
</section>
|
|
330
|
+
\`\`\``,
|
|
331
|
+
author_detail_template: `# Author Detail Template
|
|
332
|
+
|
|
333
|
+
Using an "authors" collection with detail pages:
|
|
334
|
+
|
|
335
|
+
\`\`\`html
|
|
336
|
+
<article class="author-detail">
|
|
337
|
+
<header class="author-header">
|
|
338
|
+
{{#if photo}}
|
|
339
|
+
<img src="{{photo}}" alt="{{name}}" class="author-photo-large">
|
|
340
|
+
{{/if}}
|
|
341
|
+
<h1>{{name}}</h1>
|
|
342
|
+
|
|
343
|
+
<div class="social-links">
|
|
344
|
+
{{#if email}}
|
|
345
|
+
<a href="mailto:{{email}}">Email</a>
|
|
346
|
+
{{/if}}
|
|
347
|
+
{{#if twitter}}
|
|
348
|
+
<a href="{{twitter}}">Twitter</a>
|
|
349
|
+
{{/if}}
|
|
350
|
+
{{#if linkedin}}
|
|
351
|
+
<a href="{{linkedin}}">LinkedIn</a>
|
|
352
|
+
{{/if}}
|
|
353
|
+
</div>
|
|
354
|
+
</header>
|
|
355
|
+
|
|
356
|
+
{{#if bio}}
|
|
357
|
+
<div class="author-bio">{{{bio}}}</div>
|
|
358
|
+
{{/if}}
|
|
359
|
+
|
|
360
|
+
<!-- Show posts by this author (if you have a posts collection with author relation) -->
|
|
361
|
+
<section class="author-articles">
|
|
362
|
+
<h2>Articles by {{name}}</h2>
|
|
363
|
+
<div class="articles-grid">
|
|
364
|
+
{{#each posts}}
|
|
365
|
+
{{#if (eq author.name ../name)}}
|
|
366
|
+
<a href="{{url}}" class="article-card">
|
|
367
|
+
{{#if thumbnail}}
|
|
368
|
+
<img src="{{thumbnail}}" alt="{{name}}">
|
|
369
|
+
{{/if}}
|
|
370
|
+
<h3>{{name}}</h3>
|
|
371
|
+
<p>{{summary}}</p>
|
|
372
|
+
</a>
|
|
373
|
+
{{/if}}
|
|
374
|
+
{{/each}}
|
|
375
|
+
</div>
|
|
376
|
+
</section>
|
|
377
|
+
</article>
|
|
378
|
+
\`\`\`
|
|
379
|
+
|
|
380
|
+
**Pattern:** \`{{#if (eq author.name ../name)}}\` filters posts by this author`,
|
|
381
|
+
custom_collection_template: `# Custom Collection Template
|
|
382
|
+
|
|
383
|
+
For any user-defined collection. All collections have built-in \`name\`, \`slug\`, \`url\`, and date fields.
|
|
384
|
+
|
|
385
|
+
## Index Template
|
|
386
|
+
|
|
387
|
+
\`\`\`html
|
|
388
|
+
<!-- Index Template: templates/products_index.html -->
|
|
389
|
+
<section class="products-section">
|
|
390
|
+
<h1 data-edit-key="products-title">Our Products</h1>
|
|
391
|
+
|
|
392
|
+
<div class="products-grid">
|
|
393
|
+
{{#each products sort="publishedAt" order="desc"}}
|
|
394
|
+
<article class="product-card">
|
|
395
|
+
{{#if image}}
|
|
396
|
+
<img src="{{image}}" alt="{{name}}">
|
|
397
|
+
{{/if}}
|
|
398
|
+
<h3><a href="{{url}}">{{name}}</a></h3>
|
|
399
|
+
{{#if price}}
|
|
400
|
+
<span class="price">\${{price}}</span>
|
|
401
|
+
{{/if}}
|
|
402
|
+
{{#if description}}
|
|
403
|
+
<p>{{description}}</p>
|
|
404
|
+
{{/if}}
|
|
405
|
+
<time class="date">{{publishedAt}}</time>
|
|
406
|
+
</article>
|
|
407
|
+
{{/each}}
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
{{#unless products}}
|
|
411
|
+
<p>No products yet.</p>
|
|
412
|
+
{{/unless}}
|
|
413
|
+
</section>
|
|
414
|
+
\`\`\`
|
|
415
|
+
|
|
416
|
+
## Detail Template
|
|
417
|
+
|
|
418
|
+
\`\`\`html
|
|
419
|
+
<!-- Detail Template: templates/products_detail.html -->
|
|
420
|
+
<article class="product-detail">
|
|
421
|
+
{{#if image}}
|
|
422
|
+
<img src="{{image}}" alt="{{name}}" class="hero-image">
|
|
423
|
+
{{/if}}
|
|
424
|
+
|
|
425
|
+
<h1>{{name}}</h1>
|
|
426
|
+
<time class="date">{{publishedAt}}</time>
|
|
427
|
+
|
|
428
|
+
{{#if price}}
|
|
429
|
+
<span class="price">\${{price}}</span>
|
|
430
|
+
{{/if}}
|
|
431
|
+
|
|
432
|
+
{{#if description}}
|
|
433
|
+
<div class="description">{{{description}}}</div>
|
|
434
|
+
{{/if}}
|
|
435
|
+
</article>
|
|
436
|
+
\`\`\`
|
|
437
|
+
|
|
438
|
+
## Built-in Tokens (Available on ALL items)
|
|
439
|
+
|
|
440
|
+
- \`{{name}}\` - Item name/title
|
|
441
|
+
- \`{{slug}}\` - URL slug
|
|
442
|
+
- \`{{url}}\` - Full URL to detail page
|
|
443
|
+
- \`{{publishedAt}}\` - Publish date
|
|
444
|
+
- \`{{createdAt}}\` - Creation date
|
|
445
|
+
- \`{{updatedAt}}\` - Last modified date
|
|
446
|
+
|
|
447
|
+
## In manifest.json
|
|
448
|
+
|
|
449
|
+
\`\`\`json
|
|
450
|
+
{
|
|
451
|
+
"cmsTemplates": {
|
|
452
|
+
"productsIndex": "templates/products_index.html",
|
|
453
|
+
"productsIndexPath": "/shop",
|
|
454
|
+
"productsDetail": "templates/products_detail.html",
|
|
455
|
+
"productsDetailPath": "/products"
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
\`\`\``,
|
|
459
|
+
form_handling: `# Form Handling
|
|
460
|
+
|
|
461
|
+
Forms are automatically captured by the CMS using the \`data-form\` attribute:
|
|
462
|
+
|
|
463
|
+
\`\`\`html
|
|
464
|
+
<form data-form="contact" class="contact-form">
|
|
465
|
+
<div class="form-group">
|
|
466
|
+
<label for="name">Name</label>
|
|
467
|
+
<input type="text" name="name" id="name" required>
|
|
468
|
+
</div>
|
|
469
|
+
|
|
470
|
+
<div class="form-group">
|
|
471
|
+
<label for="email">Email</label>
|
|
472
|
+
<input type="email" name="email" id="email" required>
|
|
473
|
+
</div>
|
|
474
|
+
|
|
475
|
+
<div class="form-group">
|
|
476
|
+
<label for="message">Message</label>
|
|
477
|
+
<textarea name="message" id="message" required></textarea>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<button type="submit">Send Message</button>
|
|
481
|
+
</form>
|
|
482
|
+
\`\`\`
|
|
483
|
+
|
|
484
|
+
## Form Handler Script
|
|
485
|
+
|
|
486
|
+
Add this script to handle form submissions:
|
|
487
|
+
|
|
488
|
+
\`\`\`javascript
|
|
489
|
+
// Handle all forms with data-form attribute
|
|
490
|
+
document.querySelectorAll('form[data-form]').forEach(form => {
|
|
491
|
+
form.addEventListener('submit', async (e) => {
|
|
492
|
+
e.preventDefault();
|
|
493
|
+
|
|
494
|
+
const submitBtn = form.querySelector('button[type="submit"]');
|
|
495
|
+
const originalText = submitBtn?.textContent || 'Submit';
|
|
496
|
+
|
|
497
|
+
if (submitBtn) {
|
|
498
|
+
submitBtn.disabled = true;
|
|
499
|
+
submitBtn.textContent = 'Sending...';
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
const formName = form.dataset.form || 'general';
|
|
504
|
+
const formData = new FormData(form);
|
|
505
|
+
const data = Object.fromEntries(formData);
|
|
506
|
+
|
|
507
|
+
// Endpoint is /_forms/{formName}
|
|
508
|
+
const response = await fetch('/_forms/' + formName, {
|
|
509
|
+
method: 'POST',
|
|
510
|
+
headers: { 'Content-Type': 'application/json' },
|
|
511
|
+
body: JSON.stringify(data)
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
if (response.ok) {
|
|
515
|
+
// Option 1: Redirect to thank you page
|
|
516
|
+
// window.location.href = '/thank-you';
|
|
517
|
+
|
|
518
|
+
// Option 2: Show success message
|
|
519
|
+
form.reset();
|
|
520
|
+
alert(form.dataset.successMessage || 'Thank you! Your message has been sent.');
|
|
521
|
+
} else {
|
|
522
|
+
throw new Error('Form submission failed');
|
|
523
|
+
}
|
|
524
|
+
} catch (error) {
|
|
525
|
+
console.error('Form error:', error);
|
|
526
|
+
alert('There was an error. Please try again.');
|
|
527
|
+
} finally {
|
|
528
|
+
if (submitBtn) {
|
|
529
|
+
submitBtn.disabled = false;
|
|
530
|
+
submitBtn.textContent = originalText;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
\`\`\`
|
|
536
|
+
|
|
537
|
+
**Key points:**
|
|
538
|
+
- Add \`data-form="formname"\` to identify the form (e.g., \`data-form="contact"\`)
|
|
539
|
+
- Endpoint is \`/_forms/{formName}\` (e.g., \`/_forms/contact\`)
|
|
540
|
+
- All inputs must have \`name\` attributes to be captured
|
|
541
|
+
- Add a submit button for the form to work
|
|
542
|
+
|
|
543
|
+
**Note:** The legacy \`data-form-name\` attribute is deprecated. Use \`data-form\` instead.`,
|
|
544
|
+
asset_paths: `# Asset Path Rules
|
|
545
|
+
|
|
546
|
+
**ALL asset paths must use /public/ prefix:**
|
|
547
|
+
|
|
548
|
+
\`\`\`html
|
|
549
|
+
<!-- CSS -->
|
|
550
|
+
<link rel="stylesheet" href="/public/css/style.css">
|
|
551
|
+
<link rel="stylesheet" href="/public/css/components/header.css">
|
|
552
|
+
|
|
553
|
+
<!-- JavaScript -->
|
|
554
|
+
<script src="/public/js/main.js"></script>
|
|
555
|
+
<script src="/public/js/vendor/swiper.min.js"></script>
|
|
556
|
+
|
|
557
|
+
<!-- Images -->
|
|
558
|
+
<img src="/public/images/logo.png" alt="Logo">
|
|
559
|
+
<img src="/public/images/team/john.jpg" alt="John">
|
|
560
|
+
|
|
561
|
+
<!-- Favicon -->
|
|
562
|
+
<link rel="icon" href="/public/images/favicon.ico">
|
|
563
|
+
\`\`\`
|
|
564
|
+
|
|
565
|
+
**In CSS files:**
|
|
566
|
+
\`\`\`css
|
|
567
|
+
/* Correct */
|
|
568
|
+
background-image: url('/public/images/hero-bg.jpg');
|
|
569
|
+
|
|
570
|
+
/* Wrong - will break */
|
|
571
|
+
background-image: url('../images/hero-bg.jpg');
|
|
572
|
+
background-image: url('images/hero-bg.jpg');
|
|
573
|
+
\`\`\`
|
|
574
|
+
|
|
575
|
+
**Common conversions:**
|
|
576
|
+
- \`href="css/style.css"\` → \`href="/public/css/style.css"\`
|
|
577
|
+
- \`src="../images/logo.png"\` → \`src="/public/images/logo.png"\`
|
|
578
|
+
- \`url('../fonts/custom.woff')\` → \`url('/public/fonts/custom.woff')\``,
|
|
579
|
+
image_handling: `# Image Handling in Templates
|
|
580
|
+
|
|
581
|
+
## Two Types of Images
|
|
582
|
+
|
|
583
|
+
### 1. Static/UI Images (Keep as /public/ paths)
|
|
584
|
+
Logos, icons, decorative backgrounds, UI elements - these are bundled with the site:
|
|
585
|
+
\`\`\`html
|
|
586
|
+
<!-- KEEP these as static paths -->
|
|
587
|
+
<img src="/public/images/logo.png" alt="Company Logo">
|
|
588
|
+
<img src="/public/images/icons/arrow.svg" alt="">
|
|
589
|
+
<link rel="icon" href="/public/images/favicon.ico">
|
|
590
|
+
<div style="background-image: url('/public/images/hero-pattern.svg')">
|
|
591
|
+
\`\`\`
|
|
592
|
+
|
|
593
|
+
### 2. Content Images (Use CMS Tokens)
|
|
594
|
+
Post images, team photos, product images - these are managed through the CMS:
|
|
595
|
+
\`\`\`html
|
|
596
|
+
<!-- USE CMS tokens for content -->
|
|
597
|
+
<img src="{{image}}" alt="{{name}}">
|
|
598
|
+
<img src="{{thumbnail}}" alt="{{name}}">
|
|
599
|
+
<img src="{{photo}}" alt="{{name}}">
|
|
600
|
+
\`\`\`
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
## Always Wrap Content Images in Conditionals
|
|
605
|
+
|
|
606
|
+
\`\`\`html
|
|
607
|
+
{{#if image}}
|
|
608
|
+
<img src="{{image}}" alt="{{name}}" class="hero-image">
|
|
609
|
+
{{/if}}
|
|
610
|
+
\`\`\`
|
|
611
|
+
|
|
612
|
+
Or provide a CSS fallback:
|
|
613
|
+
\`\`\`html
|
|
614
|
+
{{#if image}}
|
|
615
|
+
<img src="{{image}}" alt="{{name}}">
|
|
616
|
+
{{else}}
|
|
617
|
+
<div class="placeholder-image"></div>
|
|
618
|
+
{{/if}}
|
|
619
|
+
\`\`\`
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## Common Mistakes
|
|
624
|
+
|
|
625
|
+
1. **Replacing logos with CMS tokens** - Keep static UI images as \`/public/\` paths
|
|
626
|
+
2. **Hardcoding example content images** - Use \`{{#each}}\` loops with CMS tokens
|
|
627
|
+
3. **Missing conditionals** - Always wrap optional content images in \`{{#if}}\``,
|
|
628
|
+
relation_fields: `# Relation Fields - Linking Collections
|
|
629
|
+
|
|
630
|
+
Relation fields let you link items from one collection to another.
|
|
631
|
+
|
|
632
|
+
---
|
|
633
|
+
|
|
634
|
+
## Using Relation Fields in Templates
|
|
635
|
+
|
|
636
|
+
When a relation field is set, access the related item's data using dot notation:
|
|
637
|
+
|
|
638
|
+
\`\`\`html
|
|
639
|
+
{{#each projects}}
|
|
640
|
+
<article class="project-card">
|
|
641
|
+
<h2><a href="{{url}}">{{name}}</a></h2>
|
|
642
|
+
|
|
643
|
+
{{#if category}}
|
|
644
|
+
<!-- Access any field from the related category -->
|
|
645
|
+
<span class="category-badge">{{category.name}}</span>
|
|
646
|
+
<a href="{{category.url}}" class="category-link">
|
|
647
|
+
View all {{category.name}} projects
|
|
648
|
+
</a>
|
|
649
|
+
{{/if}}
|
|
650
|
+
</article>
|
|
651
|
+
{{/each}}
|
|
652
|
+
\`\`\`
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## Available Relation Tokens
|
|
657
|
+
|
|
658
|
+
| Token | Description |
|
|
659
|
+
|-------|-------------|
|
|
660
|
+
| \`{{relationField.name}}\` | Related item's name |
|
|
661
|
+
| \`{{relationField.slug}}\` | Related item's URL slug |
|
|
662
|
+
| \`{{relationField.url}}\` | Full URL to related item's detail page |
|
|
663
|
+
| \`{{relationField.anyField}}\` | Any field from the related collection |
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
## Filter by Relation on Detail Pages
|
|
668
|
+
|
|
669
|
+
On a category detail page, show only items with that category:
|
|
670
|
+
|
|
671
|
+
\`\`\`html
|
|
672
|
+
<h1>{{name}}</h1>
|
|
673
|
+
<p>Projects in this category:</p>
|
|
674
|
+
|
|
675
|
+
<div class="projects-grid">
|
|
676
|
+
{{#each projects}}
|
|
677
|
+
{{#if (eq category.slug ../slug)}}
|
|
678
|
+
<article class="project-card">
|
|
679
|
+
<h3>{{name}}</h3>
|
|
680
|
+
<a href="{{url}}">View Project</a>
|
|
681
|
+
</article>
|
|
682
|
+
{{/if}}
|
|
683
|
+
{{/each}}
|
|
684
|
+
</div>
|
|
685
|
+
\`\`\`
|
|
686
|
+
|
|
687
|
+
---
|
|
688
|
+
|
|
689
|
+
## Best Practices
|
|
690
|
+
|
|
691
|
+
1. **Always wrap in {{#if}}** - The relation may not be set for all items
|
|
692
|
+
2. **Use for categorization** - Tags, categories, content types
|
|
693
|
+
3. **Link related content** - Featured author, related service, parent category`,
|
|
694
|
+
data_edit_keys: `# Inline Editing with data-edit-key
|
|
695
|
+
|
|
696
|
+
Make text editable in the CMS visual editor:
|
|
697
|
+
|
|
698
|
+
\`\`\`html
|
|
699
|
+
<!-- Unique, descriptive keys -->
|
|
700
|
+
<h1 data-edit-key="home-hero-title">Welcome to Our Company</h1>
|
|
701
|
+
<p data-edit-key="home-hero-subtitle">We provide excellent services</p>
|
|
702
|
+
|
|
703
|
+
<!-- Hierarchical naming for sections -->
|
|
704
|
+
<section class="about">
|
|
705
|
+
<h2 data-edit-key="about-section-title">About Us</h2>
|
|
706
|
+
<p data-edit-key="about-section-paragraph-1">First paragraph...</p>
|
|
707
|
+
<p data-edit-key="about-section-paragraph-2">Second paragraph...</p>
|
|
708
|
+
</section>
|
|
709
|
+
|
|
710
|
+
<!-- For different pages, prefix with page name -->
|
|
711
|
+
<h1 data-edit-key="contact-page-title">Contact Us</h1>
|
|
712
|
+
<p data-edit-key="contact-intro">Get in touch with our team</p>
|
|
713
|
+
\`\`\`
|
|
714
|
+
|
|
715
|
+
**Naming conventions:**
|
|
716
|
+
- \`{page}-{section}-{element}\`
|
|
717
|
+
- Examples: \`home-hero-title\`, \`about-team-heading\`, \`contact-form-intro\`
|
|
718
|
+
- Must be unique across the entire site
|
|
719
|
+
- Use lowercase with hyphens`,
|
|
720
|
+
each_loop: `# {{#each}} Loop Syntax
|
|
721
|
+
|
|
722
|
+
**Basic loop:**
|
|
723
|
+
\`\`\`html
|
|
724
|
+
{{#each posts}}
|
|
725
|
+
<article>
|
|
726
|
+
<h2>{{name}}</h2>
|
|
727
|
+
<p>{{summary}}</p>
|
|
728
|
+
</article>
|
|
729
|
+
{{/each}}
|
|
730
|
+
\`\`\`
|
|
731
|
+
|
|
732
|
+
**With limit:**
|
|
733
|
+
\`\`\`html
|
|
734
|
+
{{#each posts limit=6}}
|
|
735
|
+
...
|
|
736
|
+
{{/each}}
|
|
737
|
+
\`\`\`
|
|
738
|
+
|
|
739
|
+
**Featured only (if collection has boolean "featured" field):**
|
|
740
|
+
\`\`\`html
|
|
741
|
+
{{#each posts featured=true}}
|
|
742
|
+
...
|
|
743
|
+
{{/each}}
|
|
744
|
+
\`\`\`
|
|
745
|
+
|
|
746
|
+
**Combined:**
|
|
747
|
+
\`\`\`html
|
|
748
|
+
{{#each posts featured=true limit=3}}
|
|
749
|
+
...
|
|
750
|
+
{{/each}}
|
|
751
|
+
\`\`\`
|
|
752
|
+
|
|
753
|
+
**With sorting:**
|
|
754
|
+
\`\`\`html
|
|
755
|
+
{{#each team sort="order" order="asc"}}
|
|
756
|
+
...
|
|
757
|
+
{{/each}}
|
|
758
|
+
\`\`\`
|
|
759
|
+
|
|
760
|
+
**Loop variables:**
|
|
761
|
+
\`\`\`html
|
|
762
|
+
{{#each posts limit=5}}
|
|
763
|
+
<article class="{{#if @first}}featured{{/if}} {{#if @last}}last{{/if}}">
|
|
764
|
+
<span class="position">Item {{@index}}</span>
|
|
765
|
+
<h2>{{name}}</h2>
|
|
766
|
+
</article>
|
|
767
|
+
{{/each}}
|
|
768
|
+
\`\`\`
|
|
769
|
+
|
|
770
|
+
- \`{{@first}}\` - true for first item
|
|
771
|
+
- \`{{@last}}\` - true for last item
|
|
772
|
+
- \`{{@index}}\` - zero-based index (0, 1, 2...)`,
|
|
773
|
+
conditional_if: `# Conditional Rendering
|
|
774
|
+
|
|
775
|
+
**{{#if}} - Render if truthy:**
|
|
776
|
+
\`\`\`html
|
|
777
|
+
{{#if image}}
|
|
778
|
+
<img src="{{image}}" alt="{{name}}">
|
|
779
|
+
{{/if}}
|
|
780
|
+
\`\`\`
|
|
781
|
+
|
|
782
|
+
**{{#if}} with {{else}}:**
|
|
783
|
+
\`\`\`html
|
|
784
|
+
{{#if thumbnail}}
|
|
785
|
+
<img src="{{thumbnail}}" alt="{{name}}">
|
|
786
|
+
{{else}}
|
|
787
|
+
<div class="placeholder">No image</div>
|
|
788
|
+
{{/if}}
|
|
789
|
+
\`\`\`
|
|
790
|
+
|
|
791
|
+
**{{#unless}} - Render if falsy:**
|
|
792
|
+
\`\`\`html
|
|
793
|
+
{{#unless featured}}
|
|
794
|
+
<span class="regular-post">Regular</span>
|
|
795
|
+
{{/unless}}
|
|
796
|
+
\`\`\`
|
|
797
|
+
|
|
798
|
+
**Nested conditionals:**
|
|
799
|
+
\`\`\`html
|
|
800
|
+
{{#if author}}
|
|
801
|
+
<div class="author-info">
|
|
802
|
+
{{#if author.photo}}
|
|
803
|
+
<img src="{{author.photo}}" alt="{{author.name}}">
|
|
804
|
+
{{/if}}
|
|
805
|
+
<span>{{author.name}}</span>
|
|
806
|
+
</div>
|
|
807
|
+
{{/if}}
|
|
808
|
+
\`\`\`
|
|
809
|
+
|
|
810
|
+
**Multiple conditions (check both):**
|
|
811
|
+
\`\`\`html
|
|
812
|
+
{{#if image}}
|
|
813
|
+
{{#if featured}}
|
|
814
|
+
<img src="{{image}}" class="featured-image">
|
|
815
|
+
{{/if}}
|
|
816
|
+
{{/if}}
|
|
817
|
+
\`\`\``,
|
|
818
|
+
nested_fields: `# Nested Field Access (Relation Fields)
|
|
819
|
+
|
|
820
|
+
Access related item fields using dot notation:
|
|
821
|
+
|
|
822
|
+
\`\`\`html
|
|
823
|
+
{{#each posts}}
|
|
824
|
+
<article>
|
|
825
|
+
<h2>{{name}}</h2>
|
|
826
|
+
|
|
827
|
+
{{#if author}}
|
|
828
|
+
<div class="author">
|
|
829
|
+
{{#if author.photo}}
|
|
830
|
+
<img src="{{author.photo}}" alt="{{author.name}}">
|
|
831
|
+
{{/if}}
|
|
832
|
+
<span>By {{author.name}}</span>
|
|
833
|
+
|
|
834
|
+
{{#if author.bio}}
|
|
835
|
+
<p class="bio">{{{author.bio}}}</p>
|
|
836
|
+
{{/if}}
|
|
837
|
+
</div>
|
|
838
|
+
{{/if}}
|
|
839
|
+
</article>
|
|
840
|
+
{{/each}}
|
|
841
|
+
\`\`\`
|
|
842
|
+
|
|
843
|
+
**Available nested tokens (for a relation field named "author"):**
|
|
844
|
+
- \`{{author.name}}\`
|
|
845
|
+
- \`{{author.slug}}\`
|
|
846
|
+
- \`{{author.url}}\`
|
|
847
|
+
- \`{{{author.bio}}}\` (triple braces for richText!)
|
|
848
|
+
- Any other field from the related collection
|
|
849
|
+
|
|
850
|
+
**Always wrap in {{#if author}}** to handle items without the relation set`,
|
|
851
|
+
featured_posts: `# Featured Posts Section
|
|
852
|
+
|
|
853
|
+
**Homepage featured posts (3 most recent):**
|
|
854
|
+
\`\`\`html
|
|
855
|
+
<section class="featured-posts">
|
|
856
|
+
<h2 data-edit-key="home-blog-title">Latest News</h2>
|
|
857
|
+
|
|
858
|
+
{{#each posts featured=true limit=3}}
|
|
859
|
+
<article class="featured-card {{#if @first}}large{{/if}}">
|
|
860
|
+
{{#if image}}
|
|
861
|
+
<img src="{{image}}" alt="{{name}}">
|
|
862
|
+
{{/if}}
|
|
863
|
+
<div class="card-content">
|
|
864
|
+
<h3><a href="{{url}}">{{name}}</a></h3>
|
|
865
|
+
<p>{{summary}}</p>
|
|
866
|
+
{{#if author}}
|
|
867
|
+
<span class="byline">By {{author.name}}</span>
|
|
868
|
+
{{/if}}
|
|
869
|
+
<time>{{publishedAt}}</time>
|
|
870
|
+
</div>
|
|
871
|
+
</article>
|
|
872
|
+
{{/each}}
|
|
873
|
+
|
|
874
|
+
<a href="/blog" class="view-all">View All Posts</a>
|
|
875
|
+
</section>
|
|
876
|
+
\`\`\`
|
|
877
|
+
|
|
878
|
+
**Note:** Requires a boolean "featured" field on your collection`,
|
|
879
|
+
parent_context: `# Parent Context References (\`../\`)
|
|
880
|
+
|
|
881
|
+
Inside loops, access the **parent scope** (the page's current item) using \`../\`:
|
|
882
|
+
|
|
883
|
+
**Use Case: Author Detail Page - Show Only This Author's Posts**
|
|
884
|
+
\`\`\`html
|
|
885
|
+
<article class="author-detail">
|
|
886
|
+
<h1>{{name}}</h1>
|
|
887
|
+
<p class="bio">{{{bio}}}</p>
|
|
888
|
+
|
|
889
|
+
<section class="author-articles">
|
|
890
|
+
<h2>Posts by {{name}}</h2>
|
|
891
|
+
|
|
892
|
+
{{#each posts}}
|
|
893
|
+
{{#if (eq author.name ../name)}}
|
|
894
|
+
<article class="post-card">
|
|
895
|
+
<h3><a href="{{url}}">{{name}}</a></h3>
|
|
896
|
+
<p>{{summary}}</p>
|
|
897
|
+
<time>{{publishedAt}}</time>
|
|
898
|
+
</article>
|
|
899
|
+
{{/if}}
|
|
900
|
+
{{/each}}
|
|
901
|
+
</section>
|
|
902
|
+
</article>
|
|
903
|
+
\`\`\`
|
|
904
|
+
|
|
905
|
+
**How It Works:**
|
|
906
|
+
- Inside \`{{#each posts}}\`, the context is each post
|
|
907
|
+
- \`author.name\` = the post's author
|
|
908
|
+
- \`../name\` = the parent context (the author being displayed on the page)
|
|
909
|
+
- Only posts where author.name matches ../name are shown
|
|
910
|
+
|
|
911
|
+
**Use Case: Category Page - Highlight Current Category**
|
|
912
|
+
\`\`\`html
|
|
913
|
+
<nav class="category-nav">
|
|
914
|
+
{{#each categories}}
|
|
915
|
+
<a href="{{url}}" class="{{#if (eq slug ../slug)}}active{{/if}}">
|
|
916
|
+
{{name}}
|
|
917
|
+
</a>
|
|
918
|
+
{{/each}}
|
|
919
|
+
</nav>
|
|
920
|
+
\`\`\`
|
|
921
|
+
|
|
922
|
+
**Available Parent Fields:**
|
|
923
|
+
- \`../name\` - Parent item's name
|
|
924
|
+
- \`../slug\` - Parent item's slug
|
|
925
|
+
- \`../fieldName\` - Any field from the parent item`,
|
|
926
|
+
equality_comparison: `# Equality Comparisons
|
|
927
|
+
|
|
928
|
+
Compare two values using \`(eq field1 field2)\` helper:
|
|
929
|
+
|
|
930
|
+
## Show When Equal ({{#if (eq ...)}})
|
|
931
|
+
\`\`\`html
|
|
932
|
+
{{#if (eq author.slug ../slug)}}
|
|
933
|
+
<span class="current-author-badge">Your Post</span>
|
|
934
|
+
{{/if}}
|
|
935
|
+
\`\`\`
|
|
936
|
+
|
|
937
|
+
## Show When NOT Equal ({{#unless (eq ...)}}) - IMPORTANT!
|
|
938
|
+
The most common pattern for "Related Posts" or "Other Items" sections:
|
|
939
|
+
|
|
940
|
+
\`\`\`html
|
|
941
|
+
<!-- On a post page, show other posts EXCEPT the current one -->
|
|
942
|
+
<h3>Related Posts</h3>
|
|
943
|
+
{{#each posts limit=3}}
|
|
944
|
+
{{#unless (eq slug ../slug)}}
|
|
945
|
+
<article>
|
|
946
|
+
<a href="{{url}}">{{name}}</a>
|
|
947
|
+
<p>{{summary}}</p>
|
|
948
|
+
</article>
|
|
949
|
+
{{/unless}}
|
|
950
|
+
{{/each}}
|
|
951
|
+
\`\`\`
|
|
952
|
+
|
|
953
|
+
**How it works:**
|
|
954
|
+
- \`../slug\` accesses the current page's slug (parent context)
|
|
955
|
+
- \`slug\` is the loop item's slug
|
|
956
|
+
- \`{{#unless (eq slug ../slug)}}\` shows content when they're NOT equal
|
|
957
|
+
- This excludes the current post from the "related" list
|
|
958
|
+
|
|
959
|
+
## Compare Field to Literal String ({{#eq}})
|
|
960
|
+
\`\`\`html
|
|
961
|
+
{{#eq status "published"}}
|
|
962
|
+
<span class="badge badge-success">Published</span>
|
|
963
|
+
{{/eq}}
|
|
964
|
+
|
|
965
|
+
{{#eq category "news"}}
|
|
966
|
+
<span class="news-icon">News</span>
|
|
967
|
+
{{/eq}}
|
|
968
|
+
\`\`\`
|
|
969
|
+
|
|
970
|
+
## Summary Table
|
|
971
|
+
|
|
972
|
+
| Syntax | Shows content when... |
|
|
973
|
+
|--------|----------------------|
|
|
974
|
+
| \`{{#if (eq a b)}}\` | a equals b |
|
|
975
|
+
| \`{{#unless (eq a b)}}\` | a does NOT equal b |
|
|
976
|
+
| \`{{#eq field "value"}}\` | field equals "value" |`,
|
|
977
|
+
comparison_helpers: `# Comparison Helpers
|
|
978
|
+
|
|
979
|
+
Use comparison helpers for numeric and string comparisons in conditionals.
|
|
980
|
+
|
|
981
|
+
## Available Helpers
|
|
982
|
+
|
|
983
|
+
| Helper | Meaning | Example |
|
|
984
|
+
|--------|---------|---------|
|
|
985
|
+
| \`lt\` | Less than | \`{{#if (lt @index 4)}}\` |
|
|
986
|
+
| \`gt\` | Greater than | \`{{#if (gt @index 0)}}\` |
|
|
987
|
+
| \`lte\` | Less than or equal | \`{{#if (lte @index 4)}}\` |
|
|
988
|
+
| \`gte\` | Greater than or equal | \`{{#if (gte @index 2)}}\` |
|
|
989
|
+
| \`ne\` | Not equal | \`{{#if (ne status "draft")}}\` |
|
|
990
|
+
| \`eq\` | Equal | \`{{#if (eq category.slug ../slug)}}\` |
|
|
991
|
+
|
|
992
|
+
---
|
|
993
|
+
|
|
994
|
+
## Loop Index Comparisons
|
|
995
|
+
|
|
996
|
+
### Show First N Items Differently
|
|
997
|
+
\`\`\`html
|
|
998
|
+
{{#each posts}}
|
|
999
|
+
{{#if (lt @index 3)}}
|
|
1000
|
+
<!-- First 3 items get special styling -->
|
|
1001
|
+
<article class="featured-card">{{name}}</article>
|
|
1002
|
+
{{else}}
|
|
1003
|
+
<!-- Remaining items -->
|
|
1004
|
+
<article class="standard-card">{{name}}</article>
|
|
1005
|
+
{{/if}}
|
|
1006
|
+
{{/each}}
|
|
1007
|
+
\`\`\`
|
|
1008
|
+
|
|
1009
|
+
### Skip First Item
|
|
1010
|
+
\`\`\`html
|
|
1011
|
+
{{#each posts}}
|
|
1012
|
+
{{#if (gt @index 0)}}
|
|
1013
|
+
<article>{{name}}</article>
|
|
1014
|
+
{{/if}}
|
|
1015
|
+
{{/each}}
|
|
1016
|
+
\`\`\`
|
|
1017
|
+
|
|
1018
|
+
### Show Items 2-5 Only
|
|
1019
|
+
\`\`\`html
|
|
1020
|
+
{{#each posts}}
|
|
1021
|
+
{{#if (gte @index 1)}}
|
|
1022
|
+
{{#if (lte @index 4)}}
|
|
1023
|
+
<article>Item {{@index}}: {{name}}</article>
|
|
1024
|
+
{{/if}}
|
|
1025
|
+
{{/if}}
|
|
1026
|
+
{{/each}}
|
|
1027
|
+
\`\`\`
|
|
1028
|
+
|
|
1029
|
+
---
|
|
1030
|
+
|
|
1031
|
+
## Field Value Comparisons
|
|
1032
|
+
|
|
1033
|
+
### Filter by Status
|
|
1034
|
+
\`\`\`html
|
|
1035
|
+
{{#each posts}}
|
|
1036
|
+
{{#if (ne status "draft")}}
|
|
1037
|
+
<article>{{name}}</article>
|
|
1038
|
+
{{/if}}
|
|
1039
|
+
{{/each}}
|
|
1040
|
+
\`\`\`
|
|
1041
|
+
|
|
1042
|
+
### Numeric Field Comparison
|
|
1043
|
+
\`\`\`html
|
|
1044
|
+
{{#each products}}
|
|
1045
|
+
{{#if (gte price 100)}}
|
|
1046
|
+
<span class="premium-badge">Premium</span>
|
|
1047
|
+
{{/if}}
|
|
1048
|
+
{{#if (lt stock 5)}}
|
|
1049
|
+
<span class="low-stock">Low Stock!</span>
|
|
1050
|
+
{{/if}}
|
|
1051
|
+
{{/each}}
|
|
1052
|
+
\`\`\`
|
|
1053
|
+
|
|
1054
|
+
---
|
|
1055
|
+
|
|
1056
|
+
## With {{#unless}}
|
|
1057
|
+
|
|
1058
|
+
The opposite of \`{{#if}}\`:
|
|
1059
|
+
|
|
1060
|
+
\`\`\`html
|
|
1061
|
+
{{#each posts}}
|
|
1062
|
+
{{#unless (lt @index 3)}}
|
|
1063
|
+
<!-- Show for items 4 and beyond -->
|
|
1064
|
+
<article class="archive-card">{{name}}</article>
|
|
1065
|
+
{{/unless}}
|
|
1066
|
+
{{/each}}
|
|
1067
|
+
\`\`\`
|
|
1068
|
+
|
|
1069
|
+
---
|
|
1070
|
+
|
|
1071
|
+
## Common Patterns
|
|
1072
|
+
|
|
1073
|
+
### Hero + Grid Layout
|
|
1074
|
+
\`\`\`html
|
|
1075
|
+
{{#each posts}}
|
|
1076
|
+
{{#if (lt @index 1)}}
|
|
1077
|
+
<!-- First item is hero -->
|
|
1078
|
+
<div class="hero-post">
|
|
1079
|
+
<h1>{{name}}</h1>
|
|
1080
|
+
{{{body}}}
|
|
1081
|
+
</div>
|
|
1082
|
+
{{else}}
|
|
1083
|
+
{{#if (lt @index 4)}}
|
|
1084
|
+
<!-- Items 2-4 in featured grid -->
|
|
1085
|
+
<div class="featured-grid-item">{{name}}</div>
|
|
1086
|
+
{{else}}
|
|
1087
|
+
<!-- Rest in list -->
|
|
1088
|
+
<div class="list-item">{{name}}</div>
|
|
1089
|
+
{{/if}}
|
|
1090
|
+
{{/if}}
|
|
1091
|
+
{{/each}}
|
|
1092
|
+
\`\`\`
|
|
1093
|
+
|
|
1094
|
+
### Pagination Preview
|
|
1095
|
+
\`\`\`html
|
|
1096
|
+
<!-- Show first 6 items, with "view more" after -->
|
|
1097
|
+
{{#each posts}}
|
|
1098
|
+
{{#if (lt @index 6)}}
|
|
1099
|
+
<article>{{name}}</article>
|
|
1100
|
+
{{/if}}
|
|
1101
|
+
{{/each}}
|
|
1102
|
+
{{#if (gt @length 6)}}
|
|
1103
|
+
<a href="/blog">View all posts</a>
|
|
1104
|
+
{{/if}}
|
|
1105
|
+
\`\`\`
|
|
1106
|
+
|
|
1107
|
+
---
|
|
1108
|
+
|
|
1109
|
+
## Summary
|
|
1110
|
+
|
|
1111
|
+
| Syntax | Shows content when... |
|
|
1112
|
+
|--------|----------------------|
|
|
1113
|
+
| \`{{#if (lt @index 4)}}\` | index < 4 (first 4 items) |
|
|
1114
|
+
| \`{{#if (gt @index 0)}}\` | index > 0 (skip first) |
|
|
1115
|
+
| \`{{#if (lte @index 4)}}\` | index <= 4 (first 5 items) |
|
|
1116
|
+
| \`{{#if (gte @index 2)}}\` | index >= 2 (skip first 2) |
|
|
1117
|
+
| \`{{#if (ne field "value")}}\` | field != "value" |
|
|
1118
|
+
| \`{{#unless (lt @index 3)}}\` | index >= 3 (opposite) |`,
|
|
1119
|
+
youtube_embed: `# YouTube Video Embeds
|
|
1120
|
+
|
|
1121
|
+
**IMPORTANT:** YouTube iframes require specific attributes to work correctly. Missing attributes will cause Error 150/153.
|
|
1122
|
+
|
|
1123
|
+
## Correct YouTube Embed Format
|
|
1124
|
+
|
|
1125
|
+
\`\`\`html
|
|
1126
|
+
<iframe
|
|
1127
|
+
src="{{videoUrl}}"
|
|
1128
|
+
title="{{name}}"
|
|
1129
|
+
frameborder="0"
|
|
1130
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
1131
|
+
referrerpolicy="strict-origin-when-cross-origin"
|
|
1132
|
+
allowfullscreen
|
|
1133
|
+
></iframe>
|
|
1134
|
+
\`\`\`
|
|
1135
|
+
|
|
1136
|
+
## Required Attributes
|
|
1137
|
+
|
|
1138
|
+
| Attribute | Required | Why |
|
|
1139
|
+
|-----------|----------|-----|
|
|
1140
|
+
| \`referrerpolicy="strict-origin-when-cross-origin"\` | **YES** | YouTube blocks embeds without this |
|
|
1141
|
+
| \`allowfullscreen\` | Recommended | Enables fullscreen button |
|
|
1142
|
+
| \`allow="..."\` | Recommended | Enables player features |
|
|
1143
|
+
| \`frameborder="0"\` | Recommended | Removes border |
|
|
1144
|
+
| \`title="..."\` | Recommended | Accessibility for screen readers |
|
|
1145
|
+
|
|
1146
|
+
## Video URL Format
|
|
1147
|
+
|
|
1148
|
+
Store YouTube URLs in embed format:
|
|
1149
|
+
\`\`\`
|
|
1150
|
+
https://www.youtube.com/embed/VIDEO_ID
|
|
1151
|
+
\`\`\`
|
|
1152
|
+
|
|
1153
|
+
**NOT** watch format:
|
|
1154
|
+
\`\`\`
|
|
1155
|
+
https://www.youtube.com/watch?v=VIDEO_ID
|
|
1156
|
+
\`\`\`
|
|
1157
|
+
|
|
1158
|
+
## Full Example with Conditional
|
|
1159
|
+
|
|
1160
|
+
\`\`\`html
|
|
1161
|
+
<div class="video-container">
|
|
1162
|
+
{{#if videoUrl}}
|
|
1163
|
+
<iframe
|
|
1164
|
+
src="{{videoUrl}}"
|
|
1165
|
+
title="{{name}}"
|
|
1166
|
+
frameborder="0"
|
|
1167
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
1168
|
+
referrerpolicy="strict-origin-when-cross-origin"
|
|
1169
|
+
allowfullscreen
|
|
1170
|
+
></iframe>
|
|
1171
|
+
{{else}}
|
|
1172
|
+
<div class="video-placeholder">
|
|
1173
|
+
<p>Video coming soon</p>
|
|
1174
|
+
</div>
|
|
1175
|
+
{{/if}}
|
|
1176
|
+
</div>
|
|
1177
|
+
\`\`\``,
|
|
1178
|
+
nested_collection_loop: `# Nested Collection Loops
|
|
1179
|
+
|
|
1180
|
+
For hierarchical content like documentation categories with pages, product categories with products, or authors with their posts.
|
|
1181
|
+
|
|
1182
|
+
**Works on ALL page types:** static pages, index pages, AND detail pages.
|
|
1183
|
+
|
|
1184
|
+
## The Pattern
|
|
1185
|
+
|
|
1186
|
+
\`\`\`html
|
|
1187
|
+
{{#each outer_collection}}
|
|
1188
|
+
<div class="category-section">
|
|
1189
|
+
<h3>{{name}}</h3>
|
|
1190
|
+
<ul>
|
|
1191
|
+
{{#each inner_collection where="relation_field.slug:{{slug}}"}}
|
|
1192
|
+
<li><a href="{{url}}">{{name}}</a></li>
|
|
1193
|
+
{{/each}}
|
|
1194
|
+
</ul>
|
|
1195
|
+
</div>
|
|
1196
|
+
{{/each}}
|
|
1197
|
+
\`\`\`
|
|
1198
|
+
|
|
1199
|
+
**How it works:**
|
|
1200
|
+
- The outer loop iterates through parent items (e.g., categories)
|
|
1201
|
+
- \`{{slug}}\` in the \`where\` clause references the current outer item's slug
|
|
1202
|
+
- The inner loop filters to only items matching that parent
|
|
1203
|
+
|
|
1204
|
+
---
|
|
1205
|
+
|
|
1206
|
+
## Using @root. Prefix
|
|
1207
|
+
|
|
1208
|
+
Use \`@root.\` to explicitly access collections from the root context:
|
|
1209
|
+
|
|
1210
|
+
\`\`\`html
|
|
1211
|
+
{{#each doc_categories}}
|
|
1212
|
+
<h3>{{name}}</h3>
|
|
1213
|
+
{{#each @root.doc_pages where="category.slug:{{slug}}"}}
|
|
1214
|
+
<a href="{{url}}">{{name}}</a>
|
|
1215
|
+
{{/each}}
|
|
1216
|
+
{{/each}}
|
|
1217
|
+
\`\`\`
|
|
1218
|
+
|
|
1219
|
+
Both syntaxes work identically:
|
|
1220
|
+
- \`{{#each doc_pages}}\` - Standard syntax
|
|
1221
|
+
- \`{{#each @root.doc_pages}}\` - Explicit root reference
|
|
1222
|
+
|
|
1223
|
+
---
|
|
1224
|
+
|
|
1225
|
+
## Documentation Sidebar Example
|
|
1226
|
+
|
|
1227
|
+
**Collections needed:**
|
|
1228
|
+
- \`doc_categories\` (name, slug, order)
|
|
1229
|
+
- \`doc_pages\` (name, slug, content, category → relation to doc_categories)
|
|
1230
|
+
|
|
1231
|
+
\`\`\`html
|
|
1232
|
+
<nav class="docs-sidebar">
|
|
1233
|
+
{{#each doc_categories sort="order" order="asc"}}
|
|
1234
|
+
<div class="sidebar-section">
|
|
1235
|
+
<h4 class="section-title">{{name}}</h4>
|
|
1236
|
+
<ul class="section-links">
|
|
1237
|
+
{{#each @root.doc_pages where="category.slug:{{slug}}" sort="order" order="asc"}}
|
|
1238
|
+
<li>
|
|
1239
|
+
<a href="{{url}}" class="doc-link">{{name}}</a>
|
|
1240
|
+
</li>
|
|
1241
|
+
{{/each}}
|
|
1242
|
+
</ul>
|
|
1243
|
+
</div>
|
|
1244
|
+
{{/each}}
|
|
1245
|
+
</nav>
|
|
1246
|
+
\`\`\`
|
|
1247
|
+
|
|
1248
|
+
This sidebar pattern can be included on:
|
|
1249
|
+
- **Static pages** (about, contact, etc.)
|
|
1250
|
+
- **Index pages** (docs listing page)
|
|
1251
|
+
- **Detail pages** (individual doc pages)
|
|
1252
|
+
|
|
1253
|
+
---
|
|
1254
|
+
|
|
1255
|
+
## Blog Categories with Posts Example
|
|
1256
|
+
|
|
1257
|
+
\`\`\`html
|
|
1258
|
+
<section class="categorized-posts">
|
|
1259
|
+
{{#each categories}}
|
|
1260
|
+
<div class="category-group">
|
|
1261
|
+
<h2><a href="{{url}}">{{name}}</a></h2>
|
|
1262
|
+
<div class="posts-grid">
|
|
1263
|
+
{{#each posts where="category.id:{{id}}" limit=3 sort="publishedAt" order="desc"}}
|
|
1264
|
+
<article class="post-card">
|
|
1265
|
+
{{#if image}}
|
|
1266
|
+
<img src="{{image}}" alt="{{name}}">
|
|
1267
|
+
{{/if}}
|
|
1268
|
+
<h3><a href="{{url}}">{{name}}</a></h3>
|
|
1269
|
+
<p>{{summary}}</p>
|
|
1270
|
+
</article>
|
|
1271
|
+
{{/each}}
|
|
1272
|
+
</div>
|
|
1273
|
+
<a href="{{url}}" class="view-all">View all {{name}}</a>
|
|
1274
|
+
</div>
|
|
1275
|
+
{{/each}}
|
|
1276
|
+
</section>
|
|
1277
|
+
\`\`\`
|
|
1278
|
+
|
|
1279
|
+
---
|
|
1280
|
+
|
|
1281
|
+
## Important Notes
|
|
1282
|
+
|
|
1283
|
+
1. **Works on ALL page types** - Static pages, index pages, AND detail pages
|
|
1284
|
+
2. **The \`where\` clause uses parent context** - \`{{slug}}\` or \`{{id}}\` comes from the outer loop's current item
|
|
1285
|
+
3. **@root. is optional** - Use it when you want to be explicit about root context
|
|
1286
|
+
4. **Supports all modifiers** - \`limit\`, \`sort\`, \`order\` work on inner loops
|
|
1287
|
+
5. **Relation field format** - Use \`relation_field.slug:{{slug}}\` or \`relation_field.id:{{id}}\`
|
|
1288
|
+
|
|
1289
|
+
---
|
|
1290
|
+
|
|
1291
|
+
## Manifest Configuration
|
|
1292
|
+
|
|
1293
|
+
\`\`\`json
|
|
1294
|
+
{
|
|
1295
|
+
"cmsTemplates": {
|
|
1296
|
+
"doc_categoriesIndex": "templates/docs_index.html",
|
|
1297
|
+
"doc_categoriesIndexPath": "/docs",
|
|
1298
|
+
"doc_pagesDetail": "templates/doc_page.html",
|
|
1299
|
+
"doc_pagesDetailPath": "/docs"
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
\`\`\`
|
|
1303
|
+
|
|
1304
|
+
This gives you URLs like:
|
|
1305
|
+
- \`/docs\` - Documentation index showing all categories with their pages
|
|
1306
|
+
- \`/docs/quick-start-guide\` - Individual doc page`,
|
|
1307
|
+
loop_variables: `# Loop Variables
|
|
1308
|
+
|
|
1309
|
+
Special variables available inside \`{{#each}}\` loops.
|
|
1310
|
+
|
|
1311
|
+
## Available Variables
|
|
1312
|
+
|
|
1313
|
+
| Variable | Description |
|
|
1314
|
+
|----------|-------------|
|
|
1315
|
+
| \`{{@index}}\` | Zero-based index (0, 1, 2...) |
|
|
1316
|
+
| \`{{@first}}\` | True for first item only |
|
|
1317
|
+
| \`{{@last}}\` | True for last item only |
|
|
1318
|
+
| \`{{@length}}\` | Total number of items |
|
|
1319
|
+
|
|
1320
|
+
---
|
|
1321
|
+
|
|
1322
|
+
## Conditional Usage
|
|
1323
|
+
|
|
1324
|
+
Use loop variables in conditionals for styling:
|
|
1325
|
+
|
|
1326
|
+
\`\`\`html
|
|
1327
|
+
{{#each team_members}}
|
|
1328
|
+
{{#if @first}}
|
|
1329
|
+
<div class="featured-member">{{name}} - Team Lead</div>
|
|
1330
|
+
{{else}}
|
|
1331
|
+
<div class="member">{{name}}</div>
|
|
1332
|
+
{{/if}}
|
|
1333
|
+
{{/each}}
|
|
1334
|
+
\`\`\`
|
|
1335
|
+
|
|
1336
|
+
---
|
|
1337
|
+
|
|
1338
|
+
## Common Patterns
|
|
1339
|
+
|
|
1340
|
+
### Add separator between items (not before first):
|
|
1341
|
+
\`\`\`html
|
|
1342
|
+
{{#each tags}}
|
|
1343
|
+
{{#unless @first}} | {{/unless}}
|
|
1344
|
+
<span>{{name}}</span>
|
|
1345
|
+
{{/each}}
|
|
1346
|
+
\`\`\`
|
|
1347
|
+
Output: \`Tag1 | Tag2 | Tag3\`
|
|
1348
|
+
|
|
1349
|
+
### Comma-separated list (no comma after last):
|
|
1350
|
+
\`\`\`html
|
|
1351
|
+
{{#each authors}}
|
|
1352
|
+
<span>{{name}}</span>{{#unless @last}}, {{/unless}}
|
|
1353
|
+
{{/each}}
|
|
1354
|
+
\`\`\`
|
|
1355
|
+
Output: \`Alice, Bob, Charlie\`
|
|
1356
|
+
|
|
1357
|
+
### Style first and last items:
|
|
1358
|
+
\`\`\`html
|
|
1359
|
+
{{#each posts}}
|
|
1360
|
+
<article class="{{#if @first}}first{{/if}} {{#if @last}}last{{/if}}">
|
|
1361
|
+
{{name}}
|
|
1362
|
+
</article>
|
|
1363
|
+
{{/each}}
|
|
1364
|
+
\`\`\`
|
|
1365
|
+
|
|
1366
|
+
### Show position number:
|
|
1367
|
+
\`\`\`html
|
|
1368
|
+
{{#each leaderboard}}
|
|
1369
|
+
<div class="rank-{{@index}}">
|
|
1370
|
+
#{{@index}}: {{name}} - {{score}} points
|
|
1371
|
+
</div>
|
|
1372
|
+
{{/each}}
|
|
1373
|
+
\`\`\`
|
|
1374
|
+
|
|
1375
|
+
---
|
|
1376
|
+
|
|
1377
|
+
## Important Notes
|
|
1378
|
+
|
|
1379
|
+
- Loop variables **only work inside \`{{#each}}\`** blocks
|
|
1380
|
+
- Using them outside a loop will not work and will log a warning
|
|
1381
|
+
- \`{{#unless @first}}\` is the opposite of \`{{#if @first}}\`
|
|
1382
|
+
- \`{{#unless @last}}\` is the opposite of \`{{#if @last}}\``,
|
|
1383
|
+
common_mistakes: `# Common AI Mistakes - Quick Reference
|
|
1384
|
+
|
|
1385
|
+
This example shows the WRONG and CORRECT patterns side-by-side for quick reference.
|
|
1386
|
+
|
|
1387
|
+
---
|
|
1388
|
+
|
|
1389
|
+
## Manifest Format
|
|
1390
|
+
|
|
1391
|
+
**WRONG:**
|
|
1392
|
+
\`\`\`json
|
|
1393
|
+
{
|
|
1394
|
+
"collections": {
|
|
1395
|
+
"posts": {
|
|
1396
|
+
"indexPath": "/blog",
|
|
1397
|
+
"indexFile": "collections/posts/index.html"
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
\`\`\`
|
|
1402
|
+
|
|
1403
|
+
**CORRECT:**
|
|
1404
|
+
\`\`\`json
|
|
1405
|
+
{
|
|
1406
|
+
"cmsTemplates": {
|
|
1407
|
+
"postsIndex": "templates/posts_index.html",
|
|
1408
|
+
"postsDetail": "templates/posts_detail.html",
|
|
1409
|
+
"postsIndexPath": "/blog",
|
|
1410
|
+
"postsDetailPath": "/blog"
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
\`\`\`
|
|
1414
|
+
|
|
1415
|
+
---
|
|
1416
|
+
|
|
1417
|
+
## Folder Structure
|
|
1418
|
+
|
|
1419
|
+
**WRONG:**
|
|
1420
|
+
\`\`\`
|
|
1421
|
+
assets/css/style.css ← Won't load
|
|
1422
|
+
collections/posts/ ← Wrong folder
|
|
1423
|
+
\`\`\`
|
|
1424
|
+
|
|
1425
|
+
**CORRECT:**
|
|
1426
|
+
\`\`\`
|
|
1427
|
+
public/css/style.css ← Loads at /css/style.css
|
|
1428
|
+
templates/posts_index.html
|
|
1429
|
+
templates/posts_detail.html
|
|
1430
|
+
\`\`\`
|
|
1431
|
+
|
|
1432
|
+
---
|
|
1433
|
+
|
|
1434
|
+
## Template Naming
|
|
1435
|
+
|
|
1436
|
+
**WRONG:**
|
|
1437
|
+
\`\`\`
|
|
1438
|
+
post-detail.html
|
|
1439
|
+
postDetail.html
|
|
1440
|
+
collections/posts/detail.html
|
|
1441
|
+
\`\`\`
|
|
1442
|
+
|
|
1443
|
+
**CORRECT:**
|
|
1444
|
+
\`\`\`
|
|
1445
|
+
posts_detail.html
|
|
1446
|
+
posts_index.html
|
|
1447
|
+
authors_detail.html
|
|
1448
|
+
\`\`\`
|
|
1449
|
+
|
|
1450
|
+
Pattern: \`{collection_slug}_{type}.html\`
|
|
1451
|
+
|
|
1452
|
+
---
|
|
1453
|
+
|
|
1454
|
+
## Inline Editing
|
|
1455
|
+
|
|
1456
|
+
**WRONG (no editing possible):**
|
|
1457
|
+
\`\`\`html
|
|
1458
|
+
<h1>{{headline}}</h1>
|
|
1459
|
+
<p>{{description}}</p>
|
|
1460
|
+
\`\`\`
|
|
1461
|
+
|
|
1462
|
+
**CORRECT (enables visual editing):**
|
|
1463
|
+
\`\`\`html
|
|
1464
|
+
<h1 data-edit-key="headline">{{headline}}</h1>
|
|
1465
|
+
<p data-edit-key="description">{{description}}</p>
|
|
1466
|
+
\`\`\`
|
|
1467
|
+
|
|
1468
|
+
---
|
|
1469
|
+
|
|
1470
|
+
## Links in Templates
|
|
1471
|
+
|
|
1472
|
+
**WRONG (hardcoded path):**
|
|
1473
|
+
\`\`\`html
|
|
1474
|
+
<a href="/posts/{{slug}}">Read more</a>
|
|
1475
|
+
\`\`\`
|
|
1476
|
+
|
|
1477
|
+
**CORRECT (uses manifest path):**
|
|
1478
|
+
\`\`\`html
|
|
1479
|
+
<a href="{{url}}">Read more</a>
|
|
1480
|
+
\`\`\`
|
|
1481
|
+
|
|
1482
|
+
The \`{{url}}\` token automatically uses the path from manifest.json.
|
|
1483
|
+
|
|
1484
|
+
---
|
|
1485
|
+
|
|
1486
|
+
## Asset References in HTML
|
|
1487
|
+
|
|
1488
|
+
**WRONG:**
|
|
1489
|
+
\`\`\`html
|
|
1490
|
+
<link href="/assets/css/style.css">
|
|
1491
|
+
<script src="assets/js/main.js">
|
|
1492
|
+
\`\`\`
|
|
1493
|
+
|
|
1494
|
+
**CORRECT:**
|
|
1495
|
+
\`\`\`html
|
|
1496
|
+
<link href="/css/style.css">
|
|
1497
|
+
<script src="/js/main.js">
|
|
1498
|
+
\`\`\`
|
|
1499
|
+
|
|
1500
|
+
Assets in \`public/\` are served without the \`public/\` prefix in the URL.
|
|
1501
|
+
|
|
1502
|
+
---
|
|
1503
|
+
|
|
1504
|
+
## Field Slugs
|
|
1505
|
+
|
|
1506
|
+
**WRONG (camelCase):**
|
|
1507
|
+
\`\`\`
|
|
1508
|
+
heroImage
|
|
1509
|
+
authorBio
|
|
1510
|
+
publishedDate
|
|
1511
|
+
\`\`\`
|
|
1512
|
+
|
|
1513
|
+
**CORRECT (snake_case):**
|
|
1514
|
+
\`\`\`
|
|
1515
|
+
hero_image
|
|
1516
|
+
author_bio
|
|
1517
|
+
published_date
|
|
1518
|
+
\`\`\`
|
|
1519
|
+
|
|
1520
|
+
---
|
|
1521
|
+
|
|
1522
|
+
## Rich Text Fields
|
|
1523
|
+
|
|
1524
|
+
**WRONG (double braces):**
|
|
1525
|
+
\`\`\`html
|
|
1526
|
+
<div>{{body}}</div> ← HTML will be escaped!
|
|
1527
|
+
\`\`\`
|
|
1528
|
+
|
|
1529
|
+
**CORRECT (triple braces):**
|
|
1530
|
+
\`\`\`html
|
|
1531
|
+
<div>{{{body}}}</div> ← HTML renders correctly
|
|
1532
|
+
\`\`\`
|
|
1533
|
+
|
|
1534
|
+
Use triple braces for rich text fields to render HTML formatting.
|
|
1535
|
+
|
|
1536
|
+
---
|
|
1537
|
+
|
|
1538
|
+
## Complete Example Package Structure
|
|
1539
|
+
|
|
1540
|
+
\`\`\`
|
|
1541
|
+
my-site/
|
|
1542
|
+
├── manifest.json
|
|
1543
|
+
├── pages/
|
|
1544
|
+
│ ├── index.html
|
|
1545
|
+
│ ├── about.html
|
|
1546
|
+
│ └── contact.html
|
|
1547
|
+
├── templates/
|
|
1548
|
+
│ ├── posts_index.html
|
|
1549
|
+
│ ├── posts_detail.html
|
|
1550
|
+
│ ├── authors_index.html
|
|
1551
|
+
│ └── authors_detail.html
|
|
1552
|
+
└── public/
|
|
1553
|
+
├── css/
|
|
1554
|
+
│ └── style.css
|
|
1555
|
+
├── js/
|
|
1556
|
+
│ └── main.js
|
|
1557
|
+
└── images/
|
|
1558
|
+
└── logo.svg
|
|
1559
|
+
\`\`\`
|
|
1560
|
+
|
|
1561
|
+
This structure will work every time!`,
|
|
1562
|
+
};
|
|
1563
|
+
/**
|
|
1564
|
+
* Returns example code for a specific pattern
|
|
1565
|
+
*/
|
|
1566
|
+
async function getExample(exampleType) {
|
|
1567
|
+
return EXAMPLES[exampleType] || `Example not found: ${exampleType}`;
|
|
1568
|
+
}
|