@nera-static/nera 1.1.0 → 1.3.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
@@ -19,11 +19,12 @@ npm run dev # or: nera dev
19
19
  | Command | What it does |
20
20
  |---|---|
21
21
  | `nera new <name>` | Scaffold a new thin Nera site (one dependency: `@nera-static/nera`). Add `--theme <name>` to start from a theme. |
22
- | `nera build` | Render `pages/` → `public/`. |
23
- | `nera dev` | Build, serve `public/`, and rebuild on change with live reload. |
22
+ | `nera build` | Render `pages/` → `public/`. With `--check`, then run `nera check` on the result — one command for CI. |
23
+ | `nera dev` | Build, serve `public/`, and rebuild on change with live reload. Watches `pages/`, `config/`, `theme/` and a local theme (`theme: ./themes/<name>`). |
24
24
  | `nera serve` | Serve the already-built `public/` folder. |
25
25
  | `nera update` | Update the site's Nera packages. On a legacy cloned site, `nera update --migrate` converts it to the thin model. |
26
- | `nera validate` | Check the site (layouts, includes, YAML) before publishing; exits non-zero on any error. |
26
+ | `nera validate` | Check the site's sources (layouts, includes, YAML) before publishing; exits non-zero on any error. |
27
+ | `nera check` | Check the built `public/` for accessibility, privacy and legal-notice problems (see below). Build first. |
27
28
 
28
29
  A scaffolded site is a thin project — no vendored engine, no clone. It lists one
29
30
  dependency and its scripts call the CLI:
@@ -55,6 +56,61 @@ later stays hidden behind them. Each starter file carries a
55
56
  (`theme-shadowed`) while such a file hides a theme's. Delete it to use the
56
57
  theme's version, or delete the marker line to keep yours.
57
58
 
