PHPackages                             ernestdefoe/cross-references - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. ernestdefoe/cross-references

ActiveFlarum-extension[Utility &amp; Helpers](/categories/utility)

ernestdefoe/cross-references
============================

GitHub-style cross-references between discussions and posts on Flarum 2: #123 inline references, pasted-URL detection, bidirectional backlinks, optional notifications, search filter, and a sidebar widget of inbound references.

3.0.0(2w ago)030↑200%MITPHPPHP ^8.2CI passing

Since May 17Pushed 1w agoCompare

[ Source](https://github.com/ernestdefoe/cross-references)[ Packagist](https://packagist.org/packages/ernestdefoe/cross-references)[ RSS](/packages/ernestdefoe-cross-references/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (3)Dependencies (1)Versions (4)Used By (0)

Cross References
================

[](#cross-references)

[![Floxum](https://camo.githubusercontent.com/f4a68cd5290e60b855d928c9a0670e3953069a583748c69888e96d754c607eb8/68747470733a2f2f666c6f78756d2e636f6d2f657874656e73696f6e2f65726e6573746465666f652f63726f73732d7265666572656e6365732f62616467652f6e616d65)](https://floxum.com/extension/ernestdefoe/cross-references)[![Version](https://camo.githubusercontent.com/a9d2b1803b2bcf30fe99b3a3e006014e5686c5214355360c5427cfbdac22b111/68747470733a2f2f666c6f78756d2e636f6d2f657874656e73696f6e2f65726e6573746465666f652f63726f73732d7265666572656e6365732f62616467652f686967686573742d76657273696f6e)](https://floxum.com/extension/ernestdefoe/cross-references)[![Downloads](https://camo.githubusercontent.com/38a59c621c84d32084e95c9967aaf7fe59d6bff85751a7dfd173c6359ba6665d/68747470733a2f2f666c6f78756d2e636f6d2f657874656e73696f6e2f65726e6573746465666f652f63726f73732d7265666572656e6365732f62616467652f646f776e6c6f616473)](https://floxum.com/extension/ernestdefoe/cross-references)[![Review](https://camo.githubusercontent.com/51e4b45a825a81c3789688a1dd8fa0976c8363fbfa3bff13c8cc99eda1d93084/68747470733a2f2f666c6f78756d2e636f6d2f657874656e73696f6e2f65726e6573746465666f652f63726f73732d7265666572656e6365732f62616467652f726576696577)](https://floxum.com/extension/ernestdefoe/cross-references)[![License](https://camo.githubusercontent.com/e79bd4f4b9a87e00afbf7c71e6ed4bad669fc5972a3c010d056f447bb0db7c0d/68747470733a2f2f666c6f78756d2e636f6d2f657874656e73696f6e2f65726e6573746465666f652f63726f73732d7265666572656e6365732f62616467652f6c6963656e7365)](https://floxum.com/extension/ernestdefoe/cross-references)

[![Flarum 2.0](https://camo.githubusercontent.com/478618f76f079b93e9f3c083a22f2c9424466c5407ee898cddad0faf286c3d80/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f466c6172756d2d253545322e302d6f72616e6765)](https://flarum.org/)[![License: MIT](https://camo.githubusercontent.com/08cef40a9105b6526ca22088bc514fbfdbc9aac1ddbf8d4e6c750e3a88a44dca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e737667)](LICENSE.md)

GitHub-style cross-references between discussions and posts for **Flarum 2**. Type `#42` in any post to link the current discussion to discussion #42 — the target picks up a backlink, the target's author can be notified, and a sidebar widget surfaces every inbound reference.

Built as a more robust Flarum 2 successor to [`club-1/flarum-ext-cross-references`](https://github.com/club-1/flarum-ext-cross-references): rename-safe rendering, visibility-aware backlinks, batched queries, and a `references:N` search filter — all wired through Flarum 2's first-party patterns.

---

What it does, at a glance
-------------------------

[](#what-it-does-at-a-glance)

### 1. Inline references render as rich chips with the live discussion title

[](#1-inline-references-render-as-rich-chips-with-the-live-discussion-title)

Type `#42` in a post and it renders as a tappable chip showing the **target discussion's current title** — pulled fresh from the database on every render, so renames flow through automatically and you never read a stale title.

[![Inline cross-reference chips rendered inside a post showing #1 Mosaic and #1 Mosaic (post #3)](docs/images/inline-refs.png)](docs/images/inline-refs.png)

### 2. Target discussions get a backlink as a first-class event-post

[](#2-target-discussions-get-a-backlink-as-a-first-class-event-post)

When a post references another discussion, a small "Referenced from #X" event-post appears in the target's stream — slot-in to your existing moderation history, search index, and reply pipeline.

[![Backlink event-post saying "tester referenced this from #6"](docs/images/backlink-event-posts.png)](docs/images/backlink-event-posts.png)

### 3. Every discussion gets an "inbound references" sidebar widget

[](#3-every-discussion-gets-an-inbound-references-sidebar-widget)

A widget on the right-hand side of `DiscussionPage` lists the most recent inbound refs, with the source author and a relative timestamp. Click an item to jump to the referring discussion.

[![Sidebar widget titled "Referenced from 2 discussions" listing Notif test and Cross-ref smoke test](docs/images/sidebar-widget.png)](docs/images/sidebar-widget.png)

### 4. The target's author gets an in-app notification

[](#4-the-targets-author-gets-an-in-app-notification)

The recipient sees the alert in their Flarum notification dropdown, with the source discussion title resolved live. Self-references (when you reference your own discussion) are silently skipped to avoid noise.

[![Notification dropdown showing "Referenced your discussion from Notif test"](docs/images/notification.png)](docs/images/notification.png)

### 5. Every post header shows a copy-to-clipboard `#N` chip

[](#5-every-post-header-shows-a-copy-to-clipboard-n-chip)

So you always know which post number to use when typing `#42/pN`. Click the chip and the canonical cross-reference (`#discussionId/pN`) is copied to your clipboard, ready to paste into a reply.

[![Post header showing the # 1 chip between the author name and the timestamp](docs/images/post-number-chip.png)](docs/images/post-number-chip.png)

---

Quick start
-----------

[](#quick-start)

```
composer require ernestdefoe/cross-references
php flarum migrate
php flarum cache:clear
```

Then **enable from the admin panel** under `Extensions → Cross References`, grant the `Use cross-references in posts` permission to whichever groups should be allowed to type `#42`, and you're done — references start working immediately on new and edited posts.

---

Usage — how to reference a discussion or a specific post
--------------------------------------------------------

[](#usage--how-to-reference-a-discussion-or-a-specific-post)

### Syntax cheat-sheet

[](#syntax-cheat-sheet)

Type this in your postWhat rendersWhere it points`#42``#42 — `discussion 42, top of the page`#42/p7``#42 —  (post #7)`discussion 42, post number 7> **Tip:** every post header shows a clickable `#N` chip — click it to copy the canonical `#discussionId/pN` reference straight to your clipboard, so you never have to guess a post number.

| `https://forum.example.com/d/42` | `#42 — ` | discussion 42 (URL auto-shortened) | | `https://forum.example.com/d/42-some-slug/7` | `#42 —  (post #7)` | discussion 42, post 7 | | `[click here](https://forum.example.com/d/42)` | a regular markdown link "click here" | preserved as-is — Markdown wins |

The `#N` form is the canonical one. Pasted forum-discussion URLs get **rewritten to `#N` at parse time** before storage, so the database only ever sees a single representation. URLs you explicitly wrap in markdown brackets (`[label](url)`) are left alone — the extension assumes you meant the custom label intentionally.

### A complete example

[](#a-complete-example)

You're replying to a support discussion and want to link out to two related threads — a bug report and a specific post inside a roadmap discussion:

**Write:**

```
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.
```

**Renders as:**

> 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.

The chips are clickable — they navigate to `/d/42` and `/d/58/3` respectively.

**Meanwhile, in discussions #42 and #58:**

- The bug report (#42) gets a new event-post that reads `you referenced this from #137` (the support thread's ID).
- The roadmap (#58) gets the same event-post pointing back to #137.
- The author of #42 and the author of #58 each get an in-app notification alerting them that their discussion was referenced.
- The sidebar of both discussions now lists `#137` as an inbound reference.

### Visibility — references respect who can see what

[](#visibility--references-respect-who-can-see-what)

References are visibility-scoped on every read path. If a viewer can't see the target (e.g., it's in a restricted tag or a private group), the chip renders as a muted `#42` placeholder with **no title leaked**. Backlink event-posts inherit the target's normal visibility, so a backlink in a private thread doesn't leak the source thread's existence to non-members.

Viewer can see target?Chip renders asYes`#42 — ` — full chip, clickableNo (private, restricted tag)`#42` (muted, `CrossReference--hidden` class)The sidebar widget applies the same scope — inbound refs from sources you can't view simply don't appear.

### Renames are automatic

[](#renames-are-automatic)

Because **titles are never persisted in the post content** — only the discussion ID is — renaming a referenced discussion flows through to every chip on every render. No reindex, no `chore:reparse`, no cache invalidation needed. The next request gets the new title.

### Searching for references

[](#searching-for-references)

Use the `references:N` filter on the discussion list to find every discussion whose posts reference discussion #N:

```
filter[references]=42

```

Either as a URL parameter on `/api/discussions` or via the standard search input if you've wired it (works alongside any other filter):

```
filter[references]=42&filter[tag]=support

```

Negation works too: `-references:42` excludes them.

---

Admin settings
--------------

[](#admin-settings)

Under `Admin → Extensions → Cross References`, three toggles:

SettingDefaultEffect**Show inline references in posts**onRender `#42` / pasted-URL refs as rich chips. Turn off to fall back to raw `#42` text.**Create backlink event-posts in target discussions**onInsert "Referenced from #X" event-posts in the target. Disable for forums that prefer quiet target threads.**Notify the target discussion's author**onSend an in-app alert when someone references the recipient's discussion. The actor never notifies themselves.And one permission:

- `Use cross-references in posts` — gate `#42` rendering at the group level. Useful for forums that want references to work only for trusted members.

---

Architecture
------------

[](#architecture)

### What gets stored

[](#what-gets-stored)

A single companion table `cross_references` with a row per `(source_post, target_discussion, target_post?)` tuple. Unique constraint on that tuple gives storage-layer dedupe — re-saving a post that mentions `#42`three times produces one row.

```
cross_references
  id                     bigint PK
  source_post_id         FK posts.id     ON DELETE CASCADE
  source_discussion_id   FK discussions.id
  target_discussion_id   FK discussions.id  ON DELETE CASCADE
  target_post_id         FK posts.id (nullable — null = whole-discussion ref)
  created_at             timestamp
  UNIQUE (source_post_id, target_discussion_id, target_post_id)
  INDEX  (target_discussion_id)   -- inbound-refs lookup
  INDEX  (source_discussion_id)   -- outbound-refs lookup

```

The table follows Flarum 2's **companion-table convention** — no columns added to `posts` or `discussions` (CLAUDE.md §45).

### How rendering works

[](#how-rendering-works)

1. **Parse-time**: The s9e/TextFormatter `Preg` plugin matches `\B#(\d+)\b`and `\B#(\d+)/p(\d+)\b` and emits `` tags into the stored XML. Pasted forum URLs are pre-rewritten to the `#N`form before the parser sees them.
2. **Pre-render**: a single batched `Discussion::whereIn('id', $ids) ->whereVisibleTo($actor)` query resolves every referenced discussion's title in one round-trip. Titles + visibility flags get injected onto each `` tag as attributes.
3. **Render**: the XSL template uses the `title` attribute when set, falling back to the `CrossReference--hidden` placeholder when visibility denies the read.

No titles are persisted in post content. No per-post N+1 query. One DB call per page render, regardless of how many cross-references are in the visible posts.

### Bidirectional backlinks + notifications

[](#bidirectional-backlinks--notifications)

The `Posted` / `Revised` event listener extracts CROSSREF tags from the post's parsed XML, diffs against the existing `cross_references` rows for that post, then:

- **Inserts** rows for new references; **deletes** rows for removed ones.
- For each new ref, creates a `CrossReferenceEventPost` (extending Flarum's `AbstractEventPost`) in the target discussion. Lives alongside core event-posts like "renamed" or "locked" — participates in moderation, search, and the standard reply pipeline.
- Optionally dispatches a `DiscussionReferencedBlueprint` notification to the target's author via the `alert` channel.

Self-references are silently dropped. The listener is wrapped in `try/catch` with PSR-3 logging so a cross-ref bug can **never** block a post save (CLAUDE.md §41).

### API endpoint

[](#api-endpoint)

```
GET /api/discussions/{id}/cross-references

```

Returns the inbound references for a discussion, with each row visibility-scoped against the requesting actor and eager-loaded with the source discussion title + first-post author. Capped at 50 rows — the response includes a `meta.capped50` boolean so a future "view all" page can fall back to a paginated source.

```
{
  "data": [
    {
      "id": 1,
      "sourceDiscussionId": 6,
      "sourcePostId": 4,
      "targetPostId": null,
      "createdAt": "2026-05-17T13:59:39+00:00",
      "source": {
        "discussionTitle": "Cross-ref smoke test",
        "discussionSlug": "cross-ref-smoke-test",
        "author": {
          "id": 2,
          "displayName": "tester",
          "username": "tester",
          "avatarUrl": null
        }
      }
    }
  ],
  "meta": { "count": 1, "capped50": false }
}
```

### Search filter

[](#search-filter)

Registered via `Extend\SearchDriver(DatabaseSearchDriver) ->addFilter(DiscussionSearcher, ReferencesFilter)`. The value is cast to `(int)` before reaching SQL — defeats the §10 wildcard / sort-allowlist trap surface; the gambit accepts numeric ids only, never strings.

---

Comparison vs. `club-1/flarum-ext-cross-references`
---------------------------------------------------

[](#comparison-vs-club-1flarum-ext-cross-references)

Concern`club-1/cross-references``ernestdefoe/cross-references`Flarum version1.x2.xDiscussion title in chipBaked into post content at saveResolved live from DB on every renderBehavior on target renameRequires `chore:reparse` to refresh stored chipsAutomatic — chips read current titleBehavior on target deletePlain link to nothingChip renders as muted `#N` placeholderVisibility-awareNo (leaks restricted titles)Yes — `whereVisibleTo($actor)` everywhereBacklink mechanismEvent postEvent post (first-class, extends `AbstractEventPost`)Notifications to target authorNoYes — `AlertableInterface` blueprintInbound-refs sidebar widgetNoYesSearch filter (`references:N`)NoYesPer-post listener failures block post savePossibleCaught + logged, never blocks saveDedupeNoneUnique index at storage layer---

Architecture notes (for extension authors reading this for reference)
---------------------------------------------------------------------

[](#architecture-notes-for-extension-authors-reading-this-for-reference)

This extension was built against the Flarum 2 security/structure playbook — relevant sections:

- **§5** — `whereVisibleTo($actor)` applied at every read path (API endpoint, render-time enrichment, search filter sub-query). Inbound-refs sidebar filters source visibility per actor before payload assembly.
- **§19** — `getData()` carries IDs only; titles re-resolved from the subject relation at render time so a target that turns private later doesn't leak its old title via a stale notification.
- **§26** — Migration uses `Flarum\Database\Migration::createTableIfNotExists`with explicit `cascadeOnDelete` FKs and a composite unique index for storage-layer dedupe.
- **§38** — Single batched query per render to resolve all titles for a post; one batched visibility check for the sidebar. No N+1.
- **§41** — `Psr\Log\LoggerInterface` injected in the listener + controller; try/catch wraps both so a ref bug can never block a post save.
- **§43** — `composer.json` constrained to `"flarum/core": "^2.0"`; no `^1.0` fallback, since v2-only `Endpoint`/`Schema` classes are imported.
- **§45** — Companion `cross_references` table; **no** columns added to core `posts`/`discussions`/`users`.
- **§46** — `DiscussionReferencedBlueprint` has a typed constructor + `TYPE` class constant + `getSubjectModel()` returns `Discussion::class`, so the polymorphic `subject` relationship resolves cleanly via `typeForModel()`.

---

Contributing
------------

[](#contributing)

Pull requests welcome. For non-trivial changes, please open an issue first so we can discuss the approach.

- `composer require ernestdefoe/cross-references:dev-main` (or path repo) in your local Flarum to develop against.
- `cd js && npm install && npm run dev` watches and rebuilds the JS bundle on every change.
- `php flarum cache:clear` after PHP changes.

Support
-------

[](#support)

Questions, bug reports, and feature requests:

- **Support forum:**
- **Issues:**

License
-------

[](#license)

[MIT](LICENSE.md) © Ernestdefoe

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance97

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~4 days

Total

3

Last Release

15d ago

Major Versions

2.0.1 → 3.0.02026-05-26

### Community

Maintainers

![](https://www.gravatar.com/avatar/c9978664264055711c21fbcf73c937f758df771d9cb40f6a25370b10123e7abc?d=identicon)[ernestdefoe](/maintainers/ernestdefoe)

---

Top Contributors

[![ernestdefoe](https://avatars.githubusercontent.com/u/24905286?v=4)](https://github.com/ernestdefoe "ernestdefoe (19 commits)")

---

Tags

mentionsflarumreferencesBacklinksdiscussions

### Embed Badge

![Health badge](/badges/ernestdefoe-cross-references/health.svg)

```
[![Health](https://phpackages.com/badges/ernestdefoe-cross-references/health.svg)](https://phpackages.com/packages/ernestdefoe-cross-references)
```

###  Alternatives

[flarum-lang/russian

Russian language pack for Flarum.

12127.5k](/packages/flarum-lang-russian)[fof/byobu

Well integrated, advanced private discussions.

61112.4k10](/packages/fof-byobu)[fof/gamification

Upvotes and downvotes for your Flarum community

4162.0k6](/packages/fof-gamification)[fof/polls

 A Flarum extension that adds polls to your discussions

25130.2k9](/packages/fof-polls)[fof/user-bio

Add a user bio to user profiles

21102.0k9](/packages/fof-user-bio)[fof/sitemap

Generate a sitemap

1896.4k2](/packages/fof-sitemap)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
