@nera-static/nera 1.5.1 → 1.7.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 +35 -4
- package/package.json +1 -1
- package/src/agent-files.js +83 -0
- package/src/commands/update.js +52 -3
- package/src/scaffold.js +12 -4
- package/template/AGENTS.md +118 -0
- package/template/CLAUDE.md +1 -0
package/README.md
CHANGED
|
@@ -16,18 +16,19 @@ npm run dev # or: nera dev
|
|
|
16
16
|
|
|
17
17
|
Already in the folder the site should live in? `nera new .` scaffolds into the
|
|
18
18
|
current folder instead. The folder must be empty apart from dotfiles such as
|
|
19
|
-
`.git`
|
|
20
|
-
|
|
19
|
+
`.git` and an `AGENTS.md`/`CLAUDE.md` an AI assistant may have written first
|
|
20
|
+
(an existing `.gitignore`, `AGENTS.md` or `CLAUDE.md` is kept), and the
|
|
21
|
+
package is named after the folder (`My Bakery` → `my-bakery`).
|
|
21
22
|
|
|
22
23
|
## Commands
|
|
23
24
|
|
|
24
25
|
| Command | What it does |
|
|
25
26
|
|---|---|
|
|
26
|
-
| `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. |
|
|
27
|
+
| `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). |
|
|
27
28
|
| `nera build` | Render `pages/` → `public/`. With `--check`, then run `nera check` on the result — one command for CI. |
|
|
28
29
|
| `nera dev` | Build, serve `public/`, and rebuild on change with live reload. Watches `pages/`, `config/`, `theme/` and a local theme (`theme: ./themes/<name>`). |
|
|
29
30
|
| `nera serve` | Serve the already-built `public/` folder. |
|
|
30
|
-
| `nera update` | Update the site's Nera packages. On a legacy cloned site, `nera update --migrate` converts it to the thin model. |
|
|
31
|
+
| `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. |
|
|
31
32
|
| `nera validate` | Check the site's sources (layouts, includes, YAML) before publishing; exits non-zero on any error. |
|
|
32
33
|
| `nera check` | Check the built `public/` for accessibility, privacy and legal-notice problems (see below). Build first. |
|
|
33
34
|
|
|
@@ -41,6 +42,36 @@ dependency and its scripts call the CLI:
|
|
|
41
42
|
}
|
|
42
43
|
```
|
|
43
44
|
|
|
45
|
+
## Instructions for AI assistants
|
|
46
|
+
|
|
47
|
+
Every scaffolded site carries an `AGENTS.md`, the file Codex, Cursor, Copilot
|
|
48
|
+
and other assistants read first, and a `CLAUDE.md` holding the single line
|
|
49
|
+
`@AGENTS.md`, so Claude Code loads the same text. `AGENTS.md` covers what
|
|
50
|
+
Nera is, the folder map, the commands, the common traps, and the check loop:
|
|
51
|
+
`nera validate`, then `nera build --check`, after every change.
|
|
52
|
+
|
|
53
|
+
It ends with a **Notes for this site** section for your own instructions —
|
|
54
|
+
brand voice, pages not to touch, where the site is deployed. It starts below
|
|
55
|
+
the marker line `<!-- nera:site-notes … -->`; keep that line in place. The
|
|
56
|
+
part above it is Nera's and may be replaced by a later Nera version; your
|
|
57
|
+
notes below it are yours.
|
|
58
|
+
|
|
59
|
+
`nera update` keeps both files current, with no extra flag. After the
|
|
60
|
+
packages are updated (and after `nera update --migrate`) it:
|
|
61
|
+
|
|
62
|
+
- creates `AGENTS.md` and `CLAUDE.md` when they are missing, so a site made
|
|
63
|
+
before 1.6.0 gets them too;
|
|
64
|
+
- replaces only the part of `AGENTS.md` above the first line starting with
|
|
65
|
+
`<!-- nera:site-notes`, taking the new text from the installed
|
|
66
|
+
`@nera-static/nera`'s template; the marker line and everything below it
|
|
67
|
+
stay byte for byte as they are;
|
|
68
|
+
- leaves an `AGENTS.md` without that line alone and prints a hint: copy the
|
|
69
|
+
marker line from the template directly above your own notes to opt in;
|
|
70
|
+
- never touches an existing `CLAUDE.md`.
|
|
71
|
+
|
|
72
|
+
A file is written only when its content changes, so running `nera update`
|
|
73
|
+
again changes nothing.
|
|
74
|
+
|
|
44
75
|
## Starting from a theme
|
|
45
76
|
|
|
46
77
|
```bash
|
package/package.json
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
// Start of the ownership line in AGENTS.md. Everything above the first line
|
|
5
|
+
// that starts with it is Nera's and is refreshed by `nera update`; that line
|
|
6
|
+
// and everything below it belong to the site owner and are kept byte for byte.
|
|
7
|
+
export const SITE_NOTES_MARKER = '<!-- nera:site-notes'
|
|
8
|
+
|
|
9
|
+
// Offset of the first line that starts with SITE_NOTES_MARKER, or -1.
|
|
10
|
+
// Only a match at a line start counts, so the marker quoted inside a line
|
|
11
|
+
// never moves the split.
|
|
12
|
+
function markerOffset(text) {
|
|
13
|
+
if (text.startsWith(SITE_NOTES_MARKER)) return 0
|
|
14
|
+
const at = text.indexOf(`\n${SITE_NOTES_MARKER}`)
|
|
15
|
+
return at === -1 ? -1 : at + 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function readIfExists(file) {
|
|
19
|
+
try {
|
|
20
|
+
return await fs.readFile(file, 'utf-8')
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err.code === 'ENOENT') return null
|
|
23
|
+
throw err
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The site's AGENTS.md with its Nera part replaced by the template's. The
|
|
28
|
+
// template's line endings follow the site's, so a CRLF checkout stays CRLF.
|
|
29
|
+
// null when the site file has no marker: then none of it is Nera's to touch.
|
|
30
|
+
function spliceAgents(siteText, templateHead) {
|
|
31
|
+
const at = markerOffset(siteText)
|
|
32
|
+
if (at === -1) return null
|
|
33
|
+
const eol = siteText.slice(0, at).includes('\r\n') ? '\r\n' : '\n'
|
|
34
|
+
return templateHead.replace(/\r?\n/g, eol) + siteText.slice(at)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Bring a site's AGENTS.md and CLAUDE.md in line with the scaffold template
|
|
38
|
+
// in templateDir: create what is missing, refresh the Nera part of an
|
|
39
|
+
// AGENTS.md that has the marker, leave a marker-less AGENTS.md and any
|
|
40
|
+
// existing CLAUDE.md alone. Writes only on change, so a repeated run is a
|
|
41
|
+
// no-op. Returns one { file, action } per file, action being `created`,
|
|
42
|
+
// `updated`, `unchanged` or `skipped`; printing is the caller's job.
|
|
43
|
+
export async function syncAgentFiles({ cwd = process.cwd(), templateDir }) {
|
|
44
|
+
const template = await fs.readFile(path.join(templateDir, 'AGENTS.md'), 'utf-8')
|
|
45
|
+
const templateAt = markerOffset(template)
|
|
46
|
+
if (templateAt === -1) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`The template's AGENTS.md (${templateDir}) has no ` +
|
|
49
|
+
`\`${SITE_NOTES_MARKER}\` line. This is a packaging bug in ` +
|
|
50
|
+
'@nera-static/nera, not a problem with your site.'
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const agentsPath = path.join(cwd, 'AGENTS.md')
|
|
55
|
+
const agents = await readIfExists(agentsPath)
|
|
56
|
+
let agentsAction
|
|
57
|
+
if (agents === null) {
|
|
58
|
+
await fs.writeFile(agentsPath, template)
|
|
59
|
+
agentsAction = 'created'
|
|
60
|
+
} else {
|
|
61
|
+
const next = spliceAgents(agents, template.slice(0, templateAt))
|
|
62
|
+
if (next === null) {
|
|
63
|
+
agentsAction = 'skipped'
|
|
64
|
+
} else if (next === agents) {
|
|
65
|
+
agentsAction = 'unchanged'
|
|
66
|
+
} else {
|
|
67
|
+
await fs.writeFile(agentsPath, next)
|
|
68
|
+
agentsAction = 'updated'
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const claudePath = path.join(cwd, 'CLAUDE.md')
|
|
73
|
+
let claudeAction = 'unchanged'
|
|
74
|
+
if ((await readIfExists(claudePath)) === null) {
|
|
75
|
+
await fs.copyFile(path.join(templateDir, 'CLAUDE.md'), claudePath)
|
|
76
|
+
claudeAction = 'created'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
{ file: 'AGENTS.md', action: agentsAction },
|
|
81
|
+
{ file: 'CLAUDE.md', action: claudeAction },
|
|
82
|
+
]
|
|
83
|
+
}
|
package/src/commands/update.js
CHANGED
|
@@ -2,6 +2,8 @@ import { execFileSync } from 'child_process'
|
|
|
2
2
|
import fs from 'fs/promises'
|
|
3
3
|
import fssync from 'fs'
|
|
4
4
|
import path from 'path'
|
|
5
|
+
import { syncAgentFiles, SITE_NOTES_MARKER } from '../agent-files.js'
|
|
6
|
+
import { templateDir as packageTemplateDir } from '../scaffold.js'
|
|
5
7
|
|
|
6
8
|
const defaultRunNpm = (npmArgs, cwd) =>
|
|
7
9
|
execFileSync('npm', npmArgs, { cwd, stdio: 'inherit' })
|
|
@@ -45,7 +47,46 @@ function printLegacyGuidance() {
|
|
|
45
47
|
console.log('\n👉 Run `nera update --migrate` to do it.')
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
// The scaffold template whose AGENTS.md / CLAUDE.md the site is synced to:
|
|
51
|
+
// the one in the site's installed @nera-static/nera (read after npm has
|
|
52
|
+
// updated it), so the instructions match the Nera version the site runs even
|
|
53
|
+
// when a global or older `nera` performs the update. Falls back to the
|
|
54
|
+
// running package's template when nothing is installed there or the installed
|
|
55
|
+
// version predates 1.6.0, which shipped no AGENTS.md.
|
|
56
|
+
export function agentTemplateDir(cwd, fallbackDir) {
|
|
57
|
+
const installed = path.join(cwd, 'node_modules', '@nera-static', 'nera', 'template')
|
|
58
|
+
return fssync.existsSync(path.join(installed, 'AGENTS.md')) ? installed : fallbackDir
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const AGENT_FILE_MESSAGES = {
|
|
62
|
+
created: (file) => ` ✓ Created ${file}`,
|
|
63
|
+
updated: (file) => ` ✓ Refreshed the Nera part of ${file}`,
|
|
64
|
+
// Neutral on purpose: an existing CLAUDE.md is kept without comparing it.
|
|
65
|
+
unchanged: (file) => ` ✓ ${file} unchanged`,
|
|
66
|
+
skipped: (file) => ` ⚠️ Left ${file} alone: it has no \`${SITE_NOTES_MARKER}\` line`,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function syncAndReportAgentFiles(cwd, fallbackDir) {
|
|
70
|
+
const dir = agentTemplateDir(cwd, fallbackDir)
|
|
71
|
+
const results = await syncAgentFiles({ cwd, templateDir: dir })
|
|
72
|
+
for (const { file, action } of results) {
|
|
73
|
+
console.log(AGENT_FILE_MESSAGES[action](file))
|
|
74
|
+
}
|
|
75
|
+
if (results.some(({ action }) => action === 'skipped')) {
|
|
76
|
+
// Relative inside the site, absolute for a global or npx-cached CLI,
|
|
77
|
+
// where a relative path would be a long run of `../`.
|
|
78
|
+
const template = path.join(dir, 'AGENTS.md')
|
|
79
|
+
const relative = path.relative(cwd, template)
|
|
80
|
+
const source = relative.startsWith('..') ? template : relative
|
|
81
|
+
console.log(
|
|
82
|
+
` To let \`nera update\` keep Nera's instructions current, copy the \`${SITE_NOTES_MARKER}\`` +
|
|
83
|
+
` line from ${source} directly above your own notes. Everything above that` +
|
|
84
|
+
' line is then Nera\'s and is replaced on update; the line and everything below it stay yours.'
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function migrateLegacy(cwd, pkg, runNpm, templateDir) {
|
|
49
90
|
console.log('🔧 Migrating this cloned site to the thin @nera-static/nera model...')
|
|
50
91
|
|
|
51
92
|
pkg.dependencies = {
|
|
@@ -89,6 +130,7 @@ async function migrateLegacy(cwd, pkg, runNpm) {
|
|
|
89
130
|
|
|
90
131
|
console.log('📦 Installing...')
|
|
91
132
|
runNpm(['install'], cwd)
|
|
133
|
+
await syncAndReportAgentFiles(cwd, templateDir)
|
|
92
134
|
|
|
93
135
|
console.log('✅ Migrated. Run `nera dev` to start.')
|
|
94
136
|
if (movedPlugins) {
|
|
@@ -100,7 +142,13 @@ async function migrateLegacy(cwd, pkg, runNpm) {
|
|
|
100
142
|
}
|
|
101
143
|
|
|
102
144
|
export async function runUpdate(args = [], options = {}) {
|
|
103
|
-
|
|
145
|
+
// templateDir: the fallback scaffold template for AGENTS.md / CLAUDE.md
|
|
146
|
+
// (see agentTemplateDir); injectable so tests need no installed package.
|
|
147
|
+
const {
|
|
148
|
+
cwd = process.cwd(),
|
|
149
|
+
runNpm = defaultRunNpm,
|
|
150
|
+
templateDir = packageTemplateDir(),
|
|
151
|
+
} = options
|
|
104
152
|
|
|
105
153
|
const pkg = JSON.parse(
|
|
106
154
|
await fs.readFile(path.join(cwd, 'package.json'), 'utf-8')
|
|
@@ -109,7 +157,7 @@ export async function runUpdate(args = [], options = {}) {
|
|
|
109
157
|
|
|
110
158
|
if (kind === 'legacy') {
|
|
111
159
|
if (args.includes('--migrate') || args.includes('--yes')) {
|
|
112
|
-
await migrateLegacy(cwd, pkg, runNpm)
|
|
160
|
+
await migrateLegacy(cwd, pkg, runNpm, templateDir)
|
|
113
161
|
} else {
|
|
114
162
|
printLegacyGuidance()
|
|
115
163
|
}
|
|
@@ -126,5 +174,6 @@ export async function runUpdate(args = [], options = {}) {
|
|
|
126
174
|
const targets = neraDeps(pkg)
|
|
127
175
|
console.log(`🔄 Updating Nera packages: ${targets.join(', ')}`)
|
|
128
176
|
runNpm(['update', ...targets], cwd)
|
|
177
|
+
await syncAndReportAgentFiles(cwd, templateDir)
|
|
129
178
|
console.log('✅ Up to date.')
|
|
130
179
|
}
|
package/src/scaffold.js
CHANGED
|
@@ -62,11 +62,18 @@ export function projectNameFromDir(dir) {
|
|
|
62
62
|
: validateProjectName(name)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Files an AI assistant may have written before running `nera new .`. They
|
|
66
|
+
// do not make the folder count as non-empty, and the user's copy is kept.
|
|
67
|
+
// Exact names only, matched case-sensitively.
|
|
68
|
+
const AGENT_FILES = new Set(['AGENTS.md', 'CLAUDE.md'])
|
|
69
|
+
|
|
65
70
|
// In-place scaffolding only goes ahead in an empty folder. Dotfiles such as
|
|
66
71
|
// `.git` or an editor folder do not count: `git init` first is the usual way
|
|
67
|
-
// to start a project.
|
|
72
|
+
// to start a project. Neither do AGENT_FILES.
|
|
68
73
|
async function assertEmptyDir(dir) {
|
|
69
|
-
const entries = (await fs.readdir(dir)).filter(
|
|
74
|
+
const entries = (await fs.readdir(dir)).filter(
|
|
75
|
+
(n) => !n.startsWith('.') && !AGENT_FILES.has(n)
|
|
76
|
+
)
|
|
70
77
|
if (entries.length > 0) {
|
|
71
78
|
throw new Error(
|
|
72
79
|
`The current folder is not empty (${entries.slice(0, 3).join(', ')}` +
|
|
@@ -136,8 +143,9 @@ async function copyDir(src, dest, { skipStarters = false } = {}) {
|
|
|
136
143
|
} else if (skipStarters && (await isStarterTemplate(from))) {
|
|
137
144
|
continue
|
|
138
145
|
} else if (fssync.existsSync(to)) {
|
|
139
|
-
// Only in place, and then only a dotfile such as .gitignore
|
|
140
|
-
// there already (assertEmptyDir):
|
|
146
|
+
// Only in place, and then only a dotfile such as .gitignore, or
|
|
147
|
+
// AGENTS.md / CLAUDE.md, can be there already (assertEmptyDir):
|
|
148
|
+
// the user's copy wins.
|
|
141
149
|
console.log(` • Kept your existing ${path.basename(to)}`)
|
|
142
150
|
} else {
|
|
143
151
|
await fs.copyFile(from, to)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Instructions for AI coding agents working on this site. People are welcome to
|
|
4
|
+
read them too.
|
|
5
|
+
|
|
6
|
+
## What this is
|
|
7
|
+
|
|
8
|
+
This site is built with [Nera](https://nera.js.org), a static site generator:
|
|
9
|
+
Markdown pages plus Pug templates in, plain HTML out. Nera is not Eleventy,
|
|
10
|
+
Hugo, Astro or Jekyll, so do not apply their conventions here. The docs are
|
|
11
|
+
at https://nera.js.org.
|
|
12
|
+
|
|
13
|
+
## Folder map
|
|
14
|
+
|
|
15
|
+
- `pages/` — the content: one Markdown file with YAML frontmatter per page
|
|
16
|
+
- `theme/views/` — the Pug templates (`layouts/`, `pages/`, and `vendor/`
|
|
17
|
+
for copied plugin templates)
|
|
18
|
+
- `theme/assets/` — CSS, JavaScript, images and fonts, copied into
|
|
19
|
+
`public/` on every build
|
|
20
|
+
- `config/` — YAML settings: `app.yaml` for the site, `<plugin>.yaml` for
|
|
21
|
+
each plugin
|
|
22
|
+
- `src/plugins/<name>/index.js` — local plugins for this site only (optional;
|
|
23
|
+
another folder can be set with `folders.plugins` in `config/app.yaml`)
|
|
24
|
+
- `public/` — the generated site. **Never edit it**: every build deletes it
|
|
25
|
+
and writes it again
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
- `npm run dev` — build, serve on http://localhost:3000 and rebuild on
|
|
30
|
+
every change
|
|
31
|
+
- `npm run build` — render `pages/` into `public/`
|
|
32
|
+
- `npx nera validate` — check pages, layouts, includes and YAML before a build
|
|
33
|
+
- `npx nera build --check` — build, then check the built `public/` for
|
|
34
|
+
accessibility, privacy and legal issues (`npx nera check` checks without
|
|
35
|
+
building)
|
|
36
|
+
|
|
37
|
+
## After every change
|
|
38
|
+
|
|
39
|
+
1. Run `npx nera validate` and fix every error it reports.
|
|
40
|
+
2. Run `npx nera build --check` and fix what it reports.
|
|
41
|
+
|
|
42
|
+
`nera validate` names the file and line of a broken layout or include and
|
|
43
|
+
finds pages the build would skip silently. Run it first.
|
|
44
|
+
|
|
45
|
+
## How Nera works (and where it trips agents up)
|
|
46
|
+
|
|
47
|
+
- **Pages are Markdown.** Write prose in Markdown, not HTML. Page structure
|
|
48
|
+
and markup shared between pages belong in `theme/views/`.
|
|
49
|
+
- **URLs end in `.html`.** `pages/a/b.md` becomes `public/a/b.html` and is
|
|
50
|
+
linked as `/a/b.html`; `pages/index.md` is `/index.html`. There are no
|
|
51
|
+
pretty URLs such as `/a/b/`.
|
|
52
|
+
- **A page needs `layout`.** The `layout` key in a page's frontmatter names a
|
|
53
|
+
file under `theme/views/`; the pages this site was created with use
|
|
54
|
+
`pages/default.pug`. A page without `layout` is not rendered and the build
|
|
55
|
+
says nothing; `nera validate` warns (`layout-missing`). Renaming or deleting
|
|
56
|
+
a layout breaks every page that uses it.
|
|
57
|
+
- **The page body.** The rendered Markdown reaches the template as `content`.
|
|
58
|
+
Without a theme, `theme/views/pages/default.pug` prints it with
|
|
59
|
+
`main !{ content }` inside `block content`; with a theme, the theme's layout
|
|
60
|
+
may already hold the `<main>`. A page has exactly one `<main>`: check the
|
|
61
|
+
layout and the page template before adding one.
|
|
62
|
+
- **`public/` is wiped on every build.** Put source files in `pages/`,
|
|
63
|
+
`theme/` or `config/`; anything written into `public/` by hand is lost.
|
|
64
|
+
- **Site files override theme files.** If `config/app.yaml` sets `theme:`,
|
|
65
|
+
the layouts come from that theme (a package or a folder), and a file in
|
|
66
|
+
`theme/views/` or `theme/assets/` replaces the theme's file at the same
|
|
67
|
+
path.
|
|
68
|
+
- **Translations.** Templates call `t('key')`, which reads
|
|
69
|
+
`translations.<lang>.key` from `config/app.yaml` (the page's `lang`, else
|
|
70
|
+
the site's).
|
|
71
|
+
|
|
72
|
+
## Plugins: look for one before writing a feature
|
|
73
|
+
|
|
74
|
+
Features come from npm packages named `@nera-static/plugin-*`. Install one with
|
|
75
|
+
`npm install`, and Nera loads it on the next build; there is nothing to
|
|
76
|
+
register. Most read optional settings from `config/<name>.yaml` (the plugin's
|
|
77
|
+
README shows the keys). Plugin catalog:
|
|
78
|
+
https://github.com/seebaermichi/nera/blob/main/PLUGINS.md
|
|
79
|
+
|
|
80
|
+
Check the catalog before hand-writing navigation, a contact form, tags, search,
|
|
81
|
+
pagination or image handling. Two that sites often need:
|
|
82
|
+
|
|
83
|
+
- `@nera-static/plugin-contact-form` — a contact form that opens the visitor's
|
|
84
|
+
mail client (`mailto:`). No backend and no form service; configured in
|
|
85
|
+
`config/contact-form.yaml`.
|
|
86
|
+
- `@nera-static/plugin-navigation` — menus from `config/navigation.yaml`.
|
|
87
|
+
|
|
88
|
+
Plugin templates are not used automatically. Copy them into the site first,
|
|
89
|
+
then include them by hand in your own template:
|
|
90
|
+
|
|
91
|
+
- `npx nera-contact-form` copies the form to
|
|
92
|
+
`theme/views/vendor/plugin-contact-form/` (and its script to
|
|
93
|
+
`theme/assets/js/`); include it with
|
|
94
|
+
`include /vendor/plugin-contact-form/contact-form`
|
|
95
|
+
- `npx nera-navigation` copies the menus to
|
|
96
|
+
`theme/views/vendor/plugin-navigation/`
|
|
97
|
+
|
|
98
|
+
The copy is skipped when the target folder already exists, so later plugin
|
|
99
|
+
updates do not reach it; `--force` overwrites the copy.
|
|
100
|
+
|
|
101
|
+
## Preview
|
|
102
|
+
|
|
103
|
+
`npm run dev` keeps running until it is stopped. If you start it in the
|
|
104
|
+
background, stop that process yourself when you are done, by its process ID.
|
|
105
|
+
Never kill processes by name (`pkill -f nera`), which stops other people's
|
|
106
|
+
servers too. If port 3000 is taken, the server picks another one and prints
|
|
107
|
+
the URL.
|
|
108
|
+
|
|
109
|
+
## Getting online
|
|
110
|
+
|
|
111
|
+
`npm run build` writes the finished site into `public/`. That folder is what
|
|
112
|
+
gets deployed: upload its contents to any static web host.
|
|
113
|
+
|
|
114
|
+
<!-- nera:site-notes — everything below this line belongs to the site owner; `nera update` keeps it as it is -->
|
|
115
|
+
## Notes for this site
|
|
116
|
+
|
|
117
|
+
Add what an agent should know about this particular site: its audience, tone,
|
|
118
|
+
languages, pages that must not change, the hosting.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|