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.
@@ -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
- function handleCreateRequest() {
20
- readerInterface.question('\n> Enter the name of the new page: ', (inputName) => {
21
- const kebabName = toKebabCase(inputName);
22
- if (!kebabName) {
23
- console.log('(!) Invalid name.');
24
- } else if (/^\d/.test(kebabName)) {
25
- console.log('(!) Invalid name. Page name cannot start with a number.');
26
- } else if (PROTECTED_PAGES.includes(kebabName)) {
27
- console.log(`(!) "${kebabName}" is a protected page and cannot be created.`);
28
- } else {
29
- addPage(kebabName, null);
30
- }
31
- displayMainMenu();
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 handleRenameRequest() {
36
- readerInterface.question('\n> Enter the name of the page to rename: ', (inputOld) => {
37
- const oldName = toKebabCase(inputOld);
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 handleRemoveRequest() {
65
- readerInterface.question('\n> Enter the name of the page to remove: ', (inputName) => {
66
- const kebabName = toKebabCase(inputName);
67
- if (!kebabName) {
68
- console.log('(!) Invalid name.');
69
- } else if (PROTECTED_PAGES.includes(kebabName)) {
70
- console.log(`(!) "${kebabName}" Is a protected page and cannot be removed.`);
71
- } else {
72
- removePage(kebabName);
73
- }
74
- displayMainMenu();
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 currentLabel = current ? ` Current path: "${current}"` : '';
81
-
82
- readerInterface.question(`${currentLabel}\n Enter the new output path: `, (inputPath) => {
83
- if (!inputPath.trim()) {
84
- console.log('(!) Invalid path.');
85
- } else {
86
- updateOutputPath(inputPath);
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
- function displayMainMenu() {
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
- readerInterface.question('\nChoose an option: ', (choice) => {
103
- const cleanChoice = choice.trim();
104
- if (cleanChoice === '1') {
105
- handleCreateRequest();
106
- } else if (cleanChoice === '2') {
107
- handleRemoveRequest();
108
- } else if (cleanChoice === '3') {
109
- handleRenameRequest();
110
- } else if (cleanChoice === '4') {
111
- handleOutputPathRequest();
112
- } else if (cleanChoice === '0') {
113
- readerInterface.close();
114
- process.exit(0);
115
- } else {
116
- console.log('(!) Invalid option.');
117
- displayMainMenu();
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.toUpperCase().replace('-', '').replace('_', '')
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
- if (!fileSystem.existsSync(SITE_DATA_PATH)) return;
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
- const niceTitle = pageName
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: niceTitle,
29
- description: "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
- fileSystem.writeFileSync(SITE_DATA_PATH, JSON.stringify(data, null, 2));
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
- if (!fileSystem.existsSync(SITE_DATA_PATH)) return;
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
- fileSystem.writeFileSync(SITE_DATA_PATH, JSON.stringify(data, null, 2));
82
+ writeSiteData(data);
55
83
  console.log(`[CLEANED] Record "${camelName}" removed.`);
56
84
  }
57
85
 
58
- module.exports = { addSiteData, removeSiteData };
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
- function toCamelCase(str) {
5
- return str.toLowerCase().replace(/[-_][a-z0-9]/g, (group) =>
6
- group.toUpperCase().replace('-', '').replace('_', '')
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 camelName = toCamelCase(pageName);
12
- if (!fileSystem.existsSync(INCLUDES_PATH)) return;
33
+ const content = readIncludes();
34
+ if (!content) return;
13
35
 
14
- let content = fileSystem.readFileSync(INCLUDES_PATH, 'utf8');
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 = `{% elif title == "${camelName}" %}\n {#{% include "component.njk" %}#}\n\n`;
19
- const updatedContent = content.replace('{% else %}', `${newElif}{% else %}`);
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
- fileSystem.writeFileSync(INCLUDES_PATH, updatedContent);
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 camelName = toCamelCase(pageName);
27
- if (!fileSystem.existsSync(INCLUDES_PATH)) return;
28
-
29
- let content = fileSystem.readFileSync(INCLUDES_PATH, 'utf8');
52
+ const content = readIncludes();
53
+ if (!content) return;
30
54
 
31
- const regex = new RegExp(`[ \\t]*\\{%\\s*elif\\s+title\\s*==\\s*"${camelName}"\\s*%\\}[\\s\\S]*?(?=[ \\t]*\\{%\\s*(?:elif|else|endif))`, 'g');
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
- let updatedContent = content.replace(regex, '');
39
- updatedContent = updatedContent.replace(/\n\s*\n\s*\n/g, '\n\n');
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
- fileSystem.writeFileSync(INCLUDES_PATH, updatedContent);
68
+ writeIncludes(updated);
42
69
  console.log(`[CLEANED] Layout block removed for "${camelName}".`);
43
70
  }
44
71
 
45
- module.exports = { addLayout, removeLayout };
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
- let content = fs.readFileSync(ELEVENTY_CONFIG, 'utf-8');
22
+ const content = fs.readFileSync(ELEVENTY_CONFIG, 'utf-8');
9
23
 
10
24
  const updated = content.replace(
11
- /const OUTPUT_DIR\s*=\s*['"`][^'"`]*['"`]/,
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 raw = fs.readFileSync(PACKAGE_JSON, 'utf-8');
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'] = `esbuild "src/frontend/js/pages/*.js" --bundle --outdir=${newPath}/js/pages --minify`;
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'] = `esbuild "src/frontend/js/pages/*.js" --bundle --outdir=${newPath}/js/pages --watch`;
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
- let normalizedPath;
43
- if (trimmed === '.') {
44
- normalizedPath = 'out';
45
- } else {
46
- const projectName = path.basename(process.cwd());
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 match = eleventyContent.match(/const OUTPUT_DIR\s*=\s*['"`]([^'"`]*)['"`]/);
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
- const match = content.match(/const OUTPUT_DIR\s*=\s*['"`]([^'"`]*)['"`]/);
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("fs");
2
- const path = require("path");
1
+ const fileSystem = require('fs');
2
+ const path = require('path');
3
3
 
4
- const { addSiteData, removeSiteData } = require("./updateData");
5
- const { addLayout, removeLayout } = require("./updateIncludes");
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
- function toCamelCase(str) {
10
- return str.toLowerCase().replace(/[-_][a-z0-9]/g, (group) =>
11
- group.toUpperCase().replace('-', '').replace('_', '')
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
- const targets = [
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 (!fileSystem.existsSync(destPath)) {
29
- const srcPath = path.join(TEMPLATES_DIR, templateFile);
32
+ if (fileSystem.existsSync(destPath)) return;
30
33
 
31
- if (templateFile === "template.njk") {
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
- console.log(`[created file] ${destPath}`);
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
- removeLayout(oldName);
69
- addLayout(newName);
70
- removeSiteData(oldName);
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
- const OUTPUT_DIR = "out";
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, "js/pages", `${camelName}.js`),
83
- path.join(OUTPUT_DIR, "css/pages", `${camelName}.css`),
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, "pages", `${pageName}.html`),
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, "pages", pageName),
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",
@@ -1,36 +1 @@
1
- # To be finished...
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
+ # ...
@@ -0,0 +1 @@
1
+ # ...
@@ -1,62 +1,83 @@
1
- # To be finished...
1
+ # Nunjucks (HTML) Components
2
2
 
3
- # Components
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
- Add a Nunjucks file in `src/components/`:
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/myComponent.njk
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
- ## Subfolders
20
+ ## Include a component
14
21
 
15
- Components can be organized into subfolders freely. Some examples:
22
+ To render a component inside a page, navigate to `src/frontend/components/layouts/` and edit `includes.njk`
16
23
 
17
- ```
18
- src/components/
19
- ├── global/ # Header, footer — included in every page
20
- ├── pages/ # Page-specific components
21
- │ ├── homepage.njk
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
- The include path must reflect the subfolder:
30
+ {% elif title == "examplePage" %}
31
+ {% include "exampleComponent1.njk" %}
32
+ {% include "subfolder/exampleComponent2.njk" %}
28
33
 
29
- ```njk
30
- {% include "pages/homepage.njk" %}
31
- {% include "modals/privacyModal.njk" %}
34
+ {% else %}
35
+ {% include "404/_404.njk" %}
36
+ {{ content | safe }}
37
+ {% endif %}
32
38
  ```
33
39
 
34
- ## Register it for a page
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
- In `src/layouts/includes.njk`, find the `elif` block for your page and include the component:
42
+ > ⚠️ A new `elif` block is automatically added when you create a page via the Assistant CLI
37
43
 
38
- ```njk
39
- {% elif title == "myPage" %}
40
- {% include "myComponent.njk" %}
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
- Multiple components can be included in the same block:
48
+ A component can include other components. This is useful for breaking complex sections into smaller, reusable pieces.
44
49
 
45
- ```njk
46
- {% elif title == "myPage" %}
47
- {% include "hero.njk" %}
48
- {% include "features.njk" %}
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 from `src/data/site.json` are available as `{{ site.* }}`:
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
- ```njk
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 }}">
@@ -1,9 +1,9 @@
1
1
  # Head & SEO
2
2
 
3
3
 
4
- `src/frontend/data/site.json` holds global settings used across all pages in `base.njk` and other components:
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
- "css": [],
39
- "js": []
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 }}`) |
@@ -1,14 +1,30 @@
1
- # To be finished...
2
-
3
1
  # JavaScript
4
2
 
5
- ## Page files
3
+ ## Page JS
4
+
5
+ Each page has its own JS entry point in `src/frontend/js/pages/`
6
6
 
7
- Each page has a JS entry point in `src/js/pages/`. It is bundled by esbuild and loaded automatically by `base.njk`. Import only what the page needs.
7
+ It is bundled and minified by esbuild and loaded automatically by `base.njk`
8
8
 
9
- ## Module categories
9
+ Import only what the page needs.
10
10
 
11
- **Call inside `DOMContentLoaded`** — these interact with the DOM and require the page to be fully loaded:
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
- **Call anywhere** — these create DOM elements dynamically and can be called at any point after the script loads:
36
+ ### Call anywhere
21
37
 
22
38
  | Module | Function |
23
39
  |---|---|
24
40
  | `modules/notification.js` | `showNotification(text, type, duration)` |
25
41
 
26
- ## Usage example
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/`, export your function, and import it in the pages that need it. Use ESM syntax (`import` / `export`) — this code is processed by esbuild.
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
- ## `assistant_utils/`
65
+ ```js
66
+ import { yourFunction } from '../modules/yourModule.js';
67
+ ```
52
68
 
53
- These scripts run directly in Node.js without a bundler. Use CommonJS syntax (`require` / `module.exports`) here, not ESM.
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. `.eleventy.js`
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 "../../../../../node_modules/bootstrap/scss/card"; // Cards
87
- @import "../../../../../node_modules/bootstrap/scss/carousel"; // Carousel
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.50",
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",
@@ -8,6 +8,7 @@ layout: base.njk
8
8
  {% include "welcome.njk" %}
9
9
 
10
10
  {% else %}
11
+ <!-- You should not touch this else statement -->
11
12
  {% include "404/_404.njk" %}
12
13
  {{ content | safe }}
13
14
  {% endif %}
@@ -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
@@ -0,0 +1,8 @@
1
+ ---
2
+ permalink: /robots.txt
3
+ eleventyExcludeFromCollections: true
4
+ ---
5
+ Sitemap: {{ site.url }}/sitemap.xml
6
+
7
+ User-agent: *
8
+ Disallow:
@@ -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-bootstrap-module";
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-bulma-module";
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-foundation-module";
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
  // ╚══════════════════════════════════════════════════════╝
@@ -1,4 +0,0 @@
1
- Sitemap: https://yoursite.com/sitemap.xml
2
-
3
- User-agent: *
4
- Disallow: