feat: add sidebar layout support for blog posts

- Add layout field to posts schema and frontmatter
- Enable docs-style sidebar layout for posts (previously pages only)
- Update Post.tsx to handle sidebar for both posts and pages
- Add layout field to Write.tsx frontmatter reference
- Update documentation in docs.md, setup-guide.md, how-to-publish.md
- Add documentation links to README.md
This commit is contained in:
Wayne Sutton
2025-12-23 00:57:21 -08:00
parent edb7fc6723
commit 0342539aac
18 changed files with 244 additions and 338 deletions

293
README.md
View File

@@ -16,6 +16,12 @@ npm run sync # dev
npm run sync:prod # production
```
## Documentation
- **[Setup Guide](content/blog/setup-guide.md)** - Complete fork and deployment guide
- **[Configuration Guide](content/blog/fork-configuration-guide.md)** - Automated or manual fork setup
- **[Full Documentation](content/pages/docs.md)** - Reference for all features and configuration
## Fork Configuration
After forking this project, you have two options to configure your site:
@@ -41,24 +47,6 @@ This updates all 11 configuration files in one step.
Follow the step-by-step guide in `FORK_CONFIG.md` to update each file manually. This guide includes code snippets and an AI agent prompt for assistance.
### Files Updated
| File | What to update |
| ----------------------------------- | --------------------------------------------------------------------------- |
| `src/config/siteConfig.ts` | Site name, title, intro, bio, blog page, logo gallery, GitHub contributions |
| `src/pages/Home.tsx` | Intro paragraph text, footer links |
| `convex/http.ts` | `SITE_URL`, `SITE_NAME`, description strings |
| `convex/rss.ts` | `SITE_URL`, `SITE_TITLE`, `SITE_DESCRIPTION` |
| `src/pages/Post.tsx` | `SITE_URL`, `SITE_NAME`, `DEFAULT_OG_IMAGE` |
| `index.html` | Title, meta description, OG tags, JSON-LD |
| `public/llms.txt` | Site name, URL, description, topics |
| `public/robots.txt` | Sitemap URL and header comment |
| `public/openapi.yaml` | API title, server URL, site name |
| `public/.well-known/ai-plugin.json` | Site name, descriptions |
| `src/context/ThemeContext.tsx` | Default theme |
See `FORK_CONFIG.md` for detailed configuration examples and the full JSON schema.
## Features
- Markdown-based blog posts with frontmatter
@@ -169,130 +157,6 @@ excerpt: "Short text for featured cards"
Your markdown content here...
```
## Images
### Open Graph Images
Add an `image` field to frontmatter for social media previews:
```yaml
image: "/images/my-header.png"
```
Recommended dimensions: 1200x630 pixels. Images can be local (`/images/...`) or external URLs.
### Inline Images
Add images in markdown content:
```markdown
![Alt text description](/images/screenshot.png)
```
Place image files in `public/images/`. The alt text displays as a caption.
### Image deployment
Images are served as static files from your git repository, not synced to Convex. After adding images:
1. Add image files to `public/images/`
2. Reference in frontmatter (`image: "/images/my-image.png"`) or markdown (`![Alt](/images/my-image.png)`)
3. Commit and push to git
4. Netlify rebuilds and serves the images
The `npm run sync` command only syncs markdown text content. Images require a full deploy via git push.
### Site Logo
Edit `src/pages/Home.tsx` to set your site logo:
```typescript
const siteConfig = {
logo: "/images/logo.svg", // Set to null to hide
// ...
};
```
Replace `public/images/logo.svg` with your own logo file.
## Featured Section
Posts and pages with `featured: true` in frontmatter appear in the featured section.
### Add to Featured
Add these fields to any post or page frontmatter:
```yaml
featured: true
featuredOrder: 1
excerpt: "A short description for the card view."
image: "/images/thumbnail.png"
```
Then run `npm run sync` (dev) or `npm run sync:prod` (production). No redeploy needed.
| Field | Description |
| --------------- | ----------------------------------------- |
| `featured` | Set `true` to show in featured section |
| `featuredOrder` | Order in featured section (lower = first) |
| `excerpt` | Short description for card view |
| `image` | Thumbnail for card view (displays square) |
| `authorName` | Author display name shown next to date |
| `authorImage` | Round author avatar image URL |
### Display Modes
The featured section supports two display modes:
- **List view** (default): Bullet list of links
- **Card view**: Grid of cards with thumbnail, title, and excerpt
Users can toggle between views. To change the default:
```typescript
const siteConfig = {
featuredViewMode: "cards", // 'list' or 'cards'
showViewToggle: true, // Allow users to switch views
};
```
### Thumbnail Images
In card view, the `image` field displays as a square thumbnail above the title. Non-square images are automatically cropped to center. The list view shows links only (no images).
Square thumbnails: 400x400px minimum (800x800px for retina). The same image can serve as both the OG image for social sharing and the featured card thumbnail.
## Blog Page
The site supports a dedicated blog page at `/blog`. Configure in `src/config/siteConfig.ts`:
```typescript
blogPage: {
enabled: true, // Enable /blog route
showInNav: true, // Show in navigation
title: "Blog", // Nav link and page title
order: 0, // Nav order (lower = first)
},
displayOnHomepage: true, // Show posts on homepage
```
| Option | Description |
| ------------------- | -------------------------------------- |
| `enabled` | Enable the `/blog` route |
| `showInNav` | Show Blog link in navigation |
| `title` | Text for nav link and page heading |
| `order` | Position in navigation (lower = first) |
| `displayOnHomepage` | Show post list on homepage |
**Display options:**
- Homepage only: `displayOnHomepage: true`, `blogPage.enabled: false`
- Blog page only: `displayOnHomepage: false`, `blogPage.enabled: true`
- Both: `displayOnHomepage: true`, `blogPage.enabled: true`
**Navigation order:** The Blog link merges with page links and sorts by order. Pages use the `order` field in frontmatter. Set `blogPage.order: 5` to position Blog after pages with order 0-4.
## Logo Gallery
The homepage includes a scrolling logo gallery with sample logos. Configure in `siteConfig`:
@@ -306,35 +170,6 @@ logoGallery: {
},
```
### Replace with your own logos
1. Add logo images to `public/images/logos/` (SVG recommended)
2. Update the images array with logos and links:
```typescript
logoGallery: {
enabled: true,
images: [
{ src: "/images/logos/your-logo-1.svg", href: "https://example.com" },
{ src: "/images/logos/your-logo-2.svg", href: "https://anothersite.com" },
],
position: "above-footer", // or "below-featured"
speed: 30, // Seconds for one scroll cycle
title: "Trusted by", // Set to undefined to hide
},
```
Each logo object supports:
- `src`: Path to the logo image (required)
- `href`: URL to link to when clicked (optional)
### Remove sample logos
Delete sample files from `public/images/logos/` and replace the images array with your own logos, or set `enabled: false` to hide the gallery entirely.
The gallery uses CSS animations for smooth infinite scrolling. Logos appear grayscale and colorize on hover.
## GitHub Contributions Graph
Display your GitHub contribution activity on the homepage. Configure in `src/config/siteConfig.ts`:
@@ -349,23 +184,6 @@ gitHubContributions: {
},
```
| Option | Description |
| -------------------- | --------------------------------------------- |
| `enabled` | `true` to show, `false` to hide |
| `username` | Your GitHub username |
| `showYearNavigation` | Show prev/next year navigation buttons |
| `linkToProfile` | Click graph to visit GitHub profile |
| `title` | Text above graph (set to `undefined` to hide) |
The graph displays with theme-aware colors that match each site theme:
- **Dark**: GitHub green on dark background
- **Light**: Standard GitHub green
- **Tan**: Warm brown tones
- **Cloud**: Gray-blue tones
Uses the public `github-contributions-api.jogruber.de` API (no GitHub token required).
## Visitor Map
Display real-time visitor locations on a world map on the stats page. Uses Netlify's built-in geo detection (no third-party API needed). Privacy friendly: only stores city, country, and coordinates. No IP addresses stored.
@@ -379,27 +197,6 @@ visitorMap: {
},
```
| Option | Description |
| --------- | ------------------------------------------- |
| `enabled` | `true` to show, `false` to hide |
| `title` | Text above map (set to `undefined` to hide) |
The map displays with theme-aware colors. Visitor dots pulse to indicate live sessions. Location data comes from Netlify's automatic geo headers at the edge.
### Favicon
Replace `public/favicon.svg` with your own icon. The default is a rounded square with the letter "m". Edit the SVG to change the letter or style.
### Default Open Graph Image
The default OG image is used when posts do not have an `image` field. Replace `public/images/og-default.svg` with your own image (1200x630 recommended).
Update the reference in `src/pages/Post.tsx`:
```typescript
const DEFAULT_OG_IMAGE = "/images/og-default.svg";
```
## Syncing Posts
Posts are synced to Convex. The sync script reads markdown files from `content/blog/` and `content/pages/`, then uploads them to your Convex database.
@@ -602,25 +399,6 @@ FIRECRAWL_API_KEY=fc-your-api-key
Imported posts are created as drafts (`published: false`). Review, edit, set `published: true`, then sync.
## How Blog Post Slugs Work
Slugs are defined in the frontmatter of each markdown file:
```markdown
---
slug: "my-post-slug"
---
```
The slug becomes the URL path: `yourdomain.com/my-post-slug`
Rules:
- Slugs must be unique across all posts
- Use lowercase letters, numbers, and hyphens
- The sync script reads the `slug` field from frontmatter
- Posts are queried by slug using a Convex index
## Theme Configuration
The default theme is Tan. Users can cycle through themes using the toggle:
@@ -640,67 +418,22 @@ const DEFAULT_THEME: Theme = "tan"; // Change to "dark", "light", or "cloud"
The blog uses a serif font (New York) by default. To switch fonts, edit `src/styles/global.css`:
```css
body {
/* Sans-serif option */
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
/* Serif option (default) */
font-family:
"New York",
-apple-system-ui-serif,
ui-serif,
Georgia,
Cambria,
"Times New Roman",
Times,
serif;
}
```
Replace the `font-family` property with your preferred font stack.
### Font Sizes
All font sizes use CSS variables defined in `:root`. Customize sizes by editing the variables:
```css
:root {
/* Base size scale */
--font-size-base: 16px;
--font-size-sm: 13px;
--font-size-lg: 17px;
--font-size-xl: 18px;
--font-size-2xl: 20px;
--font-size-3xl: 24px;
/* Component-specific (examples) */
--font-size-blog-content: 17px;
--font-size-post-title: 32px;
--font-size-nav-link: 14px;
}
```
Mobile responsive sizes are defined in a `@media (max-width: 768px)` block with smaller values.
## Write Page
A public markdown writing page at `/write` (not linked in navigation). Features:
- Three-column Cursor docs-style layout
- Content type selector (Blog Post or Page) with dynamic frontmatter templates
- Frontmatter reference panel with copy buttons for each field
- Font switcher (Serif/Sans-serif) with localStorage persistence
- Theme toggle matching the site themes (Moon, Sun, Half2Icon, Cloud)
- Word, line, and character counts
- localStorage persistence for content, content type, and font preference
- Works with Grammarly and browser spellcheck
- Warning message about refresh losing content
A public markdown writing page at `/write` (not linked in navigation).
Access directly at `yourdomain.com/write`. Content is stored in localStorage only (not synced to database). Use it to draft posts, then copy the content to a markdown file in `content/blog/` or `content/pages/` and run `npm run sync`.
## Source
Fork this project: [github.com/waynesutton/markdown-site](https://github.com/waynesutton/markdown-site)
## License
This project is licensed under the [MIT License](https://github.com/waynesutton/markdown-site/blob/main/LICENSE).
You are free to use, modify, and distribute this project in accordance with the [MIT license terms](https://opensource.org/licenses/MIT).

View File

@@ -15,7 +15,7 @@
## Current Status
v1.21.0 deployed. Blog page view mode toggle with list and card views.
v1.24.0 deployed. Sidebar layout support for blog posts.
## Completed

View File

@@ -4,6 +4,34 @@ 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/).
## [1.24.0] - 2025-12-23
### Added
- Sidebar layout support for blog posts
- Blog posts can now use `layout: "sidebar"` frontmatter field (previously only available for pages)
- Enables docs-style layout with table of contents sidebar for long-form posts
- Same features as page sidebar: automatic TOC extraction, active heading highlighting, smooth scroll navigation
- Mobile responsive: stacks to single column below 1024px
### Changed
- Updated `Post.tsx` to handle sidebar layout for both posts and pages
- Updated `Write.tsx` to include `layout` field in blog post frontmatter reference
### Technical
- Updated `convex/schema.ts`: Added optional `layout` field to posts table
- Updated `scripts/sync-posts.ts`: Parses `layout` field from post frontmatter
- Updated `convex/posts.ts`: Includes `layout` field in queries, mutations, and sync operations
- Reuses existing sidebar components and CSS (no new components needed)
### Documentation
- Updated `docs.md`: Added `layout` field to blog posts frontmatter table, updated sidebar layout section
- Updated `setup-guide.md`: Clarified sidebar layout works for both posts and pages
- Updated `how-to-publish.md`: Added `layout` field to frontmatter reference table
## [1.23.0] - 2025-12-23
### Added

View File

@@ -88,6 +88,7 @@ readTime: "5 min read"
| `featured` | No | Set `true` to show in featured section |
| `featuredOrder` | No | Order in featured section (lower first) |
| `excerpt` | No | Short description for card view |
| `layout` | No | Set to `"sidebar"` for docs-style layout with TOC |
## Write Your Content

View File

@@ -8,6 +8,7 @@ tags: ["convex", "netlify", "tutorial", "deployment"]
readTime: "8 min read"
featured: true
featuredOrder: 6
layout: "sidebar"
image: "/images/setupguide.png"
authorName: "Markdown"
authorImage: "/images/authors/markdown.png"
@@ -915,21 +916,21 @@ order: 1
Your page content here...
```
| Field | Required | Description |
| ------------- | -------- | -------------------------------------- |
| `title` | Yes | Page title (shown in nav) |
| `slug` | Yes | URL path (e.g., `/about`) |
| `published` | Yes | Set `true` to show |
| `order` | No | Display order (lower = first) |
| `authorName` | No | Author display name shown next to date |
| `authorImage` | No | Round author avatar image URL |
| Field | Required | Description |
| ------------- | -------- | ------------------------------------------------- |
| `title` | Yes | Page title (shown in nav) |
| `slug` | Yes | URL path (e.g., `/about`) |
| `published` | Yes | Set `true` to show |
| `order` | No | Display order (lower = first) |
| `authorName` | No | Author display name shown next to date |
| `authorImage` | No | Round author avatar image URL |
| `layout` | No | Set to `"sidebar"` for docs-style layout with TOC |
3. Run `npm run sync` to sync pages
Pages appear automatically in the navigation when published.
**Sidebar layout:** Add `layout: "sidebar"` to any page frontmatter to enable a docs-style layout with a table of contents sidebar. The sidebar extracts headings (H1, H2, H3) automatically and provides smooth scroll navigation. Only appears if headings exist in the page content.
**Sidebar layout:** Add `layout: "sidebar"` to any post or page frontmatter to enable a docs-style layout with a table of contents sidebar. The sidebar extracts headings (H1, H2, H3) automatically and provides smooth scroll navigation. Only appears if headings exist in the content.
### Update SEO Meta Tags

