@nera-static/nera 1.9.0 → 1.10.1
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 +19 -2
- package/bin/nera.js +1 -1
- package/package.json +2 -1
- package/skills/nera/SKILL.md +105 -0
- package/skills/nera/recipes/blog.md +48 -0
- package/skills/nera/recipes/business-site.md +46 -0
- package/skills/nera/recipes/contact-form.md +63 -0
- package/skills/nera/recipes/custom-domain.md +38 -0
- package/skills/nera/recipes/images.md +45 -0
- package/skills/nera/recipes/multilingual.md +50 -0
- package/skills/nera/recipes/portfolio.md +41 -0
- package/skills/nera/recipes/search.md +52 -0
- package/src/commands/update.js +23 -3
- package/src/scaffold.js +12 -0
- package/src/skill.js +108 -0
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
|
|
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.
|
|
3
|
+
"version": "1.10.1",
|
|
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 title becomes a caption
|
|
14
|
+
(``), 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
|
+
``.
|
|
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,52 @@
|
|
|
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`, a `title` and a
|
|
26
|
+
`# Search` heading, and `show_title: false` in `config/search.yaml`, so
|
|
27
|
+
that heading is the page's only `h1` (without the key the template adds
|
|
28
|
+
its own and `nera build --check` warns `a11y-h1`). Link the page from
|
|
29
|
+
the menu.
|
|
30
|
+
5. The template's input is already labelled (`aria-label`, default
|
|
31
|
+
"Search"). To translate the template, add `search_label`, `search_title`
|
|
32
|
+
and `search_placeholder` to the site's translations. A copy published
|
|
33
|
+
before plugin-search 2.3.0 lacks both: refresh it with
|
|
34
|
+
`npx nera-search --force` if it was never edited.
|
|
35
|
+
6. The index is written to `theme/assets/search-index.json` on every build
|
|
36
|
+
and served as `/search-index.json`. It is generated: add
|
|
37
|
+
`theme/assets/search-index*.json` to `.gitignore`.
|
|
38
|
+
7. Only pages with a `layout` are indexed. If other plugins add pages (tags),
|
|
39
|
+
run search last with `config/plugin-order.yaml` as the README shows.
|
|
40
|
+
8. `npx nera validate`, then `npx nera build --check`; try a query with
|
|
41
|
+
`npm run dev`.
|
|
42
|
+
|
|
43
|
+
## Files touched
|
|
44
|
+
|
|
45
|
+
`package.json`, `config/search.yaml` (optional), `pages/search.md`,
|
|
46
|
+
`theme/views/pages/search.pug`, `theme/views/vendor/plugin-search/`,
|
|
47
|
+
`theme/assets/js/search.js`, `.gitignore`.
|
|
48
|
+
|
|
49
|
+
## Read next
|
|
50
|
+
|
|
51
|
+
- https://nera.js.org/tutorials/add-search.md
|
|
52
|
+
- Plugin README: https://github.com/seebaermichi/nera-plugin-search
|
package/src/commands/update.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
|
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
|
+
}
|