diff --git a/AGENTS.md b/AGENTS.md index 367cc8f..fd4a73a 100644 --- a/AGENTS.md +++ b/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 diff --git a/README.md b/README.md index 7cae884..ea27a43 100644 --- a/README.md +++ b/README.md @@ -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 @@ -234,10 +234,12 @@ Then run `npm run sync` (dev) or `npm run sync:prod` (production). No redeploy n | 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) | +| `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 diff --git a/TASK.md b/TASK.md index 95358ea..355d38b 100644 --- a/TASK.md +++ b/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 diff --git a/changelog.md b/changelog.md index d6d82ff..f866bdc 100644 --- a/changelog.md +++ b/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 diff --git a/content/blog/about-this-blog.md b/content/blog/about-this-blog.md index aca45d7..41865b9 100644 --- a/content/blog/about-this-blog.md +++ b/content/blog/about-this-blog.md @@ -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 diff --git a/content/blog/fork-configuration-guide.md b/content/blog/fork-configuration-guide.md index b741c92..5d8b8ff 100644 --- a/content/blog/fork-configuration-guide.md +++ b/content/blog/fork-configuration-guide.md @@ -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." --- diff --git a/content/blog/how-to-publish.md b/content/blog/how-to-publish.md index af62d10..e647e39 100644 --- a/content/blog/how-to-publish.md +++ b/content/blog/how-to-publish.md @@ -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." --- diff --git a/content/blog/markdown-with-code-examples.md b/content/blog/markdown-with-code-examples.md index 230d328..082b386 100644 --- a/content/blog/markdown-with-code-examples.md +++ b/content/blog/markdown-with-code-examples.md @@ -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" diff --git a/content/blog/new-features-search-featured-logos.md b/content/blog/new-features-search-featured-logos.md index 19a758a..cb67d08 100644 --- a/content/blog/new-features-search-featured-logos.md +++ b/content/blog/new-features-search-featured-logos.md @@ -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." --- diff --git a/content/blog/raw-markdown-and-copy-improvements.md b/content/blog/raw-markdown-and-copy-improvements.md index c5b2855..b06dcbf 100644 --- a/content/blog/raw-markdown-and-copy-improvements.md +++ b/content/blog/raw-markdown-and-copy-improvements.md @@ -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." --- diff --git a/content/blog/setup-guide.md b/content/blog/setup-guide.md index 96fa667..8f17f08 100644 --- a/content/blog/setup-guide.md +++ b/content/blog/setup-guide.md @@ -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 @@ -845,12 +879,14 @@ 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) | +| 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 diff --git a/content/blog/using-images-in-posts.md b/content/blog/using-images-in-posts.md index b431f52..0807996 100644 --- a/content/blog/using-images-in-posts.md +++ b/content/blog/using-images-in-posts.md @@ -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" --- diff --git a/content/pages/changelog-page.md b/content/pages/changelog-page.md index e927877..f8aa76d 100644 --- a/content/pages/changelog-page.md +++ b/content/pages/changelog-page.md @@ -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 @@ -25,19 +77,19 @@ 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 | -| `convex/http.ts` | SITE_URL, SITE_NAME constants | -| `convex/rss.ts` | SITE_URL, SITE_TITLE, SITE_DESCRIPTION | -| `index.html` | Meta tags, JSON-LD, page title | -| `public/llms.txt` | Site info, GitHub link | -| `public/robots.txt` | Sitemap URL | -| `public/openapi.yaml` | Server URL, site name | -| `public/.well-known/ai-plugin.json` | Plugin metadata | -| `src/context/ThemeContext.tsx` | Default theme | +| 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 | +| `convex/http.ts` | SITE_URL, SITE_NAME constants | +| `convex/rss.ts` | SITE_URL, SITE_TITLE, SITE_DESCRIPTION | +| `index.html` | Meta tags, JSON-LD, page title | +| `public/llms.txt` | Site info, GitHub link | +| `public/robots.txt` | Sitemap URL | +| `public/openapi.yaml` | Server URL, site name | +| `public/.well-known/ai-plugin.json` | Plugin metadata | +| `src/context/ThemeContext.tsx` | Default theme | New files: `FORK_CONFIG.md`, `fork-config.json.example`, `scripts/configure-fork.ts` diff --git a/content/pages/docs.md b/content/pages/docs.md index f64482d..c9cfb32 100644 --- a/content/pages/docs.md +++ b/content/pages/docs.md @@ -91,10 +91,12 @@ Content here... | `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) | +| `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 | ### Static pages @@ -116,11 +118,38 @@ Content here... | `title` | Yes | Nav link text | | `slug` | Yes | URL path | | `published` | Yes | `true` to show | -| `order` | No | Nav order (lower = first) | -| `excerpt` | No | Short text for card view | -| `image` | No | Thumbnail for featured card view | -| `featured` | No | `true` to show in featured section | -| `featuredOrder` | No | Order in featured (lower = first) | +| `order` | No | Nav order (lower = first) | +| `excerpt` | No | Short text for card view | +| `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"]) diff --git a/convex/pages.ts b/convex/pages.ts index 71da310..dd71bd3 100644 --- a/convex/pages.ts +++ b/convex/pages.ts @@ -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++; diff --git a/convex/posts.ts b/convex/posts.ts index e6f0177..e38bfc3 100644 --- a/convex/posts.ts +++ b/convex/posts.ts @@ -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++; diff --git a/convex/schema.ts b/convex/schema.ts index 8da87d6..035bb9c 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -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"]) diff --git a/files.md b/files.md index 2b0a05a..51be62e 100644 --- a/files.md +++ b/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 | diff --git a/prds/howto-Frontmatter.md b/prds/howto-Frontmatter.md new file mode 100644 index 0000000..93974ea --- /dev/null +++ b/prds/howto-Frontmatter.md @@ -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 && ( +
+ {post.newField} +
+)} +``` + +### 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` + diff --git a/public/favicon.svg b/public/favicon.svg index a6ce309..2995692 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1,10 +1,4 @@ - - - m + + + diff --git a/public/images/authors/markdown.png b/public/images/authors/markdown.png new file mode 100644 index 0000000..54f06f0 Binary files /dev/null and b/public/images/authors/markdown.png differ diff --git a/public/images/logo.svg b/public/images/logo.svg index a6ce309..2995692 100644 --- a/public/images/logo.svg +++ b/public/images/logo.svg @@ -1,10 +1,4 @@ - - - m + + + diff --git a/public/images/logos/logo.svg b/public/images/logos/logo.svg deleted file mode 100644 index a6ce309..0000000 --- a/public/images/logos/logo.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - m - diff --git a/public/images/og-default.svg b/public/images/og-default.svg index 92100a7..72562fa 100644 --- a/public/images/og-default.svg +++ b/public/images/og-default.svg @@ -1,10 +1,8 @@ - - - markdown site + + + + + + + diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..2995692 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/raw/changelog.md b/public/raw/changelog.md index 42f3d0b..da0abe7 100644 --- a/public/raw/changelog.md +++ b/public/raw/changelog.md @@ -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 @@ -25,19 +77,19 @@ 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 | -| `convex/http.ts` | SITE_URL, SITE_NAME constants | -| `convex/rss.ts` | SITE_URL, SITE_TITLE, SITE_DESCRIPTION | -| `index.html` | Meta tags, JSON-LD, page title | -| `public/llms.txt` | Site info, GitHub link | -| `public/robots.txt` | Sitemap URL | -| `public/openapi.yaml` | Server URL, site name | -| `public/.well-known/ai-plugin.json` | Plugin metadata | -| `src/context/ThemeContext.tsx` | Default theme | +| 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 | +| `convex/http.ts` | SITE_URL, SITE_NAME constants | +| `convex/rss.ts` | SITE_URL, SITE_TITLE, SITE_DESCRIPTION | +| `index.html` | Meta tags, JSON-LD, page title | +| `public/llms.txt` | Site info, GitHub link | +| `public/robots.txt` | Sitemap URL | +| `public/openapi.yaml` | Server URL, site name | +| `public/.well-known/ai-plugin.json` | Plugin metadata | +| `src/context/ThemeContext.tsx` | Default theme | New files: `FORK_CONFIG.md`, `fork-config.json.example`, `scripts/configure-fork.ts` diff --git a/public/raw/docs.md b/public/raw/docs.md index 44ed639..64eb291 100644 --- a/public/raw/docs.md +++ b/public/raw/docs.md @@ -91,10 +91,12 @@ Content here... | `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) | +| `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 | ### Static pages @@ -116,11 +118,38 @@ Content here... | `title` | Yes | Nav link text | | `slug` | Yes | URL path | | `published` | Yes | `true` to show | -| `order` | No | Nav order (lower = first) | -| `excerpt` | No | Short text for card view | -| `image` | No | Thumbnail for featured card view | -| `featured` | No | `true` to show in featured section | -| `featuredOrder` | No | Order in featured (lower = first) | +| `order` | No | Nav order (lower = first) | +| `excerpt` | No | Short text for card view | +| `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"]) diff --git a/public/raw/setup-guide.md b/public/raw/setup-guide.md index 1bc48dc..a1a2b4d 100644 --- a/public/raw/setup-guide.md +++ b/public/raw/setup-guide.md @@ -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 @@ -842,12 +874,14 @@ 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) | +| 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 diff --git a/scripts/sync-posts.ts b/scripts/sync-posts.ts index b290ece..e2cce9c 100644 --- a/scripts/sync-posts.ts +++ b/scripts/sync-posts.ts @@ -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); diff --git a/src/components/CopyPageDropdown.tsx b/src/components/CopyPageDropdown.tsx index 31d73da..d56f2d3 100644 --- a/src/components/CopyPageDropdown.tsx +++ b/src/components/CopyPageDropdown.tsx @@ -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)}`; + }, }, ]; @@ -63,28 +89,28 @@ interface CopyPageDropdownProps { // Enhanced markdown format for better LLM parsing function formatAsMarkdown(props: CopyPageDropdownProps): string { const { title, content, url, description, date, tags, readTime } = props; - + // Build metadata section const metadataLines: string[] = []; metadataLines.push(`Source: ${url}`); if (date) metadataLines.push(`Date: ${date}`); if (readTime) metadataLines.push(`Reading time: ${readTime}`); if (tags && tags.length > 0) metadataLines.push(`Tags: ${tags.join(", ")}`); - + // Build the full markdown document let markdown = `# ${title}\n\n`; - + // Add description if available if (description) { markdown += `> ${description}\n\n`; } - + // Add metadata block markdown += `---\n${metadataLines.join("\n")}\n---\n\n`; - + // Add main content markdown += content; - + return markdown; } @@ -102,45 +128,45 @@ function generateSkillName(slug: string): string { // Follows: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview function formatAsSkill(props: CopyPageDropdownProps): string { const { title, content, slug, description, tags } = props; - + // Generate compliant skill name const skillName = generateSkillName(slug); - + // Build description with "when to use" triggers (max 1024 chars) const tagList = tags && tags.length > 0 ? tags.join(", ") : ""; let skillDescription = description || `Guide about ${title.toLowerCase()}.`; - + // Add usage triggers to description if (tagList) { skillDescription += ` Use when working with ${tagList.toLowerCase()} or when asked about ${title.toLowerCase()}.`; } else { skillDescription += ` Use when asked about ${title.toLowerCase()}.`; } - + // Truncate description if needed (max 1024 chars) if (skillDescription.length > 1024) { skillDescription = skillDescription.slice(0, 1021) + "..."; } - + // Build YAML frontmatter (required by Agent Skills spec) let skill = `---\n`; skill += `name: ${skillName}\n`; skill += `description: ${skillDescription}\n`; skill += `---\n\n`; - + // Add title skill += `# ${title}\n\n`; - + // Add instructions section skill += `## Instructions\n\n`; skill += content; - + // Add examples section placeholder if content doesn't include examples if (!content.toLowerCase().includes("## example")) { skill += `\n\n## Examples\n\n`; skill += `Use this skill when the user asks about topics covered in this guide.\n`; } - + return skill; } @@ -154,7 +180,7 @@ type FeedbackState = "idle" | "copied" | "error" | "url-too-long"; export default function CopyPageDropdown(props: CopyPageDropdownProps) { const { title } = props; - + const [isOpen, setIsOpen] = useState(false); const [feedback, setFeedback] = useState("idle"); const [feedbackMessage, setFeedbackMessage] = useState(""); @@ -195,7 +221,7 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) { const items = menu.querySelectorAll(".copy-page-item"); const currentIndex = Array.from(items).findIndex( - (item) => item === document.activeElement + (item) => item === document.activeElement, ); switch (event.key) { @@ -276,7 +302,7 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) { const handleCopyPage = async () => { const markdown = formatAsMarkdown(props); const success = await writeToClipboard(markdown); - + if (success) { setFeedback("copied"); setFeedbackMessage("Copied!"); @@ -284,18 +310,30 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) { setFeedback("error"); setFeedbackMessage("Failed to copy"); } - + clearFeedback(); setTimeout(() => setIsOpen(false), 1500); }; // 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}`; - + // Build the target URL using the service's buildUrl function if (!service.buildUrl) { // Fallback: open base URL FIRST (sync), then copy to clipboard @@ -311,9 +349,9 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) { clearFeedback(); return; } - + const targetUrl = service.buildUrl(prompt); - + // Check URL length - if too long, open base URL then copy to clipboard if (isUrlTooLong(targetUrl)) { // Open window FIRST (must be sync to avoid popup blocker) @@ -337,9 +375,11 @@ 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 const link = document.createElement("a"); link.href = url; @@ -347,10 +387,10 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) { document.body.appendChild(link); link.click(); document.body.removeChild(link); - + // Clean up object URL URL.revokeObjectURL(url); - + setFeedback("copied"); setFeedbackMessage("Downloaded!"); clearFeedback(); @@ -363,7 +403,9 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) { case "copied": return ; case "error": - return ; + return ( + + ); case "url-too-long": return ; default: @@ -447,7 +489,9 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
Open in {service.name} - + {service.description} @@ -471,11 +515,11 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
View as Markdown - - - - Open raw .md file + + Open raw .md file
@@ -488,9 +532,7 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) { >