mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-11 20:08:57 +00:00
feat: add unlisted frontmatter field to hide posts from listings while keeping direct access
This commit is contained in:
@@ -35,6 +35,7 @@ Location: `content/blog/*.md`
|
||||
| blogFeatured | boolean | false | Hero featured on /blog page |
|
||||
| newsletter | boolean | - | Override newsletter signup |
|
||||
| contactForm | boolean | false | Enable contact form |
|
||||
| unlisted | boolean | false | Hide from listings but allow direct access via slug |
|
||||
| showFooter | boolean | - | Override footer display |
|
||||
| footer | string | - | Custom footer markdown |
|
||||
| showSocialFooter | boolean | - | Override social footer |
|
||||
@@ -141,6 +142,15 @@ rightSidebar: true
|
||||
showInNav: false
|
||||
```
|
||||
|
||||
### Unlisted post (hidden from listings)
|
||||
|
||||
```yaml
|
||||
published: true
|
||||
unlisted: true
|
||||
```
|
||||
|
||||
Post remains accessible via direct link but hidden from all listings, search, and related posts.
|
||||
|
||||
### Enable contact form
|
||||
|
||||
```yaml
|
||||
|
||||
12
changelog.md
12
changelog.md
@@ -21,12 +21,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Automated CLAUDE.md updates via sync-discovery-files.ts
|
||||
- CLAUDE.md status comment updated during `npm run sync:discovery`
|
||||
- Includes current site name, post count, page count, and last updated timestamp
|
||||
- Unlisted posts feature
|
||||
- New `unlisted` frontmatter field for blog posts
|
||||
- Set `unlisted: true` to hide posts from listings while keeping them accessible via direct link
|
||||
- Unlisted posts are excluded from: blog listings, featured sections, tag pages, search results, and related posts
|
||||
- Posts remain accessible via direct URL (e.g., `/blog/post-slug`)
|
||||
- Useful for draft posts, private content, or posts you want to share via direct link only
|
||||
|
||||
### Technical
|
||||
|
||||
- New file: `CLAUDE.md` in project root
|
||||
- New directory: `.claude/skills/` with three markdown files
|
||||
- Updated: `scripts/sync-discovery-files.ts` to update CLAUDE.md alongside AGENTS.md and llms.txt
|
||||
- Updated: `convex/schema.ts` - Added `unlisted` optional boolean field to posts table
|
||||
- Updated: `convex/posts.ts` - All listing queries filter out unlisted posts (getAllPosts, getBlogFeaturedPosts, getFeaturedPosts, getAllTags, getPostsByTag, getRelatedPosts, getRecentPostsInternal)
|
||||
- Updated: `convex/search.ts` - Search excludes unlisted posts from results
|
||||
- Updated: `scripts/sync-posts.ts` - Added `unlisted` to PostFrontmatter and ParsedPost interfaces and parsing logic
|
||||
- Updated: `src/pages/Write.tsx` - Added `unlisted` to POST_FIELDS frontmatter reference
|
||||
- Updated documentation: `.claude/skills/frontmatter.md`, `content/pages/docs.md`, `content/blog/setup-guide.md`, `files.md`
|
||||
|
||||
## [2.0.0] - 2025-12-29
|
||||
|
||||
|
||||
2
content/blog/Untitled
Normal file
2
content/blog/Untitled
Normal file
@@ -0,0 +1,2 @@
|
||||
published: true
|
||||
unlisted: true
|
||||
194
content/blog/how-i-added-workos-with-cursor.md
Normal file
194
content/blog/how-i-added-workos-with-cursor.md
Normal file
@@ -0,0 +1,194 @@
|
||||
---
|
||||
title: "How I added WorkOS to my Convex app with Cursor"
|
||||
description: "A timeline of adding WorkOS AuthKit authentication to my markdown blog dashboard using Cursor, prompt engineering, and vibe coding. From PRD import to published feature."
|
||||
date: "2025-12-30"
|
||||
slug: "how-i-added-workos-with-cursor"
|
||||
tags: ["cursor", "workos", "convex", "prompt-engineering", "ai-coding"]
|
||||
readTime: "8 min read"
|
||||
featured: false
|
||||
featuredOrder: 5
|
||||
published: true
|
||||
unlisted: true
|
||||
layout: "sidebar"
|
||||
excerpt: "How I used Cursor, prompt engineering, and Claude to add WorkOS authentication to my Convex dashboard. A real timeline from PRD import to published feature."
|
||||
---
|
||||
|
||||
# How I added WorkOS to my Convex app with Cursor
|
||||
|
||||
I added WorkOS AuthKit authentication to my markdown blog dashboard using Cursor, prompt engineering, and what I call vibe coding. Here's the timeline from start to published.
|
||||
|
||||
## The goal
|
||||
|
||||
Add optional WorkOS authentication to the `/dashboard` page. The dashboard should work with or without WorkOS configured. When authentication is enabled, users log in before accessing the dashboard.
|
||||
|
||||
## Timeline: start to published
|
||||
|
||||
### Step 1: Updated docs and cursor rules
|
||||
|
||||
I started by updating `content/pages/docs.md` with a dashboard section explaining how it works. Then I updated my Cursor rules to include WorkOS documentation patterns and Convex AuthKit integration guidelines.
|
||||
|
||||
The docs update gave me a clear picture of what I wanted to build. The cursor rules helped Cursor understand the patterns I use for authentication in Convex apps.
|
||||
|
||||
### Step 2: Imported PRD from Claude
|
||||
|
||||
I had a conversation with Claude about adding WorkOS to a Convex app. Claude generated a step-by-step PRD that covered:
|
||||
|
||||
- WorkOS account setup
|
||||
- Environment variable configuration
|
||||
- Convex auth configuration
|
||||
- React component structure
|
||||
- Route handling
|
||||
|
||||
I imported this PRD into my project at `prds/workos-authkit-dashboard-guide.md`. It became the blueprint for the entire feature.
|
||||
|
||||
### Step 3: Created a plan in Cursor
|
||||
|
||||
I opened Cursor and asked it to create a plan based on the PRD. Cursor analyzed the PRD and generated a structured plan file at `.cursor/plans/workos_setup_9603c983.plan.md`.
|
||||
|
||||
The plan broke down the work into specific tasks:
|
||||
|
||||
- Create Callback.tsx
|
||||
- Add callback route to App.tsx
|
||||
- Update Dashboard.tsx with auth protection
|
||||
- Test the authentication flow
|
||||
|
||||
Each task had a clear status and description. This gave me a roadmap I could follow step by step.
|
||||
|
||||
### Step 4: Updated environment variables
|
||||
|
||||
I added WorkOS environment variables to `.env.local`:
|
||||
|
||||
```env
|
||||
VITE_WORKOS_CLIENT_ID=client_01XXXXXXXXXXXXXXXXX
|
||||
VITE_WORKOS_REDIRECT_URI=http://localhost:5173/callback
|
||||
```
|
||||
|
||||
I also added `WORKOS_CLIENT_ID` to my Convex environment variables through the Convex dashboard. These variables connect the frontend and backend to WorkOS.
|
||||
|
||||
### Step 5: Cursor and Opus built the app
|
||||
|
||||
I started implementing the plan with Cursor. I asked it to create the Callback component first. Cursor generated `src/pages/Callback.tsx` with proper WorkOS auth handling.
|
||||
|
||||
Then I asked Cursor to update `src/App.tsx` to add the callback route. It added the route correctly, matching the existing route patterns.
|
||||
|
||||
For the Dashboard component, I asked Cursor to add authentication protection. It wrapped the dashboard content with `Authenticated`, `Unauthenticated`, and `AuthLoading` components from Convex React.
|
||||
|
||||
### Step 6: Debugged routes for dashboard page
|
||||
|
||||
The dashboard needed to work in two modes:
|
||||
|
||||
1. With WorkOS configured and `requireAuth: true` - requires login
|
||||
2. Without WorkOS or with `requireAuth: false` - open access
|
||||
|
||||
I asked Cursor to implement conditional authentication. It created a utility function `isWorkOSConfigured()` that checks if WorkOS environment variables are set.
|
||||
|
||||
The Dashboard component now checks:
|
||||
|
||||
- If dashboard is disabled, show disabled message
|
||||
- If auth is required but WorkOS isn't configured, show setup instructions
|
||||
- If WorkOS isn't configured and auth isn't required, show dashboard directly
|
||||
- If WorkOS is configured, use the auth flow
|
||||
|
||||
This conditional logic ensures the dashboard works whether WorkOS is set up or not.
|
||||
|
||||
### Step 7: Frontmatter integration
|
||||
|
||||
I wanted the dashboard authentication to be configurable via `siteConfig.ts`. I added a `dashboard` configuration object:
|
||||
|
||||
```typescript
|
||||
dashboard: {
|
||||
enabled: true,
|
||||
requireAuth: false, // Set to true to require WorkOS authentication
|
||||
},
|
||||
```
|
||||
|
||||
Cursor helped me integrate this configuration into the Dashboard component. The component reads `siteConfig.dashboard.enabled` and `siteConfig.dashboard.requireAuth` to determine behavior.
|
||||
|
||||
### Step 8: Published and documented
|
||||
|
||||
After testing locally, I:
|
||||
|
||||
1. Deployed the changes to production
|
||||
2. Updated `changelog.md` with the new feature
|
||||
3. Updated `content/pages/changelog-page.md` with release notes
|
||||
4. Created a blog post: "How to setup WorkOS"
|
||||
5. Updated `files.md` with new file descriptions
|
||||
|
||||
The feature went live on December 29, 2025 as part of v1.45.0.
|
||||
|
||||
## What I learned about prompt engineering
|
||||
|
||||
### Start with documentation
|
||||
|
||||
Updating docs first helped me clarify what I wanted to build. When I asked Cursor to implement features, it had context from the docs to understand the patterns.
|
||||
|
||||
### Use PRDs as blueprints
|
||||
|
||||
The PRD from Claude became the single source of truth. Every implementation step referenced the PRD. This kept the work focused and prevented scope creep.
|
||||
|
||||
### Break work into small tasks
|
||||
|
||||
The plan file broke the work into specific, actionable tasks. Each task was small enough that Cursor could complete it in one go. This made progress visible and debugging easier.
|
||||
|
||||
### Iterate on prompts
|
||||
|
||||
When Cursor didn't get something right, I refined my prompts. Instead of "add authentication," I said "add WorkOS authentication that works with or without WorkOS configured, checking siteConfig.dashboard.requireAuth."
|
||||
|
||||
Specific prompts led to better results.
|
||||
|
||||
### Trust but verify
|
||||
|
||||
Cursor generated working code, but I tested each change. The conditional authentication logic needed manual verification to ensure it handled all cases correctly.
|
||||
|
||||
## The vibe coding experience
|
||||
|
||||
Vibe coding means working with AI tools in a flow state. You describe what you want, the AI generates code, you test it, and you iterate.
|
||||
|
||||
With Cursor, this felt natural:
|
||||
|
||||
1. I described the feature
|
||||
2. Cursor generated the code
|
||||
3. I tested it locally
|
||||
4. I asked for refinements
|
||||
5. We repeated until it worked
|
||||
|
||||
The back-and-forth felt like pair programming with a very fast partner who never gets tired.
|
||||
|
||||
## Key files created
|
||||
|
||||
- `src/pages/Callback.tsx` - Handles OAuth callback
|
||||
- `src/utils/workos.ts` - WorkOS configuration utility
|
||||
- `convex/auth.config.ts` - Convex auth configuration
|
||||
- `prds/workos-authkit-dashboard-guide.md` - Step-by-step PRD
|
||||
- `.cursor/plans/workos_setup_9603c983.plan.md` - Implementation plan
|
||||
|
||||
## Key files modified
|
||||
|
||||
- `src/main.tsx` - Added conditional WorkOS providers
|
||||
- `src/App.tsx` - Added callback route handling
|
||||
- `src/pages/Dashboard.tsx` - Added optional authentication
|
||||
- `src/config/siteConfig.ts` - Added dashboard configuration
|
||||
- `content/pages/docs.md` - Added dashboard documentation
|
||||
|
||||
## Result
|
||||
|
||||
The dashboard now supports optional WorkOS authentication. Users can:
|
||||
|
||||
- Use the dashboard without WorkOS (open access)
|
||||
- Enable WorkOS authentication via `siteConfig.dashboard.requireAuth`
|
||||
- See setup instructions if auth is required but WorkOS isn't configured
|
||||
|
||||
The implementation is clean, type-safe, and follows Convex best practices. It works whether WorkOS is configured or not.
|
||||
|
||||
## Takeaways
|
||||
|
||||
1. **Documentation first** - Writing docs clarifies requirements
|
||||
2. **PRDs as blueprints** - Import PRDs to guide implementation
|
||||
3. **Small tasks** - Break work into specific, actionable items
|
||||
4. **Specific prompts** - Detailed prompts produce better code
|
||||
5. **Test everything** - Verify AI-generated code works correctly
|
||||
6. **Iterate quickly** - Use AI tools to move fast and refine
|
||||
|
||||
Adding WorkOS to my Convex app took a few hours from start to published. Most of that time was testing and refining. The actual coding was fast thanks to Cursor and good prompt engineering.
|
||||
|
||||
The [How to setup WorkOS](https://www.markdown.fast/how-to-setup-workos) is the setup guide covers everything you need to get started
|
||||
@@ -352,6 +352,7 @@ Your markdown content here...
|
||||
| `authorName` | No | Author display name shown next to date |
|
||||
| `authorImage` | No | Round author avatar image URL |
|
||||
| `rightSidebar` | No | Enable right sidebar with CopyPageDropdown (opt-in, requires explicit `true`) |
|
||||
| `unlisted` | No | Hide from listings but allow direct access via slug. Set `true` to hide from blog listings, featured sections, tag pages, search results, and related posts. Post remains accessible via direct link. |
|
||||
|
||||
### How Frontmatter Works
|
||||
|
||||
|
||||
@@ -29,14 +29,26 @@ Released December 30, 2025
|
||||
- Automated CLAUDE.md updates via sync-discovery-files.ts
|
||||
- CLAUDE.md status comment updated during `npm run sync:discovery`
|
||||
- Includes current site name, post count, page count, and last updated timestamp
|
||||
- Unlisted posts feature
|
||||
- New `unlisted` frontmatter field for blog posts
|
||||
- Set `unlisted: true` to hide posts from listings while keeping them accessible via direct link
|
||||
- Unlisted posts are excluded from: blog listings, featured sections, tag pages, search results, and related posts
|
||||
- Posts remain accessible via direct URL (e.g., `/blog/post-slug`)
|
||||
- Useful for draft posts, private content, or posts you want to share via direct link only
|
||||
|
||||
**Technical details:**
|
||||
|
||||
- New file: `CLAUDE.md` in project root
|
||||
- New directory: `.claude/skills/` with three markdown files
|
||||
- Updated: `scripts/sync-discovery-files.ts` to update CLAUDE.md alongside AGENTS.md and llms.txt
|
||||
- Updated: `convex/schema.ts` - Added `unlisted` optional boolean field to posts table
|
||||
- Updated: `convex/posts.ts` - All listing queries filter out unlisted posts
|
||||
- Updated: `convex/search.ts` - Search excludes unlisted posts from results
|
||||
- Updated: `scripts/sync-posts.ts` - Added `unlisted` to interfaces and parsing logic
|
||||
- Updated: `src/pages/Write.tsx` - Added `unlisted` to POST_FIELDS frontmatter reference
|
||||
- Updated documentation: `.claude/skills/frontmatter.md`, `content/pages/docs.md`, `content/blog/setup-guide.md`, `files.md`
|
||||
|
||||
Updated files: `CLAUDE.md`, `.claude/skills/frontmatter.md`, `.claude/skills/convex.md`, `.claude/skills/sync.md`, `scripts/sync-discovery-files.ts`, `files.md`, `changelog.md`, `TASK.md`
|
||||
Updated files: `CLAUDE.md`, `.claude/skills/frontmatter.md`, `.claude/skills/convex.md`, `.claude/skills/sync.md`, `scripts/sync-discovery-files.ts`, `convex/schema.ts`, `convex/posts.ts`, `convex/search.ts`, `scripts/sync-posts.ts`, `src/pages/Write.tsx`, `files.md`, `changelog.md`, `content/pages/changelog-page.md`
|
||||
|
||||
## v2.0.0
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ slug: "docs"
|
||||
published: true
|
||||
order: 0
|
||||
layout: "sidebar"
|
||||
rightSidebar: true
|
||||
aiChat: true
|
||||
showFooter: true
|
||||
---
|
||||
@@ -129,6 +128,7 @@ Content here...
|
||||
| `blogFeatured` | No | Show as featured on blog page (first becomes hero, rest in 2-column row) |
|
||||
| `newsletter` | No | Override newsletter signup display (`true` to show, `false` to hide) |
|
||||
| `contactForm` | No | Enable contact form on this post |
|
||||
| `unlisted` | No | Hide from listings but allow direct access via slug. Set `true` to hide from blog listings, featured sections, tag pages, search results, and related posts. Post remains accessible via direct link. |
|
||||
| `showImageAtTop` | No | Set `true` to display the `image` field at the top of the post above the header (default: `false`) |
|
||||
|
||||
### Static pages
|
||||
@@ -173,6 +173,8 @@ Content here...
|
||||
|
||||
**Hide pages from navigation:** Set `showInNav: false` to keep a page published and accessible via direct URL, but hidden from the navigation menu. Pages with `showInNav: false` remain searchable and available via API endpoints. Useful for pages you want to link directly but not show in the main nav.
|
||||
|
||||
**Unlisted posts:** Set `unlisted: true` to hide a blog post from all listings while keeping it accessible via direct link. Unlisted posts are excluded from: blog listings (`/blog` page), featured sections (homepage), tag pages (`/tags/[tag]`), search results (Command+K), and related posts. The post remains accessible via direct URL (e.g., `/blog/post-slug`). Useful for draft posts, private content, or posts you want to share via direct link only. Note: `unlisted` only works for blog posts, not pages.
|
||||
|
||||
**Show image at top:** Add `showImageAtTop: true` to display the `image` field at the top of the post/page above the header. Default behavior: if `showImageAtTop` is not set or `false`, image only used for Open Graph previews and featured card thumbnails.
|
||||
|
||||
**Image lightbox:** Images in blog posts and pages automatically open in a full-screen lightbox when clicked (if enabled in `siteConfig.imageLightbox.enabled`). This allows readers to view images at full size. The lightbox can be closed by clicking outside the image, pressing Escape, or clicking the close button.
|
||||
|
||||
@@ -86,8 +86,11 @@ export const getAllPosts = query({
|
||||
.withIndex("by_published", (q) => q.eq("published", true))
|
||||
.collect();
|
||||
|
||||
// Filter out unlisted posts
|
||||
const listedPosts = posts.filter((p) => !p.unlisted);
|
||||
|
||||
// Sort by date descending
|
||||
const sortedPosts = posts.sort(
|
||||
const sortedPosts = listedPosts.sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
|
||||
);
|
||||
|
||||
@@ -143,7 +146,7 @@ export const getBlogFeaturedPosts = query({
|
||||
|
||||
// Filter to only published posts and sort by date descending
|
||||
const publishedFeatured = posts
|
||||
.filter((p) => p.published)
|
||||
.filter((p) => p.published && !p.unlisted)
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
|
||||
return publishedFeatured.map((post) => ({
|
||||
@@ -184,7 +187,7 @@ export const getFeaturedPosts = query({
|
||||
|
||||
// Filter to only published posts and sort by featuredOrder
|
||||
const featuredPosts = posts
|
||||
.filter((p) => p.published)
|
||||
.filter((p) => p.published && !p.unlisted)
|
||||
.sort((a, b) => {
|
||||
const orderA = a.featuredOrder ?? 999;
|
||||
const orderB = b.featuredOrder ?? 999;
|
||||
@@ -335,9 +338,9 @@ export const getRecentPostsInternal = internalQuery({
|
||||
.withIndex("by_published", (q) => q.eq("published", true))
|
||||
.collect();
|
||||
|
||||
// Filter posts by date and sort descending
|
||||
// Filter posts by date and sort descending, excluding unlisted
|
||||
const recentPosts = posts
|
||||
.filter((post) => post.date >= args.since)
|
||||
.filter((post) => post.date >= args.since && !post.unlisted)
|
||||
.sort((a, b) => b.date.localeCompare(a.date))
|
||||
.map((post) => ({
|
||||
slug: post.slug,
|
||||
@@ -617,9 +620,12 @@ export const getAllTags = query({
|
||||
.withIndex("by_published", (q) => q.eq("published", true))
|
||||
.collect();
|
||||
|
||||
// Filter out unlisted posts
|
||||
const listedPosts = posts.filter((p) => !p.unlisted);
|
||||
|
||||
// Count occurrences of each tag
|
||||
const tagCounts = new Map<string, number>();
|
||||
for (const post of posts) {
|
||||
for (const post of listedPosts) {
|
||||
for (const tag of post.tags) {
|
||||
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
||||
}
|
||||
@@ -665,9 +671,11 @@ export const getPostsByTag = query({
|
||||
.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()),
|
||||
// Filter posts that have the specified tag and are not unlisted
|
||||
const filteredPosts = posts.filter(
|
||||
(post) =>
|
||||
!post.unlisted &&
|
||||
post.tags.some((t) => t.toLowerCase() === args.tag.toLowerCase()),
|
||||
);
|
||||
|
||||
// Sort by date descending
|
||||
@@ -728,9 +736,9 @@ export const getRelatedPosts = query({
|
||||
.withIndex("by_published", (q) => q.eq("published", true))
|
||||
.collect();
|
||||
|
||||
// Find posts that share tags, excluding current post
|
||||
// Find posts that share tags, excluding current post and unlisted posts
|
||||
const relatedPosts = posts
|
||||
.filter((post) => post.slug !== args.currentSlug)
|
||||
.filter((post) => post.slug !== args.currentSlug && !post.unlisted)
|
||||
.map((post) => {
|
||||
const sharedTags = post.tags.filter((tag) =>
|
||||
args.tags.some((t) => t.toLowerCase() === tag.toLowerCase()),
|
||||
|
||||
@@ -28,6 +28,7 @@ export default defineSchema({
|
||||
blogFeatured: v.optional(v.boolean()), // Show as hero featured post on /blog page
|
||||
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
|
||||
lastSyncedAt: v.number(),
|
||||
})
|
||||
.index("by_slug", ["slug"])
|
||||
|
||||
@@ -72,6 +72,9 @@ export const search = query({
|
||||
if (seenPostIds.has(post._id)) continue;
|
||||
seenPostIds.add(post._id);
|
||||
|
||||
// Skip unlisted posts
|
||||
if (post.unlisted) continue;
|
||||
|
||||
// Create snippet from content and find anchor
|
||||
const { snippet, anchor } = createSnippet(post.content, args.query, 120);
|
||||
|
||||
|
||||
1
files.md
1
files.md
@@ -169,6 +169,7 @@ Markdown files with frontmatter for blog posts. Each file becomes a blog post.
|
||||
| `blogFeatured` | Show as featured on blog page (optional, first becomes hero, rest in 2-column row) |
|
||||
| `newsletter` | Override newsletter signup display (optional, true/false) |
|
||||
| `contactForm` | Enable contact form on this post (optional). Requires siteConfig.contactForm.enabled: true and AGENTMAIL_API_KEY/AGENTMAIL_INBOX environment variables. |
|
||||
| `unlisted` | Hide from listings but allow direct access via slug (optional). Set `true` to hide from blog listings, featured sections, tag pages, search results, and related posts. Post remains accessible via direct link. |
|
||||
|
||||
## Static Pages (`content/pages/`)
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ interface ParsedPost {
|
||||
blogFeatured?: boolean; // Show as hero featured post on /blog page
|
||||
newsletter?: boolean; // Override newsletter signup display (true/false)
|
||||
contactForm?: boolean; // Enable contact form on this post
|
||||
unlisted?: boolean; // Hide from listings but allow direct access via slug
|
||||
}
|
||||
|
||||
// Page frontmatter (for static pages like About, Projects, Contact)
|
||||
@@ -172,6 +173,7 @@ function parseMarkdownFile(filePath: string): ParsedPost | null {
|
||||
blogFeatured: frontmatter.blogFeatured, // Show as hero featured post on /blog page
|
||||
newsletter: frontmatter.newsletter, // Override newsletter signup display
|
||||
contactForm: frontmatter.contactForm, // Enable contact form on this post
|
||||
unlisted: frontmatter.unlisted, // Hide from listings but allow direct access
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error parsing ${filePath}:`, error);
|
||||
|
||||
@@ -59,6 +59,7 @@ const POST_FIELDS = [
|
||||
{ name: "blogFeatured", required: false, example: "true" },
|
||||
{ name: "newsletter", required: false, example: "true" },
|
||||
{ name: "contactForm", required: false, example: "true" },
|
||||
{ name: "unlisted", required: false, example: "true" },
|
||||
];
|
||||
|
||||
// Frontmatter field definitions for pages
|
||||
|
||||
Reference in New Issue
Block a user