diff --git a/apps/website/app/(docs)/docs/(landing)/layout.tsx b/apps/website/app/(docs)/docs/(landing)/layout.tsx new file mode 100644 index 000000000..0c9eee5c2 --- /dev/null +++ b/apps/website/app/(docs)/docs/(landing)/layout.tsx @@ -0,0 +1,11 @@ +import "~/globals.css"; + +type DocsLandingLayoutProps = { + children: React.ReactNode; +}; + +const DocsLandingLayout = ({ + children, +}: DocsLandingLayoutProps): React.ReactElement => <>{children}; + +export default DocsLandingLayout; diff --git a/apps/website/app/(docs)/docs/(landing)/page.tsx b/apps/website/app/(docs)/docs/(landing)/page.tsx new file mode 100644 index 000000000..43c929aca --- /dev/null +++ b/apps/website/app/(docs)/docs/(landing)/page.tsx @@ -0,0 +1,95 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { ArrowRight } from "lucide-react"; +import { Card, CardContent } from "@repo/ui/components/ui/card"; +import { PlatformBadge } from "~/components/PlatformBadge"; +import { Logo } from "~/components/Logo"; + +export const metadata: Metadata = { + title: "Documentation", + description: + "Choose the Discourse Graphs documentation for Roam Research or Obsidian.", +}; + +const DOCS_DESTINATIONS = [ + { + description: + "Installation, graph building, querying, and advanced workflows for the Roam Research plugin.", + href: "/docs/roam", + platform: "roam", + title: "Roam docs", + }, + { + description: + "Setup, node and relation authoring, sync, and workspace configuration for the Obsidian plugin.", + href: "/docs/obsidian", + platform: "obsidian", + title: "Obsidian docs", + }, +] as const; + +const DocsLandingPage = (): React.ReactElement => { + return ( +
+
+
+ + + Back to site + +
+
+ +
+
+
+

+ Documentation +

+

+ Choose your docs +

+

+ Discourse Graphs has separate documentation for each client. Pick + the one you are using to get the right setup instructions, + workflows, and reference pages. +

+
+ +
+ {DOCS_DESTINATIONS.map(({ description, href, platform, title }) => ( + + + +
+ + +
+ +
+

+ {title} +

+

+ {description} +

+
+ +

+ Open documentation +

+
+
+ + ))} +
+
+
+
+ ); +}; + +export default DocsLandingPage; diff --git a/apps/website/app/(docs)/docs/_components/DocsPageTemplate.tsx b/apps/website/app/(docs)/docs/_components/DocsPageTemplate.tsx new file mode 100644 index 000000000..ccd8d6506 --- /dev/null +++ b/apps/website/app/(docs)/docs/_components/DocsPageTemplate.tsx @@ -0,0 +1,34 @@ +import type { EvaluateResult } from "nextra"; +import { useMDXComponents } from "mdx-components"; + +type DocsPageTemplateProps = Omit & { + children: React.ReactNode; +}; + +const hasPrimaryHeading = (sourceCode: string): boolean => + /(^|\n)#\s+\S/m.test(sourceCode); + +const DocsPageTemplate = ({ + children, + metadata, + sourceCode, + ...wrapperProps +}: DocsPageTemplateProps): React.ReactElement => { + const { h1, wrapper } = useMDXComponents(); + // eslint-disable-next-line @typescript-eslint/naming-convention + const Wrapper = wrapper as React.ComponentType; + const H1 = h1 as React.ComponentType< + React.HTMLAttributes & { + children: React.ReactNode; + } + >; + + return ( + + {!hasPrimaryHeading(sourceCode) &&

