Compare commits

...

3 Commits

6 changed files with 77 additions and 35 deletions

View File

@ -15,14 +15,14 @@ type DefaultLayoutProps = {
}; };
export default function DefaultLayout(props: DefaultLayoutProps) { export default function DefaultLayout(props: DefaultLayoutProps) {
const { urlPathname } = usePageContext(); const pageContext = usePageContext();
return ( return (
<> <>
<div class="flex w-full flex-col font-sans"> <div class="flex w-full flex-col font-sans">
<Header /> <Header />
{urlPathname === "/" && <HeroSection />} {pageContext.urlPathname === "/" && <HeroSection />}
<div class="relative mx-auto w-full flex max-w-8xl flex-auto justify-center sm:px-2 lg:px-8 xl:px-12"> <div class="relative mx-auto w-full flex max-w-8xl flex-auto justify-center sm:px-2 lg:px-8 xl:px-12">
<div class="hidden lg:relative lg:block lg:flex-none"> <div class="hidden lg:relative lg:block lg:flex-none">

View File

@ -2,7 +2,7 @@
"scripts": { "scripts": {
"dev": "bun ./fastify-entry.ts", "dev": "bun ./fastify-entry.ts",
"build": "cross-env DEBUG=vike:error,vike:log vike build", "build": "cross-env DEBUG=vike:error,vike:log vike build",
"preview": "cross-env NODE_ENV=production bun ./fastify-server.ts", "preview": "cross-env NODE_ENV=production bun ./fastify-entry.ts",
"production": "bun run build && bun run preview", "production": "bun run build && bun run preview",
"lint": "biome lint --write .", "lint": "biome lint --write .",
"format": "biome format --write ." "format": "biome format --write ."

View File

@ -1,6 +1,8 @@
import type { SectionCache } from "@/services/DocCache";
import type { PageContext } from "vike/types"; import type { PageContext } from "vike/types";
import { useConfig } from "vike-solid/useConfig"; import { useConfig } from "vike-solid/useConfig";
import { docCache } from "@/services/DocCache";
import buildTitle from "./buildTitle"; import buildTitle from "./buildTitle";
export type Data = Awaited<ReturnType<typeof data>>; export type Data = Awaited<ReturnType<typeof data>>;
@ -9,7 +11,7 @@ export async function data(pageContext: PageContext) {
const config = useConfig(); const config = useConfig();
const { const {
exports: { tableOfContents, frontmatter }, exports: { frontmatter },
urlParsed, urlParsed,
} = pageContext; } = pageContext;
const isRoot = urlParsed.pathname === "/"; const isRoot = urlParsed.pathname === "/";
@ -19,7 +21,15 @@ export async function data(pageContext: PageContext) {
description: frontmatter?.description, description: frontmatter?.description,
}); });
let cachePathname = urlParsed.pathname.replace(/\/$/, "").replace(/^\//, "");
if (cachePathname === "") {
cachePathname = "index";
}
const doc = docCache.get(cachePathname);
return { return {
tableOfContents, sections: doc?.sections || [],
frontmatter,
}; };
} }

View File

@ -12,13 +12,15 @@ type NavigationItemProps = {
}; };
function NavigationItem(props: NavigationItemProps) { function NavigationItem(props: NavigationItemProps) {
const { urlPathname } = usePageContext(); const pageContext = usePageContext();
const [isOpened, setIsOpened] = createSignal( const [isOpened, setIsOpened] = createSignal(
props.section.links.some( props.section.links.some(
(link) => (link) =>
link.href === urlPathname || link.href === pageContext.urlPathname ||
link.subitems?.some((subitem) => subitem.href === urlPathname), link.subitems?.some(
(subitem) => subitem.href === pageContext.urlPathname,
),
), ),
); );
@ -64,9 +66,9 @@ function NavigationItem(props: NavigationItemProps) {
link={link} link={link}
onLinkClick={props.onLinkClick} onLinkClick={props.onLinkClick}
isOpened={ isOpened={
link.href === urlPathname || link.href === pageContext.urlPathname ||
link.subitems?.some( link.subitems?.some(
(subitem) => subitem.href === urlPathname, (subitem) => subitem.href === pageContext.urlPathname,
) )
} }
/> />
@ -87,14 +89,16 @@ type NavigationSubItemProps = {
function NavigationSubItem(props: NavigationSubItemProps) { function NavigationSubItem(props: NavigationSubItemProps) {
const [isOpened, setIsOpened] = createSignal(props.isOpened); const [isOpened, setIsOpened] = createSignal(props.isOpened);
const { urlPathname } = usePageContext(); const pageContext = usePageContext();
createEffect(() => { createEffect(() => {
setIsOpened( setIsOpened(
props.link.href === urlPathname || props.link.href === pageContext.urlPathname ||
props.link.subitems?.some((subitem) => subitem.href === urlPathname), props.link.subitems?.some(
(subitem) => subitem.href === pageContext.urlPathname,
),
); );
}, [urlPathname, props.link]); }, [pageContext.urlPathname, props.link]);
return ( return (
<> <>
@ -131,7 +135,7 @@ function NavigationSubItem(props: NavigationSubItemProps) {
"before:top-3 before:-translate-y-1/2 font-semibold": "before:top-3 before:-translate-y-1/2 font-semibold":
props.link.subitems, props.link.subitems,
}, },
props.link.href !== urlPathname && "before:hidden", props.link.href !== pageContext.urlPathname && "before:hidden",
isOpened() isOpened()
? "text-violet-500 before:bg-violet-500" ? "text-violet-500 before:bg-violet-500"
: "text-slate-500 before:bg-slate-300 hover:text-slate-600 hover:before:block", : "text-slate-500 before:bg-slate-300 hover:text-slate-600 hover:before:block",
@ -153,7 +157,7 @@ function NavigationSubItem(props: NavigationSubItemProps) {
onClick={props.onLinkClick} onClick={props.onLinkClick}
class={clsx( class={clsx(
"block w-full pl-3.5 before:pointer-events-none before:absolute before:top-1/2 before:-left-1 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full", "block w-full pl-3.5 before:pointer-events-none before:absolute before:top-1/2 before:-left-1 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full",
subitem.href === urlPathname subitem.href === pageContext.urlPathname
? "font-semibold text-violet-500 before:bg-violet-500" ? "font-semibold text-violet-500 before:bg-violet-500"
: "text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600 hover:before:block", : "text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600 hover:before:block",
)} )}

View File

@ -1,5 +1,4 @@
import type { TableOfContents as TableOfContentsType } from "@/remarkHeadingId"; import type { DocSection } from "@/services/DocCache";
import type { Section } from "@/libs/sections";
import type { Data } from "@/pages/+data"; import type { Data } from "@/pages/+data";
import { createSignal, createEffect, For } from "solid-js"; import { createSignal, createEffect, For } from "solid-js";
@ -8,31 +7,32 @@ import { Link } from "@/components/Link";
import clsx from "clsx"; import clsx from "clsx";
export function TableOfContents() { export function TableOfContents() {
const { tableOfContents } = useData<Data>(); const data = useData<Data>();
if (!data.sections) return null;
if (!tableOfContents) return null;
const [currentSection, setCurrentSection] = createSignal( const [currentSection, setCurrentSection] = createSignal(
tableOfContents[0]?.id, data.sections[0]?.hash,
); );
const getHeadings = () => { const getHeadings = () => {
return tableOfContents return data.sections
.map((section) => { .map((section) => {
const el = document.getElementById(section.id); if (!section.hash) return null;
const el = document.getElementById(section.hash);
if (!el) return null; if (!el) return null;
const style = window.getComputedStyle(el); const style = window.getComputedStyle(el);
const scrollMt = Number.parseFloat(style.scrollMarginTop); const scrollMt = Number.parseFloat(style.scrollMarginTop);
const top = window.scrollY + el.getBoundingClientRect().top - scrollMt; const top = window.scrollY + el.getBoundingClientRect().top - scrollMt;
return { id: section.id, top }; return { id: section.hash, top };
}) })
.filter((x): x is { id: string; top: number } => x !== null); .filter((x): x is { id: string; top: number } => x !== null);
}; };
createEffect(() => { createEffect(() => {
if (tableOfContents.length === 0) return; if (data.sections.length === 0) return;
const headings = getHeadings(); const headings = getHeadings();
function onScroll() { function onScroll() {
@ -51,10 +51,10 @@ export function TableOfContents() {
return () => { return () => {
window.removeEventListener("scroll", onScroll); window.removeEventListener("scroll", onScroll);
}; };
}, [getHeadings, tableOfContents]); }, [getHeadings, data.sections]);
function isActive(section: Section) { function isActive(section: DocSection) {
if (section.id === currentSection()) return true; if (section.hash === currentSection()) return true;
return false; return false;
// if (!section.children) return false; // if (!section.children) return false;
@ -72,19 +72,19 @@ export function TableOfContents() {
</h2> </h2>
<ol class="mt-4 space-y-3 text-sm"> <ol class="mt-4 space-y-3 text-sm">
<For each={tableOfContents}> <For each={data.sections}>
{(section) => ( {(section) => (
<li> <li>
<h3> <h3>
<Link <Link
href={`#${section.id}`} href={`#${section.hash}`}
class={clsx( class={clsx(
isActive(section) isActive(section)
? "text-violet-500" ? "text-violet-500"
: "font-normal text-slate-500 hover:text-slate-700", : "font-normal text-slate-500 hover:text-slate-700",
)} )}
> >
{section.title} {section.content}
</Link> </Link>
</h3> </h3>
</li> </li>

View File

@ -1,4 +1,5 @@
import type { FlexSearchData } from "./FlexSearchService"; import type { FlexSearchData } from "./FlexSearchService";
import type { TableOfContents } from "@/remarkHeadingId";
import type { Node } from "@markdoc/markdoc"; import type { Node } from "@markdoc/markdoc";
import { slugifyWithCounter } from "@sindresorhus/slugify"; import { slugifyWithCounter } from "@sindresorhus/slugify";
@ -7,7 +8,6 @@ import { hrtime } from "node:process";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { copyFile } from "node:fs";
const __dirname = path.resolve(); const __dirname = path.resolve();
@ -15,12 +15,14 @@ export type SectionCache = {
content: string; content: string;
frontmatter?: SectionFrontmatter; frontmatter?: SectionFrontmatter;
markdocNode: Node; markdocNode: Node;
sections: DocSection[];
}; };
export type DocSection = { export type DocSection = {
content: string; content: string;
hash?: string; hash?: string;
subsections: string[]; subsections: string[];
level?: number;
}; };
type SectionFrontmatter = { type SectionFrontmatter = {
@ -99,7 +101,12 @@ class DocCache {
if (node.type === "heading" && node.attributes?.level <= 2) { if (node.type === "heading" && node.attributes?.level <= 2) {
const hash = (node.attributes?.id as string) ?? this.slugify(content); const hash = (node.attributes?.id as string) ?? this.slugify(content);
const subsections: string[] = []; const subsections: string[] = [];
sections.push({ content, hash, subsections }); sections.push({
content,
hash,
subsections,
level: node.attributes?.level,
});
} else { } else {
sections.at(-1)?.subsections.push(content); sections.at(-1)?.subsections.push(content);
} }
@ -124,9 +131,26 @@ class DocCache {
return data; return data;
} }
private getTableOfContents(markdocNode: Node) {
const sections: DocSection[] = [];
this.extractSections(markdocNode, sections);
return sections;
}
private async cacheFile(file: string): Promise<void> { private async cacheFile(file: string): Promise<void> {
const [content, frontmatter, markdocNode] = await this.getFileContent(file); const [content, frontmatter, markdocNode] = await this.getFileContent(file);
this.cache.set(file, { content, frontmatter, markdocNode }); const filePath = file
.replace(/\+Page\.md(x)?$/, "")
.replace(/\/+/, "/")
.replace(/\/$/, "");
this.cache.set(filePath, {
content,
frontmatter,
markdocNode,
sections: this.getTableOfContents(markdocNode),
});
} }
private async populateCache(): Promise<void> { private async populateCache(): Promise<void> {
@ -157,6 +181,10 @@ class DocCache {
return DocCache.getInstance().cache; return DocCache.getInstance().cache;
} }
public getCache(): Map<string, SectionCache> {
return this.cache;
}
public get(file: string): SectionCache | undefined { public get(file: string): SectionCache | undefined {
return this.cache.get(file); return this.cache.get(file);
} }