mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 04:09:14 +00:00
Add author archive pages displaying all posts by a specific author, following the existing tag pages pattern. Author names in post headers are now clickable links that navigate to the author's page. Changes: - Add by_authorName index to posts table (convex/schema.ts) - Add getAllAuthors and getPostsByAuthor queries (convex/posts.ts) - Create AuthorPage.tsx component with list/cards view toggle - Add /author/:authorSlug route (src/App.tsx) - Make authorName clickable in Post.tsx for posts and pages - Add author link and page styles (src/styles/global.css) - Add author pages to sitemap (convex/http.ts) - Update documentation: files.md, TASK.md, changelog.md, changelog-page.md - Save implementation plan to prds/authorname-blogs.md
6.5 KiB
6.5 KiB
Author Pages Implementation Plan
Overview
Add author pages at /author/:authorSlug that display all posts by a specific author. Follows the existing tag pages pattern. Works with existing npm run sync workflow - no sync changes needed.
Files to Modify
| File | Change |
|---|---|
convex/schema.ts |
Add by_authorName index to posts table |
convex/posts.ts |
Add getAllAuthors and getPostsByAuthor queries |
src/pages/AuthorPage.tsx |
New component (based on TagPage.tsx pattern) |
src/App.tsx |
Add /author/:authorSlug route |
src/pages/Post.tsx |
Make authorName a clickable <Link> to author page |
convex/http.ts |
Add author pages to sitemap |
files.md |
Document new AuthorPage.tsx |
Implementation Steps
Step 1: Add Index to Schema
File: convex/schema.ts
Add index to posts table for efficient author queries:
// In posts table definition, add to indexes:
.index("by_authorName", ["authorName"])
Step 2: Add Convex Queries
File: convex/posts.ts
Add two new queries following existing patterns:
// Get all unique authors (similar to getAllTags)
export const getAllAuthors = query({
args: {},
returns: v.array(v.object({
name: v.string(),
slug: v.string(),
count: v.number(),
})),
handler: async (ctx) => {
const posts = await ctx.db
.query("posts")
.withIndex("by_published", (q) => q.eq("published", true))
.collect();
// Filter out unlisted posts and posts without author
const publishedPosts = posts.filter(p => !p.unlisted && p.authorName);
// Count posts per author
const authorCounts = new Map<string, number>();
for (const post of publishedPosts) {
if (post.authorName) {
const count = authorCounts.get(post.authorName) || 0;
authorCounts.set(post.authorName, count + 1);
}
}
// Convert to array with slugs
return Array.from(authorCounts.entries())
.map(([name, count]) => ({
name,
slug: name.toLowerCase().replace(/\s+/g, "-"),
count,
}))
.sort((a, b) => {
if (b.count !== a.count) return b.count - a.count;
return a.name.localeCompare(b.name);
});
},
});
// Get posts by author slug (similar to getPostsByTag)
export const getPostsByAuthor = query({
args: { authorSlug: 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 by author slug match and not unlisted
const filtered = posts.filter(post => {
if (!post.authorName || post.unlisted) return false;
const slug = post.authorName.toLowerCase().replace(/\s+/g, "-");
return slug === args.authorSlug;
});
// Sort by date descending
const sortedPosts = filtered.sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
);
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,
}));
},
});
Step 3: Create AuthorPage Component
File: src/pages/AuthorPage.tsx (new file)
Based on src/pages/TagPage.tsx pattern:
- Accept
authorSlugfrom URL params - Query
getPostsByAuthor(authorSlug) - Display author name as heading
- Show post count
- List/card view toggle with localStorage persistence
- Reuse PostList component
- Back button to blog page
- Handle loading and empty states
Step 4: Add Route
File: src/App.tsx
Add route alongside tag route:
<Route path="/author/:authorSlug" element={<AuthorPage />} />
Step 5: Make Author Name Clickable
File: src/pages/Post.tsx
Change author name from <span> to <Link>:
// Before:
{post.authorName && (
<span className="post-author-name">{post.authorName}</span>
)}
// After:
{post.authorName && (
<Link
to={`/author/${post.authorName.toLowerCase().replace(/\s+/g, "-")}`}
className="post-author-name post-author-link"
>
{post.authorName}
</Link>
)}
Step 6: Add Author Link Styles
File: src/styles/global.css
.post-author-link {
color: inherit;
text-decoration: none;
}
.post-author-link:hover {
text-decoration: underline;
}
Step 7: Add to Sitemap
File: convex/http.ts
In sitemap generation, add author pages (similar to tag pages):
const authors = await ctx.runQuery(api.posts.getAllAuthors);
// Add author page URLs
...authors.map(
(author: { slug: string }) => ` <url>
<loc>${SITE_URL}/author/${encodeURIComponent(author.slug)}</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>`,
),
Step 8: Update Documentation
File: files.md
Add AuthorPage.tsx entry:
| `AuthorPage.tsx` | Author archive page displaying posts by a specific author. Includes view mode toggle (list/cards) with localStorage persistence |
Testing Checklist
- Posts with
authorNameshow clickable link /author/wayne-suttondisplays correct posts- Authors with multiple posts show all posts
- View toggle (list/cards) works and persists
- Empty author slug shows 404 or empty state
- Sitemap includes author pages
- Mobile responsive layout works
- All four themes display correctly
No Changes Needed
scripts/sync-posts.ts- authorName already syncsconvex/schema.tsfields - authorName field exists- Frontmatter format - works as-is
References
- Tag pages pattern:
src/pages/TagPage.tsx,convex/posts.ts(getPostsByTag, getAllTags) - Convex best practices:
.claude/skills/convex.md - Schema patterns:
.claude/skills/dev.md