{metadata.title}

} + {children} +
+ ); +}; + +export default DocsPageTemplate; diff --git a/apps/website/app/(docs)/docs/_components/DocsThemeLayout.tsx b/apps/website/app/(docs)/docs/_components/DocsThemeLayout.tsx new file mode 100644 index 000000000..c2d96ebdc --- /dev/null +++ b/apps/website/app/(docs)/docs/_components/DocsThemeLayout.tsx @@ -0,0 +1,86 @@ +import type { PageMapItem } from "nextra"; +import { Search } from "nextra/components"; +import { Footer, Layout, Navbar } from "nextra-theme-docs"; +import { Logo } from "~/components/Logo"; + +type DocsSearchScope = "roam" | "obsidian"; + +type DocsThemeLayoutProps = { + children: React.ReactNode; + hideSearch?: boolean; + pageMap: PageMapItem[]; + searchScope?: DocsSearchScope; +}; + +const SEARCH_PLACEHOLDERS: Record = { + obsidian: "Search Obsidian docs...", + roam: "Search Roam docs...", +}; + +const renderSearch = ({ + hideSearch, + searchScope, +}: Pick): + | React.ReactElement + | null + | undefined => { + if (hideSearch) { + return null; + } + + if (!searchScope) { + return undefined; + } + + return ( + + ); +}; + +const DocsThemeLayout = ({ + children, + hideSearch, + pageMap, + searchScope, +}: DocsThemeLayoutProps): React.ReactElement => { + const search = renderSearch({ hideSearch, searchScope }); + + return ( +
+ + Apache 2.0 {new Date().getFullYear()} (c) Discourse Graphs. + + } + navbar={ + } + projectLink="https://github.com/DiscourseGraphs/discourse-graph" + /> + } + pageMap={pageMap} + search={search} + sidebar={{ + defaultMenuCollapseLevel: 1, + }} + toc={{ + backToTop: "Back to top", + }} + > + {children} + +
+ ); +}; + +export default DocsThemeLayout; diff --git a/apps/website/app/(docs)/docs/obsidian/[[...mdxPath]]/page.tsx b/apps/website/app/(docs)/docs/obsidian/[[...mdxPath]]/page.tsx new file mode 100644 index 000000000..f4600feb1 --- /dev/null +++ b/apps/website/app/(docs)/docs/obsidian/[[...mdxPath]]/page.tsx @@ -0,0 +1,69 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { generateStaticParamsFor, importPage } from "nextra/pages"; +import DocsPageTemplate from "../../_components/DocsPageTemplate"; + +type DocsPageProps = { + params: Promise<{ + mdxPath?: string[]; + }>; +}; + +type ImportedPage = Awaited>; + +const generateAllStaticParams = generateStaticParamsFor("mdxPath"); + +const loadPage = async (mdxPath?: string[]): Promise => + importPage(["obsidian", ...(mdxPath ?? [])]); + +export const generateStaticParams = async (): Promise< + Array<{ mdxPath?: string[] }> +> => { + const staticParams = await generateAllStaticParams(); + + return staticParams.flatMap(({ mdxPath }) => { + if (!Array.isArray(mdxPath) || mdxPath[0] !== "obsidian") { + return []; + } + + const platformPath = mdxPath.slice(1); + + return platformPath.length ? [{ mdxPath: platformPath }] : [{}]; + }); +}; + +const Page = async ({ params }: DocsPageProps): Promise => { + try { + const { mdxPath } = await params; + const result = await loadPage(mdxPath); + const { default: MDXContent, ...wrapperProps } = result; + + return ( + + + + ); + } catch (error) { + console.error("Error rendering Obsidian docs page:", error); + notFound(); + } +}; + +export const generateMetadata = async ({ + params, +}: DocsPageProps): Promise => { + try { + const { mdxPath } = await params; + const { metadata } = await loadPage(mdxPath); + + return metadata; + } catch (error) { + console.error("Error generating Obsidian docs metadata:", error); + + return { + title: "Obsidian docs", + }; + } +}; + +export default Page; diff --git a/apps/website/app/(docs)/docs/obsidian/[slug]/page.tsx b/apps/website/app/(docs)/docs/obsidian/[slug]/page.tsx deleted file mode 100644 index 81ef7c912..000000000 --- a/apps/website/app/(docs)/docs/obsidian/[slug]/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { docMap } from "~/(docs)/docs/obsidian/docMap"; -import { Metadata } from "next"; -import { - generateDocsStaticParams, - generateDocsMetadata, - DocsPage, -} from "~/components/DocsPage"; - -const Page = async ({ params }: { params: Promise<{ slug: string }> }) => { - const { slug } = await params; - const directory = docMap[slug] ?? docMap.default; - - return await DocsPage({ params: Promise.resolve({ slug }), directory }); -}; - -export default Page; - -export const generateStaticParams = () => - generateDocsStaticParams([...new Set(Object.values(docMap))]); - -export const generateMetadata = async ({ - params, -}: { - params: Promise<{ slug: string }>; -}): Promise => { - const { slug } = await params; - const directory = docMap[slug] ?? docMap.default; - return generateDocsMetadata({ - params: Promise.resolve({ slug }), - directory, - }); -}; diff --git a/apps/website/app/(docs)/docs/obsidian/layout.tsx b/apps/website/app/(docs)/docs/obsidian/layout.tsx index f71434c71..9c136cbc6 100644 --- a/apps/website/app/(docs)/docs/obsidian/layout.tsx +++ b/apps/website/app/(docs)/docs/obsidian/layout.tsx @@ -1,10 +1,22 @@ -import { navigation } from "./navigation"; -import { Layout } from "~/components/DocsLayout"; +import { getPageMap } from "nextra/page-map"; +import DocsThemeLayout from "../_components/DocsThemeLayout"; +import "../../../(nextra)/nextra-css.css"; +import "nextra-theme-docs/style-prefixed.css"; -export default function RootLayout({ - children, -}: { +type ObsidianDocsLayoutProps = { children: React.ReactNode; -}) { - return {children}; -} +}; + +const ObsidianDocsLayout = async ({ + children, +}: ObsidianDocsLayoutProps): Promise => { + const pageMap = await getPageMap("/docs/obsidian"); + + return ( + + {children} + + ); +}; + +export default ObsidianDocsLayout; diff --git a/apps/website/app/(docs)/docs/obsidian/page.tsx b/apps/website/app/(docs)/docs/obsidian/page.tsx deleted file mode 100644 index 80871f146..000000000 --- a/apps/website/app/(docs)/docs/obsidian/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { DocsRedirect } from "~/components/DocsRedirect"; -import { navigation } from "./navigation"; - -export default function Page() { - return ; -} diff --git a/apps/website/app/(docs)/docs/obsidian/pages/node-tags.md b/apps/website/app/(docs)/docs/obsidian/pages/node-tags.md index a72fa3c6e..1155a8490 100644 --- a/apps/website/app/(docs)/docs/obsidian/pages/node-tags.md +++ b/apps/website/app/(docs)/docs/obsidian/pages/node-tags.md @@ -5,8 +5,6 @@ author: "" published: true --- -## Overview - Node tags allow you to quickly create discourse nodes from tagged lines in your notes. When you assign a tag to a node type, any line containing that tag becomes a clickable element that can be converted into a discourse node. This feature streamlines your workflow by letting you mark potential discourse nodes with tags as you write, then easily convert them to full discourse nodes later. @@ -25,6 +23,7 @@ This feature streamlines your workflow by letting you mark potential discourse n ### Tag naming rules Tags must follow these rules: + - **No spaces**: Tags cannot contain whitespace - **Allowed characters**: Only letters (a-z, A-Z), numbers (0-9), and dashes (-) - **No special characters**: Characters like #, @, /, \, etc. are not allowed @@ -33,12 +32,14 @@ Tags must follow these rules: #### Tag examples **Valid tags:** + - `clm-candidate` - `question-idea` - `evidence2024` - `my-argument` **Invalid tags:** + - `clm candidate` (contains space) - `#clm-candidate` (contains #) - `clm/candidate` (contains /) @@ -57,9 +58,9 @@ When you hover over a tagged line, a button appears above the tag: 1. **Hover** over the tag you want to convert 2. Wait for the **"Create [Node Type]"** button to appear -![On hover](/docs/obsidian/on-hover-node-tag.png) + ![On hover](/docs/obsidian/on-hover-node-tag.png) 3. **Click** the button to open the node creation dialog -![Node creation dialog](/docs/obsidian/create-node-dialog-node-tag.png) + ![Node creation dialog](/docs/obsidian/create-node-dialog-node-tag.png) 4. Click "Confirm" to create node You'll see that the candidate node is now replaced with a formalized node diff --git a/apps/website/app/(docs)/docs/page.tsx b/apps/website/app/(docs)/docs/page.tsx deleted file mode 100644 index e87c18134..000000000 --- a/apps/website/app/(docs)/docs/page.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import Link from "next/link"; -import Image from "next/image"; - -const DocsPage = () => { - return ( -
-
-

