🔗 Cross References for Flarum 2 — GitHub-style #42 between discussions
Hey folks 👋 — really excited to drop this one. Cross References brings GitHub-style discussion linking to Flarum 2. Type #42 in any post and it becomes a rich, live, clickable chip — the target picks up a backlink, the author gets notified, and a sidebar widget surfaces every inbound reference.





Built as a more robust Flarum 2 successor to club-1/flarum-ext-cross-references — rename-safe rendering, visibility-aware backlinks, batched queries, and a references:N search filter.
✨ What you get
1️⃣ Inline references that stay fresh
Type #42 in a post → it renders as a tappable chip showing the target discussion's current title, pulled live from the DB on every render. Rename a discussion and every chip pointing at it updates automatically — no chore:reparse, no cache bust, no stale titles. 🎯

2️⃣ Bidirectional backlinks as first-class event-posts
When you reference another discussion, a "Referenced from #X" event-post appears in the target's stream — sitting alongside core events like "renamed" or "locked", and slotting into your existing moderation, search, and reply pipelines.

3️⃣ "Inbound references" sidebar widget
Every discussion gets a right-side widget listing recent inbound refs with source author and relative timestamp. One tap to jump to the referring discussion. 📍

4️⃣ In-app notifications for the target author
The recipient sees an alert in their Flarum notification dropdown with the source title resolved live. Self-references are silently skipped — no notification spam when you cite your own thread. 🔕

5️⃣ Copy-to-clipboard #N chip on every post header
Click the little #N chip next to the author name and the canonical reference (#discussionId/pN) lands on your clipboard, ready to paste. No more guessing post numbers. 📋

🧠 Syntax cheat-sheet
| Type this | What renders | Where it points |
#42 | #42 — <Discussion Title> | discussion 42, top of page |
#42/p7 | #42 — <Discussion Title> (post #7) | discussion 42, post 7 |
https://forum.example.com/d/42 | #42 — <Discussion Title> | discussion 42 (URL auto-shortened) |
https://forum.example.com/d/42-some-slug/7 | #42 — <Discussion Title> (post #7) | discussion 42, post 7 |
[click here](https://forum.example.com/d/42) | plain markdown link "click here" | preserved as-is — Markdown wins |
Pasted forum-discussion URLs get rewritten to #N at parse time before storage, so the database only ever sees a single canonical representation. URLs you explicitly wrap in [label](url) are left alone — the extension assumes you meant the custom label intentionally.
📝 A real-world example
You're replying to a support thread and want to link a related bug report plus a specific post inside a roadmap discussion:
You type:
Thanks for the report! This looks like the same regression as #42, and the
fix is being planned in #58/p3. I'll close this as a duplicate once #58/p3
lands.
Readers see:
Thanks for the report! This looks like the same regression as #42 Login button stops responding on mobile, and the fix is being planned in #58 Q3 roadmap (post #3). I'll close this as a duplicate once #58 Q3 roadmap (post #3) lands.
Meanwhile, in discussions #42 and #58:
- 🪧 A new event-post appears:
you referenced this from #137
- 🔔 The authors of #42 and #58 each get an in-app notification
- 📑 The sidebar of both discussions now lists #137 as an inbound reference
🔐 Visibility — references respect who can see what
References are visibility-scoped on every read path. If a viewer can't see the target (private thread, restricted tag, soft-deleted), the chip renders as a muted #42 placeholder with no title leaked. Backlink event-posts inherit the target's normal visibility — a backlink in a private thread doesn't reveal the source thread's existence to non-members.
| Viewer can see target? | Chip renders as |
| ✅ Yes | #42 — <Title> — full chip, clickable |
| ❌ No (private, restricted, deleted) | #42 — muted placeholder, no title |
The sidebar widget applies the same scope: inbound refs from sources you can't view simply don't appear.
🔍 Search filter
Find every discussion whose posts reference discussion #N:
filter[references]=42
Stack it with other filters:
filter[references]=42&filter[tag]=support
Negation works too — -references:42 excludes them. Value is cast to (int) before reaching SQL, so no wildcard / injection surface.
🚀 Quick start
composer require ernestdefoe/cross-references
php flarum migrate
php flarum cache:clear
Then in Admin → Extensions → Cross References:
- Enable the extension
- Grant the
Use cross-references in posts permission to whichever groups should be allowed to type #42
- Optionally tweak the three toggles below
That's it. References start working immediately on new and edited posts.
⚙️ Admin settings
| Setting | Default | Effect |
| Show inline references in posts | ✅ on | Render #42 / pasted-URL refs as rich chips. Off = raw #42 text. |
| Create backlink event-posts in target discussions | ✅ on | Insert "Referenced from #X" event-posts. Off = quiet target threads. |
| Notify the target discussion's author | ✅ on | Send an in-app alert when someone references their discussion. |
🆚 vs. club-1/flarum-ext-cross-references
| Concern | club-1/cross-references | ernestdefoe/cross-references |
| Flarum version | 1.x | 2.x |
| Discussion title in chip | Baked into post content at save | Resolved live from DB on every render |
| Behavior on target rename | Requires chore:reparse | Automatic — chips read current title |
| Behavior on target delete | Plain link to nothing | Muted #N placeholder |
| Visibility-aware | ❌ (leaks restricted titles) | ✅ whereVisibleTo($actor) everywhere |
| Notifications to target author | ❌ | ✅ |
| Inbound-refs sidebar widget | ❌ | ✅ |
Search filter (references:N) | ❌ | ✅ |
| Listener failure blocks post save | Possible | ❌ caught + logged, never blocks |
| Storage-layer dedupe | None | ✅ unique index |
🏗️ Under the hood (for the curious)
- Companion table
cross_references — no columns added to core posts / discussions / users
- One DB call per render to resolve all titles in visible posts — no N+1, regardless of how many refs are on screen
- No titles in post content — only IDs; rename flows through automatically
- Wrapped in try/catch + PSR-3 logging — a ref bug can never block a post save
- First-class
AbstractEventPost for backlinks — slots into moderation, search, and the standard reply pipeline
- Polymorphic blueprint with typed constructor +
TYPE constant — clean typeForModel() resolution
Full architecture writeup in the README.
🔗 Links
💬 Feedback welcome
This is fresh — v2.0.0 just shipped. Bug reports, feature ideas, edge cases I missed, weird visibility scenarios on your forum, all very welcome. Drop a thread on ernestdefoe.online or open a GitHub issue — whichever you prefer.
Tag any support threads with #cross-references so other users can find them later. 🙏
Happy referencing! 🎉
— Ernest