diff --git a/TASK.md b/TASK.md
index 7863585..bd70631 100644
--- a/TASK.md
+++ b/TASK.md
@@ -2,13 +2,32 @@
## To Do
-- [ ] fix Netlify edge functions blocking AI crawlers from static files
+- [ ] Newsletter signup
+- [ ] Comments system
+- [ ] Draft preview mode
## Current Status
-v1.24.4 deployed. Added `showInNav` field for pages and hardcoded navigation items configuration for React routes.
+v1.26.0 ready. Added tag pages, related posts, and re-enabled AI service links using GitHub raw URLs.
## Completed
+
+- [x] Tag pages at `/tags/[tag]` route with view mode toggle
+- [x] Related posts component for blog post footers (up to 3 related posts by shared tags)
+- [x] Tag links in post footers now navigate to tag archive pages
+- [x] Open in AI links (ChatGPT, Claude, Perplexity) re-enabled using GitHub raw URLs
+- [x] `gitHubRepo` configuration in siteConfig.ts for AI service URL construction
+- [x] `by_tags` index added to posts table in convex/schema.ts
+- [x] New Convex queries: `getAllTags`, `getPostsByTag`, `getRelatedPosts`
+- [x] Sitemap updated to include dynamically generated tag pages
+- [x] Documentation updated with git push requirement for AI links
+- [x] Mobile responsive styling for tag pages and related posts
+- [x] Fixed sidebar border width consistency using box-shadow instead of border-right
+- [x] Hidden sidebar scrollbar while maintaining scroll functionality
+- [x] Added top border and border-radius to sidebar wrapper using CSS variables
+- [x] Updated CSS documentation for sidebar border implementation
+- [x] Fixed mobile menu breakpoint to match sidebar hide breakpoint (1024px)
+- [x] Mobile hamburger menu now shows whenever sidebar is hidden
- [x] add MIT Licensed. Do whatevs.
- [x] Blog page view mode toggle (list and card views)
- [x] Post cards component with thumbnails, titles, excerpts, and metadata
@@ -182,9 +201,6 @@ v1.24.4 deployed. Added `showInNav` field for pages and hardcoded navigation ite
## Someday Features TBD
-- [ ] Related posts suggestions
- [ ] Newsletter signup
- [ ] Comments system
- [ ] Draft preview mode
-
-
diff --git a/changelog.md b/changelog.md
index 1231eb5..51b0527 100644
--- a/changelog.md
+++ b/changelog.md
@@ -4,6 +4,87 @@ 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.26.0] - 2025-12-24
+
+### Added
+
+- Tag pages at `/tags/[tag]` route
+ - Dynamic tag archive pages showing all posts with a specific tag
+ - View mode toggle (list/cards) with localStorage persistence
+ - Mobile responsive layout matching existing blog page design
+ - Sitemap updated to include all tag pages dynamically
+- Related posts component for blog post footers
+ - Shows up to 3 related posts based on shared tags
+ - Sorted by relevance (number of shared tags) then by date
+ - Only displays on blog posts (not static pages)
+- Improved tag links in post footers
+ - Tags now link to `/tags/[tag]` archive pages
+ - Visual styling consistent with existing theme
+- Open in AI service links re-enabled in CopyPageDropdown
+ - Uses GitHub raw URLs instead of Netlify paths (bypasses edge function issues)
+ - ChatGPT, Claude, and Perplexity links with universal prompt
+ - "Requires git push" hint for users (npm sync alone doesn't update GitHub)
+ - Visual divider separating AI options from other menu items
+
+### Changed
+
+- `src/config/siteConfig.ts`: Added `gitHubRepo` configuration for constructing raw GitHub URLs
+- `convex/schema.ts`: Added `by_tags` index to posts table for efficient tag queries
+- `convex/posts.ts`: Added `getAllTags`, `getPostsByTag`, and `getRelatedPosts` queries
+- `convex/http.ts`: Sitemap now includes dynamically generated tag pages
+- Updated `content/pages/docs.md` and `content/blog/setup-guide.md` with git push requirement for AI links
+
+### Technical
+
+- New component: `src/pages/TagPage.tsx`
+- New route: `/tags/:tag` in `src/App.tsx`
+- CSS styles for tag pages, related posts, and post tag links in `src/styles/global.css`
+- Mobile responsive breakpoints for all new components
+
+## [1.25.4] - 2025-12-24
+
+### Fixed
+
+- Sidebar border width now consistent across all pages
+ - Fixed border appearing thicker on changelog page when sidebar scrolls
+ - Changed from `border-right` to `box-shadow: inset` for consistent 1px width regardless of scrollbar presence
+ - Border now renders correctly on both docs and changelog pages
+
+### Changed
+
+- Sidebar scrollbar hidden while maintaining scroll functionality
+ - Scrollbar no longer visible but scrolling still works
+ - Applied cross-browser scrollbar hiding (Chrome/Safari/Edge, Firefox, IE)
+ - Cleaner sidebar appearance matching Cursor docs style
+
+- Sidebar styling improvements
+ - Added top border using CSS variable (`var(--border-sidebar)`) for theme consistency
+ - Added border-radius for rounded corners
+ - Updated CSS comments to document border implementation approach
+
+### Technical
+
+- `src/styles/global.css`: Changed `.post-sidebar-wrapper` border from `border-right` to `box-shadow: inset -1px 0 0`
+- `src/styles/global.css`: Added scrollbar hiding with `-ms-overflow-style: none`, `scrollbar-width: none`, and `::-webkit-scrollbar`
+- `src/styles/global.css`: Added `border-top: 1px solid var(--border-sidebar)` and `border-radius: 8px` to sidebar wrapper
+- `src/styles/global.css`: Updated CSS comments to explain border implementation choices
+
+## [1.25.3] - 2025-12-24
+
+### Fixed
+
+- Mobile menu now appears correctly at all breakpoints where sidebar is hidden
+ - Changed mobile hamburger menu breakpoint from `max-width: 768px` to `max-width: 1024px`
+ - Changed desktop hide breakpoint from `min-width: 769px` to `min-width: 1025px`
+ - Mobile menu now shows whenever sidebar is hidden (matches sidebar breakpoint)
+ - Fixed gap where users had no navigation between 769px and 1024px viewport widths
+
+### Technical
+
+- `src/styles/global.css`: Updated mobile nav controls media query to `max-width: 1024px`
+- `src/styles/global.css`: Updated desktop hide media query to `min-width: 1025px`
+- `src/styles/global.css`: Updated tablet drawer width breakpoint to `max-width: 1024px`
+
## [1.25.2] - 2025-12-24
### Changed
diff --git a/content/blog/setup-guide.md b/content/blog/setup-guide.md
index a98ded0..8fd1f5a 100644
--- a/content/blog/setup-guide.md
+++ b/content/blog/setup-guide.md
@@ -1066,18 +1066,24 @@ On mobile and tablet screens (under 768px), a hamburger menu provides navigation
Each post and page includes a share dropdown with options for AI tools:
-| 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 |
-| View as Markdown | Opens raw `.md` file in new tab |
-| Generate Skill | Downloads `{slug}-skill.md` for AI agent training |
+| Option | Description |
+| -------------------- | ------------------------------------------------- |
+| Copy page | Copies formatted markdown to clipboard |
+| 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 |
+| Download as SKILL.md | Downloads skill file for AI agent training |
-**Generate Skill** formats the content as an AI agent skill file with metadata, when to use, and instructions sections.
+**Git push required for AI links:** The "Open in ChatGPT," "Open in Claude," and "Open in Perplexity" options use GitHub raw URLs to fetch content. For these to work, your content must be pushed to GitHub with `git push`. The `npm run sync` command syncs content to Convex for your live site, but AI services fetch directly from GitHub.
-**Long content:** If content exceeds URL limits, it copies to clipboard and opens the AI service in a new tab. Paste to continue.
+| What you want | Command needed |
+| ------------------------------------ | ------------------------------ |
+| Content visible on your site | `npm run sync` or `sync:prod` |
+| AI links (ChatGPT/Claude/Perplexity) | `git push` to GitHub |
+| Both | `npm run sync` then `git push` |
+
+**Download as SKILL.md** formats the content as an Anthropic Agent Skills file with metadata, triggers, and instructions sections.
## API Endpoints
diff --git a/content/pages/docs.md b/content/pages/docs.md
index 447b4ce..b3c9f27 100644
--- a/content/pages/docs.md
+++ b/content/pages/docs.md
@@ -676,18 +676,26 @@ The menu appears automatically on screens under 768px wide.
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 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 |
+| Option | Description |
+| -------------------- | ------------------------------------------ |
+| Copy page | Copies formatted markdown to clipboard |
+| 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 |
+| Download as SKILL.md | Downloads skill file for AI agent training |
-**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.
+**Raw markdown URLs:** AI service links use GitHub raw URLs to fetch markdown content. This bypasses Netlify edge functions and provides reliable access for AI services.
-**Generate Skill:** Formats the content as an AI agent skill file with metadata, when to use, and instructions sections.
+**Git push required for AI links:** The "Open in ChatGPT," "Open in Claude," and "Open in Perplexity" options use GitHub raw URLs. For these to work, you must push your content to GitHub with `git push`. The `npm run sync` command syncs content to Convex for your live site, but AI services fetch directly from GitHub.
+
+| What you want | Command needed |
+| ------------------------------------ | ------------------------------ |
+| Content visible on your site | `npm run sync` or `sync:prod` |
+| AI links (ChatGPT/Claude/Perplexity) | `git push` to GitHub |
+| Both | `npm run sync` then `git push` |
+
+**Download as SKILL.md:** Downloads the content formatted as an Anthropic Agent Skills file with metadata, triggers, and instructions sections.
## Real-time stats
diff --git a/convex/http.ts b/convex/http.ts
index 5230115..7f8cf44 100644
--- a/convex/http.ts
+++ b/convex/http.ts
@@ -23,13 +23,14 @@ http.route({
handler: rssFullFeed,
});
-// Sitemap.xml endpoint for search engines (includes posts and pages)
+// Sitemap.xml endpoint for search engines (includes posts, pages, and tag pages)
http.route({
path: "/sitemap.xml",
method: "GET",
handler: httpAction(async (ctx) => {
const posts = await ctx.runQuery(api.posts.getAllPosts);
const pages = await ctx.runQuery(api.pages.getAllPages);
+ const tags = await ctx.runQuery(api.posts.getAllTags);
const urls = [
// Homepage
@@ -53,6 +54,14 @@ http.route({
${SITE_URL}/${page.slug}monthly0.7
+ `,
+ ),
+ // All tag pages
+ ...tags.map(
+ (tagInfo) => `
+ ${SITE_URL}/tags/${encodeURIComponent(tagInfo.tag.toLowerCase())}
+ weekly
+ 0.6`,
),
];
diff --git a/convex/posts.ts b/convex/posts.ts
index 0e03073..9958e9e 100644
--- a/convex/posts.ts
+++ b/convex/posts.ts
@@ -369,3 +369,159 @@ export const getViewCount = query({
return viewCount?.count ?? 0;
},
});
+
+// Get all unique tags from published posts
+export const getAllTags = query({
+ args: {},
+ returns: v.array(
+ v.object({
+ tag: v.string(),
+ count: v.number(),
+ }),
+ ),
+ handler: async (ctx) => {
+ const posts = await ctx.db
+ .query("posts")
+ .withIndex("by_published", (q) => q.eq("published", true))
+ .collect();
+
+ // Count occurrences of each tag
+ const tagCounts = new Map();
+ for (const post of posts) {
+ for (const tag of post.tags) {
+ tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
+ }
+ }
+
+ // Convert to array and sort by count (descending), then alphabetically
+ return Array.from(tagCounts.entries())
+ .map(([tag, count]) => ({ tag, count }))
+ .sort((a, b) => {
+ if (b.count !== a.count) return b.count - a.count;
+ return a.tag.localeCompare(b.tag);
+ });
+ },
+});
+
+// Get posts filtered by a specific tag
+export const getPostsByTag = query({
+ args: {
+ tag: v.string(),
+ },
+ returns: v.array(
+ v.object({
+ _id: v.id("posts"),
+ _creationTime: v.number(),
+ slug: v.string(),
+ title: v.string(),
+ description: v.string(),
+ date: v.string(),
+ published: v.boolean(),
+ tags: v.array(v.string()),
+ readTime: v.optional(v.string()),
+ image: v.optional(v.string()),
+ 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, args) => {
+ const posts = await ctx.db
+ .query("posts")
+ .withIndex("by_published", (q) => q.eq("published", true))
+ .collect();
+
+ // Filter posts that have the specified tag
+ const filteredPosts = posts.filter((post) =>
+ post.tags.some((t) => t.toLowerCase() === args.tag.toLowerCase()),
+ );
+
+ // Sort by date descending
+ const sortedPosts = filteredPosts.sort(
+ (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
+ );
+
+ // Return without content for list view
+ return sortedPosts.map((post) => ({
+ _id: post._id,
+ _creationTime: post._creationTime,
+ slug: post.slug,
+ title: post.title,
+ description: post.description,
+ date: post.date,
+ published: post.published,
+ tags: post.tags,
+ readTime: post.readTime,
+ image: post.image,
+ excerpt: post.excerpt,
+ featured: post.featured,
+ featuredOrder: post.featuredOrder,
+ authorName: post.authorName,
+ authorImage: post.authorImage,
+ }));
+ },
+});
+
+// Get related posts that share tags with the current post
+export const getRelatedPosts = query({
+ args: {
+ currentSlug: v.string(),
+ tags: v.array(v.string()),
+ limit: v.optional(v.number()),
+ },
+ returns: v.array(
+ v.object({
+ _id: v.id("posts"),
+ slug: v.string(),
+ title: v.string(),
+ description: v.string(),
+ date: v.string(),
+ tags: v.array(v.string()),
+ readTime: v.optional(v.string()),
+ sharedTags: v.number(),
+ }),
+ ),
+ handler: async (ctx, args) => {
+ const maxResults = args.limit ?? 3;
+
+ // Skip if no tags provided
+ if (args.tags.length === 0) {
+ return [];
+ }
+
+ const posts = await ctx.db
+ .query("posts")
+ .withIndex("by_published", (q) => q.eq("published", true))
+ .collect();
+
+ // Find posts that share tags, excluding current post
+ const relatedPosts = posts
+ .filter((post) => post.slug !== args.currentSlug)
+ .map((post) => {
+ const sharedTags = post.tags.filter((tag) =>
+ args.tags.some((t) => t.toLowerCase() === tag.toLowerCase()),
+ ).length;
+ return {
+ _id: post._id,
+ slug: post.slug,
+ title: post.title,
+ description: post.description,
+ date: post.date,
+ tags: post.tags,
+ readTime: post.readTime,
+ sharedTags,
+ };
+ })
+ .filter((post) => post.sharedTags > 0)
+ .sort((a, b) => {
+ // Sort by shared tags count first, then by date
+ if (b.sharedTags !== a.sharedTags) return b.sharedTags - a.sharedTags;
+ return new Date(b.date).getTime() - new Date(a.date).getTime();
+ })
+ .slice(0, maxResults);
+
+ return relatedPosts;
+ },
+});
diff --git a/files.md b/files.md
index 11a14c1..27b5b0b 100644
--- a/files.md
+++ b/files.md
@@ -33,7 +33,7 @@ A brief description of each file in the codebase.
| File | Description |
| --------------- | --------------------------------------------------------------------------------------------------------- |
-| `siteConfig.ts` | Centralized site configuration (name, logo, blog page, posts display, GitHub contributions, nav order, inner page logo settings, hardcoded navigation items for React routes) |
+| `siteConfig.ts` | Centralized site configuration (name, logo, blog page, posts display, GitHub contributions, nav order, inner page logo settings, hardcoded navigation items for React routes, GitHub repository config for AI service raw URLs) |
### Pages (`src/pages/`)
@@ -41,8 +41,9 @@ A brief description of each file in the codebase.
| ----------- | ----------------------------------------------------------------- |
| `Home.tsx` | Landing page with featured content and optional post list |
| `Blog.tsx` | Dedicated blog page with post list or card grid view (configurable via siteConfig.blogPage, supports view toggle). Includes back button in navigation |
-| `Post.tsx` | Individual blog post or page view with optional sidebar layout. Includes back button and CopyPageDropdown in post navigation (update SITE_URL/SITE_NAME when forking) |
+| `Post.tsx` | Individual blog post or page view with optional sidebar layout. Includes back button, CopyPageDropdown, tag links, and related posts section in footer for blog posts (update SITE_URL/SITE_NAME when forking) |
| `Stats.tsx` | Real-time analytics dashboard with visitor stats and GitHub stars |
+| `TagPage.tsx` | Tag archive page displaying posts filtered by a specific tag. Includes view mode toggle (list/cards) with localStorage persistence |
| `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 +54,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 or card grid (supports list/cards view modes) |
| `BlogPost.tsx` | Markdown renderer with syntax highlighting and collapsible sections (details/summary) |
-| `CopyPageDropdown.tsx` | Share dropdown with Copy page (markdown to clipboard), View as Markdown (opens raw .md file), and Download as SKILL.md (Anthropic Agent Skills format). AI service links (ChatGPT, Claude, Perplexity) disabled due to Netlify edge function issues |
+| `CopyPageDropdown.tsx` | Share dropdown with Copy page (markdown to clipboard), View as Markdown (opens raw .md file), Download as SKILL.md (Anthropic Agent Skills format), and Open in AI links (ChatGPT, Claude, Perplexity) using GitHub raw URLs with universal prompt |
| `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 |
@@ -86,14 +87,14 @@ A brief description of each file in the codebase.
| File | Description |
| ------------ | ------------------------------------------------------------------------------------ |
-| `global.css` | Global CSS with theme variables, centralized font-size CSS variables for all themes, sidebar styling with alternate background colors and right border for docs-style layout |
+| `global.css` | Global CSS with theme variables, centralized font-size CSS variables for all themes, sidebar styling with alternate background colors, hidden scrollbar, and consistent borders using box-shadow for docs-style layout |
## Convex Backend (`convex/`)
| File | Description |
| ------------------ | -------------------------------------------------------------------- |
-| `schema.ts` | Database schema (posts, pages, viewCounts, pageViews, activeSessions) |
-| `posts.ts` | Queries and mutations for blog posts, view counts |
+| `schema.ts` | Database schema (posts, pages, viewCounts, pageViews, activeSessions) with by_tags index for tag queries |
+| `posts.ts` | Queries and mutations for blog posts, view counts, getAllTags, getPostsByTag, and getRelatedPosts |
| `pages.ts` | Queries and mutations for static pages |
| `search.ts` | Full text search queries across posts and pages |
| `stats.ts` | Real-time stats with aggregate component for O(log n) counts, page view recording, session heartbeat |
@@ -110,7 +111,7 @@ A brief description of each file in the codebase.
| `/stats` | Real-time site analytics page |
| `/rss.xml` | RSS feed with descriptions |
| `/rss-full.xml` | RSS feed with full content for LLMs |
-| `/sitemap.xml` | Dynamic XML sitemap for search engines |
+| `/sitemap.xml` | Dynamic XML sitemap for search engines (includes posts, pages, and tag pages) |
| `/api/posts` | JSON list of all posts |
| `/api/post` | Single post as JSON or markdown |
| `/api/export` | Batch export all posts with content |
diff --git a/public/raw/docs.md b/public/raw/docs.md
index 37844e8..8ac53e2 100644
--- a/public/raw/docs.md
+++ b/public/raw/docs.md
@@ -675,18 +675,26 @@ The menu appears automatically on screens under 768px wide.
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 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 |
+| Option | Description |
+| -------------------- | ------------------------------------------ |
+| Copy page | Copies formatted markdown to clipboard |
+| 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 |
+| Download as SKILL.md | Downloads skill file for AI agent training |
-**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.
+**Raw markdown URLs:** AI service links use GitHub raw URLs to fetch markdown content. This bypasses Netlify edge functions and provides reliable access for AI services.
-**Generate Skill:** Formats the content as an AI agent skill file with metadata, when to use, and instructions sections.
+**Git push required for AI links:** The "Open in ChatGPT," "Open in Claude," and "Open in Perplexity" options use GitHub raw URLs. For these to work, you must push your content to GitHub with `git push`. The `npm run sync` command syncs content to Convex for your live site, but AI services fetch directly from GitHub.
+
+| What you want | Command needed |
+| ------------------------------------ | ------------------------------ |
+| Content visible on your site | `npm run sync` or `sync:prod` |
+| AI links (ChatGPT/Claude/Perplexity) | `git push` to GitHub |
+| Both | `npm run sync` then `git push` |
+
+**Download as SKILL.md:** Downloads the content formatted as an Anthropic Agent Skills file with metadata, triggers, and instructions sections.
## Real-time stats
diff --git a/public/raw/setup-guide.md b/public/raw/setup-guide.md
index 433ce03..c1add35 100644
--- a/public/raw/setup-guide.md
+++ b/public/raw/setup-guide.md
@@ -1060,18 +1060,24 @@ On mobile and tablet screens (under 768px), a hamburger menu provides navigation
Each post and page includes a share dropdown with options for AI tools:
-| 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 |
-| View as Markdown | Opens raw `.md` file in new tab |
-| Generate Skill | Downloads `{slug}-skill.md` for AI agent training |
+| Option | Description |
+| -------------------- | ------------------------------------------------- |
+| Copy page | Copies formatted markdown to clipboard |
+| 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 |
+| Download as SKILL.md | Downloads skill file for AI agent training |
-**Generate Skill** formats the content as an AI agent skill file with metadata, when to use, and instructions sections.
+**Git push required for AI links:** The "Open in ChatGPT," "Open in Claude," and "Open in Perplexity" options use GitHub raw URLs to fetch content. For these to work, your content must be pushed to GitHub with `git push`. The `npm run sync` command syncs content to Convex for your live site, but AI services fetch directly from GitHub.
-**Long content:** If content exceeds URL limits, it copies to clipboard and opens the AI service in a new tab. Paste to continue.
+| What you want | Command needed |
+| ------------------------------------ | ------------------------------ |
+| Content visible on your site | `npm run sync` or `sync:prod` |
+| AI links (ChatGPT/Claude/Perplexity) | `git push` to GitHub |
+| Both | `npm run sync` then `git push` |
+
+**Download as SKILL.md** formats the content as an Anthropic Agent Skills file with metadata, triggers, and instructions sections.
## API Endpoints
diff --git a/src/App.tsx b/src/App.tsx
index 7af85e2..edfbccc 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -4,6 +4,7 @@ import Post from "./pages/Post";
import Stats from "./pages/Stats";
import Blog from "./pages/Blog";
import Write from "./pages/Write";
+import TagPage from "./pages/TagPage";
import Layout from "./components/Layout";
import { usePageTracking } from "./hooks/usePageTracking";
import { SidebarProvider } from "./context/SidebarContext";
@@ -29,6 +30,8 @@ function App() {
{siteConfig.blogPage.enabled && (
} />
)}
+ {/* Tag page route - displays posts filtered by tag */}
+ } />
{/* Catch-all for post/page slugs - must be last */}
} />
diff --git a/src/components/CopyPageDropdown.tsx b/src/components/CopyPageDropdown.tsx
index 0ed02fe..747897d 100644
--- a/src/components/CopyPageDropdown.tsx
+++ b/src/components/CopyPageDropdown.tsx
@@ -1,9 +1,34 @@
import { useState, useRef, useEffect, useCallback } from "react";
-import { Copy, Check, AlertCircle, FileText, Download } from "lucide-react";
+import {
+ Copy,
+ Check,
+ AlertCircle,
+ FileText,
+ Download,
+ ExternalLink,
+} from "lucide-react";
+import siteConfig from "../config/siteConfig";
// Maximum URL length for query parameters (conservative limit)
const MAX_URL_LENGTH = 6000;
+// Universal AI prompt for reading raw markdown
+const AI_READ_PROMPT = `Read the raw markdown document at the URL below. If the content loads successfully:
+- Provide a concise accurate summary
+- Be ready to answer follow up questions using only this document
+
+If the content cannot be loaded:
+- Say so explicitly
+- Do not guess or infer content
+
+URL:`;
+
+// Construct GitHub raw URL for a slug
+function getGitHubRawUrl(slug: string): string {
+ const { owner, repo, branch, contentPath } = siteConfig.gitHubRepo;
+ return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${contentPath}/${slug}.md`;
+}
+
// Extended props interface with optional metadata
interface CopyPageDropdownProps {
title: string;
@@ -394,13 +419,103 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
- {/* AI service options temporarily disabled
- * ChatGPT, Claude, and Perplexity links were removed because
- * Netlify edge functions block AI crawler fetch requests to /raw/*.md
- * despite multiple configuration attempts. See blog post:
- * /netlify-edge-excludedpath-ai-crawlers for details.
- * Users can still copy markdown and paste into AI tools.
- */}
+ {/* Divider */}
+
+
+ {/* AI service links using GitHub raw URLs */}
+ {/* Note: Requires git push to work - npm sync alone is not sufficient */}
+
+
+
+
+
)}
diff --git a/src/config/siteConfig.ts b/src/config/siteConfig.ts
index 23ac5da..187117a 100644
--- a/src/config/siteConfig.ts
+++ b/src/config/siteConfig.ts
@@ -56,6 +56,15 @@ export interface HardcodedNavItem {
showInNav?: boolean; // Show in navigation menu (default: true)
}
+// GitHub repository configuration
+// Used for "Open in AI" links that use GitHub raw URLs
+export interface GitHubRepoConfig {
+ owner: string; // GitHub username or organization
+ repo: string; // Repository name
+ branch: string; // Default branch (e.g., "main")
+ contentPath: string; // Path to raw markdown files (e.g., "public/raw")
+}
+
// Site configuration interface
export interface SiteConfig {
// Basic site info
@@ -96,6 +105,9 @@ export interface SiteConfig {
convex: string;
netlify: string;
};
+
+ // GitHub repository configuration for AI service links
+ gitHubRepo: GitHubRepoConfig;
}
// Default site configuration
@@ -220,6 +232,17 @@ export const siteConfig: SiteConfig = {
convex: "https://convex.dev",
netlify: "https://netlify.com",
},
+
+ // GitHub repository configuration
+ // Used for "Open in AI" links (ChatGPT, Claude, Perplexity)
+ // These links use GitHub raw URLs since AI services can reliably fetch from GitHub
+ // Note: Content must be pushed to GitHub for AI links to work
+ gitHubRepo: {
+ owner: "waynesutton", // GitHub username or organization
+ repo: "markdown-site", // Repository name
+ branch: "main", // Default branch
+ contentPath: "public/raw", // Path to raw markdown files
+ },
};
// Export the config as default for easy importing
diff --git a/src/pages/Post.tsx b/src/pages/Post.tsx
index 0571d24..92c60fe 100644
--- a/src/pages/Post.tsx
+++ b/src/pages/Post.tsx
@@ -7,7 +7,7 @@ import PageSidebar from "../components/PageSidebar";
import { extractHeadings } from "../utils/extractHeadings";
import { useSidebar } from "../context/SidebarContext";
import { format, parseISO } from "date-fns";
-import { ArrowLeft, Link as LinkIcon, Twitter, Rss } from "lucide-react";
+import { ArrowLeft, Link as LinkIcon, Twitter, Rss, Tag } from "lucide-react";
import { useState, useEffect } from "react";
// Site configuration
@@ -23,6 +23,15 @@ export default function Post() {
// Check for page first, then post
const page = useQuery(api.pages.getPageBySlug, slug ? { slug } : "skip");
const post = useQuery(api.posts.getPostBySlug, slug ? { slug } : "skip");
+
+ // Fetch related posts based on current post's tags (only for blog posts, not pages)
+ const relatedPosts = useQuery(
+ api.posts.getRelatedPosts,
+ post && !page
+ ? { currentSlug: post.slug, tags: post.tags, limit: 3 }
+ : "skip",
+ );
+
const [copied, setCopied] = useState(false);
// Scroll to hash anchor after content loads
@@ -367,13 +376,37 @@ export default function Post() {
{post.tags && post.tags.length > 0 && (
+
{post.tags.map((tag) => (
-
+
{tag}
-
+
))}
)}
+
+ {/* Related posts section - only shown for blog posts with shared tags */}
+ {relatedPosts && relatedPosts.length > 0 && (
+