- Future Home of All the Docs -

-

- For now, here are the{" "} - - Roam Docs{" "} - - and the{" "} - - Obsidian Docs{" "} - -

-
- -
-
-
- ); -}; - -export default DocsPage; diff --git a/apps/website/app/(docs)/docs/roam/[[...mdxPath]]/page.tsx b/apps/website/app/(docs)/docs/roam/[[...mdxPath]]/page.tsx new file mode 100644 index 000000000..42180f867 --- /dev/null +++ b/apps/website/app/(docs)/docs/roam/[[...mdxPath]]/page.tsx @@ -0,0 +1,69 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { generateStaticParamsFor, importPage } from "nextra/pages"; +import DocsPageTemplate from "../../_components/DocsPageTemplate"; + +type DocsPageProps = { + params: Promise<{ + mdxPath?: string[]; + }>; +}; + +type ImportedPage = Awaited>; + +const generateAllStaticParams = generateStaticParamsFor("mdxPath"); + +const loadPage = async (mdxPath?: string[]): Promise => + importPage(["roam", ...(mdxPath ?? [])]); + +export const generateStaticParams = async (): Promise< + Array<{ mdxPath?: string[] }> +> => { + const staticParams = await generateAllStaticParams(); + + return staticParams.flatMap(({ mdxPath }) => { + if (!Array.isArray(mdxPath) || mdxPath[0] !== "roam") { + return []; + } + + const platformPath = mdxPath.slice(1); + + return platformPath.length ? [{ mdxPath: platformPath }] : [{}]; + }); +}; + +const Page = async ({ params }: DocsPageProps): Promise => { + try { + const { mdxPath } = await params; + const result = await loadPage(mdxPath); + const { default: MDXContent, ...wrapperProps } = result; + + return ( + + + + ); + } catch (error) { + console.error("Error rendering Roam docs page:", error); + notFound(); + } +}; + +export const generateMetadata = async ({ + params, +}: DocsPageProps): Promise => { + try { + const { mdxPath } = await params; + const { metadata } = await loadPage(mdxPath); + + return metadata; + } catch (error) { + console.error("Error generating Roam docs metadata:", error); + + return { + title: "Roam docs", + }; + } +}; + +export default Page; diff --git a/apps/website/app/(docs)/docs/roam/[slug]/page.tsx b/apps/website/app/(docs)/docs/roam/[slug]/page.tsx deleted file mode 100644 index 9617b5560..000000000 --- a/apps/website/app/(docs)/docs/roam/[slug]/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { docMap } from "~/(docs)/docs/roam/docMap"; -import { Metadata } from "next"; -import { - generateDocsStaticParams, - generateDocsMetadata, - DocsPage, -} from "~/components/DocsPage"; - -type Params = { - params: Promise<{ - slug: string; - }>; -}; - -const Page = async ({ params }: Params) => { - const { slug } = await params; - const directory = docMap[slug] ?? docMap.default; - - return await DocsPage({ params: Promise.resolve({ slug }), directory }); -}; - -export default Page; - -export const generateStaticParams = () => - generateDocsStaticParams([...new Set(Object.values(docMap))]); - -export const generateMetadata = async ({ - params, -}: Params): Promise => { - const { slug } = await params; - const directory = docMap[slug] ?? docMap.default; - return generateDocsMetadata({ params: Promise.resolve({ slug }), directory }); -}; diff --git a/apps/website/app/(docs)/docs/roam/layout.tsx b/apps/website/app/(docs)/docs/roam/layout.tsx index 18096f77c..9a82c0eb4 100644 --- a/apps/website/app/(docs)/docs/roam/layout.tsx +++ b/apps/website/app/(docs)/docs/roam/layout.tsx @@ -1,10 +1,22 @@ -import { Layout } from "~/components/DocsLayout"; -import { navigation } from "./navigation"; +import { getPageMap } from "nextra/page-map"; +import DocsThemeLayout from "../_components/DocsThemeLayout"; +import "../../../(nextra)/nextra-css.css"; +import "nextra-theme-docs/style-prefixed.css"; -export default function RootLayout({ - children, -}: { +type RoamDocsLayoutProps = { children: React.ReactNode; -}) { - return {children}; -} +}; + +const RoamDocsLayout = async ({ + children, +}: RoamDocsLayoutProps): Promise => { + const pageMap = await getPageMap("/docs/roam"); + + return ( + + {children} + + ); +}; + +export default RoamDocsLayout; diff --git a/apps/website/app/(docs)/docs/roam/page.tsx b/apps/website/app/(docs)/docs/roam/page.tsx deleted file mode 100644 index 4496ed7bf..000000000 --- a/apps/website/app/(docs)/docs/roam/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { redirect, notFound } from "next/navigation"; -import { navigation } from "./navigation"; - -export default function Page() { - const firstSection = navigation[0]; - const firstLink = firstSection?.links[0]; - - if (!firstLink?.href) { - notFound(); - } - - redirect(firstLink.href); -} diff --git a/apps/website/app/(docs)/docs/roam/pages/migration-to-stored-relations.md b/apps/website/app/(docs)/docs/roam/pages/migration-to-stored-relations.md index e79d02059..b09d38256 100644 --- a/apps/website/app/(docs)/docs/roam/pages/migration-to-stored-relations.md +++ b/apps/website/app/(docs)/docs/roam/pages/migration-to-stored-relations.md @@ -5,8 +5,6 @@ author: "" published: true --- -## Overview - Stored relations make relations load faster and more reliably. Discourse Graph installs as of v0.18.0 use them by default. Older installs need a one-time migration that copies existing pattern-based relations into the new format. This guide covers the migration flow from **Personal Settings** for older installs. If you're new to stored relations, start with the [stored relations overview](./stored-relations). diff --git a/apps/website/app/(docs)/docs/roam/pages/nodes.md b/apps/website/app/(docs)/docs/roam/pages/nodes.md index 145e199c8..8483e572e 100644 --- a/apps/website/app/(docs)/docs/roam/pages/nodes.md +++ b/apps/website/app/(docs)/docs/roam/pages/nodes.md @@ -16,7 +16,3 @@ It is not possible to directly specify that a source or target node in a relatio For example, if a node's incoming relation is `references`, that implies it is a page. Similarly, if the node's incoming relation is `has child` or `has ancestor`, that implies the node is a block. When in doubt, check the preview of your relation pattern to ensure you're correctly expressing your intentions! - -## Next - -- [Operators and relations](./operators-relations) diff --git a/apps/website/app/(docs)/docs/roam/pages/operators-relations.md b/apps/website/app/(docs)/docs/roam/pages/operators-relations.md index 766974d36..c738ef5e6 100644 --- a/apps/website/app/(docs)/docs/roam/pages/operators-relations.md +++ b/apps/website/app/(docs)/docs/roam/pages/operators-relations.md @@ -64,7 +64,3 @@ published: true - **description**: exact match to user-defined `discourse nodes` only (ALTHOUGH the autocomplete will allow you to specify other stuff that don't make sense) - **source**: a `page` (since all discourse nodes must be pages) - **target**: a `discourse node` (defined in your grammar) - -## Next - -- [Stored relations](./stored-relations) diff --git a/apps/website/app/(docs)/docs/roam/pages/relations-patterns.md b/apps/website/app/(docs)/docs/roam/pages/relations-patterns.md index 628c0fb59..f7b50398c 100644 --- a/apps/website/app/(docs)/docs/roam/pages/relations-patterns.md +++ b/apps/website/app/(docs)/docs/roam/pages/relations-patterns.md @@ -1,5 +1,5 @@ --- -title: "Relations and patterns" +title: "Legacy relations patterns" date: "2025-01-01" author: "" published: true diff --git a/apps/website/app/(docs)/docs/roam/pages/stored-relations.md b/apps/website/app/(docs)/docs/roam/pages/stored-relations.md index 6b6abfd86..4c623eb08 100644 --- a/apps/website/app/(docs)/docs/roam/pages/stored-relations.md +++ b/apps/website/app/(docs)/docs/roam/pages/stored-relations.md @@ -5,8 +5,6 @@ author: "" published: true --- -## Overview - Stored relations are explicit relationships between discourse nodes. They are created directly in the graph and saved as data, so they can be used consistently across overlays, queries, and canvases. ## What is a stored relation? diff --git a/apps/website/app/(docs)/layout.tsx b/apps/website/app/(docs)/layout.tsx index d2181de4d..5ebae9beb 100644 --- a/apps/website/app/(docs)/layout.tsx +++ b/apps/website/app/(docs)/layout.tsx @@ -1,9 +1,6 @@ import { type Metadata } from "next"; import { Inter } from "next/font/google"; -import clsx from "clsx"; -import { customScrollbar } from "~/components/DocsLayout"; import { DESCRIPTION } from "~/data/constants"; -import "~/globals.css"; const inter = Inter({ subsets: ["latin"], @@ -20,17 +17,7 @@ export const metadata: Metadata = { }; const DocsLayout = ({ children }: { children: React.ReactNode }) => { - return ( -
- {children} -
- ); + return
{children}
; }; export default DocsLayout; diff --git a/apps/website/app/(nextra)/nextra-css.css b/apps/website/app/(nextra)/nextra-css.css index 65dd5f63a..8d62f2633 100644 --- a/apps/website/app/(nextra)/nextra-css.css +++ b/apps/website/app/(nextra)/nextra-css.css @@ -1 +1,5 @@ @tailwind utilities; + +.nextra-reset { + --nextra-content-width: 90rem; +} diff --git a/apps/website/app/components/Logo.tsx b/apps/website/app/components/Logo.tsx index 0dec46a94..e7eef8ae8 100644 --- a/apps/website/app/components/Logo.tsx +++ b/apps/website/app/components/Logo.tsx @@ -1,36 +1,61 @@ "use client"; import Link from "next/link"; -import Image from "next/image"; import { usePathname } from "next/navigation"; import { PlatformBadge } from "./PlatformBadge"; const DOCS_PLATFORMS = { - "/docs/obsidian/": "obsidian", - "/docs/roam/": "roam", + "/docs/obsidian": "obsidian", + "/docs/roam": "roam", } as const; -export const Logo = () => { +type LogoProps = { + linked?: boolean; + textClassName?: string; +}; + +export const Logo = ({ + linked = true, + textClassName = "text-neutral-dark", +}: LogoProps) => { const pathname = usePathname(); const platform = Object.entries(DOCS_PLATFORMS).find(([path]) => pathname.includes(path), )?.[1]; + const brand = ( + <> +