diff --git a/apps/docs/README.md b/apps/docs/README.md index b2affe4a0..3f8ca161a 100644 --- a/apps/docs/README.md +++ b/apps/docs/README.md @@ -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 diff --git a/apps/docs/src/app/docs/category/[category]/page.tsx b/apps/docs/src/app/docs/category/[category]/page.tsx new file mode 100644 index 000000000..e7ef86778 --- /dev/null +++ b/apps/docs/src/app/docs/category/[category]/page.tsx @@ -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 = { + 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 ( + + {page.data.title} + +

This documentation has moved to a new address.

+ +
+
+ ); +} + +export function generateStaticParams() { + return Object.keys(categories).map((category) => ({ category })); +} + +export async function generateMetadata(props: PageProps): Promise { + const page = await getCategoryPage(props); + return { + title: page.data.title, + alternates: { canonical: page.url }, + robots: { index: false, follow: true }, + }; +} diff --git a/apps/docs/src/components/docs/legacy-category-redirect.tsx b/apps/docs/src/components/docs/legacy-category-redirect.tsx new file mode 100644 index 000000000..616632de5 --- /dev/null +++ b/apps/docs/src/components/docs/legacy-category-redirect.tsx @@ -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 Continue to the documentation; +} diff --git a/apps/docs/src/components/mdx/component-catalog-markdown.tsx b/apps/docs/src/components/mdx/component-catalog-markdown.tsx new file mode 100644 index 000000000..f8169af0b --- /dev/null +++ b/apps/docs/src/components/mdx/component-catalog-markdown.tsx @@ -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 ( + + ); +} + +export function ComponentCatalogMarkdown() { + return ( + <> +

+ Custom JSX {catalog.customWidgetVersion}, Mantine {catalog.mantineVersion}. Properties are optional unless + marked required. Download the complete JSON catalog. +

+

Global properties

+ +

Blocked capabilities

+
    + {catalog.blockedCapabilities.map((capability) => ( +
  • + {capability.name} ({capability.kind}): {capability.reason} +
  • + ))} +
+ {catalog.components.map((component) => ( +
+

{component.name}

+

+ {component.package}; {component.category}; safety: {component.safety}. {component.description} +

+ {component.deniedReason &&

Unavailable: {component.deniedReason}

} + {component.bind && ( +

+ Bind a temporary {component.bind.type} input with bind. Initialize with{" "} + {component.bind.initialProp}; reset with {component.bind.resetProp}. +

+ )} + {component.subcomponents.length > 0 &&

Subcomponents: {component.subcomponents.join(", ")}.

} + +
    + {component.blockedProps.map((prop) => ( +
  • + {prop.name} is blocked: {prop.reason} +
  • + ))} + {component.accessibilityRequirements.map((requirement) => ( +
  • {requirement}
  • + ))} +
+

+ Upstream documentation for {component.name} +

+
+ ))} + + ); +} diff --git a/apps/docs/src/components/mdx/metadata-markdown.tsx b/apps/docs/src/components/mdx/metadata-markdown.tsx index 74130f37b..6f4d96aeb 100644 --- a/apps/docs/src/components/mdx/metadata-markdown.tsx +++ b/apps/docs/src/components/mdx/metadata-markdown.tsx @@ -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(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), diff --git a/apps/docs/src/components/pages/getting-started/getting-started-overview.tsx b/apps/docs/src/components/pages/getting-started/getting-started-overview.tsx index d2fc2d962..30ccbad05 100644 --- a/apps/docs/src/components/pages/getting-started/getting-started-overview.tsx +++ b/apps/docs/src/components/pages/getting-started/getting-started-overview.tsx @@ -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 (

How Homarr works

-

Services expose data, integrations connect to them, widgets use the data, and boards arrange the widgets.

+

+ Services expose data, integrations connect to them, widgets use the data, and boards arrange the widgets. +

- + {!isMarkdown && }
@@ -50,19 +58,18 @@ export function GettingStartedOverview() {

Board edit mode

Add items, move them, and resize them on the grid. Each viewport can have its own layout.

- - Board docs