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.
+
diff --git a/apps/website/content/_meta.ts b/apps/website/content/_meta.ts
new file mode 100644
index 000000000..d35fd540f
--- /dev/null
+++ b/apps/website/content/_meta.ts
@@ -0,0 +1,13 @@
+import type { MetaRecord } from "nextra";
+
+const meta: MetaRecord = {
+ index: "Overview",
+ roam: {
+ title: "Roam",
+ },
+ obsidian: {
+ title: "Obsidian",
+ },
+};
+
+export default meta;
diff --git a/apps/website/content/index.mdx b/apps/website/content/index.mdx
new file mode 100644
index 000000000..3a4378e9a
--- /dev/null
+++ b/apps/website/content/index.mdx
@@ -0,0 +1,21 @@
+---
+title: Documentation
+description: Discourse Graphs documentation for the Roam extension and Obsidian plugin.
+---
+
+import { Callout, Cards } from "nextra/components";
+
+# Documentation
+
+Choose the product documentation you need. All `/docs` routes now render through Nextra, while the `/nextra` sandbox remains available for experimentation.
+
+
+ Shared conceptual docs, like What is a discourse graph? and
+ Base grammar, are available from both product sections so
+ URLs stay stable.
+
+
+
+
+
+
diff --git a/apps/website/content/obsidian/_meta.ts b/apps/website/content/obsidian/_meta.ts
new file mode 100644
index 000000000..3cd87c634
--- /dev/null
+++ b/apps/website/content/obsidian/_meta.ts
@@ -0,0 +1,16 @@
+import type { MetaRecord } from "nextra";
+
+const meta: MetaRecord = {
+ index: {
+ title: "Overview",
+ display: "hidden",
+ },
+ welcome: "Welcome",
+ fundamentals: "Fundamentals",
+ configuration: "Configuration",
+ "core-features": "Core features",
+ "advanced-features": "Advanced features",
+ "use-cases": "Use cases",
+};
+
+export default meta;
diff --git a/apps/website/content/obsidian/advanced-features/_meta.ts b/apps/website/content/obsidian/advanced-features/_meta.ts
new file mode 100644
index 000000000..ca1e99ccb
--- /dev/null
+++ b/apps/website/content/obsidian/advanced-features/_meta.ts
@@ -0,0 +1,8 @@
+import type { MetaRecord } from "nextra";
+
+const meta: MetaRecord = {
+ "command-palette": "Command palette",
+ "sync-and-import": "Sync and import",
+};
+
+export default meta;
diff --git a/apps/website/content/obsidian/advanced-features/command-palette.md b/apps/website/content/obsidian/advanced-features/command-palette.md
new file mode 100644
index 000000000..620368949
--- /dev/null
+++ b/apps/website/content/obsidian/advanced-features/command-palette.md
@@ -0,0 +1,23 @@
+---
+title: "Command palette integration"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+## Using the Command Palette
+
+The Command Palette is a powerful way to access all Discourse Graph features without memorizing keyboard shortcuts. To open it:
+
+1. Press `Cmd/Ctrl + P`
+2. Type "Discourse" to see all available commands
+ 
+
+## Customizing Commands
+
+You can customize how commands appear in the Command Palette:
+
+1. Open Obsidian Settings
+2. Go to "Hotkeys"
+3. Search for "Discourse"
+4. Modify the commands or add hotkeys
diff --git a/apps/website/content/obsidian/advanced-features/sync-and-import.md b/apps/website/content/obsidian/advanced-features/sync-and-import.md
new file mode 100644
index 000000000..aa1801aa0
--- /dev/null
+++ b/apps/website/content/obsidian/advanced-features/sync-and-import.md
@@ -0,0 +1,146 @@
+---
+title: "Sync and Import"
+date: "2026-02-16"
+author: ""
+published: true
+---
+
+The Sync and Import feature allows you to synchronize your discourse nodes with the Discourse Graph database and share them with collaborators. Once enabled, you can publish nodes to a shared group space and import nodes published by others.
+
+> **Note:** This feature is currently in **beta**. The sync functionality requires an active connection to the Discourse Graph database.
+
+> **Warning:** When using sync and import:
+>
+> - Don't edit the things you imported
+> - Don't click to create new files (from imported nodes)
+> - Don't delete imported node types
+
+## Enabling sync mode
+
+### Step 1: Open settings
+
+Open the Discourse Graph plugin settings:
+
+1. Open **Obsidian settings** (click the gear icon in the bottom-left corner, or press `Cmd/Ctrl + ,`)
+2. Scroll down in the left sidebar to **community plugins**
+3. Find **Discourse Graph**
+
+### Step 2: Reveal the admin panel
+
+The sync feature is located in a hidden **admin panel** tab that is not visible by default.
+
+1. While on the Discourse Graph settings page, press `Cmd + Shift + A` (Mac) or `Ctrl + Shift + A` (Windows/Linux)
+2. A new **admin panel** tab will appear in the settings tab bar
+
+
+
+### Step 3: Enable sync mode
+
+1. In the **admin panel** tab, find the **(BETA) Sync mode enable** toggle
+2. Click the toggle to enable sync mode
+3. Click **Save changes**
+4. A confirmation notice will appear: "Admin panel settings saved"
+5. The sync mode will initialize automatically, and you will see a notice: "Sync mode initialized successfully"
+
+Once enabled, your discourse nodes will begin syncing automatically to the Discourse Graph database. The plugin monitors file changes in your vault (with a short delay) and syncs them in the background.
+
+## Publishing a discourse node
+
+Publishing makes a synced discourse node available to other members of your group. This is how you share your work with collaborators.
+
+### Prerequisites
+
+Before publishing, make sure:
+
+- **Sync mode is enabled** (see above)
+- **You are a member of a group.** Group membership is managed by your team administrator. The Discourse Graph team will ensure you are added to the appropriate group before you can publish.
+- The discourse node you want to publish has a `nodeTypeId` in its frontmatter
+- The discourse node has been **synced at least once** (it must have a `nodeInstanceId` in its frontmatter — this is assigned automatically after the first sync)
+
+### Steps to publish
+
+1. **Open the discourse node** you want to publish in the editor
+
+2. **Open the command palette** by pressing `Cmd/Ctrl + P`
+
+3. **Search for the publish command** by typing "Publish" and select **"Discourse Graph: Publish current node to lab space"**
+ 
+
+4. The plugin will:
+ - Publish the node to your group
+ - **Automatically publish any relations** connected to this node where the other endpoint has already been published to the same group
+ - Sync any embedded assets (images, attachments) to the shared storage
+ - Update the node's frontmatter with a `publishedToGroups` field
+
+5. A confirmation notice will appear: **"Published"**
+ 
+
+**Note on publishing relations:** Relations between nodes are published **automatically** — you do not need to publish them separately. When you publish a node, the plugin checks all its relations and publishes any where:
+
+- Both the source and destination nodes are published to the same group, **and**
+- The relation type is defined in your discourse relation settings
+
+## Importing discourse nodes from another space
+
+Importing allows you to bring published discourse nodes from other group members into your vault.
+
+### Steps to import
+
+1. **Open the command palette** by pressing `Cmd/Ctrl + P`
+
+2. **Search for the import command** by typing "Import" and select **"Discourse Graph: Import nodes from another space"**
+
+
+
+3. The **import nodes** modal will open and begin loading available nodes from your groups. Once loaded, you will see a list of importable nodes **grouped by space**. Each space section shows the space name and available nodes.
+
+
+
+4. Before importing, the modal shows an **import preview** summarizing everything that will be created:
+ - Number of nodes and relations to be imported
+ - Any new node types that will be added to your vault
+ - Any new relation types that will be added
+ - Any new discourse relation triplets (source → relation → destination) that will be established
+
+5. The plugin will import each selected node **and their associated relations**, then display a progress bar. Once complete, you will see a confirmation notice.
+
+Imported nodes are saved in an `import/{spaceName}/` folder in your vault, preserving the original space organization.
+
+
+
+> **Note:** Relations are only imported when **both** the source and destination nodes are present in your local vault (either as previously imported nodes or nodes you already have locally). Relations whose endpoints are missing will be skipped.
+
+---
+
+## Refreshing imported nodes
+
+After importing, you can fetch the latest content from the original sources to keep your imported nodes up to date.
+
+1. **Open the command palette** by pressing `Cmd/Ctrl + P`
+2. Search for "Fetch" and select **"Discourse Graph: Fetch latest content from imported nodes"**
+3. The plugin will check each imported node for updates and refresh any that have changed
+
+
+
+Alternatively, you can click the "Refresh" button in the Discourse Context panel.
+
+
+
+## Summary of commands
+
+- **Sync discourse nodes to Supabase**: Manually sync all discourse nodes to the database
+- **Publish current node to lab space**: Publish the active discourse node to your group, including any relations whose other endpoint is already published to the same group
+- **Import nodes from another space**: Open the import modal to browse and import shared nodes along with their relations
+- **Fetch latest content from imported nodes**: Refresh all imported nodes with the latest content
+
+## Troubleshooting
+
+- **"Sync mode is not enabled"** — You need to enable sync mode in the admin panel first (see [Enabling sync mode](#enabling-sync-mode) above)
+- **"Please sync the node first"** — The node hasn't been synced yet. Wait for automatic sync or trigger a manual sync via the command palette using **"Discourse Graph: Sync discourse nodes to Supabase"**.
+
+
+
+- **"You are not a member of any groups"** — You need to be added to a group before you can import nodes. Contact your team administrator
+- **No importable nodes found** — Either no nodes have been published to your groups, or you have already imported all available nodes
+- The frontmatter fields related to sync are normally hidden. You can choose to display them in the setting panel
+ 
diff --git a/apps/website/content/obsidian/configuration/_meta.ts b/apps/website/content/obsidian/configuration/_meta.ts
new file mode 100644
index 000000000..5f037a041
--- /dev/null
+++ b/apps/website/content/obsidian/configuration/_meta.ts
@@ -0,0 +1,9 @@
+import type { MetaRecord } from "nextra";
+
+const meta: MetaRecord = {
+ "node-types-templates": "Node types and templates",
+ "relationship-types": "Relationship types",
+ "general-settings": "General settings",
+};
+
+export default meta;
diff --git a/apps/website/content/obsidian/configuration/general-settings.md b/apps/website/content/obsidian/configuration/general-settings.md
new file mode 100644
index 000000000..a7f2b13e7
--- /dev/null
+++ b/apps/website/content/obsidian/configuration/general-settings.md
@@ -0,0 +1,27 @@
+---
+title: "General settings"
+date: "2025-06-27"
+author: ""
+published: true
+---
+
+The General settings page in the Discourse Graph plugin provides fundamental configuration options that affect how the plugin operates. Here are the available settings:
+
+## Show IDs in frontmatter
+
+This setting controls the visibility of identifiers in your note's frontmatter section.
+
+- When enabled, node type IDs and relation type IDs will be visible in the frontmatter of your notes
+- When disabled, these IDs will be hidden from view
+- This can be useful if you prefer a cleaner frontmatter appearance while still maintaining the underlying structure
+
+## Discourse nodes folder path
+
+This setting determines where new discourse nodes will be created in your vault.
+
+- Specify a folder path where you want all new discourse nodes to be stored
+- Leave the field empty to create nodes in the root folder of your vault
+- You can use the folder suggester to easily navigate and select existing folders
+- Example format: `folder1/folder2`
+
+Changes to these settings will only take effect after clicking the "Save Changes" button. The interface will indicate when you have unsaved changes pending.
diff --git a/apps/website/content/obsidian/configuration/node-types-templates.md b/apps/website/content/obsidian/configuration/node-types-templates.md
new file mode 100644
index 000000000..c8fa32139
--- /dev/null
+++ b/apps/website/content/obsidian/configuration/node-types-templates.md
@@ -0,0 +1,52 @@
+---
+title: "Node types & templates"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+## Configuring node types
+
+Node types are the building blocks of your discourse graph. Each node type represents a different kind of content or concept in your notes.
+
+### Adding a node type
+
+1. Under "Node Types," click "Add Node Type"
+2. Enter a name for your node type (e.g., "Claim", "Evidence", "Question")
+3. Add the format for your node type. eg a claim node will have page title "CLM - {content}"
+4. **Template (Optional)**: Select a template from the dropdown to automatically apply template content when creating nodes of this type
+ - Templates are sourced from Obsidian's core Templates plugin
+ - Ensure you have the Templates plugin enabled and configured with a template folder
+ - The dropdown will show all available template files from your configured template folder
+
+ 
+
+ - Click "Save Changes"
+
+## Working with templates
+
+Templates allow you to automatically add predefined content when creating new nodes. They're especially useful for maintaining consistent structure across similar types of notes.
+
+### Setting up templates
+
+1. Create a new folder for templates
+ 
+
+2. Configure the template folder location in settings
+ 
+
+3. Create template files
+ 
+
+## Template requirements
+
+- Templates must be stored in your designated template folder
+- The Templates core plugin must be enabled
+- Template files should use Markdown format
+- Templates can include any valid Markdown content
+
+## Related
+
+- [Configure relationship types](/docs/obsidian/configuration/relationship-types)
+- [Learn about creating nodes](/docs/obsidian/core-features/creating-discourse-nodes)
+- [Explore advanced template usage](/docs/obsidian/configuration/node-types-templates#working-with-templates)
diff --git a/apps/website/content/obsidian/configuration/relationship-types.md b/apps/website/content/obsidian/configuration/relationship-types.md
new file mode 100644
index 000000000..80dfad8f2
--- /dev/null
+++ b/apps/website/content/obsidian/configuration/relationship-types.md
@@ -0,0 +1,56 @@
+---
+title: "Relationship types"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+## Understanding relationship types
+
+Relationship types define how different nodes in your discourse graph can connect to each other. Each relationship type has:
+
+- A primary label (e.g., "supports")
+- A complement label (e.g., "is supported by")
+- Rules about which node types can be connected
+
+## Adding relationship types
+
+1. Open Obsidian Settings
+2. Navigate to the "Discourse Graphs" settings tab
+3. Under "Relation Types," click "Add Relationship Type"
+4. Configure the relationship:
+ - Enter the primary label (e.g., "supports", "contradicts")
+ - Enter the complement label (e.g., "is supported by", "is contradicted by")
+ 
+5. Click "Save Changes"
+
+## Configuring valid relationships
+
+After creating relationship types, you need to define which node types can be connected by each relationship.
+
+1. Open the Discourse Relations tab in settings
+ 
+
+2. Choose the components:
+ - Source Node Type (e.g., Claim)
+ - Relationship Type (e.g., supports)
+ - Target Node Type (e.g., Question)
+ 
+
+3. Review and confirm the configuration
+ 
+
+## Example relationships
+
+Here are some common relationship types:
+
+- Claim → supports → Question
+- Evidence → supports → Claim
+- Evidence → contradicts → Claim
+- Source → informs → Question
+
+## Related
+
+- [Create your first relationship](/docs/obsidian/core-features/creating-discourse-relationships)
+- [Learn about the discourse context](/docs/obsidian/core-features/discourse-context)
+- [Explore your graph](/docs/obsidian/core-features/canvas)
diff --git a/apps/website/content/obsidian/core-features/_meta.ts b/apps/website/content/obsidian/core-features/_meta.ts
new file mode 100644
index 000000000..ef63f1b80
--- /dev/null
+++ b/apps/website/content/obsidian/core-features/_meta.ts
@@ -0,0 +1,11 @@
+import type { MetaRecord } from "nextra";
+
+const meta: MetaRecord = {
+ "creating-discourse-nodes": "Creating nodes",
+ "creating-discourse-relationships": "Creating relationships",
+ "discourse-context": "Discourse context",
+ canvas: "Canvas",
+ "node-tags": "Node tags",
+};
+
+export default meta;
diff --git a/apps/website/content/obsidian/core-features/canvas.md b/apps/website/content/obsidian/core-features/canvas.md
new file mode 100644
index 000000000..9334e9295
--- /dev/null
+++ b/apps/website/content/obsidian/core-features/canvas.md
@@ -0,0 +1,149 @@
+---
+title: "Canvas"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+The canvas feature in Discourse Graph provides an interactive visual workspace for creating and connecting discourse nodes and relations. Built on top of tldraw, it allows you to visually map out discourse structures with drag-and-drop functionality, adding other visual elements like texts, scribbles, embeddings, and images.
+
+## Creating a new canvas
+
+### Using the command palette
+
+1. Open the command palette (`Cmd/Ctrl + P`)
+2. Search for "Create new Discourse Graph canvas"
+3. Press Enter to create a new canvas
+
+
+The canvas will be created in your configured canvas folder (default: `Discourse Canvas/`) with a timestamp-based filename like `Canvas-2025-01-15-1430.md`.
+
+## Opening and viewing canvas
+
+### Opening a canvas
+
+1. Open the file of the canvas
+2. In case the file is still in markdown format, you can open the canvas mode by either
+
+Using the command palette
+
+
+Or click on the canvas icon at the top right corner
+
+
+**WARNING: DO NOT MODIFY MARKDOWN CONTENT OF FILE**
+
+## Discourse nodes
+
+### Creating new discourse nodes
+
+**Using the discourse node tool**
+
+- Click the discourse node icon in the toolbar
+- Select a node type from the right side panel
+- Click anywhere on the canvas to place a new node
+- Or drag the selected node type to the canvas
+- Enter the node title in the modal that appears
+
+
+
+### Adding existing nodes
+
+**Using node search**
+
+- Use the search bar in the top-right of the canvas
+- Type to search for existing discourse Nodes
+- Click on a node from the search results to add it to the canvas
+
+
+
+**Search filtering**
+
+- When a specific node type is selected, search results are filtered to that type
+
+
+
+## Discourse relations
+
+### Create new relations between nodes
+
+- Click the discourse node icon in the toolbar
+- Click the type of relations you want to create between two nodes
+- Click and drag the arrow from the source node to target node
+
+
+
+_Note_: The relation type selected must be compatible between the source and target nodes. Otherwise, you will receive a relation tool error. To update the setting on what relation types are possible between two kinds of discourse nodes, you can change the setting [Relation types setting](/docs/obsidian/configuration/relationship-types#configuring-valid-relationships)
+
+
+
+### Adding existing relations
+
+- Click on the discourse node you're trying to add existing relations of
+- Click on the "Relations" button that pops up. You'll see a panel showing all the relations that this node has
+- Click on the '+' or '-' to add these relations to the canvas
+ - For relations whose target discourse node isn't on canvas yet, it will be added to the canvas along with the relation
+
+
+
+## Canvas Features
+
+### Key figures
+
+You can choose to show the first image of discourse nodes of certain type. To change the setting on whether this first image show up, you can edit it in the Node Type Setting
+
+
+
+Then you will see the image show up if any is present in the file.
+
+
+
+### Open discourse node files
+
+- To open a discourse node to the side panel, you can press Shift + Click, or click on the sidebar icon when hover on discourse node.
+- To open file to a new tab, you can press Cmd + click on the file
+
+### Auto-save
+
+- Changes are automatically saved to the markdown file
+- No manual save required
+- File updates preserve the markdown structure and block references
+
+### Export Options
+
+- **Markdown View**: Switch to see the underlying markdown structure
+- **SVG Export**: Export canvas as SVG image (via tldraw)
+- **PNG Export**: Export canvas as PNG image (via tldraw)
+
+
+
+## Troubleshooting
+
+### Common Issues
+
+**Canvas Won't Load**:
+
+- Check that the file has proper frontmatter with `tldr-dg: true`
+- Verify the JSON data block is properly formatted
+- Try switching to markdown view and back to canvas view
+
+**Relations Won't Connect**:
+
+- Ensure the node types allow the relation type you're trying to create
+- Check your discourse relation configuration in settings
+- Verify both nodes are valid discourse nodes
+
+**Performance Issues**:
+
+- Consider splitting large canvases into multiple smaller ones
+- Close other resource-intensive applications
+
+**Search Not Working**:
+
+- Verify nodes have proper frontmatter with `nodeTypeId`
+- Check that the Obsidian metadata cache is up to date
+- Try restarting Obsidian to refresh the cache
+
+### Getting Help
+
+If you encounter issues with the canvas feature, reach out to our dev team via [Slack](https://join.slack.com/t/discoursegraphs/shared_invite/zt-37xklatti-cpEjgPQC0YyKYQWPNgAkEg)
diff --git a/apps/website/content/obsidian/core-features/creating-discourse-nodes.md b/apps/website/content/obsidian/core-features/creating-discourse-nodes.md
new file mode 100644
index 000000000..62f739084
--- /dev/null
+++ b/apps/website/content/obsidian/core-features/creating-discourse-nodes.md
@@ -0,0 +1,59 @@
+---
+title: "Creating discourse nodes"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+## Creating a node
+
+To create a discourse node, first select the text you want to turn into a node:
+
+
+
+There are two ways you can create a node:
+
+### Using command keys (recommended)
+
+#### Turn selected text into discourse node
+
+1. Press `Cmd + \` (or your configured hotkey)
+2. The Node Menu will open as a popup
+ 
+3. Select the node type you want to turn the text into
+4. A new discourse node will be created
+ 
+
+#### Creating new node from scratch
+
+1. Press `Cmd + \` (or your configured hotkey)
+2. Enter the title and node type
+ 
+
+### Using the right-click menu
+
+1. Right-click on the selected text
+ Alternatively, you can right-click on the selected text
+
+ 2. Choose a node type from the "Turn into discourse node" menu
+
+### Turn existing page into discourse node
+
+If a page is not a discourse node, you can turn it into one by clicking on the file menu, and chosing "Convert into" option
+
+
+After choosing a node type, you can edit the title and node type in the menu
+
+
+## Node templates
+
+When creating a node, if you've configured a [template for that node type](/docs/obsidian/configuration/node-types-templates#working-with-templates), the template content will be automatically applied to the new node.
+
+The new node will be saved in the designated folder that you created in the [General settings](/docs/obsidian/configuration/general-settings)
+
+## Related
+
+After creating nodes:
+
+- [Create relationships between nodes](/docs/obsidian/core-features/creating-discourse-relationships)
+- [Learn how to explore your graph](/docs/obsidian/core-features/canvas)
diff --git a/apps/website/content/obsidian/core-features/creating-discourse-relationships.md b/apps/website/content/obsidian/core-features/creating-discourse-relationships.md
new file mode 100644
index 000000000..8fb765ad2
--- /dev/null
+++ b/apps/website/content/obsidian/core-features/creating-discourse-relationships.md
@@ -0,0 +1,32 @@
+---
+title: "Creating discourse relationships"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+To create a relationship between discourse nodes:
+
+1. Open a note that you want to create a relationship from
+2. Open the discourse context by clicking the telescope icon on the left bar
+ 
+
+ Alternatively, you can:
+ - Set a hotkey to toggle the discourse context view
+ - Access it via the Command Palette
+ 
+
+3. Click "Add a new relation"
+
+4. Choose a relationship type from the dropdown. The dropdown shows all available relations that you have defined in the settings, and indicates what node types you can link with.
+ 
+
+5. Search for the target node by its title
+ 
+
+6. Click "Confirm" to create the relationship
+ 
+
+## Related
+
+- [Learn how to explore your graph](/docs/obsidian/core-features/canvas)
diff --git a/apps/website/content/obsidian/core-features/discourse-context.md b/apps/website/content/obsidian/core-features/discourse-context.md
new file mode 100644
index 000000000..6251122c2
--- /dev/null
+++ b/apps/website/content/obsidian/core-features/discourse-context.md
@@ -0,0 +1,43 @@
+---
+title: "Exploring your discourse graph"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+## Opening the discourse context
+
+The discourse context view is your main tool for exploring relationships between nodes in your graph. There are several ways to open it:
+
+### Method 1: Using the sidebar icon
+
+Click the telescope icon on the left sidebar to toggle the discourse context view.
+
+
+### Method 2: Using the command palette
+
+1. Open the Command Palette (`Cmd/Ctrl + P`)
+2. Search for "discourse context"
+ 
+
+### Method 3: Using hotkeys
+
+You can configure a custom hotkey in the Obsidian settings to quickly toggle the discourse context view.
+
+3. Configure a custom hotkey in settings
+
+## Using the discourse context
+
+The discourse context view shows you:
+
+- All relationships the current node has with other nodes
+- Types of relationships (supports, contradicts, etc.)
+- Quick access to related nodes
+- Tools for creating new relationships
+
+You can use this view to:
+
+- Navigate between related nodes
+- Understand how nodes connect to each other
+- Add new relationships
+- Get a quick overview of your graph structure
diff --git a/apps/website/content/obsidian/core-features/node-tags.md b/apps/website/content/obsidian/core-features/node-tags.md
new file mode 100644
index 000000000..1155a8490
--- /dev/null
+++ b/apps/website/content/obsidian/core-features/node-tags.md
@@ -0,0 +1,67 @@
+---
+title: "Node tags"
+date: "2025-10-15"
+author: ""
+published: true
+---
+
+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.
+
+## Setting up node tags
+
+### Configuring tags in settings
+
+1. Open the Discourse Graph settings
+2. Navigate to the **Node Types** section
+3. Select an existing node type or create a new one
+4. In the **Node tag** field, enter a tag identifier
+
+
+
+### 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
+- **Case-sensitive**: Tags are case-sensitive in the editor
+
+#### Tag examples
+
+**Valid tags:**
+
+- `clm-candidate`
+- `question-idea`
+- `evidence2024`
+- `my-argument`
+
+**Invalid tags:**
+
+- `clm candidate` (contains space)
+- `#clm-candidate` (contains #)
+- `clm/candidate` (contains /)
+
+## Using node tags in your notes
+
+### Adding tags to your notes
+
+Once you've configured a node tag for a node type, simply add the tag (prefixed with `#`) to any line in your notes:
+
+
+
+### Creating discourse nodes from tags
+
+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
+ 
+3. **Click** the button to open the node creation dialog
+ 
+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/content/obsidian/fundamentals/_meta.ts b/apps/website/content/obsidian/fundamentals/_meta.ts
new file mode 100644
index 000000000..a5730652c
--- /dev/null
+++ b/apps/website/content/obsidian/fundamentals/_meta.ts
@@ -0,0 +1,8 @@
+import type { MetaRecord } from "nextra";
+
+const meta: MetaRecord = {
+ "what-is-a-discourse-graph": "What is a discourse graph?",
+ "base-grammar": "Base grammar",
+};
+
+export default meta;
diff --git a/apps/website/content/obsidian/fundamentals/base-grammar.md b/apps/website/content/obsidian/fundamentals/base-grammar.md
new file mode 100644
index 000000000..e15419541
--- /dev/null
+++ b/apps/website/content/obsidian/fundamentals/base-grammar.md
@@ -0,0 +1,25 @@
+---
+title: "Base grammar"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+This is what ships with the extension.
+
+### Nodes
+
+- QUE - Question
+- CLM - Claim
+- EVD - Evidence
+- Source
+
+### Relations
+
+- EVD Informs QUE
+- EVD Supports CLM
+- EVD Opposes CLM
+
+Motivation for this base grammar is described in this [article](https://oasislab.pubpub.org/pub/54t0y9mk/release/3).
+
+This base grammar may be most useful for projects that interact with empirical evidence where you want to clearly distinguish between theory and evidence, and retain provenance to the source citations if you want to get more context.
diff --git a/apps/website/content/obsidian/fundamentals/what-is-a-discourse-graph.md b/apps/website/content/obsidian/fundamentals/what-is-a-discourse-graph.md
new file mode 100644
index 000000000..ebde4ad4e
--- /dev/null
+++ b/apps/website/content/obsidian/fundamentals/what-is-a-discourse-graph.md
@@ -0,0 +1,20 @@
+---
+title: "What is a discourse graph"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+**Discourse Graphs** are an information model for bodies of knowledge that emphasize discourse moves (such as questions, claims, and evidence), and relations (such as support or opposition), rather than papers or sources as the main units.
+
+To give an intuition for what it is, here is a figure of a visual representation of a simple Discourse Graph for a body of literature on bans and antisocial behavior in online forums. You may recognize similarities to things like argument maps.
+
+
+
+Consider how that information model foregrounds the conceptual building blocks and relationships that are important for synthesis, compared to a typical "[iTunes for papers](http://joelchan.me/assets/pdf/2019-cscw-beyond-itunes-for-papers.pdf)" model that foregrounds documents in a way that forces us to drudge through [tedious extraction work](https://dl.acm.org/doi/abs/10.1145/3295750.3298937) before we can do the thinking we want to do!
+
+
+
+This information model (theoretically!) has high potential for augmenting individual and collective "research-grade" synthesis (e.g., lit reviews for a dissertation or grant proposal).
+
+Discourse Graphs are not a new idea (you can read more about it in [this short (academic-focused) write-up](http://joelchan.me/assets/pdf/Discourse_Graphs_for_Augmented_Knowledge_Synthesis_What_and_Why.pdf) or this more [practically-oriented article](https://oasislab.pubpub.org/pub/54t0y9mk/release/3)), but the potential to use it for everyday research work is!
diff --git a/apps/website/content/obsidian/index.mdx b/apps/website/content/obsidian/index.mdx
new file mode 100644
index 000000000..683912bba
--- /dev/null
+++ b/apps/website/content/obsidian/index.mdx
@@ -0,0 +1,38 @@
+---
+title: Obsidian documentation
+description: Learn how to install, configure, and use the Discourse Graph plugin for Obsidian.
+---
+
+import { Callout, Cards } from "nextra/components";
+
+# Obsidian documentation
+
+Use these docs to install the Discourse Graph plugin for Obsidian, configure node and relationship types, and work with discourse structures directly in your vault.
+
+
+ Start with Getting started if you want the fastest path to a
+ working vault.
+
+
+
+
+
+
+
+
diff --git a/apps/website/content/obsidian/use-cases/_meta.ts b/apps/website/content/obsidian/use-cases/_meta.ts
new file mode 100644
index 000000000..c193a603e
--- /dev/null
+++ b/apps/website/content/obsidian/use-cases/_meta.ts
@@ -0,0 +1,10 @@
+import type { MetaRecord } from "nextra";
+
+const meta: MetaRecord = {
+ "literature-reviewing": "Literature review",
+ "research-roadmapping": "Research notes",
+ "reading-clubs": "Reading clubs and seminars",
+ "lab-notebooks": "Lab notebooks",
+};
+
+export default meta;
diff --git a/apps/website/content/obsidian/use-cases/lab-notebooks.md b/apps/website/content/obsidian/use-cases/lab-notebooks.md
new file mode 100644
index 000000000..9bdfa548c
--- /dev/null
+++ b/apps/website/content/obsidian/use-cases/lab-notebooks.md
@@ -0,0 +1,8 @@
+---
+title: "Lab notebooks"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+Description coming soon!
diff --git a/apps/website/content/obsidian/use-cases/literature-reviewing.md b/apps/website/content/obsidian/use-cases/literature-reviewing.md
new file mode 100644
index 000000000..3aded41b9
--- /dev/null
+++ b/apps/website/content/obsidian/use-cases/literature-reviewing.md
@@ -0,0 +1,14 @@
+---
+title: "Literature reviewing"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+This is currently the most common use case.
+
+Lukas Kawerau (aka Cortex Futura) has a course that integrates the extension into a complete system for academic literature reviewing and writing: [https://learn.cortexfutura.com/p/cite-to-write-v2](https://learn.cortexfutura.com/p/cite-to-write-v2)
+
+Lukas gives a short overview of how in this tweet thread:
+
+[https://x.com/cortexfutura/status/1441795897680011276](https://x.com/cortexfutura/status/1441795897680011276)
diff --git a/apps/website/content/obsidian/use-cases/reading-clubs.md b/apps/website/content/obsidian/use-cases/reading-clubs.md
new file mode 100644
index 000000000..51c94d6e5
--- /dev/null
+++ b/apps/website/content/obsidian/use-cases/reading-clubs.md
@@ -0,0 +1,10 @@
+---
+title: "Reading clubs"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+Description coming soon!
+
+[Get in touch](mailto:joechan@umd.edu) if you're interested in joining a Reading Club!
diff --git a/apps/website/content/obsidian/use-cases/research-roadmapping.md b/apps/website/content/obsidian/use-cases/research-roadmapping.md
new file mode 100644
index 000000000..01d12fc3d
--- /dev/null
+++ b/apps/website/content/obsidian/use-cases/research-roadmapping.md
@@ -0,0 +1,8 @@
+---
+title: "Research roadmapping"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+Description coming soon!
diff --git a/apps/website/content/obsidian/welcome/_meta.ts b/apps/website/content/obsidian/welcome/_meta.ts
new file mode 100644
index 000000000..ac6fd187f
--- /dev/null
+++ b/apps/website/content/obsidian/welcome/_meta.ts
@@ -0,0 +1,8 @@
+import type { MetaRecord } from "nextra";
+
+const meta: MetaRecord = {
+ "getting-started": "Getting started",
+ installation: "Installation",
+};
+
+export default meta;
diff --git a/apps/website/content/obsidian/welcome/getting-started.md b/apps/website/content/obsidian/welcome/getting-started.md
new file mode 100644
index 000000000..92dcb437a
--- /dev/null
+++ b/apps/website/content/obsidian/welcome/getting-started.md
@@ -0,0 +1,42 @@
+---
+title: "Getting started"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+The Discourse Graph plugin enables [Obsidian](https://obsidian.md/) users to seamlessly add additional semantic structure to their notes, including specified page types and link types that [model scientific discourse](/docs/obsidian/fundamentals/what-is-a-discourse-graph), to enable more complex and structured knowledge synthesis work, such as a complex interdisciplinary literature review, and enhanced collaboration with others on this work.
+
+For more information about Discourse Graphs, check out our website at [https://discoursegraphs.com](https://discoursegraphs.com)
+
+## Quick Start Guide
+
+1. [Install the plugin](/docs/obsidian/welcome/installation)
+2. [Configure node and relationship types](/docs/obsidian/configuration/node-types-templates)
+3. [Create your first discourse node](/docs/obsidian/core-features/creating-discourse-nodes)
+4. [Create relationships between nodes](/docs/obsidian/core-features/creating-discourse-relationships)
+5. [Explore your discourse graph](/docs/obsidian/core-features/canvas)
+
+## What's Next?
+
+- Learn about [what makes a discourse graph](/docs/obsidian/fundamentals/what-is-a-discourse-graph)
+- Explore [use cases](/docs/obsidian/use-cases/literature-reviewing) for discourse graphs
+- Join our community to share your experience and get help
+
+## Guides: Jump right in
+
+Follow our handy guides to get started on the basics as quickly as possible:
+
+- [Creating discourse nodes](/docs/obsidian/core-features/creating-discourse-nodes)
+- [Creating discourse relationships](/docs/obsidian/core-features/creating-discourse-relationships)
+- [Exploring your Discourse Graph](/docs/obsidian/core-features/canvas)
+- [Running advanced commands](/docs/obsidian/advanced-features/command-palette)
+- [Extending and personalizing your Discourse Graph](/docs/obsidian/configuration/general-settings)
+
+## Fundamentals: Dive a little deeper
+
+Learn the fundamentals of the Discourse Graph plugin to get a deeper understanding of our main features:
+
+- [What is a Discourse Graph?](/docs/obsidian/fundamentals/what-is-a-discourse-graph)
+- [Relationship types](/docs/obsidian/configuration/relationship-types)
+- [The Base Grammar: questions, claims, and evidence](/docs/obsidian/fundamentals/base-grammar)
diff --git a/apps/website/content/obsidian/welcome/installation.md b/apps/website/content/obsidian/welcome/installation.md
new file mode 100644
index 000000000..ece1d6525
--- /dev/null
+++ b/apps/website/content/obsidian/welcome/installation.md
@@ -0,0 +1,38 @@
+---
+title: "Installation"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+## Prerequisites
+
+### Install BRAT (Beta Reviewer's Auto-update Tester)
+
+1. Open Obsidian Settings
+2. Go to Community Plugins and disable Restricted Mode
+3. Click "Browse" and search for "BRAT"
+ 
+4. Install BRAT and enable it
+
+### Install DataCore via BRAT
+
+1. Open Obsidian Settings
+2. Go to "Community Plugins" → "BRAT"
+3. Click "Add Beta Plugin"
+ 
+4. Enter the repository URL: `https://github.com/blacksmithgu/datacore` and choose "Latest version"
+ 
+5. Check the box for "Enable after installing the plugin"
+6. Click "Add plugin"
+
+## Install Discourse Graphs
+
+1. Open Obsidian Settings
+2. Go to "Community Plugins" → "BRAT"
+3. Click "Add Beta Plugin"
+ 
+4. Enter the repository URL: `https://github.com/DiscourseGraphs/discourse-graph-obsidian` and choose "Latest version"
+ 
+5. Check the box for "Enable after installing the plugin"
+6. Click "Add Plugin"
diff --git a/apps/website/content/roam/_meta.ts b/apps/website/content/roam/_meta.ts
new file mode 100644
index 000000000..3ce033f3c
--- /dev/null
+++ b/apps/website/content/roam/_meta.ts
@@ -0,0 +1,14 @@
+import type { MetaRecord } from "nextra";
+
+const meta: MetaRecord = {
+ index: {
+ title: "Overview",
+ display: "hidden",
+ },
+ welcome: "Welcome",
+ guides: "Guides",
+ fundamentals: "Fundamentals",
+ "use-cases": "Use cases",
+};
+
+export default meta;
diff --git a/apps/website/content/roam/fundamentals/_meta.ts b/apps/website/content/roam/fundamentals/_meta.ts
new file mode 100644
index 000000000..c41e3335d
--- /dev/null
+++ b/apps/website/content/roam/fundamentals/_meta.ts
@@ -0,0 +1,9 @@
+import type { MetaRecord } from "nextra";
+
+const meta: MetaRecord = {
+ "what-is-a-discourse-graph": "What is a discourse graph?",
+ grammar: "Grammar",
+ "relations-patterns": "Relations and patterns",
+};
+
+export default meta;
diff --git a/apps/website/content/roam/fundamentals/grammar/_meta.ts b/apps/website/content/roam/fundamentals/grammar/_meta.ts
new file mode 100644
index 000000000..7d337e68d
--- /dev/null
+++ b/apps/website/content/roam/fundamentals/grammar/_meta.ts
@@ -0,0 +1,14 @@
+import type { MetaRecord } from "nextra";
+
+const meta: MetaRecord = {
+ index: {
+ title: "Overview",
+ display: "hidden",
+ },
+ nodes: "Nodes",
+ "operators-relations": "Operators and relations",
+ "base-grammar": "Base grammar",
+ "stored-relations": "Stored relations",
+};
+
+export default meta;
diff --git a/apps/website/content/roam/fundamentals/grammar/base-grammar.md b/apps/website/content/roam/fundamentals/grammar/base-grammar.md
new file mode 100644
index 000000000..f6f5ea289
--- /dev/null
+++ b/apps/website/content/roam/fundamentals/grammar/base-grammar.md
@@ -0,0 +1,25 @@
+---
+title: "Base grammar"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+This is what ships with the extension.
+
+## Nodes
+
+- QUE - Question
+- CLM - Claim
+- EVD - Evidence
+- Source
+
+## Relations
+
+- EVD Informs QUE
+- EVD Supports CLM
+- EVD Opposes CLM
+
+Motivation for this base grammar is described in this [article](https://oasislab.pubpub.org/pub/54t0y9mk/release/3).
+
+This base grammar may be most useful for projects that interact with empirical evidence where you want to clearly distinguish between theory and evidence, and retain provenance to the source citations if you want to get more context.
diff --git a/apps/website/content/roam/fundamentals/grammar/index.md b/apps/website/content/roam/fundamentals/grammar/index.md
new file mode 100644
index 000000000..3008ef0ac
--- /dev/null
+++ b/apps/website/content/roam/fundamentals/grammar/index.md
@@ -0,0 +1,15 @@
+---
+title: "Extension grammar"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+- [Nodes](/docs/roam/fundamentals/grammar/nodes)
+- [Operators and relations](/docs/roam/fundamentals/grammar/operators-relations)
+- [Stored relations](/docs/roam/fundamentals/grammar/stored-relations)
+- [Base grammar](/docs/roam/fundamentals/grammar/base-grammar)
+
+## Relation grammar building blocks
+
+- [Nodes](/docs/roam/fundamentals/grammar/nodes)
diff --git a/apps/website/content/roam/fundamentals/grammar/nodes.md b/apps/website/content/roam/fundamentals/grammar/nodes.md
new file mode 100644
index 000000000..8483e572e
--- /dev/null
+++ b/apps/website/content/roam/fundamentals/grammar/nodes.md
@@ -0,0 +1,18 @@
+---
+title: "Nodes"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+- `discourse node`
+ - as defined in your grammar (e.g., base grammar will include CLM or EVD, for example)
+
+- `block`
+- `page`
+
+It is not possible to directly specify that a source or target node in a relation pattern is a `block` or `page`. These are variables that are defined implicitly at the moment, by what incoming and/or outgoing relations it connects to.
+
+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!
diff --git a/apps/website/content/roam/fundamentals/grammar/operators-relations.md b/apps/website/content/roam/fundamentals/grammar/operators-relations.md
new file mode 100644
index 000000000..c738ef5e6
--- /dev/null
+++ b/apps/website/content/roam/fundamentals/grammar/operators-relations.md
@@ -0,0 +1,66 @@
+---
+title: "Operators and relations"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+## Roam-native
+
+### `references`
+
+- **description**: a block references some page. NOTE: you'll need to then chain that with something like `has title` or `with text` to identify the page.
+- **source**: a `block`
+- **target**: a `page`
+- example:
+ - this block references \[\[some page\]\]
+
+### `is in page`
+
+- **description**: source is in some page. NOTE: you'll need to then chain that with something like `has title` or `with text` to identify the page.
+- **source**: a `block`
+- **target**: a `page`
+
+### `has ancestor`
+
+- **description**: a block has some ancestor (i.e., the block is in the indentation path of some target, whether directly or indirectly)
+- **source**: a `block`
+- **target**: a `block` or `page`
+
+### `has child`
+
+- **description**: a block or page has some direct child (directly indented underneath)
+- **source**: a `block` or `page`
+- **target**: a `block`
+
+### `has descendant`
+
+- **description**: a block has some descendant (i.e., the target block is in the indentation path of the source, whether directly or indirectly). NOTE: you'll need to then chain that with something like `has title` or `with text` to identify the block.
+- **source**: a `block` or `page`
+- **target**: a `block`
+
+### `has title`
+
+- **description**: source text exactly matches some text
+- **source**: a `page`, `block`, or `discourse node`
+- **target**: a `string` that specifies the target title to match
+
+### `with text`
+
+- **description**: node content contains some text
+- **source:** a `page`, `block`, or `discourse node`
+- **target**: a `string` that specifies the target text to find in the node content
+
+### `has attribute`
+
+- **description**: has a child block with some attribute
+- **source**: a `page` or `discourse node`
+- **target**: a `string` that specifies the target attribute to be matched
+
+## Discourse-graph only
+
+### `is a`
+
+- **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)
diff --git a/apps/website/content/roam/fundamentals/grammar/stored-relations.md b/apps/website/content/roam/fundamentals/grammar/stored-relations.md
new file mode 100644
index 000000000..2fb28fc52
--- /dev/null
+++ b/apps/website/content/roam/fundamentals/grammar/stored-relations.md
@@ -0,0 +1,76 @@
+---
+title: "Stored relations"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+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?
+
+A stored relation connects two discourse nodes with a named relation type, such as a claim supporting another claim or a source informing a question.
+
+Stored relations are:
+
+- Created directly in the [**Discourse context overlay**](/docs/roam/guides/exploring-discourse-graph/discourse-context-overlay)
+- Persisted in your graph as structured data
+- Available anywhere Discourse Graph reads relationships
+
+## Why use stored relations?
+
+Stored relations give you a more direct and predictable way to work with relationships in your graph.
+
+- **Fast to read**
+ - Relations are loaded directly from stored data
+ - The Discourse context overlay can resolve them quickly
+- **Stable over time**
+ - Relations do not depend on formatting or page structure
+ - Editing nearby text does not change the relation itself
+- **Easy to understand**
+ - A relation exists because you created it
+ - Deleting a relation removes it explicitly
+
+## Creating stored relations
+
+Typical flow:
+
+1. Open the Discourse context overlay for a node.
+2. Click **Add relation**.
+3. Select the relation type.
+4. Select the source and destination nodes.
+5. Save the relation.
+
+The relation is stored immediately.
+
+## Viewing stored relations
+
+Stored relations appear throughout Discourse Graph:
+
+- The [**Discourse context overlay**](/docs/roam/guides/exploring-discourse-graph/discourse-context-overlay) shows relations for the current node
+- Queries can resolve relations from stored data
+- Canvases can use the same stored relationships
+
+## Editing and deleting stored relations
+
+- Stored relations can be deleted directly from the Discourse context overlay
+- If you want to change a relation, the usual flow is to delete the old one and create the new one
+- Relation labels are referenced by node identity, so renaming a relation label updates the associated relations automatically
+
+## Multi-user behavior
+
+Stored relations are shared across the graph.
+
+- Teammates working in the same graph see the same stored relations
+- Changes to stored relations are available to other users in that graph
+
+## For the technically inclined
+
+Stored relations are implemented as:
+
+- One block per relation
+- Located at: `roam/js/discourse-graph/relations`
+- Relation data stored in the block's hidden properties
+- Source and destination nodes referenced by UID
+
+This structure allows fast lookup, consistent rendering, and future extensions such as metadata, provenance, and annotations.
diff --git a/apps/website/content/roam/fundamentals/relations-patterns.md b/apps/website/content/roam/fundamentals/relations-patterns.md
new file mode 100644
index 000000000..00188bf2a
--- /dev/null
+++ b/apps/website/content/roam/fundamentals/relations-patterns.md
@@ -0,0 +1,53 @@
+---
+title: "Relations and patterns"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+Pattern-based relations are a **legacy workflow**. New graphs use [**stored relations**](/docs/roam/fundamentals/grammar/stored-relations) by default. If you're still using patterns or you're not sure which system your graph is on, see [**Migration to stored relations**](/docs/roam/guides/migration-to-stored-relations).
+
+The legacy pattern workflow recognizes a few common writing and outlining structures and converts them into discourse relations.
+
+- [Informed](#question-informed-by-evidence)
+- [Supported](#claim-supported-by-evidence)
+- [Opposed](#claim-opposed-by-evidence)
+
+## Question Informed by Evidence
+
+- Go into a Question page.
+- Create a block and reference an evidence page.
+
+Like this:
+
+
+
+The system recognizes that this piece of evidence **informs** the question.
+
+## Claim Supported by Evidence
+
+Create a block anywhere and reference a claim page. We'll call this the claim block.
+
+Indent a block underneath the claim block and reference the page `[[SupportedBy]]`. We'll call this the connecting block.
+
+Indent a block underneath the connecting block and reference an evidence page.
+
+Like this:
+
+
+
+The system recognizes that this piece of evidence **supports** that claim.
+
+## Claim Opposed by Evidence
+
+Create a block anywhere and reference a claim page. We'll call this the claim block.
+
+Indent a block underneath the claim block and reference the page `[[OpposedBy]]`. We'll call this the connecting block.
+
+Indent a block underneath the connecting block and reference an evidence page.
+
+Like this:
+
+
+
+The system recognizes that this piece of evidence **opposes** that claim.
diff --git a/apps/website/content/roam/fundamentals/what-is-a-discourse-graph.md b/apps/website/content/roam/fundamentals/what-is-a-discourse-graph.md
new file mode 100644
index 000000000..ebde4ad4e
--- /dev/null
+++ b/apps/website/content/roam/fundamentals/what-is-a-discourse-graph.md
@@ -0,0 +1,20 @@
+---
+title: "What is a discourse graph"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+**Discourse Graphs** are an information model for bodies of knowledge that emphasize discourse moves (such as questions, claims, and evidence), and relations (such as support or opposition), rather than papers or sources as the main units.
+
+To give an intuition for what it is, here is a figure of a visual representation of a simple Discourse Graph for a body of literature on bans and antisocial behavior in online forums. You may recognize similarities to things like argument maps.
+
+
+
+Consider how that information model foregrounds the conceptual building blocks and relationships that are important for synthesis, compared to a typical "[iTunes for papers](http://joelchan.me/assets/pdf/2019-cscw-beyond-itunes-for-papers.pdf)" model that foregrounds documents in a way that forces us to drudge through [tedious extraction work](https://dl.acm.org/doi/abs/10.1145/3295750.3298937) before we can do the thinking we want to do!
+
+
+
+This information model (theoretically!) has high potential for augmenting individual and collective "research-grade" synthesis (e.g., lit reviews for a dissertation or grant proposal).
+
+Discourse Graphs are not a new idea (you can read more about it in [this short (academic-focused) write-up](http://joelchan.me/assets/pdf/Discourse_Graphs_for_Augmented_Knowledge_Synthesis_What_and_Why.pdf) or this more [practically-oriented article](https://oasislab.pubpub.org/pub/54t0y9mk/release/3)), but the potential to use it for everyday research work is!
diff --git a/apps/website/content/roam/guides/_meta.ts b/apps/website/content/roam/guides/_meta.ts
new file mode 100644
index 000000000..071fab45c
--- /dev/null
+++ b/apps/website/content/roam/guides/_meta.ts
@@ -0,0 +1,14 @@
+import type { MetaRecord } from "nextra";
+
+const meta: MetaRecord = {
+ "creating-discourse-nodes": "Creating nodes",
+ "tagging-candidate-nodes": "Tagging candidate nodes",
+ "creating-discourse-relationships": "Creating relationships",
+ "migration-to-stored-relations": "Migration to stored relations",
+ "exploring-discourse-graph": "Exploring your discourse graph",
+ "querying-discourse-graph": "Querying",
+ "extending-personalizing-graph": "Extending",
+ "sharing-discourse-graph": "Sharing",
+};
+
+export default meta;
diff --git a/apps/website/content/roam/guides/creating-discourse-nodes.md b/apps/website/content/roam/guides/creating-discourse-nodes.md
new file mode 100644
index 000000000..d4f002001
--- /dev/null
+++ b/apps/website/content/roam/guides/creating-discourse-nodes.md
@@ -0,0 +1,24 @@
+---
+title: "Creating discourse nodes"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+The extension makes it easy to factor parts of your notes into formal nodes of a discourse graph (claims, evidence, etc.).
+
+Steps to create a discourse graph node:
+
+1. Select the text you want to turn into a formal discourse graph node
+2. Press the trigger hotkey (default is `\`) to open up the Node Menu
+3. Press the appropriate shortcut key (e.g., E for Evidence)
+
+The system will create a new page with the text as title, appropriate metadata, and template you have defined.
+
+## Demo
+
+https://www.loom.com/share/471fcf7dc13246439cb35feedb110470
+
+You can customize the template for specific nodes in the Settings Panel.
+
+
diff --git a/apps/website/content/roam/guides/creating-discourse-relationships.md b/apps/website/content/roam/guides/creating-discourse-relationships.md
new file mode 100644
index 000000000..c01d01ae2
--- /dev/null
+++ b/apps/website/content/roam/guides/creating-discourse-relationships.md
@@ -0,0 +1,26 @@
+---
+title: "Creating discourse relationships"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+Creating relationships in Discourse Graph means creating **stored relations**. Stored relations are the default way to connect discourse nodes.
+
+> Note: If you're still using pattern relations, or if you're not sure which system your graph is using, start with [**Migration to stored relations**](/docs/roam/guides/migration-to-stored-relations).
+
+## Quick start
+
+1. Open the [**Discourse context overlay**](/docs/roam/guides/exploring-discourse-graph/discourse-context-overlay) for a node.
+2. Click **Add relation**.
+3. Choose the relation type.
+4. Choose the source and destination nodes.
+5. Save the relation.
+
+The relation is stored immediately and becomes available throughout your graph.
+
+## Read next
+
+- Learn how stored relations work in [**Stored relations**](/docs/roam/fundamentals/grammar/stored-relations)
+- Explore relationships from the [**Discourse context overlay**](/docs/roam/guides/exploring-discourse-graph/discourse-context-overlay)
+- If you're still using the legacy workflow, see [**Relations and patterns**](/docs/roam/fundamentals/relations-patterns)
diff --git a/apps/website/content/roam/guides/exploring-discourse-graph/_meta.ts b/apps/website/content/roam/guides/exploring-discourse-graph/_meta.ts
new file mode 100644
index 000000000..e2f57a9f4
--- /dev/null
+++ b/apps/website/content/roam/guides/exploring-discourse-graph/_meta.ts
@@ -0,0 +1,14 @@
+import type { MetaRecord } from "nextra";
+
+const meta: MetaRecord = {
+ index: {
+ title: "Overview",
+ display: "hidden",
+ },
+ "discourse-context": "Discourse context",
+ "discourse-context-overlay": "Discourse context overlay",
+ "discourse-attributes": "Discourse attributes",
+ "node-index": "Node index",
+};
+
+export default meta;
diff --git a/apps/website/content/roam/guides/exploring-discourse-graph/discourse-attributes.md b/apps/website/content/roam/guides/exploring-discourse-graph/discourse-attributes.md
new file mode 100644
index 000000000..cdb6329eb
--- /dev/null
+++ b/apps/website/content/roam/guides/exploring-discourse-graph/discourse-attributes.md
@@ -0,0 +1,65 @@
+---
+title: "Discourse attributes"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+- [Discourse context](/docs/roam/guides/exploring-discourse-graph/discourse-context)
+- [Discourse context overlay](/docs/roam/guides/exploring-discourse-graph/discourse-context-overlay)
+- [Discourse attributes](/docs/roam/guides/exploring-discourse-graph/discourse-attributes)
+- [Node index](/docs/roam/guides/exploring-discourse-graph/node-index)
+
+## Define Discourse Attributes
+
+You can define discourse attributes that for each discourse node, which compute a numerical "score" for each instance of the node based on its discourse relations to other nodes. In the extension, we call these **discourse attributes**.
+
+These attributes can be handy for sorting/querying nodes. For instance, if you create a discourse attribute for Claim nodes that is a function of the number of Evidence nodes that Support the Claim, like this:
+
+
+
+You can add discourse attributes as a column to display and sort/filter by when [Querying your discourse graph](/docs/roam/guides/querying-discourse-graph) by adding a `discourse:{label}` selection.
+
+For example, in the index for Claims, you can return the Evidence attribute as a column (Select), and then sort in descending order by that attribute.
+
+## Basic Discourse Relation Functions
+
+A discourse attribute consists of one or more **discourse functions**, joined by one or more math operations. You can think of the discourse functions as variables that get their value from some discourse relations the node participates in.
+
+Here is the template for each discourse function: `{count:relationName:targetType}`
+
+- `count` is the operation. Atm, this is the only supported operation for basic discourse functions. We also have experimental discourse functions that operate over the discourse attributes of related nodes (see below), which allow for other operations such as `sum` and `average`
+- `relationName` is the name of the relation you want to use for the function, such as `Supported By` or `Informed By`
+- `targetType` is the name of the type of target of the relation you want to use for the function (since nodes can have relationships of the same name with multiple other nodes, such as `Supported By:Claim` or `Supported By:Evidence`)
+
+Here are some examples:
+
+- `{count:Supported By:Evidence}`
+- `{count:Informed By:Source}`
+- `{count:Opposed By:Claim}`
+
+You can use basic math operations to combine multiple discourse functions. For example, you might want to combine information across the supporting and opposing relationships to gauge how "robust" a Claim is, and give different weights to support from evidence vs. claims. You could express it like this:
+
+`{count:Supported By:Evidence} + {count:Supported By:Claim}*0.5 - {count:Opposed By:Evidence} - {count:Opposed By:Claim}*0.5`
+
+This function sums up the number of supporting relations and subtracts the number of opposing relations, but gives only half weight (`*0.5`) to supporting/opposing relations from Claims.
+
+## Compound Discourse Functions
+
+We have an experimental feature that allows us to access discourse attributes from related nodes to compute a discourse attribute. This allows us to experiment with more sophisticated ways to reason over our discourse nodes.
+
+For example, if a Claim that only gets direct support from other Claims (e.g., because it is quite general), we might care to distinguish if its supporting Claims are themselves also supported by Evidence.
+
+If each Claim node has a discourse attribute called Evidence that looks like this:
+
+`{count:Supported By:Evidence} - {count:Opposed By:Evidence}`
+
+We can define a compound discourse function that _averages_ over the Evidence attribute of Claims that support the Claim. Like this:
+
+`{average:Supported By:Claim:Evidence}`
+
+The syntax for these compound discourse functions is:
+
+`{operation:relationName:targetType:targetDiscourseAttribute}`
+
+This generalizes the syntax for the basic discourse functions by adding a discourse attribute to access from the targets, and the option of using additional operations than `count` (for now, we only support `sum` and `average`) for the function.
diff --git a/apps/website/content/roam/guides/exploring-discourse-graph/discourse-context-overlay.md b/apps/website/content/roam/guides/exploring-discourse-graph/discourse-context-overlay.md
new file mode 100644
index 000000000..4e3bf44fb
--- /dev/null
+++ b/apps/website/content/roam/guides/exploring-discourse-graph/discourse-context-overlay.md
@@ -0,0 +1,42 @@
+---
+title: "Discourse context overlay"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+- [Discourse context](/docs/roam/guides/exploring-discourse-graph/discourse-context)
+- [Discourse context overlay](/docs/roam/guides/exploring-discourse-graph/discourse-context-overlay)
+- [Discourse attributes](/docs/roam/guides/exploring-discourse-graph/discourse-attributes)
+- [Node index](/docs/roam/guides/exploring-discourse-graph/node-index)
+
+The discourse context overlay adds an icon next to each discourse node wherever it is referenced. This icon provides access to two key features:
+
+1. a popover to view the [Discourse context](/docs/roam/guides/exploring-discourse-graph/discourse-context) of the node inline, and
+2. a "Discourse score" component that displays a customizable score for the node that is some function of its relations to other nodes in the discourse graph.
+
+The overlay is an optional feature that is turned off by default. To turn it on, go to the grammar tab in the config page and check the box for overlay.
+
+
+
+## Popover
+
+The popover is simple to operate. Simply click on the icon to bring up the [discourse context](/docs/roam/guides/exploring-discourse-graph/discourse-context) component in-line for quick reference.
+
+
+
+## Discourse score
+
+By default, the discourse score displays the count of the total number of discourse relations for that node (the number on the left), as well as a count of the total number of references to that node in the graph.
+
+For example, the following score display denotes that the node participates in 5 discourse relations with other nodes, which can be seen in detail in the discourse context popover (1 informs, 1 supports, 2 consistent with, and 1 sourced by; note that 2 of these relations are [custom relations](/docs/roam/guides/extending-personalizing-graph)).
+
+
+
+You can think of this default discourse score as a rough sense of how "robust" a given node is (discourse relations are more structured, high-signal relationships) relative to how "important" it is (raw references are often noisy). If you'd like, you can also experiment with different functions of discourse relations to a node by defining your own [Discourse attributes](/docs/roam/guides/exploring-discourse-graph/discourse-attributes), and have them show up in the overlay!
+
+## Demo
+
+(old video demo)
+
+https://www.loom.com/share/b3d6094cd14a466081b8aa8495eb6542
diff --git a/apps/website/content/roam/guides/exploring-discourse-graph/discourse-context.md b/apps/website/content/roam/guides/exploring-discourse-graph/discourse-context.md
new file mode 100644
index 000000000..e97ed9b04
--- /dev/null
+++ b/apps/website/content/roam/guides/exploring-discourse-graph/discourse-context.md
@@ -0,0 +1,25 @@
+---
+title: "Discourse context"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+- [Discourse context](/docs/roam/guides/exploring-discourse-graph/discourse-context)
+- [Discourse context overlay](/docs/roam/guides/exploring-discourse-graph/discourse-context-overlay)
+- [Discourse attributes](/docs/roam/guides/exploring-discourse-graph/discourse-attributes)
+- [Node index](/docs/roam/guides/exploring-discourse-graph/node-index)
+
+The discourse context component adds a "higher-signal" linked references section to each discourse node, that allows you to explore _discourse_ relations (e.g., inform, support, etc.) between this node and other nodes in your discourse graph.
+
+## Group by target node
+
+
+
+## Filter results
+
+
+
+## Demo
+
+https://www.loom.com/share/0c66e95d0c71426e8090a8bc1cbf8544
diff --git a/apps/website/content/roam/guides/exploring-discourse-graph/index.md b/apps/website/content/roam/guides/exploring-discourse-graph/index.md
new file mode 100644
index 000000000..e627aaa4b
--- /dev/null
+++ b/apps/website/content/roam/guides/exploring-discourse-graph/index.md
@@ -0,0 +1,13 @@
+---
+title: "Exploring Your Discourse Graph"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+The extension adds features to enable you to seamlessly explore your discourse graph to enhance your thinking.
+
+- [Discourse context](/docs/roam/guides/exploring-discourse-graph/discourse-context)
+- [Discourse context overlay](/docs/roam/guides/exploring-discourse-graph/discourse-context-overlay)
+- [Discourse attributes](/docs/roam/guides/exploring-discourse-graph/discourse-attributes)
+- [Node index](/docs/roam/guides/exploring-discourse-graph/node-index)
diff --git a/apps/website/content/roam/guides/exploring-discourse-graph/node-index.md b/apps/website/content/roam/guides/exploring-discourse-graph/node-index.md
new file mode 100644
index 000000000..ede969fc1
--- /dev/null
+++ b/apps/website/content/roam/guides/exploring-discourse-graph/node-index.md
@@ -0,0 +1,17 @@
+---
+title: "Node index"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+- [Discourse context](/docs/roam/guides/exploring-discourse-graph/discourse-context)
+- [Discourse context overlay](/docs/roam/guides/exploring-discourse-graph/discourse-context-overlay)
+- [Discourse attributes](/docs/roam/guides/exploring-discourse-graph/discourse-attributes)
+- [Node index](/docs/roam/guides/exploring-discourse-graph/node-index)
+
+The extension has node settings tabs for each node you define, which provides a query over all of your nodes of that type. For example, you can go to the Claim tab to see all the Claim nodes in your graph.
+
+
+
+This can be quite handy in multiplayer settings, for example, to quickly view the latest question nodes that have been added to the graph, or who authored them, by modifying the default index query, just like a [regular query](/docs/roam/guides/querying-discourse-graph).
diff --git a/apps/website/content/roam/guides/extending-personalizing-graph.md b/apps/website/content/roam/guides/extending-personalizing-graph.md
new file mode 100644
index 000000000..e4cf2277d
--- /dev/null
+++ b/apps/website/content/roam/guides/extending-personalizing-graph.md
@@ -0,0 +1,34 @@
+---
+title: "Extending and personalizing your Discourse Graph"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+This page covers ways to extend and personalize Discourse Graph beyond the default setup.
+
+## Stored relations
+
+If you want to use the current relation workflow, start with the
+[Stored relations](/docs/roam/fundamentals/grammar/stored-relations) guide.
+
+## Pattern relation legacy
+
+Discourse Graph used to rely much more heavily on query-based pattern relations. The videos below are older examples of that legacy workflow.
+
+Note:
+
+- They use the legacy query-based pattern-relations workflow
+- They show an older UI that does not match the current product
+
+Some users extended the grammar quite extensively. For example, here is a video from a user who extended the grammar to cover structured international law research:
+
+[https://www.youtube.com/watch?v=rST7cKMO_Ds](https://www.youtube.com/watch?v=rST7cKMO_Ds)
+
+Others also extended the grammar to integrate results from their own experiments and contextualize each new result against earlier research.
+
+For example, here is a demo of creating a "consistent with" relation pattern that recognizes when two pieces of evidence both support the same thing:
+
+[https://www.loom.com/share/cb9e526a98764e95a459a6db2b66e46a](https://www.loom.com/share/cb9e526a98764e95a459a6db2b66e46a)
+
+If you want to explore or understand that legacy workflow, it will be useful to [learn more about the extension grammar](/docs/roam/fundamentals/grammar).
diff --git a/apps/website/content/roam/guides/migration-to-stored-relations.md b/apps/website/content/roam/guides/migration-to-stored-relations.md
new file mode 100644
index 000000000..282ea9344
--- /dev/null
+++ b/apps/website/content/roam/guides/migration-to-stored-relations.md
@@ -0,0 +1,88 @@
+---
+title: "Migration to stored relations"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+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](/docs/roam/fundamentals/grammar/stored-relations).
+
+## Turn on stored relations for an existing install
+
+1. Open **Personal Settings**
+2. Go to the **Home** panel
+3. Find **Enable stored relations**
+4. Turn the toggle **On**
+
+When you turn this on for an older install, you'll be shown a migration step for your graph.
+
+## Migrate your existing relations
+
+After enabling the toggle, click:
+
+`Migrate all relations`
+
+What to expect:
+
+- It may take a few minutes
+- Roam may feel frozen while it runs (this is expected)
+- You **can** work in **another Roam window** during the migration
+- When it finishes, you can continue working normally
+
+## When should I run migration again?
+
+You only need to run migration again if:
+
+- Relations were created while stored relations were turned off
+- Your team is transitioning gradually and some people are still using the older relation method
+
+## Multi-user graphs
+
+Migration runs **per graph**, but the stored relations toggle is **per user**. During rollout, it's possible for teammates to be in different modes.
+
+If some users are still using the older relation method, running migration again later helps ensure you see the most up-to-date relations.
+
+### What each user sees
+
+#### If you do NOT opt in
+
+- You can keep using **patterns** to create relations
+- The overlay will search **only pattern-based** relations
+- You **will not** see stored relations
+
+#### If you DO opt in
+
+- You will see **only stored** relations
+- You **will not** see pattern-based relations
+
+### Re-running migration (multi-user / mixed mode)
+
+You can run migration **multiple times**.
+
+This matters if:
+
+- Some users are still creating **pattern-based** relations
+- Other users are already using **stored** relations
+
+In that case, stored-relations users must re-run migration to "catch up," otherwise:
+
+- They won't see new pattern-based relations created since the last migration
+
+#### Important edge case
+
+If you:
+
+1. Delete a **stored** relation that originally came from a pattern, and then
+2. Re-run migration
+
+... it will be **re-created** from the still-existing pattern.
+
+### For the technically inclined
+
+Stored relations are implemented as:
+
+- `roam/js/discourse-graph/relations`
+
+Each relation is a **block**, with relation data stored in the block's **hidden properties**.
diff --git a/apps/website/content/roam/guides/querying-discourse-graph.md b/apps/website/content/roam/guides/querying-discourse-graph.md
new file mode 100644
index 000000000..04baa1d5c
--- /dev/null
+++ b/apps/website/content/roam/guides/querying-discourse-graph.md
@@ -0,0 +1,66 @@
+---
+title: "Querying Your Discourse Graph"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+## Query Drawer
+
+The query drawer component allows you to construct structured queries over your discourse graph, based on discourse relations (e.g., "find all evidence that supports/opposes a claim"), and reason over the results in a structured, tabular format (e.g., "find all evidence that supports a claim, and allow me to filter/sort by methodological details").
+
+### Making a query
+
+Use Command Palette (⌘+`P` on Mac,`CTRL`\+ `P` otherwise) to access the query drawer.
+
+
+
+A query drawer component will open from the left. From there, you can construct structured queries of your discourse graph and explore their results in a tabular format.
+
+
+
+## Common Query Examples
+
+### Evidence that Informs a Question
+
+
+
+### Evidence that Supports a Claim
+
+
+
+## Advanced Query Examples
+
+### Mix discourse and Roam queries
+
+Example: find all evidence that informs a question, but only if it was collected in a specific location (this example assumes at least some evidence pages have an attribute `Location::` in them)
+
+
+
+### Select node attributes to display as attributes of results
+
+Example: find all evidence that informs a question, and select a methods attribute to display so we can sort/filter on it (this example assumes at least some evidence pages have an attribute `testingRegime::` in them)
+
+
+
+### Select discourse attributes to display as attributes of results
+
+If you have defined [Discourse Attributes](/docs/roam/guides/exploring-discourse-graph/discourse-attributes) for the node you want to query, you can select it as a column in your query. The syntax for accessing a node's discourse attribute as a select is`discourse:discourseAttributeName`.
+
+Example: find all claims and display their "Evidence" discourse attributes (number of supporting evidence relations) as a column.
+
+
+
+## Naming a query
+
+You can name a query if you like!
+
+
+
+## Saving a query to its own page
+
+You can save a query to its own page if you want to keep it around for easier access.
+
+
+
+It will be saved to the namespace `discourse-graph/queries/` by default.
diff --git a/apps/website/content/roam/guides/sharing-discourse-graph.md b/apps/website/content/roam/guides/sharing-discourse-graph.md
new file mode 100644
index 000000000..be5d44b17
--- /dev/null
+++ b/apps/website/content/roam/guides/sharing-discourse-graph.md
@@ -0,0 +1,60 @@
+---
+title: "Sharing Your Discourse Graph"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+You can export your discourse graph as a whole, or from queries of your graph, to archive your most important notes, or share them with others.
+
+The extension exports to a number of formats, select an export option to see the options available.
+
+
+
+Demo:
+
+[https://www.loom.com/share/ca222cb93efb4ed890b4c9e91f05db52](https://www.loom.com/share/ca222cb93efb4ed890b4c9e91f05db52)
+
+## Export Options
+
+We have a range of options for customizing the markdown export. These can be found on the `Export` tab of the discourse graph configuration.
+
+
+
+Here is a brief explanation of each option:
+
+`Max filename length`
+
+- sets the maximum length of the filenames; this is important if you have page names that are quite long and may run afoul of, say, Windows' filename length limit of 250-260 characters.
+
+`Remove special characters`
+
+- removes all "special characters" that may lead to trouble for filenames in different operating systems, such as `?` (not allowed on Windows) or `/` (denotes file/folder boundaries).
+
+`Simplified filename`
+
+- strips away all "template" characters (i.e., everything except the `{content}` in the node format: for example, if you define a Claim node as `[[CLM]] - {content}`, and have a Claim node `[[[[CLM]] - people are lazy]]`, the exported filename will be `people are lazy`
+
+`Frontmatter`
+
+- specifies what properties to add to the YAML.
+
+- By default, the properties are:
+ - `title: {text}`
+ - `author: {author}`
+ - `date: {date}`
+
+- You can add properties as key-value pairs in the same format:
+ - 
+
+`Resolve block references` and `Resolve block embeds`
+
+- control whether you want to resolve block references/embeds in your export. You can keep this turned off if you are unsure of the privacy implications of references/embeds.
+
+`Link type`
+
+- controls whether inline page references are wikilinks (`[[like this]]`) or alias (`[like this](pageName.md)`)
+
+## Example
+
+Here is an example of a discourse graph exported to Obsidian-compatible markdown: [https://publish.obsidian.md/joelchan-notes](https://publish.obsidian.md/joelchan-notes)
diff --git a/apps/website/content/roam/guides/tagging-candidate-nodes.md b/apps/website/content/roam/guides/tagging-candidate-nodes.md
new file mode 100644
index 000000000..5259c67fc
--- /dev/null
+++ b/apps/website/content/roam/guides/tagging-candidate-nodes.md
@@ -0,0 +1,41 @@
+---
+title: "Tagging candidate nodes"
+date: "2025-09-16"
+author: ""
+published: true
+---
+
+## Purpose - quickly bookmark notes that could later become formal discourse nodes
+
+While you're taking notes, often you'll write down something that could later become a formal discourse node like a Claim, Evidence, or Result. But sometimes you don't want to break your flow to formalize it in the moment, or you may want to clarify/revisit it later to be sure that you want to turn it into a Claim/Evidence/Result.
+It's helpful to quickly tag these. Later, you can search for them and formalize them in one click.
+
+## Setup - define candidate nodes
+
+In the discourse graphs settings, for each node type, list a "candidate node" tag label:
+
+
+
+⚠️ _Important: A tag cannot use text that is already part of its associated node's format. For example, if the "Claim" node format is CLM, you cannot use #CLM as its tag. You'll get an error message if you attempt to do this._
+
+## Tag candidate nodes
+
+Tag candidate nodes with the keyboard shortcut: `\`
+
+
+
+Click or use the down arrow key to select a candidate node type.
+
+💡 _If you've customized the keyboard shortcut for creating nodes, this keyboard shortcut will change too_
+
+## Formalize candidate nodes
+
+Hover your mouse over the node tag, and click "Create Evidence"
+
+
+
+
+
+## Candidate node styling
+
+Candidate node styling is currently controlled in roam/css. You can borrow our node styling here: [template-lab CSS page](https://roamresearch.com/#/app/template-lab/page/X8V4gy32s)
diff --git a/apps/website/content/roam/index.mdx b/apps/website/content/roam/index.mdx
new file mode 100644
index 000000000..3aa0309ee
--- /dev/null
+++ b/apps/website/content/roam/index.mdx
@@ -0,0 +1,38 @@
+---
+title: Roam documentation
+description: Learn how to install, configure, and use the Discourse Graph extension for Roam.
+---
+
+import { Callout, Cards } from "nextra/components";
+
+# Roam documentation
+
+Use these docs to install the Discourse Graph extension for Roam, learn the core workflows, and understand the underlying graph model.
+
+
+ New to the extension? Start with Getting started and
+ Installation, then move into the guides.
+
+
+
+
+
+
+
+
diff --git a/apps/website/content/roam/use-cases/_meta.ts b/apps/website/content/roam/use-cases/_meta.ts
new file mode 100644
index 000000000..18e66e72c
--- /dev/null
+++ b/apps/website/content/roam/use-cases/_meta.ts
@@ -0,0 +1,11 @@
+import type { MetaRecord } from "nextra";
+
+const meta: MetaRecord = {
+ "literature-reviewing": "Literature reviewing",
+ "enhanced-zettelkasten": "Zettelkasten",
+ "reading-clubs": "Reading clubs and seminars",
+ "lab-notebooks": "Lab notebooks",
+ "research-roadmapping": "Product and research roadmapping",
+};
+
+export default meta;
diff --git a/apps/website/content/roam/use-cases/enhanced-zettelkasten.md b/apps/website/content/roam/use-cases/enhanced-zettelkasten.md
new file mode 100644
index 000000000..36ba3e2c9
--- /dev/null
+++ b/apps/website/content/roam/use-cases/enhanced-zettelkasten.md
@@ -0,0 +1,12 @@
+---
+title: "Enhanced Zettelkasten"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+Maarten Van Doorn has integrated the discourse graph into his take on applying the zettelkasten to his research.
+
+Here is a video overview:
+
+[https://www.youtube.com/watch?v=mNzUAICf4Rk](https://www.youtube.com/watch?v=mNzUAICf4Rk)
diff --git a/apps/website/content/roam/use-cases/lab-notebooks.md b/apps/website/content/roam/use-cases/lab-notebooks.md
new file mode 100644
index 000000000..9bdfa548c
--- /dev/null
+++ b/apps/website/content/roam/use-cases/lab-notebooks.md
@@ -0,0 +1,8 @@
+---
+title: "Lab notebooks"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+Description coming soon!
diff --git a/apps/website/content/roam/use-cases/literature-reviewing.md b/apps/website/content/roam/use-cases/literature-reviewing.md
new file mode 100644
index 000000000..3aded41b9
--- /dev/null
+++ b/apps/website/content/roam/use-cases/literature-reviewing.md
@@ -0,0 +1,14 @@
+---
+title: "Literature reviewing"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+This is currently the most common use case.
+
+Lukas Kawerau (aka Cortex Futura) has a course that integrates the extension into a complete system for academic literature reviewing and writing: [https://learn.cortexfutura.com/p/cite-to-write-v2](https://learn.cortexfutura.com/p/cite-to-write-v2)
+
+Lukas gives a short overview of how in this tweet thread:
+
+[https://x.com/cortexfutura/status/1441795897680011276](https://x.com/cortexfutura/status/1441795897680011276)
diff --git a/apps/website/content/roam/use-cases/reading-clubs.md b/apps/website/content/roam/use-cases/reading-clubs.md
new file mode 100644
index 000000000..51c94d6e5
--- /dev/null
+++ b/apps/website/content/roam/use-cases/reading-clubs.md
@@ -0,0 +1,10 @@
+---
+title: "Reading clubs"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+Description coming soon!
+
+[Get in touch](mailto:joechan@umd.edu) if you're interested in joining a Reading Club!
diff --git a/apps/website/content/roam/use-cases/research-roadmapping.md b/apps/website/content/roam/use-cases/research-roadmapping.md
new file mode 100644
index 000000000..01d12fc3d
--- /dev/null
+++ b/apps/website/content/roam/use-cases/research-roadmapping.md
@@ -0,0 +1,8 @@
+---
+title: "Research roadmapping"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+Description coming soon!
diff --git a/apps/website/content/roam/welcome/_meta.ts b/apps/website/content/roam/welcome/_meta.ts
new file mode 100644
index 000000000..ac6fd187f
--- /dev/null
+++ b/apps/website/content/roam/welcome/_meta.ts
@@ -0,0 +1,8 @@
+import type { MetaRecord } from "nextra";
+
+const meta: MetaRecord = {
+ "getting-started": "Getting started",
+ installation: "Installation",
+};
+
+export default meta;
diff --git a/apps/website/content/roam/welcome/getting-started.md b/apps/website/content/roam/welcome/getting-started.md
new file mode 100644
index 000000000..cacd269c9
--- /dev/null
+++ b/apps/website/content/roam/welcome/getting-started.md
@@ -0,0 +1,32 @@
+---
+title: "Getting started"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+The Discourse Graph extension enables [Roam](https://roamresearch.com/) users to seamlessly add additional semantic structure to their notes, including specified page types and link types that [model scientific discourse](/docs/roam/fundamentals/what-is-a-discourse-graph), to enable more complex and structured [knowledge synthesis work](https://oasislab.pubpub.org/pub/54t0y9mk/release/3), such as a complex interdisciplinary literature review, and enhanced collaboration with others on this work.
+
+## Overview
+
+Here is a relatively brief walkthrough of some of the main features of the extension, including creating discourse nodes and relations, and [using](/docs/roam/guides/exploring-discourse-graph), [querying](/docs/roam/guides/querying-discourse-graph), and [sharing](/docs/roam/guides/sharing-discourse-graph) the discourse graph. This is done in the context of an actual ongoing literature review.
+
+[https://www.loom.com/share/2ec80422301c451b888b65ee1d283b40](https://www.loom.com/share/2ec80422301c451b888b65ee1d283b40)
+
+## Guides: Jump right in
+
+Follow our handy guides to get started on the basics as quickly as possible:
+
+- [Creating discourse nodes](/docs/roam/guides/creating-discourse-nodes)
+- [Creating discourse relationships](/docs/roam/guides/creating-discourse-relationships)
+- [Exploring your Discourse Graph](/docs/roam/guides/exploring-discourse-graph)
+- [Querying your Discourse Graph](/docs/roam/guides/querying-discourse-graph)
+- [Extending and Personalizing your Discourse Graph](/docs/roam/guides/extending-personalizing-graph)
+
+## Fundamentals: Dive a little deeper
+
+Learn the fundamentals of the Discourse Graph extension to get a deeper understanding of our main features:
+
+- [What is a Discourse Graph?](/docs/roam/fundamentals/what-is-a-discourse-graph)
+- [The Discourse Graph extension grammar](/docs/roam/fundamentals/grammar)
+- [The base grammar: questions, claims, and evidence](/docs/roam/fundamentals/grammar/base-grammar)
diff --git a/apps/website/content/roam/welcome/installation.md b/apps/website/content/roam/welcome/installation.md
new file mode 100644
index 000000000..41a481e33
--- /dev/null
+++ b/apps/website/content/roam/welcome/installation.md
@@ -0,0 +1,14 @@
+---
+title: "Installation"
+date: "2025-01-01"
+author: ""
+published: true
+---
+
+The Discourse Graphs extension available for installation via Roam Depot.
+
+Search for Discourse Graph in Roam Depot
+
+
+
+
diff --git a/apps/website/docsRouteMap.ts b/apps/website/docsRouteMap.ts
new file mode 100644
index 000000000..d16d8a425
--- /dev/null
+++ b/apps/website/docsRouteMap.ts
@@ -0,0 +1,143 @@
+export const ROAM_DOC_SECTIONS = {
+ welcome: ["getting-started", "installation", "installation-roam-depot"],
+ guides: [
+ "creating-discourse-nodes",
+ "tagging-candidate-nodes",
+ "creating-discourse-relationships",
+ "migration-to-stored-relations",
+ "exploring-discourse-graph",
+ "querying-discourse-graph",
+ "extending-personalizing-graph",
+ "sharing-discourse-graph",
+ ],
+ fundamentals: ["what-is-a-discourse-graph", "grammar", "relations-patterns"],
+ "use-cases": [
+ "literature-reviewing",
+ "enhanced-zettelkasten",
+ "reading-clubs",
+ "lab-notebooks",
+ "research-roadmapping",
+ ],
+} as const;
+
+export const OBSIDIAN_DOC_SECTIONS = {
+ welcome: ["getting-started", "installation"],
+ fundamentals: ["what-is-a-discourse-graph", "base-grammar"],
+ configuration: [
+ "node-types-templates",
+ "relationship-types",
+ "general-settings",
+ ],
+ "core-features": [
+ "creating-discourse-nodes",
+ "creating-discourse-relationships",
+ "discourse-context",
+ "canvas",
+ "node-tags",
+ ],
+ "advanced-features": ["command-palette", "sync-and-import"],
+ "use-cases": [
+ "literature-reviewing",
+ "research-roadmapping",
+ "reading-clubs",
+ "lab-notebooks",
+ ],
+} as const;
+
+type Redirect = {
+ source: string;
+ destination: string;
+ permanent: boolean;
+};
+
+const createRedirect = (source: string, destination: string): Redirect => ({
+ source,
+ destination,
+ permanent: true,
+});
+
+const buildPlatformRedirects = (
+ platform: "roam" | "obsidian",
+ sections: Record,
+): Redirect[] =>
+ Object.entries(sections).flatMap(([section, slugs]) =>
+ slugs.map((slug) => ({
+ source: `/docs/${platform}/${slug}`,
+ destination: `/docs/${platform}/${section}/${slug}`,
+ permanent: true,
+ })),
+ );
+
+const ROAM_CUSTOM_REDIRECTS: Redirect[] = [
+ createRedirect(
+ "/docs/roam/discourse-context",
+ "/docs/roam/guides/exploring-discourse-graph/discourse-context",
+ ),
+ createRedirect(
+ "/docs/roam/discourse-context-overlay",
+ "/docs/roam/guides/exploring-discourse-graph/discourse-context-overlay",
+ ),
+ createRedirect(
+ "/docs/roam/discourse-attributes",
+ "/docs/roam/guides/exploring-discourse-graph/discourse-attributes",
+ ),
+ createRedirect(
+ "/docs/roam/node-index",
+ "/docs/roam/guides/exploring-discourse-graph/node-index",
+ ),
+ createRedirect(
+ "/docs/roam/views-and-tools/discourse-context",
+ "/docs/roam/guides/exploring-discourse-graph/discourse-context",
+ ),
+ createRedirect(
+ "/docs/roam/views-and-tools/discourse-context-overlay",
+ "/docs/roam/guides/exploring-discourse-graph/discourse-context-overlay",
+ ),
+ createRedirect(
+ "/docs/roam/views-and-tools/discourse-attributes",
+ "/docs/roam/guides/exploring-discourse-graph/discourse-attributes",
+ ),
+ createRedirect(
+ "/docs/roam/views-and-tools/node-index",
+ "/docs/roam/guides/exploring-discourse-graph/node-index",
+ ),
+ createRedirect("/docs/roam/nodes", "/docs/roam/fundamentals/grammar/nodes"),
+ createRedirect(
+ "/docs/roam/operators-relations",
+ "/docs/roam/fundamentals/grammar/operators-relations",
+ ),
+ createRedirect(
+ "/docs/roam/base-grammar",
+ "/docs/roam/fundamentals/grammar/base-grammar",
+ ),
+ createRedirect(
+ "/docs/roam/stored-relations",
+ "/docs/roam/fundamentals/grammar/stored-relations",
+ ),
+ createRedirect(
+ "/docs/roam/fundamentals/nodes",
+ "/docs/roam/fundamentals/grammar/nodes",
+ ),
+ createRedirect(
+ "/docs/roam/fundamentals/operators-relations",
+ "/docs/roam/fundamentals/grammar/operators-relations",
+ ),
+ createRedirect(
+ "/docs/roam/fundamentals/base-grammar",
+ "/docs/roam/fundamentals/grammar/base-grammar",
+ ),
+ createRedirect(
+ "/docs/roam/fundamentals/stored-relations",
+ "/docs/roam/fundamentals/grammar/stored-relations",
+ ),
+ createRedirect(
+ "/docs/roam/fundamentals/migration-to-stored-relations",
+ "/docs/roam/guides/migration-to-stored-relations",
+ ),
+];
+
+export const DOCS_REDIRECTS: Redirect[] = [
+ ...ROAM_CUSTOM_REDIRECTS,
+ ...buildPlatformRedirects("roam", ROAM_DOC_SECTIONS),
+ ...buildPlatformRedirects("obsidian", OBSIDIAN_DOC_SECTIONS),
+];
diff --git a/apps/website/eslint.config.mjs b/apps/website/eslint.config.mjs
index f8db82476..9a7035ee9 100644
--- a/apps/website/eslint.config.mjs
+++ b/apps/website/eslint.config.mjs
@@ -15,6 +15,15 @@ export default [
},
},
{
- ignores: [".next/**"],
+ files: ["scripts/**/*.mjs"],
+ languageOptions: {
+ globals: {
+ console: "readonly",
+ process: "readonly",
+ },
+ },
+ },
+ {
+ ignores: [".next/**", "public/_pagefind/**"],
},
];
diff --git a/apps/website/next.config.ts b/apps/website/next.config.ts
index 498e5acea..167835927 100644
--- a/apps/website/next.config.ts
+++ b/apps/website/next.config.ts
@@ -1,16 +1,22 @@
import nextra from "nextra";
import type { NextConfig } from "next";
import { config } from "@repo/database/dbDotEnv";
+import { DOCS_REDIRECTS } from "./docsRouteMap";
config();
-const withNextra = nextra({});
+const withNextra = nextra({
+ contentDirBasePath: "/docs",
+});
const nextConfig: NextConfig = {
reactStrictMode: true,
serverRuntimeConfig: {
maxDuration: 300,
},
+ async redirects() {
+ return DOCS_REDIRECTS;
+ },
turbopack: {
resolveAlias: {
"next-mdx-import-source-file": "./mdx-components.tsx",
diff --git a/apps/website/package.json b/apps/website/package.json
index a55b05a0c..2de7cc75b 100644
--- a/apps/website/package.json
+++ b/apps/website/package.json
@@ -7,7 +7,7 @@
"scripts": {
"dev": "next dev",
"build": "next build --no-lint",
- "postbuild": "pagefind --site .next/server/app --output-path public/_pagefind",
+ "postbuild": "node ./scripts/build-docs-search-index.mjs",
"start": "next start",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
diff --git a/apps/website/public/logo-light-48.svg b/apps/website/public/logo-light-48.svg
new file mode 100644
index 000000000..693b77557
--- /dev/null
+++ b/apps/website/public/logo-light-48.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/apps/website/scripts/build-docs-search-index.mjs b/apps/website/scripts/build-docs-search-index.mjs
new file mode 100644
index 000000000..d956ef7db
--- /dev/null
+++ b/apps/website/scripts/build-docs-search-index.mjs
@@ -0,0 +1,239 @@
+import fs from "node:fs/promises";
+import path from "node:path";
+import { pathToFileURL } from "node:url";
+import matter from "gray-matter";
+import { createIndex, close } from "pagefind";
+
+/**
+ * @typedef {Record} SearchFilters
+ */
+
+/**
+ * @typedef {{
+ * url: string;
+ * content: string;
+ * meta: {
+ * title: string;
+ * };
+ * filters?: SearchFilters;
+ * }} SearchRecord
+ */
+
+const CONTENT_ROOT = path.join(process.cwd(), "content");
+const OUTPUT_PATH = path.join(process.cwd(), "public", "_pagefind");
+const DOCS_PREFIX = "/docs";
+const MARKDOWN_EXTENSIONS = new Set([".md", ".mdx"]);
+const DOC_DIRECTORIES = ["roam", "obsidian"];
+
+/**
+ * @param {string} value
+ * @returns {string}
+ */
+const collapseWhitespace = (value) => value.replace(/\s+/g, " ").trim();
+
+/**
+ * @param {string} source
+ * @returns {string}
+ */
+export const markdownToSearchText = (source) => {
+ const withoutImports = source.replace(/^\s*(import|export)\s.+$/gm, " ");
+ const withoutFences = withoutImports.replace(/```[\s\S]*?```/g, " ");
+ const withoutImages = withoutFences.replace(
+ /!\[([^\]]*)\]\([^)]+\)/g,
+ " $1 ",
+ );
+ const withoutLinks = withoutImages.replace(/\[([^\]]+)\]\([^)]+\)/g, " $1 ");
+ const withoutHtml = withoutLinks.replace(/<[^>]+>/g, " ");
+ const withoutInlineCode = withoutHtml.replace(/`([^`]+)`/g, " $1 ");
+ const withoutMarkdownTokens = withoutInlineCode
+ .replace(/^#{1,6}\s*/gm, " ")
+ .replace(/^\s*>+\s?/gm, " ")
+ .replace(/^\s*[-*+]\s+/gm, " ")
+ .replace(/^\s*\d+\.\s+/gm, " ")
+ .replace(/\|/g, " ")
+ .replace(/[*_~]/g, " ");
+
+ return collapseWhitespace(withoutMarkdownTokens);
+};
+
+/**
+ * @param {string} absoluteFilePath
+ * @returns {string}
+ */
+export const routePathFromContentFile = (absoluteFilePath) => {
+ const relativePath = path.relative(CONTENT_ROOT, absoluteFilePath);
+ const normalizedPath = relativePath.replace(/\\/g, "/");
+ const withoutExtension = normalizedPath.replace(/\.(md|mdx)$/u, "");
+ const segments = withoutExtension.split("/");
+
+ if (withoutExtension === "index") {
+ return DOCS_PREFIX;
+ }
+
+ if (segments.at(-1) === "index") {
+ segments.pop();
+ }
+
+ return `${DOCS_PREFIX}/${segments.join("/")}`;
+};
+
+/**
+ * @param {string} absoluteFilePath
+ * @returns {SearchFilters | undefined}
+ */
+export const searchFiltersFromContentFile = (absoluteFilePath) => {
+ const relativePath = path.relative(CONTENT_ROOT, absoluteFilePath);
+ const normalizedPath = relativePath.replace(/\\/g, "/");
+ const [topLevelDirectory] = normalizedPath.split("/");
+
+ if (DOC_DIRECTORIES.includes(topLevelDirectory)) {
+ return {
+ platform: [topLevelDirectory],
+ };
+ }
+
+ return undefined;
+};
+
+/**
+ * @param {string} absoluteFilePath
+ * @returns {string}
+ */
+const titleFromFilePath = (absoluteFilePath) =>
+ path
+ .basename(absoluteFilePath, path.extname(absoluteFilePath))
+ .split("-")
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
+ .join(" ");
+
+/**
+ * @param {string} absoluteFilePath
+ * @returns {boolean}
+ */
+const isMarkdownFile = (absoluteFilePath) =>
+ MARKDOWN_EXTENSIONS.has(path.extname(absoluteFilePath));
+
+/**
+ * @param {string} directory
+ * @returns {Promise}
+ */
+const collectMarkdownFiles = async (directory) => {
+ const entries = await fs.readdir(directory, { withFileTypes: true });
+ const nestedFiles = await Promise.all(
+ entries.map(async (entry) => {
+ const absolutePath = path.join(directory, entry.name);
+
+ if (entry.isDirectory()) {
+ return collectMarkdownFiles(absolutePath);
+ }
+
+ return isMarkdownFile(absolutePath) ? [absolutePath] : [];
+ }),
+ );
+
+ return nestedFiles.flat();
+};
+
+/**
+ * @param {string} absoluteFilePath
+ * @returns {Promise}
+ */
+const readDocRecord = async (absoluteFilePath) => {
+ const rawFile = await fs.readFile(absoluteFilePath, "utf8");
+ const { content, data } = matter(rawFile);
+
+ if (data.published === false) {
+ return null;
+ }
+
+ const title =
+ typeof data.title === "string" && data.title.trim().length
+ ? data.title.trim()
+ : titleFromFilePath(absoluteFilePath);
+ const searchText = markdownToSearchText(content);
+ const filters = searchFiltersFromContentFile(absoluteFilePath);
+
+ if (!searchText.length) {
+ return null;
+ }
+
+ return {
+ url: routePathFromContentFile(absoluteFilePath),
+ content: `${title}\n${searchText}`,
+ meta: {
+ title,
+ },
+ ...(filters ? { filters } : {}),
+ };
+};
+
+/**
+ * @returns {Promise}
+ */
+const collectDocRecords = async () => {
+ const docFiles = await Promise.all(
+ DOC_DIRECTORIES.map((directory) =>
+ collectMarkdownFiles(path.join(CONTENT_ROOT, directory)),
+ ),
+ );
+ const allFiles = docFiles.flat();
+ const records = await Promise.all(allFiles.map(readDocRecord));
+
+ return records.filter((record) => record !== null);
+};
+
+/**
+ * @returns {Promise}
+ */
+const buildDocsSearchIndex = async () => {
+ await fs.rm(OUTPUT_PATH, { recursive: true, force: true });
+
+ const records = await collectDocRecords();
+ const { index, errors } = await createIndex({
+ forceLanguage: "en",
+ });
+
+ if (errors.length) {
+ throw new Error(errors.join("\n"));
+ }
+
+ if (!index) {
+ throw new Error("Pagefind did not return an index instance.");
+ }
+
+ for (const record of records) {
+ const { errors: recordErrors } = await index.addCustomRecord({
+ ...record,
+ language: "en",
+ });
+
+ if (recordErrors.length) {
+ throw new Error(recordErrors.join("\n"));
+ }
+ }
+
+ const { errors: writeErrors } = await index.writeFiles({
+ outputPath: OUTPUT_PATH,
+ });
+
+ if (writeErrors.length) {
+ throw new Error(writeErrors.join("\n"));
+ }
+
+ console.log(`Indexed ${records.length} docs pages into ${OUTPUT_PATH}`);
+};
+
+const isDirectExecution =
+ process.argv[1] &&
+ import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
+
+if (isDirectExecution) {
+ void buildDocsSearchIndex()
+ .catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+ })
+ .finally(() => {
+ void close();
+ });
+}
diff --git a/apps/website/scripts/build-docs-search-index.test.mjs b/apps/website/scripts/build-docs-search-index.test.mjs
new file mode 100644
index 000000000..a4d17e645
--- /dev/null
+++ b/apps/website/scripts/build-docs-search-index.test.mjs
@@ -0,0 +1,80 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import path from "node:path";
+import {
+ markdownToSearchText,
+ routePathFromContentFile,
+ searchFiltersFromContentFile,
+} from "./build-docs-search-index.mjs";
+
+void test("routePathFromContentFile maps content files to canonical docs routes", () => {
+ assert.equal(
+ routePathFromContentFile(
+ path.join(
+ process.cwd(),
+ "content",
+ "roam",
+ "welcome",
+ "getting-started.md",
+ ),
+ ),
+ "/docs/roam/welcome/getting-started",
+ );
+ assert.equal(
+ routePathFromContentFile(
+ path.join(process.cwd(), "content", "obsidian", "index.mdx"),
+ ),
+ "/docs/obsidian",
+ );
+ assert.equal(
+ routePathFromContentFile(path.join(process.cwd(), "content", "index.mdx")),
+ "/docs",
+ );
+});
+
+void test("markdownToSearchText strips MDX component markup but keeps readable text", () => {
+ const source = `import { Callout } from "nextra/components"
+
+# Documentation
+
+Choose the \`/docs\` routes.
+
+
+ Shared conceptual docs stay stable.
+
+
+- [Roam docs](/docs/roam)
+`;
+
+ assert.equal(
+ markdownToSearchText(source),
+ "Documentation Choose the /docs routes. Shared conceptual docs stay stable. Roam docs",
+ );
+});
+
+void test("searchFiltersFromContentFile scopes docs records by platform", () => {
+ assert.deepEqual(
+ searchFiltersFromContentFile(
+ path.join(
+ process.cwd(),
+ "content",
+ "roam",
+ "welcome",
+ "getting-started.md",
+ ),
+ ),
+ { platform: ["roam"] },
+ );
+ assert.deepEqual(
+ searchFiltersFromContentFile(
+ path.join(process.cwd(), "content", "obsidian", "index.mdx"),
+ ),
+ { platform: ["obsidian"] },
+ );
+ assert.equal(
+ searchFiltersFromContentFile(
+ path.join(process.cwd(), "content", "index.mdx"),
+ ),
+ undefined,
+ );
+});
diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json
index d7bf5c41c..a32535e2c 100644
--- a/apps/website/tsconfig.json
+++ b/apps/website/tsconfig.json
@@ -12,7 +12,7 @@
"next.config.ts",
"**/*.ts",
"**/*.tsx",
- "scripts/*tx",
+ "scripts/**/*.mjs",
".next/types/**/*.ts",
"postcss.config.mjs"
]
diff --git a/package.json b/package.json
index 318b0f778..10c565ff3 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"dev": "turbo dev",
"lint": "turbo lint",
"deploy": "turbo deploy",
- "format": "prettier --write \"**/*.{ts,tsx,md}\"",
+ "format": "prettier --write \"**/*.{js,mjs,ts,tsx,md,mdx}\"",
"prepare": "husky"
},
"devDependencies": {
@@ -34,7 +34,7 @@
"tailwindConfig": "./packages/tailwind-config/tailwind.config.ts"
},
"lint-staged": {
- "*.{ts|tsx|md}": "prettier -w"
+ "*.{ts,mjs,ts,tsx,md,mdx}": "prettier -w"
},
"dependencies": {
"posthog-js": "catalog:"