feat: Add docsSectionGroupOrder frontmatter field for controlling docs sidebar group order

This commit is contained in:
Wayne Sutton
2026-01-02 23:11:35 -08:00
parent 46a1cdf591
commit 8fe6b53600
45 changed files with 2450 additions and 338 deletions

View File

@@ -184,6 +184,7 @@ export const getPageBySlug = query({
contactForm: v.optional(v.boolean()),
newsletter: v.optional(v.boolean()),
textAlign: v.optional(v.string()),
docsSection: v.optional(v.boolean()),
}),
v.null(),
),
@@ -221,6 +222,94 @@ export const getPageBySlug = query({
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()),
}),
),
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,
}));
},
});
// 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(),
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()),
}),
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,
image: landing.image,
showImageAtTop: landing.showImageAtTop,
authorName: landing.authorName,
authorImage: landing.authorImage,
docsSectionGroup: landing.docsSectionGroup,
docsSectionOrder: landing.docsSectionOrder,
};
},
});
@@ -252,6 +341,11 @@ export const syncPagesPublic = mutation({
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()),
docsLanding: v.optional(v.boolean()),
}),
),
},
@@ -300,6 +394,11 @@ export const syncPagesPublic = mutation({
contactForm: page.contactForm,
newsletter: page.newsletter,
textAlign: page.textAlign,
docsSection: page.docsSection,
docsSectionGroup: page.docsSectionGroup,
docsSectionOrder: page.docsSectionOrder,
docsSectionGroupOrder: page.docsSectionGroupOrder,
docsLanding: page.docsLanding,
lastSyncedAt: now,
});
updated++;

View File

@@ -238,6 +238,7 @@ export const getPostBySlug = query({
aiChat: v.optional(v.boolean()),
newsletter: v.optional(v.boolean()),
contactForm: v.optional(v.boolean()),
docsSection: v.optional(v.boolean()),
}),
v.null(),
),
@@ -277,6 +278,7 @@ export const getPostBySlug = query({
aiChat: post.aiChat,
newsletter: post.newsletter,
contactForm: post.contactForm,
docsSection: post.docsSection,
};
},
});
@@ -384,6 +386,11 @@ export const syncPosts = internalMutation({
newsletter: v.optional(v.boolean()),
contactForm: v.optional(v.boolean()),
unlisted: v.optional(v.boolean()),
docsSection: v.optional(v.boolean()),
docsSectionGroup: v.optional(v.string()),
docsSectionOrder: v.optional(v.number()),
docsSectionGroupOrder: v.optional(v.number()),
docsLanding: v.optional(v.boolean()),
}),
),
},
@@ -435,6 +442,11 @@ export const syncPosts = internalMutation({
newsletter: post.newsletter,
contactForm: post.contactForm,
unlisted: post.unlisted,
docsSection: post.docsSection,
docsSectionGroup: post.docsSectionGroup,
docsSectionOrder: post.docsSectionOrder,
docsSectionGroupOrder: post.docsSectionGroupOrder,
docsLanding: post.docsLanding,
lastSyncedAt: now,
});
updated++;
@@ -490,6 +502,11 @@ export const syncPostsPublic = mutation({
newsletter: v.optional(v.boolean()),
contactForm: v.optional(v.boolean()),
unlisted: v.optional(v.boolean()),
docsSection: v.optional(v.boolean()),
docsSectionGroup: v.optional(v.string()),
docsSectionOrder: v.optional(v.number()),
docsSectionGroupOrder: v.optional(v.number()),
docsLanding: v.optional(v.boolean()),
}),
),
},
@@ -541,6 +558,11 @@ export const syncPostsPublic = mutation({
newsletter: post.newsletter,
contactForm: post.contactForm,
unlisted: post.unlisted,
docsSection: post.docsSection,
docsSectionGroup: post.docsSectionGroup,
docsSectionOrder: post.docsSectionOrder,
docsSectionGroupOrder: post.docsSectionGroupOrder,
docsLanding: post.docsLanding,
lastSyncedAt: now,
});
updated++;
@@ -874,3 +896,98 @@ export const getPostsByAuthor = query({
}));
},
});
// Get all posts marked for docs section navigation
// Used by DocsSidebar to build the left navigation
export const getDocsPosts = query({
args: {},
returns: v.array(
v.object({
_id: v.id("posts"),
slug: v.string(),
title: v.string(),
docsSectionGroup: v.optional(v.string()),
docsSectionOrder: v.optional(v.number()),
docsSectionGroupOrder: v.optional(v.number()),
}),
),
handler: async (ctx) => {
const posts = await ctx.db
.query("posts")
.withIndex("by_docsSection", (q) => q.eq("docsSection", true))
.collect();
// Filter to only published posts
const publishedDocs = posts.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((post) => ({
_id: post._id,
slug: post.slug,
title: post.title,
docsSectionGroup: post.docsSectionGroup,
docsSectionOrder: post.docsSectionOrder,
docsSectionGroupOrder: post.docsSectionGroupOrder,
}));
},
});
// Get the docs landing page (post with docsLanding: true)
// Returns null if no landing page is set
export const getDocsLandingPost = query({
args: {},
returns: v.union(
v.object({
_id: v.id("posts"),
slug: v.string(),
title: v.string(),
description: v.string(),
content: v.string(),
date: v.string(),
tags: v.array(v.string()),
readTime: 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()),
}),
v.null(),
),
handler: async (ctx) => {
// Get all docs posts and find one with docsLanding: true
const posts = await ctx.db
.query("posts")
.withIndex("by_docsSection", (q) => q.eq("docsSection", true))
.collect();
const landing = posts.find((p) => p.published && p.docsLanding);
if (!landing) return null;
return {
_id: landing._id,
slug: landing.slug,
title: landing.title,
description: landing.description,
content: landing.content,
date: landing.date,
tags: landing.tags,
readTime: landing.readTime,
image: landing.image,
showImageAtTop: landing.showImageAtTop,
authorName: landing.authorName,
authorImage: landing.authorImage,
docsSectionGroup: landing.docsSectionGroup,
docsSectionOrder: landing.docsSectionOrder,
};
},
});

View File

@@ -29,6 +29,11 @@ export default defineSchema({
newsletter: v.optional(v.boolean()), // Override newsletter signup display (true/false)
contactForm: v.optional(v.boolean()), // Enable contact form on this post
unlisted: v.optional(v.boolean()), // Hide from listings but allow direct access via slug
docsSection: v.optional(v.boolean()), // Include in docs navigation
docsSectionGroup: v.optional(v.string()), // Sidebar group name in docs
docsSectionOrder: v.optional(v.number()), // Order within group (lower = first)
docsSectionGroupOrder: v.optional(v.number()), // Order of group itself (lower = first)
docsLanding: v.optional(v.boolean()), // Use as /docs landing page
lastSyncedAt: v.number(),
})
.index("by_slug", ["slug"])
@@ -37,6 +42,7 @@ export default defineSchema({
.index("by_featured", ["featured"])
.index("by_blogFeatured", ["blogFeatured"])
.index("by_authorName", ["authorName"])
.index("by_docsSection", ["docsSection"])
.searchIndex("search_content", {
searchField: "content",
filterFields: ["published"],
@@ -70,11 +76,17 @@ export default defineSchema({
contactForm: v.optional(v.boolean()), // Enable contact form on this page
newsletter: v.optional(v.boolean()), // Override newsletter signup display (true/false)
textAlign: v.optional(v.string()), // Text alignment: "left", "center", "right" (default: "left")
docsSection: v.optional(v.boolean()), // Include in docs navigation
docsSectionGroup: v.optional(v.string()), // Sidebar group name in docs
docsSectionOrder: v.optional(v.number()), // Order within group (lower = first)
docsSectionGroupOrder: v.optional(v.number()), // Order of group itself (lower = first)
docsLanding: v.optional(v.boolean()), // Use as /docs landing page
lastSyncedAt: v.number(),
})
.index("by_slug", ["slug"])
.index("by_published", ["published"])
.index("by_featured", ["featured"])
.index("by_docsSection", ["docsSection"])
.searchIndex("search_content", {
searchField: "content",
filterFields: ["published"],