Hellena Yes, the first problem you describe is real. Flarum intentionally treats a parent tag as containing the discussions of its child tags as well.
For example:
Linux
โโโ Debian
โโโ Ubuntu
โโโ Arch
A discussion tagged Ubuntu will also appear when you visit the Linux parent.
There is currently no normal admin setting for:
"When I click Linux, show only discussions directly assigned to Linux."
However, you do not need to modify Flarum core or install another extension to change this on your own installation.
1. Parent tag without child discussions
The important thing is: do not hide the unwanted discussions with CSS or JavaScript.
That would only hide them visually after Flarum has already returned them, which means pagination and result counts could become wrong.
The better solution is to modify the discussion query using Flarum's local extend.php.
Flarum 1.x supports filter mutators specifically for modifying a listing query.
A small implementation can look like this:
<?php
use Flarum\Extend;
use Flarum\Discussion\Filter\DiscussionFilterer;
use Flarum\Filter\FilterState;
use Flarum\Query\QueryCriteria;
use Flarum\Tags\Tag;
final class ParentTagOnly
{
public function __invoke(
FilterState $state,
QueryCriteria $criteria
): void {
$tagValue = $criteria->query['tag'] ?? null;
if (!is_string($tagValue) || $tagValue === '') {
return;
}
// Find the tag being viewed.
$tagQuery = Tag::query()->where('slug', $tagValue);
if (ctype_digit($tagValue)) {
$tagQuery->orWhere('id', (int) $tagValue);
}
$tag = $tagQuery->first();
if (!$tag) {
return;
}
// We only want to change the behaviour of parent tags.
if ($tag->parent_id !== null) {
return;
}
// Get this parent's children.
$childIds = Tag::query()
->where('parent_id', $tag->id)
->pluck('id')
->all();
if (!$childIds) {
return;
}
/*
* The normal Flarum tag filter has already selected discussions
* belonging to the parent.
*
* Now exclude discussions which are also assigned
* to one of this parent's child tags.
*/
$state->getQuery()->whereNotIn(
'discussions.id',
function ($query) use ($childIds) {
$query
->select('discussion_id')
->from('discussion_tag')
->whereIn('tag_id', $childIds);
}
);
}
}
return [
(new Extend\Filter(DiscussionFilterer::class))
->addFilterMutator(ParentTagOnly::class),
];
Put this into the root Flarum extend.php after making a backup of the current file, then run:
php flarum cache:clear
The result becomes:
Linux
โโโ discussion directly in Linux
โโโ discussion directly in Linux
Debian
โโโ Debian discussion
Ubuntu
โโโ Ubuntu discussion
instead of:
Linux
โโโ Linux discussion
โโโ Debian discussion
โโโ Ubuntu discussion
โโโ Arch discussion
I would test this on a staging/copy installation first, particularly because you already have several local customizations.
Also, do not modify anything under:
vendor/flarum/
Composer can overwrite those changes during an update.
There is actually an immediate workaround requiring zero code as well.
Flarum's search supports positive and negative tag filters. For example:
tag:linux -tag:debian -tag:ubuntu -tag:arch
That means:
discussions tagged Linux, excluding those belonging to Debian, Ubuntu or Arch.
It isn't as elegant as changing the normal parent-tag page, but it proves that the separation you want is perfectly possible.
2. Link preview bubbles without third-party services
I completely agree with your privacy concern here.
You do not need LinkPreview.net, Google or any other external preview API.
I would split the implementation into two cases.
Internal Flarum links
For something such as:
https://osarena.org/d/123-some-discussion
don't fetch the webpage at all.
Your Flarum installation already knows everything about discussion 123.
The frontend can request:
/api/discussions/123
and create a small popup containing:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Discussion title โ
โ โ
โ First few words of the discussion โ
โ โ
โ Linux ยท username ยท 15 replies โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Everything remains inside osarena.org.
No tracking.
No external request.
No external API.
I would detect internal discussion URLs in JavaScript roughly like this:
function getInternalDiscussionId(url) {
const parsed = new URL(url, window.location.origin);
if (parsed.origin !== window.location.origin) {
return null;
}
const match = parsed.pathname.match(/^\/d\/(\d+)/);
return match ? match[1] : null;
}
Then:
const id = getInternalDiscussionId(link.href);
if (id) {
const discussion = await app.store.find('discussions', id);
// Build your preview bubble from the Flarum model.
}
You could trigger it on hover, focus or click depending on the behaviour you prefer.
External links
External links require a different approach.
I would not do this:
fetch(externalWebsite);
from the visitor's browser.
Instead:
Visitor
โ
osarena.org
โ
your server fetches metadata
โ
osarena.org returns sanitized preview
โ
visitor
Create your own local endpoint, for example:
/api/osarena/link-preview?url=https://kernel.org/
It could return:
{
"title": "The Linux Kernel Archives",
"description": "The primary site for the Linux kernel source.",
"host": "kernel.org"
}
Then your JavaScript creates the bubble.
I would initially show only:
Title
Description
Domain
and not external images or favicons.
That is important for your zero-tracking policy.
For example, suppose a website contains:
<meta property="og:image"
content="https://tracking-example.com/image.jpg">
If you put that URL directly into:
<img src="https://tracking-example.com/image.jpg">
every OSarena visitor viewing the preview contacts that external server.
That is no longer zero tracking.
If you eventually want preview images, download and cache them on your own server first.
One very important security requirement
Do not create a server-side preview fetcher that blindly downloads whatever URL a member supplies.
Otherwise someone could try URLs such as:
http://127.0.0.1/
http://localhost/
http://10.0.0.1/
http://169.254.169.254/
That can become an SSRF vulnerability.
The preview endpoint should therefore:
- Accept only
http and https.
- Resolve the hostname before connecting.
- Reject localhost.
- Reject private/reserved/link-local IP addresses.
- Revalidate every redirect destination.
- Set a small response-size limit.
- Set a short timeout.
- Limit the number of redirects.
- Accept HTML only.
- Cache successful metadata.
Then extract only things such as:
<title>
<meta name="description">
<meta property="og:title">
<meta property="og:description">
Sanitize the returned strings and display them in your own UI.
That gives you:
NO LinkPreview.net
NO Google favicon API
NO client-side external request
NO remote JavaScript
NO third-party tracking
which seems much closer to the philosophy you described for OSarena.
So yes โ both things you want are achievable while keeping Flarum essentially vanilla.
For the parent/child tag behaviour I would solve it at the database query/filter level.
For previews I would use:
Internal URL
โ Flarum's own API
External URL
โ your own metadata proxy/cache
And I would keep all of this in your local customization layer rather than touching vendor/.
Your approach of keeping Flarum small and adding only things you actually understand and maintain yourself is completely reasonable, particularly for a privacy-focused community.