docs: add changelog entries for v1.33.1 through v1.37.0

Add missing changelog entries to content/pages/changelog-page.md:

v1.34.0 (2025-12-26): Blog page featured layout with hero post
- blogFeatured frontmatter field for posts
- Hero card displays first featured post with landscape image
- 2-column featured row for remaining featured posts
- 3-column grid for regular posts

v1.35.0 (2025-12-26): Image support at top of posts and pages
- showImageAtTop frontmatter field
- Full-width image display above post header
- Works for both posts and pages

v1.36.0 (2025-12-27): Social footer component
- Customizable social links (8 platform types)
- Copyright with auto-updating year
- showSocialFooter frontmatter field for per-page control
- Configurable via siteConfig.socialFooter

v1.37.0 (2025-12-27): Newsletter Admin UI
- Three-column admin interface at /newsletter-admin
- Subscriber management with search and filters
- Send newsletter panel (post selection or custom email)
- Weekly digest automation (Sunday 9am UTC)
- Developer notifications (subscriber alerts, weekly stats)
- Markdown-to-HTML conversion for custom emails
This commit is contained in:
Wayne Sutton
2025-12-27 15:32:07 -08:00
parent c312a4c808
commit a87db9d171
55 changed files with 7753 additions and 1260 deletions

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env npx ts-node
/**
* Send weekly stats summary email to developer
*
* Usage:
* npm run newsletter:send:stats
*
* Environment variables (from .env.local):
* - VITE_CONVEX_URL: Convex deployment URL
* - SITE_NAME: Your site name (default: "Newsletter")
*
* Note: AGENTMAIL_API_KEY, AGENTMAIL_INBOX, and AGENTMAIL_CONTACT_EMAIL must be set in Convex dashboard
*/
import { ConvexHttpClient } from "convex/browser";
import { api } from "../convex/_generated/api";
import dotenv from "dotenv";
// Load environment variables
dotenv.config({ path: ".env.local" });
dotenv.config({ path: ".env.production.local" });
async function main() {
const convexUrl = process.env.VITE_CONVEX_URL;
if (!convexUrl) {
console.error(
"Error: VITE_CONVEX_URL not found. Run 'npx convex dev' to create .env.local"
);
process.exit(1);
}
const siteName = process.env.SITE_NAME || "Newsletter";
console.log("Sending weekly stats summary...");
console.log(`Site name: ${siteName}`);
console.log("");
const client = new ConvexHttpClient(convexUrl);
try {
// Call the mutation to schedule the stats summary send
const result = await client.mutation(api.newsletter.scheduleSendStatsSummary, {
siteName,
});
if (result.success) {
console.log("✓ Stats summary scheduled successfully!");
console.log(result.message);
console.log("");
console.log("The email will be sent to AGENTMAIL_INBOX (or AGENTMAIL_CONTACT_EMAIL if set).");
} else {
console.error("✗ Failed to send stats summary:");
console.error(result.message);
process.exit(1);
}
} catch (error) {
console.error("Error:", error);
process.exit(1);
}
}
main();

101
scripts/send-newsletter.ts Normal file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env npx ts-node
/**
* Send newsletter for a specific post to all subscribers
*
* Usage:
* npm run newsletter:send <post-slug>
*
* Example:
* npm run newsletter:send setup-guide
*
* Environment variables (from .env.local):
* - VITE_CONVEX_URL: Convex deployment URL
* - SITE_URL: Your site URL (default: https://markdown.fast)
* - SITE_NAME: Your site name (default: "Newsletter")
*
* Note: AGENTMAIL_API_KEY and AGENTMAIL_INBOX must be set in Convex dashboard
*/
import { ConvexHttpClient } from "convex/browser";
import { api } from "../convex/_generated/api";
import dotenv from "dotenv";
// Load environment variables
dotenv.config({ path: ".env.local" });
dotenv.config({ path: ".env.production.local" });
async function main() {
const postSlug = process.argv[2];
if (!postSlug) {
console.error("Usage: npm run newsletter:send <post-slug>");
console.error("Example: npm run newsletter:send setup-guide");
process.exit(1);
}
const convexUrl = process.env.VITE_CONVEX_URL;
if (!convexUrl) {
console.error(
"Error: VITE_CONVEX_URL not found. Run 'npx convex dev' to create .env.local"
);
process.exit(1);
}
const siteUrl = process.env.SITE_URL || "https://markdown.fast";
const siteName = process.env.SITE_NAME || "Newsletter";
console.log(`Sending newsletter for post: ${postSlug}`);
console.log(`Site URL: ${siteUrl}`);
console.log(`Site name: ${siteName}`);
console.log("");
const client = new ConvexHttpClient(convexUrl);
try {
// First check if post exists
const post = await client.query(api.posts.getPostBySlug, { slug: postSlug });
if (!post) {
console.error(`Error: Post "${postSlug}" not found or not published.`);
process.exit(1);
}
console.log(`Found post: "${post.title}"`);
// Get subscriber count first
const subscriberCount = await client.query(api.newsletter.getSubscriberCount);
console.log(`Active subscribers: ${subscriberCount}`);
if (subscriberCount === 0) {
console.log("No subscribers to send to.");
process.exit(0);
}
console.log("");
console.log("Sending newsletter...");
// Call the mutation directly to schedule the newsletter send
const result = await client.mutation(api.newsletter.scheduleSendPostNewsletter, {
postSlug,
siteUrl,
siteName,
});
if (result.success) {
console.log("✓ Newsletter scheduled successfully!");
console.log(result.message);
console.log("");
console.log("The newsletter is being sent in the background.");
console.log("Check the Newsletter Admin page or recent sends to see results.");
} else {
console.error("✗ Failed to send newsletter:");
console.error(result.message);
process.exit(1);
}
} catch (error) {
console.error("Error:", error);
process.exit(1);
}
}
main();

View File

@@ -39,8 +39,13 @@ interface PostFrontmatter {
authorImage?: string; // Author avatar image URL (round)
layout?: string; // Layout type: "sidebar" for docs-style layout
rightSidebar?: boolean; // Enable right sidebar with CopyPageDropdown (default: true when siteConfig.rightSidebar.enabled)
showFooter?: boolean; // Show footer on this post (overrides siteConfig default)
footer?: string; // Footer markdown content (overrides siteConfig defaultContent)
showSocialFooter?: boolean; // Show social footer on this post (overrides siteConfig default)
aiChat?: boolean; // Enable AI chat in right sidebar (requires rightSidebar: true)
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
}
interface ParsedPost {
@@ -63,8 +68,11 @@ interface ParsedPost {
rightSidebar?: boolean; // Enable right sidebar with CopyPageDropdown (default: true when siteConfig.rightSidebar.enabled)
showFooter?: boolean; // Show footer on this post (overrides siteConfig default)
footer?: string; // Footer markdown content (overrides siteConfig defaultContent)
showSocialFooter?: boolean; // Show social footer on this post (overrides siteConfig default)
aiChat?: boolean; // Enable AI chat in right sidebar (requires rightSidebar: true)
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
}
// Page frontmatter (for static pages like About, Projects, Contact)
@@ -84,7 +92,11 @@ interface PageFrontmatter {
layout?: string; // Layout type: "sidebar" for docs-style layout
rightSidebar?: boolean; // Enable right sidebar with CopyPageDropdown (default: true when siteConfig.rightSidebar.enabled)
showFooter?: boolean; // Show footer on this page (overrides siteConfig default)
footer?: string; // Footer markdown content (overrides siteConfig defaultContent)
showSocialFooter?: boolean; // Show social footer on this page (overrides siteConfig default)
aiChat?: boolean; // Enable AI chat in right sidebar (requires rightSidebar: true)
contactForm?: boolean; // Enable contact form on this page
newsletter?: boolean; // Override newsletter signup display (true/false)
}
interface ParsedPage {
@@ -104,7 +116,11 @@ interface ParsedPage {
layout?: string; // Layout type: "sidebar" for docs-style layout
rightSidebar?: boolean; // Enable right sidebar with CopyPageDropdown (default: true when siteConfig.rightSidebar.enabled)
showFooter?: boolean; // Show footer on this page (overrides siteConfig default)
footer?: string; // Footer markdown content (overrides siteConfig defaultContent)
showSocialFooter?: boolean; // Show social footer on this page (overrides siteConfig default)
aiChat?: boolean; // Enable AI chat in right sidebar (requires rightSidebar: true)
contactForm?: boolean; // Enable contact form on this page
newsletter?: boolean; // Override newsletter signup display (true/false)
}
// Calculate reading time based on word count
@@ -149,8 +165,11 @@ function parseMarkdownFile(filePath: string): ParsedPost | null {
rightSidebar: frontmatter.rightSidebar, // Enable right sidebar with CopyPageDropdown
showFooter: frontmatter.showFooter, // Show footer on this post
footer: frontmatter.footer, // Footer markdown content
showSocialFooter: frontmatter.showSocialFooter, // Show social footer on this post
aiChat: frontmatter.aiChat, // Enable AI chat in right sidebar
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
};
} catch (error) {
console.error(`Error parsing ${filePath}:`, error);
@@ -205,7 +224,11 @@ function parsePageFile(filePath: string): ParsedPage | null {
layout: frontmatter.layout, // Layout type: "sidebar" for docs-style layout
rightSidebar: frontmatter.rightSidebar, // Enable right sidebar with CopyPageDropdown
showFooter: frontmatter.showFooter, // Show footer on this page
footer: frontmatter.footer, // Footer markdown content
showSocialFooter: frontmatter.showSocialFooter, // Show social footer on this page
aiChat: frontmatter.aiChat, // Enable AI chat in right sidebar
contactForm: frontmatter.contactForm, // Enable contact form on this page
newsletter: frontmatter.newsletter, // Override newsletter signup display
};
} catch (error) {
console.error(`Error parsing page ${filePath}:`, error);