Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import rehypeSlug from "rehype-slug";
import remarkDirective from "remark-directive";
import remarkMath from "remark-math";
import { customAsidePlugin } from "./src/lib/aside/customAsidePlugin";
import { apiDocsOnly } from "./src/lib/markdown/apiDocsOnly";
import { normalizeMath } from "./src/lib/markdown/normalizeMath";
import { mermaid } from "./src/utils/mermaid";
import { redirects } from "./src/utils/redirects";
Expand All @@ -18,9 +19,9 @@ export default defineConfig({
markdown: {
remarkPlugins: [remarkMath, normalizeMath, remarkDirective, mermaid, customAsidePlugin],
rehypePlugins: [
rehypeSlug,
apiDocsOnly(rehypeSlug),
[
rehypeAutolinkHeadings,
apiDocsOnly(rehypeAutolinkHeadings),
{
behavior: "append",
properties: {
Expand Down
115 changes: 65 additions & 50 deletions src/layouts/docs-layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const { title, description, image } = Astro.props;

const pages = await getCollection("Docs");
let nav = generateDocsNav(pages);

const isApiDocs = Astro.url.pathname.startsWith("/docs/api-documentation/");
---

<BaseLayout title={title} description={description} image={image}>
Expand Down Expand Up @@ -53,11 +55,17 @@ let nav = generateDocsNav(pages);
</div>
</BaseLayout>

<!-- Shareable heading anchors: rehype-autolink-headings injects
<a class="heading-anchor" data-anchor-link><span class="heading-anchor-icon">#</span></a>
after every heading in MD/MDX; .astro pages add the same markup manually.
Styles + clipboard/scroll behaviour live here so every docs page inherits them. -->
<style is:global>
{/*
Shareable heading anchors: rehype-autolink-headings injects
<a class="heading-anchor" data-anchor-link><span class="heading-anchor-icon">#</span></a>
after every heading in MD/MDX under api-documentation; .astro pages add
the same markup manually. Styles + clipboard/scroll behaviour are gated
to API docs so non-API docs keep their original heading behaviour.
*/}
{
isApiDocs && (
<style is:global>
{`
.heading-anchor {
color: inherit;
text-decoration: none;
Expand Down Expand Up @@ -97,51 +105,58 @@ let nav = generateDocsNav(pages);
:where(h1, h2, h3, h4, h5, h6)[id] {
scroll-margin-top: 7rem;
}
</style>
`}
</style>
)
}

<script>
// Heading anchor links: native <a href="#id"> handles scroll + URL hash.
// We additionally copy the full deep-link URL to the clipboard and show a
// brief green-checkmark state so users can paste directly into Slack/etc.
document.addEventListener("click", (event) => {
const target = event.target as HTMLElement | null;
const link = target?.closest<HTMLAnchorElement>("[data-anchor-link]");
if (!link) return;
const href = link.getAttribute("href");
if (!href?.startsWith("#")) return;
const url = `${window.location.origin}${window.location.pathname}${href}`;
navigator.clipboard
?.writeText(url)
.then(() => {
link.setAttribute("data-copied", "true");
window.setTimeout(() => link.removeAttribute("data-copied"), 1500);
})
.catch(() => {
// Clipboard API unavailable (older browsers / insecure context) — the
// anchor still navigates and updates the URL hash for manual copy.
{
isApiDocs && (
<script>
{`
// Heading anchor links: native <a href="#id"> handles scroll + URL hash.
// We additionally copy the full deep-link URL to the clipboard and show a
// brief green-checkmark state so users can paste directly into Slack/etc.
document.addEventListener("click", (event) => {
const target = event.target;
const link = target && target.closest && target.closest("[data-anchor-link]");
if (!link) return;
const href = link.getAttribute("href");
if (!href || !href.startsWith("#")) return;
const url = window.location.origin + window.location.pathname + href;
if (!navigator.clipboard) return;
navigator.clipboard
.writeText(url)
.then(function () {
link.setAttribute("data-copied", "true");
window.setTimeout(function () { link.removeAttribute("data-copied"); }, 1500);
})
.catch(function () {});
});
});

// On initial page load with a hash, the browser's native scroll-to-anchor
// sometimes lands before late layout (font loading, hydration of fixed
// panels) settles — leaving the target behind the sticky header. Re-run the
// scroll after `load` and on any hashchange.
function scrollToHash() {
const hash = window.location.hash;
if (!hash || hash.length < 2) return;
let id: string;
try {
id = decodeURIComponent(hash.slice(1));
} catch {
id = hash.slice(1);
}
const el = document.getElementById(id);
if (el) el.scrollIntoView({ block: "start" });
}
if (document.readyState === "complete") {
scrollToHash();
} else {
window.addEventListener("load", scrollToHash, { once: true });
}
window.addEventListener("hashchange", scrollToHash);
</script>
// On initial page load with a hash, the browser's native scroll-to-anchor
// sometimes lands before late layout (font loading, hydration of fixed
// panels) settles — leaving the target behind the sticky header. Re-run the
// scroll after \`load\` and on any hashchange.
function scrollToHash() {
const hash = window.location.hash;
if (!hash || hash.length < 2) return;
let id;
try {
id = decodeURIComponent(hash.slice(1));
} catch (e) {
id = hash.slice(1);
}
const el = document.getElementById(id);
if (el) el.scrollIntoView({ block: "start" });
}
if (document.readyState === "complete") {
scrollToHash();
} else {
window.addEventListener("load", scrollToHash, { once: true });
}
window.addEventListener("hashchange", scrollToHash);
`}
</script>
)
}
16 changes: 16 additions & 0 deletions src/lib/markdown/apiDocsOnly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Plugin } from "unified";

const API_DOCS_PATH_FRAGMENT = "/content/Docs/api-documentation/";

export function apiDocsOnly<P extends Plugin<any[], any, any>>(plugin: P): P {
const wrapped = function (this: unknown, ...args: any[]) {
const transformer = (plugin as any).apply(this, args);
if (typeof transformer !== "function") return transformer;
return function (tree: any, file: any) {
const path = String(file?.history?.[0] ?? file?.path ?? "").replace(/\\/g, "/");
if (!path.includes(API_DOCS_PATH_FRAGMENT)) return;
return transformer(tree, file);
};
};
return wrapped as unknown as P;
}
Loading