1
0
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:
Julius Volz
2025-05-26 15:53:44 +02:00
parent 6672bdcfd7
commit 5cb15baf8e
20 changed files with 718 additions and 654 deletions

View File

@@ -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`

View File

@@ -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>

View File

@@ -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}

View File

@@ -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
View 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);
}

View 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>
);
}

View 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;
}

View File

@@ -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} />
</>
);
}

View File

@@ -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)",

View File

@@ -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() {

View File

@@ -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>

View File

@@ -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={{

View File

@@ -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 (

View File

@@ -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 = {

View File

@@ -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() {

View File

@@ -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: {

View File

@@ -1,3 +1,5 @@
"use client";
import {
Group,
Burger,

View File

@@ -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>
);
}

View File

@@ -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"

View File

@@ -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,
},
};
};