Add subdirectory routing mode for public domain deployments
Apps can now be served at domain.com/appname/ instead of requiring subdomain DNS records (appname.domain.com). Supports three subpath modes per template: native (URL base env var), strip (handle_path), and none (incompatible warning). Tested on Linux with deploy/removal lifecycle verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
<div class="form-hint-sm">
|
||||
Your app will be available at: <span id="deploy-url-preview" style="color: var(--accent); font-weight: 500;">uptime.home</span>
|
||||
</div>
|
||||
<div id="subpath-compat-warning" style="display: none; padding: 8px 12px; border-radius: 6px; background: color-mix(in srgb, #ff9800 10%, var(--card-bg)); border: 1px solid color-mix(in srgb, #ff9800 30%, transparent); margin-top: 8px; font-size: 0.85rem;"></div>
|
||||
</div>
|
||||
|
||||
<!-- DNS Configuration -->
|
||||
@@ -407,6 +408,25 @@
|
||||
const defaultSubdomain = appTemplate.subdomain || appTemplate.id.replace(/-/g, '');
|
||||
subdomainInput.value = defaultSubdomain;
|
||||
|
||||
// Show subpath compatibility warning in subdirectory mode
|
||||
const subpathWarning = document.getElementById('subpath-compat-warning');
|
||||
if (subpathWarning) {
|
||||
if (SITE.routingMode === 'subdirectory') {
|
||||
const support = appTemplate.subpathSupport || 'strip';
|
||||
if (support === 'none') {
|
||||
subpathWarning.style.display = 'block';
|
||||
subpathWarning.innerHTML = '<span style="color: #ff9800;">⚠ <strong>' + appTemplate.name + '</strong> does not support subdirectory mode. It may not work correctly at a subpath.</span>';
|
||||
} else if (support === 'strip') {
|
||||
subpathWarning.style.display = 'block';
|
||||
subpathWarning.innerHTML = '<span style="color: var(--muted);">ⓘ ' + appTemplate.name + ' has unverified subdirectory support. It may require additional configuration.</span>';
|
||||
} else {
|
||||
subpathWarning.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
subpathWarning.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-select DNS/SSL from site config (set during setup wizard)
|
||||
const cfgDnsType = SITE.defaults.dnsType || (SITE.configurationType === 'public' ? 'public' : 'private');
|
||||
const cfgSslType = SITE.defaults.sslType || (SITE.configurationType === 'public' ? 'letsencrypt' : 'internal');
|
||||
@@ -622,7 +642,10 @@
|
||||
const sslType = document.querySelector('input[name="ssl-type"]:checked').value;
|
||||
|
||||
let url = '';
|
||||
if (dnsType === 'private') {
|
||||
if (SITE.routingMode === 'subdirectory' && SITE.domain) {
|
||||
// Subdirectory mode: domain.com/app
|
||||
url = `https://${SITE.domain}/${subdomain}`;
|
||||
} else if (dnsType === 'private') {
|
||||
const protocol = sslType === 'none' ? 'http' : 'https';
|
||||
url = `${protocol}://${buildDomain(subdomain)}`;
|
||||
} else if (dnsType === 'public') {
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function serviceUrl(id) { return `https://${buildDomain(id)}`; }
|
||||
function serviceUrl(id) { return buildServiceUrl(id); }
|
||||
function el(tag, cls, txt) { const n = document.createElement(tag); if (cls) n.className = cls; if (txt) n.textContent = txt; return n; }
|
||||
|
||||
function buildGrid() {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
if (dnsPreview) dnsPreview.textContent = `${buildDomain(subdomain)} \u2192 ${ip}`;
|
||||
|
||||
const urlPreview = document.getElementById('url-preview');
|
||||
if (urlPreview) urlPreview.textContent = `https://${buildDomain(subdomain)}`;
|
||||
if (urlPreview) urlPreview.textContent = buildServiceUrl(subdomain);
|
||||
|
||||
const config = {
|
||||
subdomain, port, ip, sslType, caName, existingCa,
|
||||
@@ -534,7 +534,7 @@
|
||||
`Caddy: ${results.caddy === 'added & reloaded' ? '\u2713' : '\u2717'}`,
|
||||
`Dashboard: ${results.dashboard ? '\u2713' : '\u2717'}`
|
||||
];
|
||||
showNotification(`Service "${name}" created! ${statusParts.join(' | ')} \u2014 https://${buildDomain(subdomain)}${tailscaleOnly ? ' (Tailscale)' : ''}`, 'success', 6000);
|
||||
showNotification(`Service "${name}" created! ${statusParts.join(' | ')} \u2014 ${buildServiceUrl(subdomain)}${tailscaleOnly ? ' (Tailscale)' : ''}`, 'success', 6000);
|
||||
|
||||
closeAddServiceModal();
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
document.getElementById('service-edit-title').textContent = `Edit ${service.name}`;
|
||||
document.getElementById('edit-service-name-display').textContent = service.name;
|
||||
document.getElementById('edit-service-url-display').textContent = `https://${buildDomain(service.id)}`;
|
||||
document.getElementById('edit-service-url-display').textContent = service.url || buildServiceUrl(service.id);
|
||||
document.getElementById('edit-service-logo-preview').src = service.logo || `/assets/${service.id}.png`;
|
||||
document.getElementById('edit-subdomain').value = service.id;
|
||||
document.getElementById('edit-port').value = service.port || '';
|
||||
|
||||
@@ -33,7 +33,8 @@ const SITE = {
|
||||
dnsServers: (_cachedCfg && _cachedCfg.dnsServers) || {},
|
||||
configurationType: (_cachedCfg && _cachedCfg.configurationType) || 'homelab',
|
||||
domain: (_cachedCfg && _cachedCfg.domain) || '',
|
||||
defaults: (_cachedCfg && _cachedCfg.defaults) || {}
|
||||
defaults: (_cachedCfg && _cachedCfg.defaults) || {},
|
||||
routingMode: (_cachedCfg && _cachedCfg.routingMode) || 'subdomain'
|
||||
};
|
||||
(async function loadSiteConfig() {
|
||||
try {
|
||||
@@ -51,10 +52,12 @@ const SITE = {
|
||||
if (c.configurationType) SITE.configurationType = c.configurationType;
|
||||
if (c.domain) SITE.domain = c.domain;
|
||||
if (c.defaults) SITE.defaults = c.defaults;
|
||||
if (c.routingMode) SITE.routingMode = c.routingMode;
|
||||
// Cache config so next page load uses correct TLD even if API is slow
|
||||
localStorage.setItem('dashcaddy_site_config', JSON.stringify({
|
||||
tld: SITE.tld, dnsIp: SITE.dnsIp, dnsPort: SITE.dnsPort, dnsServers: SITE.dnsServers,
|
||||
configurationType: SITE.configurationType, domain: SITE.domain, defaults: SITE.defaults
|
||||
configurationType: SITE.configurationType, domain: SITE.domain, defaults: SITE.defaults,
|
||||
routingMode: SITE.routingMode
|
||||
}));
|
||||
// Render DNS cards dynamically based on configured servers
|
||||
renderDnsCards();
|
||||
@@ -68,6 +71,11 @@ const SITE = {
|
||||
if (proxyIpInput && SITE.dnsIp) { proxyIpInput.value = SITE.dnsIp; proxyIpInput.placeholder = SITE.dnsIp; }
|
||||
})();
|
||||
function buildDomain(sub) { return sub + SITE.tld; }
|
||||
function buildServiceUrl(sub) {
|
||||
if (SITE.routingMode === 'subdirectory' && SITE.domain) return 'https://' + SITE.domain + '/' + sub;
|
||||
if (SITE.configurationType === 'public' && SITE.domain) return 'https://' + sub + '.' + SITE.domain;
|
||||
return 'https://' + buildDomain(sub);
|
||||
}
|
||||
function getDnsServerAddr(dnsId) {
|
||||
const s = SITE.dnsServers[dnsId];
|
||||
return s ? `${s.ip}:${s.port}` : buildDomain(dnsId);
|
||||
|
||||
@@ -104,6 +104,11 @@ window.populateTimezoneSelect = function(selectEl, selectedTz) {
|
||||
} else if (currentConfigType === 'public') {
|
||||
const domain = document.getElementById('setup-public-domain')?.value?.trim() || '';
|
||||
const email = document.getElementById('setup-public-email')?.value?.trim() || '';
|
||||
const routingMode = document.querySelector('input[name="routing-mode"]:checked')?.value || 'subdirectory';
|
||||
const exampleUrls = routingMode === 'subdirectory'
|
||||
? `https://${domain}/sonarr, https://${domain}/grafana`
|
||||
: `https://sonarr.${domain}, https://grafana.${domain}`;
|
||||
const routingLabel = routingMode === 'subdirectory' ? 'Subdirectory (domain.com/app)' : 'Subdomain (app.domain.com)';
|
||||
|
||||
html += `
|
||||
<div>
|
||||
@@ -112,7 +117,8 @@ window.populateTimezoneSelect = function(selectEl, selectedTz) {
|
||||
<div><strong>Domain:</strong> ${domain}</div>
|
||||
<div><strong>SSL:</strong> Let's Encrypt</div>
|
||||
<div><strong>Email:</strong> ${email}</div>
|
||||
<div><strong>Example URLs:</strong> https://app.${domain}, https://cloud.${domain}</div>
|
||||
<div><strong>Routing:</strong> ${routingLabel}</div>
|
||||
<div><strong>Example URLs:</strong> ${exampleUrls}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -186,8 +192,9 @@ window.populateTimezoneSelect = function(selectEl, selectedTz) {
|
||||
} else if (currentConfigType === 'public') {
|
||||
config.domain = document.getElementById('setup-public-domain')?.value?.trim() || '';
|
||||
config.email = document.getElementById('setup-public-email')?.value?.trim() || '';
|
||||
config.routingMode = document.querySelector('input[name="routing-mode"]:checked')?.value || 'subdirectory';
|
||||
config.defaults = {
|
||||
dnsType: 'public',
|
||||
dnsType: config.routingMode === 'subdirectory' ? 'none' : 'public',
|
||||
sslType: 'letsencrypt',
|
||||
targetIP: 'localhost'
|
||||
};
|
||||
@@ -311,6 +318,18 @@ window.populateTimezoneSelect = function(selectEl, selectedTz) {
|
||||
};
|
||||
}
|
||||
|
||||
// Public routing mode toggle — update requirement text
|
||||
document.querySelectorAll('input[name="routing-mode"]').forEach(function(radio) {
|
||||
radio.onchange = function() {
|
||||
var note = document.getElementById('dns-requirement-note');
|
||||
if (note) {
|
||||
note.textContent = this.value === 'subdirectory'
|
||||
? 'Only one DNS record needed (for the main domain)'
|
||||
: 'You\'ll need to configure DNS manually for each subdomain';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Public navigation
|
||||
const publicBack = document.getElementById('setup-public-back');
|
||||
if (publicBack) {
|
||||
|
||||
Reference in New Issue
Block a user