Support Markdown exports for getting-started docs
This commit is contained in:
1 parent
16ba3c6977
commit
6969e3fb27
6 files changed
+185
-8
No files matched your search
@@ -53,6 +53,13 @@ When a user-facing change affects setup or behavior, update the matching page in
|
||||
Every docs page and blog post includes copy-Markdown and view-source actions. Search is generated at build time and
|
||||
runs locally in the browser; it does not depend on an external crawler. Markdown exports and search resolve
|
||||
integration credentials and widget defaults from the same typed metadata used by the pages.
|
||||
The getting-started overview and complete Custom JSX component catalog are also included, with property types,
|
||||
binding contracts, and blocked capabilities taken from the published catalog JSON.
|
||||
|
||||
Published Docusaurus `/docs/category/...` addresses remain available as static compatibility pages. They forward to
|
||||
the corresponding overview or developer setup guide, preserving query strings and anchors, and provide a direct
|
||||
link when JavaScript is disabled. Their canonical metadata points to the destination; compatibility pages are
|
||||
excluded from search and the sitemap.
|
||||
|
||||
## Kapa AI
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { DocsBody, DocsPage, DocsTitle } from "fumadocs-ui/layouts/docs/page";
|
||||
|
||||
import { LegacyCategoryRedirect } from "@/components/docs/legacy-category-redirect";
|
||||
import { source } from "@/lib/source";
|
||||
|
||||
// Docusaurus generated these URLs from category labels, not directory names.
|
||||
const categories: Record<string, string> = {
|
||||
advanced: "advanced",
|
||||
"developer-guides": "advanced/development/getting-started",
|
||||
community: "community",
|
||||
"getting-started": "getting-started",
|
||||
installation: "getting-started/installation",
|
||||
integrations: "integrations",
|
||||
management: "management",
|
||||
widgets: "widgets",
|
||||
};
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ category: string }>;
|
||||
}
|
||||
|
||||
async function getCategoryPage({ params }: PageProps) {
|
||||
const { category } = await params;
|
||||
const destination = Object.hasOwn(categories, category) && categories[category];
|
||||
if (!destination) notFound();
|
||||
const page = source.getPage(destination.split("/"));
|
||||
if (!page) throw new Error(`Missing documentation destination for legacy category: ${category}`);
|
||||
return page;
|
||||
}
|
||||
|
||||
export default async function LegacyCategoryPage(props: PageProps) {
|
||||
const page = await getCategoryPage(props);
|
||||
|
||||
return (
|
||||
<DocsPage tableOfContent={{ enabled: false }}>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsBody>
|
||||
<p>This documentation has moved to a new address.</p>
|
||||
<LegacyCategoryRedirect href={`${page.url}/`} />
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return Object.keys(categories).map((category) => ({ category }));
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: PageProps): Promise<Metadata> {
|
||||
const page = await getCategoryPage(props);
|
||||
return {
|
||||
title: page.data.title,
|
||||
alternates: { canonical: page.url },
|
||||
robots: { index: false, follow: true },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function LegacyCategoryRedirect({ href }: { href: string }) {
|
||||
useEffect(() => {
|
||||
window.location.replace(`${href}${window.location.search}${window.location.hash}`);
|
||||
}, [href]);
|
||||
|
||||
return <a href={href}>Continue to the documentation</a>;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// oxlint-disable-next-line import/no-unassigned-import -- Prevent catalog data from entering a client bundle.
|
||||
import "server-only";
|
||||
import { asMarkdown } from "fumadocs-core/server";
|
||||
|
||||
import type { CustomJsxAuthoringCatalog, CustomJsxPropDescriptor } from "@homarr/custom-widgets/catalog";
|
||||
import catalogData from "@site/public/custom-widgets/component-catalog-v1.json";
|
||||
|
||||
const catalog = catalogData as CustomJsxAuthoringCatalog;
|
||||
|
||||
function Props({ items }: { items: CustomJsxPropDescriptor[] }) {
|
||||
asMarkdown();
|
||||
return (
|
||||
<ul>
|
||||
{items.map((prop) => (
|
||||
<li key={prop.name}>
|
||||
<code>{prop.name}</code>: <code>{catalog.types[prop.typeRef] ?? "unknown"}</code>.
|
||||
{prop.required && " Required."} {prop.description}
|
||||
{prop.literalValues && (
|
||||
<> Known values: {prop.literalValues.map((value) => JSON.stringify(value)).join(", ")}.</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function ComponentCatalogMarkdown() {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
Custom JSX {catalog.customWidgetVersion}, Mantine {catalog.mantineVersion}. Properties are optional unless
|
||||
marked required. <a href="/custom-widgets/component-catalog-v1.json">Download the complete JSON catalog</a>.
|
||||
</p>
|
||||
<h2>Global properties</h2>
|
||||
<Props items={catalog.globalProps} />
|
||||
<h2>Blocked capabilities</h2>
|
||||
<ul>
|
||||
{catalog.blockedCapabilities.map((capability) => (
|
||||
<li key={`${capability.kind}-${capability.name}`}>
|
||||
<code>{capability.name}</code> ({capability.kind}): {capability.reason}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{catalog.components.map((component) => (
|
||||
<section key={component.name}>
|
||||
<h2>{component.name}</h2>
|
||||
<p>
|
||||
{component.package}; {component.category}; safety: {component.safety}. {component.description}
|
||||
</p>
|
||||
{component.deniedReason && <p>Unavailable: {component.deniedReason}</p>}
|
||||
{component.bind && (
|
||||
<p>
|
||||
Bind a temporary {component.bind.type} input with <code>bind</code>. Initialize with{" "}
|
||||
<code>{component.bind.initialProp}</code>; reset with <code>{component.bind.resetProp}</code>.
|
||||
</p>
|
||||
)}
|
||||
{component.subcomponents.length > 0 && <p>Subcomponents: {component.subcomponents.join(", ")}.</p>}
|
||||
<Props items={component.props} />
|
||||
<ul>
|
||||
{component.blockedProps.map((prop) => (
|
||||
<li key={prop.name}>
|
||||
<code>{prop.name}</code> is blocked: {prop.reason}
|
||||
</li>
|
||||
))}
|
||||
{component.accessibilityRequirements.map((requirement) => (
|
||||
<li key={requirement}>{requirement}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>
|
||||
<a href={component.documentationUrl}>Upstream documentation for {component.name}</a>
|
||||
</p>
|
||||
</section>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -9,8 +9,10 @@ import { AddingWidget } from "@/components/widgets/adding";
|
||||
import type { WidgetHeader } from "@/components/widgets/header";
|
||||
import type { WidgetIntegrations } from "@/components/widgets/integrations";
|
||||
import { WidgetInteractionGuide } from "@/components/widgets/interactions";
|
||||
import { GettingStartedOverview, InstallationPaths } from "@/components/pages/getting-started/getting-started-overview";
|
||||
import { getDocsHref } from "@/lib/docs-path";
|
||||
import type { WidgetConfiguration } from "@/types";
|
||||
import { ComponentCatalogMarkdown } from "./component-catalog-markdown";
|
||||
|
||||
// Fumadocs supplies the original MDX props, including typed metadata and JSX steps.
|
||||
function markdown<Props>(component: (props: Props) => ReactNode) {
|
||||
@@ -47,6 +49,9 @@ function configuration({ items }: WidgetConfiguration): ReactNode {
|
||||
}
|
||||
|
||||
export const metadataMarkdownComponents = {
|
||||
CustomJsxComponentReference: markdown(ComponentCatalogMarkdown),
|
||||
GettingStartedOverview: markdown(GettingStartedOverview),
|
||||
InstallationPaths: markdown(InstallationPaths),
|
||||
AddingIntegration: markdown(AddingIntegration),
|
||||
AddingWidget: markdown(AddingWidget),
|
||||
WidgetInteractionGuide: markdown(WidgetInteractionGuide),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { asMarkdown } from "fumadocs-core/server";
|
||||
import type { ElementType } from "react";
|
||||
import { IconArrowRight } from "@tabler/icons-react";
|
||||
|
||||
import addItem from "@site/docs/getting-started/img/manage-board-header-choose-item.png";
|
||||
@@ -33,15 +35,21 @@ const editingSteps = [
|
||||
];
|
||||
|
||||
export function GettingStartedOverview() {
|
||||
const isMarkdown = asMarkdown();
|
||||
let BoardLink: ElementType = Link;
|
||||
if (isMarkdown) BoardLink = "a";
|
||||
|
||||
return (
|
||||
<div className={styles.overview}>
|
||||
<section className={styles.conceptCard} aria-labelledby="getting-started-overview-title">
|
||||
<div className={styles.intro}>
|
||||
<h2 id="getting-started-overview-title">How Homarr works</h2>
|
||||
<p>Services expose data, integrations connect to them, widgets use the data, and boards arrange the widgets.</p>
|
||||
<p>
|
||||
Services expose data, integrations connect to them, widgets use the data, and boards arrange the widgets.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ConceptFlow />
|
||||
{!isMarkdown && <ConceptFlow />}
|
||||
</section>
|
||||
|
||||
<section className={styles.editor} aria-labelledby="board-editor-title">
|
||||
@@ -50,19 +58,18 @@ export function GettingStartedOverview() {
|
||||
<h3 id="board-editor-title">Board edit mode</h3>
|
||||
<p>Add items, move them, and resize them on the grid. Each viewport can have its own layout.</p>
|
||||
</div>
|
||||
<Link href="/docs/management/boards">
|
||||
Board docs <IconArrowRight size={15} aria-hidden="true" />
|
||||
</Link>
|
||||
<BoardLink href="/docs/management/boards">
|
||||
Board docs {!isMarkdown && <IconArrowRight size={15} aria-hidden="true" />}
|
||||
</BoardLink>
|
||||
</div>
|
||||
|
||||
<ol className={styles.editorSteps}>
|
||||
{editingSteps.map((step, index) => (
|
||||
<li className={styles.editorStep} key={step.title}>
|
||||
<div className={styles.stepCopy}>
|
||||
<span>{index + 1}</span>
|
||||
{!isMarkdown && <span>{index + 1}</span>}
|
||||
<div>
|
||||
<strong>{step.title}</strong>
|
||||
<small>{step.description}</small>
|
||||
<strong>{step.title}</strong> <small>{step.description}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.stepMedia}>
|
||||
@@ -101,6 +108,19 @@ const installationPaths = [
|
||||
];
|
||||
|
||||
export function InstallationPaths() {
|
||||
const isMarkdown = asMarkdown();
|
||||
if (isMarkdown) {
|
||||
return (
|
||||
<ul>
|
||||
{installationPaths.map((path) => (
|
||||
<li key={path.href}>
|
||||
<a href={path.href}>{path.title}</a> ({path.eyebrow}): {path.description}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.installPaths}>
|
||||
{installationPaths.map((path) => {
|
||||
|
||||
Reference in new issue
Block a user