git: 692a5241427c - main - www/linkwarden: New port
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Wed, 30 Sep 2026 23:51:34 UTC
The branch main has been updated by feld:
URL: https://cgit.FreeBSD.org/ports/commit/?id=692a5241427cc84bf7eee327137bd0b49f00457f
commit 692a5241427cc84bf7eee327137bd0b49f00457f
Author: Mark Felder <feld@FreeBSD.org>
AuthorDate: 2026-09-30 23:23:10 +0000
Commit: Mark Felder <feld@FreeBSD.org>
CommitDate: 2026-09-30 23:51:30 +0000
www/linkwarden: New port
Linkwarden is a self-hosted, collaborative bookmark manager that collects,
organizes and preserves webpages. Every saved link is archived as a
screenshot, a PDF, a single file HTML copy and a readable article, so the
content stays available even when the original page is gone.
Co-authored-by: Jochen Neumeister <joneum@FreeBSD.org>
---
GIDs | 2 +-
UIDs | 2 +-
www/Makefile | 1 +
www/linkwarden/Makefile | 142 +++++++++++++++++++++
www/linkwarden/distinfo | 5 +
www/linkwarden/files/linkwarden.env.sample | 41 ++++++
www/linkwarden/files/linkwarden_web.in | 116 +++++++++++++++++
www/linkwarden/files/linkwarden_worker.in | 94 ++++++++++++++
...ps_web_components_Preservation_ReadableView.tsx | 25 ++++
...patch-apps_web_components_TextStyleDropdown.tsx | 24 ++++
www/linkwarden/files/patch-apps_web_next.config.js | 10 ++
www/linkwarden/files/patch-apps_web_package.json | 19 +++
www/linkwarden/files/patch-package.json | 20 +++
.../files/patch-packages_filesystem_createFile.ts | 11 ++
.../patch-packages_filesystem_createFolder.ts | 11 ++
.../files/patch-packages_filesystem_fileExists.ts | 11 ++
.../files/patch-packages_filesystem_moveFile.ts | 11 ++
.../files/patch-packages_filesystem_readFile.ts | 11 ++
.../files/patch-packages_filesystem_removeFile.ts | 11 ++
.../patch-packages_filesystem_removeFolder.ts | 11 ++
www/linkwarden/files/pkg-message.in | 33 +++++
www/linkwarden/pkg-descr | 10 ++
22 files changed, 619 insertions(+), 2 deletions(-)
diff --git a/GIDs b/GIDs
index f17deb934ed0..368db66fd314 100644
--- a/GIDs
+++ b/GIDs
@@ -340,7 +340,7 @@ comentario:*:395:
# free: 396
rtorrent:*:397:
tiarra:*:398:
-# free: 399
+linkwarden:*:399:
evcc:*:400:
spectrumknx:*:401:
# free: 402
diff --git a/UIDs b/UIDs
index 3453960c31e5..ae98d3437a11 100644
--- a/UIDs
+++ b/UIDs
@@ -345,7 +345,7 @@ comentario:*:395:395::0:0:Comentario Daemon:/var/db/comentario:/usr/sbin/nologin
# free: 396
rtorrent:*:397:397::0:0:rtorrent Daemon User:/nonexistent:/usr/sbin/nologin
tiarra:*:398:398::0:0:Tiarra IRC Proxy:/nonexistent:/usr/sbin/nologin
-# free: 399
+linkwarden:*:399:399::0:0:Linkwarden Daemon:/var/db/linkwarden:/usr/sbin/nologin
evcc:*:400:400::0:0:evcc Daemon:/nonexistent:/usr/sbin/nologin
spectrumknx:*:401:401::0:0:SpectrumKNX Daemon:/var/db/spectrumknx:/usr/sbin/nologin
# free: 402
diff --git a/www/Makefile b/www/Makefile
index fd94fb0e7f4f..c7cdc0b9fa72 100644
--- a/www/Makefile
+++ b/www/Makefile
@@ -369,6 +369,7 @@
SUBDIR += linklint
SUBDIR += links
SUBDIR += links1
+ SUBDIR += linkwarden
SUBDIR += linux-brave
SUBDIR += linux-c7-qtwebkit
SUBDIR += linux-chrome
diff --git a/www/linkwarden/Makefile b/www/linkwarden/Makefile
new file mode 100644
index 000000000000..100b59dab23f
--- /dev/null
+++ b/www/linkwarden/Makefile
@@ -0,0 +1,142 @@
+PORTNAME= linkwarden
+DISTVERSIONPREFIX= v
+DISTVERSION= 2.16.3
+CATEGORIES= www
+MASTER_SITES= LOCAL/feld:buildkit
+DISTFILES= linkwarden-freebsd-buildkit-${DISTVERSION}${EXTRACT_SUFX}:buildkit
+
+MAINTAINER= feld@FreeBSD.org
+COMMENT= Self-hosted collaborative bookmark manager and web archive
+WWW= https://linkwarden.app/
+
+LICENSE= AGPLv3
+LICENSE_FILE= ${WRKSRC}/LICENSE.md
+
+ONLY_FOR_ARCHS= amd64
+ONLY_FOR_ARCHS_REASON= Next.js only ships SWC bindings for x86_64-unknown-freebsd
+
+BUILD_DEPENDS= ${LOCALBASE}/libexec/prisma-engines/query-engine:databases/prisma-engines
+RUN_DEPENDS= ${LOCALBASE}/bin/chrome:www/chromium \
+ ${LOCALBASE}/bin/monolith:www/monolith \
+ ${LOCALBASE}/libexec/prisma-engines/query-engine:databases/prisma-engines
+
+USES= cpe gmake nodejs:22,build,run python:build ssl
+CPE_VENDOR= linkwarden
+
+USE_GITHUB= yes
+GH_ACCOUNT= linkwarden
+
+USE_RC_SUBR= linkwarden_web linkwarden_worker
+
+USERS= linkwarden
+GROUPS= linkwarden
+
+# The buildkit distfile is generated by the maintainer and uploaded to his
+# LOCAL distfiles area. It carries the Yarn Berry cache, the Yarn 4 bundle, the
+# lockfile pinned to the prisma version of databases/prisma-engines and the
+# WebAssembly SWC bindings, so that the build needs no network. It is made by
+# unpacking the release tarball, applying the patches of this port and running,
+# with the yarn version from packageManager in package.json:
+# npm pack @yarnpkg/cli-dist@4.12.0 -> yarn.js
+# npm pack @next/swc-wasm-nodejs@15.5.21 -> swc-wasm-nodejs/
+# cacheFolder and enableGlobalCache in .yarnrc.yml pointed at <kit>/yarn-cache
+# node <kit>/yarn.js workspaces focus linkwarden @linkwarden/web \
+# @linkwarden/worker
+# cp yarn.lock <kit>/
+# plus the two fonts that next/font would otherwise fetch from Google at build
+# time, taken from the google/fonts repository (OFL):
+# fonts/Bentham-Regular.ttf, fonts/Caveat-Variable.ttf and their OFL.txt
+BUILDKIT= ${WRKDIR}/linkwarden-freebsd-buildkit-${DISTVERSION}
+YARN= ${LOCALBASE}/bin/node ${BUILDKIT}/yarn.js
+PRISMA_ENGINES= ${LOCALBASE}/libexec/prisma-engines
+
+MAKE_ENV= CI=1 \
+ NEXT_TELEMETRY_DISABLED=1 \
+ NODE_OPTIONS=--max-old-space-size=8192 \
+ PLAYWRIGHT_BROWSERS_PATH=${WRKDIR}/playwright \
+ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \
+ PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
+ PRISMA_FMT_BINARY=${PRISMA_ENGINES}/prisma-fmt \
+ PRISMA_HIDE_UPDATE_MESSAGE=1 \
+ PRISMA_QUERY_ENGINE_BINARY=${PRISMA_ENGINES}/query-engine \
+ PRISMA_QUERY_ENGINE_LIBRARY=${WRKDIR}/libquery_engine.node \
+ PRISMA_SCHEMA_ENGINE_BINARY=${PRISMA_ENGINES}/schema-engine \
+ PYTHON=${PYTHON_CMD} \
+ npm_config_nodedir=${LOCALBASE} \
+ npm_config_python=${PYTHON_CMD}
+
+PLIST_SUB= BUILDID=linkwarden-${DISTVERSIONPREFIX}${DISTVERSION}
+
+SUB_FILES= pkg-message
+SUB_LIST= NODE=${LOCALBASE}/bin/node \
+ WWWDIR=${WWWDIR}
+
+pre-build:
+ ${CP} ${BUILDKIT}/yarn.lock ${WRKSRC}/yarn.lock
+ ${LN} -sf ${PRISMA_ENGINES}/libquery_engine.so ${WRKDIR}/libquery_engine.node
+ ${ECHO_CMD} 'cacheFolder: "${BUILDKIT}/yarn-cache"' >> ${WRKSRC}/.yarnrc.yml
+ ${ECHO_CMD} 'enableGlobalCache: false' >> ${WRKSRC}/.yarnrc.yml
+ ${ECHO_CMD} 'enableNetwork: false' >> ${WRKSRC}/.yarnrc.yml
+ ${ECHO_CMD} 'enableTelemetry: false' >> ${WRKSRC}/.yarnrc.yml
+ @${MKDIR} ${WRKSRC}/apps/web/fonts
+ ${INSTALL_DATA} ${BUILDKIT}/fonts/*.ttf ${WRKSRC}/apps/web/fonts/
+
+do-build:
+ cd ${WRKSRC} && ${SETENV} ${MAKE_ENV} ${YARN} workspaces focus \
+ linkwarden @linkwarden/web @linkwarden/worker
+ @${MKDIR} ${WRKSRC}/node_modules/next/wasm/@next
+ ${CP} -R ${BUILDKIT}/swc-wasm-nodejs \
+ ${WRKSRC}/node_modules/next/wasm/@next/swc-wasm-nodejs
+ cd ${WRKSRC} && ${SETENV} ${MAKE_ENV} ${YARN} workspace \
+ @linkwarden/prisma generate
+ cd ${WRKSRC} && ${SETENV} ${MAKE_ENV} ${YARN} workspace \
+ @linkwarden/web build
+ cd ${WRKSRC} && ${SETENV} ${MAKE_ENV} ${YARN} workspaces focus \
+ --production linkwarden @linkwarden/web @linkwarden/worker
+ @${MKDIR} ${WRKSRC}/node_modules/next/wasm/@next
+ ${CP} -R ${BUILDKIT}/swc-wasm-nodejs \
+ ${WRKSRC}/node_modules/next/wasm/@next/swc-wasm-nodejs
+ cd ${WRKSRC} && ${SETENV} ${MAKE_ENV} ${YARN} workspace \
+ @linkwarden/prisma generate
+ ${RM} -r ${WRKSRC}/apps/web/.next/cache
+
+do-install:
+ ${MKDIR} ${STAGEDIR}${WWWDIR}
+ cd ${WRKSRC} && ${PAX} -rw -pp package.json yarn.lock .yarnrc.yml \
+ node_modules apps/web apps/worker packages ${STAGEDIR}${WWWDIR}
+ ${LN} -sf ${PRISMA_ENGINES}/libquery_engine.so \
+ ${STAGEDIR}${WWWDIR}/libquery_engine.node
+ ${INSTALL_DATA} ${FILESDIR}/linkwarden.env.sample ${STAGEDIR}${PREFIX}/etc/
+ ${MKDIR} ${STAGEDIR}/var/db/${PORTNAME} ${STAGEDIR}/var/log/${PORTNAME}
+
+post-install:
+# node-gyp is a build tool, it is of no use at runtime and only brings in
+# python shebangs, and the engine that prisma generate copied is the one of
+# databases/prisma-engines
+ @${RM} -r ${STAGEDIR}${WWWDIR}/node_modules/node-gyp \
+ ${STAGEDIR}${WWWDIR}/node_modules/.bin/node-gyp
+ ${LN} -sf ${PRISMA_ENGINES}/libquery_engine.so \
+ ${STAGEDIR}${WWWDIR}/node_modules/.prisma/client/libquery_engine.node
+ @${RM} -r ${STAGEDIR}${WWWDIR}/apps/web/fonts
+ @${FIND} ${STAGEDIR}${WWWDIR}/node_modules -type d \
+ \( -name .deps -o -name "build-tmp-*" -o -name node-addon-api \) \
+ -exec ${RM} -r {} + 2>/dev/null || ${TRUE}
+ @${FIND} ${STAGEDIR}${WWWDIR}/node_modules -type f \
+ \( -name "*.target.mk" -o -name "*.Makefile" -o -name Makefile \
+ -o -name nothing.a -o -name config.gypi -o -name "*.stamp" \) -delete
+ @${FIND} ${STAGEDIR}${WWWDIR} -type f \
+ \( -name "*.map" -o -name "*.md" -o -name "*.markdown" \) -delete
+ @${FIND} ${STAGEDIR}${WWWDIR}/node_modules -type d \
+ \( -name test -o -name tests -o -name __tests__ -o -name docs \) \
+ -exec ${RM} -r {} + 2>/dev/null || ${TRUE}
+ @${FIND} ${STAGEDIR}${WWWDIR} -type f -name ".DS_Store" -delete
+ @${FIND} ${STAGEDIR}${WWWDIR} -type f -name "*.sh" -exec ${CHMOD} a-x {} +
+ @${FIND} ${STAGEDIR}${WWWDIR} -type d -empty -delete
+ # Dynamically build pkg-plist because it is very large
+ @(cd ${STAGEDIR}${PREFIX} ; \
+ ${FIND} www \( -type f -or -type l \) | ${SORT} >> ${TMPPLIST})
+ @echo "@sample etc/linkwarden.env.sample" >> ${TMPPLIST}
+ @echo "@dir(linkwarden,linkwarden,750) /var/db/linkwarden" >> ${TMPPLIST}
+ @echo "@dir(linkwarden,linkwarden,750) /var/log/linkwarden" >> ${TMPPLIST}
+
+.include <bsd.port.mk>
diff --git a/www/linkwarden/distinfo b/www/linkwarden/distinfo
new file mode 100644
index 000000000000..16aac39e6121
--- /dev/null
+++ b/www/linkwarden/distinfo
@@ -0,0 +1,5 @@
+TIMESTAMP = 1790154217
+SHA256 (linkwarden-freebsd-buildkit-2.16.3.tar.gz) = 3865c4f06d251d706499de29d2838bc96385248cc26c5130c540491cd8e7e49b
+SIZE (linkwarden-freebsd-buildkit-2.16.3.tar.gz) = 226736331
+SHA256 (linkwarden-linkwarden-v2.16.3_GH0.tar.gz) = 88fd2010ee225724756f6f9da7841788622676e82717a9a2e0cb5625f915696a
+SIZE (linkwarden-linkwarden-v2.16.3_GH0.tar.gz) = 7182381
diff --git a/www/linkwarden/files/linkwarden.env.sample b/www/linkwarden/files/linkwarden.env.sample
new file mode 100644
index 000000000000..cfbded483466
--- /dev/null
+++ b/www/linkwarden/files/linkwarden.env.sample
@@ -0,0 +1,41 @@
+# Settings for linkwarden_web and linkwarden_worker. Everything in here is
+# exported into the environment of both services, so any variable described in
+# https://docs.linkwarden.app/self-hosting/environment-variables can be set.
+#
+# Keep this file readable by the linkwarden user only, it holds credentials:
+# chown root:linkwarden linkwarden.env && chmod 640 linkwarden.env
+
+# Database, see the message of this package for how to create it
+DATABASE_URL=postgresql://linkwarden:changeme@localhost:5432/linkwarden
+
+# Public address of the instance and the session secret. Generate the secret
+# with: openssl rand -base64 32
+NEXTAUTH_URL=http://localhost:3000/api/v1/auth
+NEXTAUTH_SECRET=
+
+# Where the archived pages are kept. An absolute path is used as it is, a
+# relative one is resolved against the directory above the installation.
+#STORAGE_FOLDER=/var/db/linkwarden/data
+
+# Username and password logins. Set to false when only SSO should be allowed.
+NEXT_PUBLIC_CREDENTIALS_ENABLED=true
+
+# Set to true once the first account exists, so that nobody else can register.
+#NEXT_PUBLIC_DISABLE_REGISTRATION=false
+
+# Full text search over the archived pages needs a meilisearch instance,
+# see textproc/meilisearch. Without it the search covers names, urls,
+# descriptions and tags.
+#MEILI_HOST=http://127.0.0.1:7700
+#MEILI_MASTER_KEY=
+
+# Options passed to www/monolith when a page is saved as a single file
+#MONOLITH_CUSTOM_OPTIONS=-j -F -q
+
+# Time the worker waits for a page, in milliseconds
+#BROWSER_TIMEOUT=
+
+# Mail for password resets and invitations
+#NEXT_PUBLIC_EMAIL_PROVIDER=true
+#EMAIL_FROM=linkwarden@example.org
+#EMAIL_SERVER=smtp://localhost:25
diff --git a/www/linkwarden/files/linkwarden_web.in b/www/linkwarden/files/linkwarden_web.in
new file mode 100644
index 000000000000..421b9d395db8
--- /dev/null
+++ b/www/linkwarden/files/linkwarden_web.in
@@ -0,0 +1,116 @@
+#!/bin/sh
+
+# PROVIDE: linkwarden_web
+# REQUIRE: LOGIN postgresql
+# KEYWORD: shutdown
+#
+# Add these lines to /etc/rc.conf.local or /etc/rc.conf
+# to enable this service:
+#
+# linkwarden_web_enable (bool): Set to NO by default.
+# Set it to YES to enable the frontend.
+# linkwarden_storage (str): Data directory, holds the archives.
+# Default: /var/db/linkwarden
+# linkwarden_web_host (str): Listen address. Default: 0.0.0.0
+# linkwarden_web_port (int): Listen port. Default: 3000
+# linkwarden_web_env_file (str): Environment file with DATABASE_URL,
+# NEXTAUTH_SECRET and the other settings.
+# Default: %%PREFIX%%/etc/linkwarden.env
+# linkwarden_web_automigrate (bool): Run "prisma migrate deploy" on start.
+# Default: YES
+#
+# The database schema can also be brought up to date on its own with
+# "service linkwarden_web migrate".
+
+. /etc/rc.subr
+
+name=linkwarden_web
+rcvar=linkwarden_web_enable
+
+load_rc_config $name
+
+: ${linkwarden_web_enable:=NO}
+: ${linkwarden_web_user:=linkwarden}
+: ${linkwarden_web_group:=linkwarden}
+: ${linkwarden_storage:=/var/db/linkwarden}
+: ${linkwarden_web_host:=0.0.0.0}
+: ${linkwarden_web_port:=3000}
+: ${linkwarden_web_env_file:=%%PREFIX%%/etc/linkwarden.env}
+: ${linkwarden_web_automigrate:=YES}
+
+pidfile=/var/run/linkwarden/${name}.pid
+childpidfile=/var/run/linkwarden/${name}_child.pid
+required_dirs="%%WWWDIR%%/apps/web/.next"
+
+linkwarden_web_chdir="%%WWWDIR%%/apps/web/"
+start_precmd=linkwarden_web_precmd
+stop_cmd=linkwarden_web_stop
+extra_commands="migrate"
+migrate_cmd=linkwarden_web_migrate
+
+command=/usr/sbin/daemon
+command_args="-f -S -H -P ${pidfile} -p ${childpidfile} \
+ -o /var/log/linkwarden/${name}.log \
+ %%NODE%% %%WWWDIR%%/node_modules/next/dist/bin/next start \
+ %%WWWDIR%%/apps/web"
+
+linkwarden_environment()
+{
+ if [ -r "${linkwarden_web_env_file}" ]; then
+ set -a
+ . "${linkwarden_web_env_file}"
+ set +a
+ fi
+
+ # monolith and the browser are called by name, so they have to be in PATH
+ export PATH=%%PREFIX%%/sbin:%%PREFIX%%/bin:/usr/sbin:/usr/bin:/sbin:/bin
+ export NODE_ENV=production
+ export HOSTNAME=${linkwarden_web_host}
+ export PORT=${linkwarden_web_port}
+ export STORAGE_FOLDER=${STORAGE_FOLDER:-${linkwarden_storage}/data}
+ export PRISMA_HIDE_UPDATE_MESSAGE=1
+ export PRISMA_CLI_QUERY_ENGINE_TYPE=binary
+ export PRISMA_QUERY_ENGINE_BINARY=%%PREFIX%%/libexec/prisma-engines/query-engine
+ export PRISMA_SCHEMA_ENGINE_BINARY=%%PREFIX%%/libexec/prisma-engines/schema-engine
+ export PRISMA_FMT_BINARY=%%PREFIX%%/libexec/prisma-engines/prisma-fmt
+ export PRISMA_QUERY_ENGINE_LIBRARY=%%WWWDIR%%/libquery_engine.node
+ # playwright refuses to load without this one
+ export PLAYWRIGHT_BROWSERS_PATH=${linkwarden_storage}/ms-playwright
+}
+
+linkwarden_web_migrate()
+{
+ linkwarden_environment
+ echo "Applying database migrations for ${name}."
+ su -m ${linkwarden_web_user} -c "cd ${linkwarden_storage} && \
+ %%NODE%% %%WWWDIR%%/node_modules/prisma/build/index.js \
+ migrate deploy --schema %%WWWDIR%%/packages/prisma/schema.prisma"
+}
+
+linkwarden_web_stop()
+{
+ local pid
+ pid=$(check_pidfile ${pidfile} ${command})
+ if [ -z "$pid" ]; then
+ echo "${name} not running? (check ${pidfile})."
+ return 1
+ fi
+ echo "Stopping ${name}."
+ kill -TERM -- -${pid} 2>/dev/null || kill -TERM ${pid}
+ wait_for_pids ${pid}
+ rm -f ${pidfile} ${childpidfile}
+}
+
+linkwarden_web_precmd()
+{
+ umask 027
+ install -d -m 750 -o ${linkwarden_web_user} -g ${linkwarden_web_group} \
+ /var/run/linkwarden /var/log/linkwarden \
+ ${linkwarden_storage} ${linkwarden_storage}/data
+ linkwarden_environment
+ if checkyesno linkwarden_web_automigrate; then
+ linkwarden_web_migrate || return 1
+ fi
+}
+
+run_rc_command "$1"
diff --git a/www/linkwarden/files/linkwarden_worker.in b/www/linkwarden/files/linkwarden_worker.in
new file mode 100644
index 000000000000..770103a315fc
--- /dev/null
+++ b/www/linkwarden/files/linkwarden_worker.in
@@ -0,0 +1,94 @@
+#!/bin/sh
+
+# PROVIDE: linkwarden_worker
+# REQUIRE: LOGIN postgresql meilisearch
+# KEYWORD: shutdown
+#
+# Add these lines to /etc/rc.conf.local or /etc/rc.conf
+# to enable this service:
+#
+# linkwarden_worker_enable (bool): Set to NO by default.
+# Set it to YES to enable the worker that
+# preserves the saved pages.
+# linkwarden_worker_data (str): Data directory, holds the archives.
+# Default: /var/db/linkwarden
+# linkwarden_worker_env_file (str): Environment file with DATABASE_URL and
+# the other settings.
+# Default: %%PREFIX%%/etc/linkwarden.env
+# linkwarden_worker_browser (str): Browser used for screenshots and PDFs.
+# Default: %%PREFIX%%/bin/chrome
+
+. /etc/rc.subr
+
+name=linkwarden_worker
+rcvar=linkwarden_worker_enable
+
+load_rc_config $name
+
+: ${linkwarden_worker_enable:=NO}
+: ${linkwarden_worker_user:=linkwarden}
+: ${linkwarden_worker_group:=linkwarden}
+: ${linkwarden_worker_data:=/var/db/linkwarden}
+: ${linkwarden_worker_env_file:=%%PREFIX%%/etc/linkwarden.env}
+: ${linkwarden_worker_browser:=%%PREFIX%%/bin/chrome}
+
+pidfile=/var/run/linkwarden/${name}.pid
+childpidfile=/var/run/linkwarden/${name}_child.pid
+required_files="%%WWWDIR%%/apps/worker/worker.ts"
+
+linkwarden_worker_chdir=${linkwarden_worker_data}
+start_precmd=linkwarden_worker_precmd
+stop_cmd=linkwarden_worker_stop
+
+command=/usr/sbin/daemon
+command_args="-f -S -H -P ${pidfile} -p ${childpidfile} \
+ -o /var/log/linkwarden/${name}.log \
+ %%NODE%% %%WWWDIR%%/node_modules/tsx/dist/cli.mjs \
+ %%WWWDIR%%/apps/worker/worker.ts"
+
+linkwarden_worker_stop()
+{
+ local pid
+ pid=$(check_pidfile ${pidfile} ${command})
+ if [ -z "$pid" ]; then
+ echo "${name} not running? (check ${pidfile})."
+ return 1
+ fi
+ echo "Stopping ${name}."
+ kill -TERM -- -${pid} 2>/dev/null || kill -TERM ${pid}
+ wait_for_pids ${pid}
+ rm -f ${pidfile} ${childpidfile}
+}
+
+linkwarden_worker_precmd()
+{
+ umask 027
+ install -d -m 750 -o ${linkwarden_worker_user} \
+ -g ${linkwarden_worker_group} \
+ /var/run/linkwarden /var/log/linkwarden \
+ ${linkwarden_worker_data} ${linkwarden_worker_data}/data
+
+ if [ -r "${linkwarden_worker_env_file}" ]; then
+ set -a
+ . "${linkwarden_worker_env_file}"
+ set +a
+ fi
+
+ # monolith and the browser are called by name, so they have to be in PATH
+ export PATH=%%PREFIX%%/sbin:%%PREFIX%%/bin:/usr/sbin:/usr/bin:/sbin:/bin
+ export NODE_ENV=production
+ export STORAGE_FOLDER=${STORAGE_FOLDER:-${linkwarden_worker_data}/data}
+ export PRISMA_HIDE_UPDATE_MESSAGE=1
+ export PRISMA_CLI_QUERY_ENGINE_TYPE=binary
+ export PRISMA_QUERY_ENGINE_BINARY=%%PREFIX%%/libexec/prisma-engines/query-engine
+ export PRISMA_SCHEMA_ENGINE_BINARY=%%PREFIX%%/libexec/prisma-engines/schema-engine
+ export PRISMA_FMT_BINARY=%%PREFIX%%/libexec/prisma-engines/prisma-fmt
+ export PRISMA_QUERY_ENGINE_LIBRARY=%%WWWDIR%%/libquery_engine.node
+ # playwright refuses to load without a browser path, and it is told to
+ # use the chromium of the ports tree instead of downloading one
+ export PLAYWRIGHT_BROWSERS_PATH=${linkwarden_worker_data}/ms-playwright
+ export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
+ export PLAYWRIGHT_LAUNCH_OPTIONS_EXECUTABLE_PATH=${PLAYWRIGHT_LAUNCH_OPTIONS_EXECUTABLE_PATH:-${linkwarden_worker_browser}}
+}
+
+run_rc_command "$1"
diff --git a/www/linkwarden/files/patch-apps_web_components_Preservation_ReadableView.tsx b/www/linkwarden/files/patch-apps_web_components_Preservation_ReadableView.tsx
new file mode 100644
index 000000000000..01f1015289a2
--- /dev/null
+++ b/www/linkwarden/files/patch-apps_web_components_Preservation_ReadableView.tsx
@@ -0,0 +1,25 @@
+--- apps/web/components/Preservation/ReadableView.tsx.orig 2026-09-23 09:02:23 UTC
++++ apps/web/components/Preservation/ReadableView.tsx
+@@ -25,13 +25,18 @@
+ } from "@linkwarden/router/highlights";
+ import { Highlight } from "@linkwarden/prisma/client";
+ import { useUser } from "@linkwarden/router/user";
+-import { Caveat } from "next/font/google";
+-import { Bentham } from "next/font/google";
++import localFont from "next/font/local";
+ import { Separator } from "../ui/separator";
+ import { Button } from "../ui/button";
+
+-const caveat = Caveat({ subsets: ["latin"] });
+-const bentham = Bentham({ subsets: ["latin"], weight: "400" });
++const caveat = localFont({
++ src: "../../fonts/Caveat-Variable.ttf",
++ weight: "400 700",
++});
++const bentham = localFont({
++ src: "../../fonts/Bentham-Regular.ttf",
++ weight: "400",
++});
+
+ type Props = {
+ link: LinkIncludingShortenedCollectionAndTags;
diff --git a/www/linkwarden/files/patch-apps_web_components_TextStyleDropdown.tsx b/www/linkwarden/files/patch-apps_web_components_TextStyleDropdown.tsx
new file mode 100644
index 000000000000..ad2c24e7d01d
--- /dev/null
+++ b/www/linkwarden/files/patch-apps_web_components_TextStyleDropdown.tsx
@@ -0,0 +1,24 @@
+--- apps/web/components/TextStyleDropdown.tsx.orig 2026-09-23 09:02:23 UTC
++++ apps/web/components/TextStyleDropdown.tsx
+@@ -13,12 +13,17 @@
+ import { Button } from "@/components/ui/button";
+ import { FitWidth, FormatLineSpacing, FormatSize } from "@/components/ui/icons";
+ import { useUpdateUserPreference, useUser } from "@linkwarden/router/user";
+-import { Caveat } from "next/font/google";
+-import { Bentham } from "next/font/google";
++import localFont from "next/font/local";
+ import { useTranslation } from "next-i18next";
+
+-const caveat = Caveat({ subsets: ["latin"] });
+-const bentham = Bentham({ subsets: ["latin"], weight: "400" });
++const caveat = localFont({
++ src: "../fonts/Caveat-Variable.ttf",
++ weight: "400 700",
++});
++const bentham = localFont({
++ src: "../fonts/Bentham-Regular.ttf",
++ weight: "400",
++});
+
+ const fontSizes = [
+ "12px",
diff --git a/www/linkwarden/files/patch-apps_web_next.config.js b/www/linkwarden/files/patch-apps_web_next.config.js
new file mode 100644
index 000000000000..e885a0ea5a19
--- /dev/null
+++ b/www/linkwarden/files/patch-apps_web_next.config.js
@@ -0,0 +1,10 @@
+--- apps/web/next.config.js.orig 2026-09-22 11:09:01 UTC
++++ apps/web/next.config.js
+@@ -4,6 +4,7 @@ const nextConfig = {
+
+ const nextConfig = {
+ i18n,
++ generateBuildId: () => "linkwarden-" + version,
+ reactStrictMode: true,
+ devIndicators: false,
+ staticPageGenerationTimeout: 1000,
diff --git a/www/linkwarden/files/patch-apps_web_package.json b/www/linkwarden/files/patch-apps_web_package.json
new file mode 100644
index 000000000000..e548ac80f4bd
--- /dev/null
+++ b/www/linkwarden/files/patch-apps_web_package.json
@@ -0,0 +1,19 @@
+--- apps/web/package.json.orig 2026-09-22 09:39:55 UTC
++++ apps/web/package.json
+@@ -13,7 +13,7 @@
+ "lint": "next lint",
+ "e2e": "playwright test e2e",
+ "format": "prettier --write \"**/*.{ts,tsx,js,json,md}\"",
+- "postinstall": "playwright install --with-deps chromium"
++ "postinstall": "true"
+ },
+ "dependencies": {
+ "@apple/app-store-server-library": "^3.1.0",
+@@ -29,6 +29,7 @@
+ "@linkwarden/prisma": "*",
+ "@linkwarden/router": "*",
+ "@linkwarden/types": "*",
++ "@next/swc-wasm-nodejs": "15.5.21",
+ "@phosphor-icons/core": "^2.1.1",
+ "@phosphor-icons/react": "^2.1.7",
+ "@radix-ui/react-checkbox": "^1.3.3",
diff --git a/www/linkwarden/files/patch-package.json b/www/linkwarden/files/patch-package.json
new file mode 100644
index 000000000000..6f9c64dfc9eb
--- /dev/null
+++ b/www/linkwarden/files/patch-package.json
@@ -0,0 +1,20 @@
+--- package.json.orig 2026-09-22 09:39:55 UTC
++++ package.json
+@@ -24,7 +24,7 @@
+ "format": "yarn workspaces run format",
+ "test": "vitest",
+ "coverage": "vitest run --coverage",
+- "postinstall": "yarn workspace @linkwarden/web run postinstall && patch-package"
++ "postinstall": "true"
+ },
+ "devDependencies": {
+ "vitest": "^4.1.0"
+@@ -35,6 +35,8 @@
+ "patch-package": "^8.0.0"
+ },
+ "resolutions": {
++ "@prisma/client": "6.19.0",
++ "prisma": "6.19.0",
+ "react": "19.1.0",
+ "react-dom": "19.1.0"
+ }
diff --git a/www/linkwarden/files/patch-packages_filesystem_createFile.ts b/www/linkwarden/files/patch-packages_filesystem_createFile.ts
new file mode 100644
index 000000000000..1605007a991b
--- /dev/null
+++ b/www/linkwarden/files/patch-packages_filesystem_createFile.ts
@@ -0,0 +1,11 @@
+--- packages/filesystem/createFile.ts.orig 2026-09-23 10:10:21 UTC
++++ packages/filesystem/createFile.ts
+@@ -48,7 +48,7 @@
+ }
+ } else {
+ const storagePath = process.env.STORAGE_FOLDER || "data";
+- const creationPath = path.join(
++ const creationPath = path.resolve(
+ process.cwd(),
+ "../..",
+ storagePath,
diff --git a/www/linkwarden/files/patch-packages_filesystem_createFolder.ts b/www/linkwarden/files/patch-packages_filesystem_createFolder.ts
new file mode 100644
index 000000000000..96d94708115e
--- /dev/null
+++ b/www/linkwarden/files/patch-packages_filesystem_createFolder.ts
@@ -0,0 +1,11 @@
+--- packages/filesystem/createFolder.ts.orig 2026-09-23 10:10:21 UTC
++++ packages/filesystem/createFolder.ts
+@@ -7,7 +7,7 @@
+ // Do nothing, S3 creates directories recursively
+ } else {
+ const storagePath = process.env.STORAGE_FOLDER || "data";
+- const creationPath = path.join(
++ const creationPath = path.resolve(
+ process.cwd(),
+ "../..",
+ storagePath,
diff --git a/www/linkwarden/files/patch-packages_filesystem_fileExists.ts b/www/linkwarden/files/patch-packages_filesystem_fileExists.ts
new file mode 100644
index 000000000000..08a6c9e61a65
--- /dev/null
+++ b/www/linkwarden/files/patch-packages_filesystem_fileExists.ts
@@ -0,0 +1,11 @@
+--- packages/filesystem/fileExists.ts.orig 2026-09-23 10:10:21 UTC
++++ packages/filesystem/fileExists.ts
+@@ -20,7 +20,7 @@
+ }
+
+ const storagePath = process.env.STORAGE_FOLDER || "data";
+- const creationPath = path.join(process.cwd(), "../..", storagePath, filePath);
++ const creationPath = path.resolve(process.cwd(), "../..", storagePath, filePath);
+
+ return fs.existsSync(creationPath);
+ }
diff --git a/www/linkwarden/files/patch-packages_filesystem_moveFile.ts b/www/linkwarden/files/patch-packages_filesystem_moveFile.ts
new file mode 100644
index 000000000000..ede787a64702
--- /dev/null
+++ b/www/linkwarden/files/patch-packages_filesystem_moveFile.ts
@@ -0,0 +1,11 @@
+--- packages/filesystem/moveFile.ts.orig 2026-09-23 10:10:21 UTC
++++ packages/filesystem/moveFile.ts
+@@ -28,7 +28,7 @@
+ const storagePath = process.env.STORAGE_FOLDER || "data";
+
+ const directory = (file: string) =>
+- path.join(process.cwd(), "../..", storagePath, file);
++ path.resolve(process.cwd(), "../..", storagePath, file);
+
+ if (fs.existsSync(directory(from))) {
+ fs.rename(directory(from), directory(to), (err) => {
diff --git a/www/linkwarden/files/patch-packages_filesystem_readFile.ts b/www/linkwarden/files/patch-packages_filesystem_readFile.ts
new file mode 100644
index 000000000000..a12f5175563e
--- /dev/null
+++ b/www/linkwarden/files/patch-packages_filesystem_readFile.ts
@@ -0,0 +1,11 @@
+--- packages/filesystem/readFile.ts.orig 2026-09-23 10:10:21 UTC
++++ packages/filesystem/readFile.ts
+@@ -79,7 +79,7 @@
+ }
+ } else {
+ const storagePath = process.env.STORAGE_FOLDER || "data";
+- const creationPath = path.join(
++ const creationPath = path.resolve(
+ process.cwd(),
+ "../..",
+ storagePath,
diff --git a/www/linkwarden/files/patch-packages_filesystem_removeFile.ts b/www/linkwarden/files/patch-packages_filesystem_removeFile.ts
new file mode 100644
index 000000000000..a0a74e80459b
--- /dev/null
+++ b/www/linkwarden/files/patch-packages_filesystem_removeFile.ts
@@ -0,0 +1,11 @@
+--- packages/filesystem/removeFile.ts.orig 2026-09-23 10:10:21 UTC
++++ packages/filesystem/removeFile.ts
+@@ -17,7 +17,7 @@
+ }
+ } else {
+ const storagePath = process.env.STORAGE_FOLDER || "data";
+- const creationPath = path.join(
++ const creationPath = path.resolve(
+ process.cwd(),
+ "../..",
+ storagePath,
diff --git a/www/linkwarden/files/patch-packages_filesystem_removeFolder.ts b/www/linkwarden/files/patch-packages_filesystem_removeFolder.ts
new file mode 100644
index 000000000000..fa215b1b6c1e
--- /dev/null
+++ b/www/linkwarden/files/patch-packages_filesystem_removeFolder.ts
@@ -0,0 +1,11 @@
+--- packages/filesystem/removeFolder.ts.orig 2026-09-23 10:10:21 UTC
++++ packages/filesystem/removeFolder.ts
+@@ -49,7 +49,7 @@
+ }
+ } else {
+ const storagePath = process.env.STORAGE_FOLDER || "data";
+- const creationPath = path.join(
++ const creationPath = path.resolve(
+ process.cwd(),
+ "../..",
+ storagePath,
diff --git a/www/linkwarden/files/pkg-message.in b/www/linkwarden/files/pkg-message.in
new file mode 100644
index 000000000000..59309616b988
--- /dev/null
+++ b/www/linkwarden/files/pkg-message.in
@@ -0,0 +1,33 @@
+[
+{ type: install
+ message: <<EOM
+Linkwarden needs a PostgreSQL database and a few settings before it starts.
+
+1. Create the database, for example on the same host:
+
+ su -l postgres -c "psql -c \"CREATE ROLE linkwarden LOGIN PASSWORD 'secret'\""
+ su -l postgres -c "createdb -O linkwarden linkwarden"
+
+2. Set DATABASE_URL, NEXTAUTH_URL and a NEXTAUTH_SECRET
+ (openssl rand -base64 32) in %%PREFIX%%/etc/linkwarden.env. That file
+ holds credentials, so keep it to the linkwarden user:
+
+ chown root:linkwarden %%PREFIX%%/etc/linkwarden.env
+ chmod 640 %%PREFIX%%/etc/linkwarden.env
+
+3. Enable and start both services. The frontend brings the database schema
+ up to date whenever it starts:
+
+ sysrc linkwarden_web_enable=YES linkwarden_worker_enable=YES
+ service linkwarden_web start
+ service linkwarden_worker start
+
+The frontend then listens on port 3000 and the first account is created by
+registering in the web interface.
+
+Saved pages are preserved by the worker with www/chromium and www/monolith,
+the archives end up in /var/db/linkwarden/data. Full text search over the
+archived pages needs textproc/meilisearch, see MEILI_HOST in linkwarden.env.
+EOM
+}
+]
diff --git a/www/linkwarden/pkg-descr b/www/linkwarden/pkg-descr
new file mode 100644
index 000000000000..0d98512d6315
--- /dev/null
+++ b/www/linkwarden/pkg-descr
@@ -0,0 +1,10 @@
+Linkwarden is a self-hosted, collaborative bookmark manager that collects,
+organizes and preserves webpages. Every saved link is archived as a
+screenshot, a PDF, a single file HTML copy and a readable article, so the
+content stays available even when the original page is gone.
+
+Links live in collections, can be tagged, shared with other users or made
+public, and are searchable including the text of the archived pages.
+
+The port runs the web frontend and the background worker as two separate
+services and keeps its data in PostgreSQL.