jekyll-client-search 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -0
- data/README.developer.md +272 -10
- data/README.md +224 -4
- data/assets/client-search-base.js +53 -1
- data/assets/client-search-dropdown.js +434 -0
- data/assets/client-search-related.js +10 -0
- data/lib/jekyll/client_search/configuration.rb +40 -13
- data/lib/jekyll/client_search/configuration_accessors.rb +12 -4
- data/lib/jekyll/client_search/document_builder.rb +23 -5
- data/lib/jekyll/client_search/dropdown_configuration.rb +92 -0
- data/lib/jekyll/client_search/dropdown_tag.rb +115 -0
- data/lib/jekyll/client_search/generator.rb +6 -2
- data/lib/jekyll/client_search/related_configuration.rb +1 -1
- data/lib/jekyll/client_search/related_tag.rb +30 -8
- data/lib/jekyll/client_search/runtime_config_page.rb +5 -1
- data/lib/jekyll/client_search/search_tag.rb +23 -6
- data/lib/jekyll/client_search/version.rb +1 -1
- data/lib/jekyll/client_search.rb +2 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed1c7a635cc419d9fb130411ced8e64a473c016e8188e36ddd98ea6922a098f7
|
|
4
|
+
data.tar.gz: f320844ded59274df991b1aa3d56fb22560e818105c5a0e682ea6e6a0c67df88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a12fb0052f9a727baf957735cd23fc9759f46fe7b54977acd16ed63edb14b0d63e2594d3c2bee936c9317541f8810a654acf7a5369746c9d11c39699457cf351
|
|
7
|
+
data.tar.gz: 46986b4c1a0107821f6932d8538c1aff0fcc386ddb9e9f787a4890da5f8af3ccd30a8c0b6b9257d928d3b52aa6fe625806136812cfbf781763a0f3a391cf9aa6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.3.0 — 2026-09-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `{% search_dropdown %}` Liquid tag — compact live-search dropdown for navbars and headers with keyboard navigation (Arrow/Enter/Escape), lazy index loading, icon support, and framework-agnostic semantic HTML (no Bulma/Bootstrap/Tailwind classes)
|
|
9
|
+
- `DropdownConfiguration` class with `enabled`, `max_items`, `min_chars`, `debounce_ms`, `redirect_url` options
|
|
10
|
+
- `assets/client-search-dropdown.js` runtime (434 lines) — two-stage search (AND first, fuzzy OR fallback), shared index cache with the base runtime, multi-instance support via `[data-client-search-dropdown]` attributes, icon rendering from `icon_field`
|
|
11
|
+
- `max:N` parameter on `{% related_articles %}` tag (e.g. `{% related_articles max:3 %}`, `{% related_articles sort:date max:10 %}`)
|
|
12
|
+
- `max_items` config option for `related` section (default: 5)
|
|
13
|
+
- Dropdown config included in generated `search-runtime-config.js`
|
|
14
|
+
- `dropdown_enabled?` accessor and dropdown asset in `runtime_assets`
|
|
15
|
+
- HTML-escaping (via `CGI.escapeHTML`) for `engine_url`, `engine_sri`, and `engine_crossorigin` attributes in `SearchTag` script output
|
|
16
|
+
- Defensive guard with `simplecov:disable` annotation for unreachable `embedder_asset` nil branch in `SearchTag`
|
|
17
|
+
- Ollama embedding adapter tests for empty/invalid responses and standard error handling
|
|
18
|
+
- Related analyzer tests for semantic-only similarity matching
|
|
19
|
+
- System test assertions for embeddings/retrieval content matching and non-ASCII query robustness
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- `related.max_items` default changed from `nil` (unlimited) to `5`
|
|
23
|
+
- `Configuration#initialize` refactored — extracted `normalize_configured` helper for cleaner config validation
|
|
24
|
+
- Runtime config page now merges `dropdown` config alongside `liveSearch`
|
|
25
|
+
- `SearchTag` and `RelatedTag` now return empty string when `site` is nil or `client_search` is `false` (previously crashed on nil site)
|
|
26
|
+
- Meta test (`test/meta.test.js`) converted from `forEach` to `for` loops to properly `await` settle in cross-engine comparison
|
|
27
|
+
- System tests strengthened with content assertions and non-ASCII robustness checks
|
|
28
|
+
- AGENTS.md restructured — split developer info to README.developer.md, added dropdown architecture entries
|
|
29
|
+
- README.developer.md expanded with coverage documentation, spec file table, and SimpleCov branch coverage notes
|
|
30
|
+
- README.md updated with dropdown section, related `max:N` docs, and CSS styling examples for dropdown
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- `SearchTag#render` no longer raises on nil site (now returns empty string)
|
|
34
|
+
- `RelatedTag#render` no longer raises on nil site or `client_search: false` config
|
|
35
|
+
- Meta test race condition — `forEach` callback couldn't `await settle()`, causing flaky cross-engine comparisons; fixed by switching to `for` loops
|
|
36
|
+
|
|
37
|
+
## 0.2.0 — 2026-08-27
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- Generic `passthrough_fields` config option: external plugins declare which document `data` fields to forward into the search index (e.g. `file_type`, `icon_url`, `icon_set` from jekyll-documents)
|
|
41
|
+
- Field renaming in `passthrough_fields`: entries can be strings (same name) or hashes (`{source => target}`) for integration with other search conventions
|
|
42
|
+
- `icon_field` config option (default: `"icon_url"`): tells the browser runtime which field to render as an `<img>` icon before the result title; set to `null` to disable
|
|
43
|
+
- Search index now includes `source` (collection label: `posts`, `pages`, etc.) — always present, set by the generator
|
|
44
|
+
- Result `<article>` elements carry `data-source`, `data-categories`, `data-tags`, plus `data-*` for all passthrough fields (snake_case → data-kebab-case) for CSS-based result customization
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
- Allow whitespace in script/style end tags in `DocumentBuilder#clean` (`</script\s*>` instead of `</script>`) — CodeQL: bad HTML filtering regexp
|
|
48
|
+
- Fix polynomial ReDoS in `SearchTag` and `RelatedTag` syntax regexes — strip markup before matching, remove leading/trailing `\s*` patterns that caused O(n²) backtracking (CodeQL: polynomial regex on uncontrolled data)
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
- Simplified release workflow to tag-push trigger (`push: tags: v*`) — no manual `gh release create` needed
|
|
52
|
+
- Switched to RubyGems trusted publishing (`rubygems/release-gem@v1` with OIDC)
|
|
53
|
+
- Centralized version in `version.rb` as single source of truth — removed `version` field from `package.json`
|
|
54
|
+
- Added `rake version:bump` and `rake version:check_changelog` tasks
|
|
55
|
+
- Added CHANGELOG gate to release workflow (fails if entry missing for the version)
|
|
56
|
+
- Added `npm audit` and `npm outdated` (non-blocking) to CI
|
|
57
|
+
- Replaced pre-commit hook with rubocop-only (fast); added pre-push hook (rubocop + rspec)
|
|
58
|
+
- Dropped reek from quality stack (KISS — marginal value on a small gem)
|
|
59
|
+
- Aligned AGENTS.md to the open convention; added CLAUDE.md and .windsurfrules pointer files
|
|
60
|
+
|
|
3
61
|
## 0.1.0 — 2026-08-25
|
|
4
62
|
|
|
5
63
|
- Add a pluggable search engine architecture with a base runtime and adapters.
|
data/README.developer.md
CHANGED
|
@@ -12,14 +12,66 @@ rbenv install 3.4.10 # if not already installed
|
|
|
12
12
|
rbenv local 3.4.10
|
|
13
13
|
bundle install
|
|
14
14
|
npm ci
|
|
15
|
-
bundle exec rspec
|
|
16
|
-
|
|
15
|
+
bundle exec rspec # Ruby tests (248 examples; 7 Ollama pending)
|
|
16
|
+
OLLAMA_INTEGRATION=1 bundle exec rspec # all tests including Ollama integration
|
|
17
|
+
npm test # JavaScript tests (134 tests)
|
|
17
18
|
```
|
|
18
19
|
|
|
19
20
|
Both test suites pass with only the committed Wikipedia fixtures and the
|
|
20
21
|
committed baseline JSON artifacts. No network access or external services
|
|
21
22
|
are required for the default test run.
|
|
22
23
|
|
|
24
|
+
## Quality checks and git hooks
|
|
25
|
+
|
|
26
|
+
The project uses a focused quality stack: RuboCop (style), bundler-audit
|
|
27
|
+
(security), RSpec (Ruby tests), and `npm test` (JavaScript tests). This
|
|
28
|
+
covers the actionable ground without the maintenance burden of additional
|
|
29
|
+
code-smell tools on a small focused gem.
|
|
30
|
+
|
|
31
|
+
### Rake tasks
|
|
32
|
+
|
|
33
|
+
| Task | What it runs |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `rake` (default) | `rake quality` — all checks below |
|
|
36
|
+
| `rake quality` | rubocop + bundler-audit + rspec + npm test |
|
|
37
|
+
| `rake quick` | rubocop + rspec (fast pre-push subset) |
|
|
38
|
+
| `rake rubocop` | RuboCop style check |
|
|
39
|
+
| `rake rubocop_fix` | RuboCop auto-fix |
|
|
40
|
+
| `rake bundler_audit` | `bundle-audit check --update` security scan |
|
|
41
|
+
| `rake spec` | RSpec test suite |
|
|
42
|
+
| `rake npm_test` | JavaScript test suite (`npm test`) |
|
|
43
|
+
| `rake ci` | rspec + rubocop + syntax checks + npm test + gem build |
|
|
44
|
+
|
|
45
|
+
### Git hooks
|
|
46
|
+
|
|
47
|
+
Git hooks are not committed to the repository. Install them locally after
|
|
48
|
+
cloning:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bin/install-hooks.sh
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This installs two hooks:
|
|
55
|
+
|
|
56
|
+
| Hook | What it runs | When |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| `pre-commit` | `rubocop` only (~2s) | Before each commit |
|
|
59
|
+
| `pre-push` | `rubocop + rspec` (~15s) | Before each push |
|
|
60
|
+
|
|
61
|
+
The pre-commit hook is intentionally fast (style only) to avoid bypassing
|
|
62
|
+
with `--no-verify`. The full test suite runs on pre-push and in CI. Skip
|
|
63
|
+
either with `git commit --no-verify` or `git push --no-verify`.
|
|
64
|
+
|
|
65
|
+
### CI checks
|
|
66
|
+
|
|
67
|
+
The [CI workflow](.github/workflows/ci.yml) runs on every push and pull
|
|
68
|
+
request across Ruby 3.2/3.3/3.4 and Node 22/24:
|
|
69
|
+
|
|
70
|
+
- `bundle exec rake ci` (rspec, rubocop, syntax checks, npm test, gem build)
|
|
71
|
+
- `bundle exec bundle-audit check --update` (Ruby dependency security)
|
|
72
|
+
- `npm audit --audit-level=high` (JavaScript dependency security)
|
|
73
|
+
- `npm outdated` (non-blocking — warns about outdated npm packages)
|
|
74
|
+
|
|
23
75
|
## Build-time related articles
|
|
24
76
|
|
|
25
77
|
The optional related-analysis pass writes `search-relations.json` separately
|
|
@@ -62,14 +114,9 @@ content, self-exclusion, and score sorting.
|
|
|
62
114
|
|
|
63
115
|
### Related test files
|
|
64
116
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
| `spec/related_tag_spec.rb` | Unit tests for the `{% related_articles %}` Liquid tag |
|
|
69
|
-
| `spec/search_tag_spec.rb` | Unit tests for the `{% search_form %}` Liquid tag |
|
|
70
|
-
| `spec/tasks_spec.rb` | Unit tests for the rake tasks (reference_files, install) |
|
|
71
|
-
| `spec/system_spec.rb` | System tests building the fixture site with related and search_form |
|
|
72
|
-
| `test/related.test.js` | JS tests for the related renderer (default, renderItem, filter, sort) |
|
|
117
|
+
See the [Spec files](#spec-files) table for the full list. The key related
|
|
118
|
+
test files are `spec/related_analyzer_spec.rb`, `spec/related_tag_spec.rb`,
|
|
119
|
+
`spec/system_spec.rb`, and `test/related.test.js`.
|
|
73
120
|
|
|
74
121
|
## What is committed
|
|
75
122
|
|
|
@@ -197,8 +244,223 @@ bundle exec rspec --tag system
|
|
|
197
244
|
bundle exec rspec --tag ollama_integration
|
|
198
245
|
```
|
|
199
246
|
|
|
247
|
+
## Coverage
|
|
248
|
+
|
|
249
|
+
The project uses SimpleCov 1.1+ with branch coverage. The target is 100%
|
|
250
|
+
branch coverage and ~99.6% line coverage. Three lines are intentionally
|
|
251
|
+
excluded via `# simplecov:disable branch` for an unreachable defensive
|
|
252
|
+
guard in `search_tag.rb` (the `if embedder_asset` check that can never be
|
|
253
|
+
false with current embedder types — `semantic_with_embedder?` already
|
|
254
|
+
filters out the `none` type, and both `transformers` and `ollama_api`
|
|
255
|
+
always return an asset).
|
|
256
|
+
|
|
257
|
+
The coverage report is generated at `coverage/index.html` after each RSpec
|
|
258
|
+
run. Raw data is in `coverage/.resultset.json` (SimpleCov 1.0+ format with
|
|
259
|
+
`lines` and `branches` keys per file).
|
|
260
|
+
|
|
261
|
+
To check remaining uncovered lines and branches after a run:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
python3 -c "
|
|
265
|
+
import json
|
|
266
|
+
with open('coverage/.resultset.json') as f:
|
|
267
|
+
data = json.load(f)
|
|
268
|
+
for suite, info in data.items():
|
|
269
|
+
cov = info.get('coverage', {})
|
|
270
|
+
for file, lines in sorted(cov.items()):
|
|
271
|
+
if 'lib/jekyll' not in file: continue
|
|
272
|
+
short = file.split('/lib/')[-1]
|
|
273
|
+
line_data = lines.get('lines', []) if isinstance(lines, dict) else lines
|
|
274
|
+
uncovered = [i+1 for i, v in enumerate(line_data) if v == 0]
|
|
275
|
+
if uncovered:
|
|
276
|
+
print(f'{short}: lines {uncovered}')
|
|
277
|
+
branches = lines.get('branches', {}) if isinstance(lines, dict) else {}
|
|
278
|
+
for bk, bd in branches.items():
|
|
279
|
+
for sk, count in bd.items():
|
|
280
|
+
if count == 0:
|
|
281
|
+
print(f'{short}: {sk} (count=0)')
|
|
282
|
+
"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Spec files
|
|
286
|
+
|
|
287
|
+
| File | Description |
|
|
288
|
+
| --- | --- |
|
|
289
|
+
| `spec/configuration_spec.rb` | Site configuration, defaults, validation |
|
|
290
|
+
| `spec/dropdown_configuration_spec.rb` | Dropdown config validation |
|
|
291
|
+
| `spec/dropdown_tag_spec.rb` | `{% search_dropdown %}` Liquid tag |
|
|
292
|
+
| `spec/search_tag_spec.rb` | `{% search_form %}` Liquid tag |
|
|
293
|
+
| `spec/related_tag_spec.rb` | `{% related_articles %}` Liquid tag |
|
|
294
|
+
| `spec/related_analyzer_spec.rb` | Build-time relation analysis |
|
|
295
|
+
| `spec/related_configuration_spec.rb` | Related config validation |
|
|
296
|
+
| `spec/generator_spec.rb` | Jekyll generator integration |
|
|
297
|
+
| `spec/document_builder_spec.rb` | Normalized search documents |
|
|
298
|
+
| `spec/search_index_page_spec.rb` | Generated JSON page |
|
|
299
|
+
| `spec/index_cache_spec.rb` | Index cache |
|
|
300
|
+
| `spec/embedding_cache_spec.rb` | Embedding cache |
|
|
301
|
+
| `spec/runtime_assets_spec.rb` | Runtime asset copying |
|
|
302
|
+
| `spec/runtime_config_page_spec.rb` | Runtime config page generation |
|
|
303
|
+
| `spec/ollama_embedding_adapter_spec.rb` | Ollama adapter unit tests |
|
|
304
|
+
| `spec/ollama_integration_spec.rb` | Ollama integration tests (pending) |
|
|
305
|
+
| `spec/llm_injection_spec.rb` | LLM injection baseline tests (pending) |
|
|
306
|
+
| `spec/tasks_spec.rb` | Rake tasks (reference_files, install) |
|
|
307
|
+
| `spec/gemspec_spec.rb` | Gemspec metadata |
|
|
308
|
+
| `spec/system_spec.rb` | End-to-end system tests |
|
|
309
|
+
| `test/runtime.test.js` | JS unit tests parameterized over adapters |
|
|
310
|
+
| `test/related.test.js` | JS tests for related renderer |
|
|
311
|
+
| `test/system.test.js` | JS system tests using committed baseline index |
|
|
312
|
+
|
|
200
313
|
## CI
|
|
201
314
|
|
|
202
315
|
The CI pipeline runs `bundle exec rspec` and `npm test` without arXiv
|
|
203
316
|
fixtures or Ollama. The committed baseline JSON and semantic embeddings
|
|
204
317
|
ensure all tests pass in CI without external dependencies.
|
|
318
|
+
|
|
319
|
+
## Releasing
|
|
320
|
+
|
|
321
|
+
Releases are published to RubyGems.org via the
|
|
322
|
+
[`release.yml`](.github/workflows/release.yml) GitHub Actions workflow,
|
|
323
|
+
which triggers automatically when a GitHub Release is published. The
|
|
324
|
+
workflow uses RubyGems trusted publishing (OIDC) — no API key is stored
|
|
325
|
+
in the repository.
|
|
326
|
+
|
|
327
|
+
### Prerequisites (one-time setup)
|
|
328
|
+
|
|
329
|
+
Trusted publishing must be configured on rubygems.org for the gem before
|
|
330
|
+
the first release:
|
|
331
|
+
|
|
332
|
+
1. Log into <https://rubygems.org> and open the gem's page.
|
|
333
|
+
2. Go to **Settings → Trusted Publishers → Add trusted publisher**.
|
|
334
|
+
3. Enter:
|
|
335
|
+
- **Repository**: `gundestrup/jekyll-client-search`
|
|
336
|
+
- **Workflow filename**: `release.yml`
|
|
337
|
+
- **Environment**: `release`
|
|
338
|
+
|
|
339
|
+
If the gem has never been published before, rubygems.org may require a
|
|
340
|
+
one-time manual `gem push` with an API key to create the gem name before
|
|
341
|
+
a trusted publisher can be attached. After that, all subsequent releases
|
|
342
|
+
use trusted publishing automatically.
|
|
343
|
+
|
|
344
|
+
### Version bumping
|
|
345
|
+
|
|
346
|
+
The version lives in [`lib/jekyll/client_search/version.rb`](lib/jekyll/client_search/version.rb)
|
|
347
|
+
and follows [Semantic Versioning](https://semver.org/). The gemspec reads
|
|
348
|
+
from this file — it is the **single source of truth**. No other file
|
|
349
|
+
stores the version (`package.json` is `private: true` and has no version
|
|
350
|
+
field). Use the rake task to bump it:
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
bundle exec rake "version:bump[patch]" # 0.1.0 -> 0.1.1 (bug fixes)
|
|
354
|
+
bundle exec rake "version:bump[minor]" # 0.1.0 -> 0.2.0 (new features, backwards compatible)
|
|
355
|
+
bundle exec rake "version:bump[major]" # 0.1.0 -> 1.0.0 (incompatible API changes)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
The rake task only edits `version.rb`. It does not commit, tag, or update
|
|
359
|
+
the changelog — those are manual steps in the release checklist below.
|
|
360
|
+
|
|
361
|
+
You can verify that the CHANGELOG has an entry for the current version:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
bundle exec rake version:check_changelog
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
This check also runs in the release workflow — the publish will fail if
|
|
368
|
+
`CHANGELOG.md` has no `## X.Y.Z` entry matching the version being released.
|
|
369
|
+
|
|
370
|
+
### Release checklist
|
|
371
|
+
|
|
372
|
+
1. **Ensure the working tree is clean** and on `main`:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
git status
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
2. **Ensure all 80 fixture posts are present** (needed for Ollama
|
|
379
|
+
integration tests and baseline regeneration):
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
ruby spec/fixtures/download_arxiv.rb
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Requires `pdftotext` (`brew install poppler` on macOS). Skip if the
|
|
386
|
+
arXiv posts are already present and unchanged.
|
|
387
|
+
|
|
388
|
+
3. **Run the full test suite locally**, including Ollama integration tests:
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
bundle exec rspec
|
|
392
|
+
OLLAMA_INTEGRATION=1 bundle exec rspec spec/ollama_integration_spec.rb
|
|
393
|
+
OLLAMA_INTEGRATION=1 bundle exec rspec spec/llm_injection_spec.rb
|
|
394
|
+
bundle exec rubocop
|
|
395
|
+
npm test
|
|
396
|
+
npm run lint
|
|
397
|
+
gem build jekyll-client-search.gemspec
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
The Ollama integration tests require a running [Ollama](https://ollama.ai/)
|
|
401
|
+
server with the `embeddinggemma:300m` model pulled (`ollama pull
|
|
402
|
+
embeddinggemma:300m`). Do not publish a release until the Ollama
|
|
403
|
+
integration tests pass locally — they are skipped by default and will
|
|
404
|
+
not block CI, so they must be run manually before each release.
|
|
405
|
+
|
|
406
|
+
4. **Bump the version** (see above) and update the `## Unreleased` or new
|
|
407
|
+
`## X.Y.Z — YYYY-MM-DD` section at the top of
|
|
408
|
+
[`CHANGELOG.md`](CHANGELOG.md) with a user-facing summary of changes.
|
|
409
|
+
|
|
410
|
+
5. **Commit the version bump and changelog**:
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
git add lib/jekyll/client_search/version.rb CHANGELOG.md
|
|
414
|
+
git commit -m "Release X.Y.Z: <short summary>"
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
6. **Tag the release** with an annotated tag:
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
git tag -a vX.Y.Z -m "Release X.Y.Z
|
|
421
|
+
|
|
422
|
+
<one-line summary of notable changes>"
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
7. **Push `main` and the tag** to GitHub — pushing the tag triggers the
|
|
426
|
+
release workflow automatically:
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
git push origin main
|
|
430
|
+
git push origin vX.Y.Z
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
No manual `gh release create` is needed. The workflow builds the gem,
|
|
434
|
+
attaches it to a GitHub release, and publishes to RubyGems.
|
|
435
|
+
|
|
436
|
+
8. **Watch the workflow** and verify the gem appears on RubyGems:
|
|
437
|
+
|
|
438
|
+
```bash
|
|
439
|
+
gh run watch --workflow=release.yml
|
|
440
|
+
gem list jekyll-client-search --remote --exact
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
9. **Verify the integration site** builds cleanly against the published
|
|
444
|
+
gem (update the path dependency to the released version in
|
|
445
|
+
`../gundestrup.dk` and run `bundle exec jekyll build`).
|
|
446
|
+
|
|
447
|
+
### What the release workflow does
|
|
448
|
+
|
|
449
|
+
The [`release.yml`](.github/workflows/release.yml) workflow, triggered by
|
|
450
|
+
pushing a `v*` tag:
|
|
451
|
+
|
|
452
|
+
1. Checks out the repository at the release tag (`persist-credentials: false`).
|
|
453
|
+
2. Sets up Ruby 3.4.10.
|
|
454
|
+
3. Verifies the tag name matches `v<gem version>` (fails on mismatch).
|
|
455
|
+
4. Verifies `CHANGELOG.md` has a `## X.Y.Z` entry for the version
|
|
456
|
+
(`rake version:check_changelog` — fails if missing).
|
|
457
|
+
5. Builds the gem (`gem build`).
|
|
458
|
+
6. Creates a GitHub Release and attaches the `.gem` file as a downloadable
|
|
459
|
+
asset (`softprops/action-gh-release@v2`).
|
|
460
|
+
7. Publishes the gem to RubyGems.org using trusted publishing
|
|
461
|
+
(`rubygems/release-gem@v1` with OIDC).
|
|
462
|
+
|
|
463
|
+
CI already validates tests on every push — the release workflow does not
|
|
464
|
+
re-run the test suite. It only builds and publishes. The workflow runs in
|
|
465
|
+
the `release` environment, which must match the environment name
|
|
466
|
+
configured on the trusted publisher.
|
data/README.md
CHANGED
|
@@ -25,8 +25,10 @@ and vector similarity.
|
|
|
25
25
|
- [Related articles](#related-articles)
|
|
26
26
|
- [Semantic search (embeddings)](#semantic-search-embeddings)
|
|
27
27
|
- [The search form](#the-search-form)
|
|
28
|
+
- [The search dropdown](#the-search-dropdown)
|
|
28
29
|
- [Rake tasks](#rake-tasks)
|
|
29
30
|
- [Configuration reference](#configuration-reference)
|
|
31
|
+
- [Indexing custom collections](#indexing-custom-collections)
|
|
30
32
|
- [Architecture](#architecture)
|
|
31
33
|
- [Embeddings and incremental indexing](#embeddings-and-incremental-indexing)
|
|
32
34
|
- [Related articles reference](#related-articles-reference)
|
|
@@ -138,6 +140,23 @@ Then add one line to any post layout:
|
|
|
138
140
|
{% related_articles %}
|
|
139
141
|
```
|
|
140
142
|
|
|
143
|
+
By default, 5 related articles are shown. Override per-tag with `max:N`:
|
|
144
|
+
|
|
145
|
+
```liquid
|
|
146
|
+
{% related_articles max:3 %}
|
|
147
|
+
{% related_articles sort:date max:10 %}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Or set the default in `_config.yml`:
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
client_search:
|
|
154
|
+
related:
|
|
155
|
+
enabled: true
|
|
156
|
+
max_items: 5 # default: 5; null = no limit
|
|
157
|
+
minimum_similarity: 0.55
|
|
158
|
+
```
|
|
159
|
+
|
|
141
160
|
Without embeddings, relations are based on shared tags, categories, and
|
|
142
161
|
hierarchical parent domains. With embeddings enabled, vector similarity
|
|
143
162
|
above the cutoff is also included. See
|
|
@@ -215,6 +234,160 @@ client_search:
|
|
|
215
234
|
For manual `<script>` setup (engine-specific HTML), see
|
|
216
235
|
[Browser usage reference](#browser-usage-reference).
|
|
217
236
|
|
|
237
|
+
## The search dropdown
|
|
238
|
+
|
|
239
|
+
The `{% search_dropdown %}` Liquid tag renders a compact live-search
|
|
240
|
+
dropdown suitable for navbars and headers. It is framework-agnostic —
|
|
241
|
+
emits semantic HTML with data attributes, no CSS classes from Bulma,
|
|
242
|
+
Bootstrap, Tailwind, or any other framework. Style it with your own CSS.
|
|
243
|
+
|
|
244
|
+
```liquid
|
|
245
|
+
{% search_dropdown %}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Tag modes:
|
|
249
|
+
|
|
250
|
+
| Syntax | Effect |
|
|
251
|
+
| --- | --- |
|
|
252
|
+
| `{% search_dropdown %}` | Dropdown HTML + all scripts (default) |
|
|
253
|
+
| `{% search_dropdown max:10 %}` | Show up to 10 items (default: 5) |
|
|
254
|
+
| `{% search_dropdown scripts_only %}` | Just the scripts — use with custom HTML |
|
|
255
|
+
| `{% search_dropdown no_scripts %}` | Just the dropdown HTML — load scripts yourself |
|
|
256
|
+
|
|
257
|
+
**Behavior:**
|
|
258
|
+
|
|
259
|
+
- User types → results appear in a dropdown list (live, debounced)
|
|
260
|
+
- Enter with no item selected → redirect to `redirect_url` (default
|
|
261
|
+
`/search/`) with `?q=...` for full results on the search page
|
|
262
|
+
- Arrow Up/Down → highlight items, Enter on highlighted → navigate
|
|
263
|
+
- Click an item → navigate to that document's URL
|
|
264
|
+
- Escape or click outside → close dropdown
|
|
265
|
+
- Index is lazy-loaded on first keystroke (no cost on pages where the
|
|
266
|
+
visitor doesn't search)
|
|
267
|
+
|
|
268
|
+
**Configuration:**
|
|
269
|
+
|
|
270
|
+
```yaml
|
|
271
|
+
client_search:
|
|
272
|
+
dropdown:
|
|
273
|
+
enabled: true # default
|
|
274
|
+
max_items: 5 # default, tag param max:N overrides
|
|
275
|
+
min_chars: 2 # minimum query length before search fires
|
|
276
|
+
debounce_ms: 150 # debounce delay
|
|
277
|
+
redirect_url: /search/ # where Enter redirects for full results
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Icon support:** When `icon_field` is configured (default `icon_url`)
|
|
281
|
+
and the search index includes that field (e.g. from jekyll-documents),
|
|
282
|
+
an `<img>` icon is rendered before each result title. Set `icon_field:
|
|
283
|
+
null` to disable.
|
|
284
|
+
|
|
285
|
+
**CSS styling:** The dropdown emits no framework classes. Style with:
|
|
286
|
+
|
|
287
|
+
```css
|
|
288
|
+
.client-search-dropdown { position: relative; }
|
|
289
|
+
.client-search-dropdown ul[role="listbox"] {
|
|
290
|
+
position: absolute; top: 100%; right: 0;
|
|
291
|
+
min-width: 300px; z-index: 30;
|
|
292
|
+
background: white; box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
293
|
+
list-style: none; margin: 0; padding: 0;
|
|
294
|
+
}
|
|
295
|
+
.client-search-dropdown li { padding: 0.5rem 1rem; }
|
|
296
|
+
.client-search-dropdown li:hover,
|
|
297
|
+
.client-search-dropdown li[aria-selected="true"] { background: #f5f5f5; }
|
|
298
|
+
.client-search-dropdown li a { text-decoration: none; color: inherit; }
|
|
299
|
+
|
|
300
|
+
/* Type-based styling via data attributes */
|
|
301
|
+
.client-search-dropdown li[data-source="documents"] { border-left: 3px solid #48c774; }
|
|
302
|
+
.client-search-dropdown li[data-file-type="pdf"]::before {
|
|
303
|
+
content: "PDF"; background: #ff0000; color: white;
|
|
304
|
+
padding: 0 0.3em; font-size: 0.7em; margin-right: 0.3em;
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Customizing search results with CSS
|
|
309
|
+
|
|
310
|
+
Each search result is rendered as an `<article>` with data attributes that
|
|
311
|
+
expose the document's metadata for CSS-based styling:
|
|
312
|
+
|
|
313
|
+
| Attribute | Value | Source |
|
|
314
|
+
| --- | --- | --- |
|
|
315
|
+
| `data-source` | Collection label (`posts`, `pages`, `documents`, …) | Generator (always present) |
|
|
316
|
+
| `data-categories` | Space-separated categories | Front matter |
|
|
317
|
+
| `data-tags` | Space-separated tags | Front matter |
|
|
318
|
+
| `data-*` | Any passthrough field (e.g. `data-file-type`, `data-icon-set`) | Config-driven |
|
|
319
|
+
|
|
320
|
+
**Passthrough fields:** External plugins can expose their data in search
|
|
321
|
+
results by listing field names in `passthrough_fields`. Each listed field is
|
|
322
|
+
forwarded from the document's front matter / data into the search index and
|
|
323
|
+
rendered as a `data-*` attribute on the result `<article>` (field names are
|
|
324
|
+
converted from `snake_case` to `data-kebab-case`).
|
|
325
|
+
|
|
326
|
+
**Field renaming:** Entries can be strings (same name) or hashes (rename
|
|
327
|
+
`{source => target}`) for integration with other search conventions:
|
|
328
|
+
|
|
329
|
+
```yaml
|
|
330
|
+
client_search:
|
|
331
|
+
passthrough_fields:
|
|
332
|
+
- file_type: doctype # index as "doctype" instead of "file_type"
|
|
333
|
+
- icon_url: thumbnail # index as "thumbnail" instead of "icon_url"
|
|
334
|
+
- icon_set # stays as "icon_set"
|
|
335
|
+
icon_field: thumbnail # must match the target name
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Auto-injection:** When jekyll-documents is installed and `documents` is
|
|
339
|
+
in the search collections, `file_type`, `icon_url`, and `icon_set` are
|
|
340
|
+
auto-injected into `passthrough_fields` — no manual config needed.
|
|
341
|
+
|
|
342
|
+
**Icon rendering:** When the configured `icon_field` (default: `"icon_url"`)
|
|
343
|
+
is present in a result, an `<img class="client-search-result-icon">` is
|
|
344
|
+
rendered before the title. Set `icon_field: null` to disable.
|
|
345
|
+
|
|
346
|
+
**Default icon sizing:** Icons are sized to `1em` (matching the heading's
|
|
347
|
+
font size) with `vertical-align: middle` and a small right margin — they
|
|
348
|
+
auto-scale with the text. The inline styles are self-contained (no CSS
|
|
349
|
+
dependency), so they work with any framework (Bulma, Bootstrap, Tailwind).
|
|
350
|
+
|
|
351
|
+
Override with CSS:
|
|
352
|
+
|
|
353
|
+
```css
|
|
354
|
+
/* Make icons larger */
|
|
355
|
+
.client-search-result-icon { width: 1.5em; height: 1.5em; }
|
|
356
|
+
|
|
357
|
+
/* Fixed pixel sizes (if jekyll-documents' documents.css is included) */
|
|
358
|
+
.client-search-result-icon.icon-x2 { width: 32px; height: 32px; }
|
|
359
|
+
|
|
360
|
+
/* Theme-aware sizing using jekyll-documents' icon_set */
|
|
361
|
+
.client-search-result[data-icon-set="color"] .client-search-result-icon { width: 1.5em; }
|
|
362
|
+
.client-search-result[data-icon-set="ultra-minimal"] .client-search-result-icon { width: 1em; }
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**CSS examples:**
|
|
366
|
+
|
|
367
|
+
```css
|
|
368
|
+
/* Show a different border color for posts vs pages */
|
|
369
|
+
.client-search-result[data-source="posts"] { border-left: 3px solid #3273dc; }
|
|
370
|
+
.client-search-result[data-source="pages"] { border-left: 3px solid #48c774; }
|
|
371
|
+
|
|
372
|
+
/* Add a badge showing the file type via CSS (no icon_url needed) */
|
|
373
|
+
.client-search-result[data-file-type="pdf"]::before {
|
|
374
|
+
content: "PDF";
|
|
375
|
+
background: #ff0000;
|
|
376
|
+
color: white;
|
|
377
|
+
padding: 0 0.3em;
|
|
378
|
+
font-size: 0.7em;
|
|
379
|
+
margin-right: 0.3em;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* Show categories as tags */
|
|
383
|
+
.client-search-result[data-categories~="travel"] .title::after {
|
|
384
|
+
content: " ✈";
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
This enables per-source icons, file-type badges, category indicators, and
|
|
389
|
+
tag-based styling — all through CSS, with no JavaScript changes needed.
|
|
390
|
+
|
|
218
391
|
## Rake tasks
|
|
219
392
|
|
|
220
393
|
The gem ships reference layouts and includes that you can copy into your
|
|
@@ -343,6 +516,47 @@ Each document in the index contains:
|
|
|
343
516
|
}
|
|
344
517
|
```
|
|
345
518
|
|
|
519
|
+
## Indexing custom collections
|
|
520
|
+
|
|
521
|
+
By default only `posts` are indexed. Add any Jekyll collection to the
|
|
522
|
+
`collections` list to include it in the search index:
|
|
523
|
+
|
|
524
|
+
```yaml
|
|
525
|
+
client_search:
|
|
526
|
+
collections:
|
|
527
|
+
- posts
|
|
528
|
+
- documents
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Each document in the index contains `id`, `title`, `url`, `excerpt`,
|
|
532
|
+
`content`, `categories`, `tags`, and normalized date fields. Collections
|
|
533
|
+
from other plugins work as long as their documents expose these fields.
|
|
534
|
+
|
|
535
|
+
### Integration with jekyll-documents
|
|
536
|
+
|
|
537
|
+
[jekyll-documents](https://github.com/gundestrup/jekyll-documents) turns
|
|
538
|
+
files in `assets/documents/` into a browsable `documents` collection with
|
|
539
|
+
titles, categories, and searchable content baked in at build time. To
|
|
540
|
+
include uploaded documents in search results alongside posts:
|
|
541
|
+
|
|
542
|
+
```yaml
|
|
543
|
+
# _config.yml
|
|
544
|
+
plugins:
|
|
545
|
+
- jekyll-documents
|
|
546
|
+
- jekyll-client-search
|
|
547
|
+
|
|
548
|
+
client_search:
|
|
549
|
+
collections:
|
|
550
|
+
- posts
|
|
551
|
+
- documents
|
|
552
|
+
related:
|
|
553
|
+
enabled: true
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
No additional configuration is needed — `jekyll-documents` bakes
|
|
557
|
+
`categories` (plural array) and searchable `content` into each document
|
|
558
|
+
so the `DocumentBuilder` can index them directly.
|
|
559
|
+
|
|
346
560
|
## Architecture
|
|
347
561
|
|
|
348
562
|
The plugin uses a base runtime + adapter architecture where the base owns
|
|
@@ -538,10 +752,16 @@ must return a DOM node (or `null` to skip the item):
|
|
|
538
752
|
<script src="/assets/client-search-related.js"></script>
|
|
539
753
|
<script>
|
|
540
754
|
ClientSearchRelated.run({
|
|
541
|
-
renderItem: function (item,
|
|
542
|
-
var li =
|
|
543
|
-
|
|
544
|
-
|
|
755
|
+
renderItem: function (item, dom) {
|
|
756
|
+
var li = dom.createElement("li");
|
|
757
|
+
var link = dom.createElement("a");
|
|
758
|
+
link.href = item.url;
|
|
759
|
+
link.textContent = item.title;
|
|
760
|
+
var score = dom.createElement("span");
|
|
761
|
+
score.className = "score";
|
|
762
|
+
score.textContent = (item.score * 100).toFixed(0) + "%";
|
|
763
|
+
li.appendChild(link);
|
|
764
|
+
li.appendChild(score);
|
|
545
765
|
return li;
|
|
546
766
|
}
|
|
547
767
|
});
|