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); } }
|
@media (prefers-reduced-motion: reduce) { .app-loading-gem { animation: none; transform: rotate(45deg); } }
|
||||||
</style>
|
</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="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="/favicon.svg" type="image/svg+xml" />
|
||||||
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
|
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "amerc-site",
|
"name": "amerc-site",
|
||||||
"version": "0.86.0-ogcard",
|
"version": "0.87.0-docroute",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -22,6 +22,9 @@ function highlightCode(code) {
|
|||||||
return out + esc(code.slice(last));
|
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 }) {
|
function DocsWorkspace({ canEdit }) {
|
||||||
const [docs, setDocs] = useState([]);
|
const [docs, setDocs] = useState([]);
|
||||||
const [sel, setSel] = useState(null);
|
const [sel, setSel] = useState(null);
|
||||||
@ -35,15 +38,33 @@ function DocsWorkspace({ canEdit }) {
|
|||||||
const loadList = useCallback(async () => {
|
const loadList = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const d = await api('/docs'); const list = d.docs || []; setDocs(list);
|
const d = await api('/docs'); const list = d.docs || []; setDocs(list);
|
||||||
// land on a useful doc instead of an empty "select a document"
|
// land on the doc named in the URL hash, else a useful default
|
||||||
const def = list.find((x) => /quickstart/i.test(x.title)) || list.find((x) => x.folder === 'start here') || list[0];
|
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);
|
if (def) setSel((cur) => cur ?? def.id);
|
||||||
} catch (e) { setErr(e.message); }
|
} catch (e) { setErr(e.message); }
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => { loadList(); }, [loadList]);
|
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(() => {
|
useEffect(() => {
|
||||||
if (sel == null) { setDoc(null); return; }
|
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]);
|
}, [sel]);
|
||||||
// decorate rendered code blocks with a hover copy button
|
// decorate rendered code blocks with a hover copy button
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -83,7 +104,7 @@ function DocsWorkspace({ canEdit }) {
|
|||||||
<div key={f} className="docs-folder">
|
<div key={f} className="docs-folder">
|
||||||
<div className="docs-folder-name">{f || '📁 root'}</div>
|
<div className="docs-folder-name">{f || '📁 root'}</div>
|
||||||
{folders[f].map((d) => (
|
{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>
|
</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 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 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 = [
|
const NAV = [
|
||||||
{ id: 'home', label: 'Tavern' },
|
{ id: 'home', label: 'Tavern' },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user