View File

@@ -8,6 +8,38 @@ layout: "sidebar"
All notable changes to this project.
## v1.24.0
Released December 23, 2025
**Sidebar layout for blog posts**
- Blog posts now support `layout: "sidebar"` frontmatter field
- Previously only available for static pages, now works for posts too
- Enables docs-style layout with table of contents sidebar for long-form content
- Same features as page sidebar: automatic TOC extraction, active heading highlighting, smooth scroll navigation
- Mobile responsive: stacks to single column below 1024px
Add `layout: "sidebar"` to any blog post frontmatter to enable the sidebar layout. The sidebar extracts headings (H1, H2, H3) automatically and only appears if headings exist in the content.
Example:
```yaml
---
title: "My Tutorial"
description: "A detailed guide"
date: "2025-01-20"
slug: "my-tutorial"
published: true
tags: ["tutorial"]
layout: "sidebar"
---
```
Updated files: `convex/schema.ts`, `scripts/sync-posts.ts`, `convex/posts.ts`, `src/pages/Post.tsx`, `src/pages/Write.tsx`
Documentation updated: `docs.md`, `setup-guide.md`, `how-to-publish.md`
## v1.23.0
Released December 23, 2025

View File

@@ -83,21 +83,22 @@ image: "/images/og-image.png"
Content here...
```
| Field | Required | Description |
| --------------- | -------- | -------------------------------------- |
| `title` | Yes | Post title |
| `description` | Yes | SEO description |
| `date` | Yes | YYYY-MM-DD format |
| `slug` | Yes | URL path (unique) |
| `published` | Yes | `true` to show |
| `tags` | Yes | Array of strings |
| `readTime` | No | Display time estimate |
| `image` | No | OG image and featured card thumbnail |
| `excerpt` | No | Short text for card view |
| `featured` | No | `true` to show in featured section |
| `featuredOrder` | No | Order in featured (lower = first) |
| `authorName` | No | Author display name shown next to date |
| `authorImage` | No | Round author avatar image URL |
| Field | Required | Description |
| --------------- | -------- | ------------------------------------------------- |
| `title` | Yes | Post title |
| `description` | Yes | SEO description |
| `date` | Yes | YYYY-MM-DD format |
| `slug` | Yes | URL path (unique) |
| `published` | Yes | `true` to show |
| `tags` | Yes | Array of strings |
| `readTime` | No | Display time estimate |
| `image` | No | OG image and featured card thumbnail |
| `excerpt` | No | Short text for card view |
| `featured` | No | `true` to show in featured section |
| `featuredOrder` | No | Order in featured (lower = first) |
| `authorName` | No | Author display name shown next to date |
| `authorImage` | No | Round author avatar image URL |
| `layout` | No | Set to `"sidebar"` for docs-style layout with TOC |
### Static pages
@@ -130,7 +131,7 @@ Content here...
### Sidebar layout
Pages can use a docs-style layout with a table of contents sidebar. Add `layout: "sidebar"` to the page frontmatter:
Posts and pages can use a docs-style layout with a table of contents sidebar. Add `layout: "sidebar"` to the frontmatter:
```markdown
---
@@ -153,13 +154,36 @@ layout: "sidebar"
- Left sidebar displays table of contents extracted from H1, H2, H3 headings
- Two-column layout: 220px sidebar + flexible content area
- Sidebar only appears if headings exist in the page content
- Sidebar only appears if headings exist in the content
- Active heading highlighting as you scroll
- Smooth scroll navigation when clicking TOC links
- Mobile responsive: stacks to single column below 1024px
- Works for both blog posts and static pages
The sidebar extracts headings automatically from your markdown content. No manual TOC needed.
**Example for blog post:**
```markdown
---
title: "My Tutorial"
description: "A detailed guide"
date: "2025-01-20"
slug: "my-tutorial"
published: true
tags: ["tutorial"]
layout: "sidebar"
---
# Introduction
## Getting Started
### Prerequisites
## Advanced Topics
```
### How frontmatter works
Frontmatter is the YAML metadata at the top of each markdown file between `---` markers. Here is how it flows through the system:

