@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 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
- - Auto-includes search script in assets
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 will create `assets/js/search.js` and inject the `search-index.json` into your `/public` output.
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 @nera-static/plugin-search run publish-template
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 plugin also automatically copies `assets/search-content.json` to `assets/js/search.js` on render. This file handles DOM bindings and result generation.
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
- - `assets/search-index.json`: contains all indexed page data
122
- - `assets/js/search.js`: minimal client-side logic for filtering and rendering
123
- - `app.searchIndexPath`: injected into `app` data for template access
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
- - `search.js` copy to assets folder
138
- - Config-driven field control and HTML stripping
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
 
@@ -1,37 +1,46 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import path from 'path'
4
- import fs from 'fs/promises'
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
- expectedPackageName: 'dummy', // for test-only override
17
+ force,
17
18
  })
18
19
 
19
- // Also publish search.js into assets/js/
20
- const publishSearchJS = async () => {
21
- try {
22
- const jsSource = path.join(sourceDir, 'search.js')
23
- const jsTarget = path.resolve(process.cwd(), 'assets/js/search.js')
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
- await fs.mkdir(path.dirname(jsTarget), { recursive: true })
26
- await fs.copyFile(jsSource, jsTarget)
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
- process.exit(1)
42
+ return false
32
43
  }
33
44
  }
34
45
 
35
- publishSearchJS().then(() => {
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/promises'
2
+ import fs from 'fs'
3
3
  import { getConfig } from '@nera-static/plugin-utils'
4
4
 
5
- const CONFIG_PATH = path.resolve(process.cwd(), 'config/search.yaml')
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
- export async function getAppData({ app, pagesData }) {
12
- const config = getConfig(CONFIG_PATH)
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 outputPath = path.join(app.folders.assets, filename)
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
- await fs.mkdir(path.dirname(outputPath), { recursive: true })
34
- await fs.writeFile(outputPath, JSON.stringify(index, null, 2), 'utf-8')
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
- "name": "@nera-static/plugin-search",
3
- "version": "1.0.2",
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.1.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
- }
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' && val.toLowerCase().includes(query)
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
- <li class="search__item">
26
- <a class="search__link" href="${item.href}">${title}</a>
27
- ${desc ? `<p class="search__description">${desc}</p>` : ''}
28
- </li>
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
  })