nibula 2.0.2 → 2.2.0

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 CHANGED
@@ -1,17 +1,30 @@
1
- const esbuild = require("esbuild");
2
- const glob = require("glob");
1
+ const { RenderPlugin } = require("@11ty/eleventy");
2
+ const markdownItAnchor = require("markdown-it-anchor");
3
+ const markdownItAttrs = require("markdown-it-attrs");
3
4
  const Image = require("@11ty/eleventy-img");
4
5
  const fs = require("fs");
5
6
  const path = require("path");
6
7
 
7
8
  const OUTPUT_DIR = "out";
9
+ const TEMPLATE_ENGINE = "njk";
10
+ const ANCHOR_LEVELS = [2, 3, 4];
11
+ const ANCHOR_PERMALINK_SYMBOL = "#";
12
+ const ANCHOR_PERMALINK_PLACEMENT = "after";
13
+
14
+ const MARKDOWN_BASE_PATH = "src/frontend/components/";
8
15
 
9
16
  module.exports = function (eleventyConfig) {
10
17
 
18
+ // ---------------------------------------------------------------------------
19
+ // Utilities
20
+ // ---------------------------------------------------------------------------
21
+
11
22
  function copyRecursiveSync(src, dest) {
12
23
  if (!fs.existsSync(src)) return;
13
- if (src.includes('.git')) return;
24
+ if (src.includes(".git")) return;
25
+
14
26
  const stat = fs.statSync(src);
27
+
15
28
  if (stat.isDirectory()) {
16
29
  fs.mkdirSync(dest, { recursive: true });
17
30
  for (const child of fs.readdirSync(src)) {
@@ -23,18 +36,47 @@ module.exports = function (eleventyConfig) {
23
36
  }
24
37
  }
25
38
 
39
+ // ---------------------------------------------------------------------------
40
+ // Plugins & markdown
41
+ // ---------------------------------------------------------------------------
42
+
43
+ eleventyConfig.addPlugin(RenderPlugin);
44
+
45
+ eleventyConfig.amendLibrary("md", (markdownLibrary) => {
46
+ markdownLibrary
47
+ .use(markdownItAnchor, {
48
+ level: ANCHOR_LEVELS,
49
+ permalink: markdownItAnchor.permalink.linkInsideHeader({
50
+ symbol: ANCHOR_PERMALINK_SYMBOL,
51
+ placement: ANCHOR_PERMALINK_PLACEMENT,
52
+ }),
53
+ })
54
+ .use(markdownItAttrs);
55
+ });
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Events
59
+ // ---------------------------------------------------------------------------
60
+
26
61
  eleventyConfig.on("eleventy.before", () => {
27
62
  copyRecursiveSync("src/backend", `${OUTPUT_DIR}/backend`);
28
63
  });
29
64
 
65
+ // ---------------------------------------------------------------------------
66
+ // Passthrough copy
67
+ // ---------------------------------------------------------------------------
68
+
69
+ // Project assets
70
+ eleventyConfig.addPassthroughCopy("src/frontend/assets");
71
+ eleventyConfig.addPassthroughCopy("src/frontend/robots.txt");
72
+
73
+ // Hosting files
30
74
  eleventyConfig.addPassthroughCopy({
31
75
  "src/frontend/hosting/.htaccess": ".htaccess",
32
76
  "src/frontend/hosting/web.config": "web.config",
33
77
  });
34
-
35
- eleventyConfig.addPassthroughCopy("src/frontend/assets");
36
- eleventyConfig.addPassthroughCopy("src/frontend/robots.txt");
37
78
 
79
+ // node_modules dependencies
38
80
  eleventyConfig.addPassthroughCopy({
39
81
  // Bootstrap
40
82
  "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js": "js/bootstrap.bundle.min.js",
@@ -50,8 +92,14 @@ module.exports = function (eleventyConfig) {
50
92
  // Bulma — CSS only, no JS passthrough needed
51
93
  });
52
94
 
95
+ // ---------------------------------------------------------------------------
96
+ // Shortcodes
97
+ // ---------------------------------------------------------------------------
98
+
99
+ eleventyConfig.addFilter("markdownPath", (name) => `${MARKDOWN_BASE_PATH}${name}`);
100
+
53
101
  eleventyConfig.addShortcode("image", async function (src, alt) {
54
- let metadata = await Image(src, {
102
+ const metadata = await Image(src, {
55
103
  widths: [320, 480, 720, 1280, 1920, 2048, 2560, 3840, 4096, 7680],
56
104
  formats: ["webp", "jpeg"],
57
105
  outputDir: `${OUTPUT_DIR}/assets/images/`,
@@ -66,15 +114,27 @@ module.exports = function (eleventyConfig) {
66
114
  });
67
115
  });
68
116
 
117
+ // ---------------------------------------------------------------------------
118
+ // Watch targets & dev server
119
+ // ---------------------------------------------------------------------------
120
+
69
121
  eleventyConfig.addWatchTarget("./src/frontend/scss");
70
122
  eleventyConfig.addWatchTarget("./src/frontend/routes");
71
123
  eleventyConfig.addWatchTarget("./src/frontend/data");
124
+ eleventyConfig.addWatchTarget("./src/frontend/components");
72
125
 
73
126
  eleventyConfig.setServerOptions({
74
- watch: [`${OUTPUT_DIR}/js/**/*.js`]
127
+ watch: [`${OUTPUT_DIR}/js/**/*.js`],
75
128
  });
76
129
 
130
+ // ---------------------------------------------------------------------------
131
+ // Directory configuration
132
+ // ---------------------------------------------------------------------------
133
+
77
134
  return {
135
+ markdownTemplateEngine: TEMPLATE_ENGINE,
136
+ htmlTemplateEngine: TEMPLATE_ENGINE,
137
+
78
138
  dir: {
79
139
  input: "src/frontend",
80
140
  output: OUTPUT_DIR,
package/CHANGELOG.md CHANGED
@@ -5,6 +5,114 @@ All notable changes to Nibula are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.2.0] - 2026-08-18
9
+
10
+ ### Added
11
+ - **Markdown rendered with `renderFile` is styled out of the box.**
12
+ `src/frontend/scss/modules/_markdown.scss` carries GitHub's Markdown layout —
13
+ tables, code blocks, blockquotes, heading spacing — scoped to a
14
+ `.markdown-body` container. Markdown produces plain HTML with no classes, so a
15
+ rendered `.md` came out as unstyled text regardless of the CSS framework in the
16
+ project. Colours follow `data-theme`, and the background is transparent so the
17
+ block sits on the page instead of on a panel of its own.
18
+ - The commented `renderFile` example in the route template now shows the
19
+ `.markdown-body` wrapper, which is what applies the styling.
20
+
21
+ ### Changed
22
+ - `docs/Components.md` documents the wrapper as part of the `renderFile` line,
23
+ and no longer says Markdown comes out unstyled: `{.class}` is now for adding
24
+ your framework's classes to specific elements, not for making the content
25
+ presentable in the first place.
26
+ - `docs/Styling with SCSS.md` lists `_markdown.scss` among the pre-existing
27
+ modules, noting that it is generated rather than hand-written and that its
28
+ import has to stay after the framework for its table and code block rules to
29
+ win.
30
+
31
+ ### Notes
32
+ - The styling lands in newly created projects only. To adopt it in an existing
33
+ one, copy `src/frontend/scss/modules/_markdown.scss` and import it in
34
+ `_global.scss` after the framework, then wrap your `renderFile` calls in
35
+ `<article class="markdown-body">`.
36
+
37
+ ## [2.1.0] - 2026-08-17
38
+
39
+ ### Added
40
+ - **The assistant asks which layout a new page should use.** Any `.njk` file in
41
+ `src/frontend/layouts/` is offered in an arrow-key menu, and the chosen one is
42
+ written into the route's front matter. Before, every page was created with
43
+ `base.njk` and the documentation had to remind you to change it by hand — the
44
+ assistant wrote a value the reader was then expected to correct. With a single
45
+ layout in the folder the menu is skipped, so nothing changes for a project that
46
+ has not added one; with none, the front matter is left untouched.
47
+ - `askChoice` in `tools/cli/prompt.js`: the arrow-key menu the scaffolder already
48
+ used, available to the assistant as well.
49
+ - **Markdown files can be rendered inside a page.** `markdown-it-anchor` and
50
+ `markdown-it-attrs` are installed and preconfigured, and Eleventy's
51
+ `RenderPlugin` is registered, so a `.md` file in `src/frontend/components/`
52
+ can be pulled into a route with `renderFile`. Headings from level 2 down get
53
+ an `id` and a `#` link, and `{.class}` applies a CSS class inline — markdown
54
+ produces plain HTML, so without a way to set classes the output carries none of
55
+ the project's framework styling.
56
+ - A `markdownPath` filter that prefixes the components folder, so a route writes
57
+ `"your-file.md" | markdownPath` and not the full path from the project root.
58
+ `renderFile` resolves paths from the working directory, unlike `include`, which
59
+ resolves them from the components folder.
60
+ - The template new pages are created from carries a commented `renderFile`
61
+ example next to the component one, with the data argument included:
62
+ `renderFile` does not inherit the page context, so a `.md` that reads `site.*`
63
+ renders those values empty unless the data is passed explicitly.
64
+
65
+ ### Changed
66
+ - **`tools/cli/prompt.js` creates a readline interface per question** instead of
67
+ holding one open for the whole session. The session-wide interface kept its own
68
+ keypress listener on stdin, which would have fought with the raw-mode listener
69
+ the arrow-key menu needs: both would have received every key, so arrows moved
70
+ the readline cursor and Enter fired its `line` event.
71
+ - **`markdownTemplateEngine` and `htmlTemplateEngine` are set to Nunjucks.**
72
+ Eleventy defaults `.md` files to Liquid, so a markdown file containing
73
+ `{{ site.title }}` was processed by a different template engine than every
74
+ other file in the project — working for the simple cases and silently not for
75
+ the rest.
76
+ - `src/frontend/components` is now a watch target. Eleventy tracks `.njk`
77
+ includes as template dependencies, but a `.md` read through `renderFile` is
78
+ invisible to it, so editing one changed nothing until the route itself was
79
+ saved.
80
+ - `.eleventy.js` is grouped into labelled sections, with plugins and markdown
81
+ configuration ahead of the build event and the passthrough copies.
82
+ - **Documentation is written for readers who are new to this.** The docs assumed
83
+ vocabulary a beginner does not have yet. `Deploy.md` opens with a question that
84
+ routes the reader by backend, and explains what shared hosting, a VPS, systemd,
85
+ an HTTPS certificate and `screen` actually are; the Node section now separates
86
+ trying the backend out from setting it up to survive a reboot, which read as
87
+ five equal steps before. `Head and SEO.md` starts from what the `<head>` is and
88
+ why there are two data files, and drops the unexplained jargon — canonical,
89
+ JSON-LD, noindex. `Components.md` documents markdown from what Markdown is,
90
+ with each part of the `renderFile` line explained rather than assumed.
91
+ - `Project structure.md` lists `components/` as the home of `.md` content, and
92
+ `Creating pages.md` describes both commented examples a new route carries.
93
+
94
+ ### Fixed
95
+ - `Deploy.md` told the reader to open the Nginx site file with `nano` and how to
96
+ save it, without ever saying what to paste into it. The step now names the
97
+ edited `nginx.conf` as the source.
98
+
99
+ ### Removed
100
+ - **The dead language switch in the scaffolder.** `bin/create.js` commented and
101
+ uncommented a `glob.sync` line for the JavaScript and TypeScript entry points,
102
+ but that line has not existed in `.eleventy.js` since bundling moved to
103
+ `tools/buildJs.js`, so the two markers matched nothing and the function ran as
104
+ a no-op. Entry points are collected from both language folders and resolved
105
+ against the filesystem, which makes the switch unnecessary: the folder that
106
+ does not exist contributes nothing.
107
+ - Unused `esbuild` and `glob` imports from `.eleventy.js`. Both are still real
108
+ dependencies of `tools/buildJs.js`, which is where they are used.
109
+
110
+ ### Notes
111
+ - The markdown setup lands in newly created projects only. To adopt it in an
112
+ existing one, install `markdown-it-anchor` and `markdown-it-attrs` and copy the
113
+ plugin block, the `markdownPath` filter and the two template engine settings
114
+ from the current `.eleventy.js`.
115
+
8
116
  ## [2.0.2] - 2026-08-08
9
117
 
10
118
  ### Fixed
package/bin/create.js CHANGED
@@ -126,13 +126,6 @@ const FRAMEWORKS = {
126
126
  },
127
127
  };
128
128
 
129
- // ── LANGUAGE CONFIG ───────────────────────────────────────────────────────────
130
-
131
- const LANGUAGE_ELEVENTY = Object.freeze({
132
- jsEntry: 'const entryPoints = glob.sync("src/frontend/js/pages/*.js");',
133
- tsEntry: 'const entryPoints = glob.sync("src/frontend/ts/pages/*.ts");',
134
- });
135
-
136
129
  // ── GENERATED FILE CONTENTS ───────────────────────────────────────────────────
137
130
 
138
131
  const GITIGNORE_CONTENT = `
@@ -170,6 +163,8 @@ const PROJECT_PACKAGE = {
170
163
  'bulma': '^1.0.4',
171
164
  'foundation-sites': '^6.9.0',
172
165
  'glob': '^13.0.6',
166
+ 'markdown-it-anchor': '^9.2.1',
167
+ 'markdown-it-attrs': '^5.0.1',
173
168
  'uikit': '^3.25.13',
174
169
  },
175
170
  devDependencies: {
@@ -391,23 +386,6 @@ function applyFramework(framework) {
391
386
  }
392
387
  }
393
388
 
394
- function applyLanguage(language) {
395
- const eleventyPath = path.join(targetDir, '.eleventy.js');
396
- if (!fs.existsSync(eleventyPath)) return;
397
-
398
- let content = fs.readFileSync(eleventyPath, 'utf8');
399
-
400
- if (language === LANGUAGE.TYPESCRIPT) {
401
- content = slashComment(content, LANGUAGE_ELEVENTY.jsEntry);
402
- content = slashUncomment(content, LANGUAGE_ELEVENTY.tsEntry);
403
- } else {
404
- content = slashUncomment(content, LANGUAGE_ELEVENTY.jsEntry);
405
- content = slashComment(content, LANGUAGE_ELEVENTY.tsEntry);
406
- }
407
-
408
- fs.writeFileSync(eleventyPath, content);
409
- }
410
-
411
389
  // ── UI ────────────────────────────────────────────────────────────────────────
412
390
 
413
391
  function askChoice(question, choices) {
@@ -527,7 +505,6 @@ async function init() {
527
505
  }
528
506
 
529
507
  applyFramework(framework);
530
- applyLanguage(language);
531
508
 
532
509
  installDependencies(backend);
533
510
 
@@ -25,7 +25,7 @@ Components are included in the page that renders them. Open the page's file in `
25
25
 
26
26
  ### example-page.njk <small>(`src/frontend/routes/`)</small>
27
27
 
28
- ```njk
28
+ ```js
29
29
  ---
30
30
  title: "examplePage"
31
31
  permalink: "/example-page/"
@@ -49,7 +49,7 @@ Writing HTML directly in the route works too, and is perfectly fine for a short
49
49
  A component can include other components. This is useful for breaking complex sections into smaller, reusable pieces.
50
50
 
51
51
  ### exampleComponent.njk
52
- ```njk
52
+ ```html
53
53
  <section class="hero">
54
54
  {% include "hero/hero-title.njk" %}
55
55
  {% include "hero/hero-button.njk" %}
@@ -58,6 +58,85 @@ A component can include other components. This is useful for breaking complex se
58
58
 
59
59
  > The same path rules apply: if the included component is in a subfolder, specify the full relative path.
60
60
 
61
+ ## Markdown files
62
+
63
+ ### What is Markdown
64
+
65
+ Markdown (`.md`) is a way to write formatted text without HTML tags. You write
66
+ `**bold**` instead of `<strong>bold</strong>`, and a line starting with `-`
67
+ becomes a list item. For a page that is mostly text — an article, a privacy
68
+ policy, a changelog — it is much faster than writing the markup by hand.
69
+
70
+ ### Write the file
71
+
72
+ Create a `.md` file in `src/frontend/components/`, next to your `.njk` ones.
73
+
74
+ #### article.md <small>(`src/frontend/components/`)</small>
75
+
76
+ ```markdown
77
+ ## Section title
78
+
79
+ Regular paragraph text. **Bold** and *italic* work as usual.
80
+
81
+ - First item
82
+ - Second item
83
+ ```
84
+
85
+ ### Show it in a page
86
+
87
+ Open the page's file in `src/frontend/routes/` and add this line to its body:
88
+
89
+ ```html
90
+ <article class="markdown-body">
91
+ {% renderFile "article.md" | markdownPath, { site: site } %}
92
+ </article>
93
+ ```
94
+
95
+ Four parts to it:
96
+
97
+ | Part | What it does |
98
+ |---|---|
99
+ | `class="markdown-body"` | the wrapper that applies the Markdown styling — without it your content renders unstyled |
100
+ | `renderFile` | turns the Markdown into HTML and puts it in the page |
101
+ | `markdownPath` | fills in the `src/frontend/components/` part of the path for you |
102
+ | `{ site: site }` | hands the file your site settings — see below |
103
+
104
+ Subfolders work the same way as with components:
105
+ `"legal/privacy.md" | markdownPath`
106
+
107
+ ### Why `{ site: site }`
108
+
109
+ A `.md` file can use site values, exactly like a component:
110
+
111
+ ```markdown
112
+ Welcome to {{ site.title }}
113
+ ```
114
+
115
+ But a Markdown file does not automatically see them. Anything the file needs has
116
+ to be listed in the curly braces:
117
+
118
+ ```js
119
+ {% renderFile "article.md" | markdownPath, { site: site, pages: pages } %}
120
+ ```
121
+
122
+ If you forget, nothing breaks — the value simply comes out empty. So if a
123
+ `{{ site.something }}` disappears from your page, this is why.
124
+
125
+ ### Extras you get for free
126
+
127
+ **Links to a section.** Every `##` and `###` heading gets an anchor, so you can
128
+ link straight to it: `yoursite.com/my-page/#section-title`.
129
+
130
+ **CSS classes.** Markdown produces plain HTML with no classes, so your framework's
131
+ styling doesn't reach it. Add a class in curly braces after the element:
132
+
133
+ ```markdown
134
+ ## Section title {.text-center}
135
+ ```
136
+
137
+ > ⚠️ If the `.md` file is missing or the name is misspelled, the build stops with
138
+ > an error. That is on purpose: silently skipping content you asked for is worse.
139
+
61
140
  ## Global components
62
141
 
63
142
  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
@@ -84,7 +163,7 @@ src/frontend/layouts/
84
163
 
85
164
  Same as `base.njk`, without the two global includes:
86
165
 
87
- ```njk
166
+ ```html
88
167
  <body>
89
168
  <main>
90
169
  {{ content | safe }}
@@ -96,7 +175,7 @@ Then point the route at it:
96
175
 
97
176
  ### landing.njk <small>(`src/frontend/routes/`)</small>
98
177
 
99
- ```njk
178
+ ```js
100
179
  ---
101
180
  title: "landing"
102
181
  permalink: "/landing/"
@@ -142,55 +221,17 @@ All values defined in `src/frontend/data/site.json` are globally available in ev
142
221
  `legal.privacy`, `legal.cookie` and `legal.terms` hold the URLs of your legal pages — the footer links to them. See **Head & SEO** DOC file for what every field does
143
222
 
144
223
  ### Usage in any `.njk` file
145
- ```njk
224
+ ```html
146
225
  <p>{{ site.title }}</p>
147
226
  <a href="{{ site.legal.privacy }}">Privacy Policy</a>
148
227
  <img src="{{ site.logo }}" alt="{{ site.title }}">
149
228
  ```
150
229
 
230
+ > In a `.md` file rendered with `renderFile`, these values only work if you passed
231
+ > `site` in the curly braces — see *Why `{ site: site }`* above.
232
+
151
233
  ## Your own data files
152
234
 
153
235
  `site.json` isn't special: **any** `.json` file you drop in `src/frontend/data/` becomes a global variable, and the variable takes the name of the file.
154
236
 
155
- Create `test.json`, put anything you want inside it, and every key is reachable as `test.yourKey` — no import, no configuration, no restart.
156
-
157
- ### test.json <small>(`src/frontend/data/`)</small>
158
- ```json
159
- {
160
- "testMessage": "This is a test"
161
- }
162
- ```
163
-
164
- ### Usage in any `.njk` file
165
- ```njk
166
- <p>{{ test.testMessage }}</p>
167
- ```
168
-
169
- Renders as:
170
- ```html
171
- <p>This is a test</p>
172
- ```
173
-
174
- That's the whole rule: **file name = variable name, keys inside = what you write after the dot.** `site.json` gives you `site.title` for exactly the same reason.
175
-
176
- Values can be anything JSON allows, so a list lets you loop instead of copying markup:
177
-
178
- ```json
179
- {
180
- "testMessage": "This is a test",
181
- "links": [
182
- { "label": "Home", "url": "/" },
183
- { "label": "Contact", "url": "/contact/" }
184
- ]
185
- }
186
- ```
187
-
188
- ```njk
189
- {% for link in test.links %}
190
- <a href="{{ link.url }}">{{ link.label }}</a>
191
- {% endfor %}
192
- ```
193
-
194
- Subfolders create nested names: `data/shop/products.json` becomes `{{ shop.products }}`.
195
-
196
- > ⚠️ Don't reuse a name that already exists. Another file named `site.json` in a subfolder overwrites the original one silently, with no error to point you at the cause.
237
+ Create `test.json`, put anything you want inside it, and every key is reachable as `test.yourKey` — no import, no configuration, no restart.
@@ -17,7 +17,9 @@ It also adds a stub entry in `src/frontend/data/pages.json` for the page's SEO a
17
17
 
18
18
  ## Adding content
19
19
 
20
- The route is created empty. Write your HTML in its body, or include components — see the **Components** DOC file.
20
+ The route is created empty, with two commented examples: one for including a
21
+ `.njk` component, one for rendering a `.md` file. Uncomment the one you need or
22
+ write your HTML directly in the body — see the **Components** DOC file.
21
23
 
22
24
  ## URL and title
23
25
 
@@ -33,7 +35,7 @@ A page that needs a different shell — a landing page with no header, for insta
33
35
 
34
36
  To create a URL like `domain.it/about/team`, edit the `permalink` in `src/frontend/routes/team.njk` and add the parent segment before the final slash:
35
37
 
36
- ```njk
38
+ ```js
37
39
  ---
38
40
  title: "team"
39
41
  permalink: "/about/team/"