Repository navigation
Expand file tree
/
Copy pathpagedata.py
More file actions
executable file
·686 lines (656 loc) · 47.3 KB
/
Copy pathpagedata.py
File metadata and controls
executable file
·686 lines (656 loc) · 47.3 KB
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
#!/usr/bin/env python3
"""Per-page SEO metadata for landing/ — hand-written, reviewed, versioned.
`scripts/seo/fix_heads.py` reads this table and nothing else decides what a
page's `<title>`, `<meta name="description">`, structured-data type or
breadcrumb label is. Keeping it in one file means a title change is a
one-line diff instead of a hunt through 80 HTML files.
Fields
label the page's own breadcrumb name (last ListItem). Required.
kind which JSON-LD body the page gets:
"software" -> SoftwareApplication (the product itself)
"collection" -> CollectionPage (a hub that lists pages)
"techarticle" -> TechArticle (reference documentation)
"article" -> Article (a case study)
"webpage" -> WebPage (everything else)
description the meta description AND og:description/twitter:description.
Target 110-165 characters: Google states no hard limit but
truncates "to fit the device width". Omit to keep whatever
the file already has (only done where the existing one is
already unique and correctly sized).
title overrides the file's <title>. Omit to keep the existing one.
Target <= 60 characters.
og_image optional per-page Open Graph card, as a landing-relative PNG
path under og/ (e.g. "og/does-xerj-beat-jev.png"). Omit to
keep the site-wide card. og_image_alt accompanies it; both
width and height stay the site-wide 1200x630. Blog posts use
og/blog-<slug>.png, rendered with the in-post hero and the
index thumbnail by scripts/seo/blog_covers/covers.py; keep
og_image_alt equal to that post's `og_alt` in covers.POSTS.
published / optional pinned dates for article-kind pages (the blog).
updated When set they feed datePublished/dateModified verbatim, so
the generated head does not drift with git history.
Every description below was written against that page's actual body copy.
None of them describe content the page does not contain — Google treats a
description that misrepresents the page as a reason to ignore it and generate
its own snippet.
"""
from __future__ import annotations
import pathlib
# Site-name suffix conventions already in use on the site are preserved:
# marketing pages read "XERJ.ai — Topic", docs read "XERJ.ai · Docs · Topic".
# Only the nine over-length titles and the six duplicated ones are rewritten.
PAGES: dict[str, dict[str, str]] = {
# ── root / product ──────────────────────────────────────────────────────
"index.html": dict(
label="Home", kind="software",
title="XERJ: the search engine for AI agents",
# The canonical description (seo_head.ORG_DESCRIPTION). seo_lint gates
# duplicate descriptions, so /product, /for-agents and
# /answers/what-is-xerj carry the same clause behind a short lead-in.
description="A new way for AI to search for data: one local Rust binary that indexes your code, docs, specs and logs, plus Corpus Hub packs, and returns cited answers.",
),
# `404.html` is what makes Cloudflare Pages return a real HTTP 404. With
# no top-level 404.html the project is treated as a single-page app and
# every unmatched path is answered with `index.html` and a 200 (see
# urlmap.NOINDEX and seo_lint rule 67). It is registered here like any
# other hand-written page so `fix_heads.py` owns its <head>; `noindex`
# comes from urlmap.NOINDEX, and it is kept out of sitemap.xml.
"404.html": dict(
label="Page not found", kind="webpage",
description="This URL has no page behind it on xerj.org. The response is a real HTTP 404, not a copy of the homepage — the docs, the answers hub and the site map are here.",
),
"product.html": dict(
label="Product", kind="software",
title="XERJ.ai — Local search for AI agents",
# Rewritten 2026-10-23 against the v1.0.0-rc.98 release binary
# (xerj --help, xerj mcp --help, a tools/list, and one xerj ask run)
# plus the files and fetches named on the page.
description="XERJ, the search engine for AI agents: one local Rust binary that indexes your code, docs, specs and logs, plus Corpus Hub packs, and returns cited answers.",
),
"for-agents.html": dict(
label="For AI agents", kind="software",
# 2026-10-24 CTR rewrite (GSC Oct 19-22: 318 impressions, position
# 4.6, 1 click). Every clause is on the page body: autoindex of a
# folder, keyword/semantic/vector/hybrid search, the /_memory store,
# "No SDK, no signup" in the lead.
title="XERJ for AI agents: search any folder, keep agent memory",
description="XERJ gives AI agents local folder search and memory in one binary. Keyword search is the default; neural embeddings are optional. No signup or SDK needed.",
),
"playground.html": dict(
label="Playground", kind="software",
description="Live XERJ dashboards running on seeded data — click filters, drill down, read real queries. Leave a work email to unlock the interactive playground.",
),
"playground/index.html": dict(
label="Dashboards", kind="webpage",
description="The XERJ observability dashboards application. Deliberately not indexed — open it from the playground gate at xerj.org/playground instead.",
),
"benchmarks/index.html": dict(
label="Benchmarks", kind="software",
title="XERJ.ai — Benchmarks: performance, retrieval, tokens, ZTA",
description="Every number traces to a run: the 88-cell Elasticsearch board, code retrieval measured head-to-head against Zoekt and ripgrep, 6.5x cheaper agent runs, and the zero-token architecture behind them.",
),
"benchmarks/elasticsearch.html": dict(
label="ES scorecard", kind="software",
title="XERJ vs Elasticsearch benchmarks: 88 cells, reproducible",
description="XERJ v1.0.0-rc.6 against Elasticsearch 8.13.4 in 88 measured cells: 55 win, 26 tie, 4 lose, 3 n/a. Four commands reproduce every one of them.",
),
# ── blog ────────────────────────────────────────────────────────────────
# Every post's og_image is its cover from the field-log series
# (scripts/seo/blog_covers/, README there); og_image_alt describes only
# what that card draws.
# The Jev write-up lives under /blog since 2026-09-21; it was git-mv'd
# from benchmarks/decisions.html (which 301s to it via landing/_redirects).
# published/updated are pinned so datePublished/dateModified do not drift
# with git (issue #974's flake); in-body run dates stay 2026-09-20.
"blog/does-xerj-beat-jev.html": dict(
label="Jev measured, wire spoken", kind="article",
title="Does XERJ beat JEV? On the bill, outright. On FiQA, no.",
description="1,271 judged queries: Jev rerank takes SciFact 0.7410 and FiQA 0.3638 (+0.126), our zero-API-token hybrid keeps NFCorpus 0.3448, and the FiQA run cost $0.6375.",
published="2026-09-21",
# 2026-09-27: added the closing 05 section (verdict + try-it); measurement
# content unchanged. updated follows the edit, published stays pinned.
updated="2026-09-27",
og_image="og/blog-does-xerj-beat-jev.png",
og_image_alt="Cover: “Does XERJ beat JEV?” in display type beside the post’s square-matrix art, labelled with its figures. The art: three pairs of square-matrix bars, ink for XERJ and blue for BM25+Jev, over one long blue bar and a single yellow square for the zero-cost local run",
),
# Decisions study, 2026-09-22. published/updated pinned so the schema dates
# do not drift with git (same reason as the post above). Prose written to the
# targets in docs/research/writing-corpus-2026-09/.
"blog/jev-vs-a-bm25-vote.html": dict(
label="Jev vs a BM25 vote", kind="article",
title="A BM25 vote ties Jev on spam and beats it on intent",
description="500 decisions each on SMS Spam and Banking77: a tie on spam (McNemar p=0.75), a 9-point retrieval win on 77-way intent at zero cost, and one dataset Jev wins.",
published="2026-09-22",
# 2026-09-27: added the closing 08 section (takeaway + try-it); measurement
# content unchanged. updated follows the edit, published stays pinned.
updated="2026-09-27",
og_image="og/blog-jev-vs-a-bm25-vote.png",
og_image_alt="Cover: “A BM25 vote ties Jev on spam and beats it on intent” in display type beside the post’s square-matrix art, labelled with its figures. The art: two grids of 500 squares, filled for correct answers; the ink grid has a band of yellow squares where it beats the blue grid",
),
# Size-epic stage-1 write-up, 2026-09-27. published/updated pinned like the
# two posts above; in-body run dates stay 2026-09-26/27.
"blog/cutting-the-index-28-percent.html": dict(
label="Cutting the index 28%", kind="article",
title="Cutting the index 28%: dissect first, write Rust second",
description="Four file-format changes took our 100k-doc index from 5.52 MB to 3.99 MB (−28%), each gated on dissecting real segments first. The losing rows are in the post.",
published="2026-09-27", updated="2026-09-27",
og_image="og/blog-cutting-the-index-28-percent.png",
og_image_alt="Cover: “Cutting the index 28%” in display type beside the post’s square-matrix art, labelled with its figures. The art: a large ink block of squares wrapped in a yellow L-shaped band that marks the 28% cut, beside two pairs of thin before-and-after bars",
),
# Corpus-pack GTM post, 2026-09-27. Every count traces to tools/packs/
# rust-vulns/pack-stats.json (provenance "measured", pinned by test); the
# prior-art signature table is our 2026-09 survey, phrased as a survey.
"blog/one-vulnerability-three-names.html": dict(
label="One vulnerability, three names", kind="article",
title="One vulnerability, three names — and a pack we sign",
description="The same Rust vulnerability arrives as RUSTSEC, GHSA and CVE. We collapsed 4,107 advisories to 1,950 identity-resolved records, packed them, and signed the pack — because no vulnerability database we surveyed does.",
published="2026-09-27",
# 2026-10-20: one in-body link to the offline advisory-id lookup.
updated="2026-10-20",
E870
og_image="og/blog-one-vulnerability-three-names.png",
og_image_alt="Cover: “One vulnerability, three names” in display type beside the post’s square-matrix art, labelled with its figures. The art: three outlined boxes trailing lines of small squares that converge on one square-matrix disc with a yellow seal at its centre",
),
# The rc.80 gates write-up, 2026-09-30 — the blog half of the packaging
# rule for the ten-item program (#1055-#1064): every gate verdict, wins
# and losses, each number traced to its gate run. The window was planned
# as rc.79 + rc.80 + v1.1.0 and measured as one suite; the post says so.
"blog/the-rc80-gates.html": dict(
label="The rc.80 gates", kind="article",
title="The rc.80 gates: three passed, three failed, one split",
description="Eight measured gates for rc.80. /_ask macro-F1 0.9975, xerj_map 20/30 to 0/30 and ECE 0.0088 pass. The judge, flywheel and decide head fail, numbers shown.",
published="2026-09-30", updated="2026-10-28",
og_image="og/blog-the-rc80-gates.png",
og_image_alt="Cover: “The rc.80 gates” in display type beside the post’s square-matrix art, labelled with its figures. The art: ten gates built from squares in two rows: three yellow with check marks, three ink with crosses, one half yellow and three faint",
),
# Corpus Hub launch post, 2026-10-01. Numbers trace to: docs/case-studies/
# reference-coding/SUMMARY.md (16/16, 9,982 vs 26,477, $1.58 vs $3.27),
# benchmarks/corpus-ab-2026-10/RESULTS.md (13 vs 16 confirmed, 1 vs 0 FP,
# zero corpus-traceable findings — the tie, published as a tie), and the
# registry itself on the corpus-hub branch (4 manifests + 1 recipe).
"blog/the-corpus-hub.html": dict(
label="The Corpus Hub", kind="article",
title="XERJ Corpus Hub: signed, pinned corpora for AI agents",
description="The XERJ Corpus Hub is a public registry of corpora for AI agents: sources pinned to commit SHAs, licences recorded, record packs ed25519-signed.",
published="2026-10-01",
# 2026-10-20: one in-body link to asking a pinned repo without cloning.
updated="2026-10-28",
og_image="og/blog-the-corpus-hub.png",
og_image_alt="Cover: “The Corpus Hub” in display type beside the post’s square-matrix art, labelled with its figures. The art: a 10 by 10 wall of square-matrix pack tiles in shades of ink, the first four solid yellow and the fifth framed",
),
# Guest post, 2026-10-07: Chunxiao Wang (Assay / nautilus-compass)
# answered our practitioner invitation in #1118 + #1138. Every measurement
# in the post is theirs, quoted from the issues; the only XERJ-side claims
# are the §05 registry counts (validate_hub.py run 2026-10-07: 98
# manifests, backlog 132 rows after PR #1210, 99 live, zero session/
# trajectory corpora). Updated 2026-10-11 (#1138 follow-through): the
# thin spot the post named was closed by the donated corpus — PR #1255
# (70 trajectories, CC-BY-4.0) + release pack-agent-session-trajectories-
# 2026-10-09 — so an UPDATE note now sits under §04. The 2026-10-07 counts
# are left verbatim; only the dated note was added.
"blog/eval-scars-from-agent-memory.html": dict(
label="Guest eval scars", kind="article",
title="Six eval scars from a production agent-memory system",
description="Guest recipes from Chunxiao Wang (Assay): topic slicing beat token windows on recall@1 by double digits, a 0.87 classifier nearly matched by a 0.78 always-yes baseline, and the mask bug that scored every query zero. Plus the corpus gap we checked and confirmed.",
published="2026-10-07", updated="2026-10-11",
og_image="og/blog-eval-scars-from-agent-memory.png",
og_image_alt="Cover: “Six eval scars from production agent memory” in display type beside the post’s square-matrix art, labelled with its figures. The art: six clusters of diagonal scratch marks made of small squares, five in ink and one in yellow",
),
# Exploit-hub A/B post, 2026-10-08. Every number traces to the
# sealed-truth eval (/root/eval-holdout/EVAL-REPORT.md): 23744 route/
# field from 36 in-corpus PoCs, 23 s retrieval vs 2.1 s negative
# control, the #1238 field-set table, 0/128 PoC lag, 37.6 GB honest
# group size, blogposts corpus 7 records / 107,295 B read-back 5/5.
"blog/the-exploit-hub.html": dict(
label="The exploit hub", kind="article",
title="The exploit hub, tested on three fresh CVEs",
description="We ran the exploit corpus against three CVEs it had never seen: one query named the exact route and field for one, an in-corpus 2026 exploit named the exact denylist segments the vendor shipped for another, and the third proved the corpus adds nothing when its stratum is empty. Losses included.",
published="2026-10-08",
# 2026-10-20: one in-body link to offline CVE/GHSA id lookup.
updated="2026-10-20",
og_image="og/blog-the-exploit-hub.png",
og_image_alt="Cover: “The exploit hub, tested on three fresh CVEs” in display type beside the post’s square-matrix art, labelled with its figures. The art: a network of small square nodes; a yellow hub linked to 36 yellow nodes, an ink node with one link, and an empty square frame with none",
),
# Corpus Hub v2 post, 2026-10-19. Every figure was measured or read live
# that day: timings from four cold + eleven warm runs of xerj v1.0.0-rc.97
# in an isolated HOME (lz4 pack 4,623,814 B, pack-lz4-0774d055); exit codes
# from engine/crates/xerj-hub/src/ask.rs and PR #1905; counts from the
# hub.xerj.org header (513 live), GitHub Releases (251 distinct slugs with
# a published *.hubpack.tar.zst) and the signed catalogue (14 entries,
# generated 2026-10-18T02:13Z); feedback/traffic JSON from /api/corpus-
# feedback and /api/hub-traffic for act; field-report loop #1838 -> #1848.
# 2026-10-20 refresh before publishing: repo stars/forks/contributors, hub
# stars (/api/hub-stars) and the Releases pack count (245 -> 251) re-read;
# contributor totals and the top-ten table re-checked, unchanged.
# rc.99 launch post, 2026-10-24. Headline chosen by the owner.
# Tag census: 97 v1.0.0-rc.* tags through rc.98 (rc.79 never tagged),
# so rc.99 is the 98th tagged release. The post says "just over 100
# days since rc.1" and does not print a day count taken from the tag date.
# Milestone post, 2026-10-29. Every figure fact-checked live on 2026-10-29:
# 17,486 stars (gh api repos/xerj-org/xerj, 08:56 CDT); 12,434 stars on Oct 15-26
# CDT and 12,582 in the best 12 x 24 h window; 5,000th star 2026-10-15, 17,000th
# 2026-10-27 (stargazers API, scripts/seo/blog_covers/data/xerj-stargazers.json).
# Owner asked to lead with the contributors on PR #2649. Title says
# "contributors", not "developers": the list includes xerj-team, cursoragent
# and one agent email. Outside PRs: 342 merged from 96 authors with
# authorAssociation CONTRIBUTOR or NONE (GraphQL, 08:53 CDT), 75 of them CLA
# sign-ons; field reports 44 from 39 authors. 100 contributors = GitHub
# contributors?anon=1 (103 entries) minus xerj-org, noreply@xerj.org and
# dependabot; #1 Rust repo of the day and week = Trendshift badges for repo
# 116361 (GitHub Trending Rust #1 first on 2026-10-15); 7,226 = hub.xerj.org
# /contributors/. Owner's "almost 10k" hub figure was not used (live: 7,226).
"blog/100-contributors.html": dict(
label="Milestone", kind="article",
title="100 contributors behind 12,000 stars in 12 days",
description="Meet the 100 contributors behind XERJ, which gained over 12,000 GitHub stars in 12 days. What they built, what you get now, and what comes next.",
published="2026-10-29", updated="2026-10-29",
og_image="og/blog-100-contributors.png",
og_image_alt="Cover: 100 in huge display type above the word contributors, then 12,000 stars in 12 days on a yellow highlight and 17,486 GitHub stars today; beside it a yellow area chart of the running star count from July to October 2026 that climbs steeply from 5,000 stars on Oct 15 to 17,000 stars on Oct 27; on the right a 10 by 10 grid of the 100 contributors' GitHub avatars",
),
# Performance post, 2026-10-26: a standalone post (not a release post).
# Owner asked for positive, improvement-first framing. Numbers are measured
# (gate Run 6 vs rc.99 on the 1 GB corpus, and each fix's PR); the post
# describes the gate and does not claim a release passed it.
"blog/autoindex-search-performance-improvements.html": dict(
label="Performance", kind="article",
title="How your field reports made XERJ autoindex faster",
description="Index size per indexed byte fell from 3.50x to 1.80x of source: what we improved in autoindex and search, how the performance gate works, and who helped.",
published="2026-10-26", updated="2026-10-26",
og_image="og/blog-autoindex-search-performance-improvements.png",
og_image_alt="Cover: “Faster autoindex” in display type beside the post’s square-matrix art, labelled 28 people + 217 agent reports, 454 indices, 32, and ES 9.05 · Lucene 25.4 MiB/s. The art: 28 filled squares and 217 small empty rings scattered on grid paper, the filled ones joined by hairlines to a wide block of 454 empty cells, beside 32 yellow cells at the same pitch, then a gate of ink posts around two grey-blue columns and one empty yellow cell at its base",
),
"blog/rc99-smart-autoindex-harness-tools.html": dict(
label="rc.99", kind="article",
title="rc.99: smart, resource-aware autoindex and more harness tools",
description="rc.99 is yours: 116 merged changes and the 20 community members behind them. A laptop stays usable, questions come back cited, and two hard limits stay open.",
published="2026-10-24", updated="2026-10-24",
og_image="og/blog-rc99-smart-autoindex-harness-tools.png",
og_image_alt="Cover: “rc.99” in display type beside the post’s square-matrix art, labelled 8 GiB and 2 GiB. The art: eight large squares for the 8 GiB tier, the first two yellow for the 2 GiB memtable budget, beside eight small squares for the 8 MiB document ceiling and one outlined square for JSON streamed past 64 MiB",
),
"blog/corpus-hub-v2.html": dict(
label="Corpus Hub v2", kind="article",
title="XERJ Corpus Hub v2: ask a question, get a local answer",
description="Corpus Hub v2 routes a plain question to one signed corpus, pulls only that pack (lz4: 4.4 MB) and answers locally: 1.7 to 2.3 s cold, under 60 ms warm.",
published="2026-10-19", updated="2026-10-28",
og_image="og/blog-corpus-hub-v2.png",
og_image_alt="Cover: “Corpus Hub v2” in display type beside the post’s square-matrix art, labelled with its figures. The art: a faint wall of 251 small tiles above 14 larger square-matrix tiles; a yellow route runs from the top into one yellow tile and down to a framed node",
),
"blog/index.html": dict(
label="Blog", kind="collection",
title="XERJ.ai — Blog: the engineering log",
description="100 contributors behind 12,000 stars in 12 days: the XERJ milestone post. Also faster autoindex, the rc.99 launch, Corpus Hub v2 and the exploit hub.",
updated="2026-10-29",
),
"demo/index.html": dict(
label="Real-data demo", kind="software",
title="XERJ.ai — Demo: 60M SSH events vs Elasticsearch",
description="A sales-engineer walkthrough on 60M real loghub OpenSSH events: real curl, real console clicks, each step shown against the Elasticsearch equivalent.",
),
"agent-search/index.html": dict(
label="Agentic search demo", kind="software",
),
"aise-demo.html": dict(
label="Product tour", kind="software",
description="A 90-second single-take product tour of XERJ — one 22 MB binary with no JVM, the Elasticsearch-compatible API, and the live AI-telemetry dashboards.",
),
# ── enterprise funnel ───────────────────────────────────────────────────
"pricing/index.html": dict(
label="Pricing", kind="software",
description="XERJ editions — Community, Professional and Enterprise. Every tier ships the same binary; deployment footprint, support and compliance are what differ.",
),
"security/index.html": dict(
label="Security", kind="software",
description="XERJ's security posture — mutual TLS, RBAC, audit logging, BYOK encryption and certification status for a zero-trust-ready, self-hosted data plane.",
),
"architecture/index.html": dict(
label="Architecture", kind="software",
description="XERJ deployment topologies — single node, HA cluster, multi-region and air-gapped — all from the same ~36 MB binary, with RTO and RPO targets each.",
),
"solutions/index.html": dict(
label="Solutions", kind="collection",
description="XERJ by solution — SIEM, RAG, observability, Elasticsearch replacement, operational intelligence, AI security review and semantic analytics.",
),
"resources/index.html": dict(
label="Resources", kind="collection",
),
"industries/index.html": dict(
label="Industries", kind="collection",
),
"industries/finserv.html": dict(label="Financial services", kind="webpage"),
"industries/healthcare.html": dict(label="Healthcare", kind="webpage"),
"industries/public-sector.html": dict(label="Public sector", kind="webpage"),
"industries/retail.html": dict(label="Retail", kind="webpage"),
# ── brand ───────────────────────────────────────────────────────────────
"brand.html": dict(label="Brand", kind="webpage"),
"brandbook/index.html": dict(label="Brand book", kind="webpage"),
# ── use cases ───────────────────────────────────────────────────────────
# All six shared one title and one description before this pass.
"use-cases.html": dict(
label="Use cases", kind="collection",
title="XERJ use cases: AI security audits and search, measured",
description="Published XERJ use cases cover source-code audits, log analytics and AI search. Read the measured results, limitations and reproduction steps.",
),
"use-cases/ai-search-retrieval.html": dict(
label="AI search & retrieval", kind="webpage", updated="2026-10-24",
title="XERJ.ai — Use case: AI-native search & retrieval",
description="XERJ runs BM25 and exact vector kNN in one query tree and one execution pass, so hybrid retrieval for RAG is a feature rather than an integration project.",
),
"use-cases/elasticsearch-replacement.html": dict(
label="Elastic
2360
search replacement", kind="webpage", updated="2026-10-24",
title="XERJ.ai — Use case: Elasticsearch replacement",
description="A drop-in Elasticsearch replacement — same :9200 API, same query DSL, same client libraries — on one binary instead of a JVM cluster. Measured vs ES 8.13.",
),
"use-cases/operational-intelligence.html": dict(
label="Operational intelligence", kind="webpage", updated="2026-10-24",
title="XERJ.ai — Use case: operational intelligence",
description="Replace Beats, Logstash, Elasticsearch and cold storage with one ingest endpoint, one storage engine and one retention number. Measured against ES 8.13.",
),
"use-cases/security-analytics.html": dict(
label="Security analytics", kind="webpage", updated="2026-10-24",
title="XERJ.ai — Use case: security analytics at scale",
description="SOC search-and-aggregate workloads — filters, term lookups, aggregations — on a single XERJ node instead of a four-node Elasticsearch 8.13 cluster.",
),
"use-cases/unified-observability.html": dict(
label="Unified observability", kind="webpage", updated="2026-10-24",
title="XERJ.ai — Use case: unified observability",
description="Logs, metrics and traces in one engine with one query language and one bill, instead of Loki or Splunk plus Prometheus plus Tempo. Measured vs ES 8.13.",
),
"use-cases/code-security-audit.html": dict(
label="AI code-security audit", kind="webpage", updated="2026-10-24",
title="XERJ.ai — Use case: AI code-security audit",
description="Index a codebase as security facts, compile the rule into a queryable invariant, then read only the violators — ~26,000 tokens to audit WordPress core.",
),
"use-cases/second-brain.html": dict(
label="Second brain", kind="webpage", updated="2026-10-24",
title="XERJ.ai — Use case: a second brain from a folder",
description="xerj brain <folder> turns notes into a bi-temporal, evidence-carrying knowledge graph. Every link shows the text that taught it; 121 files to 364 links.",
),
"use-cases/semantic-analytics.html": dict(
label="Semantic analytics", kind="webpage", updated="2026-10-24",
title="XERJ.ai — Use case: semantic analytics in one call",
description="Retrieval and aggregation in one POST /_search: XERJ runs aggregations over the retrieved top-k neighbour set, so text and vectors are queried together.",
),
# Added by main after this branch was cut; its own <title> and
# <meta name="description"> are unique and correctly sized, so both are
# kept verbatim and only the breadcrumb label and JSON-LD kind are set.
"use-cases/zero-token-ai-search.html": dict(
label="Zero-token AI search", kind="webpage", updated="2026-10-24",
),
# ── case studies ────────────────────────────────────────────────────────
"case-studies.html": dict(
label="Case studies", kind="collection",
description="Three engineering questions answered end to end: a measured WordPress-core security audit, semantic analytics in one request, and Postgres CDC search.",
),
"case-studies/calltree-analytics.html": dict(
label="Semantic analytics in one request", kind="article",
description="kNN and aggregations in a single POST /_search: a deep-research question over 130 synthetic support conversations. A correctness demo, not a benchmark.",
),
"case-studies/daily-dev-postgres-cdc.html": dict(
label="Postgres CDC + hybrid search", kind="article",
description="Logical-replication CDC keeps XERJ in sync with Postgres, and one hybrid RRF query replaces the tsvector plus pgvector two-query merge in your app.",
),
"case-studies/reference-coding.html": dict(
label="Reference-coding for Claude Code", kind="article",
title="XERJ.ai — Case study: reference-coding for agents",
description="Across 13 purpose-built libraries in 5 languages, XERJ retrieval made the same Claude Code model correct where its memory failed — 21/21 against 1/21.",
),
"case-studies/wordpress-security-audit.html": dict(
label="WordPress security audit", kind="article",
title="WordPress security audit with selective source retrieval",
description="A documented WordPress audit used XERJ to select source files for review. The report publishes token accounting, missed cases and its negative result.",
),
"case-studies/xerj-self-audit.html": dict(
label="XERJ audits its own Rust", kind="article",
description="XERJ indexed its own Rust engine as a call graph and found a real unauthenticated stack-overflow DoS for 10,533 tokens instead of 1.8 million. Fix shipped.",
),
# ── docs · hubs ─────────────────────────────────────────────────────────
"docs/index.html": dict(
label="Docs", kind="collection",
description="XERJ documentation — install, configure and operate the engine. Every key, flag and default here comes out of the Rust crates under engine/crates/.",
),
"docs/recipes/index.html": dict(
label="Recipes", kind="collection",
description="Task-oriented XERJ recipes, each verified end to end against a live node — zero-config indexing, retrieval by meaning, agent memory, hybrid ranking.",
),
"docs/agents/index.html": dict(
label="For AI", kind="collection",
description="Build with XERJ: the seven canonical agent operations — autoindex a folder, then search, semantic, vector kNN, hybrid and agent memory on the :9200 API.",
),
# ── docs · start ────────────────────────────────────────────────────────
"docs/quickstart.html": dict(
label="Quickstart", kind="techarticle",
description="Operator quickstart for XERJ's native REST API on port 8080 — create indices with explicit field mappings and bulk-ingest through the turbo-ingest path.",
),
"docs/install.html": dict(
label="Install", kind="techarticle",
description="Install XERJ from its single static binary — system requirements, checksum verification, directory layout and a systemd unit, on Linux, macOS or Windows.",
),
"docs/migration-from-es.html": dict(
label="Migrate from Elasticsearch", kind="techarticle",
description="Point an existing Elasticsearch client at XERJ's port 9200 listener. Exactly what the compatibility layer covers on day one, and what it does not.",
),
# ── docs · reference ────────────────────────────────────────────────────
"docs/cli.html": dict(
label="CLI reference", kind="techarticle",
description="The xerj command line — server flags, config and data-dir overrides, the autoindex and mcp subcommands, and the Corpus Hub commands: ask, hub and code --def.",
),
# Corpus Hub v2 consumer guide (xerj ask / xerj hub / MCP hub tools) plus the
# contribution walkthrough for the corpus-hub branch, whose
# tools/xerj-code/hub/CONTRIBUTING.md stays the source of record for rules.
"docs/corpus-hub.html": dict(
label="Corpus Hub", kind="techarticle",
description="Ask a pinned, signed, pre-indexed corpus with xerj ask: pulling packs, the node that holds them, offline use, the MCP hub tools, exit codes, and how to contribute a corpus.",
),
"docs/config.html": dict(
label="Config TOML", kind="techarticle",
description="The complete xerj.toml surface: every section, key and production-ready default, with runnable examples and the config-file precedence order.",
),
"docs/env.html": dict(
label="Environment variables", kind="techarticle",
description="Every environment variable the xerj binary reads — config path, log filter, server overrides, secrets, client settings — and which one wins over the TOML.",
),
"docs/api-native.html": dict(
label="Native REST API", kind="techarticle",
description="XERJ's native REST API on port 8080 — health, Prometheus metrics, index management, turbo-ingest, explain plans and the log-shaped endpoints.",
),
"docs/api-es-compat.html": dict(
label="ES-compatible API", kind="techarticle",
description="The Elasticsearch-compatible API on port 9200 — supported index, document, bulk and search operations, and the structured error for anything unsupported.",
),
# ── docs · data model ───────────────────────────────────────────────────
"docs/queries.html": dict(
label="Query types", kind="techarticle",
description="Every query type XERJ's parser dispatches, generated from the engine source — match, term, range, bool, knn, semantic, hybrid and the rest of the set.",
),
"docs/analyzers.html": dict(
label="Analyzers", kind="techarticle",
description="How XERJ turns raw text into a token stream: the four built-in analyzers, what each one is correct for, and how default_analyzer picks between them.",
),
"docs/aggregations.html": dict(
label="Aggregations", kind="techarticle",
description="Bucket and metric aggregations in XERJ, with exact counts in the bucket path and no HyperLogLog approximation — plus the deliberate sampling exceptions.",
),
"docs/vectors.html": dict(
label="Vectors & kNN", kind="techarticle",
description="Dense vectors and kNN in XERJ — full-precision storage, a persisted HNSW graph, exact rescoring of candidates, and measured recall against ES 8.13.4.",
),
"docs/ingest.html": dict(
label="Ingest pipelines", kind="techarticle",
description="Four ingest paths into XERJ — turbo-ingest NDJSON bulk, the ES bulk API, autoindex and streaming — with parsers running in-process with the writer.",
),
# ── docs · engine ───────────────────────────────────────────────────────
"docs/storage.html": dict(
label="Storage & WAL", kind="techarticle",
description="XERJ's on-disk layout: one append-only WAL per index and immutable three-file segments, all mmap'd so reads come straight out of the OS page cache.",
),
"docs/compression.html": dict(
label="Compression & encodings", kind="techarticle",
description="XERJ's two compression layers — per-field column encodings chosen at write time and Zstandard block compression — measured 1.61x smaller on disk than ES.",
),
"docs/clustering.html": dict(
label="Clustering", kind="techarticle",
description="XERJ ships an embedded Raft ring — leader election, heartbeats, and an authenticated cluster transport in one binary. What replicates today and what is roadmap, stated plainly.",
),
# ── docs · operate ──────────────────────────────────────────────────────
"docs/security.html": dict(
label="Auth & TLS", kind="techarticle",
description="Auth and TLS in XERJ: API-key authentication enabled by default under [auth], the accepted header schemes, and certificate configuration under [tls].",
),
"docs/backup-restore.html": dict(
label="Backup & restore", kind="techarticle",
description="Segments are immutable and the WAL append-only, so a XERJ backup is a filesystem copy — hot backup with rsync, the restore procedure, and the caveats.",
),
"docs/operations.html": dict(
label="Running in production", kind="techarticle",
description="Running XERJ in production — the systemd unit, non-root user setup, readiness, config reload, log levels, health and metrics, and capacity planning.",
),
"docs/metrics.html": dict(
label="Metrics", kind="techarticle",
description="GET /v1/metrics returns Prometheus text format. Every counter, histogram and gauge XERJ emits, with per-index labels wherever they make sense.",
),
"docs/troubleshooting.html": dict(
label="Troubleshooting", kind="techarticle",
description="The short list of things that go wrong in a XERJ deployment — the symptom, the metric and log line that confirm it, and the config key you turn.",
),
"docs/upgrades.html": dict(
label="Upgrades", kind="techarticle",
description="XERJ follows SemVer: patch releases are drop-in, minor releases add fields, major releases may change the on-disk format. Single-node and cluster steps.",
),
# ── docs · playbooks ────────────────────────────────────────────────────
"docs/playbooks/full-text.html": dict(
label="Playbook · Full-text search", kind="techarticle",
description="Full-text search on XERJ — BM25 scoring, analyzers, highlighters and query-time boosting, with the schema, the ingest command and the queries to copy.",
),
"docs/playbooks/log-analytics.html": dict(
label="Playbook · Log analytics", kind="techarticle",
description="Ingest Nginx, JSON, syslog or OTLP logs at line rate and query them in milliseconds — the schema, the ingest command and the queries to copy.",
),
"docs/playbooks/observability.html": dict(
label="Playbook · Observability", kind="techarticle",
description="Metrics, traces and logs as one queryable store — OTLP in, Prometheus text out, traces as a connected graph. Replaces Splunk plus Prometheus plus Tempo.",
),
"docs/playbooks/siem.html": dict(
label="Playbook · SIEM", kind="techarticle",
description="Security analytics on an event stream — auth logs, firewall drops, process executions, DNS. Schema, ingest command, and five core detection queries.",
),
"docs/playbooks/vector-search.html": dict(
label="Playbook · Vector & RAG", kind="techarticle",
description="Vector and RAG retrieval on one box — the dense_vector schema, hybrid BM25 plus kNN fusion, and dashboards for recall, latency and cache hit rate.",
),
# ── docs · agents ───────────────────────────────────────────────────────
"docs/agents/quickstart.html": dict(label="Agent quickstart", kind="techarticle"),
"docs/agents/endpoints.html": dict(label="Endpoint contract", kind="techarticle"),
# ── docs · recipes ──────────────────────────────────────────────────────
# Added by main after this branch was cut; title and description kept
# verbatim (both unique and correctly sized) — label and kind only.
"docs/recipes/air-gapped-deployment.html": dict(
label="Air-gapped deployment", kind="techarticle",
),
"docs/recipes/zero-config-autoindex.html": dict(
label="Zero-config autoindex", kind="techarticle",
title="XERJ · Docs · Recipe · Zero-config autoindex",
description="One command — xerj autoindex <folder> — turns JSONL, CSV, SQLite, PDF, DOCX, HTML and log files into typed, queryable indices with zero configuration.",
),
"docs/recipes/document-folder-index.html": dict(
label="In
A957
dex a folder of documents", kind="techarticle",
title="XERJ · Docs · Recipe · Index a document folder",
description="Point an agent at a recursive folder of PDF, DOCX, HTML, Markdown and TXT files: one xerj-index pass extracts, chunks and auto-embeds every one of them.",
),
"docs/recipes/semantic-search-rag.html": dict(
label="Semantic search & RAG", kind="techarticle",
description="Semantic search and RAG retrieval with XERJ — map a field as semantic_text, auto-embed on ingest, retrieve by meaning, with no separate vector database.",
),
"docs/recipes/hybrid-search.html": dict(label="Hybrid search", kind="techarticle"),
"docs/recipes/vector-search-knn.html": dict(
label="Vector search · kNN", kind="techarticle",
description="Vector similarity search in XERJ — dense_vector fields, Elasticsearch-8 knn queries served by HNSW when unfiltered, and pre-filtering with bool filter.",
),
"docs/recipes/vector-quantization.html": dict(
label="Vector quantization", kind="techarticle",
description="Opt a dense_vector field into scalar8 quantization: kNN scored from 1-byte-per-dimension codes at recall@10 = 0.998, originals still returned in _source.",
),
"docs/recipes/passage-retrieval.html": dict(
label="Passage retrieval", kind="techarticle",
description="Map a field as semantic_text so XERJ embeds every passage on ingest, and a long, multi-topic document competes on its single best-matching section.",
),
"docs/recipes/agentic-memory.html": dict(
label="Agent memory", kind="techarticle",
description="Give an AI agent long-term memory with XERJ's namespaced offline memory API — store, recall by vector or text, filter by metadata, forget, stay isolated.",
),
"docs/recipes/log-analytics.html": dict(
label="Log analytics", kind="techarticle",
description="Bulk-ingest application logs into XERJ and answer error-rate, latency-percentile and top-service questions with Elasticsearch-compatible aggregations.",
),
"docs/recipes/anomaly-detection.html": dict(
label="Anomaly detection", kind="techarticle",
description="Detect metric spikes on demand with XERJ's Elasticsearch-shaped anomaly detector: a moving-window z-score that flags the bucket where a metric broke out.",
),
"docs/recipes/continuous-anomaly-datafeeds.html": dict(
label="Continuous anomaly datafeeds", kind="techarticle",
description="Run an Elasticsearch _ml datafeed on XERJ to score a live metric index continuously — one pass now, then a timer that appends only newly flagged buckets.",
),
"docs/recipes/migrate-from-elasticsearch.html": dict(
label="Migrate from Elasticsearch", kind="techarticle",
description="Migrate to XERJ by pointing your client's base URL at it — XERJ speaks the Elasticsearch REST wire protocol on 9200, so queries and bulk loads survive.",
),
# ── generated article hubs ─────────────────────────────────────────────
"answers/index.html": dict(
label="Answers", kind="collection",
title="XERJ answers: how AI agents search files, code and logs",
description="Short, direct answers on how AI agents search local files, code, logs and PDFs with XERJ. Each one has the commands to run and links to its evidence.",
),
"compare/index.html": dict(
label="Comparisons", kind="collection",
title="XERJ comparisons — retrieval choices",
description="Focused comparisons for teams choosing a search, vector and agent-memory architecture, with evidence links and clear trade-offs.",
),
}
#: Directory -> (breadcrumb label, deployed URL of the hub page that covers it).
#:
#: Every URL here must be a real page in the repo (seo_lint enforces it).
#: `docs/playbooks` is deliberately absent: there is no playbooks index page,
#: so that level is skipped rather than pointed at a URL that 404s.
SECTION_HUBS: dict[str, tuple[str, str]] = {
"docs": ("Docs", "/docs/"),
"docs/recipes": ("Recipes", "/docs/recipes/"),
"docs/agents": ("For AI", "/docs/agents/"),
"use-cases": ("Use cases", "/use-cases"),
"case-studies": ("Case studies", "/case-studies"),
"industries": ("Industries", "/industries/"),
"blog": ("Blog", "/blog/"),
"answers": ("Answers", "/answers/"),
"compare": ("Comparisons", "/compare/"),
}
#: JSON-LD @type per `kind`.
KIND_TYPE = {
"software": "SoftwareApplication",
"collection": "CollectionPage",
"techarticle": "TechArticle",
"article": "Article",
"webpage": "WebPage",
}
#: og:type per `kind`. ogp.me only defines a small vocabulary; "article" for
#: anything article-shaped, "website" for everything else.
KIND_OG_TYPE = {
"software": "website",
"collection": "website",
"techarticle": "article",
"article": "article",
"webpage": "website",
}
#: kinds whose JSON-LD must carry headline/datePublished/dateModified.
ARTICLE_KINDS = frozenset({"techarticle", "article"})
def entry(rel: str) -> dict[str, str]:
try:
return PAGES[rel]
except KeyError:
# Generated article pages are described by their Markdown frontmatter.
# Import lazily so the hand-written page table remains cheap and the
# parser does not create an import cycle during tool startup.
from article_data import load_for_rel
article = load_for_rel(pathlib.Path(__file__).resolve().parents[2], rel)
if article is not None:
return article.head_meta()
raise KeyError(
f"{rel} has no entry in scripts/seo/pagedata.py or content/ — add "
f"a page entry or an article source"
) from None