Wikilink index, backlinks API, and phantom-link resolution #47

Closed
opened 2026-07-04 14:52:22 +02:00 by fable-5 · 1 comment
Collaborator

Context

Backlinks need a server-side index maintained on every content change (data-model.md page_links).

Scope

Extend the content-derivation pipeline (#35) to extract wikilinks into page_links (targets resolved by slug within the pond; unresolved targets stored as phantom rows with target_slug); endpoints: GET /pages/:id/backlinks (pages linking here, permission-filtered), GET /ponds/:id/phantom-links (aggregated missing targets); on page create/rename, resolve/re-point matching phantom rows.

Acceptance criteria

  • editing a page updates its outgoing link rows within the persistence debounce (test)
  • backlinks endpoint lists only pages the requester may read
  • creating a page with a phantom-linked slug resolves those links (fixture e2e)
  • renaming a page slug re-points existing links (they store page ids, not slugs — verify)

Technical notes

  • data-model.md (page_links), realtime-collaboration.md (derivation hook).

Dependencies

Depends on #35, #46.

Size: ~1 day


Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add de and en), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.

## Context Backlinks need a server-side index maintained on every content change (data-model.md `page_links`). ## Scope Extend the content-derivation pipeline (#35) to extract wikilinks into `page_links` (targets resolved by slug within the pond; unresolved targets stored as phantom rows with `target_slug`); endpoints: `GET /pages/:id/backlinks` (pages linking here, permission-filtered), `GET /ponds/:id/phantom-links` (aggregated missing targets); on page create/rename, resolve/re-point matching phantom rows. ## Acceptance criteria - [ ] editing a page updates its outgoing link rows within the persistence debounce (test) - [ ] backlinks endpoint lists only pages the requester may read - [ ] creating a page with a phantom-linked slug resolves those links (fixture e2e) - [ ] renaming a page slug re-points existing links (they store page ids, not slugs — verify) ## Technical notes - data-model.md (`page_links`), realtime-collaboration.md (derivation hook). ## Dependencies Depends on #35, #46. **Size**: ~1 day --- *Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add `de` **and** `en`), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.*
fable-5 added this to the M4 — Organization & search milestone 2026-07-04 14:52:22 +02:00
fable-5 added the
backend
label 2026-07-04 14:52:22 +02:00
Collaborator

Implemented in 14e69b3 (Claude Opus 4.8). Pipeline green (CI incl. all e2e packs; CD deploy Test → smoke → promote Int).

Live-verified on Test: GET /ponds/:id/phantom-links → 200 [] and GET /pages/:id/backlinks → 200 [] — the page_links index and its queries run against the migrated stage DB. Index writing, resolution, and permission filtering are covered by db tests (collab + api) and exercised end to end by the #48 e2e (a link created in the editor shows up as a backlink).

Acceptance criteria

  • editing a page updates its outgoing link rows within the persistence debounce: the collab store hook rewrites page_links for the source in the same transaction as the content cache (collab db test).
  • backlinks endpoint lists only pages the requester may read: wikilinks resolve within a pond, so seeing the pond (InterimAccessService) is the read right; an outsider gets 404 (api db test).
  • creating a page with a phantom-linked slug resolves those links (api db test; #48 e2e create-from-missing-pages).
  • renaming a page slug re-points existing links (they store page ids, not slugs — verified): backlinks store to_page_id, so a later rename of the target keeps them connected; renaming to a phantom slug resolves those rows.

Notes

  • PageLink(from_page_id, to_page_id?, target_slug), unique per (from, slug); cascade on source purge, set-null on target purge.
  • deriveContentFromDoc now also returns wikilinkSlugs (shared extractWikilinkSlugs); one index row per distinct slug, resolved to a same-pond page or null (phantom).
  • SQL gotcha fixed: unnest(...) AS slug collided with pages.slug ("column reference slug is ambiguous") — the unnest column is aliased distinctly.

Next M4 issue: #48 (backlinks panel and phantom-pages view) — already implemented on top of this.

Implemented in `14e69b3` (Claude Opus 4.8). Pipeline green (CI incl. all e2e packs; CD deploy Test → smoke → promote Int). **Live-verified on Test**: `GET /ponds/:id/phantom-links` → 200 `[]` and `GET /pages/:id/backlinks` → 200 `[]` — the `page_links` index and its queries run against the migrated stage DB. Index writing, resolution, and permission filtering are covered by db tests (collab + api) and exercised end to end by the #48 e2e (a link created in the editor shows up as a backlink). **Acceptance criteria** - [x] editing a page updates its outgoing link rows within the persistence debounce: the collab store hook rewrites `page_links` for the source in the same transaction as the content cache (collab db test). - [x] backlinks endpoint lists only pages the requester may read: wikilinks resolve within a pond, so seeing the pond (InterimAccessService) is the read right; an outsider gets 404 (api db test). - [x] creating a page with a phantom-linked slug resolves those links (api db test; #48 e2e create-from-missing-pages). - [x] renaming a page slug re-points existing links (they store page ids, not slugs — verified): backlinks store `to_page_id`, so a later rename of the target keeps them connected; renaming *to* a phantom slug resolves those rows. **Notes** - `PageLink(from_page_id, to_page_id?, target_slug)`, unique per (from, slug); cascade on source purge, set-null on target purge. - `deriveContentFromDoc` now also returns `wikilinkSlugs` (shared `extractWikilinkSlugs`); one index row per distinct slug, resolved to a same-pond page or null (phantom). - SQL gotcha fixed: `unnest(...) AS slug` collided with `pages.slug` ("column reference slug is ambiguous") — the unnest column is aliased distinctly. Next M4 issue: #48 (backlinks panel and phantom-pages view) — already implemented on top of this.
Sign in to join this conversation.
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: stwaidele/dorfteich#47
No description provided.