mirror of
https://github.com/prometheus/docs.git
synced 2026-02-05 15:45:27 +01:00
Change site to use trailing slashes, refactor docs layout+page, add canonical links
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
@@ -2,8 +2,8 @@ import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "export",
|
||||
// Optional: Change links `/me` -> `/me/` and emit `/me.html` -> `/me/index.html`
|
||||
// trailingSlash: true,
|
||||
// Change links `/me` -> `/me/` and emit `/me.html` -> `/me/index.html`
|
||||
trailingSlash: true,
|
||||
// Optional: Prevent automatic `/me` -> `/me/`, instead preserve `href`
|
||||
// skipTrailingSlashRedirect: true,
|
||||
// Optional: Change the output directory `out` -> `dist`
|
||||
|
||||
@@ -13,6 +13,7 @@ export async function generateMetadata({
|
||||
}: {
|
||||
params: Promise<{ year: string; month: string; day: string; slug: string }>;
|
||||
}) {
|
||||
const { year, month, day, slug } = await params;
|
||||
const { frontmatter } = getPost(await params);
|
||||
const excerpt = frontmatter.excerpt
|
||||
? frontmatter.excerpt.length > 160
|
||||
@@ -23,6 +24,7 @@ export async function generateMetadata({
|
||||
return getPageMetadata({
|
||||
pageTitle: `${frontmatter.title}`,
|
||||
pageDescription: excerpt,
|
||||
pagePath: `/blog/${year}/${month}/${day}/${slug}/`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,7 +36,7 @@ export default async function BlogPostPage({
|
||||
const { frontmatter, content } = getPost(await params);
|
||||
|
||||
return (
|
||||
<Box className="markdown-content" data-pagefind-body>
|
||||
<Box data-pagefind-body>
|
||||
<Title order={1} mt={0} mb="xs">
|
||||
{frontmatter.title}
|
||||
</Title>
|
||||
|
||||
@@ -2,16 +2,7 @@ import { getAllPosts } from "@/blog-helpers";
|
||||
import PromMarkdown from "@/components/PromMarkdown";
|
||||
import TOC from "@/components/TOC";
|
||||
import { getPageMetadata } from "@/page-metadata";
|
||||
import {
|
||||
Anchor,
|
||||
Title,
|
||||
Text,
|
||||
Card,
|
||||
Stack,
|
||||
Button,
|
||||
Box,
|
||||
Group,
|
||||
} from "@mantine/core";
|
||||
import { Anchor, Title, Text, Card, Stack, Button, Group } from "@mantine/core";
|
||||
import dayjs from "dayjs";
|
||||
import { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
@@ -20,6 +11,7 @@ export const metadata: Metadata = getPageMetadata({
|
||||
pageTitle: "Blog",
|
||||
pageDescription:
|
||||
"The Prometheus blog contains articles about the project, its components, and the ecosystem.",
|
||||
pagePath: "/blog/",
|
||||
});
|
||||
|
||||
function headingSlug({ year, month, day, slug }) {
|
||||
@@ -52,9 +44,7 @@ export default function BlogPage() {
|
||||
{dayjs(frontmatter.created_at).format("MMMM D, YYYY")} by{" "}
|
||||
{frontmatter.author_name}
|
||||
</Text>
|
||||
<Box className="markdown-content">
|
||||
<PromMarkdown>{excerpt}</PromMarkdown>
|
||||
</Box>
|
||||
<PromMarkdown>{excerpt}</PromMarkdown>
|
||||
|
||||
<Button
|
||||
component={Link}
|
||||
|
||||
@@ -9,6 +9,7 @@ export const metadata: Metadata = getPageMetadata({
|
||||
pageTitle: "Community",
|
||||
pageDescription:
|
||||
"Community resources around the Prometheus monitoring system and time series database.",
|
||||
pagePath: "/community/",
|
||||
});
|
||||
|
||||
export default function CommunityPage() {
|
||||
@@ -19,7 +20,7 @@ export default function CommunityPage() {
|
||||
|
||||
return (
|
||||
<Group wrap="nowrap" align="flex-start" gap="xl">
|
||||
<Box className="markdown-content" data-pagefind-body>
|
||||
<Box data-pagefind-body>
|
||||
<Title order={1}>Community</Title>
|
||||
<PromMarkdown>{content}</PromMarkdown>
|
||||
</Box>
|
||||
|
||||
213
src/app/docs/LeftNav.tsx
Normal file
213
src/app/docs/LeftNav.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
docsCollection,
|
||||
allRepoVersions,
|
||||
getDocsRoots,
|
||||
} from "@/docs-collection";
|
||||
import { DocMetadata } from "@/docs-collection-types";
|
||||
import { Box, Select, NavLink } from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import {
|
||||
IconFlask,
|
||||
IconServer,
|
||||
IconCode,
|
||||
IconThumbUp,
|
||||
IconBell,
|
||||
IconBook,
|
||||
IconSettings,
|
||||
IconHandFingerRight,
|
||||
IconChartLine,
|
||||
IconMap,
|
||||
IconFileDescription,
|
||||
IconProps,
|
||||
IconTag,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
const iconMap: Record<string, React.ComponentType<IconProps>> = {
|
||||
flask: IconFlask,
|
||||
server: IconServer,
|
||||
code: IconCode,
|
||||
"thumb-up": IconThumbUp,
|
||||
bell: IconBell,
|
||||
book: IconBook,
|
||||
settings: IconSettings,
|
||||
"hand-finger-right": IconHandFingerRight,
|
||||
"chart-line": IconChartLine,
|
||||
map: IconMap,
|
||||
"file-description": IconFileDescription,
|
||||
};
|
||||
|
||||
function NavIcon({ iconName, ...props }: { iconName: string } & IconProps) {
|
||||
const Icon = iconMap[iconName];
|
||||
if (!Icon) {
|
||||
throw new Error(`Unknown icon name: ${iconName}`);
|
||||
}
|
||||
return <Icon {...props} color="var(--mantine-color-gray-6)" />;
|
||||
}
|
||||
|
||||
// Return a navigation tree UI for the DocsTree.
|
||||
function buildRecursiveNav(
|
||||
docsTree: DocMetadata[],
|
||||
currentPageSlug: string,
|
||||
router: ReturnType<typeof useRouter>,
|
||||
level = 0
|
||||
) {
|
||||
return docsTree.map((doc) => {
|
||||
if (doc.children.length > 0) {
|
||||
// Node is a "directory".
|
||||
const fc = doc.children[0];
|
||||
const repoVersions =
|
||||
fc && fc.type === "repo-doc"
|
||||
? allRepoVersions[fc.owner][fc.repo]
|
||||
: null;
|
||||
|
||||
const currentPage = docsCollection[currentPageSlug];
|
||||
if (!currentPage) {
|
||||
throw new Error(`Current page not found: ${currentPageSlug}`);
|
||||
}
|
||||
|
||||
const currentPageVersion =
|
||||
currentPage.type === "repo-doc" &&
|
||||
fc.type === "repo-doc" &&
|
||||
currentPage.owner === fc.owner &&
|
||||
currentPage.repo === fc.repo
|
||||
? currentPage.version
|
||||
: null;
|
||||
|
||||
const shownChildren = doc.children.filter((child) => {
|
||||
if (child.hideInNav) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Always show unversioned local docs in the nav.
|
||||
if (child.type === "local-doc") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Always show latest version docs if we're not looking at a different version of the same repo.
|
||||
if (
|
||||
!currentPageVersion &&
|
||||
child.version === repoVersions?.latestVersion
|
||||
) {
|
||||
if (child.slug.startsWith(child.versionRoot)) {
|
||||
// Don't show "3.4", even if it is the latest.
|
||||
return false;
|
||||
} else {
|
||||
// Show "latest".
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're looking at a specific version and it's not the latest version,
|
||||
// show all children with that same version.
|
||||
if (child.version === currentPageVersion) {
|
||||
if (currentPageVersion !== repoVersions?.latestVersion) {
|
||||
return true;
|
||||
} else {
|
||||
if (child.slug.startsWith(child.versionRoot)) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const navIcon = doc.type === "local-doc" && doc.navIcon;
|
||||
const active = currentPageSlug.startsWith(doc.slug);
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
defaultOpened={active || undefined}
|
||||
key={doc.slug}
|
||||
href="#required-for-focus"
|
||||
label={doc.navTitle ?? doc.title}
|
||||
// We offset the children, but we do it manually via a mix of margin and padding
|
||||
// to position the left-hand-side border on the first level correctly.
|
||||
childrenOffset={0}
|
||||
leftSection={
|
||||
navIcon ? (
|
||||
<NavIcon iconName={navIcon} size={16} stroke={1.8} />
|
||||
) : undefined
|
||||
}
|
||||
ff={level === 0 ? "var(--font-inter)" : undefined}
|
||||
fw={level === 0 ? 500 : undefined}
|
||||
style={{ borderRadius: 2.5 }}
|
||||
>
|
||||
<Box
|
||||
ml={level === 0 ? 19 : 12}
|
||||
pl={level === 0 ? 9 : 2}
|
||||
style={
|
||||
level === 0
|
||||
? {
|
||||
borderLeft:
|
||||
"1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-gray-7))",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{level === 0 && repoVersions && (
|
||||
<Select
|
||||
py="xs"
|
||||
size="sm"
|
||||
leftSection={<IconTag size={16} />}
|
||||
title="Select version"
|
||||
value={currentPageVersion || repoVersions.latestVersion}
|
||||
data={repoVersions.versions.map((version) => ({
|
||||
value: version,
|
||||
label:
|
||||
version === repoVersions.latestVersion
|
||||
? `${version} (latest)`
|
||||
: repoVersions.ltsVersions.includes(version)
|
||||
? `${version} (LTS)`
|
||||
: version,
|
||||
}))}
|
||||
onChange={(version) => {
|
||||
const newPageNode = doc.children.filter(
|
||||
(child) =>
|
||||
child.type === "repo-doc" && child.version === version
|
||||
)[0];
|
||||
if (newPageNode) {
|
||||
router.push(`/docs/${newPageNode.slug}/`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{buildRecursiveNav(
|
||||
shownChildren,
|
||||
currentPageSlug,
|
||||
router,
|
||||
level + 1
|
||||
)}
|
||||
</Box>
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
const active = currentPageSlug === doc.slug;
|
||||
|
||||
// Node is a "file" (document).
|
||||
return (
|
||||
<NavLink
|
||||
active={active}
|
||||
variant="light"
|
||||
component={Link}
|
||||
key={doc.slug}
|
||||
label={doc.navTitle ?? doc.title}
|
||||
href={`/docs/${doc.slug}/`}
|
||||
style={{ borderRadius: 2.5 }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default function LeftNav() {
|
||||
const router = useRouter();
|
||||
const pageSlug = usePathname()
|
||||
.replace(/^\/docs\//, "")
|
||||
.replace(/\/$/, "");
|
||||
|
||||
return buildRecursiveNav(getDocsRoots(), pageSlug, router);
|
||||
}
|
||||
94
src/app/docs/[...slug]/PrevNextEditButtons.tsx
Normal file
94
src/app/docs/[...slug]/PrevNextEditButtons.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Group, Box, Button, Text, Stack } from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import { IconArrowRight, IconArrowLeft, IconPencil } from "@tabler/icons-react";
|
||||
import { DocMetadata } from "@/docs-collection-types";
|
||||
|
||||
export default function PrevNextEditButtons({
|
||||
currentPage,
|
||||
}: {
|
||||
currentPage: DocMetadata;
|
||||
}) {
|
||||
return (
|
||||
<Group
|
||||
component="nav"
|
||||
aria-label="pagination"
|
||||
justify="space-between"
|
||||
mt="xl"
|
||||
>
|
||||
<Box flex="1" miw={0}>
|
||||
{currentPage.prev && (
|
||||
<Button
|
||||
component={Link}
|
||||
href={`/docs/${currentPage.prev.slug}/`}
|
||||
variant="outline"
|
||||
color="var(--mantine-color-text)"
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
h={80}
|
||||
leftSection={<IconArrowLeft stroke={1.5} />}
|
||||
ta="right"
|
||||
bd="1px solid var(--mantine-color-gray-5)"
|
||||
>
|
||||
<Stack align="flex-end" gap={5}>
|
||||
<Text size="sm" fw={700}>
|
||||
Previous
|
||||
</Text>
|
||||
<Text size="sm" style={{ whiteSpace: "normal" }}>
|
||||
{currentPage.prev.title}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<Button
|
||||
flex="1"
|
||||
miw={0}
|
||||
component="a"
|
||||
href={
|
||||
currentPage.type === "local-doc"
|
||||
? `https://github.com/prometheus/docs/blob/main/docs/${currentPage.slug}.md`
|
||||
: `https://github.com/${currentPage.owner}/${
|
||||
currentPage.repo
|
||||
}/blob/main/docs/${currentPage.slug
|
||||
.split("/")
|
||||
.slice(currentPage.slugPrefix.split("/").length + 1)
|
||||
.join("/")}.md`
|
||||
}
|
||||
target="_blank"
|
||||
variant="subtle"
|
||||
color="var(--mantine-color-text)"
|
||||
w="100%"
|
||||
h={80}
|
||||
leftSection={<IconPencil size={18} stroke={1.5} />}
|
||||
fw="normal"
|
||||
>
|
||||
Edit this page
|
||||
</Button>
|
||||
<Box flex="1" miw={0}>
|
||||
{currentPage.next && (
|
||||
<Button
|
||||
component={Link}
|
||||
href={`/docs/${currentPage.next.slug}/`}
|
||||
variant="outline"
|
||||
color="var(--mantine-color-text)"
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
h={80}
|
||||
rightSection={<IconArrowRight stroke={1.5} />}
|
||||
ta="left"
|
||||
bd="1px solid var(--mantine-color-gray-5)"
|
||||
>
|
||||
<Stack align="flex-start" gap={5}>
|
||||
<Text size="sm" fw={700}>
|
||||
Next
|
||||
</Text>
|
||||
<Text size="sm" style={{ whiteSpace: "normal" }}>
|
||||
{currentPage.next.title}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
58
src/app/docs/[...slug]/VersionWarning.tsx
Normal file
58
src/app/docs/[...slug]/VersionWarning.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Alert } from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
import { DocMetadata } from "@/docs-collection-types";
|
||||
|
||||
function compareVersions(a: string, b: string): number {
|
||||
const [majorA, minorA] = a.split(".").map(Number);
|
||||
const [majorB, minorB] = b.split(".").map(Number);
|
||||
|
||||
if (majorA !== majorB) {
|
||||
return Math.sign(majorA - majorB);
|
||||
}
|
||||
return Math.sign(minorA - minorB);
|
||||
}
|
||||
|
||||
export default function VersionWarning({
|
||||
currentPage,
|
||||
}: {
|
||||
currentPage: DocMetadata;
|
||||
}) {
|
||||
if (currentPage.type === "repo-doc") {
|
||||
// TODO: Clean this up, the version could theoretically appear in the path
|
||||
// in unintended places as well.
|
||||
const latestSlug = currentPage.slug.replace(currentPage.version, "latest");
|
||||
|
||||
switch (compareVersions(currentPage.version, currentPage.latestVersion)) {
|
||||
case -1:
|
||||
return (
|
||||
<Alert
|
||||
icon={<IconInfoCircle size={16} />}
|
||||
title="Outdated version"
|
||||
color="yellow"
|
||||
mb="xl"
|
||||
>
|
||||
This page documents version {currentPage.version}, which is
|
||||
outdated. Check out the{" "}
|
||||
<Link href={`/docs/${latestSlug}`}>latest stable version.</Link>
|
||||
</Alert>
|
||||
);
|
||||
case 1:
|
||||
return (
|
||||
<Alert
|
||||
icon={<IconInfoCircle size={16} />}
|
||||
title="Pre-release version"
|
||||
color="yellow"
|
||||
mb="xl"
|
||||
>
|
||||
This page documents a pre-release version ({currentPage.version}).
|
||||
Check out the{" "}
|
||||
<Link href={`/docs/${latestSlug}`}>latest stable version</Link> (
|
||||
{currentPage.latestVersion}).
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
import fs from "fs/promises";
|
||||
// import { CodeHighlight } from "@mantine/code-highlight";
|
||||
import { docsCollection } from "@/docs-collection";
|
||||
import PromMarkdown, { isAbsoluteUrl } from "@/components/PromMarkdown";
|
||||
import docsConfig from "../../../../docs-config";
|
||||
import { getPageMetadata } from "@/page-metadata";
|
||||
import VersionWarning from "./VersionWarning";
|
||||
import { Divider } from "@mantine/core";
|
||||
import PrevNextEditButtons from "./PrevNextEditButtons";
|
||||
import { HTMLAttributes } from "react";
|
||||
import path from "path";
|
||||
|
||||
// Next.js uses this function at build time to figure out which
|
||||
// docs pages it should statically generate.
|
||||
export async function generateStaticParams() {
|
||||
const params = Object.keys(docsCollection).map((slug: string) => ({
|
||||
slug: slug.split("/"),
|
||||
slug: slug.split("/").concat(""),
|
||||
}));
|
||||
return params;
|
||||
}
|
||||
@@ -19,65 +23,101 @@ export async function generateMetadata({
|
||||
}: {
|
||||
params: Promise<{ slug: string[] }>;
|
||||
}) {
|
||||
const slug = (await params).slug;
|
||||
const docMeta = docsCollection[slug.join("/")];
|
||||
const slugArray = (await params).slug;
|
||||
const slug = slugArray.join("/");
|
||||
|
||||
const docMeta = docsCollection[slug];
|
||||
if (!docMeta) {
|
||||
throw new Error(`Page not found for slug: ${slug}`);
|
||||
}
|
||||
|
||||
return getPageMetadata({
|
||||
pageTitle: docMeta.title,
|
||||
pageDescription: `Prometheus project documentation for ${docMeta.title}`,
|
||||
pagePath: `/docs/${slug.join("/")}`,
|
||||
pagePath: `/docs/${slug}/`,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveRelativeUrl(currentPath: string, relativeUrl: string): string {
|
||||
const [pathAndQuery, hash = ""] = relativeUrl.split("#");
|
||||
const [relativePath, query = ""] = pathAndQuery.split("?");
|
||||
|
||||
const baseDir = currentPath.endsWith("/")
|
||||
? currentPath
|
||||
: path.posix.dirname(currentPath) + "/";
|
||||
|
||||
const resolvedPath = path.posix.resolve(
|
||||
baseDir,
|
||||
relativePath.replace(/\.md$/, "/")
|
||||
);
|
||||
|
||||
return resolvedPath + (query ? `?${query}` : "") + (hash ? `#${hash}` : "");
|
||||
}
|
||||
|
||||
export default async function DocsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string[] }>;
|
||||
}) {
|
||||
const slug = (await params).slug;
|
||||
const slugArray = (await params).slug;
|
||||
const slug = slugArray.join("/");
|
||||
|
||||
const docMeta = docsCollection[slug];
|
||||
if (!docMeta) {
|
||||
throw new Error(`Page not found for slug: ${slug}`);
|
||||
}
|
||||
|
||||
const docMeta = docsCollection[slug.join("/")];
|
||||
const markdown = await fs.readFile(docMeta.filePath, "utf-8");
|
||||
|
||||
const pagefindShouldIndex =
|
||||
docMeta.type === "local-doc" ||
|
||||
(docMeta.version === docMeta.latestVersion &&
|
||||
!docMeta.slug.startsWith(docMeta.versionRoot));
|
||||
|
||||
return (
|
||||
<PromMarkdown
|
||||
normalizeHref={(href: string | undefined) => {
|
||||
if (!href) {
|
||||
return href;
|
||||
<>
|
||||
<VersionWarning currentPage={docMeta} />
|
||||
<PromMarkdown
|
||||
wrapperProps={
|
||||
{
|
||||
"data-pagefind-body": pagefindShouldIndex ? "true" : undefined,
|
||||
} as HTMLAttributes<HTMLDivElement>
|
||||
}
|
||||
normalizeHref={(href: string | undefined) => {
|
||||
if (!href) {
|
||||
return href;
|
||||
}
|
||||
|
||||
// Do some postprocessing on the hrefs to make sure they point to the right place.
|
||||
if (href.startsWith(docsConfig.siteUrl)) {
|
||||
// Remove the "https://prometheus.io" from links that start with it.
|
||||
return href.slice(docsConfig.siteUrl.length);
|
||||
} else if (href.startsWith("/") && docMeta.type === "repo-doc") {
|
||||
// Turn "/<path>" into e.g. "https://github.com/prometheus/prometheus/blob/release-3.3/<path>"
|
||||
return `https://github.com/${docMeta.owner}/${docMeta.repo}/blob/release-${docMeta.version}${href}`;
|
||||
} else if (href.includes(".md") && !isAbsoluteUrl(href)) {
|
||||
// Turn "foo/bar/baz.md" into "foo/bar/baz" for relative links between Markdown pages.
|
||||
return `${href.replace(/\.md($|#)/, "$1")}`;
|
||||
}
|
||||
|
||||
return href;
|
||||
}}
|
||||
normalizeImgSrc={(src: string | Blob | undefined) => {
|
||||
// Leave anything alone that doesn't look like a normal relative URL.
|
||||
if (!src || typeof src !== "string" || isAbsoluteUrl(src)) {
|
||||
return src;
|
||||
}
|
||||
|
||||
switch (docMeta.type) {
|
||||
case "local-doc":
|
||||
// TODO: Fix this in the old Markdown files instead?
|
||||
return src.replace(/^\/assets\//, "/assets/docs/");
|
||||
case "repo-doc":
|
||||
// Do some postprocessing on the hrefs to make sure they point to the right place.
|
||||
if (href.startsWith(docsConfig.siteUrl)) {
|
||||
// Remove the "https://prometheus.io" from links that start with it.
|
||||
return href.slice(docsConfig.siteUrl.length);
|
||||
} else if (href.startsWith("/") && docMeta.type === "repo-doc") {
|
||||
// Turn "/<path>" into e.g. "https://github.com/prometheus/prometheus/blob/release-3.3/<path>"
|
||||
return `https://github.com/${docMeta.owner}/${docMeta.repo}/blob/release-${docMeta.version}${href}`;
|
||||
} else if (href.includes(".md") && !isAbsoluteUrl(href)) {
|
||||
// Turn relative links like "d.md" in "docs/a/b/c.md" into full paths like "/docs/a/b/d/".
|
||||
return resolveRelativeUrl(`/docs/${docMeta.slug}`, href);
|
||||
}
|
||||
}}
|
||||
normalizeImgSrc={(src: string | Blob | undefined) => {
|
||||
// Leave anything alone that doesn't look like a normal relative URL.
|
||||
if (
|
||||
src &&
|
||||
typeof src === "string" &&
|
||||
!isAbsoluteUrl(src) &&
|
||||
docMeta.type === "repo-doc"
|
||||
) {
|
||||
return `${docMeta.assetsRoot}/${src}`;
|
||||
default:
|
||||
throw new Error(`Unknown doc type`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{markdown}
|
||||
</PromMarkdown>
|
||||
}
|
||||
|
||||
return src;
|
||||
}}
|
||||
>
|
||||
{markdown}
|
||||
</PromMarkdown>
|
||||
<Divider my="xl" />
|
||||
<PrevNextEditButtons currentPage={docMeta} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,298 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
docsCollection,
|
||||
allRepoVersions,
|
||||
getDocsRoots,
|
||||
} from "@/docs-collection";
|
||||
import { DocMetadata } from "@/docs-collection-types";
|
||||
import {
|
||||
Group,
|
||||
Box,
|
||||
Select,
|
||||
NavLink,
|
||||
Alert,
|
||||
ScrollAreaAutosize,
|
||||
ScrollArea,
|
||||
Button,
|
||||
Popover,
|
||||
Text,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Divider,
|
||||
ScrollAreaAutosize,
|
||||
PopoverDropdown,
|
||||
PopoverTarget,
|
||||
} from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import {
|
||||
IconFlask,
|
||||
IconServer,
|
||||
IconCode,
|
||||
IconThumbUp,
|
||||
IconBell,
|
||||
IconBook,
|
||||
IconSettings,
|
||||
IconHandFingerRight,
|
||||
IconChartLine,
|
||||
IconMap,
|
||||
IconFileDescription,
|
||||
IconProps,
|
||||
IconInfoCircle,
|
||||
IconTag,
|
||||
IconMenu2,
|
||||
IconArrowRight,
|
||||
IconArrowLeft,
|
||||
IconPencil,
|
||||
} from "@tabler/icons-react";
|
||||
import { ReactElement, useEffect, useRef } from "react";
|
||||
import TOC from "@/components/TOC";
|
||||
|
||||
const iconMap: Record<string, React.ComponentType<IconProps>> = {
|
||||
flask: IconFlask,
|
||||
server: IconServer,
|
||||
code: IconCode,
|
||||
"thumb-up": IconThumbUp,
|
||||
bell: IconBell,
|
||||
book: IconBook,
|
||||
settings: IconSettings,
|
||||
"hand-finger-right": IconHandFingerRight,
|
||||
"chart-line": IconChartLine,
|
||||
map: IconMap,
|
||||
"file-description": IconFileDescription,
|
||||
};
|
||||
|
||||
function NavIcon({ iconName, ...props }: { iconName: string } & IconProps) {
|
||||
const Icon = iconMap[iconName];
|
||||
if (!Icon) {
|
||||
throw new Error(`Unknown icon name: ${iconName}`);
|
||||
}
|
||||
return <Icon {...props} color="var(--mantine-color-gray-6)" />;
|
||||
}
|
||||
|
||||
// Return a navigation tree UI for the DocsTree.
|
||||
function buildRecursiveNav(
|
||||
docsTree: DocMetadata[],
|
||||
currentPageSlug: string,
|
||||
router: ReturnType<typeof useRouter>,
|
||||
level = 0
|
||||
) {
|
||||
return docsTree.map((doc) => {
|
||||
if (doc.children.length > 0) {
|
||||
// Node is a "directory".
|
||||
const fc = doc.children[0];
|
||||
const repoVersions =
|
||||
fc && fc.type === "repo-doc"
|
||||
? allRepoVersions[fc.owner][fc.repo]
|
||||
: null;
|
||||
|
||||
const currentPage = docsCollection[currentPageSlug];
|
||||
const currentPageVersion =
|
||||
currentPage.type === "repo-doc" &&
|
||||
fc.type === "repo-doc" &&
|
||||
currentPage.owner === fc.owner &&
|
||||
currentPage.repo === fc.repo
|
||||
? currentPage.version
|
||||
: null;
|
||||
|
||||
const shownChildren = doc.children.filter((child) => {
|
||||
if (child.hideInNav) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Always show unversioned local docs in the nav.
|
||||
if (child.type === "local-doc") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Always show latest version docs if we're not looking at a different version of the same repo.
|
||||
if (
|
||||
!currentPageVersion &&
|
||||
child.version === repoVersions?.latestVersion
|
||||
) {
|
||||
if (child.slug.startsWith(child.versionRoot)) {
|
||||
// Don't show "3.4", even if it is the latest.
|
||||
return false;
|
||||
} else {
|
||||
// Show "latest".
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're looking at a specific version and it's not the latest version,
|
||||
// show all children with that same version.
|
||||
if (child.version === currentPageVersion) {
|
||||
if (currentPageVersion !== repoVersions?.latestVersion) {
|
||||
return true;
|
||||
} else {
|
||||
if (child.slug.startsWith(child.versionRoot)) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const navIcon = doc.type === "local-doc" && doc.navIcon;
|
||||
const active = currentPageSlug.startsWith(doc.slug);
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
defaultOpened={active || undefined}
|
||||
key={doc.slug}
|
||||
href="#required-for-focus"
|
||||
label={doc.navTitle ?? doc.title}
|
||||
// We offset the children, but we do it manually via a mix of margin and padding
|
||||
// to position the left-hand-side border on the first level correctly.
|
||||
childrenOffset={0}
|
||||
leftSection={
|
||||
navIcon ? (
|
||||
<NavIcon iconName={navIcon} size={16} stroke={1.8} />
|
||||
) : undefined
|
||||
}
|
||||
ff={level === 0 ? "var(--font-inter)" : undefined}
|
||||
fw={level === 0 ? 500 : undefined}
|
||||
style={{ borderRadius: 2.5 }}
|
||||
>
|
||||
<Box
|
||||
ml={level === 0 ? 19 : 12}
|
||||
pl={level === 0 ? 9 : 2}
|
||||
style={
|
||||
level === 0
|
||||
? {
|
||||
borderLeft:
|
||||
"1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-gray-7))",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{level === 0 && repoVersions && (
|
||||
<Select
|
||||
py="xs"
|
||||
size="sm"
|
||||
leftSection={<IconTag size={16} />}
|
||||
title="Select version"
|
||||
value={currentPageVersion || repoVersions.latestVersion}
|
||||
data={repoVersions.versions.map((version) => ({
|
||||
value: version,
|
||||
label:
|
||||
version === repoVersions.latestVersion
|
||||
? `${version} (latest)`
|
||||
: repoVersions.ltsVersions.includes(version)
|
||||
? `${version} (LTS)`
|
||||
: version,
|
||||
}))}
|
||||
onChange={(version) => {
|
||||
const newPageNode = doc.children.filter(
|
||||
(child) =>
|
||||
child.type === "repo-doc" && child.version === version
|
||||
)[0];
|
||||
if (newPageNode) {
|
||||
router.push(`/docs/${newPageNode.slug}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{buildRecursiveNav(
|
||||
shownChildren,
|
||||
currentPageSlug,
|
||||
router,
|
||||
level + 1
|
||||
)}
|
||||
</Box>
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
const active = currentPageSlug === doc.slug;
|
||||
|
||||
// Node is a "file" (document).
|
||||
return (
|
||||
<NavLink
|
||||
active={active}
|
||||
variant="light"
|
||||
component={Link}
|
||||
key={doc.slug}
|
||||
label={doc.navTitle ?? doc.title}
|
||||
href={`/docs/${doc.slug}`}
|
||||
style={{ borderRadius: 2.5 }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function compareVersions(a: string, b: string): number {
|
||||
const [majorA, minorA] = a.split(".").map(Number);
|
||||
const [majorB, minorB] = b.split(".").map(Number);
|
||||
|
||||
if (majorA !== majorB) {
|
||||
return Math.sign(majorA - majorB);
|
||||
}
|
||||
return Math.sign(minorA - minorB);
|
||||
}
|
||||
import LeftNav from "./LeftNav";
|
||||
import { IconMenu2 } from "@tabler/icons-react";
|
||||
|
||||
export default function DocsLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const router = useRouter();
|
||||
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();
|
||||
}, [pageSlug]);
|
||||
|
||||
let alert: ReactElement | null = null;
|
||||
if (currentPage.type === "repo-doc") {
|
||||
// TODO: Clean this up, the version could theoretically appear in the path
|
||||
// in unintended places as well.
|
||||
const latestSlug = pageSlug.replace(currentPage.version, "latest");
|
||||
|
||||
switch (compareVersions(currentPage.version, currentPage.latestVersion)) {
|
||||
case -1:
|
||||
alert = (
|
||||
<Alert
|
||||
icon={<IconInfoCircle size={16} />}
|
||||
title="Outdated version"
|
||||
color="yellow"
|
||||
mb="xl"
|
||||
>
|
||||
This page documents version {currentPage.version}, which is
|
||||
outdated. Check out the{" "}
|
||||
<Link href={`/docs/${latestSlug}`}>latest stable version.</Link>
|
||||
</Alert>
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
alert = (
|
||||
<Alert
|
||||
icon={<IconInfoCircle size={16} />}
|
||||
title="Pre-release version"
|
||||
color="yellow"
|
||||
mb="xl"
|
||||
>
|
||||
This page documents a pre-release version ({currentPage.version}).
|
||||
Check out the{" "}
|
||||
<Link href={`/docs/${latestSlug}`}>latest stable version</Link> (
|
||||
{currentPage.latestVersion}).
|
||||
</Alert>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const nav = buildRecursiveNav(getDocsRoots(), pageSlug, router);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* The mobile main nav */}
|
||||
<Popover position="bottom" withArrow shadow="md">
|
||||
<Popover.Target>
|
||||
<PopoverTarget>
|
||||
<Button
|
||||
hiddenFrom="sm"
|
||||
variant="outline"
|
||||
@@ -304,15 +33,15 @@ export default function DocsLayout({
|
||||
>
|
||||
Show nav
|
||||
</Button>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown mah="calc(100vh - var(--header-height) - var(--header-to-content-margin))">
|
||||
</PopoverTarget>
|
||||
<PopoverDropdown mah="calc(100vh - var(--header-height) - var(--header-to-content-margin))">
|
||||
<ScrollAreaAutosize
|
||||
mah="calc(80vh - var(--header-height))"
|
||||
type="never"
|
||||
>
|
||||
{nav}
|
||||
<LeftNav />
|
||||
</ScrollAreaAutosize>
|
||||
</Popover.Dropdown>
|
||||
</PopoverDropdown>
|
||||
</Popover>
|
||||
<Group wrap="nowrap" align="flex-start" gap={50}>
|
||||
{/* The left-hand side main docs nav */}
|
||||
@@ -336,110 +65,20 @@ export default function DocsLayout({
|
||||
h="calc(100vh - var(--header-height) - var(--header-to-content-margin))"
|
||||
type="never"
|
||||
>
|
||||
<Box px="xs">{nav}</Box>
|
||||
<Box px="xs">
|
||||
<LeftNav />
|
||||
</Box>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
{/* The main docs page content */}
|
||||
<Box
|
||||
miw={0}
|
||||
className="markdown-content"
|
||||
data-pagefind-body={pagefindShouldIndex ? "true" : undefined}
|
||||
>
|
||||
{alert}
|
||||
{children}
|
||||
|
||||
{/* Previous / next sibling page navigation */}
|
||||
<Divider my="xl" />
|
||||
<Group
|
||||
component="nav"
|
||||
aria-label="pagination"
|
||||
justify="space-between"
|
||||
mt="xl"
|
||||
>
|
||||
<Box flex="1" miw={0}>
|
||||
{currentPage.prev && (
|
||||
<Button
|
||||
component={Link}
|
||||
href={`/docs/${currentPage.prev.slug}`}
|
||||
variant="outline"
|
||||
color="var(--mantine-color-text)"
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
h={80}
|
||||
leftSection={<IconArrowLeft stroke={1.5} />}
|
||||
ta="right"
|
||||
bd="1px solid var(--mantine-color-gray-5)"
|
||||
>
|
||||
<Stack align="flex-end" gap={5}>
|
||||
<Text size="sm" fw={700}>
|
||||
Previous
|
||||
</Text>
|
||||
<Text size="sm" style={{ whiteSpace: "normal" }}>
|
||||
{currentPage.prev.title}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<Button
|
||||
flex="1"
|
||||
miw={0}
|
||||
component="a"
|
||||
href={
|
||||
currentPage.type === "local-doc"
|
||||
? `https://github.com/prometheus/docs/blob/main/docs/${currentPage.slug}.md`
|
||||
: `https://github.com/${currentPage.owner}/${
|
||||
currentPage.repo
|
||||
}/blob/main/docs/${currentPage.slug
|
||||
.split("/")
|
||||
.slice(currentPage.slugPrefix.split("/").length + 1)
|
||||
.join("/")}.md`
|
||||
}
|
||||
target="_blank"
|
||||
variant="subtle"
|
||||
color="var(--mantine-color-text)"
|
||||
w="100%"
|
||||
h={80}
|
||||
leftSection={<IconPencil size={18} stroke={1.5} />}
|
||||
fw="normal"
|
||||
>
|
||||
Edit this page
|
||||
</Button>
|
||||
<Box flex="1" miw={0}>
|
||||
{currentPage.next && (
|
||||
<Button
|
||||
component={Link}
|
||||
href={`/docs/${currentPage.next.slug}`}
|
||||
variant="outline"
|
||||
color="var(--mantine-color-text)"
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
h={80}
|
||||
rightSection={<IconArrowRight stroke={1.5} />}
|
||||
ta="left"
|
||||
bd="1px solid var(--mantine-color-gray-5)"
|
||||
>
|
||||
<Stack align="flex-start" gap={5}>
|
||||
<Text size="sm" fw={700}>
|
||||
Next
|
||||
</Text>
|
||||
<Text size="sm" style={{ whiteSpace: "normal" }}>
|
||||
{currentPage.next.title}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Group>
|
||||
</Box>
|
||||
<div style={{ minWidth: 0 }}>{children}</div>
|
||||
|
||||
{/* The right-hand-side table of contents for headings
|
||||
within the current document */}
|
||||
<TOC
|
||||
maw={230}
|
||||
wrapperProps={{ visibleFrom: "lg" }}
|
||||
reinitializeRef={reinitializeTOCRef}
|
||||
scrollSpyOptions={{
|
||||
selector:
|
||||
".markdown-content :is(h2, h3), .markdown-content h1:not(:first-of-type)",
|
||||
|
||||
@@ -10,6 +10,7 @@ export const metadata: Metadata = getPageMetadata({
|
||||
pageTitle: "Download",
|
||||
pageDescription:
|
||||
"Downloads for the latest releases of the Prometheus monitoring system and its major ecosystem components.",
|
||||
pagePath: "/download/",
|
||||
});
|
||||
|
||||
export default function DownloadPage() {
|
||||
|
||||
@@ -9,6 +9,7 @@ export const metadata: Metadata = getPageMetadata({
|
||||
pageTitle: "Governance",
|
||||
pageDescription:
|
||||
"Project governance rules for the Prometheus monitoring system and time series database.",
|
||||
pagePath: "/governance/",
|
||||
});
|
||||
|
||||
export default function CommunityPage() {
|
||||
@@ -19,13 +20,7 @@ export default function CommunityPage() {
|
||||
|
||||
return (
|
||||
<Group wrap="nowrap" align="flex-start" pos="relative" gap="xl">
|
||||
<Box
|
||||
pos="sticky"
|
||||
top={0}
|
||||
w="fit-content"
|
||||
className="markdown-content"
|
||||
data-pagefind-body
|
||||
>
|
||||
<Box pos="sticky" top={0} w="fit-content" data-pagefind-body>
|
||||
<Title order={1}>Governance</Title>
|
||||
<PromMarkdown>{content}</PromMarkdown>
|
||||
</Box>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"use client";
|
||||
|
||||
// import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import { Lato } from "next/font/google";
|
||||
import {
|
||||
Anchor,
|
||||
AppShell,
|
||||
AppShellMain,
|
||||
ColorSchemeScript,
|
||||
Container,
|
||||
Group,
|
||||
@@ -24,8 +23,6 @@ import "./globals.css";
|
||||
import { Header } from "@/components/Header";
|
||||
import { theme } from "@/theme";
|
||||
|
||||
import "@docsearch/css";
|
||||
|
||||
const interFont = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
@@ -58,12 +55,12 @@ export default function RootLayout({
|
||||
<AppShell header={{ height: "var(--header-height)" }}>
|
||||
<Header />
|
||||
|
||||
<AppShell.Main>
|
||||
<AppShellMain>
|
||||
<Container size="xl" mt="xl" px={{ base: "md", xs: "xl" }}>
|
||||
{children}
|
||||
<Space h={50} />
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
</AppShellMain>
|
||||
|
||||
<footer
|
||||
style={{
|
||||
|
||||
@@ -10,9 +10,8 @@ import githubLogo from "../assets/github-logo.svg";
|
||||
import { GitHubStars } from "@/components/GitHubStars";
|
||||
import { IconQuote } from "@tabler/icons-react";
|
||||
import { getPageMetadata } from "@/page-metadata";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = getPageMetadata({});
|
||||
export const metadata = getPageMetadata({ pagePath: "/" });
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
|
||||
@@ -15,6 +15,7 @@ export const metadata: Metadata = getPageMetadata({
|
||||
pageTitle: "Support & Training",
|
||||
pageDescription:
|
||||
"Support and training providers for the Prometheus monitoring system and time series database.",
|
||||
pagePath: "/support-training/",
|
||||
});
|
||||
|
||||
type ProviderCardProps = {
|
||||
|
||||
@@ -6,6 +6,7 @@ export const metadata: Metadata = getPageMetadata({
|
||||
pageTitle: "Alertmanager Routing Tree Editor",
|
||||
pageDescription:
|
||||
"A routing tree editor and visualizer for Alertmanager routing configurations.",
|
||||
pagePath: "/webtools/alerting/routing-tree-editor/",
|
||||
});
|
||||
|
||||
export default function RoutingTreeEditorPage() {
|
||||
|
||||
@@ -21,7 +21,7 @@ export const paramsToPostFileName = (params: {
|
||||
|
||||
export const postFileNameToPath = (fileName: string) => {
|
||||
const { year, month, day, slug } = postFileNameToParams(fileName);
|
||||
return `/blog/${year}/${month}/${day}/${slug}`;
|
||||
return `/blog/${year}/${month}/${day}/${slug}/`;
|
||||
};
|
||||
|
||||
export const getPostFilePath = (params: {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Group,
|
||||
Burger,
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
IconExternalLink,
|
||||
} from "@tabler/icons-react";
|
||||
import Link from "next/link";
|
||||
import { Children, PropsWithChildren } from "react";
|
||||
import { Children, HTMLAttributes, PropsWithChildren } from "react";
|
||||
import { MarkdownAsync } from "react-markdown";
|
||||
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
@@ -50,9 +50,11 @@ const h = (order: 1 | 2 | 3 | 4 | 5 | 6) => {
|
||||
|
||||
export default async function PromMarkdown({
|
||||
children,
|
||||
wrapperProps = {},
|
||||
normalizeHref = (href: string | undefined) => href,
|
||||
normalizeImgSrc = (src: string | Blob | undefined) => src,
|
||||
}: PropsWithChildren<{
|
||||
wrapperProps?: HTMLAttributes<HTMLDivElement>;
|
||||
normalizeHref?: (href: string | undefined) => string | undefined;
|
||||
normalizeImgSrc?: (
|
||||
src: string | Blob | undefined
|
||||
@@ -63,220 +65,222 @@ export default async function PromMarkdown({
|
||||
}
|
||||
|
||||
return (
|
||||
<MarkdownAsync
|
||||
remarkPlugins={[remarkFrontmatter, remarkGfm]}
|
||||
rehypePlugins={[
|
||||
// "rehypeRaw" is required for raw HTML in some old Markdown files to work.
|
||||
// See https://github.com/remarkjs/react-markdown?tab=readme-ov-file#appendix-a-html-in-markdown
|
||||
rehypeRaw,
|
||||
// Add "id" attributes to headings so links can point to them.
|
||||
rehypeSlug,
|
||||
// Add "<a>" tags with a link symbol to headings to link to themselves.
|
||||
() =>
|
||||
rehypeAutolinkHeadings({
|
||||
properties: {
|
||||
className: ["header-auto-link"],
|
||||
<div className="markdown-content" {...wrapperProps}>
|
||||
<MarkdownAsync
|
||||
remarkPlugins={[remarkFrontmatter, remarkGfm]}
|
||||
rehypePlugins={[
|
||||
// "rehypeRaw" is required for raw HTML in some old Markdown files to work.
|
||||
// See https://github.com/remarkjs/react-markdown?tab=readme-ov-file#appendix-a-html-in-markdown
|
||||
rehypeRaw,
|
||||
// Add "id" attributes to headings so links can point to them.
|
||||
rehypeSlug,
|
||||
// Add "<a>" tags with a link symbol to headings to link to themselves.
|
||||
() =>
|
||||
rehypeAutolinkHeadings({
|
||||
properties: {
|
||||
className: ["header-auto-link"],
|
||||
},
|
||||
behavior: "append",
|
||||
// Don't link top-level page headings (h1).
|
||||
test: (el) => el.tagName !== "h1",
|
||||
}),
|
||||
// Highlight code blocks with Shiki.
|
||||
[
|
||||
rehypeShiki,
|
||||
{
|
||||
theme: "github-light",
|
||||
},
|
||||
behavior: "append",
|
||||
// Don't link top-level page headings (h1).
|
||||
test: (el) => el.tagName !== "h1",
|
||||
}),
|
||||
// Highlight code blocks with Shiki.
|
||||
[
|
||||
rehypeShiki,
|
||||
{
|
||||
theme: "github-light",
|
||||
},
|
||||
],
|
||||
// Custom plugin: On the configuration page, link from placeholders like
|
||||
// <string> or <scrape_config> in code blocks to their corresponding type
|
||||
// definitions in the same file.
|
||||
//
|
||||
// Important: this has to run after rehypeSlug, since it
|
||||
// relies on the headers to already have IDs.
|
||||
//
|
||||
// TODO: Only run this on the "Configuration" page, like in the old site?
|
||||
rehypeConfigLinker,
|
||||
]}
|
||||
components={{
|
||||
a: (props) => {
|
||||
// Replace header auto-links with a custom link icon.
|
||||
const { children, node: _node, ...rest } = props;
|
||||
if (rest.className === "header-auto-link") {
|
||||
return (
|
||||
<a {...rest}>
|
||||
<IconLink size={em(14)} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
],
|
||||
// Custom plugin: On the configuration page, link from placeholders like
|
||||
// <string> or <scrape_config> in code blocks to their corresponding type
|
||||
// definitions in the same file.
|
||||
//
|
||||
// Important: this has to run after rehypeSlug, since it
|
||||
// relies on the headers to already have IDs.
|
||||
//
|
||||
// TODO: Only run this on the "Configuration" page, like in the old site?
|
||||
rehypeConfigLinker,
|
||||
]}
|
||||
components={{
|
||||
a: (props) => {
|
||||
// Replace header auto-links with a custom link icon.
|
||||
const { children, node: _node, ...rest } = props;
|
||||
if (rest.className === "header-auto-link") {
|
||||
return (
|
||||
<a {...rest}>
|
||||
<IconLink size={em(14)} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const href = normalizeHref(rest.href);
|
||||
const isExternalLink =
|
||||
isAbsoluteUrl(href || "") &&
|
||||
!href?.startsWith(`${docsConfig.siteUrl}`);
|
||||
const href = normalizeHref(rest.href);
|
||||
const isExternalLink =
|
||||
isAbsoluteUrl(href || "") &&
|
||||
!href?.startsWith(`${docsConfig.siteUrl}`);
|
||||
|
||||
if (!isExternalLink) {
|
||||
return (
|
||||
<Anchor
|
||||
inherit
|
||||
c="var(--secondary-link-color)"
|
||||
component={Link}
|
||||
{...rest}
|
||||
href={href || ""}
|
||||
>
|
||||
{children}
|
||||
</Anchor>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isExternalLink) {
|
||||
return (
|
||||
<Anchor
|
||||
inherit
|
||||
c="var(--secondary-link-color)"
|
||||
component={Link}
|
||||
{...rest}
|
||||
href={href || ""}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{children}
|
||||
{/* Only add the icon if the first child is a string. This is to avoid
|
||||
breaking the layout of other components like image links etc. */}
|
||||
{typeof Children.toArray(children)[0] === "string" ? (
|
||||
// <Group> with display: "inline-flex" somehow breaks link underlining,
|
||||
// so going for this manual solution instead.
|
||||
<span>
|
||||
{children}
|
||||
<IconExternalLink
|
||||
size="0.9em"
|
||||
style={{ marginLeft: 4, marginBottom: -1.5 }}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Anchor>
|
||||
);
|
||||
}
|
||||
},
|
||||
p: (props) => {
|
||||
const { children, node: _node, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Anchor
|
||||
inherit
|
||||
c="var(--secondary-link-color)"
|
||||
{...rest}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{/* Only add the icon if the first child is a string. This is to avoid
|
||||
breaking the layout of other components like image links etc. */}
|
||||
{typeof Children.toArray(children)[0] === "string" ? (
|
||||
// <Group> with display: "inline-flex" somehow breaks link underlining,
|
||||
// so going for this manual solution instead.
|
||||
<span>
|
||||
{children}
|
||||
<IconExternalLink
|
||||
size="0.9em"
|
||||
style={{ marginLeft: 4, marginBottom: -1.5 }}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Anchor>
|
||||
);
|
||||
},
|
||||
p: (props) => {
|
||||
const { children, node: _node, ...rest } = props;
|
||||
|
||||
// The React children can either be an array or a single element, and
|
||||
// each element can be a string or something else. The Children.toArray()
|
||||
// method from React helps us convert either situation into a predictable array.
|
||||
const arrayChildren = Children.toArray(children);
|
||||
const fc = arrayChildren[0];
|
||||
if (fc && typeof fc === "string") {
|
||||
// Render paragraphs that start with "TIP:", "NOTE:", "CAUTION:", or "TODO:"
|
||||
// as blockquotes with an appropriate icon.
|
||||
const alertRegex = new RegExp(
|
||||
/^(TIP|NOTE|CAUTION|TODO): (.*)$/,
|
||||
"s"
|
||||
);
|
||||
const match = fc.match(alertRegex);
|
||||
|
||||
if (match) {
|
||||
const alertType = match[1];
|
||||
const alertText = match[2];
|
||||
return (
|
||||
<Blockquote
|
||||
py="lg"
|
||||
my="xl"
|
||||
color={alertType === "CAUTION" ? "yellow" : "blue"}
|
||||
icon={
|
||||
alertType === "CAUTION" ? (
|
||||
<IconAlertCircle size={30} />
|
||||
) : (
|
||||
<IconInfoCircle size={30} />
|
||||
)
|
||||
}
|
||||
>
|
||||
<strong>{alertType}</strong>: {alertText}
|
||||
{arrayChildren.slice(1)}
|
||||
</Blockquote>
|
||||
// The React children can either be an array or a single element, and
|
||||
// each element can be a string or something else. The Children.toArray()
|
||||
// method from React helps us convert either situation into a predictable array.
|
||||
const arrayChildren = Children.toArray(children);
|
||||
const fc = arrayChildren[0];
|
||||
if (fc && typeof fc === "string") {
|
||||
// Render paragraphs that start with "TIP:", "NOTE:", "CAUTION:", or "TODO:"
|
||||
// as blockquotes with an appropriate icon.
|
||||
const alertRegex = new RegExp(
|
||||
/^(TIP|NOTE|CAUTION|TODO): (.*)$/,
|
||||
"s"
|
||||
);
|
||||
const match = fc.match(alertRegex);
|
||||
|
||||
if (match) {
|
||||
const alertType = match[1];
|
||||
const alertText = match[2];
|
||||
return (
|
||||
<Blockquote
|
||||
py="lg"
|
||||
my="xl"
|
||||
color={alertType === "CAUTION" ? "yellow" : "blue"}
|
||||
icon={
|
||||
alertType === "CAUTION" ? (
|
||||
<IconAlertCircle size={30} />
|
||||
) : (
|
||||
<IconInfoCircle size={30} />
|
||||
)
|
||||
}
|
||||
>
|
||||
<strong>{alertType}</strong>: {alertText}
|
||||
{arrayChildren.slice(1)}
|
||||
</Blockquote>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <p {...rest}>{children}</p>;
|
||||
},
|
||||
img: (props) => {
|
||||
const { src, node: _node, ...rest } = props;
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
return <img {...rest} src={normalizeImgSrc(src)} />;
|
||||
},
|
||||
pre: (props) => {
|
||||
const { children, node, ...rest } = props;
|
||||
const firstChild = node?.children[0];
|
||||
if (
|
||||
!firstChild ||
|
||||
firstChild?.type !== "element" ||
|
||||
firstChild?.tagName !== "code"
|
||||
) {
|
||||
return <pre {...rest}>{children}</pre>;
|
||||
}
|
||||
return <p {...rest}>{children}</p>;
|
||||
},
|
||||
img: (props) => {
|
||||
const { src, node: _node, ...rest } = props;
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
return <img {...rest} src={normalizeImgSrc(src)} />;
|
||||
},
|
||||
pre: (props) => {
|
||||
const { children, node, ...rest } = props;
|
||||
const firstChild = node?.children[0];
|
||||
if (
|
||||
!firstChild ||
|
||||
firstChild?.type !== "element" ||
|
||||
firstChild?.tagName !== "code"
|
||||
) {
|
||||
return <pre {...rest}>{children}</pre>;
|
||||
}
|
||||
|
||||
return (
|
||||
<pre
|
||||
{...rest}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
backgroundColor:
|
||||
"light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6))",
|
||||
lineHeight: 1.7,
|
||||
display: "block",
|
||||
padding: "1em",
|
||||
borderRadius: "0.5em",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
);
|
||||
},
|
||||
h1: h(1),
|
||||
h2: h(2),
|
||||
h3: h(3),
|
||||
h4: h(4),
|
||||
h5: h(5),
|
||||
h6: h(6),
|
||||
table: (props) => (
|
||||
<Table withColumnBorders withTableBorder highlightOnHover>
|
||||
{props.children}
|
||||
</Table>
|
||||
),
|
||||
th: TableTh,
|
||||
td: TableTd,
|
||||
tr: TableTr,
|
||||
thead: TableThead,
|
||||
tbody: TableTbody,
|
||||
// For <pre> tags that contain <code> tags, we need to extract the content
|
||||
// of the <code> tag and pass it to a <CodeHighlight> Mantine component. If it's
|
||||
// a <pre> tag that doesn't contain a <code> tag, we just render it as is.
|
||||
// pre: (props) => {
|
||||
// const firstChild = props.node?.children[0];
|
||||
// if (
|
||||
// firstChild?.type === "element" &&
|
||||
// firstChild?.tagName === "code"
|
||||
// ) {
|
||||
// const contentElement = firstChild.children[0];
|
||||
// if (contentElement.type !== "text") {
|
||||
// throw new Error("Code content is not text");
|
||||
// }
|
||||
// const content = contentElement.value;
|
||||
// const language = firstChild.properties?.className?.[0]?.replace(
|
||||
// "language-",
|
||||
// ""
|
||||
// );
|
||||
return (
|
||||
<pre
|
||||
{...rest}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
backgroundColor:
|
||||
"light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6))",
|
||||
lineHeight: 1.7,
|
||||
display: "block",
|
||||
padding: "1em",
|
||||
borderRadius: "0.5em",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
);
|
||||
},
|
||||
h1: h(1),
|
||||
h2: h(2),
|
||||
h3: h(3),
|
||||
h4: h(4),
|
||||
h5: h(5),
|
||||
h6: h(6),
|
||||
table: (props) => (
|
||||
<Table withColumnBorders withTableBorder highlightOnHover>
|
||||
{props.children}
|
||||
</Table>
|
||||
),
|
||||
th: TableTh,
|
||||
td: TableTd,
|
||||
tr: TableTr,
|
||||
thead: TableThead,
|
||||
tbody: TableTbody,
|
||||
// For <pre> tags that contain <code> tags, we need to extract the content
|
||||
// of the <code> tag and pass it to a <CodeHighlight> Mantine component. If it's
|
||||
// a <pre> tag that doesn't contain a <code> tag, we just render it as is.
|
||||
// pre: (props) => {
|
||||
// const firstChild = props.node?.children[0];
|
||||
// if (
|
||||
// firstChild?.type === "element" &&
|
||||
// firstChild?.tagName === "code"
|
||||
// ) {
|
||||
// const contentElement = firstChild.children[0];
|
||||
// if (contentElement.type !== "text") {
|
||||
// throw new Error("Code content is not text");
|
||||
// }
|
||||
// const content = contentElement.value;
|
||||
// const language = firstChild.properties?.className?.[0]?.replace(
|
||||
// "language-",
|
||||
// ""
|
||||
// );
|
||||
|
||||
// return (
|
||||
// <CodeHighlight code={content} language={language || "yaml"} />
|
||||
// );
|
||||
// } else {
|
||||
// return <pre {...props} />;
|
||||
// }
|
||||
// },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MarkdownAsync>
|
||||
// return (
|
||||
// <CodeHighlight code={content} language={language || "yaml"} />
|
||||
// );
|
||||
// } else {
|
||||
// return <pre {...props} />;
|
||||
// }
|
||||
// },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MarkdownAsync>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,10 +7,19 @@ import {
|
||||
TableOfContentsProps,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export default function TOC(
|
||||
props: TableOfContentsProps & { wrapperProps?: ScrollAreaAutosizeProps }
|
||||
) {
|
||||
const reinitializeTOCRef = useRef(() => {});
|
||||
const path = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
reinitializeTOCRef.current();
|
||||
}, [path]);
|
||||
|
||||
return (
|
||||
<ScrollAreaAutosize
|
||||
mah="calc(100vh - var(--header-height) - var(--header-to-content-margin))"
|
||||
@@ -25,6 +34,7 @@ export default function TOC(
|
||||
On this page
|
||||
</Text>
|
||||
<TableOfContents
|
||||
reinitializeRef={reinitializeTOCRef}
|
||||
maw={300}
|
||||
pr="xs"
|
||||
size="sm"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Metadata } from "next";
|
||||
import docsConfig from "../docs-config";
|
||||
|
||||
export const getPageMetadata = ({
|
||||
@@ -7,8 +8,8 @@ export const getPageMetadata = ({
|
||||
}: {
|
||||
pageTitle?: string;
|
||||
pageDescription?: string;
|
||||
pagePath?: string;
|
||||
}) => {
|
||||
pagePath: string;
|
||||
}): Metadata => {
|
||||
const title = pageTitle
|
||||
? `${pageTitle} | Prometheus`
|
||||
: "Prometheus - Monitoring system & time series database";
|
||||
@@ -16,14 +17,30 @@ export const getPageMetadata = ({
|
||||
pageDescription ||
|
||||
"An open-source monitoring system with a dimensional data model, flexible query language, efficient time series database and modern alerting approach.";
|
||||
|
||||
const canonicalUrl = `${docsConfig.siteUrl}${pagePath}`;
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: pagePath ? `${docsConfig.siteUrl}${pagePath}` : undefined,
|
||||
url: canonicalUrl,
|
||||
// TODO: add image.
|
||||
},
|
||||
// TODO: Add page-specific keywords here. For now, we're using the same static config as on the old site.
|
||||
keywords: [
|
||||
"prometheus",
|
||||
"monitoring",
|
||||
"monitoring system",
|
||||
"time series",
|
||||
"time series database",
|
||||
"alerting",
|
||||
"metrics",
|
||||
"telemetry",
|
||||
],
|
||||
alternates: {
|
||||
canonical: canonicalUrl,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user