feat: raw markdown URLs, author display, GitHub Stars, and frontmatter docs
v1.18.1 - CopyPageDropdown raw markdown URLs
- AI services (ChatGPT, Claude, Perplexity) now receive /raw/{slug}.md URLs
- Direct access to clean markdown content for better AI parsing
- No HTML parsing required by AI services
- Renamed buildUrlFromPageUrl to buildUrlFromRawMarkdown
v1.19.0 - Author display for posts and pages
- New optional authorName and authorImage frontmatter fields
- Round avatar image displayed next to date and read time
- Works on individual post and page views
- Write page updated with new field reference
v1.19.1 - GitHub Stars on Stats page
- Live star count from waynesutton/markdown-site repository
- Fetches from GitHub public API (no token required)
- Stats page now displays 6 cards with responsive grid
Documentation updates
- Frontmatter Flow section added to docs.md, setup-guide.md, files.md
- How frontmatter works with step-by-step processing flow
- Instructions for adding new frontmatter fields
Updated files:
- src/components/CopyPageDropdown.tsx
- src/pages/Stats.tsx
- src/pages/Post.tsx
- src/pages/Write.tsx
- src/styles/global.css
- convex/schema.ts
- convex/posts.ts
- convex/pages.ts
- scripts/sync-posts.ts
- content/blog/setup-guide.md
- content/pages/docs.md
- content/pages/changelog-page.md
- files.md
- README.md
- TASK.md
- changelog.md
- AGENTS.md
@@ -227,6 +227,8 @@ markdown-blog/
|
||||
| featuredOrder | No | Display order (lower first) |
|
||||
| excerpt | No | Short text for card view |
|
||||
| image | No | OG image path |
|
||||
| authorName | No | Author display name |
|
||||
| authorImage | No | Round author avatar URL |
|
||||
|
||||
### Static pages (content/pages/)
|
||||
|
||||
@@ -238,6 +240,8 @@ markdown-blog/
|
||||
| order | No | Nav order (lower first) |
|
||||
| featured | No | true for featured section |
|
||||
| featuredOrder | No | Display order (lower first) |
|
||||
| authorName | No | Author display name |
|
||||
| authorImage | No | Round author avatar URL |
|
||||
|
||||
## Database schema
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ See `FORK_CONFIG.md` for detailed configuration examples and the full JSON schem
|
||||
- `/rss-full.xml` - Full content RSS for LLM ingestion
|
||||
- `/.well-known/ai-plugin.json` - AI plugin manifest
|
||||
- `/openapi.yaml` - OpenAPI 3.0 specification
|
||||
- Copy Page dropdown for sharing to ChatGPT, Claude, Perplexity
|
||||
- Copy Page dropdown for sharing to ChatGPT, Claude, Perplexity (uses raw markdown URLs for better AI parsing)
|
||||
|
||||
### Content Import
|
||||
|
||||
@@ -238,6 +238,8 @@ Then run `npm run sync` (dev) or `npm run sync:prod` (production). No redeploy n
|
||||
| `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
|
||||
|
||||
|
||||
13
TASK.md
@@ -2,14 +2,21 @@
|
||||
|
||||
## To Do
|
||||
|
||||
- [ ] create a ui site config page
|
||||
|
||||
## Current Status
|
||||
|
||||
v1.18.0 deployed. Added automated fork configuration with `npm run configure` command and comprehensive fork setup guide.
|
||||
v1.19.1 deployed. Author display (authorName/authorImage) and GitHub Stars on Stats page.
|
||||
|
||||
## Completed
|
||||
|
||||
- [x] Author display for posts and pages with authorName and authorImage frontmatter fields
|
||||
- [x] Round avatar image displayed next to date and read time on post/page views
|
||||
- [x] Write page updated with new frontmatter field reference
|
||||
- [x] Documentation updated: setup-guide.md, docs.md, files.md, README.md, AGENTS.md
|
||||
- [x] PRD created: prds/howto-Frontmatter.md with reusable prompt for future updates
|
||||
- [x] GitHub Stars card on Stats page with live count from repository
|
||||
|
||||
- [x] CopyPageDropdown AI services now use raw markdown URLs for better AI parsing
|
||||
- [x] ChatGPT, Claude, and Perplexity receive /raw/{slug}.md URLs instead of page URLs
|
||||
- [x] Automated fork configuration with npm run configure
|
||||
- [x] FORK_CONFIG.md comprehensive guide with two options (automated + manual)
|
||||
- [x] fork-config.json.example template with all configuration options
|
||||
|
||||
68
changelog.md
@@ -4,6 +4,74 @@ 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.19.1] - 2025-12-21
|
||||
|
||||
### Added
|
||||
|
||||
- GitHub Stars card on Stats page
|
||||
- Displays live star count from `waynesutton/markdown-site` repository
|
||||
- Fetches from GitHub public API (no token required)
|
||||
- Uses Phosphor GithubLogo icon
|
||||
- Updates on page load
|
||||
|
||||
### Changed
|
||||
|
||||
- Stats page now displays 6 cards in a single row (previously 5)
|
||||
- Updated CSS grid for 6-column layout on desktop
|
||||
- Responsive breakpoints adjusted for 6 cards (3x2 tablet, 2x3 mobile, 1x6 small mobile)
|
||||
|
||||
### Technical
|
||||
|
||||
- Added `useState` and `useEffect` to `src/pages/Stats.tsx` for GitHub API fetch
|
||||
- Added `GithubLogo` import from `@phosphor-icons/react`
|
||||
- Updated `.stats-cards-modern` grid to `repeat(6, 1fr)`
|
||||
- Updated responsive nth-child selectors for proper borders
|
||||
|
||||
## [1.19.0] - 2025-12-21
|
||||
|
||||
### Added
|
||||
|
||||
- Author display for posts and pages
|
||||
- New optional `authorName` and `authorImage` frontmatter fields
|
||||
- Round avatar image displayed next to date and read time
|
||||
- Works on individual post and page views (not on blog list)
|
||||
- Example: `authorName: "Your Name"` and `authorImage: "/images/authors/photo.png"`
|
||||
- Author images directory at `public/images/authors/`
|
||||
- Place author avatar images here
|
||||
- Recommended: square images (they display as circles)
|
||||
- Write page updated with new frontmatter field reference
|
||||
- Shows `authorName` and `authorImage` options for both posts and pages
|
||||
|
||||
### Technical
|
||||
|
||||
- Updated `convex/schema.ts` with authorName and authorImage fields
|
||||
- Updated `scripts/sync-posts.ts` interfaces and parsing
|
||||
- Updated `convex/posts.ts` and `convex/pages.ts` queries and mutations
|
||||
- Updated `src/pages/Post.tsx` to render author info
|
||||
- Updated `src/pages/Write.tsx` with new field definitions
|
||||
- CSS styles for `.post-author`, `.post-author-image`, `.post-author-name`
|
||||
|
||||
### Documentation
|
||||
|
||||
- Updated frontmatter tables in setup-guide.md, docs.md, files.md, README.md
|
||||
- Added example usage in about-this-blog.md
|
||||
|
||||
## [1.18.1] - 2025-12-21
|
||||
|
||||
### Changed
|
||||
|
||||
- CopyPageDropdown AI services now use raw markdown URLs for better AI parsing
|
||||
- ChatGPT, Claude, and Perplexity receive `/raw/{slug}.md` URLs instead of page URLs
|
||||
- AI services can fetch and parse clean markdown content directly
|
||||
- Includes metadata headers (type, date, reading time, tags) for structured parsing
|
||||
- No HTML parsing required by AI services
|
||||
|
||||
### Technical
|
||||
|
||||
- Renamed `buildUrlFromPageUrl` to `buildUrlFromRawMarkdown` in AIService interface
|
||||
- Handler builds raw markdown URL from page origin and slug
|
||||
- Updated prompt text to reference "raw markdown file URL"
|
||||
|
||||
## [1.18.0] - 2025-12-20
|
||||
|
||||
### Added
|
||||
|
||||
@@ -9,6 +9,8 @@ readTime: "4 min read"
|
||||
featured: false
|
||||
featuredOrder: 3
|
||||
excerpt: "Learn how this open source framework works with real-time sync and instant updates."
|
||||
authorName: "Markdown Framework"
|
||||
authorImage: "/images/authors/markdown.png"
|
||||
---
|
||||
|
||||
# About This Markdown Framework
|
||||
|
||||
@@ -8,6 +8,8 @@ tags: ["configuration", "setup", "fork", "tutorial"]
|
||||
readTime: "4 min read"
|
||||
featured: true
|
||||
featuredOrder: 0
|
||||
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."
|
||||
---
|
||||
|
||||
@@ -8,6 +8,8 @@ tags: ["tutorial", "markdown", "cursor", "publishing"]
|
||||
readTime: "3 min read"
|
||||
featured: true
|
||||
featuredOrder: 3
|
||||
authorName: "Markdown"
|
||||
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."
|
||||
---
|
||||
|
||||
@@ -6,6 +6,8 @@ slug: "markdown-with-code-examples"
|
||||
published: true
|
||||
tags: ["markdown", "tutorial", "code"]
|
||||
readTime: "5 min read"
|
||||
authorName: "Markdown"
|
||||
authorImage: "/images/authors/markdown.png"
|
||||
featured: false
|
||||
featuredOrder: 5
|
||||
image: "/images/markdown.png"
|
||||
|
||||
@@ -8,6 +8,8 @@ tags: ["features", "search", "convex", "updates"]
|
||||
readTime: "4 min read"
|
||||
featured: true
|
||||
featuredOrder: 5
|
||||
authorName: "Markdown"
|
||||
authorImage: "/images/authors/markdown.png"
|
||||
image: "/images/v16.png"
|
||||
excerpt: "Search your site with Command+K. Control featured items from frontmatter. Add a logo gallery."
|
||||
---
|
||||
|
||||
@@ -9,6 +9,8 @@ readTime: "8 min read"
|
||||
featured: true
|
||||
featuredOrder: 2
|
||||
image: "/images/v17.png"
|
||||
authorName: "Markdown"
|
||||
authorImage: "/images/authors/markdown.png"
|
||||
excerpt: "12 versions of new features: automated fork config, GitHub graph, write page, mobile menu, stats aggregates, and more."
|
||||
---
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ readTime: "8 min read"
|
||||
featured: true
|
||||
featuredOrder: 3
|
||||
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."
|
||||
---
|
||||
|
||||
@@ -38,6 +40,7 @@ This guide walks you through forking [this markdown framework](https://github.co
|
||||
- [Step 8: Set Up Production Convex](#step-8-set-up-production-convex)
|
||||
- [Writing Blog Posts](#writing-blog-posts)
|
||||
- [Frontmatter Fields](#frontmatter-fields)
|
||||
- [How Frontmatter Works](#how-frontmatter-works)
|
||||
- [Adding Images](#adding-images)
|
||||
- [Sync After Adding Posts](#sync-after-adding-posts)
|
||||
- [Environment Files](#environment-files)
|
||||
@@ -144,6 +147,8 @@ export default defineSchema({
|
||||
excerpt: v.optional(v.string()),
|
||||
featured: v.optional(v.boolean()),
|
||||
featuredOrder: v.optional(v.number()),
|
||||
authorName: v.optional(v.string()),
|
||||
authorImage: v.optional(v.string()),
|
||||
lastSyncedAt: v.number(),
|
||||
})
|
||||
.index("by_slug", ["slug"])
|
||||
@@ -160,6 +165,8 @@ export default defineSchema({
|
||||
image: v.optional(v.string()),
|
||||
featured: v.optional(v.boolean()),
|
||||
featuredOrder: v.optional(v.number()),
|
||||
authorName: v.optional(v.string()),
|
||||
authorImage: v.optional(v.string()),
|
||||
lastSyncedAt: v.number(),
|
||||
})
|
||||
.index("by_slug", ["slug"])
|
||||
@@ -324,6 +331,33 @@ Your markdown content here...
|
||||
| `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 |
|
||||
|
||||
### 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:
|
||||
|
||||
**Content directories:**
|
||||
|
||||
- `content/blog/*.md` contains blog posts with frontmatter
|
||||
- `content/pages/*.md` contains static pages with frontmatter
|
||||
|
||||
**Processing flow:**
|
||||
|
||||
1. Markdown files in `content/blog/` and `content/pages/` contain YAML frontmatter
|
||||
2. `scripts/sync-posts.ts` uses `gray-matter` to parse frontmatter and validate required fields
|
||||
3. Parsed data is sent to Convex mutations (`api.posts.syncPostsPublic`, `api.pages.syncPagesPublic`)
|
||||
4. `convex/schema.ts` defines the database structure for storing the data
|
||||
|
||||
**Adding a new frontmatter field:**
|
||||
|
||||
To add a custom frontmatter field, update these files:
|
||||
|
||||
1. The interface in `scripts/sync-posts.ts` (`PostFrontmatter` or `PageFrontmatter`)
|
||||
2. The parsing logic in `parseMarkdownFile()` or `parsePageFile()` functions
|
||||
3. The schema in `convex/schema.ts`
|
||||
4. The sync mutation in `convex/posts.ts` or `convex/pages.ts`
|
||||
|
||||
### Adding Images
|
||||
|
||||
@@ -846,11 +880,13 @@ 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 |
|
||||
|
||||
3. Run `npm run sync` to sync pages
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ featured: true
|
||||
featuredOrder: 3
|
||||
tags: ["images", "tutorial", "markdown", "open-graph"]
|
||||
readTime: "4 min read"
|
||||
authorName: "Markdown"
|
||||
authorImage: "/images/authors/markdown.png"
|
||||
image: "https://images.unsplash.com/photo-1499750310107-5fef28a66643?w=1200&h=630&fit=crop"
|
||||
---
|
||||
|
||||
|
||||
@@ -7,6 +7,58 @@ order: 5
|
||||
|
||||
All notable changes to this project.
|
||||
|
||||
## v1.19.1
|
||||
|
||||
Released December 21, 2025
|
||||
|
||||
**GitHub Stars on Stats page**
|
||||
|
||||
- New GitHub Stars card displays live star count from repository
|
||||
- Fetches from GitHub public API (no token required)
|
||||
- Uses Phosphor GithubLogo icon
|
||||
- Stats page now shows 6 cards in a single row
|
||||
- Responsive layout: 3x2 on tablet, 2x3 on mobile, stacked on small screens
|
||||
|
||||
Updated files: `src/pages/Stats.tsx`, `src/styles/global.css`
|
||||
|
||||
## v1.19.0
|
||||
|
||||
Released December 21, 2025
|
||||
|
||||
**Author display for posts and pages**
|
||||
|
||||
- New optional `authorName` and `authorImage` frontmatter fields
|
||||
- Round avatar image displayed next to date and read time
|
||||
- Works on individual post and page views (not on blog list)
|
||||
- Write page updated with new frontmatter field reference
|
||||
|
||||
Example frontmatter:
|
||||
|
||||
```yaml
|
||||
authorName: "Your Name"
|
||||
authorImage: "/images/authors/photo.png"
|
||||
```
|
||||
|
||||
Place author avatar images in `public/images/authors/`. Recommended: square images (they display as circles).
|
||||
|
||||
Updated files: `convex/schema.ts`, `scripts/sync-posts.ts`, `convex/posts.ts`, `convex/pages.ts`, `src/pages/Post.tsx`, `src/pages/Write.tsx`, `src/styles/global.css`
|
||||
|
||||
Documentation updated: setup-guide.md, docs.md, files.md, README.md, AGENTS.md
|
||||
|
||||
New PRD: `prds/howto-Frontmatter.md` with reusable prompt for future frontmatter updates.
|
||||
|
||||
## v1.18.1
|
||||
|
||||
Released December 21, 2025
|
||||
|
||||
**CopyPageDropdown raw markdown URLs**
|
||||
|
||||
- AI services (ChatGPT, Claude, Perplexity) now receive raw markdown file URLs instead of page URLs
|
||||
- URL format: `/raw/{slug}.md` (e.g., `/raw/setup-guide.md`)
|
||||
- AI services can fetch and parse clean markdown content directly
|
||||
- Includes metadata headers for structured parsing
|
||||
- No HTML parsing required by AI services
|
||||
|
||||
## v1.18.0
|
||||
|
||||
Released December 20, 2025
|
||||
@@ -26,7 +78,7 @@ Two options for fork setup:
|
||||
The configure script updates all 11 configuration files:
|
||||
|
||||
| File | What it updates |
|
||||
| ----------------------------------- | ---------------------------------------- |
|
||||
| ----------------------------------- | -------------------------------------- |
|
||||
| `src/config/siteConfig.ts` | Site name, bio, GitHub, features |
|
||||
| `src/pages/Home.tsx` | Intro paragraph, footer links |
|
||||
| `src/pages/Post.tsx` | SITE_URL, SITE_NAME constants |
|
||||
|
||||
@@ -95,6 +95,8 @@ Content here...
|
||||
| `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 |
|
||||
|
||||
### Static pages
|
||||
|
||||
@@ -121,6 +123,33 @@ Content here...
|
||||
| `image` | No | Thumbnail for featured 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 |
|
||||
|
||||
### 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:
|
||||
|
||||
**Content directories:**
|
||||
|
||||
- `content/blog/*.md` contains blog posts with frontmatter
|
||||
- `content/pages/*.md` contains static pages with frontmatter
|
||||
|
||||
**Processing flow:**
|
||||
|
||||
1. Markdown files in `content/blog/` and `content/pages/` contain YAML frontmatter
|
||||
2. `scripts/sync-posts.ts` uses `gray-matter` to parse frontmatter and validate required fields
|
||||
3. Parsed data is sent to Convex mutations (`api.posts.syncPostsPublic`, `api.pages.syncPagesPublic`)
|
||||
4. `convex/schema.ts` defines the database structure for storing the data
|
||||
|
||||
**Adding a new frontmatter field:**
|
||||
|
||||
To add a custom frontmatter field, update these files:
|
||||
|
||||
1. The interface in `scripts/sync-posts.ts` (`PostFrontmatter` or `PageFrontmatter`)
|
||||
2. The parsing logic in `parseMarkdownFile()` or `parsePageFile()` functions
|
||||
3. The schema in `convex/schema.ts`
|
||||
4. The sync mutation in `convex/posts.ts` or `convex/pages.ts`
|
||||
|
||||
### Syncing content
|
||||
|
||||
@@ -515,15 +544,15 @@ Each post and page includes a share dropdown with options:
|
||||
| Option | Description |
|
||||
| ---------------- | ------------------------------------------------ |
|
||||
| Copy page | Copies formatted markdown to clipboard |
|
||||
| Open in ChatGPT | Opens ChatGPT with article content |
|
||||
| Open in Claude | Opens Claude with article content |
|
||||
| Open in Perplexity | Opens Perplexity for research with content |
|
||||
| Open in ChatGPT | Opens ChatGPT with raw markdown URL |
|
||||
| Open in Claude | Opens Claude with raw markdown URL |
|
||||
| Open in Perplexity | Opens Perplexity with raw markdown URL |
|
||||
| View as Markdown | Opens raw `.md` file in new tab |
|
||||
| Generate Skill | Downloads `{slug}-skill.md` for AI agent training |
|
||||
|
||||
**Generate Skill:** Formats the content as an AI agent skill file with metadata, when to use, and instructions sections.
|
||||
**Raw markdown URLs:** AI services receive the URL to the raw markdown file (e.g., `/raw/setup-guide.md`) instead of the page URL. This provides direct access to clean markdown content with metadata headers for better AI parsing.
|
||||
|
||||
**Long content:** If content exceeds URL limits, it copies to clipboard and opens the AI service in a new tab. Paste to continue.
|
||||
**Generate Skill:** Formats the content as an AI agent skill file with metadata, when to use, and instructions sections.
|
||||
|
||||
## Real-time stats
|
||||
|
||||
@@ -648,6 +677,8 @@ export default defineSchema({
|
||||
excerpt: v.optional(v.string()), // For card view
|
||||
featured: v.optional(v.boolean()), // Show in featured section
|
||||
featuredOrder: v.optional(v.number()), // Order in featured (lower = first)
|
||||
authorName: v.optional(v.string()), // Author display name
|
||||
authorImage: v.optional(v.string()), // Author avatar image URL
|
||||
lastSyncedAt: v.number(),
|
||||
})
|
||||
.index("by_slug", ["slug"])
|
||||
@@ -664,6 +695,8 @@ export default defineSchema({
|
||||
image: v.optional(v.string()), // Thumbnail for featured cards
|
||||
featured: v.optional(v.boolean()), // Show in featured section
|
||||
featuredOrder: v.optional(v.number()), // Order in featured (lower = first)
|
||||
authorName: v.optional(v.string()), // Author display name
|
||||
authorImage: v.optional(v.string()), // Author avatar image URL
|
||||
lastSyncedAt: v.number(),
|
||||
})
|
||||
.index("by_slug", ["slug"])
|
||||
|
||||
@@ -15,6 +15,8 @@ export const getAllPages = query({
|
||||
image: v.optional(v.string()),
|
||||
featured: v.optional(v.boolean()),
|
||||
featuredOrder: v.optional(v.number()),
|
||||
authorName: v.optional(v.string()),
|
||||
authorImage: v.optional(v.string()),
|
||||
}),
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
@@ -41,6 +43,8 @@ export const getAllPages = query({
|
||||
image: page.image,
|
||||
featured: page.featured,
|
||||
featuredOrder: page.featuredOrder,
|
||||
authorName: page.authorName,
|
||||
authorImage: page.authorImage,
|
||||
}));
|
||||
},
|
||||
});
|
||||
@@ -101,6 +105,8 @@ export const getPageBySlug = query({
|
||||
image: v.optional(v.string()),
|
||||
featured: v.optional(v.boolean()),
|
||||
featuredOrder: v.optional(v.number()),
|
||||
authorName: v.optional(v.string()),
|
||||
authorImage: v.optional(v.string()),
|
||||
}),
|
||||
v.null(),
|
||||
),
|
||||
@@ -125,6 +131,8 @@ export const getPageBySlug = query({
|
||||
image: page.image,
|
||||
featured: page.featured,
|
||||
featuredOrder: page.featuredOrder,
|
||||
authorName: page.authorName,
|
||||
authorImage: page.authorImage,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -143,6 +151,8 @@ export const syncPagesPublic = mutation({
|
||||
image: v.optional(v.string()),
|
||||
featured: v.optional(v.boolean()),
|
||||
featuredOrder: v.optional(v.number()),
|
||||
authorName: v.optional(v.string()),
|
||||
authorImage: v.optional(v.string()),
|
||||
}),
|
||||
),
|
||||
},
|
||||
@@ -178,6 +188,8 @@ export const syncPagesPublic = mutation({
|
||||
image: page.image,
|
||||
featured: page.featured,
|
||||
featuredOrder: page.featuredOrder,
|
||||
authorName: page.authorName,
|
||||
authorImage: page.authorImage,
|
||||
lastSyncedAt: now,
|
||||
});
|
||||
updated++;
|
||||
|
||||
@@ -19,6 +19,8 @@ export const getAllPosts = query({
|
||||
excerpt: v.optional(v.string()),
|
||||
featured: v.optional(v.boolean()),
|
||||
featuredOrder: v.optional(v.number()),
|
||||
authorName: v.optional(v.string()),
|
||||
authorImage: v.optional(v.string()),
|
||||
}),
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
@@ -47,6 +49,8 @@ export const getAllPosts = query({
|
||||
excerpt: post.excerpt,
|
||||
featured: post.featured,
|
||||
featuredOrder: post.featuredOrder,
|
||||
authorName: post.authorName,
|
||||
authorImage: post.authorImage,
|
||||
}));
|
||||
},
|
||||
});
|
||||
@@ -113,6 +117,8 @@ export const getPostBySlug = query({
|
||||
excerpt: v.optional(v.string()),
|
||||
featured: v.optional(v.boolean()),
|
||||
featuredOrder: v.optional(v.number()),
|
||||
authorName: v.optional(v.string()),
|
||||
authorImage: v.optional(v.string()),
|
||||
}),
|
||||
v.null(),
|
||||
),
|
||||
@@ -141,6 +147,8 @@ export const getPostBySlug = query({
|
||||
excerpt: post.excerpt,
|
||||
featured: post.featured,
|
||||
featuredOrder: post.featuredOrder,
|
||||
authorName: post.authorName,
|
||||
authorImage: post.authorImage,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -162,6 +170,8 @@ export const syncPosts = internalMutation({
|
||||
excerpt: v.optional(v.string()),
|
||||
featured: v.optional(v.boolean()),
|
||||
featuredOrder: v.optional(v.number()),
|
||||
authorName: v.optional(v.string()),
|
||||
authorImage: v.optional(v.string()),
|
||||
}),
|
||||
),
|
||||
},
|
||||
@@ -200,6 +210,8 @@ export const syncPosts = internalMutation({
|
||||
excerpt: post.excerpt,
|
||||
featured: post.featured,
|
||||
featuredOrder: post.featuredOrder,
|
||||
authorName: post.authorName,
|
||||
authorImage: post.authorImage,
|
||||
lastSyncedAt: now,
|
||||
});
|
||||
updated++;
|
||||
@@ -242,6 +254,8 @@ export const syncPostsPublic = mutation({
|
||||
excerpt: v.optional(v.string()),
|
||||
featured: v.optional(v.boolean()),
|
||||
featuredOrder: v.optional(v.number()),
|
||||
authorName: v.optional(v.string()),
|
||||
authorImage: v.optional(v.string()),
|
||||
}),
|
||||
),
|
||||
},
|
||||
@@ -280,6 +294,8 @@ export const syncPostsPublic = mutation({
|
||||
excerpt: post.excerpt,
|
||||
featured: post.featured,
|
||||
featuredOrder: post.featuredOrder,
|
||||
authorName: post.authorName,
|
||||
authorImage: post.authorImage,
|
||||
lastSyncedAt: now,
|
||||
});
|
||||
updated++;
|
||||
|
||||
@@ -16,6 +16,8 @@ export default defineSchema({
|
||||
excerpt: v.optional(v.string()), // Short excerpt for card view
|
||||
featured: v.optional(v.boolean()), // Show in featured section
|
||||
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)
|
||||
lastSyncedAt: v.number(),
|
||||
})
|
||||
.index("by_slug", ["slug"])
|
||||
@@ -42,6 +44,8 @@ export default defineSchema({
|
||||
image: v.optional(v.string()), // Thumbnail/OG image URL for featured cards
|
||||
featured: v.optional(v.boolean()), // Show in featured section
|
||||
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)
|
||||
lastSyncedAt: v.number(),
|
||||
})
|
||||
.index("by_slug", ["slug"])
|
||||
|
||||
23
files.md
@@ -42,7 +42,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 (configurable via siteConfig.blogPage) |
|
||||
| `Post.tsx` | Individual blog post view (update SITE_URL/SITE_NAME when forking) |
|
||||
| `Stats.tsx` | Real-time analytics dashboard with visitor stats |
|
||||
| `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) |
|
||||
|
||||
### Components (`src/components/`)
|
||||
@@ -53,7 +53,7 @@ A brief description of each file in the codebase.
|
||||
| `ThemeToggle.tsx` | Theme switcher (dark/light/tan/cloud) |
|
||||
| `PostList.tsx` | Year-grouped blog post list |
|
||||
| `BlogPost.tsx` | Markdown renderer with syntax highlighting |
|
||||
| `CopyPageDropdown.tsx` | Share dropdown for LLMs (ChatGPT, Claude, Perplexity) with View as Markdown and Generate Skill options |
|
||||
| `CopyPageDropdown.tsx` | Share dropdown for LLMs (ChatGPT, Claude, Perplexity) using raw markdown URLs for better AI parsing, with View as Markdown and Generate Skill options |
|
||||
| `SearchModal.tsx` | Full text search modal with keyboard navigation |
|
||||
| `FeaturedCards.tsx` | Card grid for featured posts/pages with excerpts |
|
||||
| `LogoMarquee.tsx` | Scrolling logo gallery with clickable links |
|
||||
@@ -127,6 +127,8 @@ Markdown files with frontmatter for blog posts. Each file becomes a blog post.
|
||||
| `excerpt` | Short excerpt for card view (optional) |
|
||||
| `featured` | Show in featured section (optional) |
|
||||
| `featuredOrder` | Order in featured section (optional) |
|
||||
| `authorName` | Author display name (optional) |
|
||||
| `authorImage` | Round author avatar image URL (optional) |
|
||||
|
||||
## Static Pages (`content/pages/`)
|
||||
|
||||
@@ -141,6 +143,8 @@ Markdown files for static pages like About, Projects, Contact, Changelog.
|
||||
| `excerpt` | Short excerpt for card view (optional) |
|
||||
| `featured` | Show in featured section (optional) |
|
||||
| `featuredOrder` | Order in featured section (optional) |
|
||||
| `authorName` | Author display name (optional) |
|
||||
| `authorImage` | Round author avatar image URL (optional) |
|
||||
|
||||
## Scripts (`scripts/`)
|
||||
|
||||
@@ -150,6 +154,21 @@ Markdown files for static pages like About, Projects, Contact, Changelog.
|
||||
| `import-url.ts` | Imports external URLs as markdown posts (Firecrawl) |
|
||||
| `configure-fork.ts` | Automated fork configuration (reads fork-config.json) |
|
||||
|
||||
### Frontmatter Flow
|
||||
|
||||
Frontmatter is the YAML metadata at the top of each markdown file. Here is how it flows through the system:
|
||||
|
||||
1. **Content directories** (`content/blog/*.md`, `content/pages/*.md`) contain markdown files with YAML frontmatter
|
||||
2. **`scripts/sync-posts.ts`** uses `gray-matter` to parse frontmatter and validate required fields
|
||||
3. **Convex mutations** (`api.posts.syncPostsPublic`, `api.pages.syncPagesPublic`) receive parsed data
|
||||
4. **`convex/schema.ts`** defines the database structure for storing frontmatter fields
|
||||
|
||||
**To add a new frontmatter field**, update:
|
||||
|
||||
- `scripts/sync-posts.ts`: Add to `PostFrontmatter` or `PageFrontmatter` interface and parsing logic
|
||||
- `convex/schema.ts`: Add field to the posts or pages table schema
|
||||
- `convex/posts.ts` or `convex/pages.ts`: Update sync mutation to handle new field
|
||||
|
||||
## Netlify (`netlify/edge-functions/`)
|
||||
|
||||
| File | Description |
|
||||
|
||||
235
prds/howto-Frontmatter.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# How to Add New Frontmatter Fields
|
||||
|
||||
This guide documents the process for adding new frontmatter fields to the markdown blog framework.
|
||||
|
||||
## Files Updated for authorName and authorImage (13 total)
|
||||
|
||||
When adding the `authorName` and `authorImage` frontmatter fields, these files were updated:
|
||||
|
||||
| File | What Was Updated |
|
||||
|------|------------------|
|
||||
| `convex/schema.ts` | Added fields to posts and pages table definitions |
|
||||
| `scripts/sync-posts.ts` | Added to interfaces (PostFrontmatter, ParsedPost, PageFrontmatter, ParsedPage) and parsing logic |
|
||||
| `convex/posts.ts` | Added to return validators and syncPosts/syncPostsPublic mutations |
|
||||
| `convex/pages.ts` | Added to return validators and syncPagesPublic mutation |
|
||||
| `src/pages/Post.tsx` | Added UI rendering for author display |
|
||||
| `src/pages/Write.tsx` | Added to POST_FIELDS and PAGE_FIELDS arrays |
|
||||
| `src/styles/global.css` | Added CSS styles for author display |
|
||||
| `content/blog/setup-guide.md` | Updated frontmatter tables and examples |
|
||||
| `content/pages/docs.md` | Updated frontmatter tables |
|
||||
| `files.md` | Updated frontmatter field tables |
|
||||
| `README.md` | Updated frontmatter field tables |
|
||||
| `AGENTS.md` | Updated frontmatter field tables |
|
||||
| `content/blog/about-this-blog.md` | Added example usage |
|
||||
|
||||
## Frontmatter Flow Summary
|
||||
|
||||
Frontmatter is the YAML metadata at the top of each markdown file. Here is how it flows through the system:
|
||||
|
||||
1. **Content directories** (`content/blog/*.md`, `content/pages/*.md`) contain markdown files with YAML frontmatter
|
||||
2. **`scripts/sync-posts.ts`** uses `gray-matter` to parse frontmatter and validate required fields
|
||||
3. **Convex mutations** (`api.posts.syncPostsPublic`, `api.pages.syncPagesPublic`) receive parsed data
|
||||
4. **`convex/schema.ts`** defines the database structure for storing frontmatter fields
|
||||
|
||||
## Reusable Prompt for Future Frontmatter Updates
|
||||
|
||||
Copy and customize this prompt when adding new frontmatter fields:
|
||||
|
||||
```
|
||||
Add a new optional frontmatter field called [FIELD_NAME] for [posts/pages/both].
|
||||
|
||||
Description: [What the field does]
|
||||
Type: [string/boolean/number/array]
|
||||
Display: [Where it should appear in the UI, if applicable]
|
||||
|
||||
Update these files:
|
||||
1. convex/schema.ts - Add field to [posts/pages/both] table
|
||||
2. scripts/sync-posts.ts - Add to [PostFrontmatter/PageFrontmatter] interface and parsing logic
|
||||
3. convex/posts.ts - Add to return validators and sync mutations (if for posts)
|
||||
4. convex/pages.ts - Add to return validators and sync mutations (if for pages)
|
||||
5. src/pages/Post.tsx - Add UI rendering (if display needed)
|
||||
6. src/pages/Write.tsx - Add to POST_FIELDS/PAGE_FIELDS array
|
||||
7. src/styles/global.css - Add CSS styles (if display needed)
|
||||
8. content/blog/setup-guide.md - Update frontmatter tables
|
||||
9. content/pages/docs.md - Update frontmatter tables
|
||||
10. files.md - Update frontmatter field tables
|
||||
11. README.md - Update frontmatter field tables
|
||||
12. AGENTS.md - Update frontmatter field tables
|
||||
13. Add example usage to a content file
|
||||
|
||||
After implementation:
|
||||
- Update changelog.md with the new feature
|
||||
- Update content/pages/changelog-page.md
|
||||
- Update TASK.md with completed task
|
||||
- Create/update PRD in prds/ folder if needed
|
||||
```
|
||||
|
||||
## Step-by-Step Implementation Guide
|
||||
|
||||
### Step 1: Update Schema
|
||||
|
||||
Add the field to `convex/schema.ts`:
|
||||
|
||||
```typescript
|
||||
// For posts
|
||||
posts: defineTable({
|
||||
// ... existing fields
|
||||
newField: v.optional(v.string()), // or v.number(), v.boolean(), etc.
|
||||
lastSyncedAt: v.number(),
|
||||
})
|
||||
|
||||
// For pages
|
||||
pages: defineTable({
|
||||
// ... existing fields
|
||||
newField: v.optional(v.string()),
|
||||
lastSyncedAt: v.number(),
|
||||
})
|
||||
```
|
||||
|
||||
### Step 2: Update Sync Script
|
||||
|
||||
Add to `scripts/sync-posts.ts`:
|
||||
|
||||
```typescript
|
||||
// Add to interface
|
||||
interface PostFrontmatter {
|
||||
// ... existing fields
|
||||
newField?: string;
|
||||
}
|
||||
|
||||
interface ParsedPost {
|
||||
// ... existing fields
|
||||
newField?: string;
|
||||
}
|
||||
|
||||
// Add to parsing logic in parseMarkdownFile()
|
||||
return {
|
||||
// ... existing fields
|
||||
newField: frontmatter.newField,
|
||||
};
|
||||
```
|
||||
|
||||
### Step 3: Update Convex Mutations
|
||||
|
||||
Add to `convex/posts.ts` and/or `convex/pages.ts`:
|
||||
|
||||
```typescript
|
||||
// Add to return validator
|
||||
returns: v.array(v.object({
|
||||
// ... existing fields
|
||||
newField: v.optional(v.string()),
|
||||
}))
|
||||
|
||||
// Add to args validator in sync mutation
|
||||
posts: v.array(v.object({
|
||||
// ... existing fields
|
||||
newField: v.optional(v.string()),
|
||||
}))
|
||||
|
||||
// Add to patch/insert calls
|
||||
await ctx.db.patch(existingPost._id, {
|
||||
// ... existing fields
|
||||
newField: post.newField,
|
||||
});
|
||||
```
|
||||
|
||||
### Step 4: Update UI (if needed)
|
||||
|
||||
Add to `src/pages/Post.tsx`:
|
||||
|
||||
```tsx
|
||||
{post.newField && (
|
||||
<div className="post-new-field">
|
||||
{post.newField}
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
### Step 5: Update Write Page
|
||||
|
||||
Add to `src/pages/Write.tsx`:
|
||||
|
||||
```typescript
|
||||
const POST_FIELDS = [
|
||||
// ... existing fields
|
||||
{ name: "newField", required: false, example: '"example value"' },
|
||||
];
|
||||
```
|
||||
|
||||
### Step 6: Add CSS (if needed)
|
||||
|
||||
Add to `src/styles/global.css`:
|
||||
|
||||
```css
|
||||
.post-new-field {
|
||||
/* styles */
|
||||
}
|
||||
```
|
||||
|
||||
### Step 7: Update Documentation
|
||||
|
||||
Update frontmatter tables in:
|
||||
- `content/blog/setup-guide.md`
|
||||
- `content/pages/docs.md`
|
||||
- `files.md`
|
||||
- `README.md`
|
||||
- `AGENTS.md`
|
||||
|
||||
### Step 8: Add Example
|
||||
|
||||
Add the new field to a content file as an example (e.g., `content/blog/about-this-blog.md`).
|
||||
|
||||
### Step 9: Update Changelog
|
||||
|
||||
Add entry to:
|
||||
- `changelog.md` (root)
|
||||
- `content/pages/changelog-page.md`
|
||||
|
||||
### Step 10: Run Sync
|
||||
|
||||
```bash
|
||||
npm run sync # Development
|
||||
npm run sync:prod # Production
|
||||
```
|
||||
|
||||
## Current Frontmatter Fields
|
||||
|
||||
### Blog Posts (`content/blog/*.md`)
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `title` | Yes | Post title |
|
||||
| `description` | Yes | SEO description |
|
||||
| `date` | Yes | Publication date (YYYY-MM-DD) |
|
||||
| `slug` | Yes | URL path |
|
||||
| `published` | Yes | Show publicly |
|
||||
| `tags` | Yes | Array of tags |
|
||||
| `readTime` | No | Reading time |
|
||||
| `image` | No | Header/OG image URL |
|
||||
| `excerpt` | No | Short excerpt for cards |
|
||||
| `featured` | No | Show in featured section |
|
||||
| `featuredOrder` | No | Order in featured (lower first) |
|
||||
| `authorName` | No | Author display name |
|
||||
| `authorImage` | No | Round author avatar URL |
|
||||
|
||||
### Static Pages (`content/pages/*.md`)
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `title` | Yes | Page title |
|
||||
| `slug` | Yes | URL path |
|
||||
| `published` | Yes | Show publicly |
|
||||
| `order` | No | Nav order (lower first) |
|
||||
| `excerpt` | No | Short excerpt for cards |
|
||||
| `image` | No | Thumbnail/OG image URL |
|
||||
| `featured` | No | Show in featured section |
|
||||
| `featuredOrder` | No | Order in featured (lower first) |
|
||||
| `authorName` | No | Author display name |
|
||||
| `authorImage` | No | Round author avatar URL |
|
||||
|
||||
## Related Files
|
||||
|
||||
- PRD: `prds/howto-Frontmatter.md` (this file)
|
||||
- Write conflicts guide: `prds/howtoavoidwriteconflicts.md`
|
||||
- Stats implementation: `prds/howstatsworks.md`
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-label="m logo">
|
||||
<rect x="32" y="32" width="448" height="448" rx="96" ry="96" fill="#000000"/>
|
||||
<text x="256" y="330"
|
||||
text-anchor="middle"
|
||||
font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, 'Apple Color Emoji','Segoe UI Emoji'"
|
||||
font-size="300"
|
||||
font-weight="800"
|
||||
fill="#ffffff"
|
||||
letter-spacing="-8">m</text>
|
||||
<svg width="64" height="57" viewBox="0 0 64 57" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="57" rx="6" fill="black"/>
|
||||
<path d="M39.4777 42.4294V44H24.3888V42.4294L27.4739 42.0367C28.0722 41.9619 28.4275 41.8124 28.5396 41.588C28.6518 41.3262 28.7079 40.5596 28.7079 39.2882V25.1528C28.7079 22.5725 28.3527 20.7028 27.6422 19.5435C26.9316 18.3469 25.8659 17.7485 24.4449 17.7485C23.8465 17.7485 23.136 17.9168 22.3133 18.2534C21.4906 18.5525 20.668 19.1135 19.8453 19.9362C19.06 20.7589 18.3681 21.9742 17.7698 23.5822V39.2882C17.7698 40.5596 17.8259 41.3262 17.9381 41.588C18.0503 41.8124 18.4055 41.9619 19.0039 42.0367L22.089 42.4294V44H7V42.4294L10.0851 42.0367C10.6834 41.9619 11.0387 41.8124 11.1509 41.588C11.2631 41.3262 11.3191 40.5596 11.3191 39.2882V20.2727C11.3191 19.0013 11.2631 18.2534 11.1509 18.029C11.0387 17.7672 10.6834 17.599 10.0851 17.5242L7 17.1315V15.5609H17.4333V20.4971C18.4803 18.5899 19.8266 17.2063 21.4719 16.3462C23.1173 15.4487 24.7814 15 26.4642 15C28.4836 15 30.185 15.5235 31.5687 16.5706C32.9897 17.5803 33.9993 19.0387 34.5977 20.9458C35.6073 18.8517 36.9723 17.3372 38.6924 16.4023C40.4126 15.4674 42.1328 15 43.853 15C46.508 15 48.6209 15.8788 50.1915 17.6364C51.7621 19.3939 52.5474 21.8807 52.5474 25.0967V39.2882C52.5474 40.5596 52.6035 41.3262 52.7157 41.588C52.8278 41.8124 53.1831 41.9619 53.7814 42.0367L56.8665 42.4294V44H41.7775V42.4294L44.8627 42.0367C45.461 41.9619 45.8162 41.8124 45.9284 41.588C46.0406 41.3262 46.0967 40.5596 46.0967 39.2882V25.1528C46.0967 22.5725 45.7414 20.7028 45.0309 19.5435C44.3204 18.3469 43.2547 17.7485 41.8336 17.7485C41.2353 17.7485 40.5248 17.9168 39.7021 18.2534C38.8794 18.5525 38.0567 19.1322 37.234 19.9923C36.4113 20.8523 35.7008 22.1051 35.1025 23.7505C35.1399 23.9375 35.1586 24.1618 35.1586 24.4236C35.1586 24.648 35.1586 24.8723 35.1586 25.0967V39.2882C35.1586 40.5596 35.2147 41.3262 35.3269 41.588C35.4391 41.8124 35.7943 41.9619 36.3926 42.0367L39.4777 42.4294Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 505 B After Width: | Height: | Size: 2.0 KiB |
BIN
public/images/authors/markdown.png
Normal file
|
After Width: | Height: | Size: 764 B |
@@ -1,10 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-label="m logo">
|
||||
<rect x="32" y="32" width="448" height="448" rx="96" ry="96" fill="#000000"/>
|
||||
<text x="256" y="330"
|
||||
text-anchor="middle"
|
||||
font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, 'Apple Color Emoji','Segoe UI Emoji'"
|
||||
font-size="300"
|
||||
font-weight="800"
|
||||
fill="#ffffff"
|
||||
letter-spacing="-8">m</text>
|
||||
<svg width="64" height="57" viewBox="0 0 64 57" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="57" rx="6" fill="black"/>
|
||||
<path d="M39.4777 42.4294V44H24.3888V42.4294L27.4739 42.0367C28.0722 41.9619 28.4275 41.8124 28.5396 41.588C28.6518 41.3262 28.7079 40.5596 28.7079 39.2882V25.1528C28.7079 22.5725 28.3527 20.7028 27.6422 19.5435C26.9316 18.3469 25.8659 17.7485 24.4449 17.7485C23.8465 17.7485 23.136 17.9168 22.3133 18.2534C21.4906 18.5525 20.668 19.1135 19.8453 19.9362C19.06 20.7589 18.3681 21.9742 17.7698 23.5822V39.2882C17.7698 40.5596 17.8259 41.3262 17.9381 41.588C18.0503 41.8124 18.4055 41.9619 19.0039 42.0367L22.089 42.4294V44H7V42.4294L10.0851 42.0367C10.6834 41.9619 11.0387 41.8124 11.1509 41.588C11.2631 41.3262 11.3191 40.5596 11.3191 39.2882V20.2727C11.3191 19.0013 11.2631 18.2534 11.1509 18.029C11.0387 17.7672 10.6834 17.599 10.0851 17.5242L7 17.1315V15.5609H17.4333V20.4971C18.4803 18.5899 19.8266 17.2063 21.4719 16.3462C23.1173 15.4487 24.7814 15 26.4642 15C28.4836 15 30.185 15.5235 31.5687 16.5706C32.9897 17.5803 33.9993 19.0387 34.5977 20.9458C35.6073 18.8517 36.9723 17.3372 38.6924 16.4023C40.4126 15.4674 42.1328 15 43.853 15C46.508 15 48.6209 15.8788 50.1915 17.6364C51.7621 19.3939 52.5474 21.8807 52.5474 25.0967V39.2882C52.5474 40.5596 52.6035 41.3262 52.7157 41.588C52.8278 41.8124 53.1831 41.9619 53.7814 42.0367L56.8665 42.4294V44H41.7775V42.4294L44.8627 42.0367C45.461 41.9619 45.8162 41.8124 45.9284 41.588C46.0406 41.3262 46.0967 40.5596 46.0967 39.2882V25.1528C46.0967 22.5725 45.7414 20.7028 45.0309 19.5435C44.3204 18.3469 43.2547 17.7485 41.8336 17.7485C41.2353 17.7485 40.5248 17.9168 39.7021 18.2534C38.8794 18.5525 38.0567 19.1322 37.234 19.9923C36.4113 20.8523 35.7008 22.1051 35.1025 23.7505C35.1399 23.9375 35.1586 24.1618 35.1586 24.4236C35.1586 24.648 35.1586 24.8723 35.1586 25.0967V39.2882C35.1586 40.5596 35.2147 41.3262 35.3269 41.588C35.4391 41.8124 35.7943 41.9619 36.3926 42.0367L39.4777 42.4294Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 505 B After Width: | Height: | Size: 2.0 KiB |
@@ -1,10 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-label="m logo">
|
||||
<rect x="32" y="32" width="448" height="448" rx="96" ry="96" fill="#000000"/>
|
||||
<text x="256" y="330"
|
||||
text-anchor="middle"
|
||||
font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, 'Apple Color Emoji','Segoe UI Emoji'"
|
||||
font-size="300"
|
||||
font-weight="800"
|
||||
fill="#ffffff"
|
||||
letter-spacing="-8">m</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 505 B |
|
Before Width: | Height: | Size: 487 B After Width: | Height: | Size: 70 KiB |
4
public/logo.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="64" height="57" viewBox="0 0 64 57" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="57" rx="6" fill="black"/>
|
||||
<path d="M39.4777 42.4294V44H24.3888V42.4294L27.4739 42.0367C28.0722 41.9619 28.4275 41.8124 28.5396 41.588C28.6518 41.3262 28.7079 40.5596 28.7079 39.2882V25.1528C28.7079 22.5725 28.3527 20.7028 27.6422 19.5435C26.9316 18.3469 25.8659 17.7485 24.4449 17.7485C23.8465 17.7485 23.136 17.9168 22.3133 18.2534C21.4906 18.5525 20.668 19.1135 19.8453 19.9362C19.06 20.7589 18.3681 21.9742 17.7698 23.5822V39.2882C17.7698 40.5596 17.8259 41.3262 17.9381 41.588C18.0503 41.8124 18.4055 41.9619 19.0039 42.0367L22.089 42.4294V44H7V42.4294L10.0851 42.0367C10.6834 41.9619 11.0387 41.8124 11.1509 41.588C11.2631 41.3262 11.3191 40.5596 11.3191 39.2882V20.2727C11.3191 19.0013 11.2631 18.2534 11.1509 18.029C11.0387 17.7672 10.6834 17.599 10.0851 17.5242L7 17.1315V15.5609H17.4333V20.4971C18.4803 18.5899 19.8266 17.2063 21.4719 16.3462C23.1173 15.4487 24.7814 15 26.4642 15C28.4836 15 30.185 15.5235 31.5687 16.5706C32.9897 17.5803 33.9993 19.0387 34.5977 20.9458C35.6073 18.8517 36.9723 17.3372 38.6924 16.4023C40.4126 15.4674 42.1328 15 43.853 15C46.508 15 48.6209 15.8788 50.1915 17.6364C51.7621 19.3939 52.5474 21.8807 52.5474 25.0967V39.2882C52.5474 40.5596 52.6035 41.3262 52.7157 41.588C52.8278 41.8124 53.1831 41.9619 53.7814 42.0367L56.8665 42.4294V44H41.7775V42.4294L44.8627 42.0367C45.461 41.9619 45.8162 41.8124 45.9284 41.588C46.0406 41.3262 46.0967 40.5596 46.0967 39.2882V25.1528C46.0967 22.5725 45.7414 20.7028 45.0309 19.5435C44.3204 18.3469 43.2547 17.7485 41.8336 17.7485C41.2353 17.7485 40.5248 17.9168 39.7021 18.2534C38.8794 18.5525 38.0567 19.1322 37.234 19.9923C36.4113 20.8523 35.7008 22.1051 35.1025 23.7505C35.1399 23.9375 35.1586 24.1618 35.1586 24.4236C35.1586 24.648 35.1586 24.8723 35.1586 25.0967V39.2882C35.1586 40.5596 35.2147 41.3262 35.3269 41.588C35.4391 41.8124 35.7943 41.9619 36.3926 42.0367L39.4777 42.4294Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -7,6 +7,58 @@ Date: 2025-12-21
|
||||
|
||||
All notable changes to this project.
|
||||
|
||||
## v1.19.1
|
||||
|
||||
Released December 21, 2025
|
||||
|
||||
**GitHub Stars on Stats page**
|
||||
|
||||
- New GitHub Stars card displays live star count from repository
|
||||
- Fetches from GitHub public API (no token required)
|
||||
- Uses Phosphor GithubLogo icon
|
||||
- Stats page now shows 6 cards in a single row
|
||||
- Responsive layout: 3x2 on tablet, 2x3 on mobile, stacked on small screens
|
||||
|
||||
Updated files: `src/pages/Stats.tsx`, `src/styles/global.css`
|
||||
|
||||
## v1.19.0
|
||||
|
||||
Released December 21, 2025
|
||||
|
||||
**Author display for posts and pages**
|
||||
|
||||
- New optional `authorName` and `authorImage` frontmatter fields
|
||||
- Round avatar image displayed next to date and read time
|
||||
- Works on individual post and page views (not on blog list)
|
||||
- Write page updated with new frontmatter field reference
|
||||
|
||||
Example frontmatter:
|
||||
|
||||
```yaml
|
||||
authorName: "Your Name"
|
||||
authorImage: "/images/authors/photo.png"
|
||||
```
|
||||
|
||||
Place author avatar images in `public/images/authors/`. Recommended: square images (they display as circles).
|
||||
|
||||
Updated files: `convex/schema.ts`, `scripts/sync-posts.ts`, `convex/posts.ts`, `convex/pages.ts`, `src/pages/Post.tsx`, `src/pages/Write.tsx`, `src/styles/global.css`
|
||||
|
||||
Documentation updated: setup-guide.md, docs.md, files.md, README.md, AGENTS.md
|
||||
|
||||
New PRD: `prds/howto-Frontmatter.md` with reusable prompt for future frontmatter updates.
|
||||
|
||||
## v1.18.1
|
||||
|
||||
Released December 21, 2025
|
||||
|
||||
**CopyPageDropdown raw markdown URLs**
|
||||
|
||||
- AI services (ChatGPT, Claude, Perplexity) now receive raw markdown file URLs instead of page URLs
|
||||
- URL format: `/raw/{slug}.md` (e.g., `/raw/setup-guide.md`)
|
||||
- AI services can fetch and parse clean markdown content directly
|
||||
- Includes metadata headers for structured parsing
|
||||
- No HTML parsing required by AI services
|
||||
|
||||
## v1.18.0
|
||||
|
||||
Released December 20, 2025
|
||||
@@ -26,7 +78,7 @@ Two options for fork setup:
|
||||
The configure script updates all 11 configuration files:
|
||||
|
||||
| File | What it updates |
|
||||
| ----------------------------------- | ---------------------------------------- |
|
||||
| ----------------------------------- | -------------------------------------- |
|
||||
| `src/config/siteConfig.ts` | Site name, bio, GitHub, features |
|
||||
| `src/pages/Home.tsx` | Intro paragraph, footer links |
|
||||
| `src/pages/Post.tsx` | SITE_URL, SITE_NAME constants |
|
||||
|
||||
@@ -95,6 +95,8 @@ Content here...
|
||||
| `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 |
|
||||
|
||||
### Static pages
|
||||
|
||||
@@ -121,6 +123,33 @@ Content here...
|
||||
| `image` | No | Thumbnail for featured 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 |
|
||||
|
||||
### 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:
|
||||
|
||||
**Content directories:**
|
||||
|
||||
- `content/blog/*.md` contains blog posts with frontmatter
|
||||
- `content/pages/*.md` contains static pages with frontmatter
|
||||
|
||||
**Processing flow:**
|
||||
|
||||
1. Markdown files in `content/blog/` and `content/pages/` contain YAML frontmatter
|
||||
2. `scripts/sync-posts.ts` uses `gray-matter` to parse frontmatter and validate required fields
|
||||
3. Parsed data is sent to Convex mutations (`api.posts.syncPostsPublic`, `api.pages.syncPagesPublic`)
|
||||
4. `convex/schema.ts` defines the database structure for storing the data
|
||||
|
||||
**Adding a new frontmatter field:**
|
||||
|
||||
To add a custom frontmatter field, update these files:
|
||||
|
||||
1. The interface in `scripts/sync-posts.ts` (`PostFrontmatter` or `PageFrontmatter`)
|
||||
2. The parsing logic in `parseMarkdownFile()` or `parsePageFile()` functions
|
||||
3. The schema in `convex/schema.ts`
|
||||
4. The sync mutation in `convex/posts.ts` or `convex/pages.ts`
|
||||
|
||||
### Syncing content
|
||||
|
||||
@@ -515,15 +544,15 @@ Each post and page includes a share dropdown with options:
|
||||
| Option | Description |
|
||||
| ---------------- | ------------------------------------------------ |
|
||||
| Copy page | Copies formatted markdown to clipboard |
|
||||
| Open in ChatGPT | Opens ChatGPT with article content |
|
||||
| Open in Claude | Opens Claude with article content |
|
||||
| Open in Perplexity | Opens Perplexity for research with content |
|
||||
| Open in ChatGPT | Opens ChatGPT with raw markdown URL |
|
||||
| Open in Claude | Opens Claude with raw markdown URL |
|
||||
| Open in Perplexity | Opens Perplexity with raw markdown URL |
|
||||
| View as Markdown | Opens raw `.md` file in new tab |
|
||||
| Generate Skill | Downloads `{slug}-skill.md` for AI agent training |
|
||||
|
||||
**Generate Skill:** Formats the content as an AI agent skill file with metadata, when to use, and instructions sections.
|
||||
**Raw markdown URLs:** AI services receive the URL to the raw markdown file (e.g., `/raw/setup-guide.md`) instead of the page URL. This provides direct access to clean markdown content with metadata headers for better AI parsing.
|
||||
|
||||
**Long content:** If content exceeds URL limits, it copies to clipboard and opens the AI service in a new tab. Paste to continue.
|
||||
**Generate Skill:** Formats the content as an AI agent skill file with metadata, when to use, and instructions sections.
|
||||
|
||||
## Real-time stats
|
||||
|
||||
@@ -648,6 +677,8 @@ export default defineSchema({
|
||||
excerpt: v.optional(v.string()), // For card view
|
||||
featured: v.optional(v.boolean()), // Show in featured section
|
||||
featuredOrder: v.optional(v.number()), // Order in featured (lower = first)
|
||||
authorName: v.optional(v.string()), // Author display name
|
||||
authorImage: v.optional(v.string()), // Author avatar image URL
|
||||
lastSyncedAt: v.number(),
|
||||
})
|
||||
.index("by_slug", ["slug"])
|
||||
@@ -664,6 +695,8 @@ export default defineSchema({
|
||||
image: v.optional(v.string()), // Thumbnail for featured cards
|
||||
featured: v.optional(v.boolean()), // Show in featured section
|
||||
featuredOrder: v.optional(v.number()), // Order in featured (lower = first)
|
||||
authorName: v.optional(v.string()), // Author display name
|
||||
authorImage: v.optional(v.string()), // Author avatar image URL
|
||||
lastSyncedAt: v.number(),
|
||||
})
|
||||
.index("by_slug", ["slug"])
|
||||
|
||||
@@ -35,6 +35,7 @@ This guide walks you through forking [this markdown framework](https://github.co
|
||||
- [Step 8: Set Up Production Convex](#step-8-set-up-production-convex)
|
||||
- [Writing Blog Posts](#writing-blog-posts)
|
||||
- [Frontmatter Fields](#frontmatter-fields)
|
||||
- [How Frontmatter Works](#how-frontmatter-works)
|
||||
- [Adding Images](#adding-images)
|
||||
- [Sync After Adding Posts](#sync-after-adding-posts)
|
||||
- [Environment Files](#environment-files)
|
||||
@@ -141,6 +142,8 @@ export default defineSchema({
|
||||
excerpt: v.optional(v.string()),
|
||||
featured: v.optional(v.boolean()),
|
||||
featuredOrder: v.optional(v.number()),
|
||||
authorName: v.optional(v.string()),
|
||||
authorImage: v.optional(v.string()),
|
||||
lastSyncedAt: v.number(),
|
||||
})
|
||||
.index("by_slug", ["slug"])
|
||||
@@ -157,6 +160,8 @@ export default defineSchema({
|
||||
image: v.optional(v.string()),
|
||||
featured: v.optional(v.boolean()),
|
||||
featuredOrder: v.optional(v.number()),
|
||||
authorName: v.optional(v.string()),
|
||||
authorImage: v.optional(v.string()),
|
||||
lastSyncedAt: v.number(),
|
||||
})
|
||||
.index("by_slug", ["slug"])
|
||||
@@ -321,6 +326,33 @@ Your markdown content here...
|
||||
| `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 |
|
||||
|
||||
### 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:
|
||||
|
||||
**Content directories:**
|
||||
|
||||
- `content/blog/*.md` contains blog posts with frontmatter
|
||||
- `content/pages/*.md` contains static pages with frontmatter
|
||||
|
||||
**Processing flow:**
|
||||
|
||||
1. Markdown files in `content/blog/` and `content/pages/` contain YAML frontmatter
|
||||
2. `scripts/sync-posts.ts` uses `gray-matter` to parse frontmatter and validate required fields
|
||||
3. Parsed data is sent to Convex mutations (`api.posts.syncPostsPublic`, `api.pages.syncPagesPublic`)
|
||||
4. `convex/schema.ts` defines the database structure for storing the data
|
||||
|
||||
**Adding a new frontmatter field:**
|
||||
|
||||
To add a custom frontmatter field, update these files:
|
||||
|
||||
1. The interface in `scripts/sync-posts.ts` (`PostFrontmatter` or `PageFrontmatter`)
|
||||
2. The parsing logic in `parseMarkdownFile()` or `parsePageFile()` functions
|
||||
3. The schema in `convex/schema.ts`
|
||||
4. The sync mutation in `convex/posts.ts` or `convex/pages.ts`
|
||||
|
||||
### Adding Images
|
||||
|
||||
@@ -843,11 +875,13 @@ 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 |
|
||||
|
||||
3. Run `npm run sync` to sync pages
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ interface PostFrontmatter {
|
||||
excerpt?: string; // Short excerpt for card view
|
||||
featured?: boolean; // Show in featured section
|
||||
featuredOrder?: number; // Order in featured section (lower = first)
|
||||
authorName?: string; // Author display name
|
||||
authorImage?: string; // Author avatar image URL (round)
|
||||
}
|
||||
|
||||
interface ParsedPost {
|
||||
@@ -49,6 +51,8 @@ interface ParsedPost {
|
||||
excerpt?: string; // Short excerpt for card view
|
||||
featured?: boolean; // Show in featured section
|
||||
featuredOrder?: number; // Order in featured section (lower = first)
|
||||
authorName?: string; // Author display name
|
||||
authorImage?: string; // Author avatar image URL (round)
|
||||
}
|
||||
|
||||
// Page frontmatter (for static pages like About, Projects, Contact)
|
||||
@@ -61,6 +65,8 @@ interface PageFrontmatter {
|
||||
image?: string; // Thumbnail/OG image URL for featured cards
|
||||
featured?: boolean; // Show in featured section
|
||||
featuredOrder?: number; // Order in featured section (lower = first)
|
||||
authorName?: string; // Author display name
|
||||
authorImage?: string; // Author avatar image URL (round)
|
||||
}
|
||||
|
||||
interface ParsedPage {
|
||||
@@ -73,6 +79,8 @@ interface ParsedPage {
|
||||
image?: string; // Thumbnail/OG image URL for featured cards
|
||||
featured?: boolean; // Show in featured section
|
||||
featuredOrder?: number; // Order in featured section (lower = first)
|
||||
authorName?: string; // Author display name
|
||||
authorImage?: string; // Author avatar image URL (round)
|
||||
}
|
||||
|
||||
// Calculate reading time based on word count
|
||||
@@ -110,6 +118,8 @@ function parseMarkdownFile(filePath: string): ParsedPost | null {
|
||||
excerpt: frontmatter.excerpt, // Short excerpt for card view
|
||||
featured: frontmatter.featured, // Show in featured section
|
||||
featuredOrder: frontmatter.featuredOrder, // Order in featured section
|
||||
authorName: frontmatter.authorName, // Author display name
|
||||
authorImage: frontmatter.authorImage, // Author avatar image URL
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error parsing ${filePath}:`, error);
|
||||
@@ -157,6 +167,8 @@ function parsePageFile(filePath: string): ParsedPage | null {
|
||||
image: frontmatter.image, // Thumbnail/OG image URL for featured cards
|
||||
featured: frontmatter.featured, // Show in featured section
|
||||
featuredOrder: frontmatter.featuredOrder, // Order in featured section
|
||||
authorName: frontmatter.authorName, // Author display name
|
||||
authorImage: frontmatter.authorImage, // Author avatar image URL
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error parsing page ${filePath}:`, error);
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { Copy, MessageSquare, Sparkles, Search, Check, AlertCircle, FileText, Download } from "lucide-react";
|
||||
import {
|
||||
Copy,
|
||||
MessageSquare,
|
||||
Sparkles,
|
||||
Search,
|
||||
Check,
|
||||
AlertCircle,
|
||||
FileText,
|
||||
Download,
|
||||
} from "lucide-react";
|
||||
|
||||
// Maximum URL length for query parameters (conservative limit)
|
||||
const MAX_URL_LENGTH = 6000;
|
||||
@@ -14,9 +23,11 @@ interface AIService {
|
||||
supportsUrlPrefill: boolean;
|
||||
// Custom URL builder for services with special formats
|
||||
buildUrl?: (prompt: string) => string;
|
||||
// URL-based builder - takes raw markdown file URL for better AI parsing
|
||||
buildUrlFromRawMarkdown?: (rawMarkdownUrl: string) => string;
|
||||
}
|
||||
|
||||
// All services send the full markdown content directly
|
||||
// AI services configuration - uses raw markdown URLs for better AI parsing
|
||||
const AI_SERVICES: AIService[] = [
|
||||
{
|
||||
id: "chatgpt",
|
||||
@@ -25,17 +36,27 @@ const AI_SERVICES: AIService[] = [
|
||||
baseUrl: "https://chatgpt.com/",
|
||||
description: "Analyze with ChatGPT",
|
||||
supportsUrlPrefill: true,
|
||||
// ChatGPT accepts ?q= with full text content
|
||||
buildUrl: (prompt) => `https://chatgpt.com/?q=${encodeURIComponent(prompt)}`,
|
||||
// Uses raw markdown file URL for direct content access
|
||||
buildUrlFromRawMarkdown: (rawMarkdownUrl) => {
|
||||
const prompt =
|
||||
`Summarize the page and then ask what the user needs help with. Be concise and to the point.\n\n` +
|
||||
`Here is the raw markdown file URL:\n${rawMarkdownUrl}`;
|
||||
return `https://chatgpt.com/?q=${encodeURIComponent(prompt)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "claude",
|
||||
name: "Claude",
|
||||
icon: Sparkles,
|
||||
baseUrl: "https://claude.ai/new",
|
||||
baseUrl: "https://claude.ai/",
|
||||
description: "Analyze with Claude",
|
||||
supportsUrlPrefill: true,
|
||||
buildUrl: (prompt) => `https://claude.ai/new?q=${encodeURIComponent(prompt)}`,
|
||||
buildUrlFromRawMarkdown: (rawMarkdownUrl) => {
|
||||
const prompt =
|
||||
`Summarize the page and then ask what the user needs help with. Be concise and to the point.\n\n` +
|
||||
`Here is the raw markdown file URL:\n${rawMarkdownUrl}`;
|
||||
return `https://claude.ai/new?q=${encodeURIComponent(prompt)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "perplexity",
|
||||
@@ -44,7 +65,12 @@ const AI_SERVICES: AIService[] = [
|
||||
baseUrl: "https://www.perplexity.ai/search",
|
||||
description: "Research with Perplexity",
|
||||
supportsUrlPrefill: true,
|
||||
buildUrl: (prompt) => `https://www.perplexity.ai/search?q=${encodeURIComponent(prompt)}`,
|
||||
buildUrlFromRawMarkdown: (rawMarkdownUrl) => {
|
||||
const prompt =
|
||||
`Summarize the page and then ask what the user needs help with. Be concise and to the point.\n\n` +
|
||||
`Here is the raw markdown file URL:\n${rawMarkdownUrl}`;
|
||||
return `https://www.perplexity.ai/search?q=${encodeURIComponent(prompt)}`;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -195,7 +221,7 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
|
||||
const items = menu.querySelectorAll<HTMLButtonElement>(".copy-page-item");
|
||||
const currentIndex = Array.from(items).findIndex(
|
||||
(item) => item === document.activeElement
|
||||
(item) => item === document.activeElement,
|
||||
);
|
||||
|
||||
switch (event.key) {
|
||||
@@ -290,9 +316,21 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
};
|
||||
|
||||
// Generic handler for opening AI services
|
||||
// All services receive the full markdown content directly
|
||||
// Uses raw markdown URL for better AI parsing
|
||||
// IMPORTANT: window.open must happen BEFORE any await to avoid popup blockers
|
||||
const handleOpenInAI = async (service: AIService) => {
|
||||
// Use raw markdown URL for better AI parsing
|
||||
if (service.buildUrlFromRawMarkdown) {
|
||||
// Build raw markdown URL from page URL and slug
|
||||
const origin = new URL(props.url).origin;
|
||||
const rawMarkdownUrl = `${origin}/raw/${props.slug}.md`;
|
||||
const targetUrl = service.buildUrlFromRawMarkdown(rawMarkdownUrl);
|
||||
window.open(targetUrl, "_blank");
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Other services: send full markdown content
|
||||
const markdown = formatAsMarkdown(props);
|
||||
const prompt = `Please analyze this article:\n\n${markdown}`;
|
||||
|
||||
@@ -337,7 +375,9 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
// Handle download skill file (Anthropic Agent Skills format)
|
||||
const handleDownloadSkill = () => {
|
||||
const skillContent = formatAsSkill(props);
|
||||
const blob = new Blob([skillContent], { type: "text/markdown;charset=utf-8" });
|
||||
const blob = new Blob([skillContent], {
|
||||
type: "text/markdown;charset=utf-8",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create temporary link and trigger download as SKILL.md
|
||||
@@ -363,7 +403,9 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
case "copied":
|
||||
return <Check size={16} className="copy-page-icon feedback-success" />;
|
||||
case "error":
|
||||
return <AlertCircle size={16} className="copy-page-icon feedback-error" />;
|
||||
return (
|
||||
<AlertCircle size={16} className="copy-page-icon feedback-error" />
|
||||
);
|
||||
case "url-too-long":
|
||||
return <Check size={16} className="copy-page-icon feedback-warning" />;
|
||||
default:
|
||||
@@ -447,7 +489,9 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
<div className="copy-page-item-content">
|
||||
<span className="copy-page-item-title">
|
||||
Open in {service.name}
|
||||
<span className="external-arrow" aria-hidden="true">↗</span>
|
||||
<span className="external-arrow" aria-hidden="true">
|
||||
↗
|
||||
</span>
|
||||
</span>
|
||||
<span className="copy-page-item-desc">
|
||||
{service.description}
|
||||
@@ -471,11 +515,11 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
<div className="copy-page-item-content">
|
||||
<span className="copy-page-item-title">
|
||||
View as Markdown
|
||||
<span className="external-arrow" aria-hidden="true">↗</span>
|
||||
<span className="external-arrow" aria-hidden="true">
|
||||
↗
|
||||
</span>
|
||||
<span className="copy-page-item-desc">
|
||||
Open raw .md file
|
||||
</span>
|
||||
<span className="copy-page-item-desc">Open raw .md file</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -488,9 +532,7 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
||||
>
|
||||
<Download size={16} className="copy-page-icon" aria-hidden="true" />
|
||||
<div className="copy-page-item-content">
|
||||
<span className="copy-page-item-title">
|
||||
Download as SKILL.md
|
||||
</span>
|
||||
<span className="copy-page-item-title">Download as SKILL.md</span>
|
||||
<span className="copy-page-item-desc">
|
||||
Anthropic Agent Skills format
|
||||
</span>
|
||||
|
||||
@@ -72,7 +72,7 @@ export const siteConfig: SiteConfig = {
|
||||
// Optional logo/header image (place in public/images/, set to null to hide)
|
||||
logo: "/images/logo.svg",
|
||||
intro: null, // Set in Home.tsx to allow JSX with links
|
||||
bio: `Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents.`,
|
||||
bio: `Your content is instantly available to browsers, LLMs, and AI agents.`,
|
||||
|
||||
// Featured section configuration
|
||||
// viewMode: 'list' shows bullet list, 'cards' shows card grid with excerpts
|
||||
|
||||
@@ -97,16 +97,8 @@ export default function Home() {
|
||||
<strong>
|
||||
An open-source publishing framework for AI agents and developers.
|
||||
</strong>{" "}
|
||||
Write markdown, sync from the terminal.{" "}
|
||||
<a
|
||||
href="https://github.com/waynesutton/markdown-site"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="home-text-link"
|
||||
>
|
||||
Fork it
|
||||
</a>
|
||||
, customize it, ship it.
|
||||
<br />
|
||||
Write markdown, sync from the terminal.
|
||||
</p>
|
||||
|
||||
<p className="home-bio">{siteConfig.bio}</p>
|
||||
|
||||
@@ -163,6 +163,23 @@ export default function Post() {
|
||||
<article className="post-article">
|
||||
<header className="post-header">
|
||||
<h1 className="post-title">{page.title}</h1>
|
||||
{/* Author avatar and name for pages (optional) */}
|
||||
{(page.authorImage || page.authorName) && (
|
||||
<div className="post-meta-header">
|
||||
<div className="post-author">
|
||||
{page.authorImage && (
|
||||
<img
|
||||
src={page.authorImage}
|
||||
alt={page.authorName || "Author"}
|
||||
className="post-author-image"
|
||||
/>
|
||||
)}
|
||||
{page.authorName && (
|
||||
<span className="post-author-name">{page.authorName}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<BlogPost content={page.content} />
|
||||
@@ -228,6 +245,22 @@ export default function Post() {
|
||||
<header className="post-header">
|
||||
<h1 className="post-title">{post.title}</h1>
|
||||
<div className="post-meta-header">
|
||||
{/* Author avatar and name (optional) */}
|
||||
{(post.authorImage || post.authorName) && (
|
||||
<div className="post-author">
|
||||
{post.authorImage && (
|
||||
<img
|
||||
src={post.authorImage}
|
||||
alt={post.authorName || "Author"}
|
||||
className="post-author-image"
|
||||
/>
|
||||
)}
|
||||
{post.authorName && (
|
||||
<span className="post-author-name">{post.authorName}</span>
|
||||
)}
|
||||
<span className="post-meta-separator">·</span>
|
||||
</div>
|
||||
)}
|
||||
<time className="post-date">
|
||||
{format(parseISO(post.date), "MMMM yyyy")}
|
||||
</time>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery } from "convex/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { api } from "../../convex/_generated/api";
|
||||
@@ -8,9 +9,8 @@ import {
|
||||
FileText,
|
||||
BookOpen,
|
||||
Activity,
|
||||
TrendingUp,
|
||||
Code,
|
||||
} from "lucide-react";
|
||||
import { GithubLogo } from "@phosphor-icons/react";
|
||||
|
||||
// Site launched Dec 14, 2025 at 1:00 PM (v1.0.0), stats added same day (v1.2.0)
|
||||
const SITE_LAUNCH_DATE = "Dec 14, 2025 at 1:00 PM";
|
||||
@@ -35,6 +35,17 @@ export default function Stats() {
|
||||
const navigate = useNavigate();
|
||||
const stats = useQuery(api.stats.getStats);
|
||||
|
||||
// GitHub stars state
|
||||
const [githubStars, setGithubStars] = useState<number | null>(null);
|
||||
|
||||
// Fetch GitHub stars on mount
|
||||
useEffect(() => {
|
||||
fetch("https://api.github.com/repos/waynesutton/markdown-site")
|
||||
.then((res) => res.json())
|
||||
.then((data) => setGithubStars(data.stargazers_count))
|
||||
.catch(() => setGithubStars(null));
|
||||
}, []);
|
||||
|
||||
// Don't render until stats load
|
||||
if (stats === undefined) {
|
||||
return null;
|
||||
@@ -78,6 +89,13 @@ export default function Stats() {
|
||||
value: stats.publishedPages,
|
||||
description: "Static pages",
|
||||
},
|
||||
{
|
||||
number: "06",
|
||||
icon: GithubLogo,
|
||||
title: "GitHub Stars",
|
||||
value: githubStars ?? "...",
|
||||
description: "waynesutton/markdown-site",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -35,6 +35,8 @@ const POST_FIELDS = [
|
||||
},
|
||||
{ name: "featured", required: false, example: "true" },
|
||||
{ name: "featuredOrder", required: false, example: "1" },
|
||||
{ name: "authorName", required: false, example: '"Jane Doe"' },
|
||||
{ name: "authorImage", required: false, example: '"/images/authors/jane.png"' },
|
||||
];
|
||||
|
||||
// Frontmatter field definitions for pages
|
||||
@@ -47,6 +49,8 @@ const PAGE_FIELDS = [
|
||||
{ name: "image", required: false, example: '"/images/thumbnail.png"' },
|
||||
{ name: "featured", required: false, example: "true" },
|
||||
{ name: "featuredOrder", required: false, example: "1" },
|
||||
{ name: "authorName", required: false, example: '"Jane Doe"' },
|
||||
{ name: "authorImage", required: false, example: '"/images/authors/jane.png"' },
|
||||
];
|
||||
|
||||
// Generate frontmatter template based on content type
|
||||
|
||||
@@ -750,6 +750,27 @@ body {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Author display in post header */
|
||||
.post-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.post-author-image {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.post-author-name {
|
||||
font-size: var(--font-size-post-meta-header);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.post-description {
|
||||
font-size: var(--font-size-post-description);
|
||||
color: var(--text-secondary);
|
||||
@@ -1462,7 +1483,7 @@ body {
|
||||
/* Modern horizontal cards container - 5 equal columns on large screens */
|
||||
.stats-cards-modern {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 0;
|
||||
margin-bottom: 64px;
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -1471,10 +1492,10 @@ body {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* Ensure 5-column layout on large screens (explicit rule) */
|
||||
/* Ensure 6-column layout on large screens (explicit rule) */
|
||||
@media (min-width: 1101px) {
|
||||
.stats-cards-modern {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1707,18 +1728,26 @@ body {
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.stat-card-modern:nth-child(3) {
|
||||
/* Remove right border on last card of each row (3rd, 6th) */
|
||||
.stat-card-modern:nth-child(3),
|
||||
.stat-card-modern:nth-child(6) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Add top border on second row (4th, 5th, 6th) */
|
||||
.stat-card-modern:nth-child(4),
|
||||
.stat-card-modern:nth-child(5) {
|
||||
.stat-card-modern:nth-child(5),
|
||||
.stat-card-modern:nth-child(6) {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.stat-card-modern:nth-child(4) {
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.stat-card-modern:nth-child(5) {
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -1735,19 +1764,23 @@ body {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
/* Remove right border on even cards (end of each row) */
|
||||
.stat-card-modern:nth-child(2n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Add top border on rows 2 and 3 (cards 3-6) */
|
||||
.stat-card-modern:nth-child(3),
|
||||
.stat-card-modern:nth-child(4),
|
||||
.stat-card-modern:nth-child(5) {
|
||||
.stat-card-modern:nth-child(5),
|
||||
.stat-card-modern:nth-child(6) {
|
||||
border-top: 1px solid var(--border-color);
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.stat-card-modern:nth-child(4) {
|
||||
border-right: none;
|
||||
/* Odd cards in rows 2+ need right border */
|
||||
.stat-card-modern:nth-child(3),
|
||||
.stat-card-modern:nth-child(5) {
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.stat-card-modern-value {
|
||||
|
||||