Client Runtime
@sigx/ssg ships a small set of interactive behaviors for the rendered site, all exported from @sigx/ssg/client. The generated client entry wires every one of them automatically — this page is for when you author a custom client entry, or want to drive them programmatically.
import {
setupPrefetch,
installSpaNavigation,
installPackageManagerSwitcher,
installCodeCopy,
} from '@sigx/ssg/client';
// after `.hydrate('#app')` in a custom entry:
setupPrefetch();
installSpaNavigation(router);
installPackageManagerSwitcher();
installCodeCopy();
Each install* helper is idempotent, client-only, and returns a disposer that removes its listeners.
Package-manager switcher
A shell code fence whose lines are npm/pnpm/yarn/bun commands (install/add, run, dlx, create, remove, …) is rendered with an npm/pnpm/yarn/bun tab strip, with all four variants pre-rendered server-side. The client only flips which variant is visible and persists the choice — it never rewrites highlighted text, so it never fights hydration.
pnpm add @sigx/ssg
pnpm add -D vite @sigx/viteinstallPackageManagerSwitcher() registers the tab clicks, a cross-tab storage sync, and a coalesced MutationObserver for windows that mount after first paint (SPA navigation, late hydration). The first-shown variant comes from markdown.shiki.defaultPackageManager (default 'pnpm'); a visitor's saved choice overrides it.
Reading and setting the selection
import {
getPackageManager,
setPackageManager,
onPackageManagerChange,
} from '@sigx/ssg/client';
getPackageManager(); // 'pnpm' | 'npm' | 'yarn' | 'bun'
setPackageManager('bun'); // updates every window, persists, notifies subscribers
const off = onPackageManagerChange((pm) => {
console.log('package manager is now', pm);
});
getPackageManager resolves the in-page selection, else the persisted one, else the first window's server-rendered default, else 'pnpm'. setPackageManager throws on an unknown value. onPackageManagerChange returns an unsubscribe function.
Translating a command yourself
The pure parser/renderer behind the switcher is exported too — no DOM required, so it works in SSR and tests:
import {
parsePackageManagerCommand,
translatePackageManagerCommand,
PACKAGE_MANAGERS,
DEFAULT_PACKAGE_MANAGER,
} from '@sigx/ssg/client';
translatePackageManagerCommand('npm install -D vite', 'pnpm');
// → 'pnpm add -D vite'
PACKAGE_MANAGERS; // ['pnpm', 'npm', 'yarn', 'bun']
DEFAULT_PACKAGE_MANAGER; // 'pnpm'
Search
Set search: true in ssg.config.ts to emit a search-index.json at build time (see Configuration → Built-in search). On the client, load it once and rank queries against it:
import { loadSearchIndex, searchPages } from '@sigx/ssg/client';
const index = await loadSearchIndex();
const results = searchPages(index, 'island hydration', { limit: 10 });
for (const r of results) {
// r.path, r.title, r.score, r.excerpt?, r.anchor?
const href = r.anchor ? r.path + r.anchor : r.path;
}
loadSearchIndex({ base }) handles subpath deploys (pass url to override the location entirely). searchPages is pure and dependency-free: every whitespace-separated term must match the title, a heading, the description, or the body (AND semantics), and title matches rank highest. Each result carries an excerpt around the first body match and an anchor (#id) for the best-matching heading, for deep links.
To surface a theme's built-in search UI (such as the daisyUI theme's ⌘K command palette) instead of wiring your own, set site.search: true alongside search: true — it reaches layouts via LayoutProps.site. See Themes.
Copy-code buttons
Shiki emits a copy button in every code-window header. installCodeCopy() wires them with one delegated listener; package-manager windows copy only the currently visible variant. It is safe to call on pages without any code blocks.
Prefetch on hover
Prefetch-on-hover is enabled by default (config prefetch, with a 100 ms delay) and wired automatically. Call the helpers directly in a custom entry:
import { prefetch, setupPrefetch } from '@sigx/ssg/client';
setupPrefetch({ delay: 150 });
prefetch('/blog/hello-world');
SPA navigation
installSpaNavigation(router, { base, trailingSlash }) routes same-origin internal anchor clicks — including the plain <a> elements MDX content links compile to — through the router instead of full page reloads. The browser keeps handling modified clicks, external/mailto:/tel: links, target/download anchors, same-document #hash scrolls, data-no-spa opt-outs, and anything that already called preventDefault(). Pass the deploy base on subpath deploys (it is stripped before the push). trailingSlash (default 'always') makes pushed URLs match the canonical hard-load form — folder routes gain the trailing slash, while paths with a file extension like /sitemap.xml are pushed verbatim. Returns an uninstall function.
It is wired automatically in the generated client entry; opt out globally with spaNavigation: false (see Configuration → SPA navigation).
Rendering-mode helpers
import { isStaticPage, getInitialState } from '@sigx/ssg/client';
if (!isStaticPage()) {
const state = getInitialState<{ user: string }>();
}
isStaticPage() is true when the document was statically generated (no data-ssr attribute); getInitialState() parses the JSON embedded in #__SSG_STATE__, or returns null.
Next steps
- Configuration & Content — the config options these helpers back.
- Themes — a prebuilt theme that wires all of this for you.
- API Reference — every
@sigx/ssg/clientexport with its signature.
