@nera-static/nera 1.9.0 → 1.10.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
@@ -27,11 +27,11 @@ package is named after the folder (`My Bakery` → `my-bakery`).
27
27
 
28
28
  | Command | What it does |
29
29
  |---|---|
30
- | `nera new <name>` | Scaffold a new thin Nera site (one dependency: `@nera-static/nera`). Add `--theme <name>` to start from a theme. `nera new .` scaffolds into the current, empty folder (dotfiles, `AGENTS.md` and `CLAUDE.md` allowed). |
30
+ | `nera new <name>` | Scaffold a new thin Nera site (one dependency: `@nera-static/nera`). Add `--theme <name>` to start from a theme, `--no-install` to skip `npm install`. `nera new .` scaffolds into the current, empty folder (dotfiles, `AGENTS.md` and `CLAUDE.md` allowed). Every new site also gets the `nera` skill for AI assistants. |
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` (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, 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
 
@@ -75,6 +75,23 @@ packages are updated (and after `nera update --migrate`) it:
75
75
  A file is written only when its content changes, so running `nera update`
76
76
  again changes nothing.
77
77
 
78
+ ### The `nera` skill
79
+
80
+ Every site also gets the `nera` [Agent Skill](https://agentskills.io): a
81
+ `SKILL.md` with the workflow (scaffold, content, plugins, `nera validate`,
82
+ `nera build --check`, deploy `public/`) and short recipes — business site,
83
+ blog, portfolio, contact form, search, images, multilingual site, custom
84
+ domain — that assistants load when a task matches. It lives in two folders,
85
+ `.agents/skills/nera/` (Codex, Cursor, Copilot, Gemini CLI) and
86
+ `.claude/skills/nera/` (Claude Code); commit them with the site.
87
+
88
+ Both folders are Nera's: `nera new` writes them, and `nera update` (also after
89
+ `--migrate`) replaces them with the skill of the installed `@nera-static/nera`,
90
+ removing files the new version no longer has. Put your own instructions in
91
+ `AGENTS.md`'s notes section or in a skill folder of your own; other folders
92
+ under `.agents/skills/` and `.claude/skills/` are never touched. The source is
93
+ [`skills/nera/`](skills/nera/) in this repository.
94
+
78
95
  ## Starting from a theme
79
96
 
80
97
  ```bash
