mirror of
https://github.com/kbenestad/mdcms.git
synced 2026-06-18 07:24:31 +00:00
feat: clean URLs for section-id pages
Pages whose filename matches a nav section-id now get a clean pathname
URL (e.g. /timesheet) instead of the hash-based /#pages/timesheet.md.
- _initialPathname captured at IIFE start; handles ?_route= from 404.html
- basePath determined by initBasePath() after nav data loads; subpath
deployments (e.g. /mysite/) handled automatically
- navigateTo() uses replaceState to /slug for section-id pages and falls
back to #hash for everything else
- popstate listener handles browser history if a clean URL was the entry
- resolveSlugToFile() validates that slug is both a section code and has
a pages/{slug}.md entry in navData
- app/404.html added for GitHub Pages SPA routing
https://claude.ai/code/session_01Ai8xRvmrzdhuTKiRQ2fnn9
This commit is contained in:
parent
8295cbca2c
commit
31330d19e2
3 changed files with 120 additions and 3 deletions
30
app/404.html
Normal file
30
app/404.html
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redirecting…</title>
|
||||
<script>
|
||||
// SPA routing for GitHub Pages: the server returns this 404 page for any path
|
||||
// it can't resolve. We encode the intended path as ?_route= and redirect to the
|
||||
// app root so index.html can pick it up and render the right page.
|
||||
(function () {
|
||||
var path = window.location.pathname;
|
||||
var search = window.location.search;
|
||||
var hash = window.location.hash;
|
||||
|
||||
// On GitHub Pages project sites the app lives at /repo-name/, so we keep
|
||||
// that prefix and only encode the segment after it.
|
||||
var parts = path.split('/');
|
||||
var isGhPages = window.location.hostname.endsWith('.github.io') && parts.length > 2;
|
||||
var base = isGhPages ? '/' + parts[1] + '/' : '/';
|
||||
var route = '/' + parts.slice(isGhPages ? 2 : 1).join('/');
|
||||
|
||||
var qs = '_route=' + encodeURIComponent(route);
|
||||
if (search) qs += '&' + search.slice(1);
|
||||
|
||||
window.location.replace(window.location.origin + base + '?' + qs + hash);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
|
@ -1082,6 +1082,22 @@ body {
|
|||
'use strict';
|
||||
|
||||
// ─── State ────────────────────────────────────────────────
|
||||
|
||||
// Capture the intended pathname before anything mutates the URL.
|
||||
// 404.html (GitHub Pages SPA routing) encodes the original path as ?_route=.
|
||||
const _initialPathname = (() => {
|
||||
const route = new URLSearchParams(window.location.search).get('_route');
|
||||
if (route) {
|
||||
const u = new URL(window.location);
|
||||
u.searchParams.delete('_route');
|
||||
window.history.replaceState(null, '', u);
|
||||
return route;
|
||||
}
|
||||
return window.location.pathname;
|
||||
})();
|
||||
|
||||
let basePath = '/'; // set by initBasePath() once nav data is loaded
|
||||
|
||||
let config = {};
|
||||
let navData = [];
|
||||
let navSections = [];
|
||||
|
|
@ -3022,6 +3038,34 @@ function fmtDatetime(dtStr) {
|
|||
});
|
||||
}
|
||||
|
||||
// ─── Clean URL routing ────────────────────────────────────
|
||||
|
||||
// Returns the file (e.g. "pages/timesheet.md") if the given slug is a section-id
|
||||
// that has a matching pages/{slug}.md entry in navData.
|
||||
function resolveSlugToFile(slug) {
|
||||
if (!slug || !navSections.some(s => s.code === slug)) return null;
|
||||
const file = `pages/${slug}.md`;
|
||||
return navData.some(p => p.file === file) ? file : null;
|
||||
}
|
||||
|
||||
// Called once after navData/navSections are populated.
|
||||
// Sets basePath (the app root) and returns the initial page file when the URL
|
||||
// already contains a section-id slug (direct access via clean URL or 404 redirect).
|
||||
function initBasePath() {
|
||||
const segments = _initialPathname.split('/').filter(Boolean);
|
||||
if (segments.length > 0) {
|
||||
const lastSeg = segments[segments.length - 1];
|
||||
const file = resolveSlugToFile(lastSeg);
|
||||
if (file) {
|
||||
const slugIdx = _initialPathname.lastIndexOf('/' + lastSeg);
|
||||
basePath = _initialPathname.slice(0, slugIdx + 1) || '/';
|
||||
return file;
|
||||
}
|
||||
}
|
||||
basePath = _initialPathname.endsWith('/') ? _initialPathname : _initialPathname + '/';
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Page loading ─────────────────────────────────────────
|
||||
async function navigateTo(file) {
|
||||
currentPage = file;
|
||||
|
|
@ -3046,14 +3090,23 @@ function fmtDatetime(dtStr) {
|
|||
}
|
||||
}
|
||||
|
||||
// Build a clean URL: keep origin + path, set ?cat only when non-default, set hash to conceptual file
|
||||
// Build a clean URL. Pages whose filename matches a nav section-id get a clean
|
||||
// pathname (e.g. /timesheet); all other pages keep the hash-based URL.
|
||||
const u = new URL(window.location);
|
||||
if (categoriesUse && activeCategory && activeCategory !== defaultCategoryCode) {
|
||||
u.searchParams.set('cat', activeCategory);
|
||||
} else {
|
||||
u.searchParams.delete('cat');
|
||||
}
|
||||
const slugMatch = file.match(/^pages\/([^/]+)\.md$/);
|
||||
const slug = slugMatch ? slugMatch[1] : null;
|
||||
if (slug && navSections.some(s => s.code === slug)) {
|
||||
u.pathname = basePath + slug;
|
||||
u.hash = '';
|
||||
} else {
|
||||
u.pathname = basePath;
|
||||
u.hash = '#' + file;
|
||||
}
|
||||
window.history.replaceState(null, '', u);
|
||||
|
||||
contentEl.innerHTML = '<div class="loading-spinner"></div>';
|
||||
|
|
@ -3129,6 +3182,13 @@ function fmtDatetime(dtStr) {
|
|||
if (page && page !== currentPage) navigateTo(page);
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', () => {
|
||||
const slug = window.location.pathname.replace(basePath, '').replace(/^\//, '').replace(/\/$/, '');
|
||||
const pathPage = slug ? resolveSlugToFile(slug) : null;
|
||||
const page = pathPage || getPageFromHash();
|
||||
if (page && page !== currentPage) navigateTo(page);
|
||||
});
|
||||
|
||||
// ─── Scroll to top ───────────────────────────────────────
|
||||
const scrollBtn = document.getElementById('scrollTop');
|
||||
window.addEventListener('scroll', () => {
|
||||
|
|
@ -3197,6 +3257,7 @@ function fmtDatetime(dtStr) {
|
|||
}
|
||||
renderNav();
|
||||
}
|
||||
const routeFromPath = initBasePath();
|
||||
|
||||
if (config.search !== false) {
|
||||
try {
|
||||
|
|
@ -3225,7 +3286,7 @@ function fmtDatetime(dtStr) {
|
|||
}
|
||||
|
||||
const hashPage = getPageFromHash();
|
||||
await navigateTo(hashPage || defaultPage());
|
||||
await navigateTo(routeFromPath || hashPage || defaultPage());
|
||||
} catch (err) {
|
||||
document.getElementById('app').innerHTML = `<div style="max-width:600px;margin:4rem auto;padding:2rem;text-align:center;font-family:system-ui;">
|
||||
<h1 style="color:#EF4444;font-size:1.5rem;">MD-CMS Error</h1>
|
||||
|
|
|
|||
|
|
@ -197,6 +197,32 @@ When a site uses category-suffixed page files (e.g. `page.current.md`) and is ho
|
|||
|
||||
---
|
||||
|
||||
## Clean URLs for section-id pages (`app/index.html`, `app/404.html`)
|
||||
|
||||
Pages whose filename matches a nav section-id can now be accessed at a clean URL path (e.g. `example.com/timesheet`) instead of the hash-based URL (`example.com/#pages/timesheet.md`).
|
||||
|
||||
### How it works
|
||||
|
||||
When you navigate to a page whose base filename (`timesheet`) matches a `code` entry in the `sections:` block of `nav.yml`, the renderer uses `history.replaceState` to rewrite the URL from `/#pages/timesheet.md` to `/timesheet`. All other pages continue to use hash-based URLs unchanged.
|
||||
|
||||
On startup, if the URL pathname already contains a section-id slug (because the user typed or was linked to `example.com/timesheet` directly), the renderer detects it, sets the correct base path, and loads the matching page.
|
||||
|
||||
Subpath deployments (e.g. `example.com/mysite/`) are handled automatically: the renderer determines the base from the initial pathname.
|
||||
|
||||
### 404.html for GitHub Pages
|
||||
|
||||
A new `app/404.html` file enables direct clean-URL access on GitHub Pages. When GitHub Pages serves the 404 page for an unknown path (e.g. `/timesheet`), `404.html` encodes the path as `?_route=/timesheet` and redirects to the app root. `index.html` reads `_route`, cleans up the URL, and routes to the right page. For other static hosts (Netlify, Cloudflare Pages, etc.) a `/*` → `/index.html` rewrite rule in the host's config achieves the same result.
|
||||
|
||||
### Condition
|
||||
|
||||
Only pages files that are both:
|
||||
1. located in `pages/` with a name matching a section `code` in `nav.yml`, and
|
||||
2. present in the `pages:` list in `nav.yml`
|
||||
|
||||
…get a clean URL. All other pages continue to use `#` routing.
|
||||
|
||||
---
|
||||
|
||||
## Fix: `config.yml` YAML parse errors now abort the build with a clear message
|
||||
|
||||
A malformed `config.yml` (e.g. a stray tab character, which YAML forbids as a token starter) previously caused `read_config` to silently return an empty dict. The build would proceed with no config — categories disabled, no default category code — producing a broken `nav.yml` with wrong filenames and missing `variants` fields, so category-variant pages would not appear in the sidebar.
|
||||
|
|
|
|||
Loading…
Reference in a new issue