create-berna-stencil 2.0.8 → 2.0.10
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/.eleventy.js +38 -19
- package/README.md +1 -1
- package/bin/create.js +2 -1
- package/docs/Components.md +12 -0
- package/docs/Creating pages.md +1 -2
- package/package.json +63 -61
- package/src/frontend/components/layouts/base.njk +47 -34
- package/src/frontend/components/welcome.njk +32 -668
- package/src/frontend/scss/modules/_global.scss +0 -1
package/.eleventy.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const esbuild = require("esbuild");
|
|
2
2
|
const glob = require("glob");
|
|
3
3
|
const Image = require("@11ty/eleventy-img");
|
|
4
|
+
const markdownIt = require('markdown-it');
|
|
4
5
|
const fs = require("fs");
|
|
5
6
|
const path = require("path");
|
|
6
7
|
|
|
@@ -9,32 +10,50 @@ const OUTPUT_DIR = "out";
|
|
|
9
10
|
module.exports = function (eleventyConfig) {
|
|
10
11
|
|
|
11
12
|
function copyRecursiveSync(src, dest) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
if (!fs.existsSync(src)) return;
|
|
14
|
+
if (src.includes('.git')) return;
|
|
15
|
+
const stat = fs.statSync(src);
|
|
16
|
+
if (stat.isDirectory()) {
|
|
17
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
18
|
+
for (const child of fs.readdirSync(src)) {
|
|
19
|
+
copyRecursiveSync(path.join(src, child), path.join(dest, child));
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
23
|
+
fs.copyFileSync(src, dest);
|
|
19
24
|
}
|
|
20
|
-
} else {
|
|
21
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
22
|
-
fs.copyFileSync(src, dest);
|
|
23
25
|
}
|
|
24
|
-
|
|
26
|
+
|
|
27
|
+
// =====================================================
|
|
28
|
+
// SHORTCODE — Markdown file renderer
|
|
29
|
+
// =====================================================
|
|
30
|
+
const md = markdownIt({ html: true });
|
|
31
|
+
|
|
32
|
+
eleventyConfig.addShortcode('mdFile', function(filePath) {
|
|
33
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
34
|
+
return md.render(content);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// =====================================================
|
|
38
|
+
// SHORTCODE — Markdown css
|
|
39
|
+
// =====================================================
|
|
40
|
+
eleventyConfig.addPassthroughCopy({
|
|
41
|
+
"node_modules/github-markdown-css/github-markdown-dark.css": "css/github-markdown-dark.css",
|
|
42
|
+
"node_modules/github-markdown-css/github-markdown-light.css": "css/github-markdown-light.css",
|
|
43
|
+
});
|
|
25
44
|
|
|
26
45
|
// =====================================================
|
|
27
46
|
// ESBUILD — Bundles and minifies JS files before build
|
|
28
47
|
// =====================================================
|
|
29
48
|
eleventyConfig.on("eleventy.before", async () => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
const entryPoints = glob.sync("src/frontend/js/pages/*.js");
|
|
50
|
+
await esbuild.build({
|
|
51
|
+
entryPoints,
|
|
52
|
+
bundle: true,
|
|
53
|
+
outdir: `${OUTPUT_DIR}/js/pages`,
|
|
54
|
+
minify: true,
|
|
55
|
+
});
|
|
56
|
+
copyRecursiveSync("src/backend", `${OUTPUT_DIR}/backend`);
|
|
38
57
|
});
|
|
39
58
|
|
|
40
59
|
// =====================================================
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Building a website from scratch involves a lot of moving parts: templating engin
|
|
|
12
12
|
- 📁 **Scalable structure** — a clean, opinionated project layout that grows with your needs
|
|
13
13
|
- 🌍 **Open source** — free to use, free to modify, free to share
|
|
14
14
|
|
|
15
|
-

|
|
16
16
|

|
|
17
17
|

|
|
18
18
|
|
package/bin/create.js
CHANGED
|
@@ -9,6 +9,7 @@ const templateDir = path.join(__dirname, '..');
|
|
|
9
9
|
|
|
10
10
|
const COPY_TARGETS = [
|
|
11
11
|
'src',
|
|
12
|
+
'docs',
|
|
12
13
|
'_tools',
|
|
13
14
|
'.eleventy.js',
|
|
14
15
|
'.eleventyignore',
|
|
@@ -16,7 +17,7 @@ const COPY_TARGETS = [
|
|
|
16
17
|
|
|
17
18
|
const PROJECT_PACKAGE = {
|
|
18
19
|
name: path.basename(targetDir),
|
|
19
|
-
version: '2.0.
|
|
20
|
+
version: '2.0.10',
|
|
20
21
|
private: true,
|
|
21
22
|
scripts: {
|
|
22
23
|
"build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet --load-path=node_modules",
|
package/docs/Components.md
CHANGED
|
@@ -43,6 +43,18 @@ Add a new `{% elif %}` block for each page, listing its components in order. If
|
|
|
43
43
|
|
|
44
44
|
> ⚠️ If you move or delete a component, always update `includes.njk` or the site will break
|
|
45
45
|
|
|
46
|
+
### Using Markdown files in components
|
|
47
|
+
|
|
48
|
+
You can also render a `.md` file directly inside any `.njk` component using the `mdFile` shortcode:
|
|
49
|
+
|
|
50
|
+
```njk
|
|
51
|
+
{% mdFile "docs/your-file.md" %}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The path is relative to the project root (where `.eleventy.js` lives).
|
|
55
|
+
|
|
56
|
+
> ⚠️ The file is read at build time — changes to the `.md` file trigger a rebuild in watch mode.
|
|
57
|
+
|
|
46
58
|
## Nest components
|
|
47
59
|
|
|
48
60
|
A component can include other components. This is useful for breaking complex sections into smaller, reusable pieces.
|
package/docs/Creating pages.md
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
The recommended way is via the **Assistant CLI**
|
|
4
4
|
|
|
5
5
|
## What gets created
|
|
6
|
-
+
|
|
7
6
|
For a page named `my-page`:
|
|
8
7
|
|
|
9
8
|
| File | Purpose |
|
|
@@ -22,7 +21,7 @@ For a page named `my-page`:
|
|
|
22
21
|
{% include "_myPage.njk" %}
|
|
23
22
|
```
|
|
24
23
|
|
|
25
|
-
See
|
|
24
|
+
See **Components** DOC file for more info
|
|
26
25
|
|
|
27
26
|
## URL and title
|
|
28
27
|
|
package/package.json
CHANGED
|
@@ -1,61 +1,63 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "create-berna-stencil",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "Eleventy boilerplate with per-page SCSS/JS pipeline, esbuild bundling, multi-framework CSS support and a built-in page management CLI",
|
|
5
|
-
"keywords": [],
|
|
6
|
-
"author": "Michele Garofalo",
|
|
7
|
-
"license": "Apache-2.0",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "https://github.com/rhaastrake/berna-stencil"
|
|
11
|
-
},
|
|
12
|
-
"homepage": "https://github.com/rhaastrake/berna-stencil#readme",
|
|
13
|
-
"bugs": {
|
|
14
|
-
"url": "https://github.com/rhaastrake/berna-stencil/issues"
|
|
15
|
-
},
|
|
16
|
-
"bin": {
|
|
17
|
-
"create-berna-stencil": "bin/create.js"
|
|
18
|
-
},
|
|
19
|
-
"files": [
|
|
20
|
-
"bin/",
|
|
21
|
-
"docs/",
|
|
22
|
-
"src/",
|
|
23
|
-
"_tools/",
|
|
24
|
-
".eleventy.js",
|
|
25
|
-
".eleventyignore",
|
|
26
|
-
".gitignore",
|
|
27
|
-
"LICENSE",
|
|
28
|
-
"NOTICE"
|
|
29
|
-
],
|
|
30
|
-
"engines": {
|
|
31
|
-
"node": ">=18.0.0"
|
|
32
|
-
},
|
|
33
|
-
"dependencies": {
|
|
34
|
-
"@11ty/eleventy": "^3.1.2",
|
|
35
|
-
"@11ty/eleventy-img": "^6.0.4",
|
|
36
|
-
"bootstrap": "^5.3.8",
|
|
37
|
-
"bootstrap-icons": "^1.13.1",
|
|
38
|
-
"bulma": "^1.0.4",
|
|
39
|
-
"foundation-sites": "^6.9.0",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"build:
|
|
52
|
-
"build": "
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"serve:
|
|
56
|
-
"
|
|
57
|
-
"serve
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "create-berna-stencil",
|
|
3
|
+
"version": "2.0.10",
|
|
4
|
+
"description": "Eleventy boilerplate with per-page SCSS/JS pipeline, esbuild bundling, multi-framework CSS support and a built-in page management CLI",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"author": "Michele Garofalo",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/rhaastrake/berna-stencil"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/rhaastrake/berna-stencil#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/rhaastrake/berna-stencil/issues"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"create-berna-stencil": "bin/create.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin/",
|
|
21
|
+
"docs/",
|
|
22
|
+
"src/",
|
|
23
|
+
"_tools/",
|
|
24
|
+
".eleventy.js",
|
|
25
|
+
".eleventyignore",
|
|
26
|
+
".gitignore",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"NOTICE"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@11ty/eleventy": "^3.1.2",
|
|
35
|
+
"@11ty/eleventy-img": "^6.0.4",
|
|
36
|
+
"bootstrap": "^5.3.8",
|
|
37
|
+
"bootstrap-icons": "^1.13.1",
|
|
38
|
+
"bulma": "^1.0.4",
|
|
39
|
+
"foundation-sites": "^6.9.0",
|
|
40
|
+
"github-markdown-css": "^5.9.0",
|
|
41
|
+
"glob": "^13.0.6",
|
|
42
|
+
"markdown-it": "^14.2.0",
|
|
43
|
+
"uikit": "^3.25.13"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"concurrently": "^9.2.1",
|
|
47
|
+
"esbuild": "^0.27.3",
|
|
48
|
+
"sass": "^1.77.0"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet",
|
|
52
|
+
"build:js": "esbuild \"src/frontend/js/pages/*.js\" --bundle --outdir=out/js/pages --minify",
|
|
53
|
+
"build:11ty": "eleventy",
|
|
54
|
+
"build": "npm run clean && npm run build:css && npm run build:js && npm run build:11ty",
|
|
55
|
+
"serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet",
|
|
56
|
+
"serve:js": "esbuild \"src/frontend/js/pages/*.js\" --bundle --outdir=out/js/pages --watch",
|
|
57
|
+
"serve:11ty": "eleventy --serve --quiet",
|
|
58
|
+
"clean": "node _tools/cleanOutput.js",
|
|
59
|
+
"serve": "npm run clean && concurrently \"npm run serve:11ty\" \"npm run serve:css\" \"npm run serve:js\"",
|
|
60
|
+
"assistant": "node _tools/assistant.js",
|
|
61
|
+
"postinstall": "cd src/backend/_core && composer install --quiet"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- This is the base of
|
|
1
|
+
<!-- This is the base of every page that you will create -->
|
|
2
2
|
|
|
3
3
|
<!DOCTYPE html>
|
|
4
4
|
<html lang="{{ site.lang or 'en' }}" data-bs-theme="{{ site.data_bs_theme or 'light' }}">
|
|
@@ -7,19 +7,28 @@
|
|
|
7
7
|
<meta charset="UTF-8">
|
|
8
8
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
{# Preconnect hints — establish early connections to external origins #}
|
|
11
|
+
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
|
|
12
|
+
|
|
13
|
+
{#╔══════════════════╗
|
|
14
|
+
║ PAGE DATA ║
|
|
15
|
+
╚══════════════════╝#}
|
|
16
|
+
|
|
11
17
|
{% set pageKey = title %}
|
|
12
18
|
{% set pageData = site.pages[pageKey] %}
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
{#╔══════════════════╗
|
|
21
|
+
║ SEO ║
|
|
22
|
+
╚══════════════════╝#}
|
|
23
|
+
|
|
15
24
|
<link rel="canonical" href="{{ site.url }}{{ page.url }}"/>
|
|
16
25
|
<title>{{ pageData.seo.title or title or site.title }}</title>
|
|
17
26
|
<meta name="description" content="{{ pageData.seo.description or site.description }}">
|
|
18
27
|
<meta name="keywords" content="{{ site.keywords }}">
|
|
19
28
|
<meta name="author" content="{{ site.author }}">
|
|
20
|
-
<meta name="theme-color" content=""
|
|
29
|
+
<meta name="theme-color" content="">
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
{# Open Graph #}
|
|
23
32
|
<meta property="og:title" content="{{ pageData.seo.title }}">
|
|
24
33
|
<meta property="og:description" content="{{ pageData.seo.description }}">
|
|
25
34
|
<meta property="og:type" content="website">
|
|
@@ -27,15 +36,16 @@
|
|
|
27
36
|
<meta property="og:image" content="{{ site.url }}{{ site.logo }}">
|
|
28
37
|
<meta property="og:site-name" content="{{ site.site-name }}">
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
{# Twitter Card #}
|
|
31
40
|
<meta name="twitter:card" content="summary_large_image">
|
|
32
41
|
<meta name="twitter:title" content="{{ pageData.seo.title }}">
|
|
33
42
|
<meta name="twitter:description" content="{{ pageData.seo.description }}">
|
|
34
43
|
<meta name="twitter:image" content="{{ site.url }}{{ site.logo }}">
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
{#╔══════════════════════════════════════════╗
|
|
46
|
+
║ JSON-LD — Structured data for SEO/AI ║
|
|
47
|
+
╚══════════════════════════════════════════╝#}
|
|
48
|
+
|
|
39
49
|
<script type="application/ld+json">
|
|
40
50
|
{
|
|
41
51
|
"@context": "https://schema.org",
|
|
@@ -58,67 +68,70 @@
|
|
|
58
68
|
}
|
|
59
69
|
</script>
|
|
60
70
|
|
|
61
|
-
|
|
71
|
+
{#╔══════════════════╗
|
|
72
|
+
║ FAVICON ║
|
|
73
|
+
╚══════════════════╝#}
|
|
74
|
+
|
|
62
75
|
<link rel="icon" type="image/svg+xml" href="{{ site.favicon }}">
|
|
63
|
-
<!-- (If you want to use a PNG favicon) -->
|
|
64
76
|
{#
|
|
65
77
|
<link rel="icon" type="image/png" href="/assets/favicon.png">
|
|
66
78
|
<link rel="apple-touch-icon" href="/assets/favicon.png">
|
|
67
79
|
#}
|
|
68
80
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
{#╔══════════════════╗
|
|
82
|
+
║ CSS ║
|
|
83
|
+
╚══════════════════╝#}
|
|
84
|
+
|
|
85
|
+
{# Markdown — theme-aware #}
|
|
86
|
+
{% if site.data_bs_theme == "dark" %}
|
|
87
|
+
<link rel="stylesheet" href="/css/github-markdown-dark.css">
|
|
88
|
+
{% else %}
|
|
89
|
+
<link rel="stylesheet" href="/css/github-markdown-light.css">
|
|
90
|
+
{% endif %}
|
|
91
|
+
|
|
92
|
+
{# Page CSS #}
|
|
93
|
+
<link rel="stylesheet" href="/css/pages/{{ title }}.css"/>
|
|
94
|
+
|
|
95
|
+
{# Per-page CDN CSS #}
|
|
74
96
|
{% if pageData and pageData.cdn and pageData.cdn.css %}
|
|
75
97
|
{% for link in pageData.cdn.css %}
|
|
76
98
|
<link rel="stylesheet" href="{{ link }}">
|
|
77
99
|
{% endfor %}
|
|
78
100
|
{% endif %}
|
|
101
|
+
|
|
79
102
|
</head>
|
|
80
103
|
|
|
81
104
|
<body>
|
|
82
|
-
|
|
105
|
+
|
|
83
106
|
{% include "global/header.njk" %}
|
|
84
107
|
|
|
85
|
-
<!-- Main -->
|
|
86
108
|
<main>
|
|
87
109
|
{{ content | safe }}
|
|
88
110
|
</main>
|
|
89
111
|
|
|
90
|
-
<!-- Footer -->
|
|
91
112
|
{% include "global/footer.njk" %}
|
|
92
113
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
╔═══════════════════════════════════════════╗
|
|
97
|
-
║ FRAMEWORK SCRIPTS ║
|
|
98
|
-
║ Uncomment the framework JS you are using ║
|
|
99
|
-
║ Bulma requires no JS — CSS only ║
|
|
100
|
-
╚═══════════════════════════════════════════╝
|
|
101
|
-
#}
|
|
114
|
+
{#╔══════════════════╗
|
|
115
|
+
║ SCRIPTS ║
|
|
116
|
+
╚══════════════════╝#}
|
|
102
117
|
|
|
103
|
-
{#
|
|
118
|
+
{# Framework — uncomment the one you are using #}
|
|
104
119
|
<script src="/js/bootstrap.bundle.min.js" defer></script>
|
|
105
|
-
|
|
106
|
-
{# Foundation JS — uncomment if using Foundation #}
|
|
107
120
|
{# <script src="/js/foundation.min.js" defer></script> #}
|
|
108
|
-
|
|
109
|
-
{# UIkit JS — uncomment if using UIkit #}
|
|
110
121
|
{# <script src="/js/uikit.min.js" defer></script> #}
|
|
111
122
|
{# <script src="/js/uikit-icons.min.js" defer></script> #}
|
|
112
|
-
|
|
113
123
|
{# Bulma — no JS needed #}
|
|
114
124
|
|
|
125
|
+
{# Per-page CDN JS #}
|
|
115
126
|
{% if pageData and pageData.cdn and pageData.cdn.js %}
|
|
116
127
|
{% for script in pageData.cdn.js %}
|
|
117
128
|
<script src="{{ script }}" defer></script>
|
|
118
129
|
{% endfor %}
|
|
119
130
|
{% endif %}
|
|
120
131
|
|
|
132
|
+
{# Page JS #}
|
|
121
133
|
<script src="/js/pages/{{ title }}.js"></script>
|
|
134
|
+
|
|
122
135
|
</body>
|
|
123
136
|
|
|
124
137
|
</html>
|
|
@@ -38,16 +38,16 @@
|
|
|
38
38
|
</div>
|
|
39
39
|
|
|
40
40
|
<div class="tabs-container">
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
<div id="content-welcome" class="tab-content active">
|
|
43
43
|
<div class="grid" style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
|
44
44
|
<a href="https://github.com/rhaastrake/berna-stencil" class="card" target="_blank" rel="noopener noreferrer" style="flex: 1; min-width: 250px;">
|
|
45
45
|
<svg class="card-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
46
46
|
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
|
|
47
47
|
</svg>
|
|
48
|
-
<h3>
|
|
48
|
+
<h3>GitHub repository</h3>
|
|
49
49
|
<p>Community-driven. Contributions, issues and PRs are welcome.</p>
|
|
50
|
-
<span class="card-link">Open the repository
|
|
50
|
+
<span class="card-link">Open the repository
|
|
51
51
|
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" style="margin-left:4px;">
|
|
52
52
|
<path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
|
|
53
53
|
</svg>
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
</svg>
|
|
61
61
|
<h3>Documentation</h3>
|
|
62
62
|
<p>Everything you need to get started, from setup to advanced topics and customizations</p>
|
|
63
|
-
<span class="card-link">Go to documentation
|
|
63
|
+
<span class="card-link">Go to documentation
|
|
64
64
|
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" style="margin-left:4px;">
|
|
65
65
|
<path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
|
|
66
66
|
</svg>
|
|
@@ -69,565 +69,32 @@
|
|
|
69
69
|
</div>
|
|
70
70
|
</div>
|
|
71
71
|
|
|
72
|
-
<div id="content-assistant" class="tab-content">
|
|
73
|
-
|
|
74
|
-
<h2>Assistant CLI</h2>
|
|
75
|
-
<p>An interactive CLI to manage pages without touching files manually.</p>
|
|
76
|
-
<pre><code>npm run assistant</code></pre>
|
|
77
|
-
<h3>Menu</h3>
|
|
78
|
-
<pre><code>1. Create page
|
|
79
|
-
2. Remove page
|
|
80
|
-
3. Rename page
|
|
81
|
-
4. Configure output path</code></pre>
|
|
82
|
-
<p>Use <code>CTRL/CMD + C</code> to exit.</p>
|
|
83
|
-
<h3>Create page</h3>
|
|
84
|
-
<p>Enter a page name in any format — the CLI converts it to kebab-case automatically.</p>
|
|
85
|
-
<p>For a page named <code>my-page</code>, the following files are created:</p>
|
|
86
|
-
<table>
|
|
87
|
-
<thead>
|
|
88
|
-
<tr><th>File</th><th>Purpose</th></tr>
|
|
89
|
-
</thead>
|
|
90
|
-
<tbody>
|
|
91
|
-
<tr><td><code>src/frontend/scss/pages/myPage.scss</code></td><td>SCSS entry point</td></tr>
|
|
92
|
-
<tr><td><code>src/frontend/js/pages/myPage.js</code></td><td>JS entry point</td></tr>
|
|
93
|
-
<tr><td><code>src/frontend/_routes/my-page.njk</code></td><td>Nunjucks template</td></tr>
|
|
94
|
-
</tbody>
|
|
95
|
-
</table>
|
|
96
|
-
<p>It also adds an <code>elif</code> block in <code>includes.njk</code> and a stub entry in <code>site.json</code>:</p>
|
|
97
|
-
<pre><code>"myPage": {
|
|
98
|
-
"seo": {
|
|
99
|
-
"title": "My Page",
|
|
100
|
-
"description": "description"
|
|
101
|
-
},
|
|
102
|
-
"cdn": {
|
|
103
|
-
"css": [],
|
|
104
|
-
"js": []
|
|
105
|
-
}
|
|
106
|
-
}</code></pre>
|
|
107
|
-
<h3>Remove page</h3>
|
|
108
|
-
<p>Deletes all source files for the page and cleans up the output directory, <code>includes.njk</code>, and <code>site.json</code>.</p>
|
|
109
|
-
<h3>Rename page</h3>
|
|
110
|
-
<p>Renames all three source files, updates the <code>elif</code> block in <code>includes.njk</code>, and renames the record in <code>site.json</code> while preserving all existing fields.</p>
|
|
111
|
-
<h3>Configure output path</h3>
|
|
112
|
-
<p>Updates the output directory across <code>.eleventy.js</code> and all relevant <code>package.json</code> scripts in one shot. The old output folder is deleted automatically.</p>
|
|
113
|
-
<blockquote>⚠️ <code>homepage</code> and <code>404</code> are protected — they cannot be created, removed, or renamed via the CLI.</blockquote>
|
|
114
|
-
</div>
|
|
72
|
+
<div id="content-assistant" class="tab-content markdown-body">
|
|
73
|
+
{% mdFile "docs/assistant cli.md" %}
|
|
115
74
|
</div>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<div class="markdown-body">
|
|
119
|
-
<h2>Styling with SCSS</h2>
|
|
120
|
-
<h3>Page CSS</h3>
|
|
121
|
-
<p>Each page has its own SCSS entry point in <code>src/frontend/scss/pages/</code></p>
|
|
122
|
-
<p>It must contain <code>_root.scss</code> + other modules like <code>_global.scss</code> or any other one that you need and its own specific css rules.</p>
|
|
123
|
-
<p><code>_root.scss</code> uses <code>@use</code> to enable namespaced access (<code>root.$var</code>); other modules use <code>@import</code> as they don't expose variables.</p>
|
|
124
|
-
<h4>examplePage.scss <small>(<code>src/frontend/scss/pages/</code>)</small></h4>
|
|
125
|
-
<pre><code>//==========================
|
|
126
|
-
// CSS MODULES IMPORTS
|
|
127
|
-
//==========================
|
|
128
|
-
|
|
129
|
-
@use "../modules/root" as root;
|
|
130
|
-
|
|
131
|
-
@import "../modules/global";
|
|
132
|
-
|
|
133
|
-
@import "../modules/notification";
|
|
134
|
-
|
|
135
|
-
//==========================
|
|
136
|
-
// PAGE CUSTOM CSS RULES
|
|
137
|
-
//==========================
|
|
138
|
-
|
|
139
|
-
body {
|
|
140
|
-
background-color: root.$primary;
|
|
141
|
-
}</code></pre>
|
|
142
|
-
<h3>Global Variables</h3>
|
|
143
|
-
<p>Instead of using <code>:root</code> in your custom modules or pages, the best thing to do is to centralize all your variables in a single file (that will be tree-shaken automatically by Sass).</p>
|
|
144
|
-
<h4>_root.scss <small>(<code>src/frontend/scss/modules/</code>)</small></h4>
|
|
145
|
-
<pre><code>$header-height: 10vh;
|
|
146
|
-
|
|
147
|
-
// Usage example (in any other file):
|
|
148
|
-
header {
|
|
149
|
-
height: root.$header-height;
|
|
150
|
-
}</code></pre>
|
|
151
|
-
<h3>Scss modules</h3>
|
|
152
|
-
<p>You can create your custom css modules by creating a new <code>.scss</code> file in <code>src/frontend/scss/modules/</code> (the name of the file must start with <code>_</code>).</p>
|
|
153
|
-
<p>You can create subfolders if you want to refactor the structure, but be sure to update the relative paths in the pages that import them.</p>
|
|
154
|
-
<h4>_yourModule.scss <small>(<code>src/frontend/scss/modules/subfolder/</code>)</small></h4>
|
|
155
|
-
<pre><code>@use '../root' as root;
|
|
156
|
-
|
|
157
|
-
body {
|
|
158
|
-
background-color: root.$primary;
|
|
159
|
-
}</code></pre>
|
|
160
|
-
<h4>examplePage.scss</h4>
|
|
161
|
-
<pre><code>@import "../modules/subfolder/yourModule";
|
|
162
|
-
|
|
163
|
-
// This page will now inherit the body tag rules
|
|
164
|
-
// If the same property is declared in both, the last imported one wins
|
|
165
|
-
body {
|
|
166
|
-
color: root.$dark;
|
|
167
|
-
}</code></pre>
|
|
168
|
-
<h4>Pre-existing modules</h4>
|
|
169
|
-
<table>
|
|
170
|
-
<thead>
|
|
171
|
-
<tr><th>File</th><th>Purpose</th></tr>
|
|
172
|
-
</thead>
|
|
173
|
-
<tbody>
|
|
174
|
-
<tr><td><code>_root.scss</code></td><td>Global variables (colors, spacing)</td></tr>
|
|
175
|
-
<tr><td><code>_global.scss</code></td><td>Site-wide base rules and frameworks</td></tr>
|
|
176
|
-
<tr><td><code>_typography.scss</code></td><td>Font rules</td></tr>
|
|
177
|
-
<tr><td><code>_header.scss</code></td><td>Header styles</td></tr>
|
|
178
|
-
<tr><td><code>_footer.scss</code></td><td>Footer styles</td></tr>
|
|
179
|
-
<tr><td><code>_mobile.scss</code></td><td>Media query rules</td></tr>
|
|
180
|
-
<tr><td><code>_buttons.scss</code></td><td>Style and hovers for buttons</td></tr>
|
|
181
|
-
<tr><td><code>_animations.scss</code></td><td>Keyframe animations (<code>fade-in</code>, <code>spin</code>)</td></tr>
|
|
182
|
-
<tr><td><code>_notification.scss</code></td><td>Notification component style</td></tr>
|
|
183
|
-
</tbody>
|
|
184
|
-
</table>
|
|
185
|
-
<h3>CSS Framework</h3>
|
|
186
|
-
<p>Some of the most popular css frameworks that supports scss with modules are already installed in <code>node_modules</code>.</p>
|
|
187
|
-
<p>You can choose one or none of them (more than 1 works, but you may get in various conflicts).</p>
|
|
188
|
-
<p>To enable/disable them you have to modify 3 files around the project by just commenting them.</p>
|
|
189
|
-
<h4>1. _global.scss <small>(<code>src/frontend/scss/modules/</code>)</small></h4>
|
|
190
|
-
<pre><code>@import "../modules/frameworks/bootstrap";
|
|
191
|
-
// @import "../modules/frameworks/bulma";
|
|
192
|
-
// @import "../modules/frameworks/foundation";
|
|
193
|
-
// @import "../modules/frameworks/uikit";</code></pre>
|
|
194
|
-
<h4>2. base.njk <small>(<code>src/frontend/components/layouts/</code>)</small></h4>
|
|
195
|
-
<pre><code><!-- Bootstrap -->
|
|
196
|
-
<!-- <script src="/js/bootstrap.bundle.min.js" defer></script> -->
|
|
197
|
-
|
|
198
|
-
<!-- Foundation -->
|
|
199
|
-
<!-- <script src="/js/foundation.min.js" defer></script> -->
|
|
200
|
-
|
|
201
|
-
<!-- UIkit -->
|
|
202
|
-
<!-- <script src="/js/uikit.min.js" defer></script> -->
|
|
203
|
-
<!-- <script src="/js/uikit-icons.min.js" defer></script> -->
|
|
204
|
-
|
|
205
|
-
<!-- Bulma — no JS needed --></code></pre>
|
|
206
|
-
<h4>3. .eleventy.js</h4>
|
|
207
|
-
<pre><code>eleventyConfig.addPassthroughCopy({
|
|
208
|
-
// Bootstrap
|
|
209
|
-
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js": "js/bootstrap.bundle.min.js",
|
|
210
|
-
"node_modules/bootstrap-icons/font/fonts": "css/fonts",
|
|
211
|
-
|
|
212
|
-
// Foundation
|
|
213
|
-
// "node_modules/foundation-sites/dist/js/foundation.min.js": "js/foundation.min.js",
|
|
214
|
-
|
|
215
|
-
// UIkit
|
|
216
|
-
// "node_modules/uikit/dist/js/uikit.min.js": "js/uikit.min.js",
|
|
217
|
-
// "node_modules/uikit/dist/js/uikit-icons.min.js": "js/uikit-icons.min.js",
|
|
218
|
-
|
|
219
|
-
// Bulma — CSS only, no JS passthrough needed
|
|
220
|
-
});</code></pre>
|
|
221
|
-
<h4>Reducing bundle size</h4>
|
|
222
|
-
<p>To reduce the bundle size, open the corresponding framework file (<code>src/frontend/scss/modules/frameworks/</code>) and comment out any modules you don't need.</p>
|
|
223
|
-
<pre><code>@import "bootstrap/scss/card"; // Cards
|
|
224
|
-
@import "bootstrap/scss/carousel"; // Carousel</code></pre>
|
|
225
|
-
</div>
|
|
75
|
+
<div id="content-styling" class="tab-content markdown-body">
|
|
76
|
+
{% mdFile "docs/styling with scss.md" %}
|
|
226
77
|
</div>
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<div class="markdown-body">
|
|
230
|
-
<h2>JavaScript</h2>
|
|
231
|
-
|
|
232
|
-
<h3>Page JS</h3>
|
|
233
|
-
<p>Each page has its own JS entry point in <code>src/frontend/js/pages/</code></p>
|
|
234
|
-
<p>It is bundled and minified by esbuild and loaded automatically by <code>base.njk</code></p>
|
|
235
|
-
<p>Import only what the page needs.</p>
|
|
236
|
-
|
|
237
|
-
<h4>examplePage.js <small>(<code>src/frontend/js/pages/</code>)</small></h4>
|
|
238
|
-
<pre><code class="language-js">import { showNotification } from '../modules/notification.js';
|
|
239
|
-
|
|
240
|
-
import { initNormalizePhoneNumber } from '../modules/forms/normalizePhoneNumber.js';
|
|
241
|
-
|
|
242
|
-
document.addEventListener("DOMContentLoaded", () => {
|
|
243
|
-
initNormalizePhoneNumber();
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
showNotification("Page loaded", "success", 3000);</code></pre>
|
|
247
|
-
|
|
248
|
-
<h2>Modules</h2>
|
|
249
|
-
<p>Modules live in <code>src/frontend/js/modules/</code>. Some must be called inside <code>DOMContentLoaded</code> as they interact with the DOM; others create elements dynamically and can be called anywhere.</p>
|
|
250
|
-
|
|
251
|
-
<h3>Call inside <code>DOMContentLoaded</code></h3>
|
|
252
|
-
<table>
|
|
253
|
-
<thead>
|
|
254
|
-
<tr><th>Module</th><th>Function</th></tr>
|
|
255
|
-
</thead>
|
|
256
|
-
<tbody>
|
|
257
|
-
<tr><td><code>modules/langSwitcher.js</code></td><td><code>initLangSwitcher()</code></td></tr>
|
|
258
|
-
<tr><td><code>modules/forms/form.js</code></td><td><code>initFormListener()</code></td></tr>
|
|
259
|
-
<tr><td><code>modules/forms/textAreaAutoExpand.js</code></td><td><code>initTextAreaAutoExpand()</code></td></tr>
|
|
260
|
-
<tr><td><code>modules/forms/normalizePhoneNumber.js</code></td><td><code>initNormalizePhoneNumber()</code></td></tr>
|
|
261
|
-
</tbody>
|
|
262
|
-
</table>
|
|
263
|
-
|
|
264
|
-
<h3>Call anywhere</h3>
|
|
265
|
-
<table>
|
|
266
|
-
<thead>
|
|
267
|
-
<tr><th>Module</th><th>Function</th></tr>
|
|
268
|
-
</thead>
|
|
269
|
-
<tbody>
|
|
270
|
-
<tr><td><code>modules/notification.js</code></td><td><code>showNotification(text, type, duration)</code></td></tr>
|
|
271
|
-
</tbody>
|
|
272
|
-
</table>
|
|
273
|
-
|
|
274
|
-
<h3><code>showNotification</code> parameters</h3>
|
|
275
|
-
<table>
|
|
276
|
-
<thead>
|
|
277
|
-
<tr><th>Parameter</th><th>Type</th><th>Default</th><th>Values</th></tr>
|
|
278
|
-
</thead>
|
|
279
|
-
<tbody>
|
|
280
|
-
<tr><td><code>text</code></td><td>string</td><td>—</td><td>Any string</td></tr>
|
|
281
|
-
<tr><td><code>type</code></td><td>string</td><td><code>"info"</code></td><td><code>"success"</code>, <code>"info"</code>, <code>"error"</code></td></tr>
|
|
282
|
-
<tr><td><code>duration</code></td><td>number</td><td><code>5000</code></td><td>ms, or <code>-1</code> for persistent with spinner</td></tr>
|
|
283
|
-
</tbody>
|
|
284
|
-
</table>
|
|
285
|
-
|
|
286
|
-
<h2>Adding a module</h2>
|
|
287
|
-
<p>Create a new <code>.js</code> file in <code>src/frontend/js/modules/</code>. You can organize them into subfolders freely.</p>
|
|
288
|
-
<p>Use ESM syntax — esbuild handles the bundling:</p>
|
|
289
|
-
|
|
290
|
-
<h4>yourModule.js <small>(<code>src/frontend/js/modules/</code>)</small></h4>
|
|
291
|
-
<pre><code class="language-js">
|
|
292
|
-
export function yourFunction() {
|
|
293
|
-
// ...
|
|
294
|
-
}</code></pre>
|
|
295
|
-
|
|
296
|
-
<p>Then import it in the pages that need it:</p>
|
|
297
|
-
|
|
298
|
-
<pre><code class="language-js">import { yourFunction } from '../modules/yourModule.js';</code></pre>
|
|
299
|
-
|
|
300
|
-
<blockquote>⚠️ Files inside <code>_tools/</code> run directly in Node.js without a bundler — use CommonJS (<code>require</code> / <code>module.exports</code>) there, not ESM.</blockquote>
|
|
301
|
-
</div>
|
|
78
|
+
<div id="content-javascript" class="tab-content markdown-body">
|
|
79
|
+
{% mdFile "docs/javascript.md" %}
|
|
302
80
|
</div>
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
<div id="content-creating-pages" class="tab-content">
|
|
306
|
-
<div class="markdown-body">
|
|
307
|
-
<h2>Creating Pages</h2>
|
|
308
|
-
<p>The recommended way is via the <a href="#" class="nav-to-assistant">Assistant CLI</a>.</p>
|
|
309
|
-
<h3>What gets created</h3>
|
|
310
|
-
<p>For a page named <code>my-page</code>:</p>
|
|
311
|
-
<table>
|
|
312
|
-
<thead>
|
|
313
|
-
<tr><th>File</th><th>Purpose</th></tr>
|
|
314
|
-
</thead>
|
|
315
|
-
<tbody>
|
|
316
|
-
<tr><td><code>src/pages/my-page.njk</code></td><td>Template with front matter</td></tr>
|
|
317
|
-
<tr><td><code>src/scss/pages/myPage.scss</code></td><td>Imports framework + modules</td></tr>
|
|
318
|
-
<tr><td><code>src/js/pages/myPage.js</code></td><td>Imports JS modules</td></tr>
|
|
319
|
-
</tbody>
|
|
320
|
-
</table>
|
|
321
|
-
<h3>Adding content</h3>
|
|
322
|
-
<ol>
|
|
323
|
-
<li>Create a component in <code>src/components/</code> (e.g., <code>_myPage.njk</code>)</li>
|
|
324
|
-
<li>Include it in <code>src/layouts/includes.njk</code> inside the generated <code>elif</code> block:</li>
|
|
325
|
-
</ol>
|
|
326
|
-
<pre><code>{% elif title == "myPage" %}
|
|
327
|
-
{% include "_myPage.njk" %}</code></pre>
|
|
328
|
-
<p>See <a href="#" class="nav-to-components">Components</a> for details.</p>
|
|
329
|
-
<h3>URL and title</h3>
|
|
330
|
-
<p>The URL is the kebab-case name (<code>/my-page/</code>). The <code>title</code> in the front matter is camelCase (<code>myPage</code>) and is used internally to load the correct CSS and JS files — <strong>do not change it</strong>.</p>
|
|
331
|
-
<h3>SEO</h3>
|
|
332
|
-
<p>The CLI creates a stub entry in <code>src/data/site.json</code>. Fill it in:</p>
|
|
333
|
-
<pre><code>"myPage": {
|
|
334
|
-
"seo": {
|
|
335
|
-
"title": "My Page | Site Name",
|
|
336
|
-
"description": "Page description"
|
|
337
|
-
}
|
|
338
|
-
}</code></pre>
|
|
339
|
-
<p>See <a href="#" class="nav-to-head-seo">Head & SEO</a> for all available options</p>
|
|
340
|
-
</div>
|
|
81
|
+
<div id="content-creating-pages" class="tab-content markdown-body">
|
|
82
|
+
{% mdFile "docs/creating pages.md" %}
|
|
341
83
|
</div>
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
<div class="markdown-body">
|
|
345
|
-
<h2>Nunjucks (HTML) Components</h2>
|
|
346
|
-
<h3>What is Nunjucks</h3>
|
|
347
|
-
<p>Nunjucks (<code>.njk</code>) is an HTML file that supports logic like variables, <code>if</code> statements, and <code>for</code> loops. It can extend a base layout and include other <code>.njk</code> components.</p>
|
|
348
|
-
<h3>Create a component</h3>
|
|
349
|
-
<p>Create a new <code>.njk</code> file anywhere inside <code>src/frontend/components/</code>. You can organize them into subfolders freely:</p>
|
|
350
|
-
<pre><code>src/frontend/components/
|
|
351
|
-
├── global/
|
|
352
|
-
├── layouts/
|
|
353
|
-
├── modals/
|
|
354
|
-
│ └── privacyModal.njk
|
|
355
|
-
└── welcome.njk</code></pre>
|
|
356
|
-
<h3>Include a component</h3>
|
|
357
|
-
<p>To render a component inside a page, navigate to <code>src/frontend/components/layouts/</code> and edit <code>includes.njk</code>.</p>
|
|
358
|
-
<h4>includes.njk <small>(<code>src/frontend/components/layouts/</code>)</small></h4>
|
|
359
|
-
<pre><code>{% if title == "homepage" %}
|
|
360
|
-
{% include "welcome.njk" %}
|
|
361
|
-
|
|
362
|
-
{% elif title == "examplePage" %}
|
|
363
|
-
{% include "exampleComponent1.njk" %}
|
|
364
|
-
{% include "subfolder/exampleComponent2.njk" %}
|
|
365
|
-
|
|
366
|
-
{% else %}
|
|
367
|
-
{% include "404/_404.njk" %}
|
|
368
|
-
{{ content | safe }}
|
|
369
|
-
{% endif %}</code></pre>
|
|
370
|
-
<p>Add a new <code>{% elif %}</code> block for each page, listing its components in order. If a component lives in a subfolder, specify the relative path accordingly.</p>
|
|
371
|
-
<blockquote>⚠️ A new <code>elif</code> block is automatically added when you create a page via the Assistant CLI.</blockquote>
|
|
372
|
-
<blockquote>⚠️ If you move or delete a component, always update <code>includes.njk</code> or the site will break.</blockquote>
|
|
373
|
-
<h3>Nest components</h3>
|
|
374
|
-
<p>A component can include other components. This is useful for breaking complex sections into smaller, reusable pieces.</p>
|
|
375
|
-
<h4>exampleComponent.njk</h4>
|
|
376
|
-
<pre><code><section class="hero">
|
|
377
|
-
{% include "ui/heroTitle.njk" %}
|
|
378
|
-
{% include "ui/heroButton.njk" %}
|
|
379
|
-
</section></code></pre>
|
|
380
|
-
<blockquote>The same path rules apply: if the included component is in a subfolder, specify the full relative path.</blockquote>
|
|
381
|
-
<h3>Global components</h3>
|
|
382
|
-
<p>Header and footer live in <code>src/frontend/components/global/</code> and are automatically included in every page via <code>base.njk</code>. Edit them to change the site-wide layout.</p>
|
|
383
|
-
<h3>Site data in components</h3>
|
|
384
|
-
<p>All values defined in <code>src/data/site.json</code> are globally available in every component via <code>{{ site.* }}</code>.</p>
|
|
385
|
-
<h4>site.json <small>(<code>src/data/</code>)</small></h4>
|
|
386
|
-
<pre><code>{
|
|
387
|
-
"title": "My Site",
|
|
388
|
-
"logo": "/img/logo.png",
|
|
389
|
-
"legal": {
|
|
390
|
-
"privacy": "/privacy"
|
|
391
|
-
}
|
|
392
|
-
}</code></pre>
|
|
393
|
-
<h4>Usage in any <code>.njk</code> file</h4>
|
|
394
|
-
<pre><code><p>{{ site.title }}</p>
|
|
395
|
-
<a href="{{ site.legal.privacy }}">Privacy Policy</a>
|
|
396
|
-
<img src="{{ site.logo }}" alt="{{ site.title }}"></code></pre>
|
|
397
|
-
</div>
|
|
84
|
+
<div id="content-components" class="tab-content markdown-body">
|
|
85
|
+
{% mdFile "docs/components.md" %}
|
|
398
86
|
</div>
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
<div class="markdown-body">
|
|
402
|
-
<h2>Head & SEO</h2>
|
|
403
|
-
<p>This json holds global settings used across all pages in <code>base.njk</code> and other components:</p>
|
|
404
|
-
|
|
405
|
-
<h3>site.json <small>(<code>src/frontend/data/</code>)</small></h3>
|
|
406
|
-
<pre><code>"site_name": "Site name",
|
|
407
|
-
"title": "Site title",
|
|
408
|
-
"description": "Site description",
|
|
409
|
-
"keywords": "keyword1, keyword2, keyword3",
|
|
410
|
-
"domain": "yoursite.com",
|
|
411
|
-
"url": "https://yoursite.com",
|
|
412
|
-
"lang": "en",
|
|
413
|
-
"author": "Name and surname",
|
|
414
|
-
"data_bs_theme": "dark",
|
|
415
|
-
"favicon": "/assets/brand/favicon.svg",
|
|
416
|
-
"logo": "/assets/brand/logo.svg",
|
|
417
|
-
...</code></pre>
|
|
418
|
-
|
|
419
|
-
<h2>Per-page SEO and CDN</h2>
|
|
420
|
-
<p>Each page entry is keyed by its camelCase <code>title</code> from the front matter:</p>
|
|
421
|
-
<p>If you don't want to use a particular CDN inserting it in <code>base.njk</code> for all pages, you can add extra specific CDN (CSS, JS) by inserting the link in each page of <code>site.json</code> separating them with a <code>,</code> and setting them in <code>""</code>.</p>
|
|
422
|
-
|
|
423
|
-
<h3>site.json <small>(<code>src/frontend/data/</code>)</small></h3>
|
|
424
|
-
<pre><code>"pages": {
|
|
425
|
-
...
|
|
426
|
-
"examplePage": {
|
|
427
|
-
"seo": {
|
|
428
|
-
"title": "Example Page",
|
|
429
|
-
"description": "description"
|
|
430
|
-
},
|
|
431
|
-
"cdn": {
|
|
432
|
-
// You can leave the [] empty
|
|
433
|
-
"css": ["https://example1.com/lib.min.css", "https://example2.com/lib.min.css"],
|
|
434
|
-
"js": ["https://example1.com/lib.min.js", "https://example2.com/lib.min.js"]
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
...
|
|
438
|
-
}</code></pre>
|
|
439
|
-
|
|
440
|
-
<h2>AI & SEO bots</h2>
|
|
441
|
-
<p><code>llms.txt</code> and <code>robots.txt</code> are generated automatically from <code>site.json</code> via their respective <code>.njk</code> files — no manual editing needed.</p>
|
|
442
|
-
|
|
443
|
-
<table>
|
|
444
|
-
<thead>
|
|
445
|
-
<tr><th>File</th><th>Purpose</th><th>Reachable at</th></tr>
|
|
446
|
-
</thead>
|
|
447
|
-
<tbody>
|
|
448
|
-
<tr><td><code>llms.njk</code></td><td>Tells AI models what your site is about</td><td><code>yoursite.com/llms.txt</code></td></tr>
|
|
449
|
-
<tr><td><code>robots.njk</code></td><td>Controls search engine crawling</td><td><code>yoursite.com/robots.txt</code></td></tr>
|
|
450
|
-
</tbody>
|
|
451
|
-
</table>
|
|
452
|
-
|
|
453
|
-
<p>To customize them, edit <code>src/llms.njk</code> or <code>src/robots.njk</code> directly.</p>
|
|
454
|
-
|
|
455
|
-
<h3>Customizing llms.txt</h3>
|
|
456
|
-
<p><code>src/llms.njk</code> ships with a base template — <strong>replace the placeholders with your own content</strong>:</p>
|
|
457
|
-
|
|
458
|
-
<pre><code>{% raw %}# {{ site.site_name }}
|
|
459
|
-
|
|
460
|
-
> {{ site.description }}
|
|
461
|
-
|
|
462
|
-
Built by {{ site.author }} — {{ site.url }}
|
|
463
|
-
|
|
464
|
-
## Pages
|
|
465
|
-
|
|
466
|
-
- {{ site.url }}: Homepage
|
|
467
|
-
|
|
468
|
-
## Notes
|
|
469
|
-
|
|
470
|
-
- Language: {{ site.lang }}
|
|
471
|
-
- All content may be used for AI indexing unless otherwise stated
|
|
472
|
-
{% endraw %}</code></pre>
|
|
473
|
-
|
|
474
|
-
<blockquote>The more accurate and detailed your <code>llms.txt</code>, the better AI models will understand and reference your site.</blockquote>
|
|
475
|
-
|
|
476
|
-
<h2>Configuration field description</h2>
|
|
477
|
-
<table>
|
|
478
|
-
<thead>
|
|
479
|
-
<tr><th>Field</th><th>Purpose</th></tr>
|
|
480
|
-
</thead>
|
|
481
|
-
<tbody>
|
|
482
|
-
<tr><td><code>site_name</code></td><td>Brand name (used in meta tags)</td></tr>
|
|
483
|
-
<tr><td><code>title</code></td><td>Default page title</td></tr>
|
|
484
|
-
<tr><td><code>description</code></td><td>Default meta description</td></tr>
|
|
485
|
-
<tr><td><code>keywords</code></td><td>Default meta keywords</td></tr>
|
|
486
|
-
<tr><td><code>domain</code> / <code>url</code></td><td>Used for canonical URLs and og:url</td></tr>
|
|
487
|
-
<tr><td><code>lang</code></td><td>HTML <code>lang</code> attribute</td></tr>
|
|
488
|
-
<tr><td><code>author</code></td><td>Meta author tag</td></tr>
|
|
489
|
-
<tr><td><code>data_bs_theme</code></td><td>Bootstrap color scheme (<code>light</code> / <code>dark</code>)</td></tr>
|
|
490
|
-
<tr><td><code>favicon</code></td><td>Path to the favicon</td></tr>
|
|
491
|
-
<tr><td><code>logo</code></td><td>Path to the logo (available as <code>{{ site.logo }}</code>)</td></tr>
|
|
492
|
-
</tbody>
|
|
493
|
-
</table>
|
|
494
|
-
</div>
|
|
87
|
+
<div id="content-head-seo" class="tab-content markdown-body">
|
|
88
|
+
{% mdFile "docs/head and seo.md" %}
|
|
495
89
|
</div>
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
<div class="markdown-body">
|
|
499
|
-
<h2>Backend</h2>
|
|
500
|
-
<p>The backend is a PHP REST API located in <code>src/backend/</code>, copied to the output directory automatically at build time.</p>
|
|
501
|
-
|
|
502
|
-
<h3>Structure</h3>
|
|
503
|
-
<pre><code>src/backend/
|
|
504
|
-
├── api/
|
|
505
|
-
│ ├── public/ # Endpoints accessible without an API key
|
|
506
|
-
│ └── protected/ # Endpoints requiring X-Api-Key header
|
|
507
|
-
├── database/
|
|
508
|
-
│ ├── Database.php
|
|
509
|
-
│ ├── models/
|
|
510
|
-
│ └── migrations/
|
|
511
|
-
├── config.php # Your local config — never commit this
|
|
512
|
-
└── config.example.php</code></pre>
|
|
513
|
-
|
|
514
|
-
<h3>Configuration</h3>
|
|
515
|
-
<p><code>config.php</code> works like a <code>.env</code> file — it holds secrets and environment settings that stay local and out of version control.</p>
|
|
516
|
-
<p>Copy <code>config.example.php</code> to <code>config.php</code> and fill in your values:</p>
|
|
517
|
-
|
|
518
|
-
<h4>config.php <small>(<code>src/backend/</code>)</small></h4>
|
|
519
|
-
<pre><code class="language-php">return [
|
|
520
|
-
// Default key for protected endpoints that don't have a specific key in ENDPOINT_KEYS
|
|
521
|
-
'API_KEY' => 'default-key',
|
|
522
|
-
|
|
523
|
-
// If you want restrict access to protected endpoints to specific clients, you can define custom keys for each endpoint
|
|
524
|
-
// For subfolder endpoints, use the relative path ('subfolder/endpoint')
|
|
525
|
-
'ENDPOINT_KEYS' => [
|
|
526
|
-
'example-protected' => 'custom-key',
|
|
527
|
-
],
|
|
528
|
-
|
|
529
|
-
'DB_HOST' => '127.0.0.1',
|
|
530
|
-
'DB_NAME' => 'example_db',
|
|
531
|
-
'DB_USER' => 'root',
|
|
532
|
-
'DB_PASS' => '',
|
|
533
|
-
];</code></pre>
|
|
534
|
-
|
|
535
|
-
<p><code>API_KEY</code> is the fallback key for all protected endpoints. Use <code>ENDPOINT_KEYS</code> to assign a different key to a specific endpoint — for subfolder endpoints, use the relative path as the key.</p>
|
|
536
|
-
|
|
537
|
-
<h3>How routing works</h3>
|
|
538
|
-
<p>The file path inside <code>api/</code> maps directly to the URL. Extra URL segments become route parameters available as <code>$requestParams[]</code>.</p>
|
|
539
|
-
<p>Every endpoint file has access to:</p>
|
|
540
|
-
|
|
541
|
-
<table>
|
|
542
|
-
<thead>
|
|
543
|
-
<tr>
|
|
544
|
-
<th>Variable</th>
|
|
545
|
-
<th>Description</th>
|
|
546
|
-
</tr>
|
|
547
|
-
</thead>
|
|
548
|
-
<tbody>
|
|
549
|
-
<tr>
|
|
550
|
-
<td><code>$method</code></td>
|
|
551
|
-
<td>HTTP method (<code>GET</code>, <code>POST</code>, <code>PUT</code>, <code>PATCH</code>, <code>DELETE</code>)</td>
|
|
552
|
-
</tr>
|
|
553
|
-
<tr>
|
|
554
|
-
<td><code>$requestParams</code></td>
|
|
555
|
-
<td>Extra URL segments (e.g. <code>/api/posts/42</code> → <code>['42']</code>)</td>
|
|
556
|
-
</tr>
|
|
557
|
-
</tbody>
|
|
558
|
-
</table>
|
|
559
|
-
|
|
560
|
-
<h3>Creating a public endpoint</h3>
|
|
561
|
-
<p>Create a <code>.php</code> file anywhere inside <code>api/public/</code></p>
|
|
562
|
-
|
|
563
|
-
<h4>api/public/example.php</h4>
|
|
564
|
-
<pre><code class="language-php"><?php
|
|
565
|
-
declare(strict_types=1);
|
|
566
|
-
|
|
567
|
-
require_once CORE_PATH . '/modules/Response.php';
|
|
568
|
-
|
|
569
|
-
if ($method !== 'GET') {
|
|
570
|
-
Response::error('Method not allowed', 405);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
$id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
|
|
574
|
-
|
|
575
|
-
Response::success(['id' => $id]);</code></pre>
|
|
576
|
-
|
|
577
|
-
<h3>Creating a protected endpoint</h3>
|
|
578
|
-
<p>Create a <code>.php</code> file inside <code>api/protected/</code>. The API key check happens automatically before your file runs.</p>
|
|
579
|
-
|
|
580
|
-
<h4>api/protected/example.php</h4>
|
|
581
|
-
<pre><code class="language-php"><?php
|
|
582
|
-
declare(strict_types=1);
|
|
583
|
-
|
|
584
|
-
require_once CORE_PATH . '/modules/Response.php';
|
|
585
|
-
|
|
586
|
-
if ($method !== 'GET') {
|
|
587
|
-
Response::error('Method not allowed', 405);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
Response::success(['visits' => 1024]);</code></pre>
|
|
591
|
-
|
|
592
|
-
<p>To assign a dedicated key, add it to <code>config.php</code>:</p>
|
|
593
|
-
<pre><code class="language-php">'ENDPOINT_KEYS' => [
|
|
594
|
-
'endpoint' => 'custom-key',
|
|
595
|
-
],</code></pre>
|
|
596
|
-
|
|
597
|
-
<h3>The Response helper</h3>
|
|
598
|
-
<pre><code class="language-php">Response::success($data, $code); // default 200
|
|
599
|
-
Response::error($message, $code, $details); // default 400
|
|
600
|
-
Response::noContent(); // 204</code></pre>
|
|
601
|
-
|
|
602
|
-
<h3>Pre-built endpoints</h3>
|
|
603
|
-
<table>
|
|
604
|
-
<thead>
|
|
605
|
-
<tr>
|
|
606
|
-
<th>Route</th>
|
|
607
|
-
<th>Method</th>
|
|
608
|
-
<th>Description</th>
|
|
609
|
-
</tr>
|
|
610
|
-
</thead>
|
|
611
|
-
<tbody>
|
|
612
|
-
<tr>
|
|
613
|
-
<td><code>/api/example-public</code></td>
|
|
614
|
-
<td><code>GET</code></td>
|
|
615
|
-
<td>Example endpoint that doesn't require any key</td>
|
|
616
|
-
</tr>
|
|
617
|
-
<tr>
|
|
618
|
-
<td><code>/api/example-protected</code></td>
|
|
619
|
-
<td><code>GET</code></td>
|
|
620
|
-
<td>Example endpoint that requires X-API-KEY</td>
|
|
621
|
-
</tr>
|
|
622
|
-
</tbody>
|
|
623
|
-
</table>
|
|
624
|
-
</div>
|
|
90
|
+
<div id="content-backend" class="tab-content markdown-body">
|
|
91
|
+
{% mdFile "docs/backend.md" %}
|
|
625
92
|
</div>
|
|
93
|
+
|
|
626
94
|
</div>
|
|
627
95
|
</div>
|
|
628
96
|
|
|
629
97
|
<style>
|
|
630
|
-
/* Custom Replacement Classes for Bootstrap */
|
|
631
98
|
.layout-container {
|
|
632
99
|
width: 100%;
|
|
633
100
|
max-width: 1140px;
|
|
@@ -649,14 +116,22 @@ Response::noContent(); // 204</code></pre>
|
|
|
649
116
|
color: #42b883;
|
|
650
117
|
font-weight: 600;
|
|
651
118
|
}
|
|
119
|
+
|
|
120
|
+
.markdown-body {
|
|
121
|
+
background-color: transparent !important;
|
|
122
|
+
|
|
123
|
+
h1 {
|
|
124
|
+
text-align: unset;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
652
128
|
.slogan {
|
|
653
129
|
text-align: center;
|
|
654
130
|
color: #4b71a0;
|
|
655
131
|
font-size: 1.5rem;
|
|
656
132
|
margin-bottom: 1rem;
|
|
657
133
|
}
|
|
658
|
-
|
|
659
|
-
/* Tabs Logic */
|
|
134
|
+
|
|
660
135
|
.tab-content {
|
|
661
136
|
display: none;
|
|
662
137
|
animation: fadeIn 0.3s ease-in-out;
|
|
@@ -679,9 +154,7 @@ Response::noContent(); // 204</code></pre>
|
|
|
679
154
|
gap: 0.75rem;
|
|
680
155
|
margin-top: 1rem;
|
|
681
156
|
margin-bottom: 1rem;
|
|
682
|
-
transition:
|
|
683
|
-
border-color 0.2s,
|
|
684
|
-
transform 0.15s;
|
|
157
|
+
transition: border-color 0.2s, transform 0.15s;
|
|
685
158
|
text-decoration: none;
|
|
686
159
|
}
|
|
687
160
|
.card:hover {
|
|
@@ -719,7 +192,7 @@ Response::noContent(); // 204</code></pre>
|
|
|
719
192
|
.card:hover .card-link {
|
|
720
193
|
opacity: 1;
|
|
721
194
|
}
|
|
722
|
-
|
|
195
|
+
|
|
723
196
|
.guide-filter {
|
|
724
197
|
display: flex;
|
|
725
198
|
gap: 0.5rem;
|
|
@@ -733,7 +206,6 @@ Response::noContent(); // 204</code></pre>
|
|
|
733
206
|
.guide-filter label input[type="radio"] {
|
|
734
207
|
display: none;
|
|
735
208
|
}
|
|
736
|
-
|
|
737
209
|
.filter-pill {
|
|
738
210
|
display: inline-block;
|
|
739
211
|
padding: 5px 16px;
|
|
@@ -742,102 +214,18 @@ Response::noContent(); // 204</code></pre>
|
|
|
742
214
|
background: #0d1526;
|
|
743
215
|
color: #38649b;
|
|
744
216
|
font-size: 0.9rem;
|
|
745
|
-
transition:
|
|
746
|
-
border-color 0.2s,
|
|
747
|
-
color 0.2s,
|
|
748
|
-
background 0.2s;
|
|
217
|
+
transition: border-color 0.2s, color 0.2s, background 0.2s;
|
|
749
218
|
user-select: none;
|
|
750
219
|
}
|
|
751
|
-
|
|
752
220
|
.guide-filter input[type="radio"]:checked + .filter-pill {
|
|
753
221
|
border-color: #42b883;
|
|
754
222
|
color: #42b883;
|
|
755
223
|
background: rgba(66, 184, 131, 0.08);
|
|
756
224
|
}
|
|
757
|
-
|
|
758
225
|
.guide-filter label:hover .filter-pill {
|
|
759
226
|
border-color: #42b883;
|
|
760
227
|
color: #42b883;
|
|
761
228
|
}
|
|
762
|
-
|
|
763
|
-
/* Markdown Body Styles */
|
|
764
|
-
.markdown-body {
|
|
765
|
-
color: #cbd5e1;
|
|
766
|
-
line-height: 1.6;
|
|
767
|
-
}
|
|
768
|
-
.markdown-body h2 {
|
|
769
|
-
color: #42b883;
|
|
770
|
-
margin-top: 0;
|
|
771
|
-
margin-bottom: 1rem;
|
|
772
|
-
font-size: 1.8rem;
|
|
773
|
-
}
|
|
774
|
-
.markdown-body h3 {
|
|
775
|
-
color: #cbd5e1;
|
|
776
|
-
margin-top: 1.5rem;
|
|
777
|
-
margin-bottom: 0.75rem;
|
|
778
|
-
font-size: 1.3rem;
|
|
779
|
-
}
|
|
780
|
-
.markdown-body h4 {
|
|
781
|
-
color: #cbd5e1;
|
|
782
|
-
margin-top: 1.2rem;
|
|
783
|
-
margin-bottom: 0.5rem;
|
|
784
|
-
font-size: 1.1rem;
|
|
785
|
-
font-weight: 600;
|
|
786
|
-
}
|
|
787
|
-
.markdown-body h4 small {
|
|
788
|
-
color: #8da4c2;
|
|
789
|
-
font-weight: 400;
|
|
790
|
-
font-size: 0.85em;
|
|
791
|
-
}
|
|
792
|
-
.markdown-body p {
|
|
793
|
-
margin-bottom: 1rem;
|
|
794
|
-
color: #8da4c2;
|
|
795
|
-
}
|
|
796
|
-
.markdown-body pre {
|
|
797
|
-
background: #0d1526;
|
|
798
|
-
border: 0.8px solid #1e2d4a;
|
|
799
|
-
padding: 1rem;
|
|
800
|
-
border-radius: 8px;
|
|
801
|
-
overflow-x: auto;
|
|
802
|
-
margin-bottom: 1.5rem;
|
|
803
|
-
}
|
|
804
|
-
.markdown-body pre code {
|
|
805
|
-
background: transparent;
|
|
806
|
-
padding: 0;
|
|
807
|
-
color: #cbd5e1;
|
|
808
|
-
}
|
|
809
|
-
.markdown-body code {
|
|
810
|
-
font-family: monospace;
|
|
811
|
-
background: rgba(66, 184, 131, 0.15);
|
|
812
|
-
color: #42b883;
|
|
813
|
-
padding: 0.2rem 0.4rem;
|
|
814
|
-
border-radius: 4px;
|
|
815
|
-
font-size: 0.9em;
|
|
816
|
-
}
|
|
817
|
-
.markdown-body table {
|
|
818
|
-
width: 100%;
|
|
819
|
-
border-collapse: collapse;
|
|
820
|
-
margin-bottom: 1.5rem;
|
|
821
|
-
font-size: 0.95rem;
|
|
822
|
-
}
|
|
823
|
-
.markdown-body th, .markdown-body td {
|
|
824
|
-
border: 0.8px solid #1e2d4a;
|
|
825
|
-
padding: 0.75rem 1rem;
|
|
826
|
-
text-align: left;
|
|
827
|
-
}
|
|
828
|
-
.markdown-body th {
|
|
829
|
-
background: #0d1526;
|
|
830
|
-
color: #cbd5e1;
|
|
831
|
-
font-weight: 600;
|
|
832
|
-
}
|
|
833
|
-
.markdown-body blockquote {
|
|
834
|
-
border-left: 4px solid #42b883;
|
|
835
|
-
margin: 1.5rem 0;
|
|
836
|
-
padding: 1rem 1.5rem;
|
|
837
|
-
background: rgba(66, 184, 131, 0.05);
|
|
838
|
-
border-radius: 0 8px 8px 0;
|
|
839
|
-
color: #a0aec0;
|
|
840
|
-
}
|
|
841
229
|
</style>
|
|
842
230
|
|
|
843
231
|
<script>
|
|
@@ -846,35 +234,11 @@ Response::noContent(); // 204</code></pre>
|
|
|
846
234
|
document.querySelectorAll(".tab-content").forEach((content) => {
|
|
847
235
|
content.classList.remove("active");
|
|
848
236
|
});
|
|
849
|
-
|
|
850
237
|
const targetId = `content-${e.target.value}`;
|
|
851
238
|
const targetContent = document.getElementById(targetId);
|
|
852
|
-
|
|
853
239
|
if (targetContent) {
|
|
854
240
|
targetContent.classList.add("active");
|
|
855
241
|
}
|
|
856
242
|
});
|
|
857
243
|
});
|
|
858
|
-
|
|
859
|
-
document.querySelector('.nav-to-assistant').addEventListener('click', (e) => {
|
|
860
|
-
e.preventDefault();
|
|
861
|
-
|
|
862
|
-
const assistantRadio = document.querySelector('input[name="guide-filter"][value="assistant"]');
|
|
863
|
-
|
|
864
|
-
if (assistantRadio) {
|
|
865
|
-
assistantRadio.checked = true;
|
|
866
|
-
assistantRadio.dispatchEvent(new Event('change'));
|
|
867
|
-
}
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
document.querySelector('.nav-to-head-seo').addEventListener('click', (e) => {
|
|
871
|
-
e.preventDefault();
|
|
872
|
-
|
|
873
|
-
const headAndSeoRadio = document.querySelector('input[name="guide-filter"][value="head-seo"]');
|
|
874
|
-
|
|
875
|
-
if (headAndSeoRadio) {
|
|
876
|
-
headAndSeoRadio.checked = true;
|
|
877
|
-
headAndSeoRadio.dispatchEvent(new Event('change'));
|
|
878
|
-
}
|
|
879
|
-
});
|
|
880
244
|
</script>
|