@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.
- package/README.md +127 -96
- package/package.json +6 -7
- package/src/builtinTemplateFunctions.js +85 -0
- package/src/cli/build.js +16 -9
- package/src/cli/init.js +5 -5
- package/src/cli/watch.js +129 -163
- package/src/createSiteBuilder.js +107 -91
- package/src/markdownItAsync.js +104 -0
- package/src/rtglMarkdown.js +162 -13
- package/src/screenshotRunner.js +5 -169
- package/src/utils/loadSiteConfig.js +262 -36
- package/templates/default/README.md +53 -25
- package/templates/default/data/site.yaml +3 -0
- package/templates/default/pages/blog.yaml +1 -1
- package/templates/default/pages/index.yaml +1 -1
- package/templates/default/partials/footer.yaml +1 -1
- package/templates/default/partials/header.yaml +1 -1
- package/templates/default/sites.config.yaml +24 -0
- package/templates/default/templates/base.yaml +5 -3
- package/templates/default/templates/post.yaml +6 -4
- package/src/screenshot.js +0 -250
- package/templates/default/package.json +0 -14
- package/templates/default/sites.config.js +0 -9
package/src/screenshotRunner.js
CHANGED
|
@@ -1,171 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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.
|
|
7
|
-
* @param {string} rootDir - The root directory to look for sites.config.
|
|
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}
|
|
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,
|
|
254
|
+
export async function loadSiteConfig(rootDir, throwOnError = true, _bustCache = false) {
|
|
13
255
|
try {
|
|
14
|
-
const configPath =
|
|
15
|
-
|
|
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
|
-
}
|
|
39
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
##
|
|
185
|
+
## Site Config
|
|
189
186
|
|
|
190
|
-
`sites.config.
|
|
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
|
-
```
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
211
|
+
CDN runtime scripts used by the default template can be toggled in `data/site.yaml`:
|
|
212
|
+
|
|
203
213
|
```yaml
|
|
204
|
-
|
|
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/`:
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|