feat: Multi-model AI chat and image generation in Dashboard

This commit is contained in:
Wayne Sutton
2026-01-01 22:00:46 -08:00
parent 4cfbb2588a
commit a9f56d9c04
35 changed files with 2859 additions and 179 deletions

View File

@@ -440,12 +440,14 @@ function updatePostTsx(config: ForkConfig): void {
console.log("\nUpdating src/pages/Post.tsx...");
updateFile("src/pages/Post.tsx", [
// Match any existing SITE_URL value (https://...)
{
search: /const SITE_URL = "https:\/\/markdowncms\.netlify\.app";/,
search: /const SITE_URL = "https:\/\/[^"]+";/,
replace: `const SITE_URL = "${config.siteUrl}";`,
},
// Match any existing SITE_NAME value
{
search: /const SITE_NAME = "markdown sync framework";/,
search: /const SITE_NAME = "[^"]+";/,
replace: `const SITE_NAME = "${config.siteName}";`,
},
]);
@@ -456,22 +458,31 @@ function updateConvexHttp(config: ForkConfig): void {
console.log("\nUpdating convex/http.ts...");
updateFile("convex/http.ts", [
// Match any existing SITE_URL value with process.env fallback
{
search: /const SITE_URL = process\.env\.SITE_URL \|\| "https:\/\/markdowncms\.netlify\.app";/,
search: /const SITE_URL = process\.env\.SITE_URL \|\| "https:\/\/[^"]+";/,
replace: `const SITE_URL = process.env.SITE_URL || "${config.siteUrl}";`,
},
// Match any existing SITE_NAME value (line 10)
{
search: /const SITE_NAME = "markdown sync framework";/,
search: /const SITE_NAME = "[^"]+";/,
replace: `const SITE_NAME = "${config.siteName}";`,
},
// Match any existing siteUrl in generateMetaHtml function
{
search: /const siteUrl = process\.env\.SITE_URL \|\| "https:\/\/markdowncms\.netlify\.app";/,
search: /const siteUrl = process\.env\.SITE_URL \|\| "https:\/\/[^"]+";/,
replace: `const siteUrl = process.env.SITE_URL || "${config.siteUrl}";`,
},
// Match any existing siteName in generateMetaHtml function
{
search: /const siteName = "markdown sync framework";/,
search: /const siteName = "[^"]+";/,
replace: `const siteName = "${config.siteName}";`,
},
// Update the description in API responses
{
search: /"An open-source publishing framework[^"]*"/g,
replace: `"${config.siteDescription}"`,
},
]);
}
@@ -480,14 +491,17 @@ function updateConvexRss(config: ForkConfig): void {
console.log("\nUpdating convex/rss.ts...");
updateFile("convex/rss.ts", [
// Match any existing SITE_URL value with process.env fallback
{
search: /const SITE_URL = process\.env\.SITE_URL \|\| "https:\/\/markdowncms\.netlify\.app";/,
search: /const SITE_URL = process\.env\.SITE_URL \|\| "https:\/\/[^"]+";/,
replace: `const SITE_URL = process.env.SITE_URL || "${config.siteUrl}";`,
},
// Match any existing SITE_TITLE value
{
search: /const SITE_TITLE = "markdown sync framework";/,
search: /const SITE_TITLE = "[^"]+";/,
replace: `const SITE_TITLE = "${config.siteName}";`,
},
// Match any existing SITE_DESCRIPTION value (multiline)
{
search: /const SITE_DESCRIPTION =\s*"[^"]+";/,
replace: `const SITE_DESCRIPTION =\n "${config.siteDescription}";`,
@@ -500,89 +514,94 @@ function updateIndexHtml(config: ForkConfig): void {
console.log("\nUpdating index.html...");
const replacements: Array<{ search: string | RegExp; replace: string }> = [
// Meta description
// Meta description (match any content)
{
search: /<meta\s*name="description"\s*content="[^"]*"\s*\/>/,
replace: `<meta\n name="description"\n content="${config.siteDescription}"\n />`,
},
// Meta author
// Meta author (match any content)
{
search: /<meta name="author" content="[^"]*" \/>/,
replace: `<meta name="author" content="${config.siteName}" />`,
},
// Open Graph title
// Open Graph title (match any content)
{
search: /<meta property="og:title" content="[^"]*" \/>/,
replace: `<meta property="og:title" content="${config.siteName}" />`,
},
// Open Graph description
// Open Graph description (match any content)
{
search: /<meta\s*property="og:description"\s*content="[^"]*"\s*\/>/,
replace: `<meta\n property="og:description"\n content="${config.siteDescription}"\n />`,
},
// Open Graph URL
// Open Graph URL (match any https URL)
{
search: /<meta property="og:url" content="https:\/\/markdowncms\.netlify\.app\/" \/>/,
search: /<meta property="og:url" content="https:\/\/[^"]*" \/>/,
replace: `<meta property="og:url" content="${config.siteUrl}/" />`,
},
// Open Graph site name
// Open Graph site name (match any content)
{
search: /<meta property="og:site_name" content="[^"]*" \/>/,
search: /<meta property="og:site_name" content="[^"]*"\s*\/>/,
replace: `<meta property="og:site_name" content="${config.siteName}" />`,
},
// Open Graph image
// Open Graph site name with newline formatting
{
search: /<meta\s*property="og:image"\s*content="https:\/\/markdowncms\.netlify\.app[^"]*"\s*\/>/,
replace: `<meta\n property="og:image"\n content="${config.siteUrl}/images/og-default.svg"\n />`,
search: /<meta\s*property="og:site_name"\s*content="[^"]*"\s*>/,
replace: `<meta\n property="og:site_name"\n content="${config.siteName}"\n >`,
},
// Twitter domain
// Open Graph image (match any https URL)
{
search: /<meta\s*property="og:image"\s*content="https:\/\/[^"]*"\s*\/>/,
replace: `<meta\n property="og:image"\n content="${config.siteUrl}/images/og-default.png"\n />`,
},
// Twitter domain (match any domain)
{
search: /<meta property="twitter:domain" content="[^"]*" \/>/,
replace: `<meta property="twitter:domain" content="${config.siteDomain}" />`,
},
// Twitter URL
// Twitter URL (match any https URL)
{
search: /<meta property="twitter:url" content="https:\/\/markdowncms\.netlify\.app\/" \/>/,
search: /<meta property="twitter:url" content="https:\/\/[^"]*" \/>/,
replace: `<meta property="twitter:url" content="${config.siteUrl}/" />`,
},
// Twitter title
// Twitter title (match any content)
{
search: /<meta name="twitter:title" content="[^"]*" \/>/,
replace: `<meta name="twitter:title" content="${config.siteName}" />`,
},
// Twitter description
// Twitter description (match any content)
{
search: /<meta\s*name="twitter:description"\s*content="[^"]*"\s*\/>/,
replace: `<meta\n name="twitter:description"\n content="${config.siteDescription}"\n />`,
},
// Twitter image
// Twitter image (match any https URL)
{
search: /<meta\s*name="twitter:image"\s*content="https:\/\/markdowncms\.netlify\.app[^"]*"\s*\/>/,
replace: `<meta\n name="twitter:image"\n content="${config.siteUrl}/images/og-default.svg"\n />`,
search: /<meta\s*name="twitter:image"\s*content="https:\/\/[^"]*"\s*\/>/,
replace: `<meta\n name="twitter:image"\n content="${config.siteUrl}/images/og-default.png"\n />`,
},
// JSON-LD name
// JSON-LD name (match any value)
{
search: /"name": "markdown sync framework"/g,
replace: `"name": "${config.siteName}"`,
search: /"name": "[^"]+",\s*\n\s*"url":/g,
replace: `"name": "${config.siteName}",\n "url":`,
},
// JSON-LD URL
// JSON-LD URL (match any https URL)
{
search: /"url": "https:\/\/markdowncms\.netlify\.app"/g,
search: /"url": "https:\/\/[^"]+"/g,
replace: `"url": "${config.siteUrl}"`,
},
// JSON-LD description
// JSON-LD description (match any content)
{
search: /"description": "An open-source publishing framework[^"]*"/,
search: /"description": "[^"]+"/,
replace: `"description": "${config.siteDescription}"`,
},
// JSON-LD search target
// JSON-LD search target (match any URL)
{
search: /"target": "https:\/\/markdowncms\.netlify\.app\/\?q=\{search_term_string\}"/,
search: /"target": "https:\/\/[^"]+\/\?q=\{search_term_string\}"/,
replace: `"target": "${config.siteUrl}/?q={search_term_string}"`,
},
// Page title
// Page title (match any title content)
{
search: /<title>markdown "sync" framework<\/title>/,
search: /<title>[^<]+<\/title>/,
replace: `<title>${config.siteTitle}</title>`,
},
];
@@ -733,25 +752,30 @@ function updateOpenApiYaml(config: ForkConfig): void {
const githubUrl = `https://github.com/${config.githubUsername}/${config.githubRepo}`;
updateFile("public/openapi.yaml", [
// Match any title ending with API
{
search: /title: markdown sync framework API/,
search: /title: .+ API/,
replace: `title: ${config.siteName} API`,
},
// Match any GitHub contact URL
{
search: /url: https:\/\/github\.com\/waynesutton\/markdown-site/,
search: /url: https:\/\/github\.com\/[^\/]+\/[^\s]+/,
replace: `url: ${githubUrl}`,
},
// Match any server URL (production server line)
{
search: /- url: https:\/\/markdowncms\.netlify\.app/,
replace: `- url: ${config.siteUrl}`,
search: /- url: https:\/\/[^\s]+\n\s+description: Production server/,
replace: `- url: ${config.siteUrl}\n description: Production server`,
},
// Match any example site name
{
search: /example: markdown sync framework/g,
replace: `example: ${config.siteName}`,
search: /example: .+\n\s+url:/g,
replace: `example: ${config.siteName}\n url:`,
},
// Match any example URL (for site URL)
{
search: /example: https:\/\/markdowncms\.netlify\.app/g,
replace: `example: ${config.siteUrl}`,
search: /example: https:\/\/[^\s]+\n\s+posts:/,
replace: `example: ${config.siteUrl}\n posts:`,
},
]);
}