View File

@@ -119,6 +119,7 @@ export const getPostBySlug = query({
featuredOrder: v.optional(v.number()),
authorName: v.optional(v.string()),
authorImage: v.optional(v.string()),
layout: v.optional(v.string()),
}),
v.null(),
),
@@ -149,6 +150,7 @@ export const getPostBySlug = query({
featuredOrder: post.featuredOrder,
authorName: post.authorName,
authorImage: post.authorImage,
layout: post.layout,
};
},
});
@@ -172,6 +174,7 @@ export const syncPosts = internalMutation({
featuredOrder: v.optional(v.number()),
authorName: v.optional(v.string()),
authorImage: v.optional(v.string()),
layout: v.optional(v.string()),
}),
),
},
@@ -212,6 +215,7 @@ export const syncPosts = internalMutation({
featuredOrder: post.featuredOrder,
authorName: post.authorName,
authorImage: post.authorImage,
layout: post.layout,
lastSyncedAt: now,
});
updated++;
@@ -256,6 +260,7 @@ export const syncPostsPublic = mutation({
featuredOrder: v.optional(v.number()),
authorName: v.optional(v.string()),
authorImage: v.optional(v.string()),
layout: v.optional(v.string()),
}),
),
},
@@ -296,6 +301,7 @@ export const syncPostsPublic = mutation({
featuredOrder: post.featuredOrder,
authorName: post.authorName,
authorImage: post.authorImage,
layout: post.layout,
lastSyncedAt: now,
});
updated++;