package/bin/nera.js CHANGED
@@ -13,7 +13,7 @@ const json = rest.includes('--json')
13
13
  const usage = `Nera — the static site generator CLI
14
14
 
15
15
  Usage:
16
- nera new <name> scaffold a new Nera site (--theme <name> to start from a theme)
16
+ nera new <name> scaffold a new Nera site (--theme <name> to start from a theme, --no-install to skip npm install)
17
17
  nera new . scaffold into the current, empty folder
18
18
  nera build render pages/ → public/ (--check to check the output after, --json for machine-readable output)
19
19
  nera dev build, serve and live-reload
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nera-static/nera",
3
- "version": "1.9.0",
3
+ "version": "1.10.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": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "bin",
11
+ "skills",
11
12
  "src",
12
13
  "template"
13
14
  ],
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: nera
3
+ description: Create, change, check and deploy websites built with Nera, the Markdown + Pug static site generator (npm package @nera-static/nera). Use when package.json lists @nera-static/nera, or when the user asks to build a website, page, blog, portfolio or business site with Nera, or to add a contact form, search, images, a second language or a custom domain to a Nera site. Holds the workflow, the checks to run after every change, and short recipes for common sites and features.
4
+ ---
5
+
6
+ # Nera
7
+
8
+ Nera turns Markdown pages and Pug templates into plain HTML. It is not
9
+ Eleventy, Hugo, Astro or Jekyll; do not apply their conventions.
10
+
11
+ ## Read this first
12
+
13
+ - **In an existing site, read `AGENTS.md` at the site root before anything
14
+ else.** It holds the folder map, the commands and the traps (pages need
15
+ `layout`, URLs end in `.html`, `public/` is wiped on every build, plugin
16
+ templates are copied and included by hand). This skill does not repeat it.
17
+ - No site yet? Scaffold one (step 1 below), then read its `AGENTS.md`.
18
+ - The "Notes for this site" section at the end of `AGENTS.md` belongs to the
19
+ site owner: follow it, and add to it what the next agent should know.
20
+
21
+ ## Workflow
22
+
23
+ 1. **Scaffold.** `npx @nera-static/nera new my-site` creates a folder and
24
+ installs; `npx @nera-static/nera new .` fills the current, empty folder.
25
+ `--theme <name>` starts from a theme; `--no-install` skips `npm install`.
26
+ 2. **Content.** One Markdown file per page in `pages/`, each with YAML
27
+ frontmatter that names its `layout`. Site name, language and texts for
28
+ templates go in `config/app.yaml`. Shared markup goes in `theme/views/`.
29
+ 3. **Plugins.** Look for a plugin before writing a feature (see below).
30
+ Install it, configure it in `config/<name>.yaml`, copy its templates with
31
+ its own binary and include them in a layout.
32
+ 4. **Validate.** `npx nera validate` and fix every error it reports. It
33
+ finds broken layouts and includes and pages the build would skip.
34
+ 5. **Build and check.** `npx nera build --check` and fix what it reports
35
+ (accessibility, privacy and legal hints on the built site).
36
+ `npm run dev` previews on http://localhost:3000 with live reload; stop it
37
+ when you are done.
38
+ 6. **Deploy.** `npm run build` writes the finished site into `public/`.
39
+ Upload the contents of `public/` to any static host. See
40
+ [custom-domain](recipes/custom-domain.md) for a domain of your own.
41
+
42
+ Repeat steps 2–5 for every change. For machine-readable results, add `--json`:
43
+ `npx nera validate --json`, `npx nera check --json`,
44
+ `npx nera build --check --json`.
45
+
46
+ Existing site on an older Nera? `npx nera update` updates the Nera packages
47
+ and refreshes `AGENTS.md` and this skill.
48
+
49
+ ## Look for a plugin before writing a feature
50
+
51
+ Features come from npm packages named `@nera-static/plugin-<name>`. An
52
+ installed plugin loads on the next build, with nothing to register. Do not
53
+ hand-write navigation, a contact form, tags, search, pagination, image
54
+ handling or a sitemap: a plugin exists for each. The catalog:
55
+ https://github.com/seebaermichi/nera/blob/main/PLUGINS.md
56
+
57
+ The published plugins: canonical-links, code-blocks, contact-form, images,
58
+ link-attributes, navigation, one-page, page-navigation, page-pagination,
59
+ popular-content, search, simple-page-list, sitemap, social-media-links,
60
+ stacks, statistics, tags. Each plugin's README (on npm, or
61
+ `github.com/seebaermichi/nera-plugin-<name>`) lists its config keys and its
62
+ template binary. Read it before you configure the plugin.
63
+
64
+ A feature no plugin covers can be a local plugin in
65
+ `src/plugins/<name>/index.js`; see the tutorial
66
+ [Write a local plugin](https://nera.js.org/tutorials/write-a-local-plugin.md).
67
+
68
+ ## Recipes
69
+
70
+ Short task guides. Each names the steps, the files it touches and the docs
71
+ page to open next.
72
+
73
+ - [Business site](recipes/business-site.md): home, about, services, contact,
74
+ imprint and privacy, with a menu
75
+ - [Blog](recipes/blog.md): posts, a post list, tags, pagination, sitemap
76
+ - [Portfolio](recipes/portfolio.md): project pages with responsive images
77
+ - [Contact form](recipes/contact-form.md): a `mailto:` form, no backend
78
+ - [Search](recipes/search.md): client-side search over the site
79
+ - [Images](recipes/images.md): responsive AVIF/WebP images
80
+ - [Multilingual site](recipes/multilingual.md): a second language
81
+ - [Custom domain](recipes/custom-domain.md): DNS, GitHub Pages `CNAME`,
82
+ `base_path`
83
+
84
+ ## Docs
85
+
86
+ The docs on https://nera.js.org are the truth; this skill only points to
87
+ them. For AI assistants, in English:
88
+
89
+ - Index: https://nera.js.org/llms.txt
90
+ - Everything in one file: https://nera.js.org/llms-full.txt
91
+ - Single pages as Markdown:
92
+ [getting started](https://nera.js.org/docs/getting-started.md),
93
+ [core concepts](https://nera.js.org/docs/core-concepts.md),
94
+ [writing content](https://nera.js.org/docs/writing-content.md),
95
+ [plugins](https://nera.js.org/docs/plugins.md),
96
+ [deployment](https://nera.js.org/docs/deployment.md),
97
+ [CLI](https://nera.js.org/docs/cli.md),
98
+ [using AI](https://nera.js.org/docs/using-ai.md)
99
+ - Tutorials: [build a blog](https://nera.js.org/tutorials/build-a-blog.md),
100
+ [custom navigation](https://nera.js.org/tutorials/custom-navigation.md),
101
+ [tag your content](https://nera.js.org/tutorials/tag-your-content.md),
102
+ [add search](https://nera.js.org/tutorials/add-search.md),
103
+ [prev/next pagination](https://nera.js.org/tutorials/prev-next-pagination.md),
104
+ [canonical links](https://nera.js.org/tutorials/canonical-links-seo.md),
105
+ [how nera.js.org is built](https://nera.js.org/tutorials/how-this-site-is-built.md)
@@ -0,0 +1,48 @@
1
+ # Recipe: blog
2
+
3
+ Posts in a folder, a list of posts on the home page, tags, previous/next
4
+ links and a sitemap.
5
+
6
+ ## Steps
7
+
8
+ 1. Scaffold: `npx @nera-static/nera new my-blog`, then read its `AGENTS.md`.
9
+ 2. One Markdown file per post in `pages/posts/`, e.g.
10
+ `pages/posts/first-post.md`, each with `layout`, `title`, `description`,
11
+ `date` (`2026-10-12`) and `tags` (`tags: travel, food` or a YAML list).
12
+ A post is served at `/posts/first-post.html`.
13
+ 3. Post list: `npm install @nera-static/plugin-simple-page-list`, set
14
+ `page_path: /posts`, `sortBy: date` and `sortOrder: descending` in
15
+ `config/simple-page-list.yaml`, run
16
+ `npx nera-simple-page-list`, and include the list in the template of the
17
+ home page (data: `app.pageList`).
18
+ 4. Tags: `npm install @nera-static/plugin-tags`. It builds a page per tag
19
+ at `/tags/<tag>.html`. Run `npx nera-tags` and include
20
+ `partials/tag-links` in the post template and `partials/tag-cloud` where
21
+ you want all tags.
22
+ 5. Previous/next: `npm install @nera-static/plugin-page-pagination`, give
23
+ each post a `pagination_order` number, run `npx nera-page-pagination`
24
+ and include it in the post template. It links pages of the same folder.
25
+ 6. A post template of its own: `theme/views/pages/post.pug`, extending
26
+ `../layouts/layout` like `pages/default.pug`, with exactly one `main`.
27
+ Posts then use `layout: pages/post.pug`.
28
+ 7. Sitemap: `npm install @nera-static/plugin-sitemap` and set `origin` in
29
+ `config/app.yaml`. It also writes `robots.txt`.
30
+ 8. Menu: `@nera-static/plugin-navigation`, as in
31
+ [business-site](business-site.md).
32
+ 9. `npx nera validate`, then `npx nera build --check`; fix what they report.
33
+
34
+ Each plugin's README names its config keys and include paths; read it
35
+ before step 3, 4 or 5.
36
+
37
+ ## Files touched
38
+
39
+ `pages/posts/*.md`, `pages/index.md`, `config/app.yaml`,
40
+ `config/simple-page-list.yaml`, `config/tags.yaml` (optional),
41
+ `theme/views/pages/post.pug`, `theme/views/vendor/plugin-*/`,
42
+ `package.json`.
43
+
44
+ ## Read next
45
+
46
+ - https://nera.js.org/tutorials/build-a-blog.md
47
+ - https://nera.js.org/tutorials/tag-your-content.md
48
+ - https://nera.js.org/tutorials/prev-next-pagination.md
@@ -0,0 +1,46 @@
1
+ # Recipe: business site
2
+
3
+ A small business presence: home, about, services, opening hours, contact,
4
+ imprint and privacy, with a menu on every page.
5
+
6
+ ## Steps
7
+
8
+ 1. Scaffold: `npx @nera-static/nera new my-business`, then read its
9
+ `AGENTS.md`.
10
+ 2. Set the site in `config/app.yaml`: `name`, `lang`, and `origin` (the
11
+ live URL, e.g. `https://example.com`) once the domain is known.
12
+ 3. Write one Markdown page per topic, each with `layout: pages/default.pug`,
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.
17
+ 4. Menu: `npm install @nera-static/plugin-navigation`, list the pages in
18
+ `config/navigation.yaml` (`href` ending in `.html`, `name`; groups such
19
+ as `main:` and `footer:` for a footer menu with imprint and privacy), run
20
+ `npx nera-navigation` and include a menu in
21
+ `theme/views/layouts/layout.pug` (see the plugin README for the mixins).
22
+ 5. Contact form: follow [contact-form](contact-form.md).
23
+ 6. Optional, for search engines: `@nera-static/plugin-sitemap` (needs
24
+ `origin`) and `@nera-static/plugin-canonical-links`. Social profiles:
25
+ `@nera-static/plugin-social-media-links`.
26
+ 7. Styles: a CSS file in `theme/assets/css/`, linked from the layout's
27
+ `head` (`link(rel="stylesheet" href="/css/main.css")`).
28
+ 8. `npx nera validate`, then `npx nera build --check`; fix what they report.
29
+ The legal hints from the check matter here: a business site usually
30
+ needs an imprint and a privacy page.
31
+ 9. Deploy `public/` (see [custom-domain](custom-domain.md)).
32
+
33
+ Mark invented details (address, hours, prices, legal texts) as placeholders
34
+ for the owner to replace, and say that legal pages need checking.
35
+
36
+ ## Files touched
37
+
38
+ `config/app.yaml`, `config/navigation.yaml`, `pages/*.md`,
39
+ `theme/views/layouts/layout.pug`, `theme/views/vendor/plugin-navigation/`,
40
+ `theme/assets/css/`, `package.json`.
41
+
42
+ ## Read next
43
+
44
+ - https://nera.js.org/docs/writing-content.md
45
+ - https://nera.js.org/tutorials/custom-navigation.md
46
+ - https://nera.js.org/docs/deployment.md
@@ -0,0 +1,63 @@
1
+ # Recipe: contact form
2
+
3
+ A contact form with `@nera-static/plugin-contact-form`. It opens the
4
+ visitor's mail client (`mailto:`): no backend, no form service, no account.
5
+ Do not hand-write a `<form>` or post to a third-party service.
6
+
7
+ ## Steps
8
+
9
+ 1. `npm install @nera-static/plugin-contact-form`
10
+ 2. Create `config/contact-form.yaml`. `recipient` and at least one field
11
+ are required; without them the form does not render and the build only
12
+ warns:
13
+
14
+ ```yaml
15
+ recipient: hello@example.com
16
+ fields:
17
+ - name: name
18
+ label: Your name
19
+ type: text
20
+ required: true
21
+ - name: email
22
+ label: Your email
23
+ type: email
24
+ required: true
25
+ - name: message
26
+ label: Message
27
+ type: textarea
28
+ required: true
29
+ ```
30
+
31
+ Field types: `text`, `email`, `tel`, `textarea`, `select`, `checkbox`,
32
+ `radio`. The README lists the optional keys (subject, labels, honeypot,
33
+ `consent_text` with a link to the privacy page).
34
+ 3. `npx nera-contact-form` copies the form to
35
+ `theme/views/vendor/plugin-contact-form/` and its script to
36
+ `theme/assets/js/contact-form.js`.
37
+ 4. A page template for the contact page, `theme/views/pages/contact.pug`:
38
+
39
+ ```pug
40
+ extends ../layouts/layout
41
+
42
+ block content
43
+ main
44
+ | !{ content }
45
+ include /vendor/plugin-contact-form/contact-form
46
+ ```
47
+
48
+ 5. `pages/contact.md` with `layout: pages/contact.pug`, a `title` and a
49
+ short text above the form.
50
+ 6. `npx nera validate`, then `npx nera build --check`. Open
51
+ `/contact.html` with `npm run dev` and submit once: the mail client
52
+ should open with the message filled in.
53
+
54
+ ## Files touched
55
+
56
+ `package.json`, `config/contact-form.yaml`, `pages/contact.md`,
57
+ `theme/views/pages/contact.pug`, `theme/views/vendor/plugin-contact-form/`,
58
+ `theme/assets/js/contact-form.js`.
59
+
60
+ ## Read next
61
+
62
+ - Plugin README: https://github.com/seebaermichi/nera-plugin-contact-form
63
+ - https://nera.js.org/docs/plugins.md
@@ -0,0 +1,38 @@
1
+ # Recipe: custom domain
2
+
3
+ Serve the built site under a domain of your own. Nera builds static files;
4
+ the domain is set at the host and at the domain registrar.
5
+
6
+ ## Steps
7
+
8
+ 1. Build: `npm run build`. Deploy the contents of `public/` to the host
9
+ (GitHub Pages, Netlify, any static host). Set the host to build with
10
+ `npm run build` and publish `public/`, or upload `public/` yourself.
11
+ 2. At the host, add the custom domain in its site settings.
12
+ 3. At the registrar, set the DNS records the host names for that domain,
13
+ usually a `CNAME` record for `www` pointing at the host and `A`/`AAAA`
14
+ records (or `ALIAS`) for the bare domain. Copy them from the host's own
15
+ docs; they differ per host. DNS can take hours to spread.
16
+ 4. **GitHub Pages:** it needs a file named `CNAME` (no extension) at the
17
+ root of the deployed site, holding only the domain (`www.example.com`).
18
+ Never write it into `public/`, which every build deletes. Put it in
19
+ `theme/assets/CNAME`: every file in `theme/assets/` is copied to the root
20
+ of `public/` on each build, so it ends up at `public/CNAME`.
21
+ 5. **`base_path`:** leave it unset for a custom domain or any site served
22
+ from the root. Set `base_path: /<repo>` in `config/app.yaml` only for a
23
+ site served under a sub-path, such as a GitHub project page at
24
+ `https://<user>.github.io/<repo>/`; Nera then prefixes every root-absolute
25
+ link and asset URL. Remove it again when the custom domain takes over.
26
+ 6. Set `origin: https://www.example.com` in `config/app.yaml`, so the
27
+ sitemap and canonical links use the real domain.
28
+ 7. Turn on HTTPS at the host once the domain resolves.
29
+ 8. `npx nera validate`, then `npx nera build --check`, then deploy again.
30
+
31
+ ## Files touched
32
+
33
+ `theme/assets/CNAME` (GitHub Pages), `config/app.yaml` (`origin`,
34
+ `base_path`), DNS settings outside the site.
35
+
36
+ ## Read next
37
+
38
+ - https://nera.js.org/docs/deployment.md
@@ -0,0 +1,45 @@
1
+ # Recipe: images
2
+
3
+ Responsive images with `@nera-static/plugin-images`: each picture becomes a
4
+ `<picture>` with AVIF and WebP in several widths, plus a fallback. Needs
5
+ `@nera-static/core` 4.13.0 or later (`npx nera update` brings it).
6
+
7
+ ## Steps
8
+
9
+ 1. `npm install @nera-static/plugin-images`
10
+ 2. Put the originals in the assets folder, e.g. `theme/assets/images/`.
11
+ They are referenced without `theme/assets`: `/images/photo.jpg`.
12
+ 3. In Markdown, write images as usual; the plugin rewrites them:
13
+ `![A pier at dusk](/images/pier.jpg)`. A title becomes a caption
14
+ (`![Alt](/images/pier.jpg "Caption")`), and query parameters crop or
15
+ size one image (`?w=800&ratio=16:9`, see the README).
16
+ 4. In frontmatter, `image: /images/hero.jpg` gets precomputed variants for
17
+ templates. Run `npx nera-images` to copy the `picture` mixin to
18
+ `theme/views/vendor/plugin-images/`, then in a template:
19
+
20
+ ```pug
21
+ include /vendor/plugin-images/picture
22
+ +picture(meta.images && meta.images.image, { alt: meta.image_alt })
23
+ ```
24
+
25
+ Images written directly in Pug (`img(src=…)`) are not processed.
26
+ 5. Optional `config/images.yaml`: `formats`, `widths`, `sizes`, `quality`.
27
+ 6. Variants are cached in `.nera-cache/images/`; the scaffold's
28
+ `.gitignore` already ignores `.nera-cache`.
29
+ 7. Originals with location data? List their folder (e.g. `images`) in a
30
+ `.neraignore` file at the site root; its paths are relative to the
31
+ assets folder. Only the processed variants, without metadata, are then
32
+ published.
33
+ 8. `npx nera validate`, then `npx nera build --check`; every image needs
34
+ alt text.
35
+
36
+ ## Files touched
37
+
38
+ `package.json`, `theme/assets/images/`, `pages/*.md`,
39
+ `config/images.yaml` (optional), `theme/views/vendor/plugin-images/`,
40
+ `.neraignore` (optional).
41
+
42
+ ## Read next
43
+
44
+ - Plugin README: https://github.com/seebaermichi/nera-plugin-images
45
+ - https://nera.js.org/docs/writing-content.md
@@ -0,0 +1,50 @@
1
+ # Recipe: multilingual site
2
+
3
+ A second language beside the first, e.g. English at `/` and German at
4
+ `/de/`.
5
+
6
+ ## Steps
7
+
8
+ 1. Keep the default language at the root of `pages/` and put each other
9
+ language in its own folder: `pages/de/index.md` becomes `/de/index.html`.
10
+ 2. Give every page in another language `lang` in its frontmatter
11
+ (`lang: de`). Pages without it use `lang` from `config/app.yaml`.
12
+ 3. Pair the versions of a page with the same `slug` in their frontmatter
13
+ (`slug: about` on `pages/about.md` and `pages/de/ueber-uns.md`).
14
+ 4. Template texts: write `t('key')` in Pug and add the texts per language
15
+ under `translations:` in `config/app.yaml`:
16
+
17
+ ```yaml
18
+ lang: en
19
+ translations:
20
+ en:
21
+ read_more: Read more
22
+ de:
23
+ read_more: Weiterlesen
24
+ ```
25
+
26
+ `t()` uses the page's `lang`, else the site's.
27
+ 5. In `theme/views/layouts/layout.pug`, set the document language from the
28
+ page: `html(lang=meta.lang || app.lang)`.
29
+ 6. Menus: one group per language in `config/navigation.yaml` (`main:` and
30
+ `main_de:`), each rendered with the menu mixin and the language root,
31
+ e.g. `+linkListNav(app.nav.main_de.elements, 'nav--main', '/de')`.
32
+ Add a link to the other language's version of the page.
33
+ 7. Search engines: `@nera-static/plugin-canonical-links` with
34
+ `available_languages` in `config/canonical-links.yaml` adds `hreflang`
35
+ links; `@nera-static/plugin-sitemap` lists the pairs too. Both use
36
+ `lang` and `slug`.
37
+ 8. Search and tags can keep one index or tag list per language
38
+ (`group_by_lang` in `config/search.yaml` or `config/tags.yaml`).
39
+ 9. `npx nera validate`, then `npx nera build --check`.
40
+
41
+ ## Files touched
42
+
43
+ `pages/<lang>/*.md`, `config/app.yaml`, `config/navigation.yaml`,
44
+ `config/canonical-links.yaml`, `theme/views/layouts/layout.pug`.
45
+
46
+ ## Read next
47
+
48
+ - https://nera.js.org/docs/core-concepts.md
49
+ - https://nera.js.org/tutorials/canonical-links-seo.md
50
+ - https://nera.js.org/tutorials/custom-navigation.md
@@ -0,0 +1,41 @@
1
+ # Recipe: portfolio
2
+
3
+ A home page with a grid of projects, one page per project, large responsive
4
+ images.
5
+
6
+ ## Steps
7
+
8
+ 1. Scaffold: `npx @nera-static/nera new my-portfolio`, then read its
9
+ `AGENTS.md`.
10
+ 2. Images: put originals in `theme/assets/images/` and set up
11
+ [images](images.md) first, so every photo is served as AVIF/WebP in
12
+ several widths.
13
+ 3. One Markdown file per project in `pages/projects/`, each with `layout`,
14
+ `title`, `description`, `date` and `image: /images/<file>.jpg` (the cover
15
+ picture). Show more photos in the body with
16
+ `![Alt text](/images/<file>.jpg "Caption")`.
17
+ 4. Project list: `npm install @nera-static/plugin-simple-page-list`, set
18
+ `page_path: /projects` in `config/simple-page-list.yaml`, run
19
+ `npx nera-simple-page-list`, and render `app.pageList` on the home page.
20
+ For a grid with cover images, loop over `app.pageList` in your own Pug
21
+ template instead of the published one.
22
+ 5. A project template, `theme/views/pages/project.pug`, extending
23
+ `../layouts/layout`, that shows the cover with the images plugin's
24
+ `+picture` mixin and then `content`, with exactly one `main`.
25
+ 6. Optional: `@nera-static/plugin-tags` to group projects by kind,
26
+ `@nera-static/plugin-page-pagination` for previous/next project,
27
+ `@nera-static/plugin-social-media-links`, `@nera-static/plugin-sitemap`.
28
+ 7. `npx nera validate`, then `npx nera build --check`. The check reports
29
+ images without alt text; describe what each picture shows.
30
+
31
+ ## Files touched
32
+
33
+ `theme/assets/images/`, `pages/projects/*.md`, `pages/index.md`,
34
+ `config/images.yaml` (optional), `config/simple-page-list.yaml`,
35
+ `theme/views/pages/project.pug`, `theme/views/vendor/plugin-*/`,
36
+ `package.json`.
37
+
38
+ ## Read next
39
+
40
+ - https://nera.js.org/docs/writing-content.md
41
+ - https://nera.js.org/docs/plugins.md
@@ -0,0 +1,48 @@
1
+ # Recipe: search
2
+
3
+ Client-side search with `@nera-static/plugin-search`: the build writes a
4
+ JSON index, a small script searches it in the browser. No backend.
5
+
6
+ ## Steps
7
+
8
+ 1. `npm install @nera-static/plugin-search`. It works without config;
9
+ `config/search.yaml` can change the indexed `fields` or set
10
+ `group_by_lang: true` for one index per language.
11
+ 2. `npx nera-search` copies the search template to
12
+ `theme/views/vendor/plugin-search/` and its script to
13
+ `theme/assets/js/search.js`.
14
+ 3. A page template for the search page, `theme/views/pages/search.pug`:
15
+
16
+ ```pug
17
+ extends ../layouts/layout
18
+
19
+ block content
20
+ main
21
+ | !{ content }
22
+ include /vendor/plugin-search/search.pug
23
+ ```
24
+
25
+ 4. `pages/search.md` with `layout: pages/search.pug` and a `title`, and no
26
+ `#` heading: the copied template brings its own `h1`. Link the page from
27
+ the menu.
28
+ 5. The copy is the site's own: give its search input a label
29
+ (`aria-label="Search"`), or `nera build --check` warns
30
+ (`a11y-form-label`).
31
+ 6. The index is written to `theme/assets/search-index.json` on every build
32
+ and served as `/search-index.json`. It is generated: add
33
+ `theme/assets/search-index*.json` to `.gitignore`.
34
+ 7. Only pages with a `layout` are indexed. If other plugins add pages (tags),
35
+ run search last with `config/plugin-order.yaml` as the README shows.
36
+ 8. `npx nera validate`, then `npx nera build --check`; try a query with
37
+ `npm run dev`.
38
+
39
+ ## Files touched
40
+
41
+ `package.json`, `config/search.yaml` (optional), `pages/search.md`,
42
+ `theme/views/pages/search.pug`, `theme/views/vendor/plugin-search/`,
43
+ `theme/assets/js/search.js`, `.gitignore`.
44
+
45
+ ## Read next
46
+
47
+ - https://nera.js.org/tutorials/add-search.md
48
+ - Plugin README: https://github.com/seebaermichi/nera-plugin-search
@@ -4,6 +4,7 @@ import fssync from 'fs'
4
4
  import path from 'path'
