Jeroen1991z builds a citation side panel for Dutch case law

Click a citation in chat, and the source opens beside the answer with the quoted passage highlighted in place.

searchchat-ui

Most legal AI tools cite their sources and stop there - leaving the lawyer to open a new tab, find the judgment, and hunt for the line. Jeroen1991z's Dutch fork closes that loop. Click a citation and a side panel opens: case law arrives with a button to load the full ruling, legislation loads the article automatically, and the exact passage the model quoted is highlighted and scrolled into view.

The trick is that models paraphrase. The reader handles that with layered matching - exact, case-insensitive, ellipsis-aware, and a fuzzy fallback that still finds the passage when a word or two has shifted. A large share of the work is wrestling with the Dutch government's legislation feeds, which are notoriously fiddly: the team layers several resolution strategies to land on the right article. Useful, but tightly bound to Dutch sources.

So what Worth a look for anyone building jurisdiction-specific legal chat where citation trust matters - but the plumbing is Dutch-specific and won't lift cleanly elsewhere.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

18 commits from Jeroen1991z/mikeNL, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
007c3d05 Add legal citation side panel with full text and exact-match highlight Claude 2026-05-05 ↗ GitHub
commit body
Clicking a case law or legislation citation now opens a side panel instead
of a new browser tab. The panel shows the quoted passage immediately, with
a button to load the full arrest text or article text from rechtspraak.nl /
wetten.overheid.nl. If the exact quote is found in the full text it is
highlighted in yellow and scrolled into view; if not, a notice is shown.