View File

@@ -2,13 +2,17 @@
/**
* Discovery Files Sync Script
*
* Reads siteConfig.ts and Convex data to update discovery files.
* Reads fork-config.json (if available), siteConfig.ts, and Convex data to update discovery files.
* Run with: npm run sync:discovery (dev) or npm run sync:discovery:prod (prod)
*
* This script updates:
* - AGENTS.md (project overview and current status sections)
* - CLAUDE.md (current status section for Claude Code)
* - public/llms.txt (site info, API endpoints, GitHub links)
*
* IMPORTANT: If fork-config.json exists, it will be used as the source of truth.
* This ensures that after running `npm run configure`, subsequent sync:discovery
* commands will use your configured values.
*/
import fs from "fs";
@@ -33,12 +37,48 @@ const PROJECT_ROOT = process.cwd();
const PUBLIC_DIR = path.join(PROJECT_ROOT, "public");
const ROOT_DIR = PROJECT_ROOT;
// Fork config interface (matches fork-config.json structure)
interface ForkConfig {
siteName: string;
siteTitle: string;
siteDescription: string;
siteUrl: string;
siteDomain: string;
githubUsername: string;
githubRepo: string;
contactEmail?: string;
bio?: string;
gitHubRepoConfig?: {
owner: string;
repo: string;
branch: string;
contentPath: string;
};
}
// Load fork-config.json if it exists
function loadForkConfig(): ForkConfig | null {
try {
const configPath = path.join(PROJECT_ROOT, "fork-config.json");
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, "utf-8");
const config = JSON.parse(content) as ForkConfig;
console.log("Using configuration from fork-config.json");
return config;
}
} catch (error) {
console.warn("Could not load fork-config.json, falling back to siteConfig.ts");
}
return null;
}
// Site config data structure
interface SiteConfigData {
name: string;
title: string;
bio: string;
description?: string;
siteUrl?: string; // Added to pass URL from fork-config.json
gitHubRepo?: {
owner: string;
repo: string;
@@ -47,8 +87,39 @@ interface SiteConfigData {
};
}
// Load site config from siteConfig.ts using regex
// Cached fork config
let cachedForkConfig: ForkConfig | null | undefined = undefined;
// Get fork config (cached)
function getForkConfig(): ForkConfig | null {
if (cachedForkConfig === undefined) {
cachedForkConfig = loadForkConfig();
}
return cachedForkConfig;
}
// Load site config - prioritizes fork-config.json over siteConfig.ts
function loadSiteConfig(): SiteConfigData {
// First try fork-config.json
const forkConfig = getForkConfig();
if (forkConfig) {
return {
name: forkConfig.siteName,
title: forkConfig.siteTitle,
bio: forkConfig.bio || forkConfig.siteDescription,
description: forkConfig.siteDescription,
siteUrl: forkConfig.siteUrl,
gitHubRepo: forkConfig.gitHubRepoConfig || {
owner: forkConfig.githubUsername,
repo: forkConfig.githubRepo,
branch: "main",
contentPath: "public/raw",
},
};
}
// Fall back to siteConfig.ts
console.log("No fork-config.json found, reading from siteConfig.ts");
try {
const configPath = path.join(
PROJECT_ROOT,
@@ -94,14 +165,14 @@ function loadSiteConfig(): SiteConfigData {
: undefined;
return {
name: nameMatch?.[1] || "markdown sync framework",
title: titleMatch?.[1] || "markdown sync framework",
name: nameMatch?.[1] || "Your Site Name",
title: titleMatch?.[1] || "Your Site Title",
bio:
bioMatch?.[1] ||
"An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs..",
"Your site description here.",
description:
bioMatch?.[1] ||
"An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs..",
"Your site description here.",
gitHubRepo,
};
}
@@ -110,30 +181,51 @@ function loadSiteConfig(): SiteConfigData {
}
return {
name: "markdown sync framework",
title: "markdown sync framework",
bio: "An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs..",
description:
"An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs..",
name: "Your Site Name",
title: "Your Site Title",
bio: "Your site description here.",
description: "Your site description here.",
};
}
// Get site URL from environment or config
function getSiteUrl(): string {
return (
process.env.SITE_URL || process.env.VITE_SITE_URL || "https://markdown.fast"
);
// Get site URL from fork-config.json, environment, or siteConfig
function getSiteUrl(siteConfig?: SiteConfigData): string {
// 1. Check fork-config.json (via siteConfig)
if (siteConfig?.siteUrl) {
return siteConfig.siteUrl;
}
// 2. Check fork-config.json directly
const forkConfig = getForkConfig();
if (forkConfig?.siteUrl) {
return forkConfig.siteUrl;
}
// 3. Check environment variables
if (process.env.SITE_URL) {
return process.env.SITE_URL;
}
if (process.env.VITE_SITE_URL) {
return process.env.VITE_SITE_URL;
}
// 4. Return placeholder (user should configure)
return "https://yoursite.example.com";
}
// Build GitHub URL from repo config or fallback
// Build GitHub URL from repo config or fork-config.json
function getGitHubUrl(siteConfig: SiteConfigData): string {
if (siteConfig.gitHubRepo) {
return `https://github.com/${siteConfig.gitHubRepo.owner}/${siteConfig.gitHubRepo.repo}`;
}
return (
process.env.GITHUB_REPO_URL ||
"https://github.com/waynesutton/markdown-site"
);
// Check fork-config.json directly
const forkConfig = getForkConfig();
if (forkConfig) {
return `https://github.com/${forkConfig.githubUsername}/${forkConfig.githubRepo}`;
}
// Check environment variable
if (process.env.GITHUB_REPO_URL) {
return process.env.GITHUB_REPO_URL;
}
// Return placeholder
return "https://github.com/yourusername/your-repo";
}
// Update CLAUDE.md with current status
@@ -326,9 +418,9 @@ async function syncDiscoveryFiles() {
// Initialize Convex client
const client = new ConvexHttpClient(convexUrl);
// Load site configuration
// Load site configuration (uses fork-config.json if available)
const siteConfig = loadSiteConfig();
const siteUrl = getSiteUrl();
const siteUrl = getSiteUrl(siteConfig);
console.log(`Site: ${siteConfig.name}`);
console.log(`Title: ${siteConfig.title}`);