diff --git a/INSTALLATION.md b/INSTALLATION.md
new file mode 100644
index 0000000..560abd6
--- /dev/null
+++ b/INSTALLATION.md
@@ -0,0 +1,171 @@
+# Installation Guide - Ubuntu
+
+Panduan instalasi `doclink_web` di Ubuntu dengan Apache reverse proxy untuk URL publik seperti:
+
+```text
+https://domain.com/docklink-desktop
+```
+
+## Prerequisites
+
+- Ubuntu Server
+- Node.js 18+ atau yang kompatibel
+- Apache2
+- SSL certificate jika ingin HTTPS
+
+## 1. Copy Project
+
+Masuk ke folder project:
+
+```bash
+cd /path/to/doclink_web
+```
+
+Pastikan file berikut ada:
+
+- `server.js`
+- `styles.css`
+- `logo.png`
+- `start.sh`
+- `stop.sh`
+
+## 2. Jalankan App
+
+Set base path sesuai URL publik:
+
+```bash
+export DOCLINK_BASE_PATH=/docklink-desktop
+export PORT=5173
+node server.js
+```
+
+Kalau mau jalan di background, pakai script:
+
+```bash
+DOCLINK_BASE_PATH=/docklink-desktop PORT=5173 ./start.sh
+```
+
+### Catatan
+
+- `DOCLINK_BASE_PATH` harus sama dengan path publik di browser.
+- Nama folder project di disk tidak harus sama.
+- Aplikasi tetap bisa diakses lewat port lokal `5173`, lalu Apache yang meneruskan request ke sana.
+
+## 3. Install Dependencies Ubuntu
+
+Update paket dan install Apache:
+
+```bash
+sudo apt update
+sudo apt install -y apache2
+```
+
+Kalau Node.js belum ada, install sesuai versi yang kamu pakai di server.
+
+## 4. Aktifkan Module Apache
+
+Di Ubuntu / Debian:
+
+```bash
+sudo a2enmod proxy proxy_http ssl
+sudo systemctl restart apache2
+```
+
+## 5. Buat Virtual Host
+
+Edit konfigurasi Apache di Ubuntu:
+
+```bash
+sudo nano /etc/apache2/sites-available/domain.conf
+```
+
+Contoh konfigurasi:
+
+```apache
+
+ ServerName domain.com
+ Redirect permanent / https://domain.com/
+
+
+
+ ServerName domain.com
+
+ SSLEngine on
+ SSLCertificateFile /path/to/fullchain.pem
+ SSLCertificateKeyFile /path/to/privkey.pem
+
+ ProxyPreserveHost On
+ ProxyRequests Off
+
+ RedirectMatch 301 ^/docklink-desktop$ /docklink-desktop/
+
+ ProxyPass /docklink-desktop/ http://127.0.0.1:5173/
+ ProxyPassReverse /docklink-desktop/ http://127.0.0.1:5173/
+
+```
+
+## 6. Enable Site
+
+Aktifkan site dan reload Apache:
+
+```bash
+sudo a2ensite domain.conf
+sudo systemctl reload apache2
+```
+
+## 7. Test
+
+Buka URL berikut:
+
+```text
+https://domain.com/docklink-desktop/
+```
+
+Kalau benar, aplikasi akan tampil tanpa expose port `5173` ke publik.
+
+## 8. Stop App
+
+Kalau pakai `start.sh`:
+
+```bash
+./stop.sh
+```
+
+Kalau mau stop port tertentu:
+
+```bash
+./stop.sh 5173
+```
+
+## Troubleshooting
+
+### Halaman tidak muncul
+
+- Pastikan Node.js app masih jalan di `127.0.0.1:5173`
+- Pastikan Apache module `proxy` dan `proxy_http` aktif
+- Pastikan `DOCLINK_BASE_PATH=/docklink-desktop`
+
+## Ubuntu Notes
+
+- File konfigurasi Apache biasanya ada di:
+ - `/etc/apache2/sites-available/`
+ - `/etc/apache2/sites-enabled/`
+- Log Apache biasanya ada di:
+ - `/var/log/apache2/access.log`
+ - `/var/log/apache2/error.log`
+- Service Apache dikelola dengan:
+ ```bash
+ sudo systemctl status apache2
+ sudo systemctl restart apache2
+ sudo systemctl reload apache2
+ ```
+
+### CSS atau logo tidak loading
+
+- Cek apakah request asset ikut lewat path `/docklink-desktop/...`
+- Pastikan Apache proxy tidak memotong base path secara salah
+
+### Redirect balik ke root
+
+- Pastikan semua request ke app memakai base path yang sama
+- Gunakan `DOCLINK_BASE_PATH=/docklink-desktop`
diff --git a/INSTALLATION_UBUNTU.md b/INSTALLATION_UBUNTU.md
new file mode 100644
index 0000000..fba016a
--- /dev/null
+++ b/INSTALLATION_UBUNTU.md
@@ -0,0 +1,246 @@
+# DocLink Web Installation Guide for Ubuntu
+
+This guide explains how to run `doclink_web` on Ubuntu behind Apache, using a subfolder URL such as:
+
+```text
+https://domain.com/docklink-desktop
+```
+
+## 1. Requirements
+
+- Ubuntu Server
+- Apache 2
+- Node.js 18+ or compatible
+- A valid SSL certificate if you want HTTPS
+
+## 2. Copy the Application
+
+Place the project on the server and enter the project directory:
+
+```bash
+cd /path/to/doclink_web
+```
+
+Make sure these files exist:
+
+- `server.js`
+- `styles.css`
+- `logo.png`
+- `start.sh`
+- `stop.sh`
+
+## 3. Install Apache on Ubuntu
+
+Update package lists and install Apache:
+
+```bash
+sudo apt update
+sudo apt install -y apache2
+```
+
+If Node.js is not installed yet, install the version you want to use on the server.
+
+## 4. Create the systemd Service
+
+The app should run as a service so it starts automatically and keeps running in the background.
+
+Create an environment file, for example:
+
+```bash
+sudo nano /etc/doclink-web.env
+```
+
+Example content:
+
+```bash
+DOCLINK_BASE_PATH=/docklink-desktop
+PORT=5173
+DOCLINK_API_BASE=https://devbandungraya.aplikasi.web.id/one-api-doctor/doctor_mitra
+```
+
+Then create the service file:
+
+```bash
+sudo nano /etc/systemd/system/doclink-web.service
+```
+
+Example content:
+
+```ini
+[Unit]
+Description=DocLink Web
+After=network.target
+
+[Service]
+Type=simple
+WorkingDirectory=/path/to/doclink_web
+EnvironmentFile=/etc/doclink-web.env
+ExecStart=/usr/bin/node /path/to/doclink_web/server.js
+Restart=on-failure
+RestartSec=3
+User=www-data
+Group=www-data
+
+[Install]
+WantedBy=multi-user.target
+```
+
+Reload systemd and enable the service:
+
+```bash
+sudo systemctl daemon-reload
+sudo systemctl enable doclink-web
+sudo systemctl start doclink-web
+```
+
+Check status:
+
+```bash
+sudo systemctl status doclink-web
+```
+
+## 5. Run the Application Manually
+
+This step is optional. Use it only if you want to test the app without systemd.
+
+Run it directly:
+
+```bash
+export DOCLINK_BASE_PATH=/docklink-desktop
+export PORT=5173
+node server.js
+```
+
+If you prefer the helper script:
+
+```bash
+DOCLINK_BASE_PATH=/docklink-desktop PORT=5173 ./start.sh
+```
+
+Important notes:
+
+- `DOCLINK_BASE_PATH` must match the public URL path.
+- The local project folder name can be anything.
+- Apache will forward requests to the Node.js app on `127.0.0.1:5173`.
+
+## 6. Enable Apache Modules
+
+Enable the required proxy and SSL modules:
+
+```bash
+sudo a2enmod proxy proxy_http ssl
+sudo systemctl restart apache2
+```
+
+## 7. Configure Apache Virtual Host
+
+Create or edit your site config in:
+
+```bash
+sudo nano /etc/apache2/sites-available/domain.conf
+```
+
+Example configuration:
+
+```apache
+
+ ServerName domain.com
+ Redirect permanent / https://domain.com/
+
+
+
+ ServerName domain.com
+
+ SSLEngine on
+ SSLCertificateFile /path/to/fullchain.pem
+ SSLCertificateKeyFile /path/to/privkey.pem
+
+ ProxyPreserveHost On
+ ProxyRequests Off
+
+ RedirectMatch 301 ^/docklink-desktop$ /docklink-desktop/
+
+ ProxyPass /docklink-desktop/ http://127.0.0.1:5173/
+ ProxyPassReverse /docklink-desktop/ http://127.0.0.1:5173/
+
+```
+
+If you want a reusable deployment template, start from:
+
+- `deploy/apache-vhost.template.conf`
+- `deploy/env.template`
+
+Then replace:
+
+- `__SERVER_NAME__`
+- `__BASE_PATH__`
+- `__APP_PORT__`
+- `__SSL_CERT_FILE__`
+- `__SSL_KEY_FILE__`
+
+## 8. Enable the Site
+
+Enable the site and reload Apache:
+
+```bash
+sudo a2ensite domain.conf
+sudo systemctl reload apache2
+```
+
+## 9. Verify the Deployment
+
+Open the application in the browser:
+
+```text
+https://domain.com/docklink-desktop/
+```
+
+If everything is configured correctly, the application should load without exposing port `5173` publicly.
+
+## 10. Stop the Application
+
+If you started the app with the helper script:
+
+```bash
+./stop.sh
+```
+
+Or stop a specific port:
+
+```bash
+./stop.sh 5173
+```
+
+## 11. Ubuntu-Specific Locations
+
+- Apache site configs:
+ - `/etc/apache2/sites-available/`
+ - `/etc/apache2/sites-enabled/`
+- Apache logs:
+ - `/var/log/apache2/access.log`
+ - `/var/log/apache2/error.log`
+- Apache service commands:
+ ```bash
+ sudo systemctl status apache2
+ sudo systemctl restart apache2
+ sudo systemctl reload apache2
+ ```
+
+## 12. Troubleshooting
+
+### The page does not open
+
+- Check that the `doclink-web` service is running
+- Check that the Node.js app is listening on `127.0.0.1:5173`
+- Check that Apache modules `proxy` and `proxy_http` are enabled
+- Confirm that `DOCLINK_BASE_PATH=/docklink-desktop`
+
+### CSS or logo does not load
+
+- Confirm requests are going through `/docklink-desktop/...`
+- Check the Apache proxy configuration for the correct trailing slash
+
+### Redirects go back to `/`
+
+- Make sure every public request uses the same base path
+- Keep `DOCLINK_BASE_PATH` aligned with the URL folder
diff --git a/README.md b/README.md
index 950853f..e7e5227 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,14 @@ Open:
http://localhost:5173
```
+To serve under a subfolder like `https://domain.com/folder`, set:
+
+```bash
+DOCLINK_BASE_PATH=/folder npm start
+```
+
+That makes internal links, HTMX requests, and redirects stay under the same folder.
+
## Click To Run
If you want a one-click launcher on macOS/Linux:
@@ -55,6 +63,7 @@ If you run the server in the foreground, stop it with `Ctrl+C`.
- If the upstream login rejects the credentials or is unavailable, demo mode creates a local session so the UI can still be exercised.
- Order search, order detail helpers, FPP loading, password change, and special message save are wired through the API adapter with mock fallback for local preview.
- `start.sh` writes a pid file named `.doclink-web.pid` and a log file named `doclink-web.log`.
+- Deployment templates are in `deploy/`.
## Main Files
diff --git a/deploy/apache-vhost.template.conf b/deploy/apache-vhost.template.conf
new file mode 100644
index 0000000..1ee27c5
--- /dev/null
+++ b/deploy/apache-vhost.template.conf
@@ -0,0 +1,20 @@
+
+ ServerName __SERVER_NAME__
+ Redirect permanent / https://__SERVER_NAME__/
+
+
+
+ ServerName __SERVER_NAME__
+
+ SSLEngine on
+ SSLCertificateFile __SSL_CERT_FILE__
+ SSLCertificateKeyFile __SSL_KEY_FILE__
+
+ ProxyPreserveHost On
+ ProxyRequests Off
+
+ RedirectMatch 301 ^/__BASE_PATH__$ /__BASE_PATH__/
+
+ ProxyPass /__BASE_PATH__/ http://127.0.0.1:__APP_PORT__/
+ ProxyPassReverse /__BASE_PATH__/ http://127.0.0.1:__APP_PORT__/
+
diff --git a/deploy/doclink-web.service.template b/deploy/doclink-web.service.template
new file mode 100644
index 0000000..d5511bf
--- /dev/null
+++ b/deploy/doclink-web.service.template
@@ -0,0 +1,16 @@
+[Unit]
+Description=DocLink Web
+After=network.target
+
+[Service]
+Type=simple
+WorkingDirectory=__WORKING_DIRECTORY__
+EnvironmentFile=__ENV_FILE__
+ExecStart=/usr/bin/node __WORKING_DIRECTORY__/server.js
+Restart=on-failure
+RestartSec=3
+User=__RUN_USER__
+Group=__RUN_GROUP__
+
+[Install]
+WantedBy=multi-user.target
diff --git a/deploy/env.template b/deploy/env.template
new file mode 100644
index 0000000..bfe5e0d
--- /dev/null
+++ b/deploy/env.template
@@ -0,0 +1,3 @@
+DOCLINK_BASE_PATH=/__BASE_PATH__
+PORT=__APP_PORT__
+DOCLINK_API_BASE=https://devbandungraya.aplikasi.web.id/one-api-doctor/doctor_mitra
diff --git a/server.js b/server.js
index 8a7f399..1656925 100644
--- a/server.js
+++ b/server.js
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
import { extname } from "node:path";
const PORT = Number(process.env.PORT || 5173);
+const BASE_PATH = normalizeBasePath(process.env.DOCLINK_BASE_PATH || "");
const API_BASE =
process.env.DOCLINK_API_BASE ||
"https://devbandungraya.aplikasi.web.id/one-api-doctor/doctor_mitra";
@@ -23,6 +24,36 @@ function escapeHtml(value) {
.replaceAll('"', """);
}
+function normalizeBasePath(value) {
+ const raw = String(value || "").trim();
+ if (!raw || raw === "/") return "";
+ const withLeadingSlash = raw.startsWith("/") ? raw : `/${raw}`;
+ return withLeadingSlash.replace(/\/+$/, "");
+}
+
+function appPath(pathname = "/") {
+ const text = String(pathname || "/");
+ if (/^https?:\/\//i.test(text) || text.startsWith("//")) return text;
+ if (!BASE_PATH) return text.startsWith("/") ? text : `/${text}`;
+ if (text === BASE_PATH || text.startsWith(`${BASE_PATH}/`)) return text;
+ if (text === "/" || text === "") return BASE_PATH;
+ if (text.startsWith("/")) return `${BASE_PATH}${text}`;
+ return `${BASE_PATH}/${text}`;
+}
+
+function stripBasePath(pathname) {
+ const path = String(pathname || "/");
+ if (!BASE_PATH) return path || "/";
+ if (path === BASE_PATH) return "/";
+ if (path.startsWith(`${BASE_PATH}/`)) return path.slice(BASE_PATH.length) || "/";
+ return path;
+}
+
+function rewriteHtmlPaths(html) {
+ if (!BASE_PATH || typeof html !== "string") return html;
+ return html.replace(/\b(href|src|action|hx-get|hx-post)="\/(?!\/)/g, `$1="${BASE_PATH}/`);
+}
+
function statusClass(status) {
const mapping = {
Processing: "warning",
@@ -1998,13 +2029,15 @@ function json(res, status, payload) {
}
function redirect(res, location, headers = {}) {
- res.writeHead(302, { Location: location, ...headers });
+ const resolvedLocation = typeof location === "string" && location.startsWith("/") ? appPath(location) : location;
+ res.writeHead(302, { Location: resolvedLocation, ...headers });
res.end();
}
function html(res, status, body, headers = {}) {
+ const output = rewriteHtmlPaths(body);
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", ...headers });
- res.end(body);
+ res.end(output);
}
function isHtmx(req) {
@@ -2204,7 +2237,7 @@ async function fragmentResultDetail(session, resultId) {
}
async function renderRoute(req, res, url) {
- const path = url.pathname;
+ const path = stripBasePath(url.pathname);
const query = Object.fromEntries(url.searchParams.entries());
const session = readSession(req);
const authed = Boolean(session);
@@ -2572,7 +2605,7 @@ async function renderRoute(req, res, url) {
return;
}
- html(res, 404, emptyRoute(path));
+ html(res, 404, emptyRoute(url.pathname));
}
const server = http.createServer(async (req, res) => {