59
+ ## Checking the built site
60
+
61
+ ```bash
62
+ nera build --check # build, then check — what a CI job runs
63
+ nera check # check an existing public/ without rebuilding
64
+ ```
65
+
66
+ `nera validate` reads the sources; `nera check` reads the **built** output — the
67
+ page a visitor gets, with its layout, navigation, footer, scripts, stylesheets
68
+ and fonts — and reports what a parser can find there:
69
+
70
+ - **Accessibility** (`a11y-*`, WCAG 2.2 AA where machine-decidable): `<html
71
+ lang>`, `<title>`, one `<h1>`, heading jumps, `alt` text, form labels, link
72
+ names, `<main>`, a skip link, named navigations, duplicate ids, zoom blocked
73
+ by the viewport.
74
+ - **Privacy** (`privacy-*`, DSGVO / TDDDG): resources loaded from another host
75
+ (Google Fonts, YouTube, analytics, …), `http://` resources, and — opt-in —
76
+ scripts using cookies or browser storage.
77
+ - **Legal notice** (`legal-*`, DDG / MStV): every page links to the imprint and
78
+ the privacy policy, and those pages do not cite a superseded law (TMG, TTDSG,
79
+ § 55 RStV).
80
+
81
+ Every finding is a **warning** by default, so `nera check` exits `0`; it exits
82
+ `1` only when a rule you promoted to `error` fires. Without `public/` it stops
83
+ with "run `nera build` first". A template problem is reported once, with the
84
+ number of pages it appears on.
85
+
86
+ These are hints, not legal advice, and a clean run is not proof of compliance:
87
+ automated checks find only part of the accessibility problems (about a third of
88
+ WCAG failures), and contrast, focus visibility or whether a law applies to your
89
+ site are not checked. Every report ends with that reminder.
90
+
91
+ Configure it in `config/validate.yaml` (optional):
92
+
93
+ ```yaml
94
+ rules:
95
+ a11y-img-alt: error # promote: fails the check
96
+ a11y-target-blank: warning # enable an opt-in rule
97
+ a11y-skip-link: off # disable
98
+ legal:
99
+ imprint: { de: /de/impressum.html, en: /en/imprint.html }
100
+ privacy: { de: /de/datenschutz.html, en: /en/privacy.html }
101
+ privacy:
102
+ allowed_hosts: [cdn.example.org] # third-party hosts you have accounted for
103
+ ```
104
+
105
+ Silence a rule on one page with `validate_ignore: [a11y-h1]` in its frontmatter.
106
+ Your own host is `origin` in `config/app.yaml`, else `app_origin` in
107
+ `config/canonical-links.yaml`. Without `legal.*` config, the imprint and privacy
108
+ links are found by their text in German and English only (*Impressum*,
109
+ *Imprint*, *Datenschutz*, *Privacy*, …); pages in other languages need
110
+ `legal.imprint.<lang>` / `legal.privacy.<lang>`. The full rule tables, with the
111
+ WCAG criterion or law behind each rule, are in the
112
+ [`@nera-static/validate` README](https://github.com/seebaermichi/nera-validate#checking-the-built-output).
113
+
58
114
  ## Migrating a cloned (legacy) site
59
115
 
60
116
  Older Nera sites were git clones that vendored the engine under `src/`. Inside
package/bin/nera.js CHANGED
@@ -5,6 +5,7 @@ import { runDev } from '../src/commands/dev.js'
5
5
  import { runServe } from '../src/commands/serve.js'
6
6
  import { runUpdate } from '../src/commands/update.js'
7
7
  import { runValidate } from '../src/commands/validate.js'
8
+ import { runCheck } from '../src/commands/check.js'
8
9
 
9
10
  const [, , command, ...rest] = process.argv
10
11
 
@@ -12,11 +13,12 @@ const usage = `Nera — the static site generator CLI
12
13
 
13
14
  Usage:
14
15
  nera new <name> scaffold a new Nera site (--theme <name> to start from a theme)
15
- nera build render pages/ → public/
16
+ nera build render pages/ → public/ (--check to check the output after)
16
17
  nera dev build, serve and live-reload
17
18
  nera serve serve the built public/ folder
18
19
  nera update update Nera packages (or migrate a cloned site with --migrate)
19
20
  nera validate check the site (layouts, includes, YAML) before publish
21
+ nera check check the built public/ (accessibility, privacy, legal hints)
20
22
  `
21
23
 
22
24
  try {
@@ -25,7 +27,7 @@ try {
25
27
  await runNew(rest)
26
28
  break
27
29
  case 'build':
28
- await runBuild(rest)
30
+ process.exitCode = await runBuild(rest)
29
31
  break
30
32
  case 'dev':
31
33
  await runDev(rest)
@@ -39,6 +41,9 @@ try {
39
41
  case 'validate':
40
42
  process.exit(runValidate())
41
43
  break
44
+ case 'check':
45
+ process.exit(runCheck())
46
+ break
42
47
  default:
43
48
  console.log(usage)
44
49
  // No command → help (exit 0); an unknown command → error (exit 1).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nera-static/nera",
3
- "version": "1.1.0",
3
+ "version": "1.3.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": {
@@ -42,8 +42,8 @@
42
42
  "access": "public"
43
43
  },
44
44
  "dependencies": {
45
- "@nera-static/core": "^4.9.0",
46
- "@nera-static/validate": "^1.0.0",
45
+ "@nera-static/core": "^4.12.0",
46
+ "@nera-static/validate": "^1.2.0",
47
47
  "chokidar": "^5.0.0",
48
48
  "semver": "^7.8.5",
49
49
  "vite": "^8.1.5"
@@ -1,8 +1,12 @@
1
1
  import run from '@nera-static/core'
2
+ import { runCheck } from './check.js'
2
3
 
3
4
  // Render the current site (pages/ → public/) using the shared engine. `run`
4
5
  // reads config and folders relative to the current working directory, so this
5
- // builds whichever site the CLI is invoked in.
6
- export async function runBuild() {
6
+ // builds whichever site the CLI is invoked in. With `--check`, the fresh output
7
+ // is then checked as by `nera check` — one command for CI. Returns the exit
8
+ // code: the check's, or 0 for a plain build.
9
+ export async function runBuild(args = []) {
7
10
  await run()
11
+ return args.includes('--check') ? runCheck() : 0
8
12
  }
@@ -0,0 +1,12 @@
1
+ import { validateOutput, hasErrors, formatOutputResults } from '@nera-static/validate'
2
+
3
+ // Check the built output in public/ — accessibility, privacy and legal-notice
4
+ // hints — and print the results. `nera validate` checks the sources; this checks
5
+ // what the build produced, so it needs a build first: validateOutput throws
6
+ // "run `nera build` first" when public/ is missing or holds no HTML. Returns the
7
+ // exit code (1 on any error, 0 otherwise), like runValidate.
8
+ export function runCheck({ cwd = process.cwd() } = {}) {
9
+ const results = validateOutput({ cwd })
10
+ console.log(formatOutputResults(results))
11
+ return hasErrors(results) ? 1 : 0
12
+ }
@@ -1,5 +1,6 @@
1
1
  import fssync from 'fs'
2
- import run from '@nera-static/core'
2
+ import path from 'path'
3
+ import run, { resolveSiteModel } from '@nera-static/core'
3
4
  import { startServer } from './serve.js'
4
5
 
5
6
  // Build once, serve `public/`, then rebuild on any change to the site's
@@ -19,13 +20,8 @@ export async function runDev(args = []) {
19
20
 
20
21
  const { default: chokidar } = await import('chokidar')
21
22
 
22
- // Presentation lives under theme/ in the current layout, or the deprecated
23
- // root views/ on an unmigrated site — watch whichever exists.
24
- const watchDirs = [
25
- 'pages',
26
- 'config',
27
- fssync.existsSync('theme') ? 'theme' : 'views',
28
- ].filter((d) => fssync.existsSync(d))
23
+ const watched = new Set(watchTargets())
24
+ const watcher = chokidar.watch([...watched], { ignoreInitial: true })
29
25
 
30
26
  // Serialise rebuilds: coalesce changes that land mid-build into one re-run.
31
27
  let building = false
@@ -42,22 +38,59 @@ export async function runDev(args = []) {
42
38
  console.error('❌ Build error:', err.message)
43
39
  }
44
40
  building = false
41
+ // config/app.yaml may now name a different local theme — start
42
+ // watching it too. A dropped one stays watched; that only costs an
43
+ // extra rebuild if it is edited.
44
+ for (const dir of watchTargets()) {
45
+ if (!watched.has(dir)) {
46
+ watched.add(dir)
47
+ watcher.add(dir)
48
+ }
49
+ }
45
50
  if (queued) {
46
51
  queued = false
47
52
  await rebuild()
48
53
  }
49
54
  }
50
55
 
51
- chokidar
52
- .watch(watchDirs, { ignoreInitial: true })
53
- .on('all', async (event, filePath) => {
54
- console.log(`↻ ${event} ${filePath} — rebuilding`)
55
- await rebuild()
56
- })
56
+ watcher.on('all', async (event, filePath) => {
57
+ console.log(
58
+ `↻ ${event} ${path.relative(process.cwd(), filePath)} — rebuilding`
59
+ )
60
+ await rebuild()
61
+ })
57
62
 
58
63
  return server
59
64
  }
60
65
 
66
+ // What a rebuild depends on: pages/, config/, the site's presentation folder
67
+ // — theme/ in the current layout, or the deprecated root views/ on an
68
+ // unmigrated site — and a LOCAL theme (`theme: ./themes/classic`, or the same
69
+ // via NERA_THEME), which lives outside all of those. The theme is resolved
70
+ // through core, so the watcher follows exactly the spec the build uses. Only
71
+ // the theme's payload folders are watched, never its root: `theme: .` makes
72
+ // the root the site itself, and watching public/ would rebuild forever. An
73
+ // installed theme package under node_modules is left alone: it changes only on
74
+ // npm install, and watching node_modules is expensive.
75
+ export function watchTargets(cwd = process.cwd()) {
76
+ const dirs = [
77
+ 'pages',
78
+ 'config',
79
+ fssync.existsSync(path.join(cwd, 'theme')) ? 'theme' : 'views',
80
+ ].map((d) => path.join(cwd, d))
81
+
82
+ const { theme } = resolveSiteModel({ cwd })
83
+ if (theme && !theme.root.split(path.sep).includes('node_modules')) {
84
+ dirs.push(
85
+ theme.viewsRoot,
86
+ theme.assetsRoot,
87
+ path.join(theme.root, 'config')
88
+ )
89
+ }
90
+
91
+ return [...new Set(dirs)].filter((d) => fssync.existsSync(d))
92
+ }
93
+
61
94
  const parsePort = (args) => {
62
95
  const i = args.indexOf('--port')
63
96
  const val = i >= 0 ? Number(args[i + 1]) : NaN