create-berna-stencil 1.0.50 → 1.0.52
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/_tools/assistant.js +94 -83
- package/_tools/modules/updateData.js +79 -15
- package/_tools/modules/updateIncludes.js +51 -18
- package/_tools/modules/updateOutputPath.js +34 -25
- package/_tools/modules/updatePage.js +45 -39
- package/_tools/modules/utils.js +12 -0
- package/bin/create.js +2 -2
- package/docs/Assistant CLI.md +1 -36
- package/docs/Backend.md +1 -0
- package/docs/Components.md +54 -33
- package/docs/Head and SEO.md +33 -6
- package/docs/Javascript.md +40 -24
- package/docs/Styling with SCSS.md +3 -3
- package/package.json +2 -6
- package/src/frontend/components/layouts/includes.njk +1 -0
- package/src/frontend/js/modules/forms/textAreaAutoExpand.js +2 -2
- package/src/frontend/llms.njk +18 -0
- package/src/frontend/robots.njk +8 -0
- package/src/frontend/scss/modules/frameworks/_bootstrap.scss +4 -4
- package/src/frontend/scss/modules/frameworks/_bulma.scss +2 -2
- package/src/frontend/scss/modules/frameworks/_foundation.scss +2 -2
- package/src/frontend/scss/modules/frameworks/_uikit.scss +1 -1
- package/src/frontend/robots.txt +0 -4
package/_tools/assistant.js
CHANGED
|
@@ -2,6 +2,8 @@ const readline = require('readline');
|
|
|
2
2
|
const { addPage, removePage, renamePage } = require('./modules/updatePage');
|
|
3
3
|
const { updateOutputPath, getCurrentOutputPath } = require('./modules/updateOutputPath');
|
|
4
4
|
|
|
5
|
+
// --- Setup ---
|
|
6
|
+
|
|
5
7
|
const readerInterface = readline.createInterface({
|
|
6
8
|
input: process.stdin,
|
|
7
9
|
output: process.stdout,
|
|
@@ -10,86 +12,96 @@ const readerInterface = readline.createInterface({
|
|
|
10
12
|
|
|
11
13
|
const PROTECTED_PAGES = ['homepage', '404'];
|
|
12
14
|
|
|
15
|
+
// --- Utility ---
|
|
16
|
+
|
|
17
|
+
// Converts any string to kebab-case
|
|
13
18
|
function toKebabCase(str) {
|
|
14
19
|
return str.trim().toLowerCase()
|
|
15
20
|
.replace(/[\s_]+/g, '-')
|
|
16
21
|
.replace(/-+/g, '-');
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
// Returns an error message if the name is invalid, null otherwise
|
|
25
|
+
function validatePageName(name) {
|
|
26
|
+
if (!name) return 'Invalid name.';
|
|
27
|
+
if (/^\d/.test(name)) return 'Page name cannot start with a number.';
|
|
28
|
+
if (PROTECTED_PAGES.includes(name)) return `"${name}" is a protected page name.`;
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Wraps readerInterface.question in a Promise for use with async/await
|
|
33
|
+
function ask(prompt) {
|
|
34
|
+
return new Promise(resolve =>
|
|
35
|
+
readerInterface.question(prompt, answer => resolve(answer))
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Asks for a page name, converts it to kebab-case, validates it,
|
|
40
|
+
// logs the error and returns null if invalid
|
|
41
|
+
async function askPageName(prompt) {
|
|
42
|
+
const raw = await ask(prompt);
|
|
43
|
+
const name = toKebabCase(raw);
|
|
44
|
+
const error = validatePageName(name);
|
|
45
|
+
if (error) {
|
|
46
|
+
console.log(`(!) ${error}`);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return name;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- Handlers ---
|
|
53
|
+
|
|
54
|
+
async function handleCreateRequest() {
|
|
55
|
+
const name = await askPageName('\n> Enter the name of the new page: ');
|
|
56
|
+
if (name) addPage(name, null);
|
|
33
57
|
}
|
|
34
58
|
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (!oldName) {
|
|
39
|
-
console.log('(!) Invalid name.');
|
|
40
|
-
return displayMainMenu();
|
|
41
|
-
}
|
|
42
|
-
if (PROTECTED_PAGES.includes(oldName)) {
|
|
43
|
-
console.log(`(!) "${oldName}" is a protected page and cannot be renamed.`);
|
|
44
|
-
return displayMainMenu();
|
|
45
|
-
}
|
|
46
|
-
readerInterface.question('> enter the new name: ', (inputNew) => {
|
|
47
|
-
const newName = toKebabCase(inputNew);
|
|
48
|
-
if (!newName) {
|
|
49
|
-
console.log('(!) invalid name.');
|
|
50
|
-
} else if (/^\d/.test(newName)) {
|
|
51
|
-
console.log('(!) Invalid name. Page name cannot start with a number.');
|
|
52
|
-
} else if (PROTECTED_PAGES.includes(newName)) {
|
|
53
|
-
console.log(`(!) "${newName}" is a protected page name.`);
|
|
54
|
-
} else if (oldName === newName) {
|
|
55
|
-
console.log('(!) Old and new name are the same.');
|
|
56
|
-
} else {
|
|
57
|
-
renamePage(oldName, newName);
|
|
58
|
-
}
|
|
59
|
-
displayMainMenu();
|
|
60
|
-
});
|
|
61
|
-
});
|
|
59
|
+
async function handleRemoveRequest() {
|
|
60
|
+
const name = await askPageName('\n> Enter the name of the page to remove: ');
|
|
61
|
+
if (name) removePage(name);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
64
|
+
async function handleRenameRequest() {
|
|
65
|
+
const oldName = await askPageName('\n> Enter the name of the page to rename: ');
|
|
66
|
+
if (!oldName) return;
|
|
67
|
+
|
|
68
|
+
const newName = await askPageName('> Enter the new name: ');
|
|
69
|
+
if (!newName) return;
|
|
70
|
+
|
|
71
|
+
// Extra check: old and new name must differ
|
|
72
|
+
if (oldName === newName) {
|
|
73
|
+
console.log('(!) Old and new name are the same.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
renamePage(oldName, newName);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
function handleOutputPathRequest() {
|
|
80
|
+
async function handleOutputPathRequest() {
|
|
79
81
|
const current = getCurrentOutputPath();
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
displayMainMenu();
|
|
89
|
-
});
|
|
82
|
+
const label = current ? ` Current path: "${current}"\n` : '';
|
|
83
|
+
const input = await ask(`${label} Enter the new output path: `);
|
|
84
|
+
|
|
85
|
+
if (!input.trim()) {
|
|
86
|
+
console.log('(!) Invalid path.');
|
|
87
|
+
} else {
|
|
88
|
+
updateOutputPath(input);
|
|
89
|
+
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
// --- Menu ---
|
|
93
|
+
|
|
94
|
+
// Maps each menu choice to its handler function
|
|
95
|
+
const MENU_ACTIONS = {
|
|
96
|
+
'1': handleCreateRequest,
|
|
97
|
+
'2': handleRemoveRequest,
|
|
98
|
+
'3': handleRenameRequest,
|
|
99
|
+
'4': handleOutputPathRequest,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Displays the menu, waits for input, executes the chosen action,
|
|
103
|
+
// then calls itself again to keep the CLI alive (async recursion, no stack buildup)
|
|
104
|
+
async function displayMainMenu() {
|
|
93
105
|
console.log('\n========================');
|
|
94
106
|
console.log(' Berna-Stencil CLI ');
|
|
95
107
|
console.log('========================\n');
|
|
@@ -99,24 +111,23 @@ function displayMainMenu() {
|
|
|
99
111
|
console.log('4. Configure output path');
|
|
100
112
|
console.log('\nCTRL/CMD + C to exit');
|
|
101
113
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
});
|
|
114
|
+
const choice = (await ask('\nChoose an option: ')).trim();
|
|
115
|
+
|
|
116
|
+
if (choice === '0') {
|
|
117
|
+
readerInterface.close();
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const action = MENU_ACTIONS[choice];
|
|
122
|
+
if (action) {
|
|
123
|
+
await action();
|
|
124
|
+
} else {
|
|
125
|
+
console.log('(!) Invalid option.');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Recurse to redisplay the menu after each action.
|
|
129
|
+
// Safe because each iteration fully resolves before the next one starts.
|
|
130
|
+
displayMainMenu();
|
|
120
131
|
}
|
|
121
132
|
|
|
122
133
|
displayMainMenu();
|
|
@@ -1,16 +1,46 @@
|
|
|
1
1
|
const fileSystem = require('fs');
|
|
2
|
+
|
|
2
3
|
const SITE_DATA_PATH = 'src/frontend/data/site.json';
|
|
3
4
|
|
|
5
|
+
// --- Utility ---
|
|
6
|
+
|
|
7
|
+
// Converts a kebab-case string to camelCase
|
|
8
|
+
// slice(1) removes the delimiter character reliably, avoiding the regex flag bug
|
|
9
|
+
// of a double replace('-', '') without the /g flag
|
|
4
10
|
function toCamelCase(str) {
|
|
5
11
|
return str.toLowerCase().replace(/[-_][a-z0-9]/g, (group) =>
|
|
6
|
-
group.
|
|
12
|
+
group.slice(1).toUpperCase()
|
|
7
13
|
);
|
|
8
14
|
}
|
|
9
15
|
|
|
16
|
+
// Converts a kebab-case page name to a human-readable title
|
|
17
|
+
// e.g. "about-us" → "About Us"
|
|
18
|
+
function toNiceTitle(pageName) {
|
|
19
|
+
return pageName
|
|
20
|
+
.split('-')
|
|
21
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
22
|
+
.join(' ');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Returns the parsed site.json content, or null if the file doesn't exist
|
|
26
|
+
function readSiteData() {
|
|
27
|
+
if (!fileSystem.existsSync(SITE_DATA_PATH)) return null;
|
|
28
|
+
return JSON.parse(fileSystem.readFileSync(SITE_DATA_PATH, 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Serializes and writes the data object back to site.json
|
|
32
|
+
function writeSiteData(data) {
|
|
33
|
+
fileSystem.writeFileSync(SITE_DATA_PATH, JSON.stringify(data, null, 2));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- Public API ---
|
|
37
|
+
|
|
38
|
+
// Adds a new page record to site.json
|
|
39
|
+
// Skips silently if the file doesn't exist or the record is already present
|
|
10
40
|
function addSiteData(pageName) {
|
|
11
|
-
|
|
41
|
+
const data = readSiteData();
|
|
42
|
+
if (!data) return;
|
|
12
43
|
|
|
13
|
-
const data = JSON.parse(fileSystem.readFileSync(SITE_DATA_PATH, 'utf8'));
|
|
14
44
|
const camelName = toCamelCase(pageName);
|
|
15
45
|
|
|
16
46
|
if (data.pages[camelName]) {
|
|
@@ -18,15 +48,11 @@ function addSiteData(pageName) {
|
|
|
18
48
|
return;
|
|
19
49
|
}
|
|
20
50
|
|
|
21
|
-
|
|
22
|
-
.split('-')
|
|
23
|
-
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
24
|
-
.join(' ');
|
|
25
|
-
|
|
51
|
+
// Build the default page record with SEO metadata and empty CDN arrays
|
|
26
52
|
data.pages[camelName] = {
|
|
27
53
|
seo: {
|
|
28
|
-
title:
|
|
29
|
-
description:
|
|
54
|
+
title: toNiceTitle(pageName),
|
|
55
|
+
description: 'description',
|
|
30
56
|
},
|
|
31
57
|
cdn: {
|
|
32
58
|
css: [],
|
|
@@ -34,14 +60,16 @@ function addSiteData(pageName) {
|
|
|
34
60
|
}
|
|
35
61
|
};
|
|
36
62
|
|
|
37
|
-
|
|
63
|
+
writeSiteData(data);
|
|
38
64
|
console.log(`[UPDATED] Record "${camelName}" added.`);
|
|
39
65
|
}
|
|
40
66
|
|
|
67
|
+
// Removes a page record from site.json
|
|
68
|
+
// Skips silently if the file doesn't exist or the record is not found
|
|
41
69
|
function removeSiteData(pageName) {
|
|
42
|
-
|
|
70
|
+
const data = readSiteData();
|
|
71
|
+
if (!data) return;
|
|
43
72
|
|
|
44
|
-
const data = JSON.parse(fileSystem.readFileSync(SITE_DATA_PATH, 'utf8'));
|
|
45
73
|
const camelName = toCamelCase(pageName);
|
|
46
74
|
|
|
47
75
|
if (!data.pages[camelName]) {
|
|
@@ -51,8 +79,44 @@ function removeSiteData(pageName) {
|
|
|
51
79
|
|
|
52
80
|
delete data.pages[camelName];
|
|
53
81
|
|
|
54
|
-
|
|
82
|
+
writeSiteData(data);
|
|
55
83
|
console.log(`[CLEANED] Record "${camelName}" removed.`);
|
|
56
84
|
}
|
|
57
85
|
|
|
58
|
-
|
|
86
|
+
// Renames a page record in site.json
|
|
87
|
+
// Preserves all existing fields (cdn, etc.) and only updates the SEO title
|
|
88
|
+
// Skips if the source record doesn't exist or the target name is already taken
|
|
89
|
+
function renameSiteData(oldName, newName) {
|
|
90
|
+
const data = readSiteData();
|
|
91
|
+
if (!data) return;
|
|
92
|
+
|
|
93
|
+
const oldCamel = toCamelCase(oldName);
|
|
94
|
+
const newCamel = toCamelCase(newName);
|
|
95
|
+
|
|
96
|
+
if (!data.pages[oldCamel]) {
|
|
97
|
+
console.log(`[SKIP] Record "${oldCamel}" not found.`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (data.pages[newCamel]) {
|
|
102
|
+
console.log(`[SKIP] Record "${newCamel}" already exists.`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Spread the existing record to preserve cdn and any future fields,
|
|
107
|
+
// then override only the seo.title with the new page name
|
|
108
|
+
data.pages[newCamel] = {
|
|
109
|
+
...data.pages[oldCamel],
|
|
110
|
+
seo: {
|
|
111
|
+
...data.pages[oldCamel].seo,
|
|
112
|
+
title: toNiceTitle(newName),
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
delete data.pages[oldCamel];
|
|
117
|
+
|
|
118
|
+
writeSiteData(data);
|
|
119
|
+
console.log(`[UPDATED] Record "${oldCamel}" renamed to "${newCamel}".`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { addSiteData, removeSiteData, renameSiteData };
|
|
@@ -1,45 +1,78 @@
|
|
|
1
1
|
const fileSystem = require('fs');
|
|
2
|
+
const { toCamelCase } = require('./utils');
|
|
3
|
+
|
|
2
4
|
const INCLUDES_PATH = 'src/frontend/components/layouts/includes.njk';
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
// --- Helpers ---
|
|
7
|
+
|
|
8
|
+
// Returns the file content as a string, or null if the file doesn't exist
|
|
9
|
+
function readIncludes() {
|
|
10
|
+
if (!fileSystem.existsSync(INCLUDES_PATH)) return null;
|
|
11
|
+
return fileSystem.readFileSync(INCLUDES_PATH, 'utf8');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Writes updated content back to the includes file
|
|
15
|
+
function writeIncludes(content) {
|
|
16
|
+
fileSystem.writeFileSync(INCLUDES_PATH, content);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Builds the regex that matches the elif block for a given camelCase page name.
|
|
20
|
+
// Defined once here to avoid duplication and the stateful /g flag bug:
|
|
21
|
+
// using /g with .test() advances lastIndex, making a subsequent .replace() start
|
|
22
|
+
// from the wrong position. A fresh non-/g regex avoids this entirely.
|
|
23
|
+
function buildElifRegex(camelName) {
|
|
24
|
+
return new RegExp(
|
|
25
|
+
`[ \\t]*\\{%\\s*elif\\s+title\\s*==\\s*"${camelName}"\\s*%\\}[\\s\\S]*?(?=[ \\t]*\\{%\\s*(?:elif|else|endif))`,
|
|
7
26
|
);
|
|
8
27
|
}
|
|
9
28
|
|
|
29
|
+
// --- Public API ---
|
|
30
|
+
|
|
31
|
+
// Inserts a new elif block before {% else %} for the given page
|
|
10
32
|
function addLayout(pageName) {
|
|
11
|
-
const
|
|
12
|
-
if (!
|
|
33
|
+
const content = readIncludes();
|
|
34
|
+
if (!content) return;
|
|
13
35
|
|
|
14
|
-
|
|
36
|
+
const camelName = toCamelCase(pageName);
|
|
15
37
|
|
|
38
|
+
// Skip if the block already exists
|
|
16
39
|
if (content.includes(`{% elif title == "${camelName}" %}`)) return;
|
|
17
40
|
|
|
18
|
-
const newElif =
|
|
19
|
-
|
|
41
|
+
const newElif =
|
|
42
|
+
`{% elif title == "${camelName}" %}\n` +
|
|
43
|
+
` {# Insert your includes under this page #}\n` +
|
|
44
|
+
` {#{% include "component.njk" %}#}\n\n`;
|
|
20
45
|
|
|
21
|
-
|
|
46
|
+
writeIncludes(content.replace('{% else %}', `${newElif}{% else %}`));
|
|
22
47
|
console.log(`[UPDATED] Layout block added for "${camelName}".`);
|
|
23
48
|
}
|
|
24
49
|
|
|
50
|
+
// Removes the elif block for the given page, then collapses extra blank lines
|
|
25
51
|
function removeLayout(pageName) {
|
|
26
|
-
const
|
|
27
|
-
if (!
|
|
28
|
-
|
|
29
|
-
let content = fileSystem.readFileSync(INCLUDES_PATH, 'utf8');
|
|
52
|
+
const content = readIncludes();
|
|
53
|
+
if (!content) return;
|
|
30
54
|
|
|
31
|
-
const
|
|
55
|
+
const camelName = toCamelCase(pageName);
|
|
56
|
+
const regex = buildElifRegex(camelName);
|
|
32
57
|
|
|
33
58
|
if (!regex.test(content)) {
|
|
34
59
|
console.log(`[DEBUG] Layout block for "${camelName}" not found. Skipped.`);
|
|
35
60
|
return;
|
|
36
61
|
}
|
|
37
62
|
|
|
38
|
-
|
|
39
|
-
|
|
63
|
+
// Build a fresh regex instance for replace to avoid stale lastIndex
|
|
64
|
+
const updated = content
|
|
65
|
+
.replace(buildElifRegex(camelName), '')
|
|
66
|
+
.replace(/\n\s*\n\s*\n/g, '\n\n');
|
|
40
67
|
|
|
41
|
-
|
|
68
|
+
writeIncludes(updated);
|
|
42
69
|
console.log(`[CLEANED] Layout block removed for "${camelName}".`);
|
|
43
70
|
}
|
|
44
71
|
|
|
45
|
-
|
|
72
|
+
// Renames a layout block by removing the old one and inserting a new one
|
|
73
|
+
function renameLayout(oldName, newName) {
|
|
74
|
+
removeLayout(oldName);
|
|
75
|
+
addLayout(newName);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { addLayout, removeLayout, renameLayout };
|
|
@@ -4,11 +4,25 @@ const path = require('path');
|
|
|
4
4
|
const ELEVENTY_CONFIG = path.resolve(__dirname, '../../.eleventy.js');
|
|
5
5
|
const PACKAGE_JSON = path.resolve(__dirname, '../../package.json');
|
|
6
6
|
|
|
7
|
+
// Regex to locate the OUTPUT_DIR declaration in .eleventy.js.
|
|
8
|
+
// Extracted as a constant to avoid writing it by hand in multiple places.
|
|
9
|
+
const OUTPUT_DIR_REGEX = /const OUTPUT_DIR\s*=\s*['"`]([^'"`]*)['"`]/;
|
|
10
|
+
|
|
11
|
+
// --- Helpers ---
|
|
12
|
+
|
|
13
|
+
// Reads OUTPUT_DIR value from a given file content string, or returns null
|
|
14
|
+
function parseOutputDir(content) {
|
|
15
|
+
const match = content.match(OUTPUT_DIR_REGEX);
|
|
16
|
+
return match ? match[1] : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// --- Updaters ---
|
|
20
|
+
|
|
7
21
|
function updateEleventyConfig(newPath) {
|
|
8
|
-
|
|
22
|
+
const content = fs.readFileSync(ELEVENTY_CONFIG, 'utf-8');
|
|
9
23
|
|
|
10
24
|
const updated = content.replace(
|
|
11
|
-
|
|
25
|
+
OUTPUT_DIR_REGEX,
|
|
12
26
|
`const OUTPUT_DIR = "${newPath}"`
|
|
13
27
|
);
|
|
14
28
|
|
|
@@ -23,34 +37,37 @@ function updateEleventyConfig(newPath) {
|
|
|
23
37
|
}
|
|
24
38
|
|
|
25
39
|
function updatePackageJson(newPath) {
|
|
26
|
-
const
|
|
27
|
-
const pkg = JSON.parse(raw);
|
|
40
|
+
const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf-8'));
|
|
28
41
|
|
|
42
|
+
// Reconstruct all output-dependent scripts from scratch to avoid
|
|
43
|
+
// partial string replacement bugs on the outdir flag
|
|
29
44
|
pkg.scripts['build:css'] = `sass src/frontend/scss:${newPath}/css --no-source-map --style=compressed --quiet`;
|
|
30
|
-
pkg.scripts['build:js']
|
|
45
|
+
pkg.scripts['build:js'] = `esbuild "src/frontend/js/pages/*.js" --bundle --outdir=${newPath}/js/pages --minify`;
|
|
31
46
|
pkg.scripts['serve:css'] = `sass --watch src/frontend/scss:${newPath}/css --no-source-map --quiet`;
|
|
32
|
-
pkg.scripts['serve:js']
|
|
47
|
+
pkg.scripts['serve:js'] = `esbuild "src/frontend/js/pages/*.js" --bundle --outdir=${newPath}/js/pages --watch`;
|
|
33
48
|
|
|
34
49
|
fs.writeFileSync(PACKAGE_JSON, JSON.stringify(pkg, null, 2), 'utf-8');
|
|
35
50
|
console.log(`(✓) package.json updated → ${newPath}`);
|
|
36
51
|
return true;
|
|
37
52
|
}
|
|
38
53
|
|
|
54
|
+
// --- Public API ---
|
|
55
|
+
|
|
39
56
|
function updateOutputPath(newPath) {
|
|
40
57
|
const trimmed = newPath.trim().replace(/\\/g, '/');
|
|
41
58
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
normalizedPath = trimmed.replace(/\/$/, '') + '/' + projectName + '-out';
|
|
48
|
-
}
|
|
59
|
+
// Normalize the path: bare "." becomes "out", everything else gets a
|
|
60
|
+
// project-scoped suffix to avoid collisions
|
|
61
|
+
const normalizedPath = trimmed === '.'
|
|
62
|
+
? 'out'
|
|
63
|
+
: `${trimmed.replace(/\/$/, '')}/${path.basename(process.cwd())}-out`;
|
|
49
64
|
|
|
65
|
+
// Read the config once and reuse it to get the old path —
|
|
66
|
+
// avoids a second disk read inside updateEleventyConfig
|
|
50
67
|
const eleventyContent = fs.readFileSync(ELEVENTY_CONFIG, 'utf-8');
|
|
51
|
-
const
|
|
52
|
-
const oldPath = match ? match[1] : null;
|
|
68
|
+
const oldPath = parseOutputDir(eleventyContent);
|
|
53
69
|
|
|
70
|
+
// Delete the old output folder if it exists
|
|
54
71
|
if (oldPath) {
|
|
55
72
|
const oldAbsPath = path.resolve(__dirname, '../../', oldPath);
|
|
56
73
|
if (fs.existsSync(oldAbsPath)) {
|
|
@@ -67,19 +84,11 @@ function updateOutputPath(newPath) {
|
|
|
67
84
|
updateEleventyConfig(normalizedPath);
|
|
68
85
|
}
|
|
69
86
|
|
|
87
|
+
// Returns the current OUTPUT_DIR value from .eleventy.js, or null on failure
|
|
70
88
|
function getCurrentOutputPath() {
|
|
71
89
|
try {
|
|
72
90
|
const content = fs.readFileSync(ELEVENTY_CONFIG, 'utf-8');
|
|
73
|
-
|
|
74
|
-
if (!match) return null;
|
|
75
|
-
|
|
76
|
-
const outputDir = match[1];
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const parent = path.dirname(outputDir);
|
|
80
|
-
const projectName = path.basename(outputDir);
|
|
81
|
-
|
|
82
|
-
return `${parent}/${projectName}`;
|
|
91
|
+
return parseOutputDir(content);
|
|
83
92
|
} catch {
|
|
84
93
|
return null;
|
|
85
94
|
}
|
|
@@ -1,45 +1,49 @@
|
|
|
1
|
-
const fileSystem = require(
|
|
2
|
-
const path = require(
|
|
1
|
+
const fileSystem = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
3
|
|
|
4
|
-
const { addSiteData, removeSiteData } = require(
|
|
5
|
-
const { addLayout, removeLayout } = require(
|
|
4
|
+
const { addSiteData, removeSiteData, renameSiteData } = require('./updateData');
|
|
5
|
+
const { addLayout, removeLayout, renameLayout } = require('./updateIncludes');
|
|
6
|
+
const { getCurrentOutputPath } = require('./updateOutputPath');
|
|
7
|
+
const { toCamelCase } = require('./utils');
|
|
6
8
|
|
|
7
9
|
const TEMPLATES_DIR = path.join(__dirname, '..', 'res', 'templates');
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// --- Helpers ---
|
|
12
|
+
|
|
13
|
+
// Returns the three file targets (scss, js, njk) for a given page name
|
|
14
|
+
function getPageTargets(pageName) {
|
|
15
|
+
const camelName = toCamelCase(pageName);
|
|
16
|
+
return [
|
|
17
|
+
{ folder: 'src/frontend/scss/pages', templateFile: 'template.scss', fileName: `${camelName}.scss` },
|
|
18
|
+
{ folder: 'src/frontend/js/pages', templateFile: 'template.js', fileName: `${camelName}.js` },
|
|
19
|
+
{ folder: 'src/frontend/_routes', templateFile: 'template.njk', fileName: `${pageName}.njk` },
|
|
20
|
+
];
|
|
13
21
|
}
|
|
14
22
|
|
|
23
|
+
// --- Public API ---
|
|
24
|
+
|
|
15
25
|
function addPage(pageName) {
|
|
16
26
|
const camelName = toCamelCase(pageName);
|
|
17
27
|
|
|
18
|
-
|
|
19
|
-
{ folder: "src/frontend/scss/pages", templateFile: "template.scss", fileName: `${camelName}.scss` },
|
|
20
|
-
{ folder: "src/frontend/js/pages", templateFile: "template.js", fileName: `${camelName}.js` },
|
|
21
|
-
{ folder: "src/frontend/_routes", templateFile: "template.njk", fileName: `${pageName}.njk` },
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
targets.forEach(({ folder, templateFile, fileName }) => {
|
|
28
|
+
getPageTargets(pageName).forEach(({ folder, templateFile, fileName }) => {
|
|
25
29
|
const destPath = path.join(folder, fileName);
|
|
26
30
|
fileSystem.mkdirSync(folder, { recursive: true });
|
|
27
31
|
|
|
28
|
-
if (
|
|
29
|
-
const srcPath = path.join(TEMPLATES_DIR, templateFile);
|
|
32
|
+
if (fileSystem.existsSync(destPath)) return;
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
let content = fileSystem.readFileSync(srcPath, 'utf8');
|
|
33
|
-
content = content
|
|
34
|
-
.replace(/^title:.*$/m, `title: "${camelName}"`)
|
|
35
|
-
.replace(/^permalink:.*$/m, `permalink: "/${pageName}/"`);
|
|
36
|
-
fileSystem.writeFileSync(destPath, content);
|
|
37
|
-
} else {
|
|
38
|
-
fileSystem.copyFileSync(srcPath, destPath);
|
|
39
|
-
}
|
|
34
|
+
const srcPath = path.join(TEMPLATES_DIR, templateFile);
|
|
40
35
|
|
|
41
|
-
|
|
36
|
+
if (templateFile === 'template.njk') {
|
|
37
|
+
// Patch the frontmatter placeholders with the actual page values
|
|
38
|
+
const content = fileSystem.readFileSync(srcPath, 'utf8')
|
|
39
|
+
.replace(/^title:.*$/m, `title: "${camelName}"`)
|
|
40
|
+
.replace(/^permalink:.*$/m, `permalink: "/${pageName}/"`);
|
|
41
|
+
fileSystem.writeFileSync(destPath, content);
|
|
42
|
+
} else {
|
|
43
|
+
fileSystem.copyFileSync(srcPath, destPath);
|
|
42
44
|
}
|
|
45
|
+
|
|
46
|
+
console.log(`[created file] ${destPath}`);
|
|
43
47
|
});
|
|
44
48
|
|
|
45
49
|
addLayout(pageName);
|
|
@@ -50,10 +54,11 @@ function renamePage(oldName, newName) {
|
|
|
50
54
|
const oldCamel = toCamelCase(oldName);
|
|
51
55
|
const newCamel = toCamelCase(newName);
|
|
52
56
|
|
|
57
|
+
// Use consistent src/frontend/ paths (matching addPage)
|
|
53
58
|
const filesToRename = [
|
|
54
|
-
{ src: `src/scss/pages/${oldCamel}.scss`, dest: `src/scss/pages/${newCamel}.scss` },
|
|
55
|
-
{ src: `src/js/pages/${oldCamel}.js`, dest: `src/js/pages/${newCamel}.js` },
|
|
56
|
-
{ src: `src/_routes/${oldName}.njk`, dest: `src/_routes/${newName}.njk` },
|
|
59
|
+
{ src: `src/frontend/scss/pages/${oldCamel}.scss`, dest: `src/frontend/scss/pages/${newCamel}.scss` },
|
|
60
|
+
{ src: `src/frontend/js/pages/${oldCamel}.js`, dest: `src/frontend/js/pages/${newCamel}.js` },
|
|
61
|
+
{ src: `src/frontend/_routes/${oldName}.njk`, dest: `src/frontend/_routes/${newName}.njk` },
|
|
57
62
|
];
|
|
58
63
|
|
|
59
64
|
filesToRename.forEach(({ src, dest }) => {
|
|
@@ -65,29 +70,30 @@ function renamePage(oldName, newName) {
|
|
|
65
70
|
console.log(`[renamed] ${src} → ${dest}`);
|
|
66
71
|
});
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
addSiteData(newName);
|
|
73
|
+
// Use atomic rename helpers instead of remove + add separately
|
|
74
|
+
renameLayout(oldName, newName);
|
|
75
|
+
renameSiteData(oldName, newName);
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
function removePage(pageName) {
|
|
75
79
|
const camelName = toCamelCase(pageName);
|
|
76
|
-
|
|
80
|
+
|
|
81
|
+
// Read the actual current output dir instead of hardcoding "out"
|
|
82
|
+
const OUTPUT_DIR = getCurrentOutputPath() || 'out';
|
|
77
83
|
|
|
78
84
|
const filesToDelete = [
|
|
79
85
|
`src/frontend/scss/pages/${camelName}.scss`,
|
|
80
86
|
`src/frontend/js/pages/${camelName}.js`,
|
|
81
87
|
`src/frontend/_routes/${pageName}.njk`,
|
|
82
|
-
path.join(OUTPUT_DIR,
|
|
83
|
-
path.join(OUTPUT_DIR,
|
|
88
|
+
path.join(OUTPUT_DIR, 'js/pages', `${camelName}.js`),
|
|
89
|
+
path.join(OUTPUT_DIR, 'css/pages', `${camelName}.css`),
|
|
84
90
|
path.join(OUTPUT_DIR, `${pageName}.html`),
|
|
85
|
-
path.join(OUTPUT_DIR,
|
|
91
|
+
path.join(OUTPUT_DIR, 'pages', `${pageName}.html`),
|
|
86
92
|
];
|
|
87
93
|
|
|
88
94
|
const foldersToDelete = [
|
|
89
95
|
path.join(OUTPUT_DIR, pageName),
|
|
90
|
-
path.join(OUTPUT_DIR,
|
|
96
|
+
path.join(OUTPUT_DIR, 'pages', pageName),
|
|
91
97
|
];
|
|
92
98
|
|
|
93
99
|
filesToDelete.forEach(f => {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Shared utility functions used across modules
|
|
2
|
+
|
|
3
|
+
// Converts a kebab-case or snake_case string to camelCase.
|
|
4
|
+
// Uses slice(1) to remove the delimiter, avoiding the double-replace bug
|
|
5
|
+
// that occurs when using .replace('-', '') without the /g flag.
|
|
6
|
+
function toCamelCase(str) {
|
|
7
|
+
return str.toLowerCase().replace(/[-_][a-z0-9]/g, (group) =>
|
|
8
|
+
group.slice(1).toUpperCase()
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = { toCamelCase };
|
package/bin/create.js
CHANGED
|
@@ -19,11 +19,11 @@ const PROJECT_PACKAGE = {
|
|
|
19
19
|
version: '1.0.47',
|
|
20
20
|
private: true,
|
|
21
21
|
scripts: {
|
|
22
|
-
"build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet",
|
|
22
|
+
"build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet --load-path=node_modules",
|
|
23
23
|
"build:js": "esbuild \"src/frontend/js/pages/*.js\" --bundle --outdir=out/js/pages --minify",
|
|
24
24
|
"build:11ty": "eleventy",
|
|
25
25
|
"build": "npm run build:css && npm run build:js && npm run build:11ty",
|
|
26
|
-
"serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet",
|
|
26
|
+
"serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet --load-path=node_modules",
|
|
27
27
|
"serve:js": "esbuild \"src/frontend/js/pages/*.js\" --bundle --outdir=out/js/pages --watch",
|
|
28
28
|
"serve:11ty": "eleventy --serve --quiet",
|
|
29
29
|
"clean": "node _tools/cleanOutput.js",
|
package/docs/Assistant CLI.md
CHANGED
|
@@ -1,36 +1 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
# Assistant CLI
|
|
4
|
-
|
|
5
|
-
`assistant.js` is a CLI tool to manage pages and project configuration without editing files manually.
|
|
6
|
-
|
|
7
|
-
## Run
|
|
8
|
-
```bash
|
|
9
|
-
node assistant.js
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## Options
|
|
13
|
-
|
|
14
|
-
### 1. Create a page
|
|
15
|
-
Enter a page name in kebab-case (e.g. `contact-us`). The CLI generates:
|
|
16
|
-
|
|
17
|
-
| File | Purpose |
|
|
18
|
-
|---|---|
|
|
19
|
-
| `src/pages/contact-us.njk` | Page template |
|
|
20
|
-
| `src/scss/pages/contactUs.scss` | Page styles |
|
|
21
|
-
| `src/js/pages/contactUs.js` | Page scripts |
|
|
22
|
-
|
|
23
|
-
It also registers the page automatically in:
|
|
24
|
-
- `src/layouts/includes.njk` — adds an `elif` block for component routing
|
|
25
|
-
- `src/data/site.json` — adds an SEO entry under `pages`
|
|
26
|
-
|
|
27
|
-
> Protected pages (`homepage`, `404`) cannot be created or removed.
|
|
28
|
-
|
|
29
|
-
### 2. Remove a page
|
|
30
|
-
Enter the page name to delete all related files and clean up registrations.
|
|
31
|
-
|
|
32
|
-
### 3. Configure output path
|
|
33
|
-
Change the build output directory. Updates both `.eleventy.js` and `package.json` automatically.
|
|
34
|
-
|
|
35
|
-
## Naming conventions
|
|
36
|
-
Page names are always kebab-case for URLs and file names (`contact-us`), and camelCase for SCSS and JS (`contactUs`). The CLI handles the conversion automatically.
|
|
1
|
+
# ...
|
package/docs/Backend.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# ...
|
package/docs/Components.md
CHANGED
|
@@ -1,62 +1,83 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Nunjucks (HTML) Components
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## What is Nunjucks
|
|
4
|
+
|
|
5
|
+
Nunjucks (`.njk`) is an HTML file that supports logic like variables, `if` statements, and `for` loops. It can extend a base layout and include other `.njk` components
|
|
4
6
|
|
|
5
7
|
## Create a component
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
Create a new `.njk` file anywhere inside `src/frontend/components/`. You can organize them into subfolders freely
|
|
8
10
|
|
|
9
11
|
```
|
|
10
|
-
src/components/
|
|
12
|
+
src/frontend/components/
|
|
13
|
+
├── global/
|
|
14
|
+
├── layouts/
|
|
15
|
+
├── modals/
|
|
16
|
+
│ └── privacyModal.njk # You can move it to a modals/subfolder
|
|
17
|
+
├── welcome.njk
|
|
11
18
|
```
|
|
12
19
|
|
|
13
|
-
##
|
|
20
|
+
## Include a component
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
To render a component inside a page, navigate to `src/frontend/components/layouts/` and edit `includes.njk`
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
│ └── contactUs.njk
|
|
23
|
-
└── modals/ # Shared modals (privacy, cookies, etc.)
|
|
24
|
-
└── privacyModal.njk
|
|
25
|
-
```
|
|
24
|
+
### includes.njk <small>(`src/frontend/components/layouts/`)</small>
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
{% if title == "homepage" %}
|
|
28
|
+
{% include "welcome.njk" %}
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
{% elif title == "examplePage" %}
|
|
31
|
+
{% include "exampleComponent1.njk" %}
|
|
32
|
+
{% include "subfolder/exampleComponent2.njk" %}
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
{% include "
|
|
31
|
-
{
|
|
34
|
+
{% else %}
|
|
35
|
+
{% include "404/_404.njk" %}
|
|
36
|
+
{{ content | safe }}
|
|
37
|
+
{% endif %}
|
|
32
38
|
```
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
Add a new `{% elif %}` block for each page, listing its components in order. If a component lives in a subfolder, specify the relative path accordingly
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
> ⚠️ A new `elif` block is automatically added when you create a page via the Assistant CLI
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
```
|
|
44
|
+
> ⚠️ If you move or delete a component, always update `includes.njk` or the site will break
|
|
45
|
+
|
|
46
|
+
## Nest components
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
A component can include other components. This is useful for breaking complex sections into smaller, reusable pieces.
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{% include "
|
|
50
|
+
### exampleComponent.njk
|
|
51
|
+
```js
|
|
52
|
+
<section class="hero">
|
|
53
|
+
{% include "ui/heroTitle.njk" %}
|
|
54
|
+
{% include "ui/heroButton.njk" %}
|
|
55
|
+
</section>
|
|
49
56
|
```
|
|
50
57
|
|
|
58
|
+
> The same path rules apply: if the included component is in a subfolder, specify the full relative path.
|
|
59
|
+
|
|
51
60
|
## Global components
|
|
52
61
|
|
|
53
|
-
Header and footer live in `src/components/global/` and are automatically included in every page via `base.njk`. Edit them to change the site-wide layout
|
|
62
|
+
Header and footer live in `src/frontend/components/global/` and are automatically included in every page via `base.njk`. Edit them to change the site-wide layout
|
|
54
63
|
|
|
55
64
|
## Site data in components
|
|
56
65
|
|
|
57
|
-
All values
|
|
66
|
+
All values defined in `src/data/site.json` are globally available in every component via `{{ site.* }}`
|
|
67
|
+
|
|
68
|
+
### site.json <small>(`src/data/`)</small>
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"title": "My Site",
|
|
72
|
+
"logo": "/img/logo.png",
|
|
73
|
+
"legal": {
|
|
74
|
+
"privacy": "/privacy"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
58
78
|
|
|
59
|
-
|
|
79
|
+
### Usage in any `.njk` file
|
|
80
|
+
```js
|
|
60
81
|
<p>{{ site.title }}</p>
|
|
61
82
|
<a href="{{ site.legal.privacy }}">Privacy Policy</a>
|
|
62
83
|
<img src="{{ site.logo }}" alt="{{ site.title }}">
|
package/docs/Head and SEO.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Head & SEO
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
This json holds global settings used across all pages in `base.njk` and other components:
|
|
5
5
|
|
|
6
|
-
### site.json
|
|
6
|
+
### site.json <small>`(src/frontend/data/)`</small>
|
|
7
7
|
```json
|
|
8
8
|
"site_name": "Site name",
|
|
9
9
|
"title": "Site title",
|
|
@@ -25,7 +25,7 @@ Each page entry is keyed by its camelCase `title` from the front matter:
|
|
|
25
25
|
|
|
26
26
|
If you don't want to use a particular cdn inserting it in `base.njk` for all pages, you can add extra specific cdn (css, js) by inserting the link in each page of site.json separating them with a `,` and setting them in ""
|
|
27
27
|
|
|
28
|
-
### site.json
|
|
28
|
+
### site.json <small>`(src/frontend/data/)`</small>
|
|
29
29
|
```json
|
|
30
30
|
"pages": {
|
|
31
31
|
...
|
|
@@ -35,10 +35,37 @@ If you don't want to use a particular cdn inserting it in `base.njk` for all pag
|
|
|
35
35
|
"description": "description"
|
|
36
36
|
},
|
|
37
37
|
"cdn": {
|
|
38
|
-
|
|
39
|
-
"
|
|
38
|
+
// You can leave the [] empty
|
|
39
|
+
"css": ["https://example1.com/lib.min.css", "https://example2.com/lib.min.css"],
|
|
40
|
+
"js": ["https://example1.com/lib.min.js", "https://example2.com/lib.min.js"]
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
...
|
|
43
44
|
}
|
|
44
|
-
```
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## AI & SEO bots
|
|
48
|
+
|
|
49
|
+
`llms.txt` and `robots.txt` are generated automatically from `site.json` via their respective `.njk` files — no manual editing needed.
|
|
50
|
+
|
|
51
|
+
| File | Purpose | Reachable at |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `llms.njk` | Tells AI models what your site is about | `yoursite.com/llms.txt` |
|
|
54
|
+
| `robots.njk` | Controls search engine crawling | `yoursite.com/robots.txt` |
|
|
55
|
+
|
|
56
|
+
To customize them, edit `src/llms.njk` or `src/robots.njk` directly.
|
|
57
|
+
|
|
58
|
+
## Configuration field description
|
|
59
|
+
|
|
60
|
+
| Field | Purpose |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `site_name` | Brand name (used in meta tags) |
|
|
63
|
+
| `title` | Default page title |
|
|
64
|
+
| `description` | Default meta description |
|
|
65
|
+
| `keywords` | Default meta keywords |
|
|
66
|
+
| `domain` / `url` | Used for canonical URLs and og:url |
|
|
67
|
+
| `lang` | HTML `lang` attribute |
|
|
68
|
+
| `author` | Meta author tag |
|
|
69
|
+
| `data_bs_theme` | Bootstrap color scheme (`light` / `dark`) |
|
|
70
|
+
| `favicon` | Path to the favicon |
|
|
71
|
+
| `logo` | Path to the logo (available as `{{ site.logo }}`) |
|
package/docs/Javascript.md
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
# To be finished...
|
|
2
|
-
|
|
3
1
|
# JavaScript
|
|
4
2
|
|
|
5
|
-
## Page
|
|
3
|
+
## Page JS
|
|
4
|
+
|
|
5
|
+
Each page has its own JS entry point in `src/frontend/js/pages/`
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
It is bundled and minified by esbuild and loaded automatically by `base.njk`
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Import only what the page needs.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
### examplePage.js <small>(`src/frontend/js/pages/`)</small>
|
|
12
|
+
```js
|
|
13
|
+
import { initLangSwitcher } from '../modules/langSwitcher.js';
|
|
14
|
+
import { showNotification } from '../modules/notification.js';
|
|
15
|
+
|
|
16
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
17
|
+
initLangSwitcher();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
showNotification("Page loaded", "success", 3000);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Modules
|
|
24
|
+
|
|
25
|
+
Modules live in `src/frontend/js/modules/`. Some must be called inside `DOMContentLoaded` as they interact with the DOM; others create elements dynamically and can be called anywhere.
|
|
26
|
+
|
|
27
|
+
### Call inside `DOMContentLoaded`
|
|
12
28
|
|
|
13
29
|
| Module | Function |
|
|
14
30
|
|---|---|
|
|
@@ -17,26 +33,13 @@ Each page has a JS entry point in `src/js/pages/`. It is bundled by esbuild and
|
|
|
17
33
|
| `modules/forms/textAreaAutoExpand.js` | `initTextAreaAutoExpand()` |
|
|
18
34
|
| `modules/forms/normalizePhoneNumber.js` | `initNormalizePhoneNumber()` |
|
|
19
35
|
|
|
20
|
-
|
|
36
|
+
### Call anywhere
|
|
21
37
|
|
|
22
38
|
| Module | Function |
|
|
23
39
|
|---|---|
|
|
24
40
|
| `modules/notification.js` | `showNotification(text, type, duration)` |
|
|
25
41
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```js
|
|
29
|
-
import { initLangSwitcher } from '../modules/langSwitcher.js';
|
|
30
|
-
import { showNotification } from '../modules/notification.js';
|
|
31
|
-
|
|
32
|
-
document.addEventListener("DOMContentLoaded", () => {
|
|
33
|
-
initLangSwitcher();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
showNotification("Page loaded", "success", 3000);
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## `showNotification` reference
|
|
42
|
+
### `showNotification` parameters
|
|
40
43
|
|
|
41
44
|
| Parameter | Type | Default | Values |
|
|
42
45
|
|---|---|---|---|
|
|
@@ -46,8 +49,21 @@ showNotification("Page loaded", "success", 3000);
|
|
|
46
49
|
|
|
47
50
|
## Adding a module
|
|
48
51
|
|
|
49
|
-
Create a file in `src/js/modules
|
|
52
|
+
Create a new `.js` file in `src/frontend/js/modules/`. You can organize them into subfolders freely.
|
|
53
|
+
|
|
54
|
+
Use ESM syntax — esbuild handles the bundling:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
// _yourModule.js
|
|
58
|
+
export function yourFunction() {
|
|
59
|
+
// ...
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then import it in the pages that need it:
|
|
50
64
|
|
|
51
|
-
|
|
65
|
+
```js
|
|
66
|
+
import { yourFunction } from '../modules/yourModule.js';
|
|
67
|
+
```
|
|
52
68
|
|
|
53
|
-
|
|
69
|
+
> ⚠️ Files inside `_tools/` run directly in Node.js without a bundler — use CommonJS (`require` / `module.exports`) there, not ESM.
|
|
@@ -61,7 +61,7 @@ To enable/disable them you have to modify 3 files around the project by just com
|
|
|
61
61
|
{# Bulma — no JS needed #}
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
### 3.
|
|
64
|
+
### 3. .eleventy.js
|
|
65
65
|
|
|
66
66
|
```javascript
|
|
67
67
|
eleventyConfig.addPassthroughCopy({
|
|
@@ -83,8 +83,8 @@ eleventyConfig.addPassthroughCopy({
|
|
|
83
83
|
### Reducing bundle size
|
|
84
84
|
To reduce the bundle size, open the corresponding framework file (`src/frontend/scss/modules/frameworks/`) and comment out any modules you don't need
|
|
85
85
|
```scss
|
|
86
|
-
@import "
|
|
87
|
-
@import "
|
|
86
|
+
@import "bootstrap/scss/card"; // Cards
|
|
87
|
+
@import "bootstrap/scss/carousel"; // Carousel
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
## Global Variables
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-berna-stencil",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.52",
|
|
4
4
|
"description": "Eleventy boilerplate with per-page SCSS/JS pipeline, esbuild bundling, multi-framework CSS support and a built-in page management CLI",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"author": "Michele Garofalo",
|
|
@@ -20,10 +20,6 @@
|
|
|
20
20
|
"bin/",
|
|
21
21
|
"docs/",
|
|
22
22
|
"src/",
|
|
23
|
-
"src/_routes",
|
|
24
|
-
"src/assets/files",
|
|
25
|
-
"src/assets/videos",
|
|
26
|
-
"src/assets/images",
|
|
27
23
|
"_tools/",
|
|
28
24
|
".eleventy.js",
|
|
29
25
|
".eleventyignore",
|
|
@@ -51,7 +47,7 @@
|
|
|
51
47
|
"build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet",
|
|
52
48
|
"build:js": "esbuild \"src/frontend/js/pages/*.js\" --bundle --outdir=out/js/pages --minify",
|
|
53
49
|
"build:11ty": "eleventy",
|
|
54
|
-
"build": "npm run build:css && npm run build:js && npm run build:11ty",
|
|
50
|
+
"build": "npm run clean && npm run build:css && npm run build:js && npm run build:11ty",
|
|
55
51
|
"serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet",
|
|
56
52
|
"serve:js": "esbuild \"src/frontend/js/pages/*.js\" --bundle --outdir=out/js/pages --watch",
|
|
57
53
|
"serve:11ty": "eleventy --serve --quiet",
|
|
@@ -14,14 +14,14 @@ export function initTextAreaAutoExpand() {
|
|
|
14
14
|
element.addEventListener("input", () => expand(element));
|
|
15
15
|
if (element.value) expand(element);
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
function expand(element) {
|
|
19
19
|
element.rows = element.dataset.minRows;
|
|
20
20
|
while (element.scrollHeight > element.clientHeight && element.rows < MAX_ROWS) {
|
|
21
21
|
element.rows += 1;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
document.querySelectorAll("textarea").forEach(setup);
|
|
26
26
|
|
|
27
27
|
const observer = new MutationObserver((mutations) => {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
permalink: /llms.txt
|
|
3
|
+
eleventyExcludeFromCollections: true
|
|
4
|
+
---
|
|
5
|
+
# {{ site.site_name }}
|
|
6
|
+
|
|
7
|
+
> {{ site.description }}
|
|
8
|
+
|
|
9
|
+
Built by {{ site.author }} — {{ site.url }}
|
|
10
|
+
|
|
11
|
+
## Pages
|
|
12
|
+
|
|
13
|
+
- {{ site.url }}: Homepage
|
|
14
|
+
|
|
15
|
+
## Notes
|
|
16
|
+
|
|
17
|
+
- Language: {{ site.lang }}
|
|
18
|
+
- All content may be used for AI indexing unless otherwise stated
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
// ╔══════════════════════════════════════════════════════╗
|
|
2
|
-
// ║ BOOTSTRAP MODULES ║
|
|
2
|
+
// ║ ../../../../../node_modules/BOOTSTRAP MODULES ║
|
|
3
3
|
// ║ Comment out any module you don't need to improve ║
|
|
4
4
|
// ║ performance. Core section is required by all others ║
|
|
5
5
|
// ╚══════════════════════════════════════════════════════╝
|
|
6
6
|
|
|
7
7
|
// Imports can be filtered by commenting them
|
|
8
8
|
// Example:
|
|
9
|
-
// @import "../../../../../node_modules/bootstrap/scss/example
|
|
9
|
+
// @import "../../../../../node_modules/bootstrap/scss/example-../../../../../node_modules/bootstrap-module";
|
|
10
10
|
|
|
11
11
|
//==========================
|
|
12
12
|
// 1️⃣ CORE – Functions and variables
|
|
13
13
|
// ⚠️ Do not comment these out — required by all other modules
|
|
14
14
|
//==========================
|
|
15
|
-
@import "../../../../../node_modules/bootstrap/scss/functions"; // Bootstrap SCSS functions
|
|
15
|
+
@import "../../../../../node_modules/bootstrap/scss/functions"; // ../../../../../node_modules/Bootstrap SCSS functions
|
|
16
16
|
@import "../../../../../node_modules/bootstrap/scss/variables"; // Core variables (colors, spacing, fonts)
|
|
17
17
|
@import "../../../../../node_modules/bootstrap/scss/variables-dark"; // Dark theme variables
|
|
18
18
|
@import "../../../../../node_modules/bootstrap/scss/maps"; // Maps for spacing, colors, breakpoints
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
@import "../../../../../node_modules/bootstrap/scss/forms/validation"; // Validation states
|
|
78
78
|
|
|
79
79
|
//==========================
|
|
80
|
-
// 8️⃣ COMPONENTS – All Bootstrap components
|
|
80
|
+
// 8️⃣ COMPONENTS – All ../../../../../node_modules/Bootstrap components
|
|
81
81
|
//==========================
|
|
82
82
|
@import "../../../../../node_modules/bootstrap/scss/alert"; // Alerts
|
|
83
83
|
@import "../../../../../node_modules/bootstrap/scss/badge"; // Badges
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// ╔══════════════════════════════════════════════════════╗
|
|
2
|
-
// ║ BULMA MODULES ║
|
|
2
|
+
// ║ ../../../../../node_modules/BULMA MODULES ║
|
|
3
3
|
// ║ Comment out any module you don't need to improve ║
|
|
4
4
|
// ║ performance. Core section is required by all others ║
|
|
5
5
|
// ╚══════════════════════════════════════════════════════╝
|
|
6
6
|
|
|
7
7
|
// Imports can be filtered by commenting them
|
|
8
8
|
// Example:
|
|
9
|
-
// @import "../../../../../node_modules/bulma/sass/example
|
|
9
|
+
// @import "../../../../../node_modules/bulma/sass/example-../../../../../node_modules/bulma-module";
|
|
10
10
|
|
|
11
11
|
//==========================
|
|
12
12
|
// 1️⃣ CORE – Utilities and variables
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// ╔══════════════════════════════════════════════════════╗
|
|
2
|
-
// ║ FOUNDATION MODULES ║
|
|
2
|
+
// ║ ../../../../../node_modules/FOUNDATION MODULES ║
|
|
3
3
|
// ║ Comment out any module you don't need to improve ║
|
|
4
4
|
// ║ performance. Core section is required by all others ║
|
|
5
5
|
// ╚══════════════════════════════════════════════════════╝
|
|
6
6
|
|
|
7
7
|
// Imports can be filtered by commenting them
|
|
8
8
|
// Example:
|
|
9
|
-
// @import "../../../../../node_modules/foundation-sites/scss/example
|
|
9
|
+
// @import "../../../../../node_modules/foundation-sites/scss/example-../../../../../node_modules/foundation-module";
|
|
10
10
|
|
|
11
11
|
//==========================
|
|
12
12
|
// 1️⃣ CORE – Utilities and functions
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ╔══════════════════════════════════════════════════════╗
|
|
2
|
-
// ║ UIKIT MODULES ║
|
|
2
|
+
// ║ ../../../../../node_modules/UIKIT MODULES ║
|
|
3
3
|
// ║ Comment out any module you don't need to improve ║
|
|
4
4
|
// ║ performance. Core section is required by all others ║
|
|
5
5
|
// ╚══════════════════════════════════════════════════════╝
|
package/src/frontend/robots.txt
DELETED