A modern, self-hosted WebUI designed for enterprises to centralize and publish internal or external links/URLs for employees, collaborators, and clients.
Instead of scattering important resources across emails, chats, and shared drives, LinkPage provides a single branded landing page (e.g. linkpage.company.com) where teams can quickly access everything they need — from internal tools and documentation to public-facing resources.
| Light Mode | Dark Mode |
|---|---|
![]() |
![]() |
The public page greets end users with a clean card grid, sticky group tabs for filtering, a live search bar, and auto-fetched favicons — no sign-in required.
| Light Mode | Dark Mode |
|---|---|
![]() |
![]() |
The admin panel is protected by a secret token. The left sidebar lists all groups; the main area shows link cards with drag-to-reorder, bulk selection, import/export, and one-click link creation.
Per-link analytics show total clicks, unique visitors, today and this-week counts, a recent-click log with IP and device type, and a bar chart of top visitor IPs.
The settings modal covers branding (custom site title, separate logos for light and dark mode), an icon library (manage reusable icons, opt-in to saving fetched favicons), public access (optional password gate, default group), security (one-click admin token rotation), and an About section with an on-demand "Check for updates" button.
- Public page — clean, searchable landing page for end users with group tabs and multi-keyword search
- Admin panel — full link and group management behind a secure token gate
- Multiple groups per link — assign a link to as many groups as needed via a checkbox multi-select
- Sections & subsections — organize cards under named section headings, and nest one level of subsections inside them; drag to reorder in the sidebar (reflected on the public page), or drag a link straight onto a group/section/subsection
- Live updates on the public page — opened tabs fade-refresh within a fraction of a second when an admin adds, edits, reorders, or deletes anything (Server-Sent Events)
- Reusable icon library — every uploaded icon goes into a shared, searchable library; pick an existing icon for new links, bulk-select and delete, and export/import the whole library as a single file; the Settings favicon appears here too
- Stock icon picker — choose from a built-in line-icon set and a colour, instead of uploading; file links get a coloured file-type icon, and links with no icon fall back to the Settings favicon
- In-place file editor — edit attached text files (HTML, XML, JSON, TXT, CSV, MD, SVG) in the admin with an expand view, line numbers, syntax highlighting, and a find bar; saves go live instantly
- File attachments — link cards can point to an uploaded file (PDF, Office docs, archives, images, text, HTML/XML/JSON) instead of a URL, with a 100 MB cap and an extension whitelist
- Password-protected groups — gate sensitive groups with a password; per-group unlock behaviour: auto-lock after 30 s (kiosk-safe) or stay unlocked for the browser session
- Admin audit log — every admin change and every link click is recorded with the entity name, time, IP and device; live auto-refresh, search/type filters, and CSV/JSON export
- Hide links from the public page — keep a link in the admin without exposing it publicly (eye-toggle on every card)
- Custom group colors — pick any color via the native picker swatch
- Pinned default group — choose which group the public page opens on (new visitors only — returning visitors keep the tab they were last viewing)
- Copy link — one-click copy button on every public card, with a toast confirmation
- Click analytics — per-link stats: total clicks, unique visitors, today/week counts, top IPs
- Broken link checker — automatic health check every 6 hours, flags dead links in the admin
- Custom branding — upload separate logos for light and dark mode, a custom browser tab favicon, and set a custom site title
- Smart favicon fetching — three-tier strategy (parse the page for
<link rel="icon">, then/favicon.ico, then Google as fallback) so favicons work for intranet sites too; fetched in the background so saving a link is never blocked, and cached server-side - Drag-to-reorder — reorder links, groups, sections, and subsections by dragging
- Bulk actions — multi-select (shift-click for ranges, Select all, Esc to clear) to delete, move into a section, hide, or show multiple links at once
- In-app update checker — Settings shows the current build, and a one-click "Check for updates" button polls GitHub Releases for newer versions and shows the release notes inline
- Versioned database migrations — transactional, idempotent schema upgrades run on every startup; logs what changed so any past version upgrades cleanly
- Mobile-friendly UI — slide-over sidebar, full-width modal sheets, larger tap targets, and a kebab overflow menu on the admin top bar
- Import / Export — backup and restore links, groups, sections and subsections as JSON (idempotent re-import)
- Public password gate — optionally protect the public page with a password
- Light & dark mode — Apple-style design with OS preference detection
- Docker ready — single
docker compose upto deploy with persistent storage
linkPage/
├── src/
│ ├── server.js # Express API + static file serving
│ └── database.js # SQLite setup and all data access functions
├── public/
│ ├── index.html # Public page (end users)
│ ├── app.js # Public page logic
│ ├── style.css # Shared Apple-style theme (CSS variables)
│ └── admin/
│ ├── index.html # Admin panel
│ ├── admin.js # Admin panel logic
│ └── admin.css # Admin-specific styles
├── Dockerfile
└── docker-compose.yml
All persistent data lives in /app/data inside the container, mapped to ./linkpage_data on the host:
| Path | Purpose |
|---|---|
data/links.db |
SQLite database — links, groups, sections & subsections, link↔group mappings, icon library, settings, click analytics, and the admin audit log. The schema is auto-migrated on startup. |
data/admin-token.txt |
Admin token generated on first startup |
data/uploads/ |
Uploaded images, logos, favicons, and link file attachments |
There is no login system. On first startup the server generates a 64-character hex token and saves it to data/admin-token.txt. This token must be entered in the admin panel to unlock management features. It is stored in your browser's localStorage and can be rotated from the admin settings at any time.
Every link card points to /r/:id (a server-side redirect) instead of the destination URL directly. The server records the click — IP address, user agent, timestamp — in SQLite before redirecting. This works without JavaScript and is not blocked by ad blockers.
- Docker and Docker Compose installed
The easiest way to run LinkPage. No need to clone the repository or build anything.
docker-compose.prod.yml
services:
linkpage:
image: tomasneto26/linkpage:latest
ports:
- "3000:3000"
volumes:
- ./linkpage_data:/app/data
restart: unless-stopped
environment:
- PORT=3000
- DATA_DIR=/app/data
- UPLOADS_DIR=/app/data/uploads1. Create the file
curl -O https://raw.githubusercontent.com/Tomasneto404/linkPage/main/docker-compose.prod.ymlOr create it manually with the contents above.
2. Start the app
docker compose -f docker-compose.prod.yml up -dDocker will pull the image from Docker Hub automatically and start the container.
3. Get your admin token
docker compose -f docker-compose.prod.yml logs linkpageLook for a line like:
│ 🔑 Admin Token: 5c5665cfeaf662e95f98e832e2cdbaa3c2ab5d0b...
Copy the full token. It is saved to linkpage_data/admin-token.txt and reused across restarts.
4. Open the app
| Page | URL |
|---|---|
| Public page | http://localhost:3000 |
| Admin panel | http://localhost:3000/admin |
Paste the token into the admin panel to unlock link management.
Updating to the latest image
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -dClone the repository and build the image locally.
docker-compose.yml
services:
linkpage:
build: .
ports:
- "3000:3000"
volumes:
- ./linkpage_data:/app/data
restart: unless-stopped
environment:
- PORT=3000
- DATA_DIR=/app/data
- UPLOADS_DIR=/app/data/uploads
volumes:
linkpage_data:Steps
git clone https://github.com/Tomasneto404/linkPage.git
cd linkPage
docker compose up -dUpdating
git pull
docker compose down
docker compose up -d --build| Field | What it does |
|---|---|
image |
Pulls the pre-built image from Docker Hub |
build: . |
Builds the image from the local Dockerfile (Option B only) |
ports: "3000:3000" |
Exposes the app on port 3000. Change the left side for a different host port (e.g. "80:3000") |
volumes: ./linkpage_data:/app/data |
Maps a local folder to the container so the database and uploads survive restarts and image updates |
restart: unless-stopped |
Auto-restarts the container on crash or host reboot |
DATA_DIR / UPLOADS_DIR |
Tell the app where to store the database and uploaded files inside the container |
docker compose down -v
docker compose up -dA new admin token will be generated on the next startup.
npm install
npm startThe app starts on port 3000. On first run it creates data/links.db and data/admin-token.txt in the project root.
For development with auto-restart on file changes:
npm run devAll configuration is done via environment variables:
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Port the server listens on |
DATA_DIR |
./data |
Directory for the database and admin token file |
UPLOADS_DIR |
./data/uploads |
Directory for uploaded images and cached favicons |
RELEASES_REPO |
Tomasneto404/linkPage |
GitHub owner/repo polled by the in-app update checker. Override to point at a fork or private mirror. |
Read endpoints are public. All write endpoints require the X-Admin-Token header.
If a public password is configured, read endpoints also require an X-Public-Password header.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/auth/verify |
— | Verify admin token |
| POST | /api/auth/verify-public |
— | Verify public password |
| POST | /api/auth/rotate-token |
Admin | Generate a new admin token |
| GET | /api/settings |
— | Get site title, logos, favicon, pinned group, password status |
| POST | /api/settings/site-title |
Admin | Set the site title |
| POST | /api/settings/logo/:variant |
Admin | Upload logo (light or dark) |
| DELETE | /api/settings/logo/:variant |
Admin | Remove logo |
| POST | /api/settings/favicon |
Admin | Upload custom browser-tab favicon |
| DELETE | /api/settings/favicon |
Admin | Remove custom favicon |
| POST | /api/settings/pinned-group |
Admin | Pin a default group for the public page |
| POST | /api/settings/public-password |
Admin | Set public password |
| DELETE | /api/settings/public-password |
Admin | Remove public password |
| POST | /api/settings/save-favicons |
Admin | Toggle auto-save of fetched favicons into the icon library |
| GET | /api/icons |
Admin | List every icon in the reusable library with usage counts |
| POST | /api/icons |
Admin | Upload a new icon directly into the library |
| DELETE | /api/icons/:id |
Admin | Delete an icon (also clears it from any link using it) |
| POST | /api/icons/bulk-delete |
Admin | Delete multiple icons at once |
| GET | /api/icons/export |
Admin | Export the whole icon library as a self-contained JSON bundle |
| POST | /api/icons/import |
Admin | Import an icon-library bundle (idempotent, de-duped by content) |
| GET | /api/favicon-preview |
Admin | Server-side favicon fetch used by the link form's live preview |
| GET | /api/version |
Admin | Check current build against the latest GitHub release |
| GET | /api/audit |
Admin | List audit-log entries (search, type filter, pagination) |
| DELETE | /api/audit |
Admin | Clear the audit log (or prune with ?days=N) |
| GET | /api/audit/export |
Admin | Export the audit log as CSV or JSON |
| GET | /api/events |
— | Server-Sent Events stream — fires whenever data changes, so the public page can fade-refresh in real time |
| GET | /api/links |
Public | List all links (honors is_hidden, group locks, and public password) |
| POST | /api/links |
Admin | Create a link (URL or file upload) |
| PUT | /api/links/:id |
Admin | Update a link (URL or file upload) |
| POST | /api/links/:id/visibility |
Admin | Toggle a link's public visibility |
| DELETE | /api/links/:id |
Admin | Delete a link |
| GET | /api/links/:id/file |
Admin | Read an attached text file's contents (for the editor) |
| PUT | /api/links/:id/file |
Admin | Save edited contents back to an attached text file |
| POST | /api/links/reorder |
Admin | Save new link order |
| POST | /api/links/bulk-delete |
Admin | Delete multiple links |
| GET | /api/links/export |
Admin | Export links, groups, sections & subsections as JSON |
| POST | /api/links/import |
Admin | Import from JSON (idempotent — creates only what's missing) |
| GET | /api/links/:id/clicks |
Admin | Click detail for a link |
| GET | /api/groups |
Public | List all groups (with nested sections/subsections) |
| POST | /api/groups |
Admin | Create a group |
| PUT | /api/groups/:id |
Admin | Update a group (name, color, password, unlock mode) |
| DELETE | /api/groups/:id |
Admin | Delete a group |
| POST | /api/groups/reorder |
Admin | Save new group order |
| POST | /api/groups/:id/unlock |
— | Unlock a password-protected group (issues a signed cookie) |
| POST | /api/groups/:id/lock |
— | Lock a group (clears the unlock cookie) |
| POST | /api/groups/:id/sections |
Admin | Create a section (or subsection, via parent_section_id) in a group |
| PUT | /api/sections/:id |
Admin | Rename a section |
| DELETE | /api/sections/:id |
Admin | Delete a section (and its subsections) |
| POST | /api/sections/reorder |
Admin | Save new section/subsection order |
| GET | /api/stats |
Admin | Aggregate click stats for all links |
| GET | /r/:id |
— | Redirect (or stream a file attachment) and record the click |





