<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Linkwarden 1.21.1: full-text search on archived content is silently unavailable]]></title><description><![CDATA[<p dir="auto">Hey, another thing that I stumbled upon: the full-text search is not working on Cloudron. The search engine/db Meilisearch is missing. Here is a write up about it done together with AI (Claude Opus4.7):</p>
<h2>Symptom</h2>
<p dir="auto">Linkwarden's main selling point — beyond bookmarking — is that it preserves the full text of every saved page (<code>textContent</code>, extracted via Mozilla Readability and stored in PostgreSQL). On Cloudron's <code>app.linkwarden.cloudronapp@1.21.1</code> this content is correctly archived and shows up on link records. But the search box does not actually search it.</p>
<p dir="auto">Easy way to reproduce on any 1.21.1 instance:</p>
<ol>
<li>Save a link whose article body contains a distinctive word that does <strong>not</strong> appear in the title, URL, description or any tag.</li>
<li>Wait for preservation to finish; confirm via <code>GET /api/v1/links/&lt;id&gt;</code> that <code>textContent</code> is populated and contains the word.</li>
<li>Search for that word in the UI (or <code>GET /api/v1/links?searchQueryString=&lt;word&gt;</code>).</li>
<li>Result: zero hits.</li>
</ol>
<p dir="auto">So the bookmarks are preserved, but the preserved content is functionally invisible to the search UI. That's surprising — and (as far as I can tell) not mentioned anywhere in the Cloudron app docs or in the forum.</p>
<h2>Root cause</h2>
<p dir="auto"><code>apps/web/lib/api/controllers/search/searchLinks.ts</code> has two code paths:</p>
<pre><code class="language-ts">if (meiliClient &amp;&amp; query.searchQueryString) {
  // → Meilisearch path: indexed search across all fields incl. textContent
} else {
  // Fallback: No Meilisearch
  searchConditions.push({ name:        { contains: q } });
  searchConditions.push({ url:         { contains: q } });
  searchConditions.push({ description: { contains: q } });
  searchConditions.push({ tags: { some: { name: { contains: q } } } });
  // ← textContent is intentionally not in the WHERE clause
}
</code></pre>
<p dir="auto">The switch is <code>MEILI_MASTER_KEY</code>. Linkwarden's own <a href="https://docs.linkwarden.app/self-hosting/environment-variables" target="_blank" rel="noopener noreferrer nofollow ugc">env-vars docs</a> state it plainly: <em>"Linkwarden only initializes the MeiliSearch client when this value is set."</em> The Cloudron package ships with no <code>MEILI_*</code> env vars, so the client is <code>null</code> → fallback path → archived body text is never queried.</p>
<p dir="auto">The indexing worker (<code>apps/worker/workers/linkIndexing.ts</code>) is present in the image and ready to push records to a Meilisearch instance — it just has no instance to talk to.</p>
<h2>Upstream's position</h2>
<p dir="auto">Linkwarden's <a href="https://github.com/linkwarden/linkwarden/blob/main/docker-compose.yml" target="_blank" rel="noopener noreferrer nofollow ugc">reference <code>docker-compose.yml</code></a> treats Meilisearch as a first-class sidecar, not an optional add-on:</p>
<pre><code class="language-yaml">linkwarden:
  depends_on:
    - postgres
    - meilisearch
meilisearch:
  image: getmeili/meilisearch:v1.12.8
</code></pre>
<p dir="auto">The <a href="https://docs.linkwarden.app/Usage/advanced-search" target="_blank" rel="noopener noreferrer nofollow ugc">docs</a> describe content-level search ("Advanced Search") as the default expectation for self-hosters running ≥ 2.10.</p>
<p dir="auto">Cloudron's single-container packaging model legitimately makes that hard, but the resulting feature gap isn't currently visible to users.</p>
<h2>Suggestion</h2>
<p dir="auto">Three options, in increasing order of effort:</p>
<ol>
<li>
<p dir="auto"><strong>Doc-only fix.</strong> Add a "Limitations" / "Optional: full-text search" section to the Cloudron Linkwarden app docs explaining that the bundled image runs in the no-Meilisearch fallback mode, what that costs (no body-text search, no advanced operators), and pointing users at <code>MEILI_HOST</code> / <code>MEILI_MASTER_KEY</code> if they want to bring their own instance. This already works today — the Linkwarden code reads those env vars unchanged, so a self-hoster can spin up <code>getmeili/meilisearch</code> next to Cloudron and point Linkwarden at it via <code>cloudron env set</code>.</p>
</li>
<li>
<p dir="auto"><strong>Make the integration discoverable.</strong> Document <code>MEILI_HOST</code> / <code>MEILI_MASTER_KEY</code> as recognised env vars on the app page (they already work via <code>cloudron env set</code> — they're just not advertised). A startup log line on the worker — "Meilisearch disabled — full-text search on archived content will not be available" — would also turn the silent degradation into a visible one.</p>
</li>
<li>
<p dir="auto"><strong>Bundled sidecar.</strong> Ship Meilisearch alongside the Linkwarden container in the package. I understand from <a href="https://forum.cloudron.io/topic/2518/paid-meilisearch-app-for-self-hosters">topic #2518</a> that a standalone Meilisearch app was deemed out of scope ("like a database … not really like a web app"), so I'm not asking for that — but bundling it as an internal dependency of the Linkwarden package would side-step that objection while bringing the package in line with upstream's expected architecture.</p>
</li>
</ol>
<p dir="auto">(1) seems strictly better than the current state regardless of whether (2) or (3) ever happen — right now users only learn the limitation by reading the source.</p>
<h2>Verification offer</h2>
<p dir="auto">Happy to test any of the above on the 1.21.1 instance and report back. From reading the code I'd expect that pointing Linkwarden at an external Meilisearch via <code>cloudron env set MEILI_HOST=… MEILI_MASTER_KEY=…</code> works without any package changes — the fallback should flip to the Meilisearch path on next restart and the indexing worker should backfill existing links — but I haven't tried it yet, and confirming this is exactly the kind of thing I'm offering to do.</p>
<h2>Possibly related</h2>
<ul>
<li><a href="https://docs.linkwarden.app/self-hosting/installation" target="_blank" rel="noopener noreferrer nofollow ugc">docs.linkwarden.app/self-hosting/installation</a> — upstream install guide listing Meilisearch as part of the standard setup.</li>
<li>The Cloudron app currently runs <code>apps/worker/workers/linkIndexing.ts</code> as a no-op every cycle; might be worth gating it (or at least the log lines) on <code>MEILI_MASTER_KEY</code> being set, to avoid future user confusion.</li>
</ul>
]]></description><link>https://forum.cloudron.io/topic/15472/linkwarden-1.21.1-full-text-search-on-archived-content-is-silently-unavailable</link><generator>RSS for Node</generator><lastBuildDate>Fri, 01 May 2026 12:00:38 GMT</lastBuildDate><atom:link href="https://forum.cloudron.io/topic/15472.rss" rel="self" type="application/rss+xml"/><pubDate>Fri, 01 May 2026 06:21:11 GMT</pubDate><ttl>60</ttl></channel></rss>