5
5
  import { syncAgentFiles, SITE_NOTES_MARKER } from '../agent-files.js'
6
6
  import { templateDir as packageTemplateDir } from '../scaffold.js'
7
+ import { installedSkillDir, skillSourceDir, syncSkill } from '../skill.js'
7
8
 
8
9
  const defaultRunNpm = (npmArgs, cwd) =>
9
10
  execFileSync('npm', npmArgs, { cwd, stdio: 'inherit' })
@@ -86,7 +87,22 @@ async function syncAndReportAgentFiles(cwd, fallbackDir) {
86
87
  }
87
88
  }
88
89
 
89
- async function migrateLegacy(cwd, pkg, runNpm, templateDir) {
90
+ const SKILL_MESSAGES = {
91
+ created: (dir) => ` ✓ Added the Nera skill in ${dir}`,
92
+ updated: (dir) => ` ✓ Refreshed the Nera skill in ${dir}`,
93
+ unchanged: (dir) => ` ✓ ${dir} unchanged`,
94
+ }
95
+
96
+ // The skill folders are wholly Nera's, so they always follow the installed
97
+ // version (see installedSkillDir).
98
+ async function syncAndReportSkill(cwd, fallbackDir) {
99
+ const results = await syncSkill({ cwd, sourceDir: installedSkillDir(cwd, fallbackDir) })
100
+ for (const { path: dir, action } of results) {
101
+ console.log(SKILL_MESSAGES[action](dir))
102
+ }
103
+ }
104
+
105
+ async function migrateLegacy(cwd, pkg, runNpm, templateDir, skillDir) {
90
106
  console.log('🔧 Migrating this cloned site to the thin @nera-static/nera model...')
91
107
 
92
108
  pkg.dependencies = {
@@ -131,6 +147,7 @@ async function migrateLegacy(cwd, pkg, runNpm, templateDir) {
131
147
  console.log('📦 Installing...')
132
148
  runNpm(['install'], cwd)
133
149
  await syncAndReportAgentFiles(cwd, templateDir)
150
+ await syncAndReportSkill(cwd, skillDir)
134
151
 
135
152
  console.log('✅ Migrated. Run `nera dev` to start.')
136
153
  if (movedPlugins) {
@@ -143,11 +160,13 @@ async function migrateLegacy(cwd, pkg, runNpm, templateDir) {
143
160
 
144
161
  export async function runUpdate(args = [], options = {}) {
145
162
  // templateDir: the fallback scaffold template for AGENTS.md / CLAUDE.md
146
- // (see agentTemplateDir); injectable so tests need no installed package.
163
+ // (see agentTemplateDir); skillDir: the fallback Nera skill (see
164
+ // installedSkillDir). Both injectable so tests need no installed package.
147
165
  const {
148
166
  cwd = process.cwd(),
149
167
  runNpm = defaultRunNpm,
150
168
  templateDir = packageTemplateDir(),
169
+ skillDir = skillSourceDir(),
151
170
  } = options
152
171
 
153
172
  const pkg = JSON.parse(
@@ -157,7 +176,7 @@ export async function runUpdate(args = [], options = {}) {
157
176
 
158
177
  if (kind === 'legacy') {
159
178
  if (args.includes('--migrate') || args.includes('--yes')) {
160
- await migrateLegacy(cwd, pkg, runNpm, templateDir)
179
+ await migrateLegacy(cwd, pkg, runNpm, templateDir, skillDir)
161
180
  } else {
162
181
  printLegacyGuidance()
163
182
  }
@@ -175,5 +194,6 @@ export async function runUpdate(args = [], options = {}) {
175
194
  console.log(`🔄 Updating Nera packages: ${targets.join(', ')}`)
176
195
  runNpm(['update', ...targets], cwd)
177
196
  await syncAndReportAgentFiles(cwd, templateDir)
197
+ await syncAndReportSkill(cwd, skillDir)
178
198
  console.log('✅ Up to date.')
179
199
  }
package/src/scaffold.js CHANGED
@@ -3,6 +3,7 @@ import fs from 'fs/promises'
3
3
  import fssync from 'fs'
4
4
  import path from 'path'
5
5
  import { fileURLToPath } from 'url'
6
+ import { skillSourceDir, syncSkill } from './skill.js'
6
7
 
7
8
  // Directory-safe and npm-safe: no separators, no traversal, no leading dot or
8
9
  // dash, nothing the shell could reinterpret. Deliberately stricter than npm's
@@ -182,6 +183,16 @@ async function applyTheme(targetDir, theme) {
182
183
  console.log(` ✓ Using theme "${theme}"${pkgName ? ` (${pkgName})` : ''}`)
183
184
  }
184
185
 
186
+ // The Nera skill for AI assistants, in both folders the clients read
187
+ // (.agents/skills/nera, .claude/skills/nera). Meant to be committed, and
188
+ // refreshed by `nera update`.
189
+ async function addSkill(targetDir) {
190
+ const results = await syncSkill({ cwd: targetDir, sourceDir: skillSourceDir() })
191
+ console.log(
192
+ ` ✓ Added the Nera skill for AI assistants (${results.map((r) => r.path).join(', ')})`
193
+ )
194
+ }
195
+
185
196
  // Scaffold a thin Nera site: copy the template, name it, and (by default)
186
197
  // install its single dependency, @nera-static/nera. No git clone, no vendored
187
198
  // engine — the opposite of the old installer's clone-and-strip flow.
@@ -204,6 +215,7 @@ export async function scaffoldProject(projectName, options = {}) {
204
215
  await copyDir(templateDir(), targetDir, { skipStarters: Boolean(theme) })
205
216
  await personalize(targetDir, packageName)
206
217
  if (theme) await applyTheme(targetDir, theme)
218
+ await addSkill(targetDir)
207
219
 
208
220
  if (install) {
209
221
  console.log('📦 Installing dependencies...')
package/src/skill.js ADDED
@@ -0,0 +1,108 @@
1
+ import fs from 'fs/promises'
2
+ import fssync from 'fs'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ // The folders of a site the Nera skill is copied into: `.agents/skills/` is
7
+ // read by Codex, Cursor, Copilot and Gemini CLI, `.claude/skills/` by Claude
8
+ // Code. Each `nera/` folder is wholly Nera's; nothing beside it is touched.
9
+ export const SKILL_TARGETS = [
10
+ path.join('.agents', 'skills', 'nera'),
11
+ path.join('.claude', 'skills', 'nera'),
12
+ ]
13
+
14
+ // The skill's source ships inside this package (package.json `files`).
15
+ export const skillSourceDir = () =>
16
+ path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'skills', 'nera')
17
+
18
+ // The skill a site is synced to: the one in the site's installed
19
+ // @nera-static/nera (read after npm has updated it), so it matches the Nera
20
+ // version the site runs. Falls back to fallbackDir when nothing is installed
21
+ // there or the installed version predates the skill.
22
+ export function installedSkillDir(cwd, fallbackDir) {
23
+ const installed = path.join(cwd, 'node_modules', '@nera-static', 'nera', 'skills', 'nera')
24
+ return fssync.existsSync(path.join(installed, 'SKILL.md')) ? installed : fallbackDir
25
+ }
26
+
27
+ async function readIfExists(file) {
28
+ try {
29
+ return await fs.readFile(file)
30
+ } catch (err) {
31
+ if (err.code === 'ENOENT' || err.code === 'EISDIR') return null
32
+ throw err
33
+ }
34
+ }
35
+
36
+ async function entries(dir) {
37
+ try {
38
+ return await fs.readdir(dir, { withFileTypes: true })
39
+ } catch (err) {
40
+ if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return []
41
+ throw err
42
+ }
43
+ }
44
+
45
+ // Make `target` an exact copy of `source`: copy what is missing, rewrite what
46
+ // differs, remove what the source no longer has. Writes only on change.
47
+ // Returns true when anything was written or removed.
48
+ async function mirror(source, target) {
49
+ let changed = false
50
+ const stat = await fs.lstat(target).catch(() => null)
51
+ if (stat && !stat.isDirectory()) {
52
+ await fs.rm(target, { recursive: true, force: true })
53
+ }
54
+ if (!stat || !stat.isDirectory()) {
55
+ await fs.mkdir(target, { recursive: true })
56
+ changed = true
57
+ }
58
+
59
+ const wanted = new Set()
60
+ for (const entry of await entries(source)) {
61
+ wanted.add(entry.name)
62
+ const from = path.join(source, entry.name)
63
+ const to = path.join(target, entry.name)
64
+ if (entry.isDirectory()) {
65
+ if (await mirror(from, to)) changed = true
66
+ continue
67
+ }
68
+ const content = await fs.readFile(from)
69
+ const current = await readIfExists(to)
70
+ if (current !== null && current.equals(content)) continue
71
+ await fs.rm(to, { recursive: true, force: true })
72
+ await fs.writeFile(to, content)
73
+ changed = true
74
+ }
75
+
76
+ for (const entry of await entries(target)) {
77
+ if (wanted.has(entry.name)) continue
78
+ await fs.rm(path.join(target, entry.name), { recursive: true, force: true })
79
+ changed = true
80
+ }
81
+ return changed
82
+ }
83
+
84
+ // Bring a site's copies of the Nera skill in line with sourceDir: each of
85
+ // SKILL_TARGETS becomes an exact mirror of it. Nothing outside those two
86
+ // `nera/` folders is read or written. Returns one { path, action } per
87
+ // folder (path relative to cwd, with `/`), action being `created`,
88
+ // `updated` or `unchanged`; printing is the caller's job.
89
+ export async function syncSkill({ cwd = process.cwd(), sourceDir }) {
90
+ if (!fssync.existsSync(path.join(sourceDir, 'SKILL.md'))) {
91
+ throw new Error(
92
+ `The Nera skill (${sourceDir}) has no SKILL.md. This is a packaging ` +
93
+ 'bug in @nera-static/nera, not a problem with your site.'
94
+ )
95
+ }
96
+
97
+ const results = []
98
+ for (const target of SKILL_TARGETS) {
99
+ const dir = path.join(cwd, target)
100
+ const existed = fssync.existsSync(dir)
101
+ const changed = await mirror(sourceDir, dir)
102
+ results.push({
103
+ path: target.split(path.sep).join('/'),
104
+ action: !existed ? 'created' : changed ? 'updated' : 'unchanged',
105
+ })
106
+ }
107
+ return results
108
+ }