View File

@@ -18,6 +18,7 @@ export default defineSchema({
featuredOrder: v.optional(v.number()), // Order in featured section (lower = first)
authorName: v.optional(v.string()), // Author display name
authorImage: v.optional(v.string()), // Author avatar image URL (round)
layout: v.optional(v.string()), // Layout type: "sidebar" for docs-style layout
lastSyncedAt: v.number(),
})
.index("by_slug", ["slug"])

View File

@@ -41,7 +41,7 @@ A brief description of each file in the codebase.
| ----------- | ----------------------------------------------------------------- |
| `Home.tsx` | Landing page with featured content and optional post list |
| `Blog.tsx` | Dedicated blog page with post list or card grid view (configurable via siteConfig.blogPage, supports view toggle) |
| `Post.tsx` | Individual blog post view (update SITE_URL/SITE_NAME when forking) |
| `Post.tsx` | Individual blog post or page view with optional sidebar layout (update SITE_URL/SITE_NAME when forking) |
| `Stats.tsx` | Real-time analytics dashboard with visitor stats and GitHub stars |
| `Write.tsx` | Three-column markdown writing page with Cursor docs-style UI, frontmatter reference with copy buttons, theme toggle, font switcher (serif/sans-serif), and localStorage persistence (not linked in nav) |

