@rettangoli/sites 0.2.7 → 1.0.0-rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,171 +1,7 @@
1
- import http from 'node:http';
2
- import path from 'node:path';
3
- import fs from 'node:fs';
4
- import { existsSync } from 'node:fs';
5
- import { buildSite } from './cli/build.js';
6
- import { createScreenshotCapture } from './screenshot.js';
7
- import { loadSiteConfig } from './utils/loadSiteConfig.js';
8
-
9
- function getContentType(ext) {
10
- const types = {
11
- '.html': 'text/html',
12
- '.css': 'text/css',
13
- '.js': 'application/javascript',
14
- '.json': 'application/json',
15
- '.png': 'image/png',
16
- '.jpg': 'image/jpeg',
17
- '.gif': 'image/gif',
18
- '.svg': 'image/svg+xml',
19
- '.ico': 'image/x-icon'
20
- };
21
- return types[ext] || 'application/octet-stream';
22
- }
23
-
24
- function serveFile(filePath, res, skipWebSocket = false) {
25
- const ext = path.extname(filePath);
26
- const contentType = getContentType(ext);
27
-
28
- try {
29
- const content = fs.readFileSync(filePath);
30
- res.writeHead(200, { 'Content-Type': contentType });
31
- res.end(content);
32
- } catch (err) {
33
- console.error('Error serving file:', err);
34
- res.writeHead(500);
35
- res.end('Internal Server Error');
36
- }
37
- }
38
-
39
- function handleRequest(req, res, siteDir = '_site') {
40
- const urlParts = req.url.split('?');
41
- let urlPath = urlParts[0];
42
- const queryString = urlParts[1] || '';
43
-
44
- // Check if this is a screenshot request
45
- const isScreenshotRequest = queryString.includes('screenshot=true');
46
-
47
- // Default to index.html for root
48
- if (urlPath === '/') {
49
- urlPath = '/index.html';
50
- }
51
-
52
- // Handle trailing slash - remove it for processing
53
- const hasTrailingSlash = urlPath.endsWith('/') && urlPath !== '/';
54
- if (hasTrailingSlash) {
55
- urlPath = urlPath.slice(0, -1);
56
- }
57
-
58
- // Handle paths without extensions
59
- if (!path.extname(urlPath)) {
60
- // First try as .html file
61
- const htmlPath = path.join(siteDir, urlPath + '.html');
62
- if (existsSync(htmlPath)) {
63
- urlPath = urlPath + '.html';
64
- } else {
65
- // Try as directory with index.html
66
- const indexPath = path.join(siteDir, urlPath, 'index.html');
67
- if (existsSync(indexPath)) {
68
- urlPath = path.join(urlPath, 'index.html');
69
- }
70
- }
71
- }
72
-
73
- const filePath = path.join(siteDir, urlPath);
74
-
75
- // Check if file exists
76
- if (!existsSync(filePath)) {
77
- res.writeHead(404);
78
- res.end('404 Not Found');
79
- return;
80
- }
81
-
82
- // Check if it's a directory
83
- const stats = fs.statSync(filePath);
84
- if (stats.isDirectory()) {
85
- // Try to serve index.html from the directory
86
- const indexPath = path.join(filePath, 'index.html');
87
- if (existsSync(indexPath)) {
88
- return serveFile(indexPath, res, isScreenshotRequest);
89
- } else {
90
- res.writeHead(404);
91
- res.end('404 Not Found');
92
- return;
93
- }
94
- }
95
-
96
- // Serve the file
97
- serveFile(filePath, res, isScreenshotRequest);
98
- }
99
-
100
- function createTempServer(port = 3001, siteDir = '_site') {
101
- const httpServer = http.createServer((req, res) => {
102
- handleRequest(req, res, siteDir);
103
- });
104
-
105
- const start = () => {
106
- return new Promise((resolve, reject) => {
107
- httpServer.listen(port, '0.0.0.0', () => {
108
- console.log(`📡 Temp server running at http://localhost:${port}/`);
109
- resolve();
110
- });
111
-
112
- httpServer.on('error', reject);
113
- });
114
- };
115
-
116
- const close = () => {
117
- return new Promise((resolve) => {
118
- httpServer.close(() => {
119
- console.log('📡 Temp server closed');
120
- resolve();
121
- });
122
- });
123
- };
124
-
125
- return { start, close };
126
- }
127
-
128
- const screenshotCommand = async (options = {}) => {
129
- const {
130
- port = 3001,
131
- rootDir = '.'
132
- } = options;
133
-
134
- console.log('📸 Starting screenshot capture for all pages...');
135
-
136
- // Load config to get ignore patterns
137
- const config = await loadSiteConfig(rootDir, false);
138
- const ignorePatterns = config?.screenshots?.ignore || [];
139
-
140
- if (ignorePatterns.length > 0) {
141
- console.log('📋 Ignore patterns:', ignorePatterns);
142
- }
143
-
144
- // Build the site first (with screenshot mode enabled)
145
- console.log('Building site...');
146
- await buildSite({ rootDir, isScreenshotMode: true });
147
- console.log('Build complete');
148
-
149
- // Start temporary server
150
- const server = createTempServer(port);
151
- await server.start();
152
-
153
- // Initialize screenshot capture
154
- const screenshotCapture = await createScreenshotCapture(port);
155
-
156
- try {
157
- // Capture screenshots for all pages
158
- const pagesDir = path.join(rootDir, 'pages');
159
- await screenshotCapture.captureAllPages(pagesDir, { ignorePatterns });
160
-
161
- console.log('✅ All screenshots captured successfully!');
162
- } catch (error) {
163
- console.error('❌ Error capturing screenshots:', error);
164
- } finally {
165
- // Clean up
166
- await screenshotCapture.close();
167
- await server.close();
168
- }
1
+ const screenshotCommand = async () => {
2
+ throw new Error(
3
+ 'Screenshot CLI for @rettangoli/sites was removed. Use "rtgl vt generate" with "vt.url" and optional "vt.service.start".',
4
+ );
169
5
  };
170
6
 
171
- export default screenshotCommand;
7
+ export default screenshotCommand;
@@ -1,46 +1,272 @@
1
- import path from 'path';
2
- import { pathToFileURL } from 'url';
3
- import MarkdownIt from 'markdown-it';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import yaml from 'js-yaml';
4
+
5
+ const ALLOWED_TOP_LEVEL_KEYS = new Set(['markdown', 'markdownit', 'build']);
6
+ const MARKDOWN_BOOLEAN_KEYS = new Set(['html', 'linkify', 'typographer', 'breaks', 'xhtmlOut']);
7
+ const MARKDOWN_STRING_KEYS = new Set(['langPrefix', 'quotes', 'preset']);
8
+ const MARKDOWN_NUMBER_KEYS = new Set(['maxNesting']);
9
+ const ALLOWED_MARKDOWN_KEYS = new Set([
10
+ ...MARKDOWN_BOOLEAN_KEYS,
11
+ ...MARKDOWN_STRING_KEYS,
12
+ ...MARKDOWN_NUMBER_KEYS,
13
+ 'shiki',
14
+ 'headingAnchors',
15
+ 'codePreview'
16
+ ]);
17
+ const SHIKI_BOOLEAN_KEYS = new Set(['enabled']);
18
+ const SHIKI_STRING_KEYS = new Set(['theme']);
19
+ const ALLOWED_SHIKI_KEYS = new Set([...SHIKI_BOOLEAN_KEYS, ...SHIKI_STRING_KEYS]);
20
+ const CODE_PREVIEW_BOOLEAN_KEYS = new Set(['enabled', 'showSource']);
21
+ const CODE_PREVIEW_STRING_KEYS = new Set(['theme']);
22
+ const ALLOWED_CODE_PREVIEW_KEYS = new Set([...CODE_PREVIEW_BOOLEAN_KEYS, ...CODE_PREVIEW_STRING_KEYS]);
23
+ const ALLOWED_PRESETS = new Set(['default', 'commonmark', 'zero']);
24
+ const HEADING_ANCHORS_BOOLEAN_KEYS = new Set(['enabled', 'wrap']);
25
+ const HEADING_ANCHORS_STRING_KEYS = new Set(['slugMode', 'fallback']);
26
+ const ALLOWED_HEADING_ANCHORS_KEYS = new Set([...HEADING_ANCHORS_BOOLEAN_KEYS, ...HEADING_ANCHORS_STRING_KEYS]);
27
+ const ALLOWED_HEADING_ANCHOR_SLUG_MODES = new Set(['ascii', 'unicode']);
28
+ const BUILD_BOOLEAN_KEYS = new Set(['keepMarkdownFiles']);
29
+ const ALLOWED_BUILD_KEYS = new Set([...BUILD_BOOLEAN_KEYS]);
30
+ let didWarnLegacyMarkdownKey = false;
31
+
32
+ function isPlainObject(value) {
33
+ return value && typeof value === 'object' && !Array.isArray(value);
34
+ }
35
+
36
+ function validateHeadingAnchors(value, configPath) {
37
+ if (typeof value === 'boolean') {
38
+ return;
39
+ }
40
+
41
+ if (!isPlainObject(value)) {
42
+ throw new Error(`Invalid markdown option "headingAnchors" in "${configPath}": expected a boolean or object.`);
43
+ }
44
+
45
+ for (const key of Object.keys(value)) {
46
+ if (!ALLOWED_HEADING_ANCHORS_KEYS.has(key)) {
47
+ throw new Error(
48
+ `Unsupported headingAnchors option "${key}" in "${configPath}". Supported options: ${Array.from(ALLOWED_HEADING_ANCHORS_KEYS).join(', ')}.`
49
+ );
50
+ }
51
+
52
+ if (HEADING_ANCHORS_BOOLEAN_KEYS.has(key) && typeof value[key] !== 'boolean') {
53
+ throw new Error(`Invalid headingAnchors option "${key}" in "${configPath}": expected a boolean.`);
54
+ }
55
+
56
+ if (HEADING_ANCHORS_STRING_KEYS.has(key) && typeof value[key] !== 'string') {
57
+ throw new Error(`Invalid headingAnchors option "${key}" in "${configPath}": expected a string.`);
58
+ }
59
+ }
60
+
61
+ if (value.slugMode !== undefined && !ALLOWED_HEADING_ANCHOR_SLUG_MODES.has(value.slugMode)) {
62
+ throw new Error(
63
+ `Invalid headingAnchors slugMode "${value.slugMode}" in "${configPath}". Allowed: ${Array.from(ALLOWED_HEADING_ANCHOR_SLUG_MODES).join(', ')}.`
64
+ );
65
+ }
66
+
67
+ if (value.fallback !== undefined && value.fallback.trim() === '') {
68
+ throw new Error(`Invalid headingAnchors option "fallback" in "${configPath}": expected a non-empty string.`);
69
+ }
70
+ }
71
+
72
+ function validateCodePreview(value, configPath) {
73
+ if (typeof value === 'boolean') {
74
+ return;
75
+ }
76
+
77
+ if (!isPlainObject(value)) {
78
+ throw new Error(`Invalid markdown option "codePreview" in "${configPath}": expected a boolean or object.`);
79
+ }
80
+
81
+ for (const key of Object.keys(value)) {
82
+ if (!ALLOWED_CODE_PREVIEW_KEYS.has(key)) {
83
+ throw new Error(
84
+ `Unsupported codePreview option "${key}" in "${configPath}". Supported options: ${Array.from(ALLOWED_CODE_PREVIEW_KEYS).join(', ')}.`
85
+ );
86
+ }
87
+
88
+ if (CODE_PREVIEW_BOOLEAN_KEYS.has(key) && typeof value[key] !== 'boolean') {
89
+ throw new Error(`Invalid codePreview option "${key}" in "${configPath}": expected a boolean.`);
90
+ }
91
+
92
+ if (CODE_PREVIEW_STRING_KEYS.has(key) && typeof value[key] !== 'string') {
93
+ throw new Error(`Invalid codePreview option "${key}" in "${configPath}": expected a string.`);
94
+ }
95
+ }
96
+
97
+ if (typeof value.theme === 'string' && value.theme.trim() === '') {
98
+ throw new Error(`Invalid codePreview option "theme" in "${configPath}": expected a non-empty string.`);
99
+ }
100
+ }
101
+
102
+ function validateBuildConfig(value, configPath) {
103
+ if (!isPlainObject(value)) {
104
+ throw new Error(`Invalid build config in "${configPath}": expected an object.`);
105
+ }
106
+
107
+ for (const key of Object.keys(value)) {
108
+ if (!ALLOWED_BUILD_KEYS.has(key)) {
109
+ throw new Error(
110
+ `Unsupported build option "${key}" in "${configPath}". Supported options: ${Array.from(ALLOWED_BUILD_KEYS).join(', ')}.`
111
+ );
112
+ }
113
+
114
+ if (BUILD_BOOLEAN_KEYS.has(key) && typeof value[key] !== 'boolean') {
115
+ throw new Error(`Invalid build option "${key}" in "${configPath}": expected a boolean.`);
116
+ }
117
+ }
118
+
119
+ return { ...value };
120
+ }
121
+
122
+ function validateConfig(rawConfig, configPath) {
123
+ if (rawConfig == null) {
124
+ return {};
125
+ }
126
+
127
+ if (!isPlainObject(rawConfig)) {
128
+ throw new Error(`Invalid site config in "${configPath}": expected a YAML object at the top level.`);
129
+ }
130
+
131
+ const config = { ...rawConfig };
132
+
133
+ for (const key of Object.keys(config)) {
134
+ if (!ALLOWED_TOP_LEVEL_KEYS.has(key)) {
135
+ throw new Error(
136
+ `Unsupported key "${key}" in "${configPath}". Supported keys: markdownit (recommended), markdown (legacy alias), build.`
137
+ );
138
+ }
139
+ }
140
+
141
+ if (config.markdown !== undefined && config.markdownit !== undefined) {
142
+ throw new Error(`Use only one of "markdownit" (recommended) or "markdown" (legacy alias) in "${configPath}".`);
143
+ }
144
+
145
+ if (config.markdown !== undefined && config.markdownit === undefined && !didWarnLegacyMarkdownKey) {
146
+ console.warn(`"${configPath}" uses legacy key "markdown". Please rename it to "markdownit".`);
147
+ didWarnLegacyMarkdownKey = true;
148
+ }
149
+
150
+ const normalizedConfig = {};
151
+ const markdownConfig = config.markdownit ?? config.markdown;
152
+ if (markdownConfig !== undefined) {
153
+ if (!isPlainObject(markdownConfig)) {
154
+ throw new Error(`Invalid markdown config in "${configPath}": expected an object.`);
155
+ }
156
+
157
+ for (const key of Object.keys(markdownConfig)) {
158
+ if (!ALLOWED_MARKDOWN_KEYS.has(key)) {
159
+ throw new Error(
160
+ `Unsupported markdown option "${key}" in "${configPath}". Supported options: ${Array.from(ALLOWED_MARKDOWN_KEYS).join(', ')}.`
161
+ );
162
+ }
163
+
164
+ if (key === 'headingAnchors') {
165
+ validateHeadingAnchors(markdownConfig.headingAnchors, configPath);
166
+ continue;
167
+ }
168
+
169
+ if (key === 'codePreview') {
170
+ validateCodePreview(markdownConfig.codePreview, configPath);
171
+ continue;
172
+ }
173
+
174
+ if (MARKDOWN_BOOLEAN_KEYS.has(key) && typeof markdownConfig[key] !== 'boolean') {
175
+ throw new Error(`Invalid markdown option "${key}" in "${configPath}": expected a boolean.`);
176
+ }
177
+
178
+ if (MARKDOWN_STRING_KEYS.has(key) && typeof markdownConfig[key] !== 'string') {
179
+ throw new Error(`Invalid markdown option "${key}" in "${configPath}": expected a string.`);
180
+ }
181
+
182
+ if (MARKDOWN_NUMBER_KEYS.has(key) && typeof markdownConfig[key] !== 'number') {
183
+ throw new Error(`Invalid markdown option "${key}" in "${configPath}": expected a number.`);
184
+ }
185
+
186
+ if (key === 'maxNesting' && !Number.isInteger(markdownConfig[key])) {
187
+ throw new Error(`Invalid markdown option "${key}" in "${configPath}": expected an integer.`);
188
+ }
189
+
190
+ if (key === 'preset' && !ALLOWED_PRESETS.has(markdownConfig[key])) {
191
+ throw new Error(`Invalid markdown preset "${markdownConfig[key]}" in "${configPath}". Allowed: ${Array.from(ALLOWED_PRESETS).join(', ')}.`);
192
+ }
193
+ }
194
+
195
+ if (markdownConfig.shiki !== undefined) {
196
+ if (!isPlainObject(markdownConfig.shiki)) {
197
+ throw new Error(`Invalid markdown option "shiki" in "${configPath}": expected an object.`);
198
+ }
199
+
200
+ for (const key of Object.keys(markdownConfig.shiki)) {
201
+ if (!ALLOWED_SHIKI_KEYS.has(key)) {
202
+ throw new Error(
203
+ `Unsupported shiki option "${key}" in "${configPath}". Supported options: ${Array.from(ALLOWED_SHIKI_KEYS).join(', ')}.`
204
+ );
205
+ }
206
+
207
+ if (SHIKI_BOOLEAN_KEYS.has(key) && typeof markdownConfig.shiki[key] !== 'boolean') {
208
+ throw new Error(`Invalid shiki option "${key}" in "${configPath}": expected a boolean.`);
209
+ }
210
+
211
+ if (SHIKI_STRING_KEYS.has(key) && typeof markdownConfig.shiki[key] !== 'string') {
212
+ throw new Error(`Invalid shiki option "${key}" in "${configPath}": expected a string.`);
213
+ }
214
+ }
215
+ }
216
+
217
+ normalizedConfig.markdown = { ...markdownConfig };
218
+ }
219
+
220
+ if (config.build !== undefined) {
221
+ normalizedConfig.build = validateBuildConfig(config.build, configPath);
222
+ }
223
+
224
+ return normalizedConfig;
225
+ }
226
+
227
+ function readFirstExistingConfigPath(rootDir) {
228
+ const yamlPath = path.join(rootDir, 'sites.config.yaml');
229
+ const ymlPath = path.join(rootDir, 'sites.config.yml');
230
+ const legacyJsPath = path.join(rootDir, 'sites.config.js');
231
+
232
+ if (fs.existsSync(legacyJsPath)) {
233
+ throw new Error(`"${legacyJsPath}" is no longer supported. Rename it to "sites.config.yaml".`);
234
+ }
235
+
236
+ if (fs.existsSync(yamlPath)) {
237
+ return yamlPath;
238
+ }
239
+
240
+ if (fs.existsSync(ymlPath)) {
241
+ return ymlPath;
242
+ }
243
+
244
+ return null;
245
+ }
4
246
 
5
247
  /**
6
- * Load the sites.config.js file from a given directory
7
- * @param {string} rootDir - The root directory to look for sites.config.js
248
+ * Load the sites.config.yaml/.yml file from a given directory
249
+ * @param {string} rootDir - The root directory to look for sites.config.yaml/.yml
8
250
  * @param {boolean} throwOnError - Whether to throw on errors other than file not found
9
- * @param {boolean} bustCache - Whether to bypass module cache (for reloading)
251
+ * @param {boolean} _bustCache - Kept for callsite compatibility; not used for YAML
10
252
  * @returns {Promise<Object>} The loaded config object or empty object if not found
11
253
  */
12
- export async function loadSiteConfig(rootDir, throwOnError = true, bustCache = false) {
254
+ export async function loadSiteConfig(rootDir, throwOnError = true, _bustCache = false) {
13
255
  try {
14
- const configPath = path.join(rootDir, 'sites.config.js');
15
- let importUrl = pathToFileURL(configPath).href;
16
-
17
- // Add timestamp to force reload if cache busting is requested
18
- if (bustCache) {
19
- importUrl += `?t=${Date.now()}`;
20
- }
21
-
22
- const configModule = await import(importUrl);
23
- const configExport = configModule.default;
24
-
25
- // Check if the export is a function
26
- if (typeof configExport === 'function') {
27
- // Call the function with markdownit constructor
28
- return configExport({ markdownit: MarkdownIt }) || {};
29
- }
30
-
31
- // Otherwise return as is (for backward compatibility)
32
- return configExport || {};
33
- } catch (e) {
34
- // Only ignore file not found errors
35
- if (e.code === 'ENOENT' || e.code === 'ERR_MODULE_NOT_FOUND') {
36
- // Config file is optional, return empty config
256
+ const configPath = readFirstExistingConfigPath(rootDir);
257
+
258
+ if (!configPath) {
37
259
  return {};
38
- } else if (throwOnError) {
39
- // Re-throw any other errors (syntax errors, module not found, etc.)
260
+ }
261
+
262
+ const configContent = fs.readFileSync(configPath, 'utf8');
263
+ const parsed = yaml.load(configContent, { schema: yaml.JSON_SCHEMA });
264
+ return validateConfig(parsed, configPath);
265
+ } catch (e) {
266
+ if (throwOnError) {
40
267
  throw e;
41
- } else {
42
- console.error('Error loading sites.config.js:', e);
43
- return {};
44
268
  }
269
+ console.error('Error loading site YAML config:', e);
270
+ return {};
45
271
  }
46
- }
272
+ }
@@ -2,22 +2,19 @@
2
2
 
3
3
  A static site built with [Rettangoli Sites](https://github.com/yuusoft-org/rettangoli) using the [rtgl UI](https://github.com/yuusoft-org/rettangoli/tree/main/packages/rettangoli-ui) framework.
4
4
 
5
+ This template works without a site-level `package.json`; run commands with `bunx rtgl`.
6
+
5
7
  ## Getting Started
6
8
 
7
9
  ```bash
8
- bun install
9
- bun run build
10
- bun run serve
10
+ bunx rtgl sites build
11
+ bunx rtgl sites watch
12
+ bunx rtgl sites watch --reload-mode full
13
+ bunx rtgl sites build --root-dir . --output-path dist
11
14
  ```
12
15
 
13
- ## Scripts
14
-
15
- | Script | Description |
16
- |--------|-------------|
17
- | `bun run build` | Build site to `_site/` |
18
- | `bun run watch` | Build and watch for changes |
19
- | `bun run serve` | Serve `_site/` locally |
20
- | `bun run screenshot` | Build and capture screenshots |
16
+ `--reload-mode body` (default) does body replacement; `--reload-mode full` does full page refresh.
17
+ Preferred CLI flags are `--root-dir` and `--output-path` (`--rootDir`/`--outputPath` are legacy aliases).
21
18
 
22
19
  ## Project Structure
23
20
 
@@ -27,7 +24,7 @@ bun run serve
27
24
  ├── partials/ # Reusable components
28
25
  ├── data/ # Global data files
29
26
  ├── static/ # Static assets (copied as-is)
30
- ├── sites.config.js # Configuration and custom functions
27
+ ├── sites.config.yaml # Optional site settings
31
28
  └── _site/ # Generated output
32
29
  ```
33
30
 
@@ -75,7 +72,7 @@ Content in **markdown**.
75
72
  ```yaml
76
73
  # Layout with rtgl-view
77
74
  - rtgl-view d="h" g="lg" av="c": # horizontal, gap, align vertical center
78
- - rtgl-view flex="1": # flex grow
75
+ - rtgl-view w="1fg": # flex grow
79
76
  - rtgl-view w="200" h="100": # fixed width/height
80
77
 
81
78
  # Text with rtgl-text
@@ -185,25 +182,56 @@ Collection item properties:
185
182
  - `item.data` - Frontmatter (title, date, etc.)
186
183
  - `item.content` - Raw content
187
184
 
188
- ## Configuration
185
+ ## Site Config
189
186
 
190
- `sites.config.js` for custom functions:
187
+ Use `sites.config.yaml` with top-level `markdownit` for simple, non-JS options.
188
+ Legacy key `markdown` still works as an alias.
191
189
 
192
- ```javascript
193
- export default {
194
- functions: {
195
- sortDate: (list) => [...list].sort((a, b) =>
196
- new Date(b.data.date) - new Date(a.data.date)
197
- ),
198
- },
199
- }
190
+ ```yaml
191
+ markdownit:
192
+ preset: default
193
+ html: true
194
+ xhtmlOut: false
195
+ linkify: true
196
+ typographer: false
197
+ breaks: false
198
+ langPrefix: language-
199
+ quotes: "\u201c\u201d\u2018\u2019"
200
+ maxNesting: 100
201
+ shiki:
202
+ enabled: true
203
+ theme: slack-dark
204
+ headingAnchors:
205
+ enabled: true
206
+ slugMode: unicode
207
+ wrap: true
208
+ fallback: section
200
209
  ```
201
210
 
202
- Use in templates:
211
+ CDN runtime scripts used by the default template can be toggled in `data/site.yaml`:
212
+
203
213
  ```yaml
204
- - $for post in sortDate(collections.blog):
214
+ assets:
215
+ loadUiFromCdn: true
216
+ loadConstructStyleSheetsPolyfill: true
205
217
  ```
206
218
 
219
+ ## Built-in Template Functions
220
+
221
+ Use these directly in `${...}` expressions:
222
+
223
+ - `encodeURI(value)`
224
+ - `encodeURIComponent(value)`
225
+ - `decodeURI(value)`
226
+ - `decodeURIComponent(value)`
227
+ - `jsonStringify(value, space = 0)`
228
+ - `formatDate(value, format = "YYYYMMDDHHmmss", useUtc = true)`
229
+ - `now(format = "YYYYMMDDHHmmss", useUtc = true)`
230
+ - `toQueryString(object)`
231
+
232
+ Date format tokens: `YYYY`, `MM`, `DD`, `HH`, `mm`, `ss`.
233
+ `decodeURI`/`decodeURIComponent` return the original input when decoding fails.
234
+
207
235
  ## Static Files
208
236
 
209
237
  Everything in `static/` is copied to `_site/`:
@@ -1,4 +1,7 @@
1
1
  name: My Site
2
+ assets:
3
+ loadUiFromCdn: true
4
+ loadConstructStyleSheetsPolyfill: true
2
5
 
3
6
  nav:
4
7
  - text: Home
@@ -10,5 +10,5 @@ title: Blog
10
10
  - rtgl-view w="f" p="lg" bgc="su" br="lg" h-bgc="suv" h-cur="p":
11
11
  - rtgl-view d="h" av="c" w="f":
12
12
  - rtgl-text s="lg": ${post.data.title}
13
- - rtgl-view flex="1":
13
+ - rtgl-view w="1fg":
14
14
  - rtgl-text s="sm" c="mu-fg": ${post.data.date}
@@ -16,6 +16,6 @@ title: My Site
16
16
  - rtgl-text s="h2" mb="lg": Features
17
17
  - rtgl-view d="h" g="lg" w="f" md-d="v":
18
18
  - $for feature in site.landing.features:
19
- - rtgl-view flex="1" p="lg" bgc="su" br="lg":
19
+ - rtgl-view w="1fg" p="lg" bgc="su" br="lg":
20
20
  - rtgl-text s="h4" mb="sm": ${feature.title}
21
21
  - rtgl-text c="mu-fg": ${feature.description}
@@ -2,7 +2,7 @@
2
2
  - rtgl-view md-w="100vw" lg-w="768" w="1024" ph="lg" pv="xl":
3
3
  - rtgl-view d="h" w="f" av="c":
4
4
  - rtgl-text s="sm" c="mu-fg": © 2024 ${site.name}
5
- - rtgl-view flex="1":
5
+ - rtgl-view w="1fg":
6
6
  - rtgl-view d="h" g="lg":
7
7
  - $for link in site.footer.links:
8
8
  - 'a href="${link.href}" style="text-decoration: none"':
@@ -2,7 +2,7 @@
2
2
  - rtgl-view md-w="100vw" lg-w="768" w="1024" d="h" av="c" h="64" ph="lg":
3
3
  - 'a href="/" style="text-decoration: none"':
4
4
  - rtgl-text s="lg" fw="bold": ${site.name}
5
- - rtgl-view flex="1":
5
+ - rtgl-view w="1fg":
6
6
  - rtgl-view d="h" g="lg" av="c":
7
7
  - $for link in site.nav:
8
8
  - 'a href="${link.href}" style="text-decoration: none"':
@@ -0,0 +1,24 @@
1
+ # Optional site configuration.
2
+ # You can keep this file, remove it, or tune the values below.
3
+ markdownit:
4
+ preset: default
5
+ html: true
6
+ xhtmlOut: false
7
+ linkify: true
8
+ typographer: false
9
+ breaks: false
10
+ langPrefix: language-
11
+ quotes: "\u201c\u201d\u2018\u2019"
12
+ maxNesting: 100
13
+ shiki:
14
+ enabled: true
15
+ theme: slack-dark
16
+ codePreview:
17
+ enabled: false
18
+ showSource: true
19
+ theme: slack-dark
20
+ headingAnchors:
21
+ enabled: true
22
+ slugMode: unicode
23
+ wrap: true
24
+ fallback: section