@nera-static/plugin-search 1.0.2 → 1.1.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/CHANGELOG.md +48 -0
- package/README.md +44 -9
- package/bin/publish-template.js +22 -13
- package/index.js +12 -7
- package/package.json +56 -55
- package/views/search.js +34 -9
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,54 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.0] - 2026-07-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `--force` flag on the `nera-search` publish command, to deliberately
|
|
13
|
+
overwrite already-published templates and `assets/js/search.js`
|
|
14
|
+
- `assets/js/search.js` is now skipped when it already exists, instead of
|
|
15
|
+
being overwritten silently. Re-running the publish command no longer
|
|
16
|
+
discards local edits to the search script
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **`getAppData` is now synchronous.** Nera only began awaiting plugin hooks
|
|
21
|
+
in generator 4.3.0, and that fix does not reach already-cloned sites. On
|
|
22
|
+
any earlier generator the previous `async` signature replaced the entire
|
|
23
|
+
`app` object with a Promise, silently discarding every `app.yaml` value —
|
|
24
|
+
`lang`, `translations`, `folders` — for this plugin and every plugin
|
|
25
|
+
ordered after it. Builds still exited 0, so the damage was invisible:
|
|
26
|
+
pages rendered with untranslated raw keys and no `lang` attribute
|
|
27
|
+
- the search index path no longer throws `Path must be a string` when
|
|
28
|
+
`app.yaml` defines a `folders` block without an `assets` key. It now falls
|
|
29
|
+
back to `./assets`, the generator's own default for that key
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- configuration is read inside `getAppData` rather than at module load, so
|
|
34
|
+
edits to `config/search.yaml` take effect without a restart
|
|
35
|
+
- `@nera-static/plugin-utils` range raised to `^1.2.0`, which is where the
|
|
36
|
+
`force` option lands. The previous `^1.1.0` permitted but did not
|
|
37
|
+
guarantee it, so `--force` could be accepted and then silently ignored
|
|
38
|
+
|
|
39
|
+
### Documentation
|
|
40
|
+
|
|
41
|
+
- corrected three false README claims: the search script is **not**
|
|
42
|
+
auto-included (the publish command copies it), the index path is **not**
|
|
43
|
+
fixed at `assets/search-index.json` (it follows `folders.assets` and
|
|
44
|
+
`output_filename`), and `app.searchIndexPath` is available to your own
|
|
45
|
+
templates but is not used by the shipped `search.pug`
|
|
46
|
+
- documented that the index is written into the **source** assets folder,
|
|
47
|
+
with a `.gitignore` note. Writing to `public/` is not possible — Nera
|
|
48
|
+
deletes and repopulates it after plugins run
|
|
49
|
+
- fixed an invalid `npx` invocation; the command is `npx nera-search`
|
|
50
|
+
|
|
51
|
+
## [1.0.3] - 2025-07-25
|
|
52
|
+
|
|
53
|
+
### Change
|
|
54
|
+
- Enhance search.js to show parts of the results text
|
|
55
|
+
|
|
8
56
|
## [1.0.2] - 2025-07-25
|
|
9
57
|
|
|
10
58
|
### Fix
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ A plugin for the [Nera](https://github.com/seebaermichi/nera) static site genera
|
|
|
8
8
|
- Fully client-side – no backend or JavaScript framework needed
|
|
9
9
|
- Configurable search fields (e.g., `title`, `excerpt`, `description`)
|
|
10
10
|
- Option to strip HTML from content before indexing
|
|
11
|
-
-
|
|
11
|
+
- Ships a client-side search script, published on request
|
|
12
12
|
- Includes ready-to-use Pug template with BEM-compatible markup
|
|
13
13
|
- Supports multiple search inputs per page
|
|
14
14
|
- Full compatibility with Nera v4.2.0+
|
|
@@ -72,7 +72,10 @@ layout: pages/default.pug
|
|
|
72
72
|
script(src="/js/search.js")
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
The plugin
|
|
75
|
+
The plugin writes the index into your **source** assets folder, and Nera copies
|
|
76
|
+
that folder into `public/` at the end of the render — so the built site serves
|
|
77
|
+
it from `/search-index.json`. See [Generated Output](#-generated-output) for
|
|
78
|
+
what this means for version control.
|
|
76
79
|
|
|
77
80
|
You can include multiple search inputs on a page by using different `[data-results]` selectors.
|
|
78
81
|
|
|
@@ -90,7 +93,7 @@ You can include multiple search inputs on a page by using different `[data-resul
|
|
|
90
93
|
To customize the default view:
|
|
91
94
|
|
|
92
95
|
```bash
|
|
93
|
-
npx
|
|
96
|
+
npx nera-search
|
|
94
97
|
```
|
|
95
98
|
|
|
96
99
|
This will copy:
|
|
@@ -101,7 +104,15 @@ views/vendor/plugin-search/search.pug
|
|
|
101
104
|
|
|
102
105
|
to your local project. You can now edit or extend the search markup freely.
|
|
103
106
|
|
|
104
|
-
The
|
|
107
|
+
The command also copies the `search.js` file to `assets/js/`. It handles DOM bindings and result generation.
|
|
108
|
+
|
|
109
|
+
Both steps **skip if the destination already exists**, so re-running the command
|
|
110
|
+
never discards your edits. To deliberately overwrite them with the packaged
|
|
111
|
+
versions:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx nera-search --force
|
|
115
|
+
```
|
|
105
116
|
|
|
106
117
|
## 🎨 Styling
|
|
107
118
|
|
|
@@ -118,9 +129,32 @@ Customize freely via your own stylesheets.
|
|
|
118
129
|
|
|
119
130
|
## 📊 Generated Output
|
|
120
131
|
|
|
121
|
-
-
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
- `<assets folder>/<output_filename>`: contains all indexed page data. The
|
|
133
|
+
location follows `folders.assets` in your `config/app.yaml` (default
|
|
134
|
+
`./assets`) and `output_filename` in `config/search.yaml` (default
|
|
135
|
+
`search-index.json`) — it is **not** fixed at `assets/search-index.json`.
|
|
136
|
+
- `assets/js/search.js`: minimal client-side logic for filtering and rendering,
|
|
137
|
+
copied by the publish command rather than by the render.
|
|
138
|
+
- `app.searchIndexPath`: the index's public URL, added to `app` data. Available
|
|
139
|
+
to your own templates; the shipped `search.pug` does not use it, since
|
|
140
|
+
`search.js` resolves the path itself.
|
|
141
|
+
|
|
142
|
+
### ⚠️ The index is written into your source tree
|
|
143
|
+
|
|
144
|
+
The index has to be written into the **source** assets folder, not into
|
|
145
|
+
`public/`. Nera deletes `public/` and then copies assets into it *after*
|
|
146
|
+
plugins run, so anything a plugin writes directly to `public/` is discarded.
|
|
147
|
+
|
|
148
|
+
That means each render creates or updates a generated file inside a directory
|
|
149
|
+
you probably track in git. To keep it out of version control while still
|
|
150
|
+
serving it, add it to `.gitignore`:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
assets/search-index.json
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Do **not** add it to `.neraignore` — that would stop Nera copying it into
|
|
157
|
+
`public/`, and the search would find no index at runtime.
|
|
124
158
|
|
|
125
159
|
## 🧪 Development
|
|
126
160
|
|
|
@@ -134,8 +168,9 @@ Tests use [Vitest](https://vitest.dev) and cover:
|
|
|
134
168
|
|
|
135
169
|
- Index creation from `pagesData`
|
|
136
170
|
- Correct JSON structure and file writing
|
|
137
|
-
- `
|
|
138
|
-
-
|
|
171
|
+
- That `getAppData` stays synchronous and preserves existing `app` values
|
|
172
|
+
- Fallback to `./assets` when `folders.assets` is absent
|
|
173
|
+
- Config-driven field control, custom output filename, and HTML stripping
|
|
139
174
|
|
|
140
175
|
## 🧑💻 Author
|
|
141
176
|
|
package/bin/publish-template.js
CHANGED
|
@@ -1,37 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import path from 'path'
|
|
4
|
-
import fs from 'fs
|
|
4
|
+
import fs from 'fs'
|
|
5
5
|
import { fileURLToPath } from 'url'
|
|
6
6
|
import { publishAllTemplates } from '@nera-static/plugin-utils'
|
|
7
7
|
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
9
|
const pluginName = 'plugin-search'
|
|
10
10
|
const sourceDir = path.resolve(__dirname, '../views/')
|
|
11
|
+
const force = process.argv.includes('--force')
|
|
11
12
|
|
|
12
13
|
// Publish all pug templates to views/vendor/plugin-search/
|
|
13
14
|
const result = publishAllTemplates({
|
|
14
15
|
pluginName,
|
|
15
16
|
sourceDir,
|
|
16
|
-
|
|
17
|
+
force,
|
|
17
18
|
})
|
|
18
19
|
|
|
19
|
-
// Also publish search.js into assets/js
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
// Also publish search.js into assets/js/. Same skip-if-exists rule as the
|
|
21
|
+
// templates above — re-running this command must not discard user edits.
|
|
22
|
+
const publishSearchJS = () => {
|
|
23
|
+
const jsSource = path.join(sourceDir, 'search.js')
|
|
24
|
+
const jsTarget = path.resolve(process.cwd(), 'assets/js/search.js')
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
if (fs.existsSync(jsTarget) && !force) {
|
|
27
|
+
console.log(
|
|
28
|
+
'⚠️ assets/js/search.js already exists — skipping.\n' +
|
|
29
|
+
' Re-run with --force to overwrite (this will discard your edits).'
|
|
30
|
+
)
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
fs.mkdirSync(path.dirname(jsTarget), { recursive: true })
|
|
36
|
+
fs.copyFileSync(jsSource, jsTarget)
|
|
27
37
|
|
|
28
38
|
console.log('✓ search.js copied to assets/js/search.js')
|
|
39
|
+
return true
|
|
29
40
|
} catch (err) {
|
|
30
41
|
console.error('✗ Failed to copy search.js to assets/js/', err)
|
|
31
|
-
|
|
42
|
+
return false
|
|
32
43
|
}
|
|
33
44
|
}
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
process.exit(result ? 0 : 1)
|
|
37
|
-
})
|
|
46
|
+
process.exit(result && publishSearchJS() ? 0 : 1)
|
package/index.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
|
-
import fs from 'fs
|
|
2
|
+
import fs from 'fs'
|
|
3
3
|
import { getConfig } from '@nera-static/plugin-utils'
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const DEFAULT_ASSETS_FOLDER = './assets'
|
|
6
6
|
|
|
7
7
|
function stripHtml(html) {
|
|
8
8
|
return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim()
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// Must stay synchronous. The generator only started awaiting plugin hooks in
|
|
12
|
+
// 4.3.0, and that fix never reaches already-cloned sites — an async hook there
|
|
13
|
+
// replaces `app` with a Promise, silently wiping every `app.*` value for each
|
|
14
|
+
// plugin that runs later.
|
|
15
|
+
export function getAppData({ app, pagesData }) {
|
|
16
|
+
const config = getConfig(path.resolve(process.cwd(), 'config/search.yaml'))
|
|
13
17
|
const filename = config.output_filename || 'search-index.json'
|
|
14
|
-
const
|
|
18
|
+
const assetsFolder = app?.folders?.assets || DEFAULT_ASSETS_FOLDER
|
|
19
|
+
const outputPath = path.join(assetsFolder, filename)
|
|
15
20
|
|
|
16
21
|
const fields = config.fields || ['title', 'excerpt', 'content', 'href']
|
|
17
22
|
const strip = config.strip_html ?? true
|
|
@@ -30,8 +35,8 @@ export async function getAppData({ app, pagesData }) {
|
|
|
30
35
|
return item
|
|
31
36
|
})
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
|
|
39
|
+
fs.writeFileSync(outputPath, JSON.stringify(index, null, 2), 'utf-8')
|
|
35
40
|
|
|
36
41
|
return {
|
|
37
42
|
...app,
|
package/package.json
CHANGED
|
@@ -1,57 +1,58 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
2
|
+
"name": "@nera-static/plugin-search",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A plugin for the Nera static site generator that builds a JSON search index and provides a client-side search script.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"publish-template": "node ./bin/publish-template.js",
|
|
8
|
+
"lint": "eslint .",
|
|
9
|
+
"test": "vitest"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./index.js"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"nera-search": "./bin/publish-template.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin",
|
|
20
|
+
"config",
|
|
21
|
+
"views",
|
|
22
|
+
"index.js",
|
|
23
|
+
"CHANGELOG.md"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"nera",
|
|
27
|
+
"plugin",
|
|
28
|
+
"search",
|
|
29
|
+
"static-site",
|
|
30
|
+
"client-side-search",
|
|
31
|
+
"search-index",
|
|
32
|
+
"lunr",
|
|
33
|
+
"json"
|
|
34
|
+
],
|
|
35
|
+
"author": "Michael Becker",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/seebaermichi/nera-plugin-search.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/seebaermichi/nera-plugin-search/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/seebaermichi/nera-plugin-search#readme",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@nera-static/plugin-utils": "^1.2.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"eslint": "^9.31.0",
|
|
53
|
+
"husky": "^9.1.7",
|
|
54
|
+
"pug": "^3.0.3",
|
|
55
|
+
"vitest": "^3.2.4"
|
|
56
|
+
},
|
|
57
|
+
"sideEffects": false
|
|
57
58
|
}
|
package/views/search.js
CHANGED
|
@@ -12,21 +12,46 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
12
12
|
input.addEventListener('input', () => {
|
|
13
13
|
const query = input.value.toLowerCase()
|
|
14
14
|
|
|
15
|
+
if (!query) {
|
|
16
|
+
resultList.innerHTML = ''
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
const results = data.filter((item) => {
|
|
16
|
-
return Object.values(item).some(val =>
|
|
17
|
-
typeof val === 'string'
|
|
18
|
-
|
|
21
|
+
return Object.values(item).some(val => {
|
|
22
|
+
if (typeof val === 'string') {
|
|
23
|
+
const lowerVal = val.toLowerCase()
|
|
24
|
+
const matchIndex = lowerVal.indexOf(query)
|
|
25
|
+
if (matchIndex !== -1) {
|
|
26
|
+
const words = val.split(/\s+/)
|
|
27
|
+
const matchWordIndex = words.findIndex(word => word.toLowerCase().includes(query))
|
|
28
|
+
|
|
29
|
+
const snippetStart = Math.max(0, matchWordIndex - 5)
|
|
30
|
+
const snippetEnd = Math.min(words.length, matchWordIndex + 6)
|
|
31
|
+
const snippet = words.slice(snippetStart, snippetEnd).join(' ')
|
|
32
|
+
|
|
33
|
+
const highlighted = snippet.replace(
|
|
34
|
+
new RegExp(`(${query})`, 'ig'),
|
|
35
|
+
'<span class="bg-yellow-100">$1</span>'
|
|
36
|
+
)
|
|
37
|
+
item.resultText = `...${highlighted}...`
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return false
|
|
43
|
+
})
|
|
19
44
|
})
|
|
20
45
|
|
|
21
46
|
resultList.innerHTML = results.map((item) => {
|
|
22
47
|
const title = item.title || item.href || 'Untitled'
|
|
23
|
-
const desc = item.excerpt || item.description ||
|
|
48
|
+
const desc = item.excerpt || item.description || item.resultText
|
|
24
49
|
return `
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
50
|
+
<li class="list-none">
|
|
51
|
+
<a class="" href="${item.href}">${title}</a>
|
|
52
|
+
${desc ? `<p class="">${desc}</p>` : ''}
|
|
53
|
+
</li>
|
|
54
|
+
`
|
|
30
55
|
}).join('')
|
|
31
56
|
})
|
|
32
57
|
})
|