docs: shareable URLs per document (0.87.0)
the docs site kept the selected document only in React state, so you couldn't bookmark, share, or refresh-into a specific doc, and browser back/forward did nothing — a real gap for a developer docs site people want to link into. - each doc now has a slug URL: docs.amerc.ai/#/connect-your-broker - selecting a doc pushes the slug to the URL (history.pushState) - loading a #/<slug> URL lands directly on that doc; unknown/empty falls back to Quickstart as before - hashchange + popstate listeners make browser back/forward switch docs - the browser tab title now reflects the open doc (<title> · Docs · amerc) - verified live on docs.amerc.ai: click updates URL+view+title, deep-links land correctly, back/forward navigate between docs
This commit is contained in:
parent
f0c8ea22cf
commit
60d045b55f
@ -17,7 +17,7 @@
|
||||
@media (prefers-reduced-motion: reduce) { .app-loading-gem { animation: none; transform: rotate(45deg); } }
|
||||
</style>
|
||||
<meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." />
|
||||
<meta name="version" content="0.86.0-ogcard" />
|
||||
<meta name="version" content="0.87.0-docroute" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amerc-site",
|
||||
"version": "0.86.0-ogcard",
|
||||
"version": "0.87.0-docroute",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -22,6 +22,9 @@ function highlightCode(code) {
|
||||
return out + esc(code.slice(last));
|
||||
}
|
||||
|
||||
// URL-safe slug from a doc title, for shareable #/<slug> doc links.
|
||||
const slugify = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'doc';
|
||||
|
||||
function DocsWorkspace({ canEdit }) {
|
||||
const [docs, setDocs] = useState([]);
|
||||
const [sel, setSel] = useState(null);
|
||||
@ -35,15 +38,33 @@ function DocsWorkspace({ canEdit }) {
|
||||
const loadList = useCallback(async () => {
|
||||
try {
|
||||
const d = await api('/docs'); const list = d.docs || []; setDocs(list);
|
||||
// land on a useful doc instead of an empty "select a document"
|
||||
const def = list.find((x) => /quickstart/i.test(x.title)) || list.find((x) => x.folder === 'start here') || list[0];
|
||||
// land on the doc named in the URL hash, else a useful default
|
||||
const slug = window.location.hash.replace(/^#\/?/, '');
|
||||
const fromHash = slug && list.find((x) => slugify(x.title) === slug);
|
||||
const def = fromHash || list.find((x) => /quickstart/i.test(x.title)) || list.find((x) => x.folder === 'start here') || list[0];
|
||||
if (def) setSel((cur) => cur ?? def.id);
|
||||
} catch (e) { setErr(e.message); }
|
||||
}, []);
|
||||
useEffect(() => { loadList(); }, [loadList]);
|
||||
// shareable doc URLs: select via #/<slug>, and follow browser back/forward
|
||||
const openDoc = useCallback((d) => {
|
||||
setSel(d.id);
|
||||
const slug = slugify(d.title);
|
||||
if (window.location.hash !== `#/${slug}`) window.history.pushState(null, '', `${window.location.pathname}${window.location.search}#/${slug}`);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const onHash = () => {
|
||||
const slug = window.location.hash.replace(/^#\/?/, '');
|
||||
const d = docs.find((x) => slugify(x.title) === slug);
|
||||
if (d) setSel(d.id);
|
||||
};
|
||||
window.addEventListener('hashchange', onHash);
|
||||
window.addEventListener('popstate', onHash);
|
||||
return () => { window.removeEventListener('hashchange', onHash); window.removeEventListener('popstate', onHash); };
|
||||
}, [docs]);
|
||||
useEffect(() => {
|
||||
if (sel == null) { setDoc(null); return; }
|
||||
api(`/docs/${sel}`).then((d) => { setDoc(d.doc); setDraft({ title: d.doc.title, folder: d.doc.folder, body: d.doc.body }); setEditing(false); }).catch((e) => setErr(e.message));
|
||||
api(`/docs/${sel}`).then((d) => { setDoc(d.doc); setDraft({ title: d.doc.title, folder: d.doc.folder, body: d.doc.body }); setEditing(false); document.title = `${d.doc.title} · Docs · amerc`; }).catch((e) => setErr(e.message));
|
||||
}, [sel]);
|
||||
// decorate rendered code blocks with a hover copy button
|
||||
useEffect(() => {
|
||||
@ -83,7 +104,7 @@ function DocsWorkspace({ canEdit }) {
|
||||
<div key={f} className="docs-folder">
|
||||
<div className="docs-folder-name">{f || '📁 root'}</div>
|
||||
{folders[f].map((d) => (
|
||||
<button key={d.id} className={`docs-item${sel === d.id ? ' on' : ''}`} onClick={() => setSel(d.id)}>{d.title}</button>
|
||||
<button key={d.id} className={`docs-item${sel === d.id ? ' on' : ''}`} onClick={() => openDoc(d)}>{d.title}</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -10,7 +10,7 @@ import { api } from './api.js';
|
||||
const Menu = ({ size = 20 }) => (<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></svg>);
|
||||
const X = ({ size = 20 }) => (<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>);
|
||||
|
||||
const RELEASE = '0.86.0-ogcard';
|
||||
const RELEASE = '0.87.0-docroute';
|
||||
|
||||
const NAV = [
|
||||
{ id: 'home', label: 'Tavern' },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user