View File

@@ -7,6 +7,38 @@ Date: 2025-12-23
All notable changes to this project.
## v1.24.0
Released December 23, 2025
**Sidebar layout for blog posts**
- Blog posts now support `layout: "sidebar"` frontmatter field
- Previously only available for static pages, now works for posts too
- Enables docs-style layout with table of contents sidebar for long-form content
- Same features as page sidebar: automatic TOC extraction, active heading highlighting, smooth scroll navigation
- Mobile responsive: stacks to single column below 1024px
Add `layout: "sidebar"` to any blog post frontmatter to enable the sidebar layout. The sidebar extracts headings (H1, H2, H3) automatically and only appears if headings exist in the content.
Example:
```yaml
---
title: "My Tutorial"
description: "A detailed guide"
date: "2025-01-20"
slug: "my-tutorial"
published: true
tags: ["tutorial"]
layout: "sidebar"
---
```
Updated files: `convex/schema.ts`, `scripts/sync-posts.ts`, `convex/posts.ts`, `src/pages/Post.tsx`, `src/pages/Write.tsx`
Documentation updated: `docs.md`, `setup-guide.md`, `how-to-publish.md`
## v1.23.0
Released December 23, 2025

View File

@@ -82,21 +82,22 @@ image: "/images/og-image.png"
Content here...
```
| Field | Required | Description |
| --------------- | -------- | -------------------------------------- |
| `title` | Yes | Post title |
| `description` | Yes | SEO description |
| `date` | Yes | YYYY-MM-DD format |
| `slug` | Yes | URL path (unique) |
| `published` | Yes | `true` to show |
| `tags` | Yes | Array of strings |
| `readTime` | No | Display time estimate |
| `image` | No | OG image and featured card thumbnail |
| `excerpt` | No | Short text for card view |
| `featured` | No | `true` to show in featured section |
| `featuredOrder` | No | Order in featured (lower = first) |
| `authorName` | No | Author display name shown next to date |
| `authorImage` | No | Round author avatar image URL |
| Field | Required | Description |
| --------------- | -------- | ------------------------------------------------- |
| `title` | Yes | Post title |
| `description` | Yes | SEO description |
| `date` | Yes | YYYY-MM-DD format |
| `slug` | Yes | URL path (unique) |
| `published` | Yes | `true` to show |
| `tags` | Yes | Array of strings |
| `readTime` | No | Display time estimate |
| `image` | No | OG image and featured card thumbnail |
| `excerpt` | No | Short text for card view |
| `featured` | No | `true` to show in featured section |
| `featuredOrder` | No | Order in featured (lower = first) |
| `authorName` | No | Author display name shown next to date |
| `authorImage` | No | Round author avatar image URL |
| `layout` | No | Set to `"sidebar"` for docs-style layout with TOC |
### Static pages
@@ -129,7 +130,7 @@ Content here...
### Sidebar layout
Pages can use a docs-style layout with a table of contents sidebar. Add `layout: "sidebar"` to the page frontmatter:
Posts and pages can use a docs-style layout with a table of contents sidebar. Add `layout: "sidebar"` to the frontmatter:
```markdown
---
@@ -152,13 +153,36 @@ layout: "sidebar"
- Left sidebar displays table of contents extracted from H1, H2, H3 headings
- Two-column layout: 220px sidebar + flexible content area
- Sidebar only appears if headings exist in the page content
- Sidebar only appears if headings exist in the content
- Active heading highlighting as you scroll
- Smooth scroll navigation when clicking TOC links
- Mobile responsive: stacks to single column below 1024px
- Works for both blog posts and static pages
The sidebar extracts headings automatically from your markdown content. No manual TOC needed.
**Example for blog post:**
```markdown
---
title: "My Tutorial"
description: "A detailed guide"
date: "2025-01-20"
slug: "my-tutorial"
published: true
tags: ["tutorial"]
layout: "sidebar"
---
# Introduction
## Getting Started
### Prerequisites
## Advanced Topics
```
### How frontmatter works
Frontmatter is the YAML metadata at the top of each markdown file between `---` markers. Here is how it flows through the system:

View File

@@ -83,6 +83,7 @@ readTime: "5 min read"
| `featured` | No | Set `true` to show in featured section |
| `featuredOrder` | No | Order in featured section (lower first) |
| `excerpt` | No | Short description for card view |
| `layout` | No | Set to `"sidebar"` for docs-style layout with TOC |
## Write Your Content

View File

@@ -910,21 +910,21 @@ order: 1
Your page content here...
```
| Field | Required | Description |
| ------------- | -------- | -------------------------------------- |
| `title` | Yes | Page title (shown in nav) |
| `slug` | Yes | URL path (e.g., `/about`) |
| `published` | Yes | Set `true` to show |
| `order` | No | Display order (lower = first) |
| `authorName` | No | Author display name shown next to date |
| `authorImage` | No | Round author avatar image URL |
| Field | Required | Description |
| ------------- | -------- | ------------------------------------------------- |
| `title` | Yes | Page title (shown in nav) |
| `slug` | Yes | URL path (e.g., `/about`) |
| `published` | Yes | Set `true` to show |
| `order` | No | Display order (lower = first) |
| `authorName` | No | Author display name shown next to date |
| `authorImage` | No | Round author avatar image URL |
| `layout` | No | Set to `"sidebar"` for docs-style layout with TOC |
3. Run `npm run sync` to sync pages
Pages appear automatically in the navigation when published.
**Sidebar layout:** Add `layout: "sidebar"` to any page frontmatter to enable a docs-style layout with a table of contents sidebar. The sidebar extracts headings (H1, H2, H3) automatically and provides smooth scroll navigation. Only appears if headings exist in the page content.
**Sidebar layout:** Add `layout: "sidebar"` to any post or page frontmatter to enable a docs-style layout with a table of contents sidebar. The sidebar extracts headings (H1, H2, H3) automatically and provides smooth scroll navigation. Only appears if headings exist in the content.
### Update SEO Meta Tags

View File

@@ -36,6 +36,7 @@ interface PostFrontmatter {
featuredOrder?: number; // Order in featured section (lower = first)
authorName?: string; // Author display name
authorImage?: string; // Author avatar image URL (round)
layout?: string; // Layout type: "sidebar" for docs-style layout
}
interface ParsedPost {
@@ -53,6 +54,7 @@ interface ParsedPost {
featuredOrder?: number; // Order in featured section (lower = first)
authorName?: string; // Author display name
authorImage?: string; // Author avatar image URL (round)
layout?: string; // Layout type: "sidebar" for docs-style layout
}
// Page frontmatter (for static pages like About, Projects, Contact)
@@ -122,6 +124,7 @@ function parseMarkdownFile(filePath: string): ParsedPost | null {
featuredOrder: frontmatter.featuredOrder, // Order in featured section
authorName: frontmatter.authorName, // Author display name
authorImage: frontmatter.authorImage, // Author avatar image URL
layout: frontmatter.layout, // Layout type: "sidebar" for docs-style layout
};
} catch (error) {
console.error(`Error parsing ${filePath}:`, error);

View File

@@ -269,7 +269,13 @@ export default function Home() {
>
GitHub
</a>
.
. This project is licensed under the MIT{" "}
<a
href="https://github.com/waynesutton/markdown-site?tab=MIT-1-ov-file"
className="home-text-link"
>
License.
</a>{" "}
</p>
</section>
</div>

View File

@@ -236,10 +236,14 @@ export default function Post() {
);
};
// Extract headings for sidebar TOC (only for posts with layout: "sidebar")
const headings = post?.layout === "sidebar" ? extractHeadings(post.content) : [];
const hasSidebar = headings.length > 0;
// Render blog post with full metadata
return (
<div className="post-page">
<nav className="post-nav">
<div className={`post-page ${hasSidebar ? "post-page-with-sidebar" : ""}`}>
<nav className={`post-nav ${hasSidebar ? "post-nav-with-sidebar" : ""}`}>
<button onClick={() => navigate("/")} className="back-button">
<ArrowLeft size={16} />
<span>Back</span>
@@ -257,7 +261,15 @@ export default function Post() {
/>
</nav>
<article className="post-article">
<div className={hasSidebar ? "post-content-with-sidebar" : ""}>
{/* Left sidebar - TOC */}
{hasSidebar && (
<aside className="post-sidebar-wrapper post-sidebar-left">
<PageSidebar headings={headings} activeId={location.hash.slice(1)} />
</aside>
)}
<article className={`post-article ${hasSidebar ? "post-article-with-sidebar" : ""}`}>
<header className="post-header">
<h1 className="post-title">{post.title}</h1>
<div className="post-meta-header">
@@ -335,6 +347,7 @@ export default function Post() {
)}
</footer>
</article>
</div>
</div>
);
}

View File

@@ -37,6 +37,7 @@ const POST_FIELDS = [
{ name: "featuredOrder", required: false, example: "1" },
{ name: "authorName", required: false, example: '"Jane Doe"' },
{ name: "authorImage", required: false, example: '"/images/authors/jane.png"' },
{ name: "layout", required: false, example: '"sidebar"' },
];
// Frontmatter field definitions for pages