A self-hostable link portal: a responsive grid of your organization's links, set as everyone's browser new-tab / start page — plus a canary router that makes sure a device off the office network never lands on a "page not found."
No database, no framework, no build step. Two PHP pages over flat-file JSON.
- Portal (
lms-menu/index.php) — the panel grid, with inline admin editing (edit links, panels, headings, banner, and settings in place; drag-and-drop reorder; hide/delete panels). Renders fromconfig.json. - Canary router (
lms-router/index.php) — the page you actually set as the start URL. It loads a tiny "canary" image that only exists on the internal server: if it loads, redirect to the internal portal; if it errors or times out (3s), redirect to a public fallback URL. Readsrouter-config.json.
- A web host that runs PHP (no extensions or database needed).
- The two halves are usually deployed to two locations: the portal on an internal/in-house server, the router on the public web. For a demo they can sit side by side.
For the full walkthrough — verifying both branches and troubleshooting — see
INSTALL.md. The short version:
- Unpack this bundle on your web host. Serve
lms-menu/index.phpandlms-router/index.phpasindex.phpat whatever paths you choose (e.g.lms-menu/internal,lms-router/public). - Portal first run: browse to the portal. It auto-creates
config.json(seeded with a sample layout) and walks you through creating a single admin account (stored bcrypt-hashed insideconfig.json). Log in via the small lock icon in the footer, then edit in place. - Canary image: the portal ships a real 1×1
lms-menu/canary.png. The router probes this to detect the internal server. Keep it a valid, tiny, loadable image — an empty/broken file makes the router always fall back to external. - Router config: the router auto-creates
router-config.jsonwith placeholders. Edit it so:internal_url→ your internal portal URLexternal_url→ your public fallback URLcanary_image→ the URL of the portal'scanary.pngon the internal server
- Set the router URL as the browser start / new-tab page on your devices.
These files are generated/edited on the server and are not part of this bundle:
config.json— portal layout, site settings, and the bcrypt-hashed admin credential created at first-run setup (auto-generated on first run)router-config.json— the router URLs (auto-generated, then edited)
Security note:
config.json(which holds the hashed admin credential) lives inside the webroot, so it may be fetchable by URL. The bundle ships a.htaccessdenying*.jsonon Apache/LiteSpeed; nginx users add the equivalent fromNGINX.md. Moving these outside the webroot viaAPP_*env vars is optional per-deployment hardening, not something the stock install does. Don't put real credentials on a public host before at least the deny rule is in place.
Each PHP file carries an internal lms <build>.<ts6> stamp near the top. Auto-generated JSON
files carry a _lms_version field tying the data to the code that generated it.
MIT — see LICENSE. © 2026 smisco <info@smisco.biz>
Website: https://smisco.biz/lms