Files
wiki/convex/pages.ts

460 lines
14 KiB
TypeScript
Raw Permalink Normal View History

import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";
// Get all pages (published and unpublished) for dashboard admin view
export const listAll = query({
args: {},
returns: v.array(
v.object({
_id: v.id("pages"),
_creationTime: v.number(),
slug: v.string(),
title: v.string(),
content: v.string(),
published: v.boolean(),
order: v.optional(v.number()),
showInNav: v.optional(v.boolean()),
excerpt: v.optional(v.string()),
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()),
source: v.optional(v.union(v.literal("dashboard"), v.literal("sync"))),
}),
),
handler: async (ctx) => {
const pages = await ctx.db.query("pages").collect();
// Sort by order, then by title
const sortedPages = pages.sort((a, b) => {
const orderA = a.order ?? 999;
const orderB = b.order ?? 999;
if (orderA !== orderB) return orderA - orderB;
return a.title.localeCompare(b.title);
});
return sortedPages.map((page) => ({
_id: page._id,
_creationTime: page._creationTime,
slug: page.slug,
title: page.title,
content: page.content,
published: page.published,
order: page.order,
showInNav: page.showInNav,
excerpt: page.excerpt,
image: page.image,
featured: page.featured,
featuredOrder: page.featuredOrder,
authorName: page.authorName,
authorImage: page.authorImage,
source: page.source,
}));
},
});
// Get all published pages for navigation
export const getAllPages = query({
args: {},
returns: v.array(
v.object({
_id: v.id("pages"),
slug: v.string(),
title: v.string(),
published: v.boolean(),
order: v.optional(v.number()),
showInNav: v.optional(v.boolean()),
excerpt: v.optional(v.string()),
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()),
layout: v.optional(v.string()),
rightSidebar: v.optional(v.boolean()),
showFooter: v.optional(v.boolean()),
footer: v.optional(v.string()),
}),
),
handler: async (ctx) => {
const pages = await ctx.db
.query("pages")
.withIndex("by_published", (q) => q.eq("published", true))
.collect();
// Filter out pages where showInNav is explicitly false
// Default to true for backwards compatibility (undefined/null = show in nav)
const visiblePages = pages.filter(
(page) => page.showInNav !== false,
);
// Sort by order (lower numbers first), then by title
const sortedPages = visiblePages.sort((a, b) => {
const orderA = a.order ?? 999;
const orderB = b.order ?? 999;
if (orderA !== orderB) return orderA - orderB;
return a.title.localeCompare(b.title);
});
return sortedPages.map((page) => ({
_id: page._id,
slug: page.slug,
title: page.title,
published: page.published,
order: page.order,
showInNav: page.showInNav,
excerpt: page.excerpt,
image: page.image,
featured: page.featured,
featuredOrder: page.featuredOrder,
authorName: page.authorName,
authorImage: page.authorImage,
layout: page.layout,
rightSidebar: page.rightSidebar,
showFooter: page.showFooter,
}));
},
});
// Get featured pages for the homepage featured section
export const getFeaturedPages = query({
args: {},
returns: v.array(
v.object({
_id: v.id("pages"),
slug: v.string(),
title: v.string(),
excerpt: v.optional(v.string()),
image: v.optional(v.string()),
featuredOrder: v.optional(v.number()),
}),
),
handler: async (ctx) => {
const pages = await ctx.db
.query("pages")
.withIndex("by_featured", (q) => q.eq("featured", true))
.collect();
// Filter to only published pages and sort by featuredOrder
const featuredPages = pages
.filter((p) => p.published)
.sort((a, b) => {
const orderA = a.featuredOrder ?? 999;
const orderB = b.featuredOrder ?? 999;
return orderA - orderB;
});
return featuredPages.map((page) => ({
_id: page._id,
slug: page.slug,
title: page.title,
excerpt: page.excerpt,
image: page.image,
featuredOrder: page.featuredOrder,
}));
},
});
// Get a single page by slug
export const getPageBySlug = query({
args: {
slug: v.string(),
},
returns: v.union(
v.object({
_id: v.id("pages"),
slug: v.string(),
title: v.string(),
content: v.string(),
published: v.boolean(),
order: v.optional(v.number()),
showInNav: v.optional(v.boolean()),
excerpt: v.optional(v.string()),
image: v.optional(v.string()),
showImageAtTop: v.optional(v.boolean()),
featured: v.optional(v.boolean()),
featuredOrder: v.optional(v.number()),
authorName: v.optional(v.string()),
authorImage: v.optional(v.string()),
layout: v.optional(v.string()),
rightSidebar: v.optional(v.boolean()),
showFooter: v.optional(v.boolean()),
footer: v.optional(v.string()),
showSocialFooter: v.optional(v.boolean()),
feat: add AI Agent chat integration with Anthropic Claude API Add AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled on Write page (replaces textarea) and optionally in RightSidebar on posts/pages via frontmatter. Features: - AIChatView component with per-page chat history - Page content context support for AI responses - Markdown rendering for AI responses - User-friendly error handling for missing API keys - System prompt configurable via Convex environment variables - Anonymous session authentication using localStorage Environment variables required: - ANTHROPIC_API_KEY (required) - CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES (optional split prompts) - CLAUDE_SYSTEM_PROMPT (optional single prompt fallback) Configuration: - siteConfig.aiChat.enabledOnWritePage: Enable Agent toggle on /write page - siteConfig.aiChat.enabledOnContent: Allow Agent on posts/pages via frontmatter - Frontmatter aiChat: true (requires rightSidebar: true) Updated files: - src/components/AIChatView.tsx: AI chat interface component - src/components/RightSidebar.tsx: Conditional Agent rendering - src/pages/Write.tsx: Agent mode toggle (title changes to Agent) - convex/aiChats.ts: Chat history queries and mutations - convex/aiChatActions.ts: Claude API integration with error handling - convex/schema.ts: aiChats table with indexes - src/config/siteConfig.ts: AIChatConfig interface - Documentation updated across all files Documentation: - files.md: Updated component descriptions - changelog.md: Added v1.33.0 entry - TASK.md: Marked AI chat tasks as completed - README.md: Added AI Agent Chat section - content/pages/docs.md: Added AI Agent chat documentation - content/blog/setup-guide.md: Added AI Agent chat setup instructions - public/raw/changelog.md: Added v1.33.0 entry
2025-12-26 12:31:33 -08:00
aiChat: v.optional(v.boolean()),
contactForm: v.optional(v.boolean()),
newsletter: v.optional(v.boolean()),
textAlign: v.optional(v.string()),
docsSection: v.optional(v.boolean()),
}),
v.null(),
),
handler: async (ctx, args) => {
const page = await ctx.db
.query("pages")
.withIndex("by_slug", (q) => q.eq("slug", args.slug))
.first();
if (!page || !page.published) {
return null;
}
return {
_id: page._id,
slug: page.slug,
title: page.title,
content: page.content,
published: page.published,
order: page.order,
showInNav: page.showInNav,
excerpt: page.excerpt,
image: page.image,
showImageAtTop: page.showImageAtTop,
featured: page.featured,
featuredOrder: page.featuredOrder,
authorName: page.authorName,
authorImage: page.authorImage,
layout: page.layout,
rightSidebar: page.rightSidebar,
showFooter: page.showFooter,
footer: page.footer,
showSocialFooter: page.showSocialFooter,
feat: add AI Agent chat integration with Anthropic Claude API Add AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled on Write page (replaces textarea) and optionally in RightSidebar on posts/pages via frontmatter. Features: - AIChatView component with per-page chat history - Page content context support for AI responses - Markdown rendering for AI responses - User-friendly error handling for missing API keys - System prompt configurable via Convex environment variables - Anonymous session authentication using localStorage Environment variables required: - ANTHROPIC_API_KEY (required) - CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES (optional split prompts) - CLAUDE_SYSTEM_PROMPT (optional single prompt fallback) Configuration: - siteConfig.aiChat.enabledOnWritePage: Enable Agent toggle on /write page - siteConfig.aiChat.enabledOnContent: Allow Agent on posts/pages via frontmatter - Frontmatter aiChat: true (requires rightSidebar: true) Updated files: - src/components/AIChatView.tsx: AI chat interface component - src/components/RightSidebar.tsx: Conditional Agent rendering - src/pages/Write.tsx: Agent mode toggle (title changes to Agent) - convex/aiChats.ts: Chat history queries and mutations - convex/aiChatActions.ts: Claude API integration with error handling - convex/schema.ts: aiChats table with indexes - src/config/siteConfig.ts: AIChatConfig interface - Documentation updated across all files Documentation: - files.md: Updated component descriptions - changelog.md: Added v1.33.0 entry - TASK.md: Marked AI chat tasks as completed - README.md: Added AI Agent Chat section - content/pages/docs.md: Added AI Agent chat documentation - content/blog/setup-guide.md: Added AI Agent chat setup instructions - public/raw/changelog.md: Added v1.33.0 entry
2025-12-26 12:31:33 -08:00
aiChat: page.aiChat,
contactForm: page.contactForm,
newsletter: page.newsletter,
textAlign: page.textAlign,
docsSection: page.docsSection,
};
},
});
// Get all pages marked for docs section navigation
// Used by DocsSidebar to build the left navigation
export const getDocsPages = query({
args: {},
returns: v.array(
v.object({
_id: v.id("pages"),
slug: v.string(),
title: v.string(),
docsSectionGroup: v.optional(v.string()),
docsSectionOrder: v.optional(v.number()),
docsSectionGroupOrder: v.optional(v.number()),
docsSectionGroupIcon: v.optional(v.string()),
}),
),
handler: async (ctx) => {
const pages = await ctx.db
.query("pages")
.withIndex("by_docsSection", (q) => q.eq("docsSection", true))
.collect();
// Filter to only published pages
const publishedDocs = pages.filter((p) => p.published);
// Sort by docsSectionOrder, then by title
const sortedDocs = publishedDocs.sort((a, b) => {
const orderA = a.docsSectionOrder ?? 999;
const orderB = b.docsSectionOrder ?? 999;
if (orderA !== orderB) return orderA - orderB;
return a.title.localeCompare(b.title);
});
return sortedDocs.map((page) => ({
_id: page._id,
slug: page.slug,
title: page.title,
docsSectionGroup: page.docsSectionGroup,
docsSectionOrder: page.docsSectionOrder,
docsSectionGroupOrder: page.docsSectionGroupOrder,
docsSectionGroupIcon: page.docsSectionGroupIcon,
}));
},
});
// Get the docs landing page (page with docsLanding: true)
// Returns null if no landing page is set
export const getDocsLandingPage = query({
args: {},
returns: v.union(
v.object({
_id: v.id("pages"),
slug: v.string(),
title: v.string(),
content: v.string(),
2026-01-09 14:54:00 -08:00
excerpt: v.optional(v.string()),
image: v.optional(v.string()),
showImageAtTop: v.optional(v.boolean()),
authorName: v.optional(v.string()),
authorImage: v.optional(v.string()),
docsSectionGroup: v.optional(v.string()),
docsSectionOrder: v.optional(v.number()),
2026-01-09 14:54:00 -08:00
showFooter: v.optional(v.boolean()),
footer: v.optional(v.string()),
aiChat: v.optional(v.boolean()),
}),
v.null(),
),
handler: async (ctx) => {
// Get all docs pages and find one with docsLanding: true
const pages = await ctx.db
.query("pages")
.withIndex("by_docsSection", (q) => q.eq("docsSection", true))
.collect();
const landing = pages.find((p) => p.published && p.docsLanding);
if (!landing) return null;
return {
_id: landing._id,
slug: landing.slug,
title: landing.title,
content: landing.content,
2026-01-09 14:54:00 -08:00
excerpt: landing.excerpt,
image: landing.image,
showImageAtTop: landing.showImageAtTop,
authorName: landing.authorName,
authorImage: landing.authorImage,
docsSectionGroup: landing.docsSectionGroup,
docsSectionOrder: landing.docsSectionOrder,
2026-01-09 14:54:00 -08:00
showFooter: landing.showFooter,
footer: landing.footer,
aiChat: landing.aiChat,
};
},
});
// Public mutation for syncing pages from markdown files
// Respects source field: only syncs pages where source !== "dashboard"
export const syncPagesPublic = mutation({
args: {
pages: v.array(
v.object({
slug: v.string(),
title: v.string(),
content: v.string(),
published: v.boolean(),
order: v.optional(v.number()),
showInNav: v.optional(v.boolean()),
excerpt: v.optional(v.string()),
image: v.optional(v.string()),
showImageAtTop: v.optional(v.boolean()),
featured: v.optional(v.boolean()),
featuredOrder: v.optional(v.number()),
authorName: v.optional(v.string()),
authorImage: v.optional(v.string()),
layout: v.optional(v.string()),
rightSidebar: v.optional(v.boolean()),
showFooter: v.optional(v.boolean()),
footer: v.optional(v.string()),
showSocialFooter: v.optional(v.boolean()),
feat: add AI Agent chat integration with Anthropic Claude API Add AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled on Write page (replaces textarea) and optionally in RightSidebar on posts/pages via frontmatter. Features: - AIChatView component with per-page chat history - Page content context support for AI responses - Markdown rendering for AI responses - User-friendly error handling for missing API keys - System prompt configurable via Convex environment variables - Anonymous session authentication using localStorage Environment variables required: - ANTHROPIC_API_KEY (required) - CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES (optional split prompts) - CLAUDE_SYSTEM_PROMPT (optional single prompt fallback) Configuration: - siteConfig.aiChat.enabledOnWritePage: Enable Agent toggle on /write page - siteConfig.aiChat.enabledOnContent: Allow Agent on posts/pages via frontmatter - Frontmatter aiChat: true (requires rightSidebar: true) Updated files: - src/components/AIChatView.tsx: AI chat interface component - src/components/RightSidebar.tsx: Conditional Agent rendering - src/pages/Write.tsx: Agent mode toggle (title changes to Agent) - convex/aiChats.ts: Chat history queries and mutations - convex/aiChatActions.ts: Claude API integration with error handling - convex/schema.ts: aiChats table with indexes - src/config/siteConfig.ts: AIChatConfig interface - Documentation updated across all files Documentation: - files.md: Updated component descriptions - changelog.md: Added v1.33.0 entry - TASK.md: Marked AI chat tasks as completed - README.md: Added AI Agent Chat section - content/pages/docs.md: Added AI Agent chat documentation - content/blog/setup-guide.md: Added AI Agent chat setup instructions - public/raw/changelog.md: Added v1.33.0 entry
2025-12-26 12:31:33 -08:00
aiChat: v.optional(v.boolean()),
contactForm: v.optional(v.boolean()),
newsletter: v.optional(v.boolean()),
textAlign: v.optional(v.string()),
docsSection: v.optional(v.boolean()),
docsSectionGroup: v.optional(v.string()),
docsSectionOrder: v.optional(v.number()),
docsSectionGroupOrder: v.optional(v.number()),
docsSectionGroupIcon: v.optional(v.string()),
docsLanding: v.optional(v.boolean()),
}),
),
},
returns: v.object({
created: v.number(),
updated: v.number(),
deleted: v.number(),
skipped: v.number(),
}),
handler: async (ctx, args) => {
let created = 0;
let updated = 0;
let deleted = 0;
let skipped = 0;
const now = Date.now();
const incomingSlugs = new Set(args.pages.map((p) => p.slug));
// Get all existing pages
const existingPages = await ctx.db.query("pages").collect();
const existingBySlug = new Map(existingPages.map((p) => [p.slug, p]));
// Upsert incoming pages (only if source !== "dashboard")
for (const page of args.pages) {
const existing = existingBySlug.get(page.slug);
if (existing) {
// Skip dashboard-created pages - don't overwrite them
if (existing.source === "dashboard") {
skipped++;
continue;
}
// Capture version before update (async, non-blocking)
await ctx.scheduler.runAfter(0, internal.versions.createVersion, {
contentType: "page",
contentId: existing._id,
slug: existing.slug,
title: existing.title,
content: existing.content,
source: "sync",
});
// Update existing sync page
await ctx.db.patch(existing._id, {
title: page.title,
content: page.content,
published: page.published,
order: page.order,
showInNav: page.showInNav,
excerpt: page.excerpt,
image: page.image,
showImageAtTop: page.showImageAtTop,
featured: page.featured,
featuredOrder: page.featuredOrder,
authorName: page.authorName,
authorImage: page.authorImage,
layout: page.layout,
rightSidebar: page.rightSidebar,
showFooter: page.showFooter,
footer: page.footer,
showSocialFooter: page.showSocialFooter,
feat: add AI Agent chat integration with Anthropic Claude API Add AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled on Write page (replaces textarea) and optionally in RightSidebar on posts/pages via frontmatter. Features: - AIChatView component with per-page chat history - Page content context support for AI responses - Markdown rendering for AI responses - User-friendly error handling for missing API keys - System prompt configurable via Convex environment variables - Anonymous session authentication using localStorage Environment variables required: - ANTHROPIC_API_KEY (required) - CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES (optional split prompts) - CLAUDE_SYSTEM_PROMPT (optional single prompt fallback) Configuration: - siteConfig.aiChat.enabledOnWritePage: Enable Agent toggle on /write page - siteConfig.aiChat.enabledOnContent: Allow Agent on posts/pages via frontmatter - Frontmatter aiChat: true (requires rightSidebar: true) Updated files: - src/components/AIChatView.tsx: AI chat interface component - src/components/RightSidebar.tsx: Conditional Agent rendering - src/pages/Write.tsx: Agent mode toggle (title changes to Agent) - convex/aiChats.ts: Chat history queries and mutations - convex/aiChatActions.ts: Claude API integration with error handling - convex/schema.ts: aiChats table with indexes - src/config/siteConfig.ts: AIChatConfig interface - Documentation updated across all files Documentation: - files.md: Updated component descriptions - changelog.md: Added v1.33.0 entry - TASK.md: Marked AI chat tasks as completed - README.md: Added AI Agent Chat section - content/pages/docs.md: Added AI Agent chat documentation - content/blog/setup-guide.md: Added AI Agent chat setup instructions - public/raw/changelog.md: Added v1.33.0 entry
2025-12-26 12:31:33 -08:00
aiChat: page.aiChat,
contactForm: page.contactForm,
newsletter: page.newsletter,
textAlign: page.textAlign,
docsSection: page.docsSection,
docsSectionGroup: page.docsSectionGroup,
docsSectionOrder: page.docsSectionOrder,
docsSectionGroupOrder: page.docsSectionGroupOrder,
docsSectionGroupIcon: page.docsSectionGroupIcon,
docsLanding: page.docsLanding,
source: "sync",
lastSyncedAt: now,
});
updated++;
} else {
// Create new page with source: "sync"
await ctx.db.insert("pages", {
...page,
source: "sync",
lastSyncedAt: now,
});
created++;
}
}
// Delete pages that no longer exist in the repo (but not dashboard pages)
for (const existing of existingPages) {
if (!incomingSlugs.has(existing.slug) && existing.source !== "dashboard") {
await ctx.db.delete(existing._id);
deleted++;
}
}
return { created, updated, deleted, skipped };
},
});