mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-11 20:08:57 +00:00
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:
293
README.md
293
README.md
@@ -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
|
||||

|
||||
```
|
||||
|
||||
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 (``)
|
||||
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).
|
||||
|
||||
2
TASK.md
2
TASK.md
@@ -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
|
||||
|
||||
|
||||
28
changelog.md
28
changelog.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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"])
|
||||
|
||||
2
files.md
2
files.md
@@ -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) |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user