@terrymooreii/sia 2.1.5 → 2.1.6
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/_config.yml +3 -1
- package/docs/README.md +132 -0
- package/docs/creating-themes.md +772 -0
- package/docs/front-matter.md +557 -0
- package/docs/markdown-guide.md +536 -0
- package/docs/template-reference.md +581 -0
- package/lib/assets.js +1 -1
- package/lib/config.js +3 -1
- package/lib/content.js +74 -2
- package/lib/templates.js +3 -2
- package/package.json +1 -1
- package/readme.md +2 -1
- package/themes/developer/includes/hero.njk +6 -0
- package/themes/developer/pages/index.njk +1 -4
- package/themes/magazine/includes/hero.njk +8 -0
- package/themes/magazine/pages/index.njk +4 -9
- package/themes/main/includes/hero.njk +6 -0
- package/themes/main/pages/index.njk +1 -4
- package/themes/minimal/includes/hero.njk +6 -0
- package/themes/minimal/pages/index.njk +2 -5
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
# Template Reference
|
|
2
|
+
|
|
3
|
+
Sia uses [Nunjucks](https://mozilla.github.io/nunjucks/) as its templating engine. This guide covers all available variables and custom filters.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Global Variables](#global-variables)
|
|
8
|
+
- [Page Context Variables](#page-context-variables)
|
|
9
|
+
- [Pagination Variables](#pagination-variables)
|
|
10
|
+
- [Tag Page Variables](#tag-page-variables)
|
|
11
|
+
- [Listing Page Variables](#listing-page-variables)
|
|
12
|
+
- [Custom Filters](#custom-filters)
|
|
13
|
+
- [Nunjucks Basics](#nunjucks-basics)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Global Variables
|
|
18
|
+
|
|
19
|
+
These variables are available in all templates.
|
|
20
|
+
|
|
21
|
+
### `site`
|
|
22
|
+
|
|
23
|
+
Site configuration from `_config.yml`:
|
|
24
|
+
|
|
25
|
+
| Property | Type | Description |
|
|
26
|
+
|----------|------|-------------|
|
|
27
|
+
| `site.title` | string | Site title |
|
|
28
|
+
| `site.description` | string | Site description |
|
|
29
|
+
| `site.url` | string | Full site URL (e.g., `https://example.com`) |
|
|
30
|
+
| `site.author` | string | Default author name |
|
|
31
|
+
| `site.basePath` | string | URL path prefix (extracted from `site.url`) |
|
|
32
|
+
|
|
33
|
+
**Example:**
|
|
34
|
+
|
|
35
|
+
```nunjucks
|
|
36
|
+
<title>{{ site.title }}</title>
|
|
37
|
+
<meta name="description" content="{{ site.description }}">
|
|
38
|
+
<link rel="canonical" href="{{ site.url }}{{ page.url }}">
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### `config`
|
|
42
|
+
|
|
43
|
+
The full configuration object including all settings:
|
|
44
|
+
|
|
45
|
+
| Property | Type | Description |
|
|
46
|
+
|----------|------|-------------|
|
|
47
|
+
| `config.theme` | object | Theme configuration object |
|
|
48
|
+
| `config.theme.name` | string | Current theme name |
|
|
49
|
+
| `config.input` | string | Input directory name |
|
|
50
|
+
| `config.output` | string | Output directory name |
|
|
51
|
+
| `config.collections` | object | Collection configurations |
|
|
52
|
+
| `config.pagination.size` | number | Items per page |
|
|
53
|
+
| `config.server.port` | number | Dev server port |
|
|
54
|
+
| `config.server.showDrafts` | boolean | Show drafts in dev mode |
|
|
55
|
+
|
|
56
|
+
### `collections`
|
|
57
|
+
|
|
58
|
+
Object containing all content collections:
|
|
59
|
+
|
|
60
|
+
| Property | Type | Description |
|
|
61
|
+
|----------|------|-------------|
|
|
62
|
+
| `collections.posts` | array | All blog posts |
|
|
63
|
+
| `collections.pages` | array | All static pages |
|
|
64
|
+
| `collections.notes` | array | All notes |
|
|
65
|
+
|
|
66
|
+
**Example:**
|
|
67
|
+
|
|
68
|
+
```nunjucks
|
|
69
|
+
{% for post in collections.posts | limit(5) %}
|
|
70
|
+
<article>
|
|
71
|
+
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
|
|
72
|
+
<time>{{ post.date | date('long') }}</time>
|
|
73
|
+
</article>
|
|
74
|
+
{% endfor %}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `tags`
|
|
78
|
+
|
|
79
|
+
Object containing tag data, keyed by normalized tag name:
|
|
80
|
+
|
|
81
|
+
```nunjucks
|
|
82
|
+
{% for tagName, tagData in tags %}
|
|
83
|
+
<a href="/tags/{{ tagData.slug }}/">
|
|
84
|
+
{{ tagData.name }} ({{ tagData.count }})
|
|
85
|
+
</a>
|
|
86
|
+
{% endfor %}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Each tag object contains:
|
|
90
|
+
|
|
91
|
+
| Property | Type | Description |
|
|
92
|
+
|----------|------|-------------|
|
|
93
|
+
| `name` | string | Original tag name |
|
|
94
|
+
| `slug` | string | URL-friendly slug |
|
|
95
|
+
| `items` | array | Content items with this tag |
|
|
96
|
+
| `count` | number | Number of items |
|
|
97
|
+
|
|
98
|
+
### `allTags`
|
|
99
|
+
|
|
100
|
+
Array of all tags sorted by count (most used first):
|
|
101
|
+
|
|
102
|
+
```nunjucks
|
|
103
|
+
<ul class="tag-cloud">
|
|
104
|
+
{% for tag in allTags %}
|
|
105
|
+
<li><a href="/tags/{{ tag.slug }}/">{{ tag.name }}</a></li>
|
|
106
|
+
{% endfor %}
|
|
107
|
+
</ul>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Page Context Variables
|
|
113
|
+
|
|
114
|
+
These variables are available when rendering individual content items (posts, pages, notes).
|
|
115
|
+
|
|
116
|
+
### `page`
|
|
117
|
+
|
|
118
|
+
The current content item being rendered:
|
|
119
|
+
|
|
120
|
+
| Property | Type | Description |
|
|
121
|
+
|----------|------|-------------|
|
|
122
|
+
| `page.title` | string | Content title |
|
|
123
|
+
| `page.date` | Date | Publication date |
|
|
124
|
+
| `page.tags` | array | Array of tag strings |
|
|
125
|
+
| `page.content` | string | Rendered HTML content |
|
|
126
|
+
| `page.excerpt` | string | Plain text excerpt |
|
|
127
|
+
| `page.excerptHtml` | string | HTML-rendered excerpt |
|
|
128
|
+
| `page.slug` | string | URL slug |
|
|
129
|
+
| `page.url` | string | Full URL path (with basePath) |
|
|
130
|
+
| `page.draft` | boolean | Draft status |
|
|
131
|
+
| `page.layout` | string | Layout template name |
|
|
132
|
+
| `page.collection` | string | Collection name (`posts`, `pages`, `notes`) |
|
|
133
|
+
| `page.image` | string | Featured image path |
|
|
134
|
+
| `page.author` | string | Author (overrides site author) |
|
|
135
|
+
| `page.description` | string | Meta description |
|
|
136
|
+
| `page.rawContent` | string | Original markdown content |
|
|
137
|
+
| `page.filePath` | string | Source file path |
|
|
138
|
+
| `page.outputPath` | string | Output file path |
|
|
139
|
+
|
|
140
|
+
**Example:**
|
|
141
|
+
|
|
142
|
+
```nunjucks
|
|
143
|
+
<article>
|
|
144
|
+
<h1>{{ page.title }}</h1>
|
|
145
|
+
<time datetime="{{ page.date | date('iso') }}">
|
|
146
|
+
{{ page.date | date('long') }}
|
|
147
|
+
</time>
|
|
148
|
+
|
|
149
|
+
{% if page.tags and page.tags.length %}
|
|
150
|
+
<div class="tags">
|
|
151
|
+
{% for tag in page.tags %}
|
|
152
|
+
<a href="/tags/{{ tag | slug }}/">{{ tag }}</a>
|
|
153
|
+
{% endfor %}
|
|
154
|
+
</div>
|
|
155
|
+
{% endif %}
|
|
156
|
+
|
|
157
|
+
<div class="content">
|
|
158
|
+
{{ content | safe }}
|
|
159
|
+
</div>
|
|
160
|
+
</article>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### `content`
|
|
164
|
+
|
|
165
|
+
The rendered HTML content of the current page. Always use with the `safe` filter:
|
|
166
|
+
|
|
167
|
+
```nunjucks
|
|
168
|
+
<div class="prose">
|
|
169
|
+
{{ content | safe }}
|
|
170
|
+
</div>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### `title`
|
|
174
|
+
|
|
175
|
+
The page title, passed separately for convenience:
|
|
176
|
+
|
|
177
|
+
```nunjucks
|
|
178
|
+
<title>{% if title %}{{ title }} | {% endif %}{{ site.title }}</title>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Pagination Variables
|
|
184
|
+
|
|
185
|
+
Available on paginated listing pages (blog, notes, tags).
|
|
186
|
+
|
|
187
|
+
### `pagination`
|
|
188
|
+
|
|
189
|
+
| Property | Type | Description |
|
|
190
|
+
|----------|------|-------------|
|
|
191
|
+
| `pagination.pageNumber` | number | Current page (1-indexed) |
|
|
192
|
+
| `pagination.totalPages` | number | Total number of pages |
|
|
193
|
+
| `pagination.totalItems` | number | Total items across all pages |
|
|
194
|
+
| `pagination.isFirst` | boolean | Is this the first page? |
|
|
195
|
+
| `pagination.isLast` | boolean | Is this the last page? |
|
|
196
|
+
| `pagination.previousPage` | number/null | Previous page number |
|
|
197
|
+
| `pagination.nextPage` | number/null | Next page number |
|
|
198
|
+
| `pagination.previousUrl` | string/null | URL to previous page |
|
|
199
|
+
| `pagination.nextUrl` | string/null | URL to next page |
|
|
200
|
+
| `pagination.url` | string | Current page URL |
|
|
201
|
+
| `pagination.items` | array | Items on current page |
|
|
202
|
+
| `pagination.startIndex` | number | Start index of items |
|
|
203
|
+
| `pagination.endIndex` | number | End index of items |
|
|
204
|
+
|
|
205
|
+
**Example:**
|
|
206
|
+
|
|
207
|
+
```nunjucks
|
|
208
|
+
{% if pagination and pagination.totalPages > 1 %}
|
|
209
|
+
<nav class="pagination">
|
|
210
|
+
<span>Page {{ pagination.pageNumber }} of {{ pagination.totalPages }}</span>
|
|
211
|
+
|
|
212
|
+
{% if pagination.previousUrl %}
|
|
213
|
+
<a href="{{ pagination.previousUrl }}">← Newer</a>
|
|
214
|
+
{% endif %}
|
|
215
|
+
|
|
216
|
+
{% if pagination.nextUrl %}
|
|
217
|
+
<a href="{{ pagination.nextUrl }}">Older →</a>
|
|
218
|
+
{% endif %}
|
|
219
|
+
</nav>
|
|
220
|
+
{% endif %}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Tag Page Variables
|
|
226
|
+
|
|
227
|
+
Available on individual tag pages (`/tags/:slug/`).
|
|
228
|
+
|
|
229
|
+
### `tag`
|
|
230
|
+
|
|
231
|
+
The current tag being displayed:
|
|
232
|
+
|
|
233
|
+
| Property | Type | Description |
|
|
234
|
+
|----------|------|-------------|
|
|
235
|
+
| `tag.name` | string | Original tag name |
|
|
236
|
+
| `tag.slug` | string | URL-friendly slug |
|
|
237
|
+
| `tag.items` | array | All items with this tag |
|
|
238
|
+
| `tag.count` | number | Number of items |
|
|
239
|
+
|
|
240
|
+
**Example:**
|
|
241
|
+
|
|
242
|
+
```nunjucks
|
|
243
|
+
<h1>Tagged: {{ tag.name }}</h1>
|
|
244
|
+
<p>{{ tag.count }} item{% if tag.count != 1 %}s{% endif %}</p>
|
|
245
|
+
|
|
246
|
+
{% for post in posts %}
|
|
247
|
+
<article>
|
|
248
|
+
<a href="{{ post.url }}">{{ post.title }}</a>
|
|
249
|
+
</article>
|
|
250
|
+
{% endfor %}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Listing Page Variables
|
|
256
|
+
|
|
257
|
+
### Blog Listing (`/blog/`)
|
|
258
|
+
|
|
259
|
+
| Variable | Type | Description |
|
|
260
|
+
|----------|------|-------------|
|
|
261
|
+
| `posts` | array | Posts for current page |
|
|
262
|
+
| `pagination` | object | Pagination data |
|
|
263
|
+
|
|
264
|
+
### Notes Listing (`/notes/`)
|
|
265
|
+
|
|
266
|
+
| Variable | Type | Description |
|
|
267
|
+
|----------|------|-------------|
|
|
268
|
+
| `notes` | array | Notes for current page |
|
|
269
|
+
| `pagination` | object | Pagination data |
|
|
270
|
+
|
|
271
|
+
### Tags Listing (`/tags/`)
|
|
272
|
+
|
|
273
|
+
| Variable | Type | Description |
|
|
274
|
+
|----------|------|-------------|
|
|
275
|
+
| `allTags` | array | All tags sorted by count |
|
|
276
|
+
| `tags` | object | Tag data keyed by slug |
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Custom Filters
|
|
281
|
+
|
|
282
|
+
Sia provides these custom Nunjucks filters:
|
|
283
|
+
|
|
284
|
+
### `date(format)`
|
|
285
|
+
|
|
286
|
+
Format a date. Supports a special `'now'` keyword to get the current date.
|
|
287
|
+
|
|
288
|
+
**Formats:**
|
|
289
|
+
|
|
290
|
+
| Format | Output Example |
|
|
291
|
+
|--------|----------------|
|
|
292
|
+
| `'short'` | Dec 17, 2024 |
|
|
293
|
+
| `'long'` | December 17, 2024 |
|
|
294
|
+
| `'iso'` | 2024-12-17 |
|
|
295
|
+
| `'rss'` | Tue, 17 Dec 2024 00:00:00 GMT |
|
|
296
|
+
| `'year'` | 2024 |
|
|
297
|
+
| `'month'` | December 2024 |
|
|
298
|
+
| `'time'` | 3:45 PM |
|
|
299
|
+
| `'full'` | Tuesday, December 17, 2024 |
|
|
300
|
+
| `'full_time'` | Tue, Dec 17, 2024, 3:45 PM |
|
|
301
|
+
|
|
302
|
+
**Examples:**
|
|
303
|
+
|
|
304
|
+
```nunjucks
|
|
305
|
+
{{ page.date | date('long') }}
|
|
306
|
+
<!-- December 17, 2024 -->
|
|
307
|
+
|
|
308
|
+
{{ page.date | date('iso') }}
|
|
309
|
+
<!-- 2024-12-17 -->
|
|
310
|
+
|
|
311
|
+
<time datetime="{{ page.date | date('iso') }}">
|
|
312
|
+
{{ page.date | date('full') }}
|
|
313
|
+
</time>
|
|
314
|
+
|
|
315
|
+
<!-- Current year -->
|
|
316
|
+
© {{ 'now' | date('year') }}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### `slug`
|
|
320
|
+
|
|
321
|
+
Generate a URL-friendly slug from a string:
|
|
322
|
+
|
|
323
|
+
```nunjucks
|
|
324
|
+
{{ "Hello World!" | slug }}
|
|
325
|
+
<!-- hello-world -->
|
|
326
|
+
|
|
327
|
+
<a href="/tags/{{ tag | slug }}/">{{ tag }}</a>
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### `excerpt(length)`
|
|
331
|
+
|
|
332
|
+
Extract an excerpt from HTML content. Default length is 200 characters.
|
|
333
|
+
|
|
334
|
+
```nunjucks
|
|
335
|
+
{{ post.content | excerpt }}
|
|
336
|
+
<!-- First 200 characters... -->
|
|
337
|
+
|
|
338
|
+
{{ post.content | excerpt(100) }}
|
|
339
|
+
<!-- First 100 characters... -->
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### `limit(count)`
|
|
343
|
+
|
|
344
|
+
Limit an array to the first N items:
|
|
345
|
+
|
|
346
|
+
```nunjucks
|
|
347
|
+
{% for post in collections.posts | limit(5) %}
|
|
348
|
+
<!-- First 5 posts -->
|
|
349
|
+
{% endfor %}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### `skip(count)`
|
|
353
|
+
|
|
354
|
+
Skip the first N items in an array:
|
|
355
|
+
|
|
356
|
+
```nunjucks
|
|
357
|
+
{% for post in collections.posts | skip(3) %}
|
|
358
|
+
<!-- All posts except first 3 -->
|
|
359
|
+
{% endfor %}
|
|
360
|
+
|
|
361
|
+
{% for post in collections.posts | skip(5) | limit(5) %}
|
|
362
|
+
<!-- Posts 6-10 -->
|
|
363
|
+
{% endfor %}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### `wordCount`
|
|
367
|
+
|
|
368
|
+
Get the word count of content:
|
|
369
|
+
|
|
370
|
+
```nunjucks
|
|
371
|
+
{{ post.content | wordCount }} words
|
|
372
|
+
<!-- 1234 words -->
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### `readingTime(wordsPerMinute)`
|
|
376
|
+
|
|
377
|
+
Estimate reading time. Default is 200 words per minute.
|
|
378
|
+
|
|
379
|
+
```nunjucks
|
|
380
|
+
{{ post.content | readingTime }}
|
|
381
|
+
<!-- 5 min read -->
|
|
382
|
+
|
|
383
|
+
{{ post.content | readingTime(250) }}
|
|
384
|
+
<!-- 4 min read -->
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### `groupBy(key)`
|
|
388
|
+
|
|
389
|
+
Group an array of objects by a property:
|
|
390
|
+
|
|
391
|
+
```nunjucks
|
|
392
|
+
{% set postsByYear = collections.posts | groupBy('year') %}
|
|
393
|
+
{% for year, posts in postsByYear %}
|
|
394
|
+
<h2>{{ year }}</h2>
|
|
395
|
+
{% for post in posts %}
|
|
396
|
+
<a href="{{ post.url }}">{{ post.title }}</a>
|
|
397
|
+
{% endfor %}
|
|
398
|
+
{% endfor %}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### `sortBy(key, order)`
|
|
402
|
+
|
|
403
|
+
Sort an array by a property. Order can be `'asc'` (default) or `'desc'`.
|
|
404
|
+
|
|
405
|
+
```nunjucks
|
|
406
|
+
{% for post in collections.posts | sortBy('title', 'asc') %}
|
|
407
|
+
<!-- Posts sorted alphabetically by title -->
|
|
408
|
+
{% endfor %}
|
|
409
|
+
|
|
410
|
+
{% for post in collections.posts | sortBy('date', 'desc') %}
|
|
411
|
+
<!-- Posts sorted newest first -->
|
|
412
|
+
{% endfor %}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### `where(key, value)`
|
|
416
|
+
|
|
417
|
+
Filter items where a property equals a value:
|
|
418
|
+
|
|
419
|
+
```nunjucks
|
|
420
|
+
{% for post in collections.posts | where('draft', false) %}
|
|
421
|
+
<!-- Only non-draft posts -->
|
|
422
|
+
{% endfor %}
|
|
423
|
+
|
|
424
|
+
{% for post in collections.posts | where('author', 'John') %}
|
|
425
|
+
<!-- Only posts by John -->
|
|
426
|
+
{% endfor %}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### `withTag(tag)`
|
|
430
|
+
|
|
431
|
+
Filter items that have a specific tag:
|
|
432
|
+
|
|
433
|
+
```nunjucks
|
|
434
|
+
{% for post in collections.posts | withTag('javascript') %}
|
|
435
|
+
<!-- Only posts tagged with 'javascript' -->
|
|
436
|
+
{% endfor %}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### `json(spaces)`
|
|
440
|
+
|
|
441
|
+
Convert an object to JSON string. Useful for debugging.
|
|
442
|
+
|
|
443
|
+
```nunjucks
|
|
444
|
+
<script>
|
|
445
|
+
const data = {{ page | json | safe }};
|
|
446
|
+
</script>
|
|
447
|
+
|
|
448
|
+
<!-- Pretty printed with 2 spaces -->
|
|
449
|
+
<pre>{{ page | json(2) }}</pre>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### `url`
|
|
453
|
+
|
|
454
|
+
Prepend the site's basePath to a URL. Essential for sites hosted in subdirectories.
|
|
455
|
+
|
|
456
|
+
```nunjucks
|
|
457
|
+
<a href="{{ '/' | url }}">Home</a>
|
|
458
|
+
<link rel="stylesheet" href="{{ '/styles/main.css' | url }}">
|
|
459
|
+
<a href="{{ '/blog/' | url }}">Blog</a>
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
If your site is at `https://example.com/blog/`, the `url` filter ensures paths work correctly.
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Nunjucks Basics
|
|
467
|
+
|
|
468
|
+
### Variables
|
|
469
|
+
|
|
470
|
+
```nunjucks
|
|
471
|
+
{{ variable }}
|
|
472
|
+
{{ object.property }}
|
|
473
|
+
{{ array[0] }}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Conditionals
|
|
477
|
+
|
|
478
|
+
```nunjucks
|
|
479
|
+
{% if condition %}
|
|
480
|
+
<!-- content -->
|
|
481
|
+
{% elif otherCondition %}
|
|
482
|
+
<!-- content -->
|
|
483
|
+
{% else %}
|
|
484
|
+
<!-- content -->
|
|
485
|
+
{% endif %}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Loops
|
|
489
|
+
|
|
490
|
+
```nunjucks
|
|
491
|
+
{% for item in items %}
|
|
492
|
+
{{ item }}
|
|
493
|
+
{{ loop.index }} <!-- 1-indexed -->
|
|
494
|
+
{{ loop.index0 }} <!-- 0-indexed -->
|
|
495
|
+
{{ loop.first }} <!-- true on first iteration -->
|
|
496
|
+
{{ loop.last }} <!-- true on last iteration -->
|
|
497
|
+
{{ loop.length }} <!-- total items -->
|
|
498
|
+
{% else %}
|
|
499
|
+
<!-- if items is empty -->
|
|
500
|
+
{% endfor %}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Template Inheritance
|
|
504
|
+
|
|
505
|
+
**Base template (`base.njk`):**
|
|
506
|
+
|
|
507
|
+
```nunjucks
|
|
508
|
+
<!DOCTYPE html>
|
|
509
|
+
<html>
|
|
510
|
+
<head>
|
|
511
|
+
{% block head %}{% endblock %}
|
|
512
|
+
</head>
|
|
513
|
+
<body>
|
|
514
|
+
{% block content %}{% endblock %}
|
|
515
|
+
</body>
|
|
516
|
+
</html>
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
**Child template:**
|
|
520
|
+
|
|
521
|
+
```nunjucks
|
|
522
|
+
{% extends "base.njk" %}
|
|
523
|
+
|
|
524
|
+
{% block head %}
|
|
525
|
+
<title>{{ title }}</title>
|
|
526
|
+
{% endblock %}
|
|
527
|
+
|
|
528
|
+
{% block content %}
|
|
529
|
+
<h1>{{ page.title }}</h1>
|
|
530
|
+
{{ content | safe }}
|
|
531
|
+
{% endblock %}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Includes
|
|
535
|
+
|
|
536
|
+
```nunjucks
|
|
537
|
+
{% include "header.njk" %}
|
|
538
|
+
{% include "footer.njk" %}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### Comments
|
|
542
|
+
|
|
543
|
+
```nunjucks
|
|
544
|
+
{# This is a comment that won't appear in output #}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Safe Output
|
|
548
|
+
|
|
549
|
+
Use `safe` to output HTML without escaping:
|
|
550
|
+
|
|
551
|
+
```nunjucks
|
|
552
|
+
{{ content | safe }}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Setting Variables
|
|
556
|
+
|
|
557
|
+
```nunjucks
|
|
558
|
+
{% set myVar = "value" %}
|
|
559
|
+
{% set myArray = [1, 2, 3] %}
|
|
560
|
+
{% set myObj = { key: "value" } %}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Macros
|
|
564
|
+
|
|
565
|
+
Create reusable template functions:
|
|
566
|
+
|
|
567
|
+
```nunjucks
|
|
568
|
+
{% macro postCard(post) %}
|
|
569
|
+
<article class="post-card">
|
|
570
|
+
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
|
|
571
|
+
<time>{{ post.date | date('short') }}</time>
|
|
572
|
+
</article>
|
|
573
|
+
{% endmacro %}
|
|
574
|
+
|
|
575
|
+
<!-- Use the macro -->
|
|
576
|
+
{% for post in posts %}
|
|
577
|
+
{{ postCard(post) }}
|
|
578
|
+
{% endfor %}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
For more Nunjucks features, see the [official documentation](https://mozilla.github.io/nunjucks/templating.html).
|
package/lib/assets.js
CHANGED
|
@@ -141,7 +141,7 @@ export function copyDefaultStyles(config, themesDir) {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
// Copy styles from the selected theme
|
|
144
|
-
const themeName = config.theme || 'main';
|
|
144
|
+
const themeName = config.theme?.name || 'main';
|
|
145
145
|
const themeStylesDir = join(themesDir, themeName, 'styles');
|
|
146
146
|
|
|
147
147
|
if (existsSync(themeStylesDir)) {
|
package/lib/config.js
CHANGED
|
@@ -10,7 +10,9 @@ const defaultConfig = {
|
|
|
10
10
|
url: 'http://localhost:3000',
|
|
11
11
|
author: 'Anonymous'
|
|
12
12
|
},
|
|
13
|
-
theme:
|
|
13
|
+
theme: {
|
|
14
|
+
name: 'main' // Theme to use: 'main', 'minimal', 'developer', or 'magazine'
|
|
15
|
+
},
|
|
14
16
|
input: 'src',
|
|
15
17
|
output: 'dist',
|
|
16
18
|
layouts: '_layouts',
|
package/lib/content.js
CHANGED
|
@@ -247,6 +247,72 @@ marked.use({
|
|
|
247
247
|
}
|
|
248
248
|
});
|
|
249
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Safely truncate markdown text without breaking inline formatting
|
|
252
|
+
* Avoids cutting in the middle of links, bold, italic, code, images, etc.
|
|
253
|
+
*/
|
|
254
|
+
function truncateMarkdownSafely(text, maxLength) {
|
|
255
|
+
if (text.length <= maxLength) {
|
|
256
|
+
return text;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Find all inline markdown element ranges to avoid cutting inside them
|
|
260
|
+
const inlinePatterns = [
|
|
261
|
+
/!\[([^\]]*)\]\([^)]*\)/g, // Images  - must come before links
|
|
262
|
+
/\[([^\]]*)\]\([^)]*\)/g, // Links [text](url)
|
|
263
|
+
/\[([^\]]*)\]\[[^\]]*\]/g, // Reference links [text][ref]
|
|
264
|
+
/\*\*([^*]+)\*\*/g, // Bold **text**
|
|
265
|
+
/__([^_]+)__/g, // Bold __text__
|
|
266
|
+
/\*([^*\n]+)\*/g, // Italic *text*
|
|
267
|
+
/_([^_\n]+)_/g, // Italic _text_
|
|
268
|
+
/`([^`]+)`/g, // Inline code `code`
|
|
269
|
+
/~~([^~]+)~~/g, // Strikethrough ~~text~~
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
// Collect all ranges where inline elements exist
|
|
273
|
+
const ranges = [];
|
|
274
|
+
for (const pattern of inlinePatterns) {
|
|
275
|
+
let match;
|
|
276
|
+
// Reset lastIndex for each pattern
|
|
277
|
+
pattern.lastIndex = 0;
|
|
278
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
279
|
+
ranges.push({ start: match.index, end: match.index + match[0].length });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Sort ranges by start position
|
|
284
|
+
ranges.sort((a, b) => a.start - b.start);
|
|
285
|
+
|
|
286
|
+
// Find the best truncation point
|
|
287
|
+
let truncateAt = maxLength;
|
|
288
|
+
|
|
289
|
+
// Check if our target position is inside any markdown element
|
|
290
|
+
for (const range of ranges) {
|
|
291
|
+
if (truncateAt > range.start && truncateAt < range.end) {
|
|
292
|
+
// We're inside this element - decide whether to include it or exclude it
|
|
293
|
+
if (range.end <= maxLength + 50) {
|
|
294
|
+
// Include the whole element if it doesn't extend too far past our limit
|
|
295
|
+
truncateAt = range.end;
|
|
296
|
+
} else {
|
|
297
|
+
// Otherwise, truncate before this element starts
|
|
298
|
+
truncateAt = range.start;
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Try to break at a word boundary for cleaner excerpts
|
|
305
|
+
if (truncateAt > 0) {
|
|
306
|
+
const lastSpace = text.lastIndexOf(' ', truncateAt);
|
|
307
|
+
// Only use word boundary if it's reasonably close to our target
|
|
308
|
+
if (lastSpace > truncateAt - 30 && lastSpace > maxLength * 0.5) {
|
|
309
|
+
truncateAt = lastSpace;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return text.substring(0, truncateAt).trim() + '...';
|
|
314
|
+
}
|
|
315
|
+
|
|
250
316
|
/**
|
|
251
317
|
* Generate a URL-friendly slug from a string
|
|
252
318
|
*/
|
|
@@ -321,12 +387,17 @@ export function parseContent(filePath) {
|
|
|
321
387
|
if (!excerpt) {
|
|
322
388
|
const firstParagraph = markdown.split('\n\n')[0];
|
|
323
389
|
excerpt = firstParagraph.replace(/^#+\s+.+\n?/, '').trim();
|
|
324
|
-
// Limit excerpt length
|
|
390
|
+
// Limit excerpt length using safe truncation that preserves markdown syntax
|
|
325
391
|
if (excerpt.length > 200) {
|
|
326
|
-
excerpt = excerpt
|
|
392
|
+
excerpt = truncateMarkdownSafely(excerpt, 200);
|
|
327
393
|
}
|
|
328
394
|
}
|
|
329
395
|
|
|
396
|
+
// Create HTML version of excerpt for templates that need rendered output
|
|
397
|
+
let excerptHtml = marked.parse(excerpt);
|
|
398
|
+
// Clean up the HTML (remove wrapping <p> tags for inline use)
|
|
399
|
+
excerptHtml = excerptHtml.replace(/^<p>/, '').replace(/<\/p>\n?$/, '');
|
|
400
|
+
|
|
330
401
|
// Normalize tags to array
|
|
331
402
|
let tags = frontMatter.tags || [];
|
|
332
403
|
if (typeof tags === 'string') {
|
|
@@ -338,6 +409,7 @@ export function parseContent(filePath) {
|
|
|
338
409
|
slug,
|
|
339
410
|
date,
|
|
340
411
|
excerpt,
|
|
412
|
+
excerptHtml,
|
|
341
413
|
tags,
|
|
342
414
|
content: html,
|
|
343
415
|
rawContent: markdown,
|
package/lib/templates.js
CHANGED
|
@@ -12,7 +12,8 @@ const __dirname = dirname(__filename);
|
|
|
12
12
|
function dateFilter(date, format = 'long') {
|
|
13
13
|
if (!date) return '';
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// Handle special "now" keyword for current date/time
|
|
16
|
+
const d = (date === 'now') ? new Date() : new Date(date);
|
|
16
17
|
|
|
17
18
|
if (isNaN(d.getTime())) return '';
|
|
18
19
|
|
|
@@ -205,7 +206,7 @@ export function createTemplateEngine(config) {
|
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
// Default templates from the selected theme
|
|
208
|
-
const themeName = config.theme || 'main';
|
|
209
|
+
const themeName = config.theme?.name || 'main';
|
|
209
210
|
const themeDir = join(__dirname, '..', 'themes', themeName);
|
|
210
211
|
templatePaths.push(join(themeDir, 'layouts'));
|
|
211
212
|
templatePaths.push(join(themeDir, 'includes'));
|