Also cleans up mvt/internet fields from route-level search_sources types.

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
fe1f464a Format case law text as markdown in side panel Claude 2026-05-05 ↗ GitHub
commit body
- Add xmlToMarkdown() converter that preserves structure from
  rechtspraak.nl XML: section headings (##), r.o. numbers (**3.1**),
  paragraph breaks, emphasis, lists, footnotes
- fetchCaseLaw() accepts display option: markdown at 80K chars for
  side panel, plain text at 15K chars for LLM tool calls
- LegalPanel uses ReactMarkdown + rehype-raw to render the markdown
- Exact-match quote is wrapped in <mark> before rendering, highlighted
  in yellow; panel scrolls to it automatically

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
e1d9a9e0 Fix metadata leak and paragraph spacing in legal side panel Claude 2026-05-05 ↗ GitHub
commit body
- Fix uitspraak/conclusie regex to match tags with attributes (e.g.
  id="..."), preventing fallback to full XML which leaked Dublin Core
  metadata into the displayed text
- When fallback is unavoidable, strip known metadata elements first
- Remove prose-p:my-2 override (was shrinking default spacing); use
  prose-p:mb-4 for more breathing room between paragraphs
- Increase heading top margins for clearer section separation

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
dbd25688 Use [...] omission notation and range-based quote matching Claude 2026-05-05 ↗ GitHub
commit body
System prompt now instructs the model to quote exact words and use [...]
for omissions within a passage (standard legal citation practice).

Quote matching now tries: exact → case-insensitive → [...] split.
For [...] quotes, the highlighted span covers from the first segment
to the last, including the omitted middle text. The 'not found' notice
only appears if none of the three strategies can locate the passage.

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
4763f72a Auto-load legislation in side panel; fix 503 via SRU XML lookup Claude 2026-05-05 ↗ GitHub
commit body
Legislation articles now load automatically when the panel opens -
no button needed (articles are short). Case law keeps the load button.

Fix HTTP 503: stop scraping wetten.overheid.nl HTML (which blocks bots).
Instead resolve the current XML URL via the zoekservice.overheid.nl SRU
endpoint (same one used for legislation search), falling back to the
manifest on repository.officiele-overheidspublicaties.nl.

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
55c579b5 Fix BWB XML URL lookup: use 'any' operator and filter by bwb_id Claude 2026-05-05 ↗ GitHub
commit body
The SRU identifier field contains a full URL so exact match (=) never
finds anything. Switch to 'any' (contains) and try both
overheidbwb.identifier and dcterms.identifier. Also verify each record
actually matches the requested bwb_id before accepting its locatie_toestand.

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
b86bb894 Thread xml_url through legislation citation pipeline to fix article loading Claude 2026-05-05 ↗ GitHub
commit body
Passes the xml_url (resolved during search_legislation) directly to
fetchLegalArticle, bypassing the unreliable SRU secondary lookup that
was causing 503/empty-result errors when opening legislation in the side panel.

Also adds xml_url field to MikeCitationAnnotation type so the frontend
can forward it in the /legal/article API call.

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
9e4acb5f Fix BWB manifest parsing by stripping namespace prefixes Claude 2026-05-05 ↗ GitHub
commit body
The manifest XML may use ns2:, ns3:, or other prefixes depending on
the server response. Hardcoding ns2: caused silent failures that left
targetUrl empty, resulting in the 'Kon BWBR niet ophalen' error.
Now strips all namespace prefixes before parsing so extractTag works
regardless of prefix.

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
2357d694 Improve fetchLegislationArticle: better errors, fix book:article number format Claude 2026-05-05 ↗ GitHub
commit body
- Error messages now say exactly which step failed (SRU/manifest/HTTP status)
  so failures are diagnosable from the backend logs
- Handle book:article format like 6:162 by also searching for just the
  article number (162) since BWB XML headers only contain the plain number
- Manifest HTTP error status is now surfaced instead of silently swallowed

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
ad53e9c4 Fix LegalPanel: eliminate stale-closure bug that blocked auto-load Claude 2026-05-05 ↗ GitHub
commit body
Two effects with identical deps caused a race: the second effect read
fullText from the prior render (non-null when switching citations),
so the !fullText guard silently skipped loadFullText() and left the
panel stuck with no text, no button, and no error shown.

Fix: merge into one effect that resets state and triggers the
legislation fetch inline (with cancel flag). Rename loadFullText to
loadCaseLaw since it now only handles the manual case-law button.

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
83a2844c Add source link at bottom of full text; fix manifest entry filter Claude 2026-05-05 ↗ GitHub
commit body
LegalPanel: after the full text, show a text link to the source
website (rechtspraak.nl for case law, wetten.overheid.nl for legislation).

Manifest: the GeldigTot='9999' filter was too strict - actual manifests
may use different end dates. Now filters by GeldigVanaf <= today instead,
picking the most recent version that has come into effect.

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
90a71eb5 Make title a clickable link; fix manifest URL extraction Claude 2026-05-05 ↗ GitHub
commit body
LegalPanel: title in header is now a clickable link with ExternalLink
icon, opening the source site directly. Bottom link removed.

Manifest: drop element-based parsing entirely. The manifest uses a
<work> root element with a completely different structure than assumed.
Instead, regex-extract all repository XML URLs containing the bwb_id
and a date segment, then pick the most recent one before today.
This works regardless of the manifest XML schema.

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
7161f2c8 Fix same-arrest scroll; robust BWB XML URL resolution Claude 2026-05-05 ↗ GitHub
commit body
LegalPanel: add citation.quote to scroll effect deps so clicking a
different footnote in the same open arrest scrolls to the new highlight.

Backend: multi-strategy BWB XML URL resolution:
1. Full repository URLs in manifest XML
2. Filename pattern BWBR..._YYYY-MM-DD.xml in manifest (manifest may store
   filenames rather than full URLs)
3. Any YYYY-MM-DD date in manifest → construct URL and try top 3
4. Last resort: HEAD-probe the last 8 monthly start dates + prev year

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
ee87b225 Format article lids properly; remove broken wetten.overheid.nl link Claude 2026-05-05 ↗ GitHub
commit body
Backend: replace plain-text article extraction with XML-aware parsing.
findArticleXml() locates the <artikel> by matching <nr>. formatArticleXml()
extracts each <lid> with its <lidnr> number, renders them as separate
markdown paragraphs (**1** text\n\n**2** text) so lids appear with
blank lines between them as on wetten.overheid.nl.

Frontend: clickable title link only shown for case law (rechtspraak.nl
URLs are reliable); legislation title is plain text since the article
fragment URL format cannot be reliably constructed.

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
fa78a296 Fix findArticleXml: walk backwards from <nr> to enclosing <artikel> Claude 2026-05-05 ↗ GitHub
commit body
The old regex matched from the very first <artikel> in the file up to
the target <nr>, capturing all preceding articles. New approach:
1. Find <nr>N</nr> position
2. lastIndexOf('<artikel') backwards to get the correct opener
3. Track nesting depth forward to find the matching </artikel>

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
6083347c Remove duplicate article subtitle for legislation in side panel Claude 2026-05-05 ↗ GitHub
https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
b52b6ecb Fix stale side panel tabs persisting across chat navigation Claude 2026-05-06 ↗ GitHub
ChatView had no key prop, so React reused the same instance when
navigating between chats - leaving old tabs mounted. Adding key={id}
forces a clean remount on every chat change.

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex
35da3e83 Add fuzzy word-overlap fallback for passage highlighting Claude 2026-05-06 ↗ GitHub
commit body
When the AI slightly paraphrases a quote (e.g. 'tussen' vs 'van'),
the exact and case-insensitive searches fail. A sliding window over
word tokens now finds the best-matching span and highlights it if
≥75% of words overlap, covering single-word substitutions.

https://claude.ai/code/session_016JxWnFc3baeDgkzDgxY6ex

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-17.md from inside the repo you want the changes in.

⬇ Download capture-thread-17.md