mirror of
https://github.com/prometheus/docs.git
synced 2026-02-05 06:45:01 +01:00
Add Pagefind-based search
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
17
package-lock.json
generated
17
package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"hast-util-from-html": "^2.0.3",
|
||||
"hast-util-select": "^6.0.4",
|
||||
"hastscript": "^9.0.1",
|
||||
"html-entities": "^2.6.0",
|
||||
"next": "15.3.1",
|
||||
"octokit": "^4.1.3",
|
||||
"react": "^19.0.0",
|
||||
@@ -5485,6 +5486,22 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-entities": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
|
||||
"integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/mdevils"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://patreon.com/mdevils"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-url-attributes": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"hast-util-from-html": "^2.0.3",
|
||||
"hast-util-select": "^6.0.4",
|
||||
"hastscript": "^9.0.1",
|
||||
"html-entities": "^2.6.0",
|
||||
"next": "15.3.1",
|
||||
"octokit": "^4.1.3",
|
||||
"react": "^19.0.0",
|
||||
|
||||
@@ -30,7 +30,7 @@ export default async function BlogPostPage({
|
||||
const { frontmatter, content } = getPost(await params);
|
||||
|
||||
return (
|
||||
<Box className="markdown-content">
|
||||
<Box className="markdown-content" data-pagefind-body>
|
||||
<Title order={2} mt={0} mb="xs">
|
||||
{frontmatter.title}
|
||||
</Title>
|
||||
|
||||
@@ -11,7 +11,13 @@ export default function CommunityPage() {
|
||||
|
||||
return (
|
||||
<Group wrap="nowrap" align="flex-start" pos="relative">
|
||||
<Box pos="sticky" top={0} w="fit-content" className="markdown-content">
|
||||
<Box
|
||||
pos="sticky"
|
||||
top={0}
|
||||
w="fit-content"
|
||||
className="markdown-content"
|
||||
data-pagefind-body
|
||||
>
|
||||
<Title order={1}>Community</Title>
|
||||
<PromMarkdown>{content}</PromMarkdown>
|
||||
</Box>
|
||||
|
||||
@@ -234,6 +234,10 @@ export default function DocsLayout({
|
||||
const pageSlug = usePathname().replace(/^\/docs\//, "");
|
||||
const currentPage = docsCollection[pageSlug];
|
||||
const reinitializeTOCRef = useRef(() => {});
|
||||
const pagefindShouldIndex =
|
||||
currentPage.type === "local-doc" ||
|
||||
(currentPage.version === currentPage.latestVersion &&
|
||||
!currentPage.slug.startsWith(currentPage.versionRoot));
|
||||
|
||||
useEffect(() => {
|
||||
reinitializeTOCRef.current();
|
||||
@@ -329,7 +333,11 @@ export default function DocsLayout({
|
||||
</Box>
|
||||
|
||||
{/* The main docs page content */}
|
||||
<Box miw={0} className="markdown-content">
|
||||
<Box
|
||||
miw={0}
|
||||
className="markdown-content"
|
||||
data-pagefind-body={pagefindShouldIndex ? "true" : undefined}
|
||||
>
|
||||
{alert}
|
||||
{children}
|
||||
|
||||
|
||||
@@ -45,15 +45,6 @@ export default function RootLayout({
|
||||
}>) {
|
||||
const [burgerOpened, { toggle: toggleBurger }] = useDisclosure();
|
||||
|
||||
// useEffect(() => {
|
||||
// docsearch({
|
||||
// container: "#docsearch",
|
||||
// appId: "prometheus",
|
||||
// indexName: "prometheus",
|
||||
// apiKey: "48ac0b7924908a1fd40b1cb18b402ba1",
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<html lang="en" {...mantineHtmlProps} className={interFont.variable}>
|
||||
<head>
|
||||
@@ -70,7 +61,7 @@ export default function RootLayout({
|
||||
}}
|
||||
>
|
||||
<Header burgerOpened={burgerOpened} toggleBurger={toggleBurger} />
|
||||
{/* <div id="docsearch" /> */}
|
||||
|
||||
<AppShell.Main px={{ base: 0, sm: "md" }}>
|
||||
<Container
|
||||
size="xl"
|
||||
|
||||
@@ -9,19 +9,15 @@ import {
|
||||
AppShell,
|
||||
} from "@mantine/core";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
IconDashboard,
|
||||
IconFileText,
|
||||
IconHome,
|
||||
IconSearch,
|
||||
} from "@tabler/icons-react";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
import prometheusLogo from "../assets/prometheus-logo.svg";
|
||||
import classes from "./Header.module.css";
|
||||
import githubLogo from "../assets/github-logo.svg";
|
||||
import Link from "next/link";
|
||||
import { ThemeSelector } from "./ThemeSelector";
|
||||
import { Spotlight, SpotlightActionData, spotlight } from "@mantine/spotlight";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { spotlight } from "@mantine/spotlight";
|
||||
import SpotlightSearch from "./SpotlightSearch";
|
||||
|
||||
const links = [
|
||||
{
|
||||
@@ -35,30 +31,6 @@ const links = [
|
||||
{ link: "/blog", label: "Blog" },
|
||||
];
|
||||
|
||||
const actions: SpotlightActionData[] = [
|
||||
{
|
||||
id: "home",
|
||||
label: "Home",
|
||||
description: "Get to home page",
|
||||
onClick: () => console.log("Home"),
|
||||
leftSection: <IconHome size={24} stroke={1.5} />,
|
||||
},
|
||||
{
|
||||
id: "dashboard",
|
||||
label: "Dashboard",
|
||||
description: "Get full information about current system status",
|
||||
onClick: () => console.log("Dashboard"),
|
||||
leftSection: <IconDashboard size={24} stroke={1.5} />,
|
||||
},
|
||||
{
|
||||
id: "documentation",
|
||||
label: "Documentation",
|
||||
description: "Visit documentation to lean more about all features",
|
||||
onClick: () => console.log("Documentation"),
|
||||
leftSection: <IconFileText size={24} stroke={1.5} />,
|
||||
},
|
||||
];
|
||||
|
||||
export const Header = ({
|
||||
burgerOpened,
|
||||
toggleBurger,
|
||||
@@ -191,6 +163,7 @@ export const Header = ({
|
||||
</Group>
|
||||
</div>
|
||||
</Container>
|
||||
<SpotlightSearch />
|
||||
</AppShell.Header>
|
||||
<AppShell.Navbar p="lg">
|
||||
{items}
|
||||
@@ -198,15 +171,6 @@ export const Header = ({
|
||||
{actionIcons}
|
||||
</Group>
|
||||
</AppShell.Navbar>
|
||||
<Spotlight
|
||||
actions={actions}
|
||||
nothingFound="Nothing found..."
|
||||
highlightQuery
|
||||
searchProps={{
|
||||
leftSection: <IconSearch size={20} stroke={1.5} />,
|
||||
placeholder: "Search...",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
206
src/components/SpotlightSearch.tsx
Normal file
206
src/components/SpotlightSearch.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
"use client";
|
||||
|
||||
import { Spotlight } from "@mantine/spotlight";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Divider, Group, Highlight, Loader, Space, Text } from "@mantine/core";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { decode } from "html-entities";
|
||||
|
||||
// Extend Window interface to include pagefind
|
||||
declare global {
|
||||
interface Window {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
pagefind: any;
|
||||
}
|
||||
}
|
||||
|
||||
const SearchResult = ({
|
||||
query,
|
||||
result,
|
||||
}: {
|
||||
query: string;
|
||||
result: PagefindResult;
|
||||
}) => {
|
||||
const [data, setData] = useState<PagefindResultData | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const data = await result.data();
|
||||
setData(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, [result]);
|
||||
|
||||
if (data === null) {
|
||||
return (
|
||||
<Group justify="center" my="sm">
|
||||
<Loader size="sm" color="gray" variant="dots" />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Spotlight.ActionsGroup label={data.meta.title}>
|
||||
<Space h="xs" />
|
||||
{data.sub_results.slice(0, 4).map((subResult, subIdx) => (
|
||||
<Spotlight.Action
|
||||
key={`${result.id}-${subIdx}`}
|
||||
id={`${result.id}-${subIdx}`}
|
||||
label={subResult.title}
|
||||
description={subResult.excerpt}
|
||||
onClick={() => {
|
||||
router.push(
|
||||
subResult.url.replace(/(\/[^?#]+)\.html(?=[?#]|$)/, "$1")
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Group wrap="nowrap" gap="xs" align="flex-start" w="100%">
|
||||
<Highlight
|
||||
highlight={query}
|
||||
fw="bold"
|
||||
fz="sm"
|
||||
w="30%"
|
||||
display="block"
|
||||
>
|
||||
{subResult.title}
|
||||
</Highlight>
|
||||
<Highlight
|
||||
highlight={query}
|
||||
size="xs"
|
||||
display="block"
|
||||
opacity={0.7}
|
||||
flex="1"
|
||||
>
|
||||
{decode(subResult.excerpt.replace(/<\/?mark>/g, ""))}
|
||||
</Highlight>
|
||||
</Group>
|
||||
</Spotlight.Action>
|
||||
))}
|
||||
</Spotlight.ActionsGroup>
|
||||
);
|
||||
};
|
||||
|
||||
type PagefindSubResult = {
|
||||
title: string;
|
||||
url: string;
|
||||
excerpt: string;
|
||||
anchor?: {
|
||||
element: string;
|
||||
id: string;
|
||||
text: string;
|
||||
location: number;
|
||||
};
|
||||
};
|
||||
|
||||
type PagefindResultData = {
|
||||
url: string;
|
||||
content: string;
|
||||
excerpt: string;
|
||||
meta: {
|
||||
title: string;
|
||||
};
|
||||
sub_results: PagefindSubResult[];
|
||||
};
|
||||
|
||||
type PagefindResult = {
|
||||
id: string;
|
||||
score: number;
|
||||
words: string[];
|
||||
data: () => Promise<PagefindResultData>;
|
||||
};
|
||||
|
||||
export default function SpotlightSearch() {
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const [results, setResults] = useState<PagefindResult[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadPagefind() {
|
||||
if (typeof window.pagefind === "undefined") {
|
||||
try {
|
||||
window.pagefind = await import(
|
||||
// @ts-expect-error pagefind.js generated after build
|
||||
/* webpackIgnore: true */ "/pagefind/pagefind.js"
|
||||
);
|
||||
await window.pagefind.options({
|
||||
ranking: {
|
||||
pageLength: 0,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
window.pagefind = {
|
||||
search: () => ({
|
||||
results: [
|
||||
{
|
||||
id: "error",
|
||||
score: 0,
|
||||
words: [],
|
||||
data: () =>
|
||||
Promise.resolve({
|
||||
url: "",
|
||||
content: "",
|
||||
excerpt: "",
|
||||
meta: {
|
||||
title: `Error importing pagefind.js`,
|
||||
},
|
||||
sub_results: [
|
||||
{
|
||||
title: "Error",
|
||||
url: "",
|
||||
excerpt: `NOTE: Search only works with a static build, not in dev mode. Error: ${e}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
loadPagefind();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Spotlight.Root
|
||||
size="xl"
|
||||
maxHeight="90vh"
|
||||
scrollable
|
||||
onQueryChange={async (query) => {
|
||||
setSearchInput(query);
|
||||
console.log("searching for", query);
|
||||
const search = await window.pagefind.debouncedSearch(query);
|
||||
if (search === null) {
|
||||
// A more recent search call has been made, nothing to do.
|
||||
console.log("search cancelled");
|
||||
return;
|
||||
}
|
||||
console.log(`Found ${search.results.length} results`);
|
||||
setResults(search.results as PagefindResult[]);
|
||||
}}
|
||||
>
|
||||
<Spotlight.Search
|
||||
placeholder="Search..."
|
||||
leftSection={<IconSearch stroke={1.5} />}
|
||||
/>
|
||||
<Spotlight.ActionsList>
|
||||
{results.length > 0 ? (
|
||||
results.map((result, idx) => (
|
||||
<React.Fragment key={result.id}>
|
||||
|{result.id}|{idx > 0 && <Divider my="xs" />}
|
||||
<SearchResult query={searchInput} result={result} />
|
||||
</React.Fragment>
|
||||
))
|
||||
) : (
|
||||
<Spotlight.Empty>Nothing found...</Spotlight.Empty>
|
||||
)}
|
||||
</Spotlight.ActionsList>
|
||||
</Spotlight.Root>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user