diff --git a/.cursor/plans/starlight-style_docs_section_68c9052e.plan.md b/.cursor/plans/starlight-style_docs_section_68c9052e.plan.md new file mode 100644 index 0000000..87d715e --- /dev/null +++ b/.cursor/plans/starlight-style_docs_section_68c9052e.plan.md @@ -0,0 +1,235 @@ +--- +name: Starlight-style Docs Section +overview: Implement a Starlight-style documentation section with configurable URL slug, left navigation sidebar showing grouped docs pages/posts, and right TOC sidebar for the current page content. +todos: + - id: config + content: Add DocsSectionConfig interface and docsSection config to siteConfig.ts + status: pending + - id: schema + content: Add docsSection, docsSectionGroup, docsSectionOrder fields to convex/schema.ts + status: pending + - id: queries + content: Add getDocsPosts and getDocsPages queries to convex/posts.ts and convex/pages.ts + status: pending + - id: sync + content: Update scripts/sync-posts.ts to parse new frontmatter fields + status: pending + - id: docs-sidebar + content: Create DocsSidebar.tsx component for left navigation + status: pending + - id: docs-toc + content: Create DocsTOC.tsx component for right table of contents + status: pending + - id: docs-layout + content: Create DocsLayout.tsx three-column layout wrapper + status: pending + - id: docs-page + content: Create DocsPage.tsx landing page component + status: pending + - id: post-integration + content: "Update Post.tsx to use DocsLayout when docsSection: true" + status: pending + - id: routing + content: Add docs route to App.tsx + status: pending + - id: styles + content: Add docs layout CSS styles to global.css + status: pending + - id: test-content + content: Add docsSection frontmatter to existing content for testing + status: pending +--- + +# Starlight-style Docs Section + +## Overview + +Create a documentation layout similar to [Astro Starlight](https://starlight.astro.build/manual-setup/) with: + +- **Left sidebar**: Navigation menu showing grouped docs pages/posts (collapsible sections) +- **Right sidebar**: Table of contents for the current page/post +- **Main content**: The page/post content +- **Configurable**: Base URL slug set in siteConfig + +## Architecture + +```mermaid +flowchart TB + subgraph Config[Configuration Layer] + SC[siteConfig.docsSection] + FM[Frontmatter Fields] + end + + subgraph Data[Data Layer] + Schema[convex/schema.ts] + Posts[convex/posts.ts] + Pages[convex/pages.ts] + end + + subgraph Components[Component Layer] + DocsLayout[DocsLayout.tsx] + DocsSidebar[DocsSidebar.tsx] + DocsTOC[DocsTOC.tsx] + end + + subgraph Routing[Routing Layer] + AppTsx[App.tsx] + DocsRoute[/docs route] + DocsPageRoute[/docs-page/:slug] + end + + SC --> DocsLayout + FM --> Schema + Schema --> Posts + Schema --> Pages + Posts --> DocsSidebar + Pages --> DocsSidebar + DocsLayout --> DocsSidebar + DocsLayout --> DocsTOC + AppTsx --> DocsRoute + AppTsx --> DocsPageRoute +``` + +## Implementation Details + +### 1. Site Configuration + +Add to [`src/config/siteConfig.ts`](src/config/siteConfig.ts): + +```typescript +docsSection: { + enabled: true, + slug: "docs", // Base URL: /docs + title: "Documentation", // Page title + showInNav: true, // Show in navigation + order: 1, // Nav order + defaultExpanded: true, // Expand all groups by default +} +``` + +### 2. New Frontmatter Fields + +For pages and posts that should appear in docs navigation: + +```yaml +--- +title: "Setup Guide" +slug: "setup-guide" +docsSection: true # Include in docs navigation +docsSectionGroup: "Getting Started" # Group name in sidebar +docsSectionOrder: 1 # Order within group (lower = first) +--- +``` + +### 3. Database Schema Updates + +Add to [`convex/schema.ts`](convex/schema.ts) for both `posts` and `pages` tables: + +```typescript +docsSection: v.optional(v.boolean()), // Include in docs navigation +docsSectionGroup: v.optional(v.string()), // Sidebar group name +docsSectionOrder: v.optional(v.number()), // Order within group +``` + +### 4. New Components + +**DocsSidebar.tsx** (left navigation): + +- Fetches all pages/posts with `docsSection: true` +- Groups by `docsSectionGroup` +- Sorts by `docsSectionOrder` +- Collapsible group sections with ChevronRight icons +- Highlights current page +- Persists expanded state to localStorage + +**DocsTOC.tsx** (right TOC): + +- Reuses heading extraction from `extractHeadings.ts` +- Similar to current `PageSidebar.tsx` but positioned on right +- Active heading highlighting on scroll +- Smooth scroll navigation + +**DocsLayout.tsx** (three-column layout): + +- Left: DocsSidebar (navigation) +- Center: Main content +- Right: DocsTOC (table of contents) +- Responsive: stacks on mobile + +### 5. Routing Changes + +Update [`src/App.tsx`](src/App.tsx): + +```typescript +// Docs landing page (shows first doc or overview) +{ + siteConfig.docsSection?.enabled && ( + } /> + ); +} + +// Existing catch-all handles individual doc pages +} />; +``` + +### 6. Post/Page Rendering + +Update [`src/pages/Post.tsx`](src/pages/Post.tsx): + +- Detect if current page/post has `docsSection: true` +- If yes, render with `DocsLayout` instead of current layout +- Pass headings to DocsTOC for right sidebar + +### 7. Sync Script Updates + +Update [`scripts/sync-posts.ts`](scripts/sync-posts.ts): + +- Parse new frontmatter fields: `docsSection`, `docsSectionGroup`, `docsSectionOrder` +- Include in sync payload for both posts and pages + +### 8. CSS Styling + +Add to [`src/styles/global.css`](src/styles/global.css): + +- Three-column docs grid layout +- DocsSidebar styles (groups, links, active states) +- DocsTOC styles (right-aligned, smaller font) +- Mobile responsive breakpoints +- Theme-aware colors using existing CSS variables + +## File Changes Summary + +| File | Change | + +| -------------------------------- | ---------------------------------------------------------------- | + +| `src/config/siteConfig.ts` | Add `DocsSectionConfig` interface and `docsSection` config | + +| `convex/schema.ts` | Add `docsSection`, `docsSectionGroup`, `docsSectionOrder` fields | + +| `convex/posts.ts` | Add `getDocsPosts` query | + +| `convex/pages.ts` | Add `getDocsPages` query | + +| `scripts/sync-posts.ts` | Parse new frontmatter fields | + +| `src/components/DocsSidebar.tsx` | New component for left navigation | + +| `src/components/DocsTOC.tsx` | New component for right TOC | + +| `src/components/DocsLayout.tsx` | New three-column layout wrapper | + +| `src/pages/DocsPage.tsx` | New docs landing page | + +| `src/pages/Post.tsx` | Conditional docs layout rendering | + +| `src/App.tsx` | Add docs route | + +| `src/styles/global.css` | Add docs layout styles | + +## Migration Path + +1. Existing pages/posts continue working unchanged +2. Add `docsSection: true` to content you want in docs navigation +3. Set base slug in siteConfig (default: "docs") +4. Run `npm run sync` to update database with new fields diff --git a/TASK.md b/TASK.md index 184d789..d3a493b 100644 --- a/TASK.md +++ b/TASK.md @@ -4,6 +4,7 @@ - [ ] docs pages - [ ] fix site confg link + - [ ] npm package ## Current Status diff --git a/changelog.md b/changelog.md index 0112f71..6e4f22b 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [2.7.0] - 2026-01-02 + +### Added + +- `docsSectionGroupOrder` frontmatter field for controlling docs sidebar group order + - Groups are sorted by the minimum `docsSectionGroupOrder` value among items in each group + - Lower numbers appear first, groups without this field sort alphabetically + - Works alongside `docsSection`, `docsSectionGroup`, and `docsSectionOrder` fields + +### Technical + +- Updated `convex/schema.ts` to include `docsSectionGroupOrder` field in posts and pages tables +- Updated `convex/posts.ts` and `convex/pages.ts` queries and mutations to handle `docsSectionGroupOrder` +- Updated `scripts/sync-posts.ts` to parse `docsSectionGroupOrder` from frontmatter +- Updated `src/components/DocsSidebar.tsx` to sort groups by `docsSectionGroupOrder` + ## [2.6.0] - 2026-01-01 ### Added diff --git a/content/blog/fork-configuration-guide.md b/content/blog/fork-configuration-guide.md index ba5226f..874eb9d 100644 --- a/content/blog/fork-configuration-guide.md +++ b/content/blog/fork-configuration-guide.md @@ -13,6 +13,10 @@ authorName: "Markdown" authorImage: "/images/authors/markdown.png" image: "/images/forkconfig.png" excerpt: "Set up your forked site with npm run configure or follow the manual FORK_CONFIG.md guide." +docsSection: true +docsSectionGroup: "Setup" +docsSectionGroupOrder: 1 +docsSectionOrder: 1 --- # Configure your fork in one command @@ -97,19 +101,19 @@ If you prefer to update files manually, follow the guide in `FORK_CONFIG.md`. It The configuration script updates these files: -| File | What changes | -| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| File | What changes | +| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `src/config/siteConfig.ts` | Site name, bio, GitHub username, gitHubRepo config, features (logo gallery, GitHub contributions, visitor map, blog page, posts display, homepage, right sidebar, footer, social footer, AI chat, newsletter, contact form, newsletter admin, stats page, MCP server, dashboard, image lightbox) | -| `src/pages/Home.tsx` | Intro paragraph, footer links | -| `src/pages/Post.tsx` | SITE_URL, SITE_NAME constants | -| `convex/http.ts` | SITE_URL, SITE_NAME constants | -| `convex/rss.ts` | SITE_URL, SITE_TITLE, SITE_DESCRIPTION | -| `index.html` | Meta tags, JSON-LD, page title | -| `public/llms.txt` | Site info, GitHub link | -| `public/robots.txt` | Sitemap URL | -| `public/openapi.yaml` | Server URL, site name | -| `public/.well-known/ai-plugin.json` | Plugin metadata | -| `src/context/ThemeContext.tsx` | Default theme | +| `src/pages/Home.tsx` | Intro paragraph, footer links | +| `src/pages/Post.tsx` | SITE_URL, SITE_NAME constants | +| `convex/http.ts` | SITE_URL, SITE_NAME constants | +| `convex/rss.ts` | SITE_URL, SITE_TITLE, SITE_DESCRIPTION | +| `index.html` | Meta tags, JSON-LD, page title | +| `public/llms.txt` | Site info, GitHub link | +| `public/robots.txt` | Sitemap URL | +| `public/openapi.yaml` | Server URL, site name | +| `public/.well-known/ai-plugin.json` | Plugin metadata | +| `src/context/ThemeContext.tsx` | Default theme | ## Optional settings diff --git a/content/blog/how-to-publish.md b/content/blog/how-to-publish.md index 64b34bb..2f69f99 100644 --- a/content/blog/how-to-publish.md +++ b/content/blog/how-to-publish.md @@ -14,6 +14,13 @@ blogFeatured: true authorImage: "/images/authors/markdown.png" image: "/images/matthew-smith-Rfflri94rs8-unsplash.jpg" excerpt: "Quick guide to writing and publishing markdown posts with npm run sync." +aiChat: true +docsSection: true +docsSectionGroup: "Publishing" +docsSectionOrder: 3 +docsSectionGroupOrder: 3 +--- + --- # How to Publish a Blog Post diff --git a/content/blog/how-to-setup-workos.md b/content/blog/how-to-setup-workos.md index b4c77f0..068ab13 100644 --- a/content/blog/how-to-setup-workos.md +++ b/content/blog/how-to-setup-workos.md @@ -11,6 +11,10 @@ featuredOrder: 4 layout: "sidebar" image: /images/workos.png excerpt: "Complete guide to setting up WorkOS AuthKit authentication for your dashboard. WorkOS is optional and can be configured in siteConfig.ts." +docsSection: true +docsSectionOrder: 2 +docsSectionGroup: "Components" +docsLanding: true --- # How to setup WorkOS diff --git a/content/blog/how-to-use-agentmail.md b/content/blog/how-to-use-agentmail.md index 11b8c34..f9a89a9 100644 --- a/content/blog/how-to-use-agentmail.md +++ b/content/blog/how-to-use-agentmail.md @@ -10,6 +10,10 @@ layout: "sidebar" blogFeatured: true image: /images/agentmail-blog.png tags: ["agentmail", "newsletter", "email", "setup"] +docsSection: true +docsSectionOrder: 2 +docsSectionGroup: "Components" +docsLanding: true --- AgentMail provides email infrastructure for your markdown blog, enabling newsletter subscriptions, contact forms, and automated email notifications. This guide covers setup, configuration, and usage. diff --git a/content/blog/how-to-use-firecrawl.md b/content/blog/how-to-use-firecrawl.md index 397a13c..7de601a 100644 --- a/content/blog/how-to-use-firecrawl.md +++ b/content/blog/how-to-use-firecrawl.md @@ -8,6 +8,10 @@ featured: true featuredOrder: 6 image: /images/firecrwall-blog.png tags: ["tutorial", "firecrawl", "import"] +docsSection: true +docsSectionOrder: 2 +docsSectionGroup: "Components" +docsLanding: true --- # How to use Firecrawl diff --git a/content/blog/how-to-use-mcp-server.md b/content/blog/how-to-use-mcp-server.md index 5dbb162..59b4b07 100644 --- a/content/blog/how-to-use-mcp-server.md +++ b/content/blog/how-to-use-mcp-server.md @@ -8,6 +8,10 @@ published: true blogFeatured: true layout: "sidebar" tags: ["mcp", "cursor", "ai", "tutorial", "netlify"] +docsSection: true +docsSectionOrder: 2 +docsSectionGroup: "Components" +docsLanding: true --- This site includes an HTTP-based Model Context Protocol (MCP) server that allows AI tools like Cursor, Claude Desktop, and other MCP-compatible clients to access blog content programmatically. diff --git a/content/blog/how-to-use-the-markdown-sync-dashboard.md b/content/blog/how-to-use-the-markdown-sync-dashboard.md index 12c5edd..9877174 100644 --- a/content/blog/how-to-use-the-markdown-sync-dashboard.md +++ b/content/blog/how-to-use-the-markdown-sync-dashboard.md @@ -11,6 +11,10 @@ layout: "sidebar" featuredOrder: 2 image: /images/dashboard.png excerpt: "A complete guide to using the dashboard for managing your markdown blog without leaving your browser." +docsSection: true +docsSectionOrder: 2 +docsSectionGroup: "Components" +docsLanding: true --- # How to use the Markdown sync dashboard diff --git a/content/blog/markdown-with-code-examples.md b/content/blog/markdown-with-code-examples.md index c3f4ba7..e055db9 100644 --- a/content/blog/markdown-with-code-examples.md +++ b/content/blog/markdown-with-code-examples.md @@ -12,6 +12,10 @@ featured: false layout: "sidebar" featuredOrder: 5 image: "/images/markdown.png" +docsSection: true +docsSectionOrder: 3 +docsSectionGroup: "Publishing" +docsLanding: true --- # Writing Markdown with Code Examples @@ -346,7 +350,8 @@ Embed a YouTube video using an iframe: height="315" src="https://www.youtube.com/embed/dQw4w9WgXcQ" title="YouTube video" - allowfullscreen> + allowfullscreen +> ``` @@ -362,7 +367,8 @@ Embed a tweet using the Twitter embed URL: ``` @@ -380,7 +386,8 @@ Use `youtube-nocookie.com` for privacy-enhanced embeds: height="315" src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ" title="YouTube video" - allowfullscreen> + allowfullscreen +> ``` diff --git a/content/blog/setup-guide.md b/content/blog/setup-guide.md index 30c4a83..222de64 100644 --- a/content/blog/setup-guide.md +++ b/content/blog/setup-guide.md @@ -14,6 +14,10 @@ image: "/images/setupguide.png" authorName: "Markdown" authorImage: "/images/authors/markdown.png" excerpt: "Complete guide to fork, set up, and deploy your own markdown framework in under 10 minutes." +docsSection: true +docsSectionOrder: 1 +docsSectionGroup: "Setup" +docsLanding: true --- # Fork and Deploy Your Own Markdown Framework @@ -63,6 +67,7 @@ This guide walks you through forking [this markdown framework](https://github.co - [Visitor Map](#visitor-map) - [Logo Gallery](#logo-gallery) - [Blog page](#blog-page) + - [Homepage Post Limit](#homepage-post-limit) - [Hardcoded Navigation Items](#hardcoded-navigation-items) - [Scroll-to-top button](#scroll-to-top-button) - [Change the Default Theme](#change-the-default-theme) @@ -71,10 +76,15 @@ This guide walks you through forking [this markdown framework](https://github.co - [Add Static Pages (Optional)](#add-static-pages-optional) - [Update SEO Meta Tags](#update-seo-meta-tags) - [Update llms.txt and robots.txt](#update-llmstxt-and-robotstxt) + - [Tag Pages and Related Posts](#tag-pages-and-related-posts) - [Search](#search) - [Using Search](#using-search) - [How It Works](#how-it-works) - [Real-time Stats](#real-time-stats) + - [Footer Configuration](#footer-configuration) + - [Social Footer Configuration](#social-footer-configuration) + - [Right Sidebar Configuration](#right-sidebar-configuration) + - [Contact Form Configuration](#contact-form-configuration) - [Newsletter Admin](#newsletter-admin) - [Mobile Navigation](#mobile-navigation) - [Copy Page Dropdown](#copy-page-dropdown) @@ -87,7 +97,9 @@ This guide walks you through forking [this markdown framework](https://github.co - [Project Structure](#project-structure) - [Write Page](#write-page) - [AI Agent chat](#ai-agent-chat) + - [Dashboard](#dashboard) - [Next Steps](#next-steps) + - [MCP Server](#mcp-server) ## Prerequisites @@ -336,23 +348,27 @@ Your markdown content here... ### Frontmatter Fields -| Field | Required | Description | -| --------------- | -------- | ----------------------------------------------------------------------------- | -| `title` | Yes | Post title | -| `description` | Yes | Short description for SEO | -| `date` | Yes | Publication date (YYYY-MM-DD) | -| `slug` | Yes | URL path (must be unique) | -| `published` | Yes | Set to `true` to publish | -| `tags` | Yes | Array of topic tags | -| `readTime` | No | Estimated reading time | -| `image` | No | Header/Open Graph image URL | -| `excerpt` | No | Short excerpt for card view | -| `featured` | No | Set `true` to show in featured section | -| `featuredOrder` | No | Order in featured section (lower = first) | -| `authorName` | No | Author display name shown next to date | -| `authorImage` | No | Round author avatar image URL | -| `rightSidebar` | No | Enable right sidebar with CopyPageDropdown (opt-in, requires explicit `true`) | +| Field | Required | Description | +| --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `title` | Yes | Post title | +| `description` | Yes | Short description for SEO | +| `date` | Yes | Publication date (YYYY-MM-DD) | +| `slug` | Yes | URL path (must be unique) | +| `published` | Yes | Set to `true` to publish | +| `tags` | Yes | Array of topic tags | +| `readTime` | No | Estimated reading time | +| `image` | No | Header/Open Graph image URL | +| `excerpt` | No | Short excerpt for card view | +| `featured` | No | Set `true` to show in featured section | +| `featuredOrder` | No | Order in featured section (lower = first) | +| `authorName` | No | Author display name shown next to date | +| `authorImage` | No | Round author avatar image URL | +| `rightSidebar` | No | Enable right sidebar with CopyPageDropdown (opt-in, requires explicit `true`) | | `unlisted` | No | Hide from listings but allow direct access via slug. Set `true` to hide from blog listings, featured sections, tag pages, search results, and related posts. Post remains accessible via direct link. | +| `docsSection` | No | Include in docs sidebar. Set `true` to show in the docs section navigation. | +| `docsSectionGroup` | No | Group name for docs sidebar. Posts with the same group name appear together. | +| `docsSectionOrder` | No | Order within docs group. Lower numbers appear first within the group. | +| `docsSectionGroupOrder` | No | Order of the group in docs sidebar. Lower numbers make the group appear first. Groups without this field sort alphabetically. | ### How Frontmatter Works @@ -1591,11 +1607,11 @@ The Dashboard includes a dedicated AI Agent section with a tab-based UI for Chat Agent requires API keys for the providers you want to use. Set these in Convex environment variables: -| Variable | Provider | Features | -| --- | --- | --- | -| `ANTHROPIC_API_KEY` | Anthropic | Claude Sonnet 4 chat | -| `OPENAI_API_KEY` | OpenAI | GPT-4o chat | -| `GOOGLE_AI_API_KEY` | Google | Gemini 2.0 Flash chat + image generation | +| Variable | Provider | Features | +| ------------------- | --------- | ---------------------------------------- | +| `ANTHROPIC_API_KEY` | Anthropic | Claude Sonnet 4 chat | +| `OPENAI_API_KEY` | OpenAI | GPT-4o chat | +| `GOOGLE_AI_API_KEY` | Google | Gemini 2.0 Flash chat + image generation | **Optional system prompt variables:** @@ -1680,6 +1696,7 @@ npm run sync-server ``` This starts a local HTTP server on `localhost:3001` that: + - Executes sync commands when requested from the dashboard - Streams output in real-time to the dashboard terminal view - Shows server status (online/offline) in the dashboard diff --git a/content/blog/team-workflows-git-version-control.md b/content/blog/team-workflows-git-version-control.md index 0dc13ed..b58c205 100644 --- a/content/blog/team-workflows-git-version-control.md +++ b/content/blog/team-workflows-git-version-control.md @@ -11,6 +11,10 @@ featured: false layout: "sidebar" newsletter: true excerpt: "Learn how teams use git for markdown version control, sync to Convex deployments, and automate production workflows." +docsSection: true +docsSectionOrder: 1 +docsSectionGroup: "Setup" +docsLanding: true --- # Team Workflows with Git Version Control diff --git a/content/blog/using-images-in-posts.md b/content/blog/using-images-in-posts.md index d6cfd54..581420d 100644 --- a/content/blog/using-images-in-posts.md +++ b/content/blog/using-images-in-posts.md @@ -14,6 +14,10 @@ showImageAtTop: true authorName: "Markdown" authorImage: "/images/authors/markdown.png" image: "https://images.unsplash.com/photo-1499750310107-5fef28a66643?w=1200&h=630&fit=crop" +docsSection: true +docsSectionOrder: 3 +docsSectionGroup: "Publishing" +docsLanding: true --- # Using Images in Blog Posts diff --git a/content/pages/changelog-page.md b/content/pages/changelog-page.md index 1de4ca8..8d7ce93 100644 --- a/content/pages/changelog-page.md +++ b/content/pages/changelog-page.md @@ -5,11 +5,42 @@ published: true order: 5 rightSidebar: false layout: "sidebar" +docsSection: true +docsSectionOrder: 4 --- All notable changes to this project. ![](https://img.shields.io/badge/License-MIT-yellow.svg) +## v2.7.0 + +Released January 2, 2026 + +**Docs sidebar group ordering** + +- New `docsSectionGroupOrder` frontmatter field for controlling docs sidebar group order +- Groups are sorted by the minimum `docsSectionGroupOrder` value among items in each group +- Lower numbers appear first, groups without this field sort alphabetically +- Works alongside `docsSection`, `docsSectionGroup`, and `docsSectionOrder` fields + +**Example usage:** + +```yaml +--- +docsSection: true +docsSectionGroup: "Getting Started" +docsSectionGroupOrder: 1 +docsSectionOrder: 1 +--- +``` + +**Technical details:** + +- Updated `convex/schema.ts` with `docsSectionGroupOrder` field in posts and pages tables +- Updated `convex/posts.ts` and `convex/pages.ts` queries and mutations +- Updated `scripts/sync-posts.ts` to parse `docsSectionGroupOrder` from frontmatter +- Updated `src/components/DocsSidebar.tsx` to sort groups by `docsSectionGroupOrder` + ## v2.6.0 Released January 1, 2026 diff --git a/content/pages/docs.md b/content/pages/docs.md index beb71bb..e284a98 100644 --- a/content/pages/docs.md +++ b/content/pages/docs.md @@ -1,12 +1,17 @@ --- -title: "Docs" -slug: "docs" +title: "Documentation" +slug: "documentation" published: true order: 0 +showInNav: false layout: "sidebar" aiChat: true rightSidebar: true showFooter: true +docsSection: true +docsSectionOrder: 1 +docsSectionGroup: "Setup" +docsLanding: true --- ## Getting Started @@ -130,6 +135,10 @@ Content here... | `newsletter` | No | Override newsletter signup display (`true` to show, `false` to hide) | | `contactForm` | No | Enable contact form on this post | | `unlisted` | No | Hide from listings but allow direct access via slug. Set `true` to hide from blog listings, featured sections, tag pages, search results, and related posts. Post remains accessible via direct link. | +| `docsSection` | No | Include in docs sidebar. Set `true` to show in the docs section navigation. | +| `docsSectionGroup` | No | Group name for docs sidebar. Posts with the same group name appear together. | +| `docsSectionOrder` | No | Order within docs group. Lower numbers appear first within the group. | +| `docsSectionGroupOrder` | No | Order of the group in docs sidebar. Lower numbers make the group appear first. Groups without this field sort alphabetically. | | `showImageAtTop` | No | Set `true` to display the `image` field at the top of the post above the header (default: `false`) | ### Static pages @@ -173,6 +182,10 @@ Content here... | `contactForm` | No | Enable contact form on this page | | `showImageAtTop` | No | Set `true` to display the `image` field at the top of the page above the header (default: `false`) | | `textAlign` | No | Text alignment: "left" (default), "center", or "right". Used by `home.md` for home intro alignment | +| `docsSection` | No | Include in docs sidebar. Set `true` to show in the docs section navigation. | +| `docsSectionGroup` | No | Group name for docs sidebar. Pages with the same group name appear together. | +| `docsSectionOrder` | No | Order within docs group. Lower numbers appear first within the group. | +| `docsSectionGroupOrder` | No | Order of the group in docs sidebar. Lower numbers make the group appear first. Groups without this field sort alphabetically. | **Hide pages from navigation:** Set `showInNav: false` to keep a page published and accessible via direct URL, but hidden from the navigation menu. Pages with `showInNav: false` remain searchable and available via API endpoints. Useful for pages you want to link directly but not show in the main nav. @@ -1117,10 +1130,10 @@ The Dashboard includes a dedicated AI Agent section with tab-based UI for Chat a **Environment Variables (Convex):** -| Variable | Description | -| --- | --- | -| `ANTHROPIC_API_KEY` | Required for Claude Sonnet 4 | -| `OPENAI_API_KEY` | Required for GPT-4o | +| Variable | Description | +| ------------------- | -------------------------------------------------- | +| `ANTHROPIC_API_KEY` | Required for Claude Sonnet 4 | +| `OPENAI_API_KEY` | Required for GPT-4o | | `GOOGLE_AI_API_KEY` | Required for Gemini 2.0 Flash and image generation | **Note:** Only configure the API keys for models you want to use. If a key is not set, users see a helpful setup message when they try to use that model. diff --git a/convex/pages.ts b/convex/pages.ts index de4b740..d246e27 100644 --- a/convex/pages.ts +++ b/convex/pages.ts @@ -184,6 +184,7 @@ export const getPageBySlug = query({ contactForm: v.optional(v.boolean()), newsletter: v.optional(v.boolean()), textAlign: v.optional(v.string()), + docsSection: v.optional(v.boolean()), }), v.null(), ), @@ -221,6 +222,94 @@ export const getPageBySlug = query({ contactForm: page.contactForm, newsletter: page.newsletter, textAlign: page.textAlign, + docsSection: page.docsSection, + }; + }, +}); + +// Get all pages marked for docs section navigation +// Used by DocsSidebar to build the left navigation +export const getDocsPages = query({ + args: {}, + returns: v.array( + v.object({ + _id: v.id("pages"), + slug: v.string(), + title: v.string(), + docsSectionGroup: v.optional(v.string()), + docsSectionOrder: v.optional(v.number()), + docsSectionGroupOrder: v.optional(v.number()), + }), + ), + handler: async (ctx) => { + const pages = await ctx.db + .query("pages") + .withIndex("by_docsSection", (q) => q.eq("docsSection", true)) + .collect(); + + // Filter to only published pages + const publishedDocs = pages.filter((p) => p.published); + + // Sort by docsSectionOrder, then by title + const sortedDocs = publishedDocs.sort((a, b) => { + const orderA = a.docsSectionOrder ?? 999; + const orderB = b.docsSectionOrder ?? 999; + if (orderA !== orderB) return orderA - orderB; + return a.title.localeCompare(b.title); + }); + + return sortedDocs.map((page) => ({ + _id: page._id, + slug: page.slug, + title: page.title, + docsSectionGroup: page.docsSectionGroup, + docsSectionOrder: page.docsSectionOrder, + docsSectionGroupOrder: page.docsSectionGroupOrder, + })); + }, +}); + +// Get the docs landing page (page with docsLanding: true) +// Returns null if no landing page is set +export const getDocsLandingPage = query({ + args: {}, + returns: v.union( + v.object({ + _id: v.id("pages"), + slug: v.string(), + title: v.string(), + content: v.string(), + image: v.optional(v.string()), + showImageAtTop: v.optional(v.boolean()), + authorName: v.optional(v.string()), + authorImage: v.optional(v.string()), + docsSectionGroup: v.optional(v.string()), + docsSectionOrder: v.optional(v.number()), + }), + v.null(), + ), + handler: async (ctx) => { + // Get all docs pages and find one with docsLanding: true + const pages = await ctx.db + .query("pages") + .withIndex("by_docsSection", (q) => q.eq("docsSection", true)) + .collect(); + + const landing = pages.find((p) => p.published && p.docsLanding); + + if (!landing) return null; + + return { + _id: landing._id, + slug: landing.slug, + title: landing.title, + content: landing.content, + image: landing.image, + showImageAtTop: landing.showImageAtTop, + authorName: landing.authorName, + authorImage: landing.authorImage, + docsSectionGroup: landing.docsSectionGroup, + docsSectionOrder: landing.docsSectionOrder, }; }, }); @@ -252,6 +341,11 @@ export const syncPagesPublic = mutation({ contactForm: v.optional(v.boolean()), newsletter: v.optional(v.boolean()), textAlign: v.optional(v.string()), + docsSection: v.optional(v.boolean()), + docsSectionGroup: v.optional(v.string()), + docsSectionOrder: v.optional(v.number()), + docsSectionGroupOrder: v.optional(v.number()), + docsLanding: v.optional(v.boolean()), }), ), }, @@ -300,6 +394,11 @@ export const syncPagesPublic = mutation({ contactForm: page.contactForm, newsletter: page.newsletter, textAlign: page.textAlign, + docsSection: page.docsSection, + docsSectionGroup: page.docsSectionGroup, + docsSectionOrder: page.docsSectionOrder, + docsSectionGroupOrder: page.docsSectionGroupOrder, + docsLanding: page.docsLanding, lastSyncedAt: now, }); updated++; diff --git a/convex/posts.ts b/convex/posts.ts index f01f964..de7b88d 100644 --- a/convex/posts.ts +++ b/convex/posts.ts @@ -238,6 +238,7 @@ export const getPostBySlug = query({ aiChat: v.optional(v.boolean()), newsletter: v.optional(v.boolean()), contactForm: v.optional(v.boolean()), + docsSection: v.optional(v.boolean()), }), v.null(), ), @@ -277,6 +278,7 @@ export const getPostBySlug = query({ aiChat: post.aiChat, newsletter: post.newsletter, contactForm: post.contactForm, + docsSection: post.docsSection, }; }, }); @@ -384,6 +386,11 @@ export const syncPosts = internalMutation({ newsletter: v.optional(v.boolean()), contactForm: v.optional(v.boolean()), unlisted: v.optional(v.boolean()), + docsSection: v.optional(v.boolean()), + docsSectionGroup: v.optional(v.string()), + docsSectionOrder: v.optional(v.number()), + docsSectionGroupOrder: v.optional(v.number()), + docsLanding: v.optional(v.boolean()), }), ), }, @@ -435,6 +442,11 @@ export const syncPosts = internalMutation({ newsletter: post.newsletter, contactForm: post.contactForm, unlisted: post.unlisted, + docsSection: post.docsSection, + docsSectionGroup: post.docsSectionGroup, + docsSectionOrder: post.docsSectionOrder, + docsSectionGroupOrder: post.docsSectionGroupOrder, + docsLanding: post.docsLanding, lastSyncedAt: now, }); updated++; @@ -490,6 +502,11 @@ export const syncPostsPublic = mutation({ newsletter: v.optional(v.boolean()), contactForm: v.optional(v.boolean()), unlisted: v.optional(v.boolean()), + docsSection: v.optional(v.boolean()), + docsSectionGroup: v.optional(v.string()), + docsSectionOrder: v.optional(v.number()), + docsSectionGroupOrder: v.optional(v.number()), + docsLanding: v.optional(v.boolean()), }), ), }, @@ -541,6 +558,11 @@ export const syncPostsPublic = mutation({ newsletter: post.newsletter, contactForm: post.contactForm, unlisted: post.unlisted, + docsSection: post.docsSection, + docsSectionGroup: post.docsSectionGroup, + docsSectionOrder: post.docsSectionOrder, + docsSectionGroupOrder: post.docsSectionGroupOrder, + docsLanding: post.docsLanding, lastSyncedAt: now, }); updated++; @@ -874,3 +896,98 @@ export const getPostsByAuthor = query({ })); }, }); + +// Get all posts marked for docs section navigation +// Used by DocsSidebar to build the left navigation +export const getDocsPosts = query({ + args: {}, + returns: v.array( + v.object({ + _id: v.id("posts"), + slug: v.string(), + title: v.string(), + docsSectionGroup: v.optional(v.string()), + docsSectionOrder: v.optional(v.number()), + docsSectionGroupOrder: v.optional(v.number()), + }), + ), + handler: async (ctx) => { + const posts = await ctx.db + .query("posts") + .withIndex("by_docsSection", (q) => q.eq("docsSection", true)) + .collect(); + + // Filter to only published posts + const publishedDocs = posts.filter((p) => p.published); + + // Sort by docsSectionOrder, then by title + const sortedDocs = publishedDocs.sort((a, b) => { + const orderA = a.docsSectionOrder ?? 999; + const orderB = b.docsSectionOrder ?? 999; + if (orderA !== orderB) return orderA - orderB; + return a.title.localeCompare(b.title); + }); + + return sortedDocs.map((post) => ({ + _id: post._id, + slug: post.slug, + title: post.title, + docsSectionGroup: post.docsSectionGroup, + docsSectionOrder: post.docsSectionOrder, + docsSectionGroupOrder: post.docsSectionGroupOrder, + })); + }, +}); + +// Get the docs landing page (post with docsLanding: true) +// Returns null if no landing page is set +export const getDocsLandingPost = query({ + args: {}, + returns: v.union( + v.object({ + _id: v.id("posts"), + slug: v.string(), + title: v.string(), + description: v.string(), + content: v.string(), + date: v.string(), + tags: v.array(v.string()), + readTime: v.optional(v.string()), + image: v.optional(v.string()), + showImageAtTop: v.optional(v.boolean()), + authorName: v.optional(v.string()), + authorImage: v.optional(v.string()), + docsSectionGroup: v.optional(v.string()), + docsSectionOrder: v.optional(v.number()), + }), + v.null(), + ), + handler: async (ctx) => { + // Get all docs posts and find one with docsLanding: true + const posts = await ctx.db + .query("posts") + .withIndex("by_docsSection", (q) => q.eq("docsSection", true)) + .collect(); + + const landing = posts.find((p) => p.published && p.docsLanding); + + if (!landing) return null; + + return { + _id: landing._id, + slug: landing.slug, + title: landing.title, + description: landing.description, + content: landing.content, + date: landing.date, + tags: landing.tags, + readTime: landing.readTime, + image: landing.image, + showImageAtTop: landing.showImageAtTop, + authorName: landing.authorName, + authorImage: landing.authorImage, + docsSectionGroup: landing.docsSectionGroup, + docsSectionOrder: landing.docsSectionOrder, + }; + }, +}); diff --git a/convex/schema.ts b/convex/schema.ts index 531e8a8..8fb9715 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -29,6 +29,11 @@ export default defineSchema({ newsletter: v.optional(v.boolean()), // Override newsletter signup display (true/false) contactForm: v.optional(v.boolean()), // Enable contact form on this post unlisted: v.optional(v.boolean()), // Hide from listings but allow direct access via slug + docsSection: v.optional(v.boolean()), // Include in docs navigation + docsSectionGroup: v.optional(v.string()), // Sidebar group name in docs + docsSectionOrder: v.optional(v.number()), // Order within group (lower = first) + docsSectionGroupOrder: v.optional(v.number()), // Order of group itself (lower = first) + docsLanding: v.optional(v.boolean()), // Use as /docs landing page lastSyncedAt: v.number(), }) .index("by_slug", ["slug"]) @@ -37,6 +42,7 @@ export default defineSchema({ .index("by_featured", ["featured"]) .index("by_blogFeatured", ["blogFeatured"]) .index("by_authorName", ["authorName"]) + .index("by_docsSection", ["docsSection"]) .searchIndex("search_content", { searchField: "content", filterFields: ["published"], @@ -70,11 +76,17 @@ export default defineSchema({ contactForm: v.optional(v.boolean()), // Enable contact form on this page newsletter: v.optional(v.boolean()), // Override newsletter signup display (true/false) textAlign: v.optional(v.string()), // Text alignment: "left", "center", "right" (default: "left") + docsSection: v.optional(v.boolean()), // Include in docs navigation + docsSectionGroup: v.optional(v.string()), // Sidebar group name in docs + docsSectionOrder: v.optional(v.number()), // Order within group (lower = first) + docsSectionGroupOrder: v.optional(v.number()), // Order of group itself (lower = first) + docsLanding: v.optional(v.boolean()), // Use as /docs landing page lastSyncedAt: v.number(), }) .index("by_slug", ["slug"]) .index("by_published", ["published"]) .index("by_featured", ["featured"]) + .index("by_docsSection", ["docsSection"]) .searchIndex("search_content", { searchField: "content", filterFields: ["published"], diff --git a/files.md b/files.md index 8c7af7f..5138803 100644 --- a/files.md +++ b/files.md @@ -172,6 +172,10 @@ Markdown files with frontmatter for blog posts. Each file becomes a blog post. | `newsletter` | Override newsletter signup display (optional, true/false) | | `contactForm` | Enable contact form on this post (optional). Requires siteConfig.contactForm.enabled: true and AGENTMAIL_API_KEY/AGENTMAIL_INBOX environment variables. | | `unlisted` | Hide from listings but allow direct access via slug (optional). Set `true` to hide from blog listings, featured sections, tag pages, search results, and related posts. Post remains accessible via direct link. | +| `docsSection` | Include in docs sidebar (optional). Set `true` to show in the docs section navigation. | +| `docsSectionGroup` | Group name for docs sidebar (optional). Posts with the same group name appear together. | +| `docsSectionOrder` | Order within docs group (optional). Lower numbers appear first within the group. | +| `docsSectionGroupOrder` | Order of the group in docs sidebar (optional). Lower numbers make the group appear first. Groups without this field sort alphabetically. | ## Static Pages (`content/pages/`) @@ -203,6 +207,10 @@ Markdown files for static pages like About, Projects, Contact, Changelog. | `newsletter` | Override newsletter signup display (optional, true/false) | | `contactForm` | Enable contact form on this page (optional). Requires siteConfig.contactForm.enabled: true and AGENTMAIL_API_KEY/AGENTMAIL_INBOX environment variables. | | `textAlign` | Text alignment: "left", "center", "right" (optional, default: "left"). Used by home.md for home intro content alignment | +| `docsSection` | Include in docs sidebar (optional). Set `true` to show in the docs section navigation. | +| `docsSectionGroup` | Group name for docs sidebar (optional). Pages with the same group name appear together. | +| `docsSectionOrder` | Order within docs group (optional). Lower numbers appear first within the group. | +| `docsSectionGroupOrder` | Order of the group in docs sidebar (optional). Lower numbers make the group appear first. Groups without this field sort alphabetically. | ## Scripts (`scripts/`) diff --git a/public/raw/about.md b/public/raw/about.md index 2e7aaae..fab6d9b 100644 --- a/public/raw/about.md +++ b/public/raw/about.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-02 +Date: 2026-01-03 --- An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify. diff --git a/public/raw/changelog.md b/public/raw/changelog.md index f8ea049..8963a93 100644 --- a/public/raw/changelog.md +++ b/public/raw/changelog.md @@ -2,12 +2,41 @@ --- Type: page -Date: 2026-01-02 +Date: 2026-01-03 --- All notable changes to this project. ![](https://img.shields.io/badge/License-MIT-yellow.svg) +## v2.7.0 + +Released January 2, 2026 + +**Docs sidebar group ordering** + +- New `docsSectionGroupOrder` frontmatter field for controlling docs sidebar group order +- Groups are sorted by the minimum `docsSectionGroupOrder` value among items in each group +- Lower numbers appear first, groups without this field sort alphabetically +- Works alongside `docsSection`, `docsSectionGroup`, and `docsSectionOrder` fields + +**Example usage:** + +```yaml +--- +docsSection: true +docsSectionGroup: "Getting Started" +docsSectionGroupOrder: 1 +docsSectionOrder: 1 +--- +``` + +**Technical details:** + +- Updated `convex/schema.ts` with `docsSectionGroupOrder` field in posts and pages tables +- Updated `convex/posts.ts` and `convex/pages.ts` queries and mutations +- Updated `scripts/sync-posts.ts` to parse `docsSectionGroupOrder` from frontmatter +- Updated `src/components/DocsSidebar.tsx` to sort groups by `docsSectionGroupOrder` + ## v2.6.0 Released January 1, 2026 diff --git a/public/raw/contact.md b/public/raw/contact.md index 8e05357..4171513 100644 --- a/public/raw/contact.md +++ b/public/raw/contact.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-02 +Date: 2026-01-03 --- You found the contact page. Nice diff --git a/public/raw/docs.md b/public/raw/documentation.md similarity index 96% rename from public/raw/docs.md rename to public/raw/documentation.md index 62fea4e..3127646 100644 --- a/public/raw/docs.md +++ b/public/raw/documentation.md @@ -1,8 +1,8 @@ -# Docs +# Documentation --- Type: page -Date: 2026-01-02 +Date: 2026-01-03 --- ## Getting Started @@ -126,6 +126,10 @@ Content here... | `newsletter` | No | Override newsletter signup display (`true` to show, `false` to hide) | | `contactForm` | No | Enable contact form on this post | | `unlisted` | No | Hide from listings but allow direct access via slug. Set `true` to hide from blog listings, featured sections, tag pages, search results, and related posts. Post remains accessible via direct link. | +| `docsSection` | No | Include in docs sidebar. Set `true` to show in the docs section navigation. | +| `docsSectionGroup` | No | Group name for docs sidebar. Posts with the same group name appear together. | +| `docsSectionOrder` | No | Order within docs group. Lower numbers appear first within the group. | +| `docsSectionGroupOrder` | No | Order of the group in docs sidebar. Lower numbers make the group appear first. Groups without this field sort alphabetically. | | `showImageAtTop` | No | Set `true` to display the `image` field at the top of the post above the header (default: `false`) | ### Static pages @@ -169,6 +173,10 @@ Content here... | `contactForm` | No | Enable contact form on this page | | `showImageAtTop` | No | Set `true` to display the `image` field at the top of the page above the header (default: `false`) | | `textAlign` | No | Text alignment: "left" (default), "center", or "right". Used by `home.md` for home intro alignment | +| `docsSection` | No | Include in docs sidebar. Set `true` to show in the docs section navigation. | +| `docsSectionGroup` | No | Group name for docs sidebar. Pages with the same group name appear together. | +| `docsSectionOrder` | No | Order within docs group. Lower numbers appear first within the group. | +| `docsSectionGroupOrder` | No | Order of the group in docs sidebar. Lower numbers make the group appear first. Groups without this field sort alphabetically. | **Hide pages from navigation:** Set `showInNav: false` to keep a page published and accessible via direct URL, but hidden from the navigation menu. Pages with `showInNav: false` remain searchable and available via API endpoints. Useful for pages you want to link directly but not show in the main nav. @@ -1113,10 +1121,10 @@ The Dashboard includes a dedicated AI Agent section with tab-based UI for Chat a **Environment Variables (Convex):** -| Variable | Description | -| --- | --- | -| `ANTHROPIC_API_KEY` | Required for Claude Sonnet 4 | -| `OPENAI_API_KEY` | Required for GPT-4o | +| Variable | Description | +| ------------------- | -------------------------------------------------- | +| `ANTHROPIC_API_KEY` | Required for Claude Sonnet 4 | +| `OPENAI_API_KEY` | Required for GPT-4o | | `GOOGLE_AI_API_KEY` | Required for Gemini 2.0 Flash and image generation | **Note:** Only configure the API keys for models you want to use. If a key is not set, users see a helpful setup message when they try to use that model. diff --git a/public/raw/footer.md b/public/raw/footer.md index 2223f0c..aaea11d 100644 --- a/public/raw/footer.md +++ b/public/raw/footer.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-02 +Date: 2026-01-03 --- Built with [Convex](https://convex.dev) for real-time sync and deployed on [Netlify](https://netlify.com). Read the [project on GitHub](https://github.com/waynesutton/markdown-site) to fork and deploy your own. View [real-time site stats](/stats). diff --git a/public/raw/fork-configuration-guide.md b/public/raw/fork-configuration-guide.md index f9a9ef8..cd683b4 100644 --- a/public/raw/fork-configuration-guide.md +++ b/public/raw/fork-configuration-guide.md @@ -91,19 +91,19 @@ If you prefer to update files manually, follow the guide in `FORK_CONFIG.md`. It The configuration script updates these files: -| File | What changes | -| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| File | What changes | +| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `src/config/siteConfig.ts` | Site name, bio, GitHub username, gitHubRepo config, features (logo gallery, GitHub contributions, visitor map, blog page, posts display, homepage, right sidebar, footer, social footer, AI chat, newsletter, contact form, newsletter admin, stats page, MCP server, dashboard, image lightbox) | -| `src/pages/Home.tsx` | Intro paragraph, footer links | -| `src/pages/Post.tsx` | SITE_URL, SITE_NAME constants | -| `convex/http.ts` | SITE_URL, SITE_NAME constants | -| `convex/rss.ts` | SITE_URL, SITE_TITLE, SITE_DESCRIPTION | -| `index.html` | Meta tags, JSON-LD, page title | -| `public/llms.txt` | Site info, GitHub link | -| `public/robots.txt` | Sitemap URL | -| `public/openapi.yaml` | Server URL, site name | -| `public/.well-known/ai-plugin.json` | Plugin metadata | -| `src/context/ThemeContext.tsx` | Default theme | +| `src/pages/Home.tsx` | Intro paragraph, footer links | +| `src/pages/Post.tsx` | SITE_URL, SITE_NAME constants | +| `convex/http.ts` | SITE_URL, SITE_NAME constants | +| `convex/rss.ts` | SITE_URL, SITE_TITLE, SITE_DESCRIPTION | +| `index.html` | Meta tags, JSON-LD, page title | +| `public/llms.txt` | Site info, GitHub link | +| `public/robots.txt` | Sitemap URL | +| `public/openapi.yaml` | Server URL, site name | +| `public/.well-known/ai-plugin.json` | Plugin metadata | +| `src/context/ThemeContext.tsx` | Default theme | ## Optional settings diff --git a/public/raw/home-intro.md b/public/raw/home-intro.md index 3f69cf4..bdec283 100644 --- a/public/raw/home-intro.md +++ b/public/raw/home-intro.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-02 +Date: 2026-01-03 --- An open-source publishing framework built for AI agents and developers to ship **[docs](/docs)**, or **[blogs](/blog)** or **[websites](/)**. diff --git a/public/raw/how-to-publish.md b/public/raw/how-to-publish.md index dd546c5..e86db41 100644 --- a/public/raw/how-to-publish.md +++ b/public/raw/how-to-publish.md @@ -9,6 +9,8 @@ Reading time: 3 min read Tags: tutorial, markdown, cursor, IDE, publishing --- +--- + # How to Publish a Blog Post ![nature](/images/matthew-smith-Rfflri94rs8-unsplash.jpg) diff --git a/public/raw/index.md b/public/raw/index.md index f7d183d..77d42b4 100644 --- a/public/raw/index.md +++ b/public/raw/index.md @@ -45,7 +45,7 @@ This is the homepage index of all published content. - **[Footer](/raw/footer.md)** - **[Home Intro](/raw/home-intro.md)** -- **[Docs](/raw/docs.md)** +- **[Documentation](/raw/documentation.md)** - **[About](/raw/about.md)** - An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs. - **[Projects](/raw/projects.md)** - **[Contact](/raw/contact.md)** diff --git a/public/raw/markdown-with-code-examples.md b/public/raw/markdown-with-code-examples.md index c8313b5..02bca1c 100644 --- a/public/raw/markdown-with-code-examples.md +++ b/public/raw/markdown-with-code-examples.md @@ -341,7 +341,8 @@ Embed a YouTube video using an iframe: height="315" src="https://www.youtube.com/embed/dQw4w9WgXcQ" title="YouTube video" - allowfullscreen> + allowfullscreen +> ``` @@ -357,7 +358,8 @@ Embed a tweet using the Twitter embed URL: ``` @@ -375,7 +377,8 @@ Use `youtube-nocookie.com` for privacy-enhanced embeds: height="315" src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ" title="YouTube video" - allowfullscreen> + allowfullscreen +> ``` diff --git a/public/raw/newsletter.md b/public/raw/newsletter.md index e6529ac..f5d9776 100644 --- a/public/raw/newsletter.md +++ b/public/raw/newsletter.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-02 +Date: 2026-01-03 --- # Newsletter Demo Page diff --git a/public/raw/projects.md b/public/raw/projects.md index 892c6a4..26cfc19 100644 --- a/public/raw/projects.md +++ b/public/raw/projects.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-02 +Date: 2026-01-03 --- This markdown framework is open source and built to be extended. Here is what ships out of the box. diff --git a/public/raw/setup-guide.md b/public/raw/setup-guide.md index f53127c..b18932e 100644 --- a/public/raw/setup-guide.md +++ b/public/raw/setup-guide.md @@ -56,6 +56,7 @@ This guide walks you through forking [this markdown framework](https://github.co - [Visitor Map](#visitor-map) - [Logo Gallery](#logo-gallery) - [Blog page](#blog-page) + - [Homepage Post Limit](#homepage-post-limit) - [Hardcoded Navigation Items](#hardcoded-navigation-items) - [Scroll-to-top button](#scroll-to-top-button) - [Change the Default Theme](#change-the-default-theme) @@ -64,10 +65,15 @@ This guide walks you through forking [this markdown framework](https://github.co - [Add Static Pages (Optional)](#add-static-pages-optional) - [Update SEO Meta Tags](#update-seo-meta-tags) - [Update llms.txt and robots.txt](#update-llmstxt-and-robotstxt) + - [Tag Pages and Related Posts](#tag-pages-and-related-posts) - [Search](#search) - [Using Search](#using-search) - [How It Works](#how-it-works) - [Real-time Stats](#real-time-stats) + - [Footer Configuration](#footer-configuration) + - [Social Footer Configuration](#social-footer-configuration) + - [Right Sidebar Configuration](#right-sidebar-configuration) + - [Contact Form Configuration](#contact-form-configuration) - [Newsletter Admin](#newsletter-admin) - [Mobile Navigation](#mobile-navigation) - [Copy Page Dropdown](#copy-page-dropdown) @@ -80,7 +86,9 @@ This guide walks you through forking [this markdown framework](https://github.co - [Project Structure](#project-structure) - [Write Page](#write-page) - [AI Agent chat](#ai-agent-chat) + - [Dashboard](#dashboard) - [Next Steps](#next-steps) + - [MCP Server](#mcp-server) ## Prerequisites @@ -329,23 +337,27 @@ Your markdown content here... ### Frontmatter Fields -| Field | Required | Description | -| --------------- | -------- | ----------------------------------------------------------------------------- | -| `title` | Yes | Post title | -| `description` | Yes | Short description for SEO | -| `date` | Yes | Publication date (YYYY-MM-DD) | -| `slug` | Yes | URL path (must be unique) | -| `published` | Yes | Set to `true` to publish | -| `tags` | Yes | Array of topic tags | -| `readTime` | No | Estimated reading time | -| `image` | No | Header/Open Graph image URL | -| `excerpt` | No | Short excerpt for card view | -| `featured` | No | Set `true` to show in featured section | -| `featuredOrder` | No | Order in featured section (lower = first) | -| `authorName` | No | Author display name shown next to date | -| `authorImage` | No | Round author avatar image URL | -| `rightSidebar` | No | Enable right sidebar with CopyPageDropdown (opt-in, requires explicit `true`) | +| Field | Required | Description | +| --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `title` | Yes | Post title | +| `description` | Yes | Short description for SEO | +| `date` | Yes | Publication date (YYYY-MM-DD) | +| `slug` | Yes | URL path (must be unique) | +| `published` | Yes | Set to `true` to publish | +| `tags` | Yes | Array of topic tags | +| `readTime` | No | Estimated reading time | +| `image` | No | Header/Open Graph image URL | +| `excerpt` | No | Short excerpt for card view | +| `featured` | No | Set `true` to show in featured section | +| `featuredOrder` | No | Order in featured section (lower = first) | +| `authorName` | No | Author display name shown next to date | +| `authorImage` | No | Round author avatar image URL | +| `rightSidebar` | No | Enable right sidebar with CopyPageDropdown (opt-in, requires explicit `true`) | | `unlisted` | No | Hide from listings but allow direct access via slug. Set `true` to hide from blog listings, featured sections, tag pages, search results, and related posts. Post remains accessible via direct link. | +| `docsSection` | No | Include in docs sidebar. Set `true` to show in the docs section navigation. | +| `docsSectionGroup` | No | Group name for docs sidebar. Posts with the same group name appear together. | +| `docsSectionOrder` | No | Order within docs group. Lower numbers appear first within the group. | +| `docsSectionGroupOrder` | No | Order of the group in docs sidebar. Lower numbers make the group appear first. Groups without this field sort alphabetically. | ### How Frontmatter Works @@ -1584,11 +1596,11 @@ The Dashboard includes a dedicated AI Agent section with a tab-based UI for Chat Agent requires API keys for the providers you want to use. Set these in Convex environment variables: -| Variable | Provider | Features | -| --- | --- | --- | -| `ANTHROPIC_API_KEY` | Anthropic | Claude Sonnet 4 chat | -| `OPENAI_API_KEY` | OpenAI | GPT-4o chat | -| `GOOGLE_AI_API_KEY` | Google | Gemini 2.0 Flash chat + image generation | +| Variable | Provider | Features | +| ------------------- | --------- | ---------------------------------------- | +| `ANTHROPIC_API_KEY` | Anthropic | Claude Sonnet 4 chat | +| `OPENAI_API_KEY` | OpenAI | GPT-4o chat | +| `GOOGLE_AI_API_KEY` | Google | Gemini 2.0 Flash chat + image generation | **Optional system prompt variables:** @@ -1673,6 +1685,7 @@ npm run sync-server ``` This starts a local HTTP server on `localhost:3001` that: + - Executes sync commands when requested from the dashboard - Streams output in real-time to the dashboard terminal view - Shows server status (online/offline) in the dashboard diff --git a/scripts/sync-posts.ts b/scripts/sync-posts.ts index 29a080b..5c108a0 100644 --- a/scripts/sync-posts.ts +++ b/scripts/sync-posts.ts @@ -46,6 +46,11 @@ interface PostFrontmatter { blogFeatured?: boolean; // Show as hero featured post on /blog page newsletter?: boolean; // Override newsletter signup display (true/false) contactForm?: boolean; // Enable contact form on this post + docsSection?: boolean; // Include in docs navigation + docsSectionGroup?: string; // Sidebar group name in docs + docsSectionOrder?: number; // Order within group (lower = first) + docsSectionGroupOrder?: number; // Order of group itself (lower = first) + docsLanding?: boolean; // Use as /docs landing page } interface ParsedPost { @@ -74,6 +79,11 @@ interface ParsedPost { newsletter?: boolean; // Override newsletter signup display (true/false) contactForm?: boolean; // Enable contact form on this post unlisted?: boolean; // Hide from listings but allow direct access via slug + docsSection?: boolean; // Include in docs navigation + docsSectionGroup?: string; // Sidebar group name in docs + docsSectionOrder?: number; // Order within group (lower = first) + docsSectionGroupOrder?: number; // Order of group itself (lower = first) + docsLanding?: boolean; // Use as /docs landing page } // Page frontmatter (for static pages like About, Projects, Contact) @@ -99,6 +109,11 @@ interface PageFrontmatter { contactForm?: boolean; // Enable contact form on this page newsletter?: boolean; // Override newsletter signup display (true/false) textAlign?: string; // Text alignment: "left", "center", "right" (default: "left") + docsSection?: boolean; // Include in docs navigation + docsSectionGroup?: string; // Sidebar group name in docs + docsSectionOrder?: number; // Order within group (lower = first) + docsSectionGroupOrder?: number; // Order of group itself (lower = first) + docsLanding?: boolean; // Use as /docs landing page } interface ParsedPage { @@ -124,6 +139,11 @@ interface ParsedPage { contactForm?: boolean; // Enable contact form on this page newsletter?: boolean; // Override newsletter signup display (true/false) textAlign?: string; // Text alignment: "left", "center", "right" (default: "left") + docsSection?: boolean; // Include in docs navigation + docsSectionGroup?: string; // Sidebar group name in docs + docsSectionOrder?: number; // Order within group (lower = first) + docsSectionGroupOrder?: number; // Order of group itself (lower = first) + docsLanding?: boolean; // Use as /docs landing page } // Calculate reading time based on word count @@ -174,6 +194,11 @@ function parseMarkdownFile(filePath: string): ParsedPost | null { newsletter: frontmatter.newsletter, // Override newsletter signup display contactForm: frontmatter.contactForm, // Enable contact form on this post unlisted: frontmatter.unlisted, // Hide from listings but allow direct access + docsSection: frontmatter.docsSection, // Include in docs navigation + docsSectionGroup: frontmatter.docsSectionGroup, // Sidebar group name + docsSectionOrder: frontmatter.docsSectionOrder, // Order within group + docsSectionGroupOrder: frontmatter.docsSectionGroupOrder, // Order of group itself + docsLanding: frontmatter.docsLanding, // Use as docs landing page }; } catch (error) { console.error(`Error parsing ${filePath}:`, error); @@ -234,6 +259,11 @@ function parsePageFile(filePath: string): ParsedPage | null { contactForm: frontmatter.contactForm, // Enable contact form on this page newsletter: frontmatter.newsletter, // Override newsletter signup display textAlign: frontmatter.textAlign, // Text alignment: "left", "center", "right" + docsSection: frontmatter.docsSection, // Include in docs navigation + docsSectionGroup: frontmatter.docsSectionGroup, // Sidebar group name + docsSectionOrder: frontmatter.docsSectionOrder, // Order within group + docsSectionGroupOrder: frontmatter.docsSectionGroupOrder, // Order of group itself + docsLanding: frontmatter.docsLanding, // Use as docs landing page }; } catch (error) { console.error(`Error parsing page ${filePath}:`, error); diff --git a/src/App.tsx b/src/App.tsx index ad51d8a..533b082 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import Home from "./pages/Home"; import Post from "./pages/Post"; import Stats from "./pages/Stats"; import Blog from "./pages/Blog"; +import DocsPage from "./pages/DocsPage"; import Write from "./pages/Write"; import TagPage from "./pages/TagPage"; import AuthorPage from "./pages/AuthorPage"; @@ -86,6 +87,13 @@ function App() { {siteConfig.blogPage.enabled && ( } /> )} + {/* Docs page route - only enabled when docsSection.enabled is true */} + {siteConfig.docsSection?.enabled && ( + } + /> + )} {/* Tag page route - displays posts filtered by tag */} } /> {/* Author page route - displays posts by a specific author */} diff --git a/src/components/BlogPost.tsx b/src/components/BlogPost.tsx index 1bcd322..e5d7019 100644 --- a/src/components/BlogPost.tsx +++ b/src/components/BlogPost.tsx @@ -34,11 +34,17 @@ const sanitizeSchema = { div: ["style"], // Allow inline styles on div for grid layouts p: ["style"], // Allow inline styles on p elements a: ["style", "href", "target", "rel"], // Allow inline styles on links - img: [ - ...(defaultSchema.attributes?.img || []), + img: [...(defaultSchema.attributes?.img || []), "style"], // Allow inline styles on images + iframe: [ + "src", + "width", + "height", + "allow", + "allowfullscreen", + "frameborder", + "title", "style", - ], // Allow inline styles on images - iframe: ["src", "width", "height", "allow", "allowfullscreen", "frameborder", "title", "style"], // Allow iframe with specific attributes + ], // Allow iframe with specific attributes }, }; @@ -350,20 +356,26 @@ function stripHtmlComments(content: string): string { newsletter: "___NEWSLETTER_PLACEHOLDER___", contactform: "___CONTACTFORM_PLACEHOLDER___", }; - + let processed = content; - + // Replace special placeholders with markers - processed = processed.replace(//gi, markers.newsletter); - processed = processed.replace(//gi, markers.contactform); - + processed = processed.replace( + //gi, + markers.newsletter, + ); + processed = processed.replace( + //gi, + markers.contactform, + ); + // Remove all remaining HTML comments (including multi-line) processed = processed.replace(//g, ""); - + // Restore special placeholders processed = processed.replace(markers.newsletter, ""); processed = processed.replace(markers.contactform, ""); - + return processed; } @@ -371,13 +383,13 @@ function stripHtmlComments(content: string): string { // Supports: and function parseContentForEmbeds(content: string): ContentSegment[] { const segments: ContentSegment[] = []; - + // Pattern matches or (case insensitive) const pattern = //gi; - + let lastIndex = 0; let match: RegExpExecArray | null; - + while ((match = pattern.exec(content)) !== null) { // Add content before the placeholder if (match.index > lastIndex) { @@ -386,7 +398,7 @@ function parseContentForEmbeds(content: string): ContentSegment[] { segments.push({ type: "content", value: textBefore }); } } - + // Add the embed placeholder const embedType = match[1].toLowerCase(); if (embedType === "newsletter") { @@ -394,10 +406,10 @@ function parseContentForEmbeds(content: string): ContentSegment[] { } else if (embedType === "contactform") { segments.push({ type: "contactform" }); } - + lastIndex = match.index + match[0].length; } - + // Add remaining content after last placeholder if (lastIndex < content.length) { const remaining = content.slice(lastIndex); @@ -405,12 +417,12 @@ function parseContentForEmbeds(content: string): ContentSegment[] { segments.push({ type: "content", value: remaining }); } } - + // If no placeholders found, return single content segment if (segments.length === 0) { segments.push({ type: "content", value: content }); } - + return segments; } @@ -459,9 +471,16 @@ function HeadingAnchor({ id }: { id: string }) { ); } -export default function BlogPost({ content, slug, pageType = "post" }: BlogPostProps) { +export default function BlogPost({ + content, + slug, + pageType = "post", +}: BlogPostProps) { const { theme } = useTheme(); - const [lightboxImage, setLightboxImage] = useState<{ src: string; alt: string } | null>(null); + const [lightboxImage, setLightboxImage] = useState<{ + src: string; + alt: string; + } | null>(null); const isLightboxEnabled = siteConfig.imageLightbox?.enabled !== false; const getCodeTheme = () => { @@ -479,7 +498,7 @@ export default function BlogPost({ content, slug, pageType = "post" }: BlogPostP // Strip HTML comments (except special placeholders) before processing const cleanedContent = stripHtmlComments(content); - + // Parse content for inline embeds const segments = parseContentForEmbeds(cleanedContent); const hasInlineEmbeds = segments.some((s) => s.type !== "content"); @@ -492,21 +511,25 @@ export default function BlogPost({ content, slug, pageType = "post" }: BlogPostP rehypePlugins={[rehypeRaw, [rehypeSanitize, sanitizeSchema]]} components={{ code(codeProps) { - const { className, children, node, style, ...restProps } = codeProps as { - className?: string; - children?: React.ReactNode; - node?: { tagName?: string; properties?: { className?: string[] } }; - style?: React.CSSProperties; - inline?: boolean; - }; + const { className, children, node, style, ...restProps } = + codeProps as { + className?: string; + children?: React.ReactNode; + node?: { + tagName?: string; + properties?: { className?: string[] }; + }; + style?: React.CSSProperties; + inline?: boolean; + }; const match = /language-(\w+)/.exec(className || ""); - + // Detect inline code: no language class AND content is short without newlines const codeContent = String(children); - const hasNewlines = codeContent.includes('\n'); + const hasNewlines = codeContent.includes("\n"); const isShort = codeContent.length < 80; const hasLanguage = !!match || !!className; - + // It's inline only if: no language, short content, no newlines const isInline = !hasLanguage && isShort && !hasNewlines; @@ -521,16 +544,20 @@ export default function BlogPost({ content, slug, pageType = "post" }: BlogPostP const codeString = String(children).replace(/\n$/, ""); const language = match ? match[1] : "text"; const isTextBlock = language === "text"; - + // Custom styles for text blocks to enable wrapping - const textBlockStyle = isTextBlock ? { - whiteSpace: "pre-wrap" as const, - wordWrap: "break-word" as const, - overflowWrap: "break-word" as const, - } : {}; - + const textBlockStyle = isTextBlock + ? { + whiteSpace: "pre-wrap" as const, + wordWrap: "break-word" as const, + overflowWrap: "break-word" as const, + } + : {}; + return ( -
+
{match && {match[1]}} {codeString} @@ -681,7 +710,7 @@ export default function BlogPost({ content, slug, pageType = "post" }: BlogPostP const url = new URL(src); const isAllowed = ALLOWED_IFRAME_DOMAINS.some( (domain) => - url.hostname === domain || url.hostname.endsWith("." + domain) + url.hostname === domain || url.hostname.endsWith("." + domain), ); if (!isAllowed) return null; @@ -727,10 +756,7 @@ export default function BlogPost({ content, slug, pageType = "post" }: BlogPostP if (segment.type === "contactform") { // Contact form inline return siteConfig.contactForm?.enabled ? ( - + ) : null; } // Markdown content segment @@ -756,214 +782,227 @@ export default function BlogPost({ content, slug, pageType = "post" }: BlogPostP remarkPlugins={[remarkGfm, remarkBreaks]} rehypePlugins={[rehypeRaw, [rehypeSanitize, sanitizeSchema]]} components={{ - code(codeProps) { - const { className, children, node, style, ...restProps } = codeProps as { - className?: string; - children?: React.ReactNode; - node?: { tagName?: string; properties?: { className?: string[] } }; - style?: React.CSSProperties; - inline?: boolean; - }; - const match = /language-(\w+)/.exec(className || ""); - - // Detect inline code: no language class AND content is short without newlines - // Fenced code blocks (even without language) are longer or have structure - const codeContent = String(children); - const hasNewlines = codeContent.includes('\n'); - const isShort = codeContent.length < 80; - const hasLanguage = !!match || !!className; - - // It's inline only if: no language, short content, no newlines - const isInline = !hasLanguage && isShort && !hasNewlines; + code(codeProps) { + const { className, children, node, style, ...restProps } = + codeProps as { + className?: string; + children?: React.ReactNode; + node?: { + tagName?: string; + properties?: { className?: string[] }; + }; + style?: React.CSSProperties; + inline?: boolean; + }; + const match = /language-(\w+)/.exec(className || ""); - if (isInline) { - return ( - - {children} - - ); - } + // Detect inline code: no language class AND content is short without newlines + // Fenced code blocks (even without language) are longer or have structure + const codeContent = String(children); + const hasNewlines = codeContent.includes("\n"); + const isShort = codeContent.length < 80; + const hasLanguage = !!match || !!className; - const codeString = String(children).replace(/\n$/, ""); - const language = match ? match[1] : "text"; - const isTextBlock = language === "text"; - - // Custom styles for text blocks to enable wrapping - const textBlockStyle = isTextBlock ? { - whiteSpace: "pre-wrap" as const, - wordWrap: "break-word" as const, - overflowWrap: "break-word" as const, - } : {}; - - return ( -
- {match && {match[1]}} - - - {codeString} - -
- ); - }, - img({ src, alt }) { - const handleImageClick = () => { - if (isLightboxEnabled && src) { - setLightboxImage({ src, alt: alt || "" }); + // It's inline only if: no language, short content, no newlines + const isInline = !hasLanguage && isShort && !hasNewlines; + + if (isInline) { + return ( + + {children} + + ); } - }; - return ( - - {alt - {alt && {alt}} - - ); - }, - a({ href, children }) { - const isExternal = href?.startsWith("http"); - return ( - - {children} - - ); - }, - blockquote({ children }) { - return ( -
{children}
- ); - }, - h1({ children }) { - const id = generateSlug(getTextContent(children)); - return ( -

- - {children} -

- ); - }, - h2({ children }) { - const id = generateSlug(getTextContent(children)); - return ( -

- - {children} -

- ); - }, - h3({ children }) { - const id = generateSlug(getTextContent(children)); - return ( -

- - {children} -

- ); - }, - h4({ children }) { - const id = generateSlug(getTextContent(children)); - return ( -

- - {children} -

- ); - }, - h5({ children }) { - const id = generateSlug(getTextContent(children)); - return ( -
- - {children} -
- ); - }, - h6({ children }) { - const id = generateSlug(getTextContent(children)); - return ( -
- - {children} -
- ); - }, - ul({ children }) { - return
    {children}
; - }, - ol({ children }) { - return
    {children}
; - }, - li({ children }) { - return
  • {children}
  • ; - }, - hr() { - return
    ; - }, - // Table components for GitHub-style tables - table({ children }) { - return ( -
    - {children}
    -
    - ); - }, - thead({ children }) { - return {children}; - }, - tbody({ children }) { - return {children}; - }, - tr({ children }) { - return {children}; - }, - th({ children }) { - return {children}; - }, - td({ children }) { - return {children}; - }, - // Iframe component with domain whitelisting for YouTube and Twitter/X - iframe(props) { - const src = props.src as string; - if (!src) return null; - try { - const url = new URL(src); - const isAllowed = ALLOWED_IFRAME_DOMAINS.some( - (domain) => - url.hostname === domain || url.hostname.endsWith("." + domain) - ); - if (!isAllowed) return null; + const codeString = String(children).replace(/\n$/, ""); + const language = match ? match[1] : "text"; + const isTextBlock = language === "text"; + + // Custom styles for text blocks to enable wrapping + const textBlockStyle = isTextBlock + ? { + whiteSpace: "pre-wrap" as const, + wordWrap: "break-word" as const, + overflowWrap: "break-word" as const, + } + : {}; return ( -
    -