mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 04:09:14 +00:00
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:
63
scripts/send-newsletter-stats.ts
Normal file
63
scripts/send-newsletter-stats.ts
Normal 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
101
scripts/send-newsletter.ts
Normal 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();
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user