@nera-static/nera 1.14.1 → 1.16.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/README.md CHANGED
@@ -31,7 +31,7 @@ package is named after the folder (`My Bakery` → `my-bakery`).
31
31
  | `nera build` | Render `pages/` → `public/`. With `--check`, then run `nera check` on the result — one command for CI. `--json` prints the result as one JSON object (see [Machine-readable output](#machine-readable-output)). |
32
32
  | `nera dev` | Build, serve `public/`, and rebuild on change with live reload. Watches `pages/`, `config/`, `theme/` and a local theme (`theme: ./themes/<name>`). |
33
33
  | `nera serve` | Serve the already-built `public/` folder. |
34
- | `nera update` | Update the site's Nera packages, then create a missing `AGENTS.md`/`CLAUDE.md` and refresh the Nera part of `AGENTS.md` and the `nera` skill (see [Instructions for AI assistants](#instructions-for-ai-assistants)). On a legacy cloned site, `nera update --migrate` converts it to the thin model. |
34
+ | `nera update` | Update the site's Nera packages and raise their caret ranges in `package.json` (and `package-lock.json`) to the installed versions, then create a missing `AGENTS.md`/`CLAUDE.md` and refresh the Nera part of `AGENTS.md` and the `nera` skill (see [Instructions for AI assistants](#instructions-for-ai-assistants)). On a legacy cloned site, `nera update --migrate` converts it to the thin model. |
35
35
  | `nera validate` | Check the site's sources (layouts, includes, YAML) before publishing; exits non-zero on any error. `--json` for machine-readable output. |
36
36
  | `nera check` | Check the built `public/` for accessibility, privacy and legal-notice problems (see below). Build first. `--json` for machine-readable output. |
37
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nera-static/nera",
3
- "version": "1.14.1",
3
+ "version": "1.16.0",
4
4
  "description": "The Nera CLI — scaffold, build, preview and update a Nera static site with one command over the @nera-static/core engine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,12 +11,15 @@ imprint and privacy, with a menu on every page.
11
11
  live URL, e.g. `https://example.com`) once the domain is known.
12
12
  3. Write one Markdown page per topic, each with `layout: pages/default.pug`,
13
13
  a `title` and a `description`: `pages/index.md`, `pages/about.md`,
14
- `pages/services.md`, `pages/opening-hours.md`, `pages/contact.md`,
15
- `pages/imprint.md`, `pages/privacy.md`. Opening hours fit a Markdown
16
- table.
14
+ `pages/services.md`, `pages/opening-hours.md`, `pages/contact.md`.
15
+ Opening hours fit a Markdown table. A new site already has
16
+ `pages/imprint.md` and `pages/privacy.md` as placeholders, linked from
17
+ the layout's footer: replace their text, or write them if they are
18
+ missing.
17
19
  4. Menu: `npm install @nera-static/plugin-navigation`, list the pages in
18
20
  `config/navigation.yaml` (`href` ending in `.html`, `name`; groups such
19
- as `main:` and `footer:` for a footer menu with imprint and privacy), run
21
+ as `main:` and `footer:`; a footer menu replaces the layout's own
22
+ imprint and privacy links, so keep only one of the two), run
20
23
  `npx nera-navigation` and include a menu in
21
24
  `theme/views/layouts/layout.pug` (see the plugin README for the mixins).
22
25
  5. Contact form: follow [contact-form](contact-form.md).
@@ -18,6 +18,78 @@ export function neraDeps(pkg) {
18
18
  )
19
19
  }
20
20
 
21
+ // The caret range `range` raised to the installed `version`, or null when it
22
+ // stays: only a plain `^x.y.z` with x ≥ 1, only upwards, never across a major.
23
+ // Exact, tilde, tag, git and 0.x ranges are the user's choice and stay as
24
+ // they are. `npm update` installs within the range but never rewrites it.
25
+ export function raisedRange(range, version) {
26
+ const from = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(range ?? '')
27
+ const to = /^(\d+)\.(\d+)\.(\d+)$/.exec(version ?? '')
28
+ if (!from || !to) return null
29
+ const [a, b] = [from, to].map((m) => m.slice(1).map(Number))
30
+ if (a[0] < 1 || a[0] !== b[0]) return null
31
+ const newer = b[1] > a[1] || (b[1] === a[1] && b[2] > a[2])
32
+ return newer ? `^${version}` : null
33
+ }
34
+
35
+ const indentOf = (text) => /^[ \t]+(?=")/m.exec(text)?.[0] ?? ' '
36
+
37
+ // Raise the site's caret ranges of `names` to what is installed in its
38
+ // node_modules, keeping the file's own indentation, and the same ranges in
39
+ // the root entry of package-lock.json, so the two stay in sync. Returns the
40
+ // changes as [name, from, to].
41
+ export async function raiseRanges(cwd, names) {
42
+ const pkgPath = path.join(cwd, 'package.json')
43
+ const text = await fs.readFile(pkgPath, 'utf-8')
44
+ const pkg = JSON.parse(text)
45
+ const raised = {}
46
+ const changes = []
47
+ for (const field of ['dependencies', 'devDependencies']) {
48
+ for (const name of names) {
49
+ const range = pkg[field]?.[name]
50
+ if (typeof range !== 'string') continue
51
+ let version
52
+ try {
53
+ const installed = path.join(cwd, 'node_modules', ...name.split('/'), 'package.json')
54
+ version = JSON.parse(await fs.readFile(installed, 'utf-8')).version
55
+ } catch {
56
+ continue
57
+ }
58
+ const to = raisedRange(range, version)
59
+ if (to) {
60
+ pkg[field][name] = to
61
+ raised[field] = { ...raised[field], [name]: [range, to] }
62
+ changes.push([name, range, to])
63
+ }
64
+ }
65
+ }
66
+ if (changes.length === 0) return changes
67
+ await fs.writeFile(pkgPath, `${JSON.stringify(pkg, null, indentOf(text))}\n`)
68
+
69
+ const lockPath = path.join(cwd, 'package-lock.json')
70
+ let lockText
71
+ try {
72
+ lockText = await fs.readFile(lockPath, 'utf-8')
73
+ } catch {
74
+ return changes
75
+ }
76
+ const lock = JSON.parse(lockText)
77
+ const root = lock.packages?.['']
78
+ let lockChanged = false
79
+ for (const [field, entries] of Object.entries(raised)) {
80
+ for (const [name, [from, to]] of Object.entries(entries)) {
81
+ if (root?.[field]?.[name] === from) {
82
+ root[field][name] = to
83
+ lockChanged = true
84
+ }
85
+ }
86
+ }
87
+ if (lockChanged) {
88
+ await fs.writeFile(lockPath, `${JSON.stringify(lock, null, indentOf(lockText))}\n`)
89
+ }
90
+ return changes
91
+ }
92
+
21
93
  // A THIN site depends on @nera-static/nera and carries no engine source.
22
94
  // A LEGACY site is a clone of the old generator: it vendors the engine under
23
95
  // src/ and has no @nera-static/nera dependency — the case `nera update` migrates.
@@ -193,6 +265,9 @@ export async function runUpdate(args = [], options = {}) {
193
265
  const targets = neraDeps(pkg)
194
266
  console.log(`🔄 Updating Nera packages: ${targets.join(', ')}`)
195
267
  runNpm(['update', ...targets], cwd)
268
+ for (const [name, from, to] of await raiseRanges(cwd, targets)) {
269
+ console.log(` ✓ ${name}: ${from} → ${to} in package.json`)
270
+ }
196
271
  await syncAndReportAgentFiles(cwd, templateDir)
197
272
  await syncAndReportSkill(cwd, skillDir)
198
273
  console.log('✅ Up to date.')
@@ -61,10 +61,9 @@ finds pages the build would skip silently. Run it first.
61
61
  says nothing; `nera validate` warns (`layout-missing`). Renaming or deleting
62
62
  a layout breaks every page that uses it.
63
63
  - **The page body.** The rendered Markdown reaches the template as `content`.
64
- Without a theme, `theme/views/pages/default.pug` prints it with
65
- `main !{ content }` inside `block content`; with a theme, the theme's layout
66
- may already hold the `<main>`. A page has exactly one `<main>`: check the
67
- layout and the page template before adding one.
64
+ Without a theme, `theme/views/pages/default.pug` prints it in a `main`
65
+ element inside `block content`; with a theme, the theme's layout may hold
66
+ the `<main>`. A page has exactly one `<main>`: check before adding one.
68
67
  - **`public/` is wiped on every build.** Put source files in `pages/`,
69
68
  `theme/` or `config/`; anything written into `public/` by hand is lost.
70
69
  - **Site files override theme files.** If `config/app.yaml` sets `theme:`,
@@ -115,7 +114,8 @@ the URL.
115
114
  ## Getting online
116
115
 
117
116
  `npm run build` writes the finished site into `public/`. That folder is what
118
- gets deployed: upload its contents to any static web host.
117
+ gets deployed: upload its contents to any static web host. Replace the
118
+ imprint and privacy placeholders in `pages/` first, if the site has them.
119
119
 
120
120
  <!-- nera:site-notes — everything below this line belongs to the site owner; `nera update` keeps it as it is -->
121
121
  ## Notes for this site
@@ -6,3 +6,6 @@ translations:
6
6
  en:
7
7
  app_description: A static site built with Nera
8
8
  app_keywords: nera, static site generator
9
+ skip_to_content: Skip to content
10
+ imprint: Imprint
11
+ privacy: Privacy
@@ -0,0 +1,21 @@
1
+ ---
2
+ layout: pages/default.pug
3
+ title: Imprint
4
+ description: Imprint placeholder — replace it with your own legal notice
5
+ ---
6
+ # Imprint
7
+
8
+ **This is a placeholder.** It is not a legal notice. Replace this page with your
9
+ own imprint before you publish the site; if you are unsure what you need, ask a
10
+ lawyer.
11
+
12
+ ## To do
13
+
14
+ What an imprint typically names (the exact duty depends on your country and on
15
+ what the site does):
16
+
17
+ - who runs the site: full name, or company name and legal form
18
+ - a postal address where you can be reached
19
+ - fast ways to contact you: email address, and often a phone number
20
+ - the person responsible for the content, if the law asks for one
21
+ - register entries and VAT ID, if your business has them
@@ -0,0 +1,23 @@
1
+ ---
2
+ layout: pages/default.pug
3
+ title: Privacy
4
+ description: Privacy policy placeholder — replace it with your own privacy policy
5
+ ---
6
+ # Privacy
7
+
8
+ **This is a placeholder.** It is not a privacy policy. Replace this page with
9
+ your own privacy policy before you publish the site; if you are unsure what you
10
+ need, ask a lawyer.
11
+
12
+ ## To do
13
+
14
+ What a privacy policy typically covers (the exact duty depends on your country
15
+ and on what the site does):
16
+
17
+ - who is responsible for the data processing, and how to contact them
18
+ - what your hosting provider logs (such as IP addresses) and for how long
19
+ - every plugin, form, font, embed or analytics tool that processes visitor
20
+ data, its purpose and legal basis
21
+ - how long data is kept
22
+ - the visitor's rights (access, correction, deletion, objection) and where to
23
+ complain
@@ -9,6 +9,14 @@ html(lang=app.lang)
9
9
  meta(name="keywords", content=`${ meta.keywords || t('app_keywords') }`)
10
10
  meta(name="generator" content="nera")
11
11
  meta(name="robots" content=`${ meta.robots || app.robots }`)
12
+ style.
13
+ .skip-link { position: absolute; left: -9999px; }
14
+ .skip-link:focus { position: static; }
12
15
 
13
16
  body
17
+ a.skip-link(href="#main") #{ t('skip_to_content') }
14
18
  block content
19
+ footer
20
+ a(href="/imprint.html") #{ t('imprint') }
21
+ = ' · '
22
+ a(href="/privacy.html") #{ t('privacy') }
@@ -2,4 +2,4 @@
2
2
  extends ../layouts/layout
3
3
 
4
4
  block content
5
- main !{ content }
5
+ main#main !{ content }