From 55f4ada61a92db170e92d02ee696f204ff18e4fe Mon Sep 17 00:00:00 2001 From: Wayne Sutton Date: Sat, 10 Jan 2026 23:46:08 -0800 Subject: [PATCH] new: npx create-markdown-sync CLI , ui , related post thumbnails features --- .gitignore | 4 + FORK_CONFIG.md | 41 +- TASK.md | 26 +- changelog.md | 46 + content/blog/docs-opencode.md | 4 +- content/blog/fork-configuration-guide.md | 43 +- content/blog/how-to-use-firecrawl.md | 2 +- content/pages/changelog-page.md | 59 ++ content/pages/home.md | 8 +- convex/posts.ts | 8 + files.md | 28 +- package-lock.json | 842 +++++++++++++++++- package.json | 3 + packages/create-markdown-sync/README.md | 66 ++ packages/create-markdown-sync/package.json | 54 ++ packages/create-markdown-sync/src/clone.ts | 76 ++ .../create-markdown-sync/src/configure.ts | 302 +++++++ .../create-markdown-sync/src/convex-setup.ts | 107 +++ packages/create-markdown-sync/src/index.ts | 117 +++ packages/create-markdown-sync/src/install.ts | 92 ++ packages/create-markdown-sync/src/utils.ts | 133 +++ packages/create-markdown-sync/src/wizard.ts | 604 +++++++++++++ packages/create-markdown-sync/tsconfig.json | 20 + public/raw/about.md | 2 +- public/raw/changelog.md | 84 +- public/raw/contact.md | 2 +- public/raw/docs-ask-ai.md | 2 +- public/raw/docs-configuration.md | 2 +- public/raw/docs-content.md | 2 +- public/raw/docs-dashboard.md | 2 +- public/raw/docs-deployment.md | 2 +- public/raw/docs-frontmatter.md | 2 +- public/raw/docs-media-setup.md | 2 +- public/raw/docs-opencode.md | 2 + public/raw/docs-search.md | 2 +- public/raw/docs-semantic-search.md | 2 +- public/raw/documentation.md | 2 +- public/raw/footer.md | 2 +- public/raw/fork-configuration-guide.md | 43 +- public/raw/home-intro.md | 12 +- public/raw/index.md | 10 +- public/raw/newsletter.md | 2 +- public/raw/projects.md | 2 +- scripts/configure-fork.ts | 107 ++- src/components/BlogPost.tsx | 9 +- src/components/DiffCodeBlock.tsx | 47 + src/components/Footer.tsx | 255 ++++++ src/config/siteConfig.ts | 19 +- src/pages/Dashboard.tsx | 171 +++- src/pages/Home.tsx | 304 +++++++ src/pages/Post.tsx | 156 +++- src/styles/global.css | 399 ++++++++- 52 files changed, 4173 insertions(+), 160 deletions(-) create mode 100644 packages/create-markdown-sync/README.md create mode 100644 packages/create-markdown-sync/package.json create mode 100644 packages/create-markdown-sync/src/clone.ts create mode 100644 packages/create-markdown-sync/src/configure.ts create mode 100644 packages/create-markdown-sync/src/convex-setup.ts create mode 100644 packages/create-markdown-sync/src/index.ts create mode 100644 packages/create-markdown-sync/src/install.ts create mode 100644 packages/create-markdown-sync/src/utils.ts create mode 100644 packages/create-markdown-sync/src/wizard.ts create mode 100644 packages/create-markdown-sync/tsconfig.json diff --git a/.gitignore b/.gitignore index d5ec4e2..73841f0 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,10 @@ dist-ssr # Fork configuration (user-specific) fork-config.json +# CLI package build artifacts +packages/*/dist/ +packages/*/node_modules/ + diff --git a/FORK_CONFIG.md b/FORK_CONFIG.md index 985ab90..03f1518 100644 --- a/FORK_CONFIG.md +++ b/FORK_CONFIG.md @@ -1,12 +1,47 @@ # Fork Configuration Guide -After forking this repo, update these files with your site information. Choose one of two options: +After forking this repo, update these files with your site information. Choose one of three options: **Important**: Keep your `fork-config.json` file after configuring. The `sync:discovery` commands will use it to update discovery files (`AGENTS.md`, `CLAUDE.md`, `public/llms.txt`) with your configured values. --- -## Option 1: Automated Script (Recommended) +## Option 1: npx CLI (Recommended) + +Run a single command to scaffold and configure your project with an interactive wizard: + +```bash +npx create-markdown-sync my-site +``` + +The interactive wizard will: + +1. Clone the repository +2. Walk through all configuration options (site name, URL, features, etc.) +3. Install dependencies +4. Set up Convex backend (opens browser for login) +5. Run initial content sync +6. Open your site in the browser + +### After setup + +```bash +cd my-site +npm run dev # Start dev server at localhost:5173 +npm run sync # Sync content changes +``` + +### CLI Options + +```bash +npx create-markdown-sync my-site --force # Overwrite existing directory +npx create-markdown-sync my-site --skip-convex # Skip Convex setup +npx create-markdown-sync my-site --skip-open # Don't open browser after setup +``` + +--- + +## Option 2: Automated Script Run a single command to configure all files automatically. @@ -76,7 +111,7 @@ npm run dev # Test locally --- -## Option 2: Manual Configuration +## Option 3: Manual Configuration Edit each file individually following the guide below. diff --git a/TASK.md b/TASK.md index b36b7c4..e99593b 100644 --- a/TASK.md +++ b/TASK.md @@ -4,10 +4,34 @@ ## Current Status -v2.18.1 ready. README cleanup with docs links. +v2.19.0 ready. npx create-markdown-sync CLI. ## Completed +- [x] npx create-markdown-sync CLI (v2.19.0) + - [x] Created packages/create-markdown-sync/ monorepo package + - [x] Interactive wizard with 13 sections (50+ prompts) + - [x] Clone template from GitHub via giget + - [x] Configure site settings automatically + - [x] Install dependencies and set up Convex + - [x] Disable WorkOS auth by default (empty auth.config.ts) + - [x] Start dev server and open browser + - [x] Clear next steps with docs, deployment, and WorkOS links + - [x] Template fixes for siteConfig.ts embedded quotes + - [x] npm publishable package + +- [x] Related posts thumbnail view with toggle (v2.18.2) + - [x] Added thumbnail view as default for related posts section + - [x] Card layout with image on left, title/excerpt/meta on right + - [x] Added view toggle button (same icons as homepage featured section) + - [x] Added RelatedPostsConfig interface to siteConfig.ts + - [x] Added relatedPosts config options: defaultViewMode, showViewToggle + - [x] Added config UI in Dashboard ConfigSection + - [x] Updated getRelatedPosts query to return image, excerpt, authorName, authorImage + - [x] Added localStorage persistence for view mode preference + - [x] Added ~100 lines of CSS for thumbnail card styles + - [x] Mobile responsive design for thumbnail cards + - [x] README.md streamlined with docs links (v2.18.1) - [x] Reduced from 609 lines to 155 lines - [x] Added Documentation section with links to markdown.fast/docs diff --git a/changelog.md b/changelog.md index 6dd8cfe..4c04fd8 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,52 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [2.19.0] - 2026-01-10 + +### Added + +- `npx create-markdown-sync` CLI for scaffolding new projects + - Interactive wizard with 13 sections covering all configuration options + - Clones template from GitHub via giget + - Configures site settings automatically + - Installs dependencies + - Sets up Convex project (optional WorkOS auth disabled by default) + - Starts dev server and opens browser + - Clear next steps with docs, deployment, and WorkOS setup links + +### Technical + +- New `packages/create-markdown-sync/` monorepo package +- CLI files: index.ts, wizard.ts, clone.ts, configure.ts, install.ts, convex-setup.ts, utils.ts +- Template fixes for siteConfig.ts embedded quotes +- Empty auth.config.ts when auth not required (prevents WorkOS blocking) +- Added workspaces to root package.json +- Updated .gitignore for packages/*/dist/ and packages/*/node_modules/ + +## [2.18.2] - 2026-01-10 + +### Added + +- Related posts thumbnail view with toggle + - New thumbnail view shows post image, title, description, author, and date + - Toggle button to switch between thumbnail and list views (same icons as homepage featured) + - View preference saved to localStorage + - Default view mode and toggle visibility configurable via siteConfig.relatedPosts + - Dashboard Config section for related posts settings + +### Changed + +- Updated getRelatedPosts query to return image, excerpt, authorName, authorImage fields +- Related posts section now has header with title and optional toggle button + +### Technical + +- Added `RelatedPostsConfig` interface to siteConfig.ts +- Added `relatedPosts` configuration to SiteConfig interface +- Updated convex/posts.ts getRelatedPosts query with additional return fields +- Added related posts thumbnail CSS styles (~100 lines) +- Added relatedPostsDefaultViewMode and relatedPostsShowViewToggle to Dashboard ConfigSection + ## [2.18.1] - 2026-01-10 ### Changed diff --git a/content/blog/docs-opencode.md b/content/blog/docs-opencode.md index 5f59636..e72f5ab 100644 --- a/content/blog/docs-opencode.md +++ b/content/blog/docs-opencode.md @@ -1,6 +1,7 @@ --- title: "OpenCode Integration" slug: "docs-opencode" +description: "This framework includes full OpenCode support with agents, commands, skills, and plugins." date: "2026-01-10" published: true tags: ["opencode", "plugins", "terminal"] @@ -8,7 +9,8 @@ readTime: "4 min read" order: 2 showInNav: false layout: "sidebar" -featuredOrder: 2 +featuredOrder: 6 +featured: true blogFeatured: true rightSidebar: true showImageAtTop: false diff --git a/content/blog/fork-configuration-guide.md b/content/blog/fork-configuration-guide.md index 28693db..1bd8c42 100644 --- a/content/blog/fork-configuration-guide.md +++ b/content/blog/fork-configuration-guide.md @@ -23,9 +23,37 @@ docsSectionOrder: 1 After forking this markdown framework, you need to update configuration files with your site information. This affects your site name, URLs, RSS feeds, social sharing metadata, and AI discovery files. -Previously this meant editing 10+ files manually. Now you have two options. +Previously this meant editing 10+ files manually. Now you have three options. -## Option 1: Automated configuration +## Option 1: npx CLI (Recommended) + +The fastest way to get started. Run a single command to create a new project: + +```bash +npx create-markdown-sync my-site +``` + +The interactive wizard will: +1. Clone the template repository +2. Walk through all configuration options (site name, URLs, features, etc.) +3. Install dependencies +4. Set up Convex (opens browser for login) +5. Start the dev server and open your browser + +After setup, follow the on-screen instructions: + +```bash +cd my-site +npx convex dev # Start Convex (required first time) +npm run sync # Sync content (in another terminal) +npm run dev # Start dev server +``` + +**Resources:** +- Deployment: https://www.markdown.fast/docs-deployment +- WorkOS auth: https://www.markdown.fast/how-to-setup-workos + +## Option 2: Automated configuration Run a single command to configure everything at once. @@ -91,7 +119,7 @@ Updating public/.well-known/ai-plugin.json... Configuration complete! ``` -## Option 2: Manual configuration +## Option 3: Manual configuration If you prefer to update files manually, follow the guide in `FORK_CONFIG.md`. It includes: @@ -342,11 +370,12 @@ If you want to clear the sample content, delete the markdown files in those dire ## Summary -Two options after forking: +Three options to get started: -1. **Automated**: `cp fork-config.json.example fork-config.json`, edit JSON, run `npm run configure` -2. **Manual**: Follow `FORK_CONFIG.md` step-by-step or paste the AI prompt into Claude/ChatGPT +1. **npx CLI (Recommended)**: `npx create-markdown-sync my-site` - interactive wizard creates and configures everything +2. **Automated**: `cp fork-config.json.example fork-config.json`, edit JSON, run `npm run configure` +3. **Manual**: Follow `FORK_CONFIG.md` step-by-step or paste the AI prompt into Claude/ChatGPT -Both approaches update the same 11 files. The automated option takes about 30 seconds. The manual option gives you more control over each change. +The npx CLI is the fastest option for new projects. The automated and manual options work best for existing forks. Fork it, configure it, ship it. diff --git a/content/blog/how-to-use-firecrawl.md b/content/blog/how-to-use-firecrawl.md index 7de601a..2c55995 100644 --- a/content/blog/how-to-use-firecrawl.md +++ b/content/blog/how-to-use-firecrawl.md @@ -4,7 +4,7 @@ description: "Import external articles as markdown posts using Firecrawl. Get yo date: "2025-12-26" slug: "how-to-use-firecrawl" published: true -featured: true +featured: false featuredOrder: 6 image: /images/firecrwall-blog.png tags: ["tutorial", "firecrawl", "import"] diff --git a/content/pages/changelog-page.md b/content/pages/changelog-page.md index 098e9b2..20a8529 100644 --- a/content/pages/changelog-page.md +++ b/content/pages/changelog-page.md @@ -11,6 +11,65 @@ docsSectionOrder: 4 All notable changes to this project. +## v2.19.0 + +Released January 10, 2026 + +**npx create-markdown-sync CLI** + +Added a CLI tool to scaffold new markdown-sync projects with a single command. Run `npx create-markdown-sync my-site` to clone the template, configure your site through an interactive wizard, install dependencies, and set up Convex. + +**Changes:** + +- Interactive wizard with 13 sections covering all configuration options +- Clones template from GitHub via giget +- Configures site settings automatically (siteConfig.ts, fork-config.json) +- Installs dependencies with your preferred package manager +- Sets up Convex project with optional WorkOS auth (disabled by default) +- Starts dev server and opens browser +- Clear next steps with links to docs, deployment guide, and WorkOS setup + +**Files changed:** + +- `packages/create-markdown-sync/` - New monorepo package +- `packages/create-markdown-sync/src/index.ts` - CLI entry point +- `packages/create-markdown-sync/src/wizard.ts` - Interactive prompts +- `packages/create-markdown-sync/src/clone.ts` - Template cloning +- `packages/create-markdown-sync/src/configure.ts` - Site configuration +- `packages/create-markdown-sync/src/install.ts` - Dependency installation +- `packages/create-markdown-sync/src/convex-setup.ts` - Convex initialization +- `packages/create-markdown-sync/src/utils.ts` - Helper utilities +- `package.json` - Added workspaces configuration +- `.gitignore` - Added packages/*/dist/ and packages/*/node_modules/ + +--- + +## v2.18.2 + +Released January 10, 2026 + +**Related posts thumbnail view with toggle** + +Added a new thumbnail view for related posts at the bottom of blog posts. Shows post image, title, description, author avatar, author name, and date. Users can toggle between the new thumbnail view and the existing list view, with their preference saved to localStorage. + +**Changes:** + +- New thumbnail view with image on left, content on right (like attached screenshot) +- Toggle button using same icons as homepage featured section +- View preference saved to localStorage +- Configuration via siteConfig.relatedPosts (defaultViewMode, showViewToggle) +- Dashboard Config section for related posts settings + +**Files changed:** + +- `convex/posts.ts` - Updated getRelatedPosts query with image, excerpt, authorName, authorImage +- `src/config/siteConfig.ts` - Added RelatedPostsConfig interface and relatedPosts config +- `src/pages/Post.tsx` - Added thumbnail view, toggle state, and view mode rendering +- `src/pages/Dashboard.tsx` - Added Related Posts config card in ConfigSection +- `src/styles/global.css` - Added thumbnail view CSS styles + +--- + ## v2.18.1 Released January 10, 2026 diff --git a/content/pages/home.md b/content/pages/home.md index e8a7199..0158533 100644 --- a/content/pages/home.md +++ b/content/pages/home.md @@ -7,9 +7,9 @@ order: -1 textAlign: "left" --- -An open-source publishing framework built for AI agents and developers to ship **[docs](/docs)**, or **[blogs](/blog)** or **[websites](/)**. +The open-source markdown publishing framework for developers and AI agents to ship **[docs](/docs)**, or **[blogs](/blog)** or **[websites](/)** that's always in sync. -Write markdown, sync from the terminal. **[Fork it](https://github.com/waynesutton/markdown-site)**, customize it, ship it. +**[Fork it](https://github.com/waynesutton/markdown-site)** or npm npx create-markdown-sync my-site, customize it, ship it. **Semantic search** - Find content by meaning, not just keywords. **Ask AI** - Chat with your site content. Get answers with sources. + +``` + +``` diff --git a/convex/posts.ts b/convex/posts.ts index f82c1cd..5bd9439 100644 --- a/convex/posts.ts +++ b/convex/posts.ts @@ -773,6 +773,10 @@ export const getRelatedPosts = query({ date: v.string(), tags: v.array(v.string()), readTime: v.optional(v.string()), + image: v.optional(v.string()), + excerpt: v.optional(v.string()), + authorName: v.optional(v.string()), + authorImage: v.optional(v.string()), sharedTags: v.number(), }), ), @@ -804,6 +808,10 @@ export const getRelatedPosts = query({ date: post.date, tags: post.tags, readTime: post.readTime, + image: post.image, + excerpt: post.excerpt, + authorName: post.authorName, + authorImage: post.authorImage, sharedTags, }; }) diff --git a/files.md b/files.md index 4e5c474..1ce29dc 100644 --- a/files.md +++ b/files.md @@ -35,7 +35,7 @@ A brief description of each file in the codebase. | File | Description | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `siteConfig.ts` | Centralized site configuration (name, logo, blog page, posts display with homepage post limit and read more link, featured section with configurable title via featuredTitle, GitHub contributions, nav order, inner page logo settings, hardcoded navigation items for React routes, GitHub repository config for AI service raw URLs, font family configuration, right sidebar configuration, footer configuration with markdown support, social footer configuration, homepage configuration, AI chat configuration, aiDashboard configuration with multi-model support for text chat and image generation, newsletter configuration with admin and notifications, contact form configuration, weekly digest configuration, stats page configuration with public/private toggle, dashboard configuration with optional WorkOS authentication via requireAuth, image lightbox configuration with enabled toggle, semantic search configuration with enabled toggle and disabled by default to avoid blocking forks without OPENAI_API_KEY, twitter configuration for Twitter Cards meta tags, askAI configuration with enabled toggle, default model, and available models for header Ask AI feature) | +| `siteConfig.ts` | Centralized site configuration (name, logo, blog page, posts display with homepage post limit and read more link, featured section with configurable title via featuredTitle, GitHub contributions, nav order, inner page logo settings, hardcoded navigation items for React routes, GitHub repository config for AI service raw URLs, font family configuration, right sidebar configuration, footer configuration with markdown support, social footer configuration, homepage configuration, AI chat configuration, aiDashboard configuration with multi-model support for text chat and image generation, newsletter configuration with admin and notifications, contact form configuration, weekly digest configuration, stats page configuration with public/private toggle, dashboard configuration with optional WorkOS authentication via requireAuth, image lightbox configuration with enabled toggle, semantic search configuration with enabled toggle and disabled by default to avoid blocking forks without OPENAI_API_KEY, twitter configuration for Twitter Cards meta tags, askAI configuration with enabled toggle, default model, and available models for header Ask AI feature, relatedPosts configuration with defaultViewMode and showViewToggle options) | ### Pages (`src/pages/`) @@ -43,7 +43,7 @@ A brief description of each file in the codebase. | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Home.tsx` | Landing page with featured content and optional post list. Fetches home intro content from `content/pages/home.md` (slug: `home-intro`) for synced markdown intro text. Supports configurable post limit (homePostsLimit) and optional "read more" link (homePostsReadMore) via siteConfig.postsDisplay. Falls back to siteConfig.bio if home-intro page not found. Home intro content uses blog heading styles (blog-h1 through blog-h6) with clickable anchor links, matching blog post typography. Includes helper functions (generateSlug, getTextContent, HeadingAnchor) for heading ID generation and anchor links. Featured section title configurable via siteConfig.featuredTitle (default: "Get started:"). | | `Blog.tsx` | Dedicated blog page with featured layout: hero post (first blogFeatured), featured row (remaining blogFeatured in 2 columns with excerpts), and regular posts (3 columns without excerpts). Supports list/card view toggle. Includes back button in navigation | -| `Post.tsx` | Individual blog post or page view with optional left sidebar (TOC) and right sidebar (CopyPageDropdown). Includes back button (hidden when used as homepage), tag links, related posts section in footer for blog posts, footer component with markdown support (fetches footer.md content from Convex), and social footer. Supports 3-column layout at 1135px+. Can display image at top when showImageAtTop: true. Can be used as custom homepage via siteConfig.homepage (update SITE_URL/SITE_NAME when forking). SEO: Dynamic canonical URL, hreflang tags, og:url consistency, and twitter:site meta tags. DOM order optimized for SEO (article before sidebar, CSS order for visual layout). | +| `Post.tsx` | Individual blog post or page view with optional left sidebar (TOC) and right sidebar (CopyPageDropdown). Includes back button (hidden when used as homepage), tag links, related posts section in footer for blog posts with thumbnail/list view toggle, footer component with markdown support (fetches footer.md content from Convex), and social footer. Supports 3-column layout at 1135px+. Can display image at top when showImageAtTop: true. Can be used as custom homepage via siteConfig.homepage (update SITE_URL/SITE_NAME when forking). SEO: Dynamic canonical URL, hreflang tags, og:url consistency, and twitter:site meta tags. DOM order optimized for SEO (article before sidebar, CSS order for visual layout). Related posts view mode persists in localStorage. | | `Stats.tsx` | Real-time analytics dashboard with visitor stats and GitHub stars. Configurable via `siteConfig.statsPage` to enable/disable public access and navigation visibility. Shows disabled message when `enabled: false` (similar to NewsletterAdmin pattern). | | `DocsPage.tsx` | Docs landing page component for `/docs` route. Renders the page/post with `docsLanding: true` in DocsLayout. Fetches landing content via `getDocsLandingPage` and `getDocsLandingPost` queries. Includes Footer component (respects showFooter frontmatter), AI chat support (aiChatEnabled), and fallback to first docs item if no landing page is set. | | `TagPage.tsx` | Tag archive page displaying posts filtered by a specific tag. Includes view mode toggle (list/cards) with localStorage persistence | @@ -118,7 +118,7 @@ A brief description of each file in the codebase. | `schema.ts` | Database schema (posts, pages, viewCounts, pageViews, activeSessions, aiChats, aiGeneratedImages, newsletterSubscribers, newsletterSentPosts, contactMessages, askAISessions, contentVersions, versionControlSettings) with indexes for tag queries (by_tags), AI queries, blog featured posts (by_blogFeatured), source tracking (by_source), vector search (by_embedding), and version history (by_content, by_createdAt). Posts and pages include showSocialFooter, showImageAtTop, blogFeatured, contactForm, source, and embedding fields for frontmatter control, cloud CMS tracking, and semantic search. contentVersions stores snapshots before content updates. versionControlSettings stores the enable/disable toggle. | | `cms.ts` | CRUD mutations for dashboard cloud CMS: createPost, updatePost, deletePost, createPage, updatePage, deletePage, exportPostAsMarkdown, exportPageAsMarkdown. Posts/pages created via dashboard have `source: "dashboard"` (protected from sync overwrites). Captures versions before updates when version control is enabled. | | `importAction.ts` | Server-side Convex action for direct URL import via Firecrawl API. Scrapes URL, converts to markdown, saves directly to database with `source: "dashboard"`. Requires FIRECRAWL_API_KEY environment variable. | -| `posts.ts` | Queries and mutations for blog posts, view counts, getAllTags, getPostsByTag, getRelatedPosts, and getBlogFeaturedPosts. Includes tag-based queries for tag pages and related posts functionality. | +| `posts.ts` | Queries and mutations for blog posts, view counts, getAllTags, getPostsByTag, getRelatedPosts (returns image, excerpt, authorName, authorImage for thumbnail view), and getBlogFeaturedPosts. Includes tag-based queries for tag pages and related posts functionality. | | `pages.ts` | Queries and mutations for static pages | | `search.ts` | Full text search queries across posts and pages | | `semanticSearch.ts` | Vector-based semantic search action using OpenAI embeddings | @@ -352,6 +352,28 @@ Files include a metadata header with type (post/page), date, reading time, and t | `convex.md` | Convex patterns specific to this app (indexes, mutations, queries) | | `sync.md` | How sync commands work and content flow from markdown to database | +## CLI Package (`packages/create-markdown-sync/`) + +NPM CLI package for scaffolding new markdown-sync projects with a single command. + +| File | Description | +| ---- | ----------- | +| `package.json` | CLI package config with bin entry point | +| `tsconfig.json` | TypeScript config for CLI | +| `README.md` | NPM package readme | +| `src/index.ts` | Main entry point with CLI argument parsing | +| `src/wizard.ts` | Interactive prompts (13 sections, 50+ prompts) | +| `src/clone.ts` | Repository cloning via giget | +| `src/configure.ts` | Fork config generation and template fixes | +| `src/install.ts` | Dependency installation and dev server | +| `src/convex-setup.ts` | Convex project initialization | +| `src/utils.ts` | Validation helpers, logging, package manager detection | + +**Usage:** +```bash +npx create-markdown-sync my-site +``` + ## Cursor Rules (`.cursor/rules/`) | File | Description | diff --git a/package-lock.json b/package-lock.json index c361476..c1fe78e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "markdown-site", "version": "1.0.0", + "workspaces": [ + "packages/*" + ], "dependencies": { "@anthropic-ai/sdk": "^0.71.2", "@convex-dev/aggregate": "^0.2.0", @@ -3680,6 +3683,27 @@ "form-data": "^4.0.4" } }, + "node_modules/@types/prompts": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz", + "integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "kleur": "^3.0.3" + } + }, + "node_modules/@types/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -4167,7 +4191,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -4534,6 +4557,21 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4737,6 +4775,24 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -4744,6 +4800,33 @@ "license": "MIT", "peer": true }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -4818,6 +4901,21 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -4959,6 +5057,10 @@ "node": ">= 0.10" } }, + "node_modules/create-markdown-sync": { + "resolved": "packages/create-markdown-sync", + "link": true + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5064,6 +5166,34 @@ "dev": true, "license": "MIT" }, + "node_modules/default-browser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", + "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -5081,6 +5211,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -5691,6 +5833,29 @@ "node": ">=18.0.0" } }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -6122,6 +6287,36 @@ "node": ">= 0.8" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6235,6 +6430,18 @@ "node": ">=6.9.0" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -6281,6 +6488,18 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", @@ -6293,6 +6512,24 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.5.tgz", + "integrity": "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.5.4", + "pathe": "^2.0.3", + "tar": "^6.2.1" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -6877,6 +7114,15 @@ "node": ">= 14" } }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -7038,6 +7284,21 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -7089,6 +7350,36 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -7145,6 +7436,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -7332,6 +7662,15 @@ "node": ">=0.10.0" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -7375,6 +7714,46 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -7787,6 +8166,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -8395,6 +8780,30 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -8419,6 +8828,61 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -8497,6 +8961,12 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -8504,6 +8974,59 @@ "dev": true, "license": "MIT" }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.5.4.tgz", + "integrity": "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "tinyexec": "^0.3.2", + "ufo": "^1.5.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/nypm/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8571,6 +9094,21 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/oniguruma-parser": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", @@ -8588,6 +9126,24 @@ "regex-recursion": "^6.0.2" } }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openai": { "version": "4.104.0", "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", @@ -8671,6 +9227,91 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -8888,6 +9529,17 @@ "node": ">=16.20.0" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -8950,6 +9602,28 @@ "node": ">=6" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -9677,6 +10351,37 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -9836,6 +10541,18 @@ "node": ">= 18" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10171,6 +10888,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -10227,6 +10950,18 @@ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "license": "MIT" }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -10340,6 +11075,18 @@ "node": ">=0.10.0" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -10402,6 +11149,38 @@ "node": ">=8" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -11204,6 +11983,12 @@ "integrity": "sha512-dFSOFBKV6uwaloBCCUhxlD3Pr/P1a/tJdcmPrTXCHlEFD3faj0mztjcGn6VBAhQ0/Bdy8K3VWrrqwbt/ffsYsg==", "license": "MIT" }, + "node_modules/ufo": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.2.tgz", + "integrity": "sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==", + "license": "MIT" + }, "node_modules/unconfig": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/unconfig/-/unconfig-7.4.2.tgz", @@ -12266,6 +13051,21 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -12322,6 +13122,46 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "packages/create-markdown-sync": { + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "execa": "^8.0.1", + "giget": "^1.2.3", + "kleur": "^4.1.5", + "open": "^10.1.0", + "ora": "^8.0.1", + "prompts": "^2.4.2" + }, + "bin": { + "create-markdown-sync": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/prompts": "^2.4.9", + "typescript": "^5.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "packages/create-markdown-sync/node_modules/@types/node": { + "version": "20.19.28", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.28.tgz", + "integrity": "sha512-VyKBr25BuFDzBFCK5sUM6ZXiWfqgCTwTAOK8qzGV/m9FCirXYDlmczJ+d5dXBAQALGCdRRdbteKYfJ84NGEusw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "packages/create-markdown-sync/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 3f9d141..a7369b8 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "private": true, "version": "1.0.0", "type": "module", + "workspaces": [ + "packages/*" + ], "scripts": { "dev": "vite", "dev:convex": "convex dev", diff --git a/packages/create-markdown-sync/README.md b/packages/create-markdown-sync/README.md new file mode 100644 index 0000000..b1ed168 --- /dev/null +++ b/packages/create-markdown-sync/README.md @@ -0,0 +1,66 @@ +# create-markdown-sync + +Create a markdown-sync site with a single command. + +## Quick Start + +```bash +npx create-markdown-sync my-site +``` + +This interactive CLI will: + +1. Clone the markdown-sync framework +2. Walk through configuration (site name, URL, features, etc.) +3. Install dependencies +4. Set up Convex backend +5. Run initial content sync +6. Open your site in the browser + +## Usage + +```bash +# Create a new project +npx create-markdown-sync my-blog + +# With specific package manager +npx create-markdown-sync my-blog --pm yarn + +# Skip Convex setup (configure later) +npx create-markdown-sync my-blog --skip-convex +``` + +## What You Get + +A fully configured markdown-sync site with: + +- Real-time content sync via Convex +- Markdown-based blog posts and pages +- Full-text and semantic search +- RSS feeds and sitemap +- AI integrations (Claude, GPT-4, Gemini) +- Newsletter subscriptions (via AgentMail) +- MCP server for AI tool integration +- Dashboard for content management +- Deploy-ready for Netlify + +## Requirements + +- Node.js 18 or higher +- npm, yarn, pnpm, or bun + +## After Setup + +```bash +cd my-site +npm run dev # Start dev server at localhost:5173 +npm run sync # Sync content changes to Convex +``` + +## Documentation + +Full documentation at [markdown.fast/docs](https://www.markdown.fast/docs) + +## License + +MIT diff --git a/packages/create-markdown-sync/package.json b/packages/create-markdown-sync/package.json new file mode 100644 index 0000000..6ad45fb --- /dev/null +++ b/packages/create-markdown-sync/package.json @@ -0,0 +1,54 @@ +{ + "name": "create-markdown-sync", + "version": "0.1.0", + "description": "Create a markdown-sync site with a single command", + "main": "dist/index.js", + "bin": { + "create-markdown-sync": "dist/index.js" + }, + "type": "module", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "start": "node dist/index.js" + }, + "keywords": [ + "markdown", + "blog", + "convex", + "cli", + "scaffolding", + "create", + "static-site", + "cms" + ], + "author": "Wayne Sutton", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/waynesutton/markdown-site" + }, + "bugs": { + "url": "https://github.com/waynesutton/markdown-site/issues" + }, + "homepage": "https://github.com/waynesutton/markdown-site#readme", + "engines": { + "node": ">=18" + }, + "dependencies": { + "execa": "^8.0.1", + "giget": "^1.2.3", + "kleur": "^4.1.5", + "open": "^10.1.0", + "ora": "^8.0.1", + "prompts": "^2.4.2" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/prompts": "^2.4.9", + "typescript": "^5.2.2" + } +} diff --git a/packages/create-markdown-sync/src/clone.ts b/packages/create-markdown-sync/src/clone.ts new file mode 100644 index 0000000..74ab558 --- /dev/null +++ b/packages/create-markdown-sync/src/clone.ts @@ -0,0 +1,76 @@ +import { downloadTemplate } from 'giget'; +import { existsSync } from 'fs'; +import { resolve } from 'path'; +import ora from 'ora'; +import { log } from './utils.js'; + +const TEMPLATE_REPO = 'github:waynesutton/markdown-site'; + +export interface CloneOptions { + projectName: string; + cwd?: string; + force?: boolean; +} + +export async function cloneTemplate(options: CloneOptions): Promise { + const { projectName, cwd = process.cwd(), force = false } = options; + const targetDir = resolve(cwd, projectName); + + // Check if directory already exists + if (existsSync(targetDir) && !force) { + throw new Error( + `Directory "${projectName}" already exists. Use --force to overwrite.` + ); + } + + const spinner = ora('Cloning markdown-sync template...').start(); + + try { + await downloadTemplate(TEMPLATE_REPO, { + dir: targetDir, + force, + preferOffline: false, + }); + + spinner.succeed('Template cloned successfully'); + return targetDir; + } catch (error) { + spinner.fail('Failed to clone template'); + + if (error instanceof Error) { + if (error.message.includes('404')) { + log.error('Template repository not found. Please check the repository URL.'); + } else if (error.message.includes('network')) { + log.error('Network error. Please check your internet connection.'); + } else { + log.error(error.message); + } + } + + throw error; + } +} + +// Remove files that shouldn't be in the cloned project +export async function cleanupClonedFiles(projectDir: string): Promise { + const { rm } = await import('fs/promises'); + const { join } = await import('path'); + + const filesToRemove = [ + // Remove CLI package from cloned repo (it's installed via npm) + 'packages', + // Remove any existing fork-config.json (will be regenerated) + 'fork-config.json', + // Remove git history for fresh start + '.git', + ]; + + for (const file of filesToRemove) { + const filePath = join(projectDir, file); + try { + await rm(filePath, { recursive: true, force: true }); + } catch { + // Ignore if file doesn't exist + } + } +} diff --git a/packages/create-markdown-sync/src/configure.ts b/packages/create-markdown-sync/src/configure.ts new file mode 100644 index 0000000..fd80b91 --- /dev/null +++ b/packages/create-markdown-sync/src/configure.ts @@ -0,0 +1,302 @@ +import { readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { execa } from 'execa'; +import ora from 'ora'; +import type { WizardAnswers } from './wizard.js'; +import { log, extractDomain } from './utils.js'; + +// Fix template siteConfig.ts to have clean values before running configure-fork.ts +// This is needed because the template may have values with embedded quotes that break regex +function fixTemplateSiteConfig(projectDir: string): void { + const siteConfigPath = join(projectDir, 'src/config/siteConfig.ts'); + let content = readFileSync(siteConfigPath, 'utf-8'); + + // Fix name field - replace any problematic value with a clean placeholder + // Match: name: 'anything' or name: "anything" (properly handling escaped quotes) + content = content.replace( + /name: ['"].*?["'](?:.*?['"])?,/, + 'name: "markdown-sync",' + ); + + // Also try a more aggressive fix for malformed values + content = content.replace( + /name: '.*?framework',/, + 'name: "markdown-sync",' + ); + + writeFileSync(siteConfigPath, content, 'utf-8'); +} + +// Create empty auth config when user doesn't need authentication +// This prevents WorkOS env var errors from blocking Convex setup +function disableAuthConfig(projectDir: string): void { + const authConfigPath = join(projectDir, 'convex/auth.config.ts'); + + // Replace with empty auth config (no providers) + const emptyAuthConfig = `// Auth configuration (WorkOS disabled) +// To enable WorkOS authentication, see: https://docs.convex.dev/auth/authkit/ +// +// 1. Create a WorkOS account at https://workos.com +// 2. Set WORKOS_CLIENT_ID in your Convex dashboard environment variables +// 3. Replace this file with the WorkOS auth config + +const authConfig = { + providers: [], +}; + +export default authConfig; +`; + + writeFileSync(authConfigPath, emptyAuthConfig, 'utf-8'); +} + +// Convert wizard answers to fork-config.json structure +function buildForkConfig(answers: WizardAnswers): Record { + return { + siteName: answers.siteName, + siteTitle: answers.siteTitle, + siteDescription: answers.siteDescription, + siteUrl: answers.siteUrl, + siteDomain: extractDomain(answers.siteUrl), + githubUsername: answers.githubUsername, + githubRepo: answers.githubRepo, + contactEmail: answers.contactEmail, + creator: { + name: answers.creatorName, + twitter: answers.twitter, + linkedin: answers.linkedin, + github: answers.github, + }, + bio: answers.bio, + gitHubRepoConfig: { + owner: answers.githubUsername, + repo: answers.githubRepo, + branch: answers.branch, + contentPath: answers.contentPath, + }, + logoGallery: { + enabled: answers.logoGalleryEnabled, + title: 'Built with', + scrolling: answers.logoGalleryScrolling, + maxItems: 4, + }, + gitHubContributions: { + enabled: answers.githubContributionsEnabled, + username: answers.githubContributionsUsername, + showYearNavigation: true, + linkToProfile: true, + title: 'GitHub Activity', + }, + visitorMap: { + enabled: answers.visitorMapEnabled, + title: 'Live Visitors', + }, + blogPage: { + enabled: answers.blogPageEnabled, + showInNav: true, + title: answers.blogPageTitle, + description: 'All posts from the blog, sorted by date.', + order: 2, + }, + postsDisplay: { + showOnHome: answers.showPostsOnHome, + showOnBlogPage: true, + homePostsLimit: answers.homePostsLimit || undefined, + homePostsReadMore: answers.homePostsReadMoreEnabled + ? { + enabled: true, + text: answers.homePostsReadMoreText, + link: answers.homePostsReadMoreLink, + } + : { + enabled: false, + text: 'Read more blog posts', + link: '/blog', + }, + }, + featuredViewMode: answers.featuredViewMode, + showViewToggle: answers.showViewToggle, + theme: answers.theme, + fontFamily: answers.fontFamily, + homepage: { + type: answers.homepageType, + slug: null, + originalHomeRoute: '/home', + }, + rightSidebar: { + enabled: true, + minWidth: 1135, + }, + footer: { + enabled: answers.footerEnabled, + showOnHomepage: true, + showOnPosts: true, + showOnPages: true, + showOnBlogPage: true, + defaultContent: answers.footerDefaultContent, + }, + socialFooter: { + enabled: answers.socialFooterEnabled, + showOnHomepage: true, + showOnPosts: true, + showOnPages: true, + showOnBlogPage: true, + showInHeader: answers.socialFooterShowInHeader, + socialLinks: [ + { + platform: 'github', + url: `https://github.com/${answers.githubUsername}/${answers.githubRepo}`, + }, + { + platform: 'twitter', + url: answers.twitter, + }, + { + platform: 'linkedin', + url: answers.linkedin, + }, + ], + copyright: { + siteName: answers.copyrightSiteName, + showYear: true, + }, + }, + aiChat: { + enabledOnWritePage: false, + enabledOnContent: false, + }, + aiDashboard: { + enableImageGeneration: true, + defaultTextModel: 'claude-sonnet-4-20250514', + textModels: [ + { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: 'anthropic' }, + { id: 'gpt-4o', name: 'GPT-4o', provider: 'openai' }, + { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', provider: 'google' }, + ], + imageModels: [ + { id: 'gemini-2.0-flash-exp-image-generation', name: 'Nano Banana', provider: 'google' }, + { id: 'imagen-3.0-generate-002', name: 'Nano Banana Pro', provider: 'google' }, + ], + }, + newsletter: { + enabled: answers.newsletterEnabled, + agentmail: { + inbox: 'newsletter@mail.agentmail.to', + }, + signup: { + home: { + enabled: answers.newsletterHomeEnabled, + position: 'above-footer', + title: 'Stay Updated', + description: 'Get new posts delivered to your inbox.', + }, + blogPage: { + enabled: answers.newsletterEnabled, + position: 'above-footer', + title: 'Subscribe', + description: 'Get notified when new posts are published.', + }, + posts: { + enabled: answers.newsletterEnabled, + position: 'below-content', + title: 'Enjoyed this post?', + description: 'Subscribe for more updates.', + }, + }, + }, + contactForm: { + enabled: answers.contactFormEnabled, + title: answers.contactFormTitle, + description: 'Send us a message and we\'ll get back to you.', + }, + newsletterAdmin: { + enabled: false, + showInNav: false, + }, + newsletterNotifications: { + enabled: false, + newSubscriberAlert: false, + weeklyStatsSummary: false, + }, + weeklyDigest: { + enabled: false, + dayOfWeek: 0, + subject: 'Weekly Digest', + }, + statsPage: { + enabled: answers.statsPageEnabled, + showInNav: answers.statsPageEnabled, + }, + mcpServer: { + enabled: answers.mcpServerEnabled, + endpoint: '/mcp', + publicRateLimit: 50, + authenticatedRateLimit: 1000, + requireAuth: false, + }, + imageLightbox: { + enabled: answers.imageLightboxEnabled, + }, + dashboard: { + enabled: answers.dashboardEnabled, + requireAuth: answers.dashboardRequireAuth, + }, + semanticSearch: { + enabled: answers.semanticSearchEnabled, + }, + twitter: { + site: answers.twitterSite, + creator: answers.twitterCreator, + }, + askAI: { + enabled: answers.askAIEnabled, + defaultModel: 'claude-sonnet-4-20250514', + models: [ + { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: 'anthropic' }, + { id: 'gpt-4o', name: 'GPT-4o', provider: 'openai' }, + ], + }, + }; +} + +export async function configureProject( + projectDir: string, + answers: WizardAnswers +): Promise { + const spinner = ora('Generating configuration...').start(); + + try { + // 1. Fix template siteConfig.ts to have clean values (fixes embedded quote issues) + fixTemplateSiteConfig(projectDir); + + // 2. Disable auth config if user doesn't need authentication + // This prevents WorkOS env var errors from blocking Convex setup + if (!answers.dashboardRequireAuth) { + disableAuthConfig(projectDir); + } + + // 3. Build fork-config.json content + const forkConfig = buildForkConfig(answers); + + // 4. Write fork-config.json + const configPath = join(projectDir, 'fork-config.json'); + writeFileSync(configPath, JSON.stringify(forkConfig, null, 2)); + spinner.text = 'Running configuration script...'; + + // 5. Run existing configure-fork.ts script with --silent flag + await execa('npx', ['tsx', 'scripts/configure-fork.ts', '--silent'], { + cwd: projectDir, + stdio: 'pipe', // Capture output + }); + + spinner.succeed('Site configured successfully'); + } catch (error) { + spinner.fail('Configuration failed'); + + if (error instanceof Error) { + log.error(error.message); + } + + throw error; + } +} diff --git a/packages/create-markdown-sync/src/convex-setup.ts b/packages/create-markdown-sync/src/convex-setup.ts new file mode 100644 index 0000000..7afba72 --- /dev/null +++ b/packages/create-markdown-sync/src/convex-setup.ts @@ -0,0 +1,107 @@ +import { execa } from 'execa'; +import ora from 'ora'; +import { log } from './utils.js'; + +export async function setupConvex( + projectDir: string, + projectName: string +): Promise { + const spinner = ora('Setting up Convex...').start(); + + try { + // Check if user is logged in to Convex + spinner.text = 'Checking Convex authentication...'; + + const { stdout: whoami } = await execa('npx', ['convex', 'whoami'], { + cwd: projectDir, + reject: false, + }); + + // If not logged in, prompt for login + if (!whoami || whoami.includes('Not logged in')) { + spinner.text = 'Opening browser for Convex login...'; + + await execa('npx', ['convex', 'login'], { + cwd: projectDir, + stdio: 'inherit', // Show login flow + }); + } + + // Initialize Convex project + // Stop spinner to allow interactive prompts from Convex CLI + spinner.stop(); + console.log(''); + log.step('Initializing Convex project...'); + console.log(''); + + // Use convex dev --once to set up project without running in watch mode + // This creates .env.local with CONVEX_URL + // stdio: 'inherit' allows user to respond to Convex prompts (new project vs existing) + await execa('npx', ['convex', 'dev', '--once'], { + cwd: projectDir, + stdio: 'inherit', + env: { + ...process.env, + // Set project name if needed + CONVEX_PROJECT_NAME: projectName, + }, + }); + + console.log(''); + log.success('Convex project initialized'); + return true; + } catch (error) { + // Spinner may already be stopped, so use log.error instead + spinner.stop(); + + // Check if .env.local was created despite the error + // This happens when Convex project is created but auth config has missing env vars + const fs = await import('fs'); + const path = await import('path'); + const envLocalPath = path.join(projectDir, '.env.local'); + + if (fs.existsSync(envLocalPath)) { + const envContent = fs.readFileSync(envLocalPath, 'utf-8'); + if (envContent.includes('CONVEX_DEPLOYMENT') || envContent.includes('VITE_CONVEX_URL')) { + // Convex was set up, just auth config had issues (missing WORKOS_CLIENT_ID etc) + console.log(''); + log.success('Convex project created'); + log.warn('Auth config has missing environment variables (optional)'); + log.info('Set them up later in the Convex dashboard if you want authentication'); + return true; + } + } + + log.error('Convex setup failed'); + + if (error instanceof Error) { + if (error.message.includes('ENOENT')) { + log.error('Convex CLI not found. Install with: npm install -g convex'); + } else { + log.error(error.message); + } + } + + log.warn('You can set up Convex later with: npx convex dev'); + return false; + } +} + +export async function deployConvexFunctions(projectDir: string): Promise { + const spinner = ora('Deploying Convex functions...').start(); + + try { + await execa('npx', ['convex', 'deploy'], { + cwd: projectDir, + stdio: 'pipe', + }); + + spinner.succeed('Convex functions deployed'); + } catch (error) { + spinner.fail('Convex deployment failed'); + + if (error instanceof Error) { + log.warn('You can deploy later with: npx convex deploy'); + } + } +} diff --git a/packages/create-markdown-sync/src/index.ts b/packages/create-markdown-sync/src/index.ts new file mode 100644 index 0000000..c35c53b --- /dev/null +++ b/packages/create-markdown-sync/src/index.ts @@ -0,0 +1,117 @@ +#!/usr/bin/env node + +import { printBanner, printSuccess, log } from './utils.js'; +import { runWizard } from './wizard.js'; +import { cloneTemplate, cleanupClonedFiles } from './clone.js'; +import { configureProject } from './configure.js'; +import { installDependencies, runInitialSync, startDevServer } from './install.js'; +import { setupConvex } from './convex-setup.js'; + +const VERSION = '0.1.0'; + +async function main(): Promise { + // Parse command line arguments + const args = process.argv.slice(2); + const projectName = args.find(arg => !arg.startsWith('-')); + const force = args.includes('--force') || args.includes('-f'); + const skipConvex = args.includes('--skip-convex'); + const skipOpen = args.includes('--skip-open'); + const showHelp = args.includes('--help') || args.includes('-h'); + const showVersion = args.includes('--version') || args.includes('-v'); + + // Handle --version + if (showVersion) { + console.log(`create-markdown-sync v${VERSION}`); + process.exit(0); + } + + // Handle --help + if (showHelp) { + console.log(` +create-markdown-sync v${VERSION} + +Create a markdown-sync site with a single command. + +Usage: + npx create-markdown-sync [project-name] [options] + +Options: + -f, --force Overwrite existing directory + --skip-convex Skip Convex setup + --skip-open Don't open browser after setup + -h, --help Show this help message + -v, --version Show version number + +Examples: + npx create-markdown-sync my-blog + npx create-markdown-sync my-site --force + npx create-markdown-sync my-app --skip-convex + +Documentation: https://www.markdown.fast/docs +`); + process.exit(0); + } + + // Print welcome banner + printBanner(VERSION); + + try { + // Run interactive wizard + const answers = await runWizard(projectName); + + if (!answers) { + log.error('Setup cancelled'); + process.exit(1); + } + + console.log(''); + log.step(`Creating project in ./${answers.projectName}`); + console.log(''); + + // Step 1: Clone template + const projectDir = await cloneTemplate({ + projectName: answers.projectName, + force, + }); + + // Step 2: Clean up cloned files + await cleanupClonedFiles(projectDir); + + // Step 3: Configure project + await configureProject(projectDir, answers); + + // Step 4: Install dependencies + await installDependencies(projectDir, answers.packageManager); + + // Step 5: Setup Convex (if not skipped) + let convexSetup = false; + if (answers.initConvex && !skipConvex) { + convexSetup = await setupConvex(projectDir, answers.convexProjectName); + } + + // Step 6: Run initial sync (only if Convex is set up) + if (convexSetup) { + await runInitialSync(projectDir, answers.packageManager); + } + + // Step 7: Start dev server and open browser + if (!skipOpen) { + await startDevServer(projectDir, answers.packageManager); + } + + // Print success message + printSuccess(answers.projectName); + } catch (error) { + console.log(''); + + if (error instanceof Error) { + log.error(error.message); + } else { + log.error('An unexpected error occurred'); + } + + process.exit(1); + } +} + +main(); diff --git a/packages/create-markdown-sync/src/install.ts b/packages/create-markdown-sync/src/install.ts new file mode 100644 index 0000000..88e8f99 --- /dev/null +++ b/packages/create-markdown-sync/src/install.ts @@ -0,0 +1,92 @@ +import { execa } from 'execa'; +import ora from 'ora'; +import open from 'open'; +import { log, sleep, getInstallCommand, getRunCommand } from './utils.js'; + +export async function installDependencies( + projectDir: string, + packageManager: string +): Promise { + const spinner = ora('Installing dependencies...').start(); + + try { + const [cmd, ...args] = getInstallCommand(packageManager); + + await execa(cmd, args, { + cwd: projectDir, + stdio: 'pipe', // Suppress output + }); + + spinner.succeed('Dependencies installed'); + } catch (error) { + spinner.fail('Failed to install dependencies'); + + if (error instanceof Error) { + log.error(error.message); + } + + throw error; + } +} + +export async function runInitialSync( + projectDir: string, + packageManager: string +): Promise { + const spinner = ora('Running initial content sync...').start(); + + try { + const [cmd, ...args] = getRunCommand(packageManager, 'sync'); + + await execa(cmd, args, { + cwd: projectDir, + stdio: 'pipe', + }); + + spinner.succeed('Initial sync completed'); + } catch { + // Sync often fails on first run because Convex functions need to be deployed first + // This is expected and not an error - just inform the user what to do + spinner.stop(); + log.warn('Sync requires Convex functions to be deployed first'); + log.info('After setup completes, run: npx convex dev'); + log.info('Then in another terminal: npm run sync'); + } +} + +export async function startDevServer( + projectDir: string, + packageManager: string +): Promise { + const spinner = ora('Starting development server...').start(); + + try { + const [cmd, ...args] = getRunCommand(packageManager, 'dev'); + + // Start dev server in detached mode (won't block) + const devProcess = execa(cmd, args, { + cwd: projectDir, + detached: true, + stdio: 'ignore', + }); + + // Unref to allow parent process to exit + devProcess.unref(); + + spinner.succeed('Development server starting'); + + // Wait for server to be ready + log.info('Waiting for server to start...'); + await sleep(3000); + + // Open browser + await open('http://localhost:5173'); + log.success('Opened browser at http://localhost:5173'); + } catch (error) { + spinner.fail('Failed to start development server'); + + if (error instanceof Error) { + log.warn('You can start the server manually with: npm run dev'); + } + } +} diff --git a/packages/create-markdown-sync/src/utils.ts b/packages/create-markdown-sync/src/utils.ts new file mode 100644 index 0000000..85e7c69 --- /dev/null +++ b/packages/create-markdown-sync/src/utils.ts @@ -0,0 +1,133 @@ +import kleur from 'kleur'; + +// Sleep helper for async delays +export function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +// Validate email format +export function isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} + +// Validate URL format +export function isValidUrl(url: string): boolean { + try { + new URL(url); + return true; + } catch { + return false; + } +} + +// Extract domain from URL +export function extractDomain(url: string): string { + try { + const parsed = new URL(url); + return parsed.hostname; + } catch { + return ''; + } +} + +// Validate GitHub username (alphanumeric and hyphens) +export function isValidGitHubUsername(username: string): boolean { + const usernameRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/; + return usernameRegex.test(username); +} + +// Extract GitHub username from URL +export function extractGitHubUsername(url: string): string { + const match = url.match(/github\.com\/([^\/]+)/); + return match ? match[1] : ''; +} + +// Extract Twitter handle from URL +export function extractTwitterHandle(url: string): string { + const match = url.match(/(?:twitter\.com|x\.com)\/([^\/]+)/); + return match ? `@${match[1]}` : ''; +} + +// Logging helpers with colors +export const log = { + info: (msg: string) => console.log(kleur.blue('i') + ' ' + msg), + success: (msg: string) => console.log(kleur.green('✓') + ' ' + msg), + warn: (msg: string) => console.log(kleur.yellow('!') + ' ' + msg), + error: (msg: string) => console.log(kleur.red('✗') + ' ' + msg), + step: (msg: string) => console.log(kleur.cyan('→') + ' ' + msg), + dim: (msg: string) => console.log(kleur.dim(msg)), +}; + +// Print a section header +export function printSection(title: string, current: number, total: number): void { + console.log(''); + console.log(kleur.bold().cyan(`[${current}/${total}] ${title}`)); + console.log(kleur.dim('─'.repeat(40))); +} + +// Print welcome banner +export function printBanner(version: string): void { + console.log(''); + console.log(kleur.bold().cyan('create-markdown-sync') + kleur.dim(` v${version}`)); + console.log(''); +} + +// Print success message with next steps +export function printSuccess(projectName: string): void { + console.log(''); + console.log(kleur.bold().green('Success!') + ' Your site is ready.'); + console.log(''); + console.log('Next steps:'); + console.log(kleur.cyan(` cd ${projectName}`)); + console.log(kleur.cyan(' npx convex dev') + kleur.dim(' # Start Convex (required first time)')); + console.log(kleur.cyan(' npm run sync') + kleur.dim(' # Sync content (in another terminal)')); + console.log(kleur.cyan(' npm run dev') + kleur.dim(' # Start dev server')); + console.log(''); + console.log('Resources:'); + console.log(kleur.dim(' Docs: https://www.markdown.fast/docs')); + console.log(kleur.dim(' Deployment: https://www.markdown.fast/docs-deployment')); + console.log(kleur.dim(' WorkOS: https://www.markdown.fast/how-to-setup-workos')); + console.log(''); + console.log(kleur.dim('To remove and start over:')); + console.log(kleur.dim(` rm -rf ${projectName}`)); + console.log(kleur.dim(` npx create-markdown-sync ${projectName}`)); + console.log(''); +} + +// Detect available package manager +export function detectPackageManager(): 'npm' | 'yarn' | 'pnpm' | 'bun' { + const userAgent = process.env.npm_config_user_agent || ''; + if (userAgent.includes('yarn')) return 'yarn'; + if (userAgent.includes('pnpm')) return 'pnpm'; + if (userAgent.includes('bun')) return 'bun'; + return 'npm'; +} + +// Get install command for package manager +export function getInstallCommand(pm: string): string[] { + switch (pm) { + case 'yarn': + return ['yarn']; + case 'pnpm': + return ['pnpm', 'install']; + case 'bun': + return ['bun', 'install']; + default: + return ['npm', 'install']; + } +} + +// Get run command for package manager +export function getRunCommand(pm: string, script: string): string[] { + switch (pm) { + case 'yarn': + return ['yarn', script]; + case 'pnpm': + return ['pnpm', script]; + case 'bun': + return ['bun', 'run', script]; + default: + return ['npm', 'run', script]; + } +} diff --git a/packages/create-markdown-sync/src/wizard.ts b/packages/create-markdown-sync/src/wizard.ts new file mode 100644 index 0000000..6f4cb7d --- /dev/null +++ b/packages/create-markdown-sync/src/wizard.ts @@ -0,0 +1,604 @@ +import prompts from 'prompts'; +import { + printSection, + isValidEmail, + isValidUrl, + extractDomain, + extractGitHubUsername, + extractTwitterHandle, + detectPackageManager, +} from './utils.js'; + +// Wizard answers interface matching fork-config.json structure +export interface WizardAnswers { + // Section 1: Project Setup + projectName: string; + packageManager: 'npm' | 'yarn' | 'pnpm' | 'bun'; + + // Section 2: Site Identity + siteName: string; + siteTitle: string; + siteDescription: string; + siteUrl: string; + contactEmail: string; + + // Section 3: Creator Info + creatorName: string; + twitter: string; + linkedin: string; + github: string; + + // Section 4: GitHub Repository + githubUsername: string; + githubRepo: string; + branch: string; + contentPath: string; + + // Section 5: Appearance + theme: 'dark' | 'light' | 'tan' | 'cloud'; + fontFamily: 'serif' | 'sans' | 'monospace'; + bio: string; + + // Section 6: Homepage & Featured + homepageType: 'default' | 'page' | 'post'; + featuredViewMode: 'cards' | 'list'; + featuredTitle: string; + showViewToggle: boolean; + + // Section 7: Blog & Posts + blogPageEnabled: boolean; + blogPageTitle: string; + showPostsOnHome: boolean; + homePostsLimit: number; + homePostsReadMoreEnabled: boolean; + homePostsReadMoreText: string; + homePostsReadMoreLink: string; + + // Section 8: Features + logoGalleryEnabled: boolean; + logoGalleryScrolling: boolean; + githubContributionsEnabled: boolean; + githubContributionsUsername: string; + visitorMapEnabled: boolean; + statsPageEnabled: boolean; + imageLightboxEnabled: boolean; + + // Section 9: Footer & Social + footerEnabled: boolean; + footerDefaultContent: string; + socialFooterEnabled: boolean; + socialFooterShowInHeader: boolean; + copyrightSiteName: string; + + // Section 10: Newsletter & Contact + newsletterEnabled: boolean; + newsletterHomeEnabled: boolean; + contactFormEnabled: boolean; + contactFormTitle: string; + + // Section 11: Advanced Features + mcpServerEnabled: boolean; + semanticSearchEnabled: boolean; + askAIEnabled: boolean; + dashboardEnabled: boolean; + dashboardRequireAuth: boolean; + + // Section 12: Twitter/X Config + twitterSite: string; + twitterCreator: string; + + // Section 13: Convex Setup + initConvex: boolean; + convexProjectName: string; +} + +const TOTAL_SECTIONS = 13; + +export async function runWizard(initialProjectName?: string): Promise { + const answers: Partial = {}; + + // Handle cancellation + const onCancel = () => { + console.log('\nSetup cancelled.'); + process.exit(0); + }; + + // SECTION 1: Project Setup + printSection('Project Setup', 1, TOTAL_SECTIONS); + + const section1 = await prompts([ + { + type: 'text', + name: 'projectName', + message: 'Project name (directory)', + initial: initialProjectName || 'my-markdown-site', + validate: (value: string) => { + if (!value.trim()) return 'Project name is required'; + if (!/^[a-zA-Z0-9-_]+$/.test(value)) return 'Only letters, numbers, hyphens, and underscores'; + return true; + }, + }, + { + type: 'select', + name: 'packageManager', + message: 'Package manager', + choices: [ + { title: 'npm', value: 'npm' }, + { title: 'yarn', value: 'yarn' }, + { title: 'pnpm', value: 'pnpm' }, + { title: 'bun', value: 'bun' }, + ], + initial: ['npm', 'yarn', 'pnpm', 'bun'].indexOf(detectPackageManager()), + }, + ], { onCancel }); + + Object.assign(answers, section1); + + // SECTION 2: Site Identity + printSection('Site Identity', 2, TOTAL_SECTIONS); + + const section2 = await prompts([ + { + type: 'text', + name: 'siteName', + message: 'Site name', + initial: 'My Site', + }, + { + type: 'text', + name: 'siteTitle', + message: 'Tagline', + initial: 'A markdown-powered site', + }, + { + type: 'text', + name: 'siteDescription', + message: 'Description (one sentence)', + initial: 'A site built with markdown-sync framework.', + }, + { + type: 'text', + name: 'siteUrl', + message: 'Site URL', + initial: `https://${answers.projectName}.netlify.app`, + validate: (value: string) => isValidUrl(value) || 'Enter a valid URL', + }, + { + type: 'text', + name: 'contactEmail', + message: 'Contact email', + validate: (value: string) => !value || isValidEmail(value) || 'Enter a valid email', + }, + ], { onCancel }); + + Object.assign(answers, section2); + + // SECTION 3: Creator Info + printSection('Creator Info', 3, TOTAL_SECTIONS); + + const section3 = await prompts([ + { + type: 'text', + name: 'creatorName', + message: 'Your name', + }, + { + type: 'text', + name: 'twitter', + message: 'Twitter/X URL', + initial: 'https://x.com/', + }, + { + type: 'text', + name: 'linkedin', + message: 'LinkedIn URL', + initial: 'https://linkedin.com/in/', + }, + { + type: 'text', + name: 'github', + message: 'GitHub URL', + initial: 'https://github.com/', + }, + ], { onCancel }); + + Object.assign(answers, section3); + + // SECTION 4: GitHub Repository + printSection('GitHub Repository', 4, TOTAL_SECTIONS); + + const section4 = await prompts([ + { + type: 'text', + name: 'githubUsername', + message: 'GitHub username', + initial: extractGitHubUsername(answers.github || ''), + }, + { + type: 'text', + name: 'githubRepo', + message: 'Repository name', + initial: answers.projectName, + }, + { + type: 'text', + name: 'branch', + message: 'Default branch', + initial: 'main', + }, + { + type: 'text', + name: 'contentPath', + message: 'Content path', + initial: 'public/raw', + }, + ], { onCancel }); + + Object.assign(answers, section4); + + // SECTION 5: Appearance + printSection('Appearance', 5, TOTAL_SECTIONS); + + const section5 = await prompts([ + { + type: 'select', + name: 'theme', + message: 'Default theme', + choices: [ + { title: 'Tan (warm)', value: 'tan' }, + { title: 'Light', value: 'light' }, + { title: 'Dark', value: 'dark' }, + { title: 'Cloud (blue-gray)', value: 'cloud' }, + ], + initial: 0, + }, + { + type: 'select', + name: 'fontFamily', + message: 'Font family', + choices: [ + { title: 'Sans-serif (system fonts)', value: 'sans' }, + { title: 'Serif (New York)', value: 'serif' }, + { title: 'Monospace (IBM Plex Mono)', value: 'monospace' }, + ], + initial: 0, + }, + { + type: 'text', + name: 'bio', + message: 'Bio text', + initial: 'Your content is instantly available to browsers, LLMs, and AI agents.', + }, + ], { onCancel }); + + Object.assign(answers, section5); + + // SECTION 6: Homepage & Featured + printSection('Homepage & Featured', 6, TOTAL_SECTIONS); + + const section6 = await prompts([ + { + type: 'select', + name: 'homepageType', + message: 'Homepage type', + choices: [ + { title: 'Default (standard homepage)', value: 'default' }, + { title: 'Page (use a static page)', value: 'page' }, + { title: 'Post (use a blog post)', value: 'post' }, + ], + initial: 0, + }, + { + type: 'select', + name: 'featuredViewMode', + message: 'Featured section view', + choices: [ + { title: 'Cards (grid with excerpts)', value: 'cards' }, + { title: 'List (bullet list)', value: 'list' }, + ], + initial: 0, + }, + { + type: 'text', + name: 'featuredTitle', + message: 'Featured section title', + initial: 'Get started:', + }, + { + type: 'confirm', + name: 'showViewToggle', + message: 'Show view toggle button?', + initial: true, + }, + ], { onCancel }); + + Object.assign(answers, section6); + + // SECTION 7: Blog & Posts + printSection('Blog & Posts', 7, TOTAL_SECTIONS); + + const section7 = await prompts([ + { + type: 'confirm', + name: 'blogPageEnabled', + message: 'Enable dedicated /blog page?', + initial: true, + }, + { + type: (prev: boolean) => prev ? 'text' : null, + name: 'blogPageTitle', + message: 'Blog page title', + initial: 'Blog', + }, + { + type: 'confirm', + name: 'showPostsOnHome', + message: 'Show posts on homepage?', + initial: true, + }, + { + type: (prev: boolean) => prev ? 'number' : null, + name: 'homePostsLimit', + message: 'Posts limit on homepage (0 for all)', + initial: 5, + min: 0, + max: 100, + }, + { + type: (prev: number, values: { showPostsOnHome: boolean }) => + values.showPostsOnHome && prev > 0 ? 'confirm' : null, + name: 'homePostsReadMoreEnabled', + message: 'Show "read more" link?', + initial: true, + }, + { + type: (prev: boolean) => prev ? 'text' : null, + name: 'homePostsReadMoreText', + message: 'Read more text', + initial: 'Read more blog posts', + }, + { + type: (prev: string) => prev ? 'text' : null, + name: 'homePostsReadMoreLink', + message: 'Read more link URL', + initial: '/blog', + }, + ], { onCancel }); + + // Set defaults for skipped questions (spread first, then defaults) + Object.assign(answers, { + ...section7, + blogPageTitle: section7.blogPageTitle || 'Blog', + homePostsLimit: section7.homePostsLimit ?? 5, + homePostsReadMoreEnabled: section7.homePostsReadMoreEnabled ?? true, + homePostsReadMoreText: section7.homePostsReadMoreText || 'Read more blog posts', + homePostsReadMoreLink: section7.homePostsReadMoreLink || '/blog', + }); + + // SECTION 8: Features + printSection('Features', 8, TOTAL_SECTIONS); + + const section8 = await prompts([ + { + type: 'confirm', + name: 'logoGalleryEnabled', + message: 'Enable logo gallery?', + initial: true, + }, + { + type: (prev: boolean) => prev ? 'confirm' : null, + name: 'logoGalleryScrolling', + message: 'Scrolling marquee?', + initial: false, + }, + { + type: 'confirm', + name: 'githubContributionsEnabled', + message: 'Enable GitHub contributions graph?', + initial: true, + }, + { + type: (prev: boolean) => prev ? 'text' : null, + name: 'githubContributionsUsername', + message: 'GitHub username for contributions', + initial: answers.githubUsername, + }, + { + type: 'confirm', + name: 'visitorMapEnabled', + message: 'Enable visitor map on stats page?', + initial: false, + }, + { + type: 'confirm', + name: 'statsPageEnabled', + message: 'Enable public stats page?', + initial: true, + }, + { + type: 'confirm', + name: 'imageLightboxEnabled', + message: 'Enable image lightbox (click to magnify)?', + initial: true, + }, + ], { onCancel }); + + Object.assign(answers, { + ...section8, + logoGalleryScrolling: section8.logoGalleryScrolling ?? false, + githubContributionsUsername: section8.githubContributionsUsername || answers.githubUsername, + }); + + // SECTION 9: Footer & Social + printSection('Footer & Social', 9, TOTAL_SECTIONS); + + const section9 = await prompts([ + { + type: 'confirm', + name: 'footerEnabled', + message: 'Enable footer?', + initial: true, + }, + { + type: (prev: boolean) => prev ? 'text' : null, + name: 'footerDefaultContent', + message: 'Footer content (markdown)', + initial: 'Built with [Convex](https://convex.dev) for real-time sync.', + }, + { + type: 'confirm', + name: 'socialFooterEnabled', + message: 'Enable social footer (icons + copyright)?', + initial: true, + }, + { + type: (prev: boolean) => prev ? 'confirm' : null, + name: 'socialFooterShowInHeader', + message: 'Show social icons in header?', + initial: true, + }, + { + type: (prev: boolean, values: { socialFooterEnabled: boolean }) => + values.socialFooterEnabled ? 'text' : null, + name: 'copyrightSiteName', + message: 'Copyright site name', + initial: answers.siteName, + }, + ], { onCancel }); + + Object.assign(answers, { + ...section9, + footerDefaultContent: section9.footerDefaultContent || '', + socialFooterShowInHeader: section9.socialFooterShowInHeader ?? true, + copyrightSiteName: section9.copyrightSiteName || answers.siteName, + }); + + // SECTION 10: Newsletter & Contact + printSection('Newsletter & Contact', 10, TOTAL_SECTIONS); + + const section10 = await prompts([ + { + type: 'confirm', + name: 'newsletterEnabled', + message: 'Enable newsletter signups?', + initial: false, + hint: 'Requires AgentMail setup', + }, + { + type: (prev: boolean) => prev ? 'confirm' : null, + name: 'newsletterHomeEnabled', + message: 'Show signup on homepage?', + initial: true, + }, + { + type: 'confirm', + name: 'contactFormEnabled', + message: 'Enable contact form?', + initial: false, + hint: 'Requires AgentMail setup', + }, + { + type: (prev: boolean) => prev ? 'text' : null, + name: 'contactFormTitle', + message: 'Contact form title', + initial: 'Get in Touch', + }, + ], { onCancel }); + + Object.assign(answers, { + ...section10, + newsletterHomeEnabled: section10.newsletterHomeEnabled ?? false, + contactFormTitle: section10.contactFormTitle || 'Get in Touch', + }); + + // SECTION 11: Advanced Features + printSection('Advanced Features', 11, TOTAL_SECTIONS); + + const section11 = await prompts([ + { + type: 'confirm', + name: 'mcpServerEnabled', + message: 'Enable MCP server for AI tools?', + initial: true, + }, + { + type: 'confirm', + name: 'semanticSearchEnabled', + message: 'Enable semantic search?', + initial: false, + hint: 'Requires OpenAI API key', + }, + { + type: 'confirm', + name: 'askAIEnabled', + message: 'Enable Ask AI header button?', + initial: false, + hint: 'Requires semantic search + LLM API key', + }, + { + type: 'confirm', + name: 'dashboardEnabled', + message: 'Enable admin dashboard?', + initial: true, + }, + { + type: (prev: boolean) => prev ? 'confirm' : null, + name: 'dashboardRequireAuth', + message: 'Require authentication for dashboard?', + initial: false, + hint: 'Requires WorkOS setup', + }, + ], { onCancel }); + + Object.assign(answers, { + ...section11, + dashboardRequireAuth: section11.dashboardRequireAuth ?? false, + }); + + // SECTION 12: Twitter/X Config + printSection('Twitter/X Config', 12, TOTAL_SECTIONS); + + const section12 = await prompts([ + { + type: 'text', + name: 'twitterSite', + message: 'Twitter site handle', + initial: extractTwitterHandle(answers.twitter || ''), + hint: 'For Twitter Cards', + }, + { + type: 'text', + name: 'twitterCreator', + message: 'Twitter creator handle', + initial: extractTwitterHandle(answers.twitter || ''), + }, + ], { onCancel }); + + Object.assign(answers, section12); + + // SECTION 13: Convex Setup + printSection('Convex Setup', 13, TOTAL_SECTIONS); + + const section13 = await prompts([ + { + type: 'confirm', + name: 'initConvex', + message: 'Initialize Convex project now?', + initial: true, + hint: 'Opens browser for login', + }, + { + type: (prev: boolean) => prev ? 'text' : null, + name: 'convexProjectName', + message: 'Convex project name', + initial: answers.projectName, + }, + ], { onCancel }); + + Object.assign(answers, { + ...section13, + convexProjectName: section13.convexProjectName || answers.projectName, + }); + + return answers as WizardAnswers; +} diff --git a/packages/create-markdown-sync/tsconfig.json b/packages/create-markdown-sync/tsconfig.json new file mode 100644 index 0000000..479c240 --- /dev/null +++ b/packages/create-markdown-sync/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/public/raw/about.md b/public/raw/about.md index cc4279e..2d0e0b8 100644 --- a/public/raw/about.md +++ b/public/raw/about.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs. Two ways to publish: write markdown and sync from the terminal, or use the web dashboard. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify. diff --git a/public/raw/changelog.md b/public/raw/changelog.md index d6724d8..1b05cc4 100644 --- a/public/raw/changelog.md +++ b/public/raw/changelog.md @@ -2,11 +2,93 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- All notable changes to this project. +## v2.19.0 + +Released January 10, 2026 + +**npx create-markdown-sync CLI** + +Added a CLI tool to scaffold new markdown-sync projects with a single command. Run `npx create-markdown-sync my-site` to clone the template, configure your site through an interactive wizard, install dependencies, and set up Convex. + +**Changes:** + +- Interactive wizard with 13 sections covering all configuration options +- Clones template from GitHub via giget +- Configures site settings automatically (siteConfig.ts, fork-config.json) +- Installs dependencies with your preferred package manager +- Sets up Convex project with optional WorkOS auth (disabled by default) +- Starts dev server and opens browser +- Clear next steps with links to docs, deployment guide, and WorkOS setup + +**Files changed:** + +- `packages/create-markdown-sync/` - New monorepo package +- `packages/create-markdown-sync/src/index.ts` - CLI entry point +- `packages/create-markdown-sync/src/wizard.ts` - Interactive prompts +- `packages/create-markdown-sync/src/clone.ts` - Template cloning +- `packages/create-markdown-sync/src/configure.ts` - Site configuration +- `packages/create-markdown-sync/src/install.ts` - Dependency installation +- `packages/create-markdown-sync/src/convex-setup.ts` - Convex initialization +- `packages/create-markdown-sync/src/utils.ts` - Helper utilities +- `package.json` - Added workspaces configuration +- `.gitignore` - Added packages/*/dist/ and packages/*/node_modules/ + +--- + +## v2.18.2 + +Released January 10, 2026 + +**Related posts thumbnail view with toggle** + +Added a new thumbnail view for related posts at the bottom of blog posts. Shows post image, title, description, author avatar, author name, and date. Users can toggle between the new thumbnail view and the existing list view, with their preference saved to localStorage. + +**Changes:** + +- New thumbnail view with image on left, content on right (like attached screenshot) +- Toggle button using same icons as homepage featured section +- View preference saved to localStorage +- Configuration via siteConfig.relatedPosts (defaultViewMode, showViewToggle) +- Dashboard Config section for related posts settings + +**Files changed:** + +- `convex/posts.ts` - Updated getRelatedPosts query with image, excerpt, authorName, authorImage +- `src/config/siteConfig.ts` - Added RelatedPostsConfig interface and relatedPosts config +- `src/pages/Post.tsx` - Added thumbnail view, toggle state, and view mode rendering +- `src/pages/Dashboard.tsx` - Added Related Posts config card in ConfigSection +- `src/styles/global.css` - Added thumbnail view CSS styles + +--- + +## v2.18.1 + +Released January 10, 2026 + +**README.md streamlined with docs links** + +Reduced README from 609 lines to 155 lines. Detailed documentation now lives on the live site at markdown.fast/docs. + +**Changes:** + +- Added Documentation section with link to markdown.fast/docs +- Added Guides subsection with links to Setup, Fork Configuration, Dashboard, WorkOS, MCP Server, and AgentMail guides +- Simplified Features section to brief summary with link to About page +- Simplified Fork Configuration to quick commands with doc link +- Kept sync commands, setup, and Netlify deployment sections +- Removed sections now covered by live docs + +**Files changed:** + +- `README.md` - Streamlined from 609 to 155 lines + +--- + ## v2.18.0 Released January 10, 2026 diff --git a/public/raw/contact.md b/public/raw/contact.md index db332e8..a77e632 100644 --- a/public/raw/contact.md +++ b/public/raw/contact.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- You found the contact page. Nice diff --git a/public/raw/docs-ask-ai.md b/public/raw/docs-ask-ai.md index a9b6ce9..b90ca88 100644 --- a/public/raw/docs-ask-ai.md +++ b/public/raw/docs-ask-ai.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- ## Ask AI diff --git a/public/raw/docs-configuration.md b/public/raw/docs-configuration.md index 55d72cb..5fbdf8f 100644 --- a/public/raw/docs-configuration.md +++ b/public/raw/docs-configuration.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- ## Configuration diff --git a/public/raw/docs-content.md b/public/raw/docs-content.md index 5de1cc0..deefdbc 100644 --- a/public/raw/docs-content.md +++ b/public/raw/docs-content.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- ## Content diff --git a/public/raw/docs-dashboard.md b/public/raw/docs-dashboard.md index 4802cd6..9ef4ce1 100644 --- a/public/raw/docs-dashboard.md +++ b/public/raw/docs-dashboard.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- ## Dashboard diff --git a/public/raw/docs-deployment.md b/public/raw/docs-deployment.md index c65d56a..d58b73a 100644 --- a/public/raw/docs-deployment.md +++ b/public/raw/docs-deployment.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- ## Deployment diff --git a/public/raw/docs-frontmatter.md b/public/raw/docs-frontmatter.md index eb55613..5ace343 100644 --- a/public/raw/docs-frontmatter.md +++ b/public/raw/docs-frontmatter.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- ## Frontmatter diff --git a/public/raw/docs-media-setup.md b/public/raw/docs-media-setup.md index 89ba17a..1d5df3e 100644 --- a/public/raw/docs-media-setup.md +++ b/public/raw/docs-media-setup.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- Set up image uploads for the dashboard using ConvexFS and Bunny.net CDN. diff --git a/public/raw/docs-opencode.md b/public/raw/docs-opencode.md index 853cbb7..f51dc36 100644 --- a/public/raw/docs-opencode.md +++ b/public/raw/docs-opencode.md @@ -1,5 +1,7 @@ # OpenCode Integration +> This framework includes full OpenCode support with agents, commands, skills, and plugins. + --- Type: post Date: 2026-01-10 diff --git a/public/raw/docs-search.md b/public/raw/docs-search.md index 31ba94b..f83e42e 100644 --- a/public/raw/docs-search.md +++ b/public/raw/docs-search.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- ## Keyword Search diff --git a/public/raw/docs-semantic-search.md b/public/raw/docs-semantic-search.md index 010eb53..a100025 100644 --- a/public/raw/docs-semantic-search.md +++ b/public/raw/docs-semantic-search.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- ## Semantic Search diff --git a/public/raw/documentation.md b/public/raw/documentation.md index b4f190d..2936dec 100644 --- a/public/raw/documentation.md +++ b/public/raw/documentation.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- ## Getting started diff --git a/public/raw/footer.md b/public/raw/footer.md index f810945..f35a161 100644 --- a/public/raw/footer.md +++ b/public/raw/footer.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- Built with [Convex](https://convex.dev) for real-time sync and deployed on [Netlify](https://netlify.com). Read the [project on GitHub](https://github.com/waynesutton/markdown-site) to fork and deploy your own. View [real-time site stats](/stats). diff --git a/public/raw/fork-configuration-guide.md b/public/raw/fork-configuration-guide.md index a017b0a..3cc5c90 100644 --- a/public/raw/fork-configuration-guide.md +++ b/public/raw/fork-configuration-guide.md @@ -13,9 +13,37 @@ Tags: configuration, setup, fork, tutorial After forking this markdown framework, you need to update configuration files with your site information. This affects your site name, URLs, RSS feeds, social sharing metadata, and AI discovery files. -Previously this meant editing 10+ files manually. Now you have two options. +Previously this meant editing 10+ files manually. Now you have three options. -## Option 1: Automated configuration +## Option 1: npx CLI (Recommended) + +The fastest way to get started. Run a single command to create a new project: + +```bash +npx create-markdown-sync my-site +``` + +The interactive wizard will: +1. Clone the template repository +2. Walk through all configuration options (site name, URLs, features, etc.) +3. Install dependencies +4. Set up Convex (opens browser for login) +5. Start the dev server and open your browser + +After setup, follow the on-screen instructions: + +```bash +cd my-site +npx convex dev # Start Convex (required first time) +npm run sync # Sync content (in another terminal) +npm run dev # Start dev server +``` + +**Resources:** +- Deployment: https://www.markdown.fast/docs-deployment +- WorkOS auth: https://www.markdown.fast/how-to-setup-workos + +## Option 2: Automated configuration Run a single command to configure everything at once. @@ -81,7 +109,7 @@ Updating public/.well-known/ai-plugin.json... Configuration complete! ``` -## Option 2: Manual configuration +## Option 3: Manual configuration If you prefer to update files manually, follow the guide in `FORK_CONFIG.md`. It includes: @@ -332,11 +360,12 @@ If you want to clear the sample content, delete the markdown files in those dire ## Summary -Two options after forking: +Three options to get started: -1. **Automated**: `cp fork-config.json.example fork-config.json`, edit JSON, run `npm run configure` -2. **Manual**: Follow `FORK_CONFIG.md` step-by-step or paste the AI prompt into Claude/ChatGPT +1. **npx CLI (Recommended)**: `npx create-markdown-sync my-site` - interactive wizard creates and configures everything +2. **Automated**: `cp fork-config.json.example fork-config.json`, edit JSON, run `npm run configure` +3. **Manual**: Follow `FORK_CONFIG.md` step-by-step or paste the AI prompt into Claude/ChatGPT -Both approaches update the same 11 files. The automated option takes about 30 seconds. The manual option gives you more control over each change. +The npx CLI is the fastest option for new projects. The automated and manual options work best for existing forks. Fork it, configure it, ship it. \ No newline at end of file diff --git a/public/raw/home-intro.md b/public/raw/home-intro.md index 084f770..249f5c1 100644 --- a/public/raw/home-intro.md +++ b/public/raw/home-intro.md @@ -2,12 +2,12 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- -An open-source publishing framework built for AI agents and developers to ship **[docs](/docs)**, or **[blogs](/blog)** or **[websites](/)**. +The open-source markdown publishing framework for developers and AI agents to ship **[docs](/docs)**, or **[blogs](/blog)** or **[websites](/)** that's always in sync. -Write markdown, sync from the terminal. **[Fork it](https://github.com/waynesutton/markdown-site)**, customize it, ship it. +**[Fork it](https://github.com/waynesutton/markdown-site)** or npm npx create-markdown-sync my-site, customize it, ship it. **Semantic search** - Find content by meaning, not just keywords. -**Ask AI** - Chat with your site content. Get answers with sources. \ No newline at end of file +**Ask AI** - Chat with your site content. Get answers with sources. + +``` + +``` \ No newline at end of file diff --git a/public/raw/index.md b/public/raw/index.md index 4c26d7a..1832f44 100644 --- a/public/raw/index.md +++ b/public/raw/index.md @@ -1,8 +1,8 @@ # Homepage -An open-source publishing framework built for AI agents and developers to ship **[docs](/docs)**, or **[blogs](/blog)** or **[websites](/)**. +The open-source markdown publishing framework for developers and AI agents to ship **[docs](/docs)**, or **[blogs](/blog)** or **[websites](/)** that's always in sync. -Write markdown, sync from the terminal. **[Fork it](https://github.com/waynesutton/markdown-site)**, customize it, ship it. +**[Fork it](https://github.com/waynesutton/markdown-site)** or npm npx create-markdown-sync my-site, customize it, ship it. **Ask AI** - Chat with your site content. Get answers with sources. +``` + +``` + --- ## Blog Posts (20) -- **[OpenCode Integration](/raw/docs-opencode.md)** +- **[OpenCode Integration](/raw/docs-opencode.md)** - This framework includes full OpenCode support with agents, commands, skills, and plugins. - Date: 2026-01-10 | Reading time: 4 min read | Tags: opencode, plugins, terminal - **[How to Use Code Blocks](/raw/how-to-use-code-blocks.md)** - A guide to syntax highlighting, diff rendering, and code formatting in your markdown posts. - Date: 2026-01-07 | Reading time: 4 min read | Tags: tutorial, markdown, code, syntax-highlighting diff --git a/public/raw/newsletter.md b/public/raw/newsletter.md index e84ca85..8742664 100644 --- a/public/raw/newsletter.md +++ b/public/raw/newsletter.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- # Newsletter Demo Page diff --git a/public/raw/projects.md b/public/raw/projects.md index e0e8903..392579a 100644 --- a/public/raw/projects.md +++ b/public/raw/projects.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-10 +Date: 2026-01-11 --- This markdown framework is open source and built to be extended. Here is what ships out of the box. diff --git a/scripts/configure-fork.ts b/scripts/configure-fork.ts index 5b9c018..2a58ef0 100644 --- a/scripts/configure-fork.ts +++ b/scripts/configure-fork.ts @@ -29,6 +29,19 @@ import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +// Silent mode flag (for CLI usage) +const silent = process.argv.includes("--silent"); + +// Log helper that respects silent mode +function log(message: string): void { + if (!silent) log(message); +} + +// Warn helper that always shows warnings +function warn(message: string): void { + warn(message); +} + // Configuration interface matching fork-config.json interface ForkConfig { siteName: string; @@ -112,10 +125,10 @@ function readConfig(): ForkConfig { if (!fs.existsSync(configPath)) { console.error("Error: fork-config.json not found."); - console.log("\nTo get started:"); - console.log("1. Copy fork-config.json.example to fork-config.json"); - console.log("2. Edit fork-config.json with your site information"); - console.log("3. Run npm run configure again"); + log("\nTo get started:"); + log("1. Copy fork-config.json.example to fork-config.json"); + log("2. Edit fork-config.json with your site information"); + log("3. Run npm run configure again"); process.exit(1); } @@ -131,7 +144,7 @@ function updateFile( const filePath = path.join(PROJECT_ROOT, relativePath); if (!fs.existsSync(filePath)) { - console.warn(`Warning: ${relativePath} not found, skipping.`); + warn(`Warning: ${relativePath} not found, skipping.`); return; } @@ -148,29 +161,37 @@ function updateFile( if (modified) { fs.writeFileSync(filePath, content, "utf-8"); - console.log(` Updated: ${relativePath}`); + log(` Updated: ${relativePath}`); } else { - console.log(` No changes: ${relativePath}`); + log(` No changes: ${relativePath}`); } } // Update siteConfig.ts function updateSiteConfig(config: ForkConfig): void { - console.log("\nUpdating src/config/siteConfig.ts..."); + log("\nUpdating src/config/siteConfig.ts..."); const filePath = path.join(PROJECT_ROOT, "src/config/siteConfig.ts"); let content = fs.readFileSync(filePath, "utf-8"); - // Update site name + // Update site name (match single-quoted or double-quoted strings properly) content = content.replace( - /name: ['"].*?['"]/, - `name: '${config.siteName}'`, + /name: '(?:[^'\\]|\\.)*'/, + `name: '${config.siteName.replace(/'/g, "\\'")}'`, + ); + content = content.replace( + /name: "(?:[^"\\]|\\.)*"/, + `name: "${config.siteName.replace(/"/g, '\\"')}"`, ); - // Update site title + // Update site title (match single-quoted or double-quoted strings properly) content = content.replace( - /title: ['"].*?['"]/, - `title: "${config.siteTitle}"`, + /title: '(?:[^'\\]|\\.)*'/, + `title: "${config.siteTitle.replace(/"/g, '\\"')}"`, + ); + content = content.replace( + /title: "(?:[^"\\]|\\.)*"/, + `title: "${config.siteTitle.replace(/"/g, '\\"')}"`, ); // Update bio @@ -389,12 +410,12 @@ function updateSiteConfig(config: ForkConfig): void { } fs.writeFileSync(filePath, content, "utf-8"); - console.log(` Updated: src/config/siteConfig.ts`); + log(` Updated: src/config/siteConfig.ts`); } // Update Home.tsx function updateHomeTsx(config: ForkConfig): void { - console.log("\nUpdating src/pages/Home.tsx..."); + log("\nUpdating src/pages/Home.tsx..."); const githubRepoUrl = `https://github.com/${config.githubUsername}/${config.githubRepo}`; @@ -439,7 +460,7 @@ function updateHomeTsx(config: ForkConfig): void { // Update Post.tsx function updatePostTsx(config: ForkConfig): void { - console.log("\nUpdating src/pages/Post.tsx..."); + log("\nUpdating src/pages/Post.tsx..."); updateFile("src/pages/Post.tsx", [ // Match any existing SITE_URL value (https://...) @@ -457,7 +478,7 @@ function updatePostTsx(config: ForkConfig): void { // Update DocsPage.tsx function updateDocsPageTsx(config: ForkConfig): void { - console.log("\nUpdating src/pages/DocsPage.tsx..."); + log("\nUpdating src/pages/DocsPage.tsx..."); updateFile("src/pages/DocsPage.tsx", [ // Match any existing SITE_URL value (https://...) @@ -470,7 +491,7 @@ function updateDocsPageTsx(config: ForkConfig): void { // Update convex/http.ts function updateConvexHttp(config: ForkConfig): void { - console.log("\nUpdating convex/http.ts..."); + log("\nUpdating convex/http.ts..."); updateFile("convex/http.ts", [ // Match any existing SITE_URL value with process.env fallback @@ -503,7 +524,7 @@ function updateConvexHttp(config: ForkConfig): void { // Update convex/rss.ts function updateConvexRss(config: ForkConfig): void { - console.log("\nUpdating convex/rss.ts..."); + log("\nUpdating convex/rss.ts..."); updateFile("convex/rss.ts", [ // Match any existing SITE_URL value with process.env fallback @@ -526,7 +547,7 @@ function updateConvexRss(config: ForkConfig): void { // Update index.html function updateIndexHtml(config: ForkConfig): void { - console.log("\nUpdating index.html..."); + log("\nUpdating index.html..."); const replacements: Array<{ search: string | RegExp; replace: string }> = [ // Meta description (match any content) @@ -626,7 +647,7 @@ function updateIndexHtml(config: ForkConfig): void { // Update public/llms.txt function updateLlmsTxt(config: ForkConfig): void { - console.log("\nUpdating public/llms.txt..."); + log("\nUpdating public/llms.txt..."); const githubUrl = `https://github.com/${config.githubUsername}/${config.githubRepo}`; @@ -713,12 +734,12 @@ Each post contains: const filePath = path.join(PROJECT_ROOT, "public/llms.txt"); fs.writeFileSync(filePath, content, "utf-8"); - console.log(` Updated: public/llms.txt`); + log(` Updated: public/llms.txt`); } // Update public/robots.txt function updateRobotsTxt(config: ForkConfig): void { - console.log("\nUpdating public/robots.txt..."); + log("\nUpdating public/robots.txt..."); const content = `# robots.txt for ${config.siteName} # https://www.robotstxt.org/ @@ -757,12 +778,12 @@ Crawl-delay: 1 const filePath = path.join(PROJECT_ROOT, "public/robots.txt"); fs.writeFileSync(filePath, content, "utf-8"); - console.log(` Updated: public/robots.txt`); + log(` Updated: public/robots.txt`); } // Update public/openapi.yaml function updateOpenApiYaml(config: ForkConfig): void { - console.log("\nUpdating public/openapi.yaml..."); + log("\nUpdating public/openapi.yaml..."); const githubUrl = `https://github.com/${config.githubUsername}/${config.githubRepo}`; // Extract domain from siteUrl for example URLs (without www. if present) @@ -814,7 +835,7 @@ function updateOpenApiYaml(config: ForkConfig): void { // Update public/.well-known/ai-plugin.json function updateAiPluginJson(config: ForkConfig): void { - console.log("\nUpdating public/.well-known/ai-plugin.json..."); + log("\nUpdating public/.well-known/ai-plugin.json..."); const pluginName = config.siteName.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, ""); @@ -838,14 +859,14 @@ function updateAiPluginJson(config: ForkConfig): void { const filePath = path.join(PROJECT_ROOT, "public/.well-known/ai-plugin.json"); fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + "\n", "utf-8"); - console.log(` Updated: public/.well-known/ai-plugin.json`); + log(` Updated: public/.well-known/ai-plugin.json`); } // Update default theme in siteConfig.ts function updateThemeConfig(config: ForkConfig): void { if (!config.theme) return; - console.log("\nUpdating default theme in src/config/siteConfig.ts..."); + log("\nUpdating default theme in src/config/siteConfig.ts..."); updateFile("src/config/siteConfig.ts", [ { @@ -857,7 +878,7 @@ function updateThemeConfig(config: ForkConfig): void { // Update netlify/edge-functions/mcp.ts function updateMcpEdgeFunction(config: ForkConfig): void { - console.log("\nUpdating netlify/edge-functions/mcp.ts..."); + log("\nUpdating netlify/edge-functions/mcp.ts..."); updateFile("netlify/edge-functions/mcp.ts", [ // Match any existing SITE_URL constant @@ -880,7 +901,7 @@ function updateMcpEdgeFunction(config: ForkConfig): void { // Update scripts/send-newsletter.ts function updateSendNewsletter(config: ForkConfig): void { - console.log("\nUpdating scripts/send-newsletter.ts..."); + log("\nUpdating scripts/send-newsletter.ts..."); updateFile("scripts/send-newsletter.ts", [ // Match any existing SITE_URL fallback in comment @@ -898,13 +919,13 @@ function updateSendNewsletter(config: ForkConfig): void { // Main function function main(): void { - console.log("Fork Configuration Script"); - console.log("=========================\n"); + log("Fork Configuration Script"); + log("=========================\n"); // Read configuration const config = readConfig(); - console.log(`Configuring site: ${config.siteName}`); - console.log(`URL: ${config.siteUrl}`); + log(`Configuring site: ${config.siteName}`); + log(`URL: ${config.siteUrl}`); // Apply updates to all files updateSiteConfig(config); @@ -922,14 +943,14 @@ function main(): void { updateMcpEdgeFunction(config); updateSendNewsletter(config); - console.log("\n========================="); - console.log("Configuration complete!"); - console.log("\nNext steps:"); - console.log("1. Review the changes with: git diff"); - console.log("2. Run: npx convex dev (if not already running)"); - console.log("3. Run: npm run sync (to sync content to development)"); - console.log("4. Run: npm run dev (to start the dev server)"); - console.log("5. Deploy to Netlify when ready"); + log("\n========================="); + log("Configuration complete!"); + log("\nNext steps:"); + log("1. Review the changes with: git diff"); + log("2. Run: npx convex dev (if not already running)"); + log("3. Run: npm run sync (to sync content to development)"); + log("4. Run: npm run dev (to start the dev server)"); + log("5. Deploy to Netlify when ready"); } main(); diff --git a/src/components/BlogPost.tsx b/src/components/BlogPost.tsx index 3479b7c..4c196fc 100644 --- a/src/components/BlogPost.tsx +++ b/src/components/BlogPost.tsx @@ -1,4 +1,5 @@ import React, { useState, useRef } from "react"; +import { createPortal } from "react-dom"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import remarkBreaks from "remark-breaks"; @@ -135,7 +136,7 @@ function InlineCopyButton({ command }: { command: string }) { ); } -// Image lightbox component +// Image lightbox component - uses portal to escape contain: layout function ImageLightbox({ src, alt, @@ -165,7 +166,8 @@ function ImageLightbox({ }; }, [onClose]); - return ( + // Use portal to render at document body level, escaping contain: layout + return createPortal(
- + , + document.body ); } diff --git a/src/components/DiffCodeBlock.tsx b/src/components/DiffCodeBlock.tsx index 7f438dc..fd9d01b 100644 --- a/src/components/DiffCodeBlock.tsx +++ b/src/components/DiffCodeBlock.tsx @@ -11,6 +11,21 @@ const THEME_MAP: Record = { cloud: "light", }; +// Check if content is a valid unified diff format +// Valid diffs have headers like "diff --git", "---", "+++", or "@@" +function isValidDiff(code: string): boolean { + const lines = code.trim().split("\n"); + // Check for common diff indicators + const hasDiffHeader = lines.some( + (line) => + line.startsWith("diff ") || + line.startsWith("--- ") || + line.startsWith("+++ ") || + line.startsWith("@@ ") + ); + return hasDiffHeader; +} + interface DiffCodeBlockProps { code: string; language: "diff" | "patch"; @@ -30,6 +45,38 @@ export default function DiffCodeBlock({ code, language }: DiffCodeBlockProps) { setTimeout(() => setCopied(false), 2000); }; + // If not a valid diff format, render as simple code block with diff styling + if (!isValidDiff(code)) { + return ( +
+ {language} + +
+          
+            {code.split("\n").map((line, i) => {
+              let className = "";
+              if (line.startsWith("+")) className = "diff-added";
+              else if (line.startsWith("-")) className = "diff-removed";
+              return (
+                
+                  {line}
+                  {"\n"}
+                
+              );
+            })}
+          
+        
+
+ ); + } + return (
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 3d8e8e7..c77a680 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,10 +1,179 @@ +import React, { useState } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import remarkBreaks from "remark-breaks"; import rehypeRaw from "rehype-raw"; import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; +import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter"; +import bash from "react-syntax-highlighter/dist/esm/languages/prism/bash"; +import javascript from "react-syntax-highlighter/dist/esm/languages/prism/javascript"; +import typescript from "react-syntax-highlighter/dist/esm/languages/prism/typescript"; +import markdown from "react-syntax-highlighter/dist/esm/languages/prism/markdown"; +import diff from "react-syntax-highlighter/dist/esm/languages/prism/diff"; +import json from "react-syntax-highlighter/dist/esm/languages/prism/json"; +import { Copy, Check } from "lucide-react"; +import { useTheme } from "../context/ThemeContext"; +import DiffCodeBlock from "./DiffCodeBlock"; import siteConfig from "../config/siteConfig"; +// Register languages for syntax highlighting +SyntaxHighlighter.registerLanguage("bash", bash); +SyntaxHighlighter.registerLanguage("shell", bash); +SyntaxHighlighter.registerLanguage("sh", bash); +SyntaxHighlighter.registerLanguage("javascript", javascript); +SyntaxHighlighter.registerLanguage("js", javascript); +SyntaxHighlighter.registerLanguage("typescript", typescript); +SyntaxHighlighter.registerLanguage("ts", typescript); +SyntaxHighlighter.registerLanguage("markdown", markdown); +SyntaxHighlighter.registerLanguage("md", markdown); +SyntaxHighlighter.registerLanguage("diff", diff); +SyntaxHighlighter.registerLanguage("json", json); + +// Cursor Dark Theme for syntax highlighting +const cursorDarkTheme: { [key: string]: React.CSSProperties } = { + 'code[class*="language-"]': { + color: "#e4e4e7", + background: "none", + fontFamily: "var(--font-mono)", + fontSize: "0.9em", + textAlign: "left", + whiteSpace: "pre", + wordSpacing: "normal", + wordBreak: "normal", + wordWrap: "normal", + lineHeight: "1.6", + tabSize: 2, + }, + 'pre[class*="language-"]': { + color: "#e4e4e7", + background: "#18181b", + padding: "1.25em", + margin: "0", + overflow: "auto", + borderRadius: "0.5rem", + }, + comment: { color: "#71717a" }, + punctuation: { color: "#a1a1aa" }, + property: { color: "#93c5fd" }, + string: { color: "#86efac" }, + keyword: { color: "#c4b5fd" }, + function: { color: "#fcd34d" }, + number: { color: "#fdba74" }, + operator: { color: "#f9a8d4" }, + "class-name": { color: "#93c5fd" }, + boolean: { color: "#fdba74" }, + variable: { color: "#e4e4e7" }, + "attr-name": { color: "#93c5fd" }, + "attr-value": { color: "#86efac" }, + tag: { color: "#f87171" }, + deleted: { color: "#f87171", background: "rgba(248, 113, 113, 0.1)" }, + inserted: { color: "#86efac", background: "rgba(134, 239, 172, 0.1)" }, +}; + +// Cursor Light Theme for syntax highlighting +const cursorLightTheme: { [key: string]: React.CSSProperties } = { + 'code[class*="language-"]': { + color: "#27272a", + background: "none", + fontFamily: "var(--font-mono)", + fontSize: "0.9em", + textAlign: "left", + whiteSpace: "pre", + wordSpacing: "normal", + wordBreak: "normal", + wordWrap: "normal", + lineHeight: "1.6", + tabSize: 2, + }, + 'pre[class*="language-"]': { + color: "#27272a", + background: "#f4f4f5", + padding: "1.25em", + margin: "0", + overflow: "auto", + borderRadius: "0.5rem", + }, + comment: { color: "#71717a" }, + punctuation: { color: "#52525b" }, + property: { color: "#2563eb" }, + string: { color: "#16a34a" }, + keyword: { color: "#7c3aed" }, + function: { color: "#ca8a04" }, + number: { color: "#ea580c" }, + operator: { color: "#db2777" }, + "class-name": { color: "#2563eb" }, + boolean: { color: "#ea580c" }, + variable: { color: "#27272a" }, + "attr-name": { color: "#2563eb" }, + "attr-value": { color: "#16a34a" }, + tag: { color: "#dc2626" }, + deleted: { color: "#dc2626", background: "rgba(220, 38, 38, 0.1)" }, + inserted: { color: "#16a34a", background: "rgba(22, 163, 74, 0.1)" }, +}; + +// Tan Theme for syntax highlighting +const cursorTanTheme: { [key: string]: React.CSSProperties } = { + 'code[class*="language-"]': { + color: "#44403c", + background: "none", + fontFamily: "var(--font-mono)", + fontSize: "0.9em", + textAlign: "left", + whiteSpace: "pre", + wordSpacing: "normal", + wordBreak: "normal", + wordWrap: "normal", + lineHeight: "1.6", + tabSize: 2, + }, + 'pre[class*="language-"]': { + color: "#44403c", + background: "#f5f5f0", + padding: "1.25em", + margin: "0", + overflow: "auto", + borderRadius: "0.5rem", + }, + comment: { color: "#78716c" }, + punctuation: { color: "#57534e" }, + property: { color: "#1d4ed8" }, + string: { color: "#15803d" }, + keyword: { color: "#6d28d9" }, + function: { color: "#a16207" }, + number: { color: "#c2410c" }, + operator: { color: "#be185d" }, + "class-name": { color: "#1d4ed8" }, + boolean: { color: "#c2410c" }, + variable: { color: "#44403c" }, + "attr-name": { color: "#1d4ed8" }, + "attr-value": { color: "#15803d" }, + tag: { color: "#b91c1c" }, + deleted: { color: "#b91c1c", background: "rgba(185, 28, 28, 0.1)" }, + inserted: { color: "#15803d", background: "rgba(21, 128, 61, 0.1)" }, +}; + +// Copy button component for code blocks +function CodeCopyButton({ code }: { code: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + await navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + ); +} + // Sanitize schema for footer markdown (allows links, paragraphs, line breaks, images) // style attribute is sanitized by rehypeSanitize to remove dangerous CSS const footerSanitizeSchema = { @@ -25,8 +194,23 @@ interface FooterProps { } export default function Footer({ content }: FooterProps) { + const { theme } = useTheme(); const { footer } = siteConfig; + // Get code theme based on current theme + const getCodeTheme = () => { + switch (theme) { + case "dark": + return cursorDarkTheme; + case "light": + return cursorLightTheme; + case "tan": + return cursorTanTheme; + default: + return cursorDarkTheme; + } + }; + // Don't render if footer is globally disabled if (!footer.enabled) { return null; @@ -81,6 +265,77 @@ export default function Footer({ content }: FooterProps) { ); }, + // Code blocks with syntax highlighting + code(codeProps) { + const { className, children, style, ...restProps } = + codeProps as { + className?: string; + children?: React.ReactNode; + style?: React.CSSProperties; + }; + const match = /language-(\w+)/.exec(className || ""); + + // Detect inline code vs code blocks + const codeContent = String(children); + const hasNewlines = codeContent.includes("\n"); + const isShort = codeContent.length < 80; + const hasLanguage = !!match || !!className; + + // It's inline only if: no language, short content, no newlines + const isInline = !hasLanguage && isShort && !hasNewlines; + + if (isInline) { + return ( + + {children} + + ); + } + + const codeString = String(children).replace(/\n$/, ""); + const language = match ? match[1] : "text"; + + // Route diff/patch to DiffCodeBlock for enhanced diff rendering + if (language === "diff" || language === "patch") { + return ( + + ); + } + + const isTextBlock = language === "text"; + + // Custom styles for text blocks to enable wrapping + const textBlockStyle = isTextBlock + ? { + whiteSpace: "pre-wrap" as const, + wordWrap: "break-word" as const, + overflowWrap: "break-word" as const, + } + : {}; + + return ( +
+ {match && {match[1]}} + + + {codeString} + +
+ ); + }, }} > {footerContent} diff --git a/src/config/siteConfig.ts b/src/config/siteConfig.ts index 37bead2..8c62b23 100644 --- a/src/config/siteConfig.ts +++ b/src/config/siteConfig.ts @@ -272,6 +272,13 @@ export interface AskAIConfig { models: AIModelOption[]; // Available models for Ask AI } +// Related posts configuration +// Controls the display of related posts at the bottom of blog posts +export interface RelatedPostsConfig { + defaultViewMode: "list" | "thumbnails"; // Default view mode for related posts + showViewToggle: boolean; // Show toggle button to switch between views +} + // Social link configuration for social footer export interface SocialLink { platform: @@ -413,13 +420,16 @@ export interface SiteConfig { // Ask AI configuration (optional) askAI?: AskAIConfig; + + // Related posts configuration (optional) + relatedPosts?: RelatedPostsConfig; } // Default site configuration // Customize this for your site export const siteConfig: SiteConfig = { // Basic site info - name: 'markdown "sync" framework', + name: "markdown sync", title: "markdown sync framework", // Optional logo/header image (place in public/images/, set to null to hide) logo: "/images/logo.svg", @@ -836,6 +846,13 @@ export const siteConfig: SiteConfig = { }, ], }, + + // Related posts configuration + // Controls the display of related posts at the bottom of blog posts + relatedPosts: { + defaultViewMode: "thumbnails", // Default view: "list" or "thumbnails" + showViewToggle: true, // Show toggle button to switch between views + }, }; // Export the config as default for easy importing diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 5ddb05c..1004783 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -276,6 +276,7 @@ interface ConfirmDeleteModalProps { isOpen: boolean; onClose: () => void; onConfirm: () => void; + onCopy: () => void; title: string; itemName: string; itemType: "post" | "page"; @@ -286,11 +287,20 @@ function ConfirmDeleteModal({ isOpen, onClose, onConfirm, + onCopy, title, itemName, itemType, isDeleting, }: ConfirmDeleteModalProps) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + await onCopy(); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + const handleBackdropClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget && !isDeleting) { onClose(); @@ -309,6 +319,13 @@ function ConfirmDeleteModal({ return () => document.removeEventListener("keydown", handleEsc); }, [isOpen, onClose, isDeleting]); + // Reset copied state when modal closes + useEffect(() => { + if (!isOpen) { + setCopied(false); + } + }, [isOpen]); + if (!isOpen) return null; return ( @@ -340,6 +357,30 @@ function ConfirmDeleteModal({ This action cannot be undone. The {itemType} will be permanently removed from the database.

+
+
+ + Would you like to copy the markdown before deleting? +
+ +
@@ -670,7 +711,8 @@ function DashboardContent() { id: string; title: string; type: "post" | "page"; - }>({ isOpen: false, id: "", title: "", type: "post" }); + item: ContentItem | null; + }>({ isOpen: false, id: "", title: "", type: "post", item: null }); const [isDeleting, setIsDeleting] = useState(false); // Sync server state @@ -862,12 +904,13 @@ function DashboardContent() { // Show delete confirmation modal for a post const handleDeletePost = useCallback( - (id: string, title: string) => { + (item: ContentItem) => { setDeleteModal({ isOpen: true, - id, - title, + id: item._id, + title: item.title, type: "post", + item, }); }, [], @@ -875,12 +918,13 @@ function DashboardContent() { // Show delete confirmation modal for a page const handleDeletePage = useCallback( - (id: string, title: string) => { + (item: ContentItem) => { setDeleteModal({ isOpen: true, - id, - title, + id: item._id, + title: item.title, type: "page", + item, }); }, [], @@ -889,7 +933,7 @@ function DashboardContent() { // Close delete modal const closeDeleteModal = useCallback(() => { if (!isDeleting) { - setDeleteModal({ isOpen: false, id: "", title: "", type: "post" }); + setDeleteModal({ isOpen: false, id: "", title: "", type: "post", item: null }); } }, [isDeleting]); @@ -904,7 +948,7 @@ function DashboardContent() { await deletePageMutation({ id: deleteModal.id as Id<"pages"> }); addToast("Page deleted successfully", "success"); } - setDeleteModal({ isOpen: false, id: "", title: "", type: "post" }); + setDeleteModal({ isOpen: false, id: "", title: "", type: "post", item: null }); } catch (error) { addToast( error instanceof Error ? error.message : `Failed to delete ${deleteModal.type}`, @@ -1005,6 +1049,14 @@ function DashboardContent() { [], ); + // Copy markdown content before deletion + const handleCopyBeforeDelete = useCallback(async () => { + if (!deleteModal.item) return; + const markdown = generateMarkdown(deleteModal.item, deleteModal.type); + await navigator.clipboard.writeText(markdown); + addToast("Markdown copied to clipboard", "success"); + }, [deleteModal, generateMarkdown, addToast]); + // Download markdown file const handleDownloadMarkdown = useCallback(() => { if (!editingItem) return; @@ -1166,6 +1218,7 @@ function DashboardContent() { isOpen={deleteModal.isOpen} onClose={closeDeleteModal} onConfirm={confirmDelete} + onCopy={handleCopyBeforeDelete} title="Delete Confirmation" itemName={deleteModal.title} itemType={deleteModal.type} @@ -1275,7 +1328,7 @@ function DashboardContent() { setSearchQuery(e.target.value)} className="dashboard-search-input" @@ -1414,6 +1467,7 @@ function DashboardContent() { sidebarCollapsed={sidebarCollapsed} setSidebarCollapsed={setSidebarCollapsed} addToast={addToast} + setActiveSection={setActiveSection} /> )} @@ -1424,6 +1478,7 @@ function DashboardContent() { sidebarCollapsed={sidebarCollapsed} setSidebarCollapsed={setSidebarCollapsed} addToast={addToast} + setActiveSection={setActiveSection} /> )} @@ -1503,7 +1558,7 @@ function PostsListView({ posts: ContentItem[]; onEdit: (post: ContentItem) => void; searchQuery: string; - onDelete: (id: string, title: string) => void; + onDelete: (item: ContentItem) => void; }) { const [filter, setFilter] = useState<"all" | "published" | "draft">("all"); const [itemsPerPage, setItemsPerPage] = useState(15); @@ -1651,7 +1706,7 @@ function PostsListView({ {post.source === "dashboard" && (
+ {/* Related Posts */} +
+

Related Posts

+
+ + +
+
+ +
+

+ Controls the display of related posts at the bottom of blog posts. Thumbnails view shows image, title, description and author. +

+
+ {/* Version Control */} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 60327a9..84f98f3 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -4,6 +4,17 @@ import { useQuery } from "convex/react"; import { api } from "../../convex/_generated/api"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; +import rehypeRaw from "rehype-raw"; +import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; +import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter"; +import bash from "react-syntax-highlighter/dist/esm/languages/prism/bash"; +import javascript from "react-syntax-highlighter/dist/esm/languages/prism/javascript"; +import typescript from "react-syntax-highlighter/dist/esm/languages/prism/typescript"; +import markdown from "react-syntax-highlighter/dist/esm/languages/prism/markdown"; +import diff from "react-syntax-highlighter/dist/esm/languages/prism/diff"; +import json from "react-syntax-highlighter/dist/esm/languages/prism/json"; +import { Copy, Check } from "lucide-react"; +import { useTheme } from "../context/ThemeContext"; import PostList from "../components/PostList"; import FeaturedCards from "../components/FeaturedCards"; import LogoMarquee from "../components/LogoMarquee"; @@ -11,8 +22,200 @@ import GitHubContributions from "../components/GitHubContributions"; import Footer from "../components/Footer"; import SocialFooter from "../components/SocialFooter"; import NewsletterSignup from "../components/NewsletterSignup"; +import DiffCodeBlock from "../components/DiffCodeBlock"; import siteConfig from "../config/siteConfig"; +// Sanitize schema for home intro markdown +const homeSanitizeSchema = { + ...defaultSchema, + attributes: { + ...defaultSchema.attributes, + span: ["className", "class", "style"], + }, +}; + +// Register languages for syntax highlighting +SyntaxHighlighter.registerLanguage("bash", bash); +SyntaxHighlighter.registerLanguage("shell", bash); +SyntaxHighlighter.registerLanguage("sh", bash); +SyntaxHighlighter.registerLanguage("javascript", javascript); +SyntaxHighlighter.registerLanguage("js", javascript); +SyntaxHighlighter.registerLanguage("typescript", typescript); +SyntaxHighlighter.registerLanguage("ts", typescript); +SyntaxHighlighter.registerLanguage("markdown", markdown); +SyntaxHighlighter.registerLanguage("md", markdown); +SyntaxHighlighter.registerLanguage("diff", diff); +SyntaxHighlighter.registerLanguage("json", json); + +// Cursor Dark Theme for syntax highlighting +const cursorDarkTheme: { [key: string]: React.CSSProperties } = { + 'code[class*="language-"]': { + color: "#e4e4e7", + background: "none", + fontFamily: "var(--font-mono)", + fontSize: "0.9em", + textAlign: "left", + whiteSpace: "pre", + wordSpacing: "normal", + wordBreak: "normal", + wordWrap: "normal", + lineHeight: "1.6", + tabSize: 2, + }, + 'pre[class*="language-"]': { + color: "#e4e4e7", + background: "#18181b", + padding: "1.25em", + margin: "0", + overflow: "auto", + borderRadius: "0.5rem", + }, + comment: { color: "#71717a" }, + punctuation: { color: "#a1a1aa" }, + property: { color: "#93c5fd" }, + string: { color: "#86efac" }, + keyword: { color: "#c4b5fd" }, + function: { color: "#fcd34d" }, + number: { color: "#fdba74" }, + operator: { color: "#f9a8d4" }, + "class-name": { color: "#93c5fd" }, + boolean: { color: "#fdba74" }, + variable: { color: "#e4e4e7" }, + "attr-name": { color: "#93c5fd" }, + "attr-value": { color: "#86efac" }, + tag: { color: "#f87171" }, + deleted: { color: "#f87171", background: "rgba(248, 113, 113, 0.1)" }, + inserted: { color: "#86efac", background: "rgba(134, 239, 172, 0.1)" }, +}; + +// Cursor Light Theme for syntax highlighting +const cursorLightTheme: { [key: string]: React.CSSProperties } = { + 'code[class*="language-"]': { + color: "#27272a", + background: "none", + fontFamily: "var(--font-mono)", + fontSize: "0.9em", + textAlign: "left", + whiteSpace: "pre", + wordSpacing: "normal", + wordBreak: "normal", + wordWrap: "normal", + lineHeight: "1.6", + tabSize: 2, + }, + 'pre[class*="language-"]': { + color: "#27272a", + background: "#f4f4f5", + padding: "1.25em", + margin: "0", + overflow: "auto", + borderRadius: "0.5rem", + }, + comment: { color: "#71717a" }, + punctuation: { color: "#52525b" }, + property: { color: "#2563eb" }, + string: { color: "#16a34a" }, + keyword: { color: "#7c3aed" }, + function: { color: "#ca8a04" }, + number: { color: "#ea580c" }, + operator: { color: "#db2777" }, + "class-name": { color: "#2563eb" }, + boolean: { color: "#ea580c" }, + variable: { color: "#27272a" }, + "attr-name": { color: "#2563eb" }, + "attr-value": { color: "#16a34a" }, + tag: { color: "#dc2626" }, + deleted: { color: "#dc2626", background: "rgba(220, 38, 38, 0.1)" }, + inserted: { color: "#16a34a", background: "rgba(22, 163, 74, 0.1)" }, +}; + +// Tan Theme for syntax highlighting +const cursorTanTheme: { [key: string]: React.CSSProperties } = { + 'code[class*="language-"]': { + color: "#44403c", + background: "none", + fontFamily: "var(--font-mono)", + fontSize: "0.9em", + textAlign: "left", + whiteSpace: "pre", + wordSpacing: "normal", + wordBreak: "normal", + wordWrap: "normal", + lineHeight: "1.6", + tabSize: 2, + }, + 'pre[class*="language-"]': { + color: "#44403c", + background: "#f5f5f0", + padding: "1.25em", + margin: "0", + overflow: "auto", + borderRadius: "0.5rem", + }, + comment: { color: "#78716c" }, + punctuation: { color: "#57534e" }, + property: { color: "#1d4ed8" }, + string: { color: "#15803d" }, + keyword: { color: "#6d28d9" }, + function: { color: "#a16207" }, + number: { color: "#c2410c" }, + operator: { color: "#be185d" }, + "class-name": { color: "#1d4ed8" }, + boolean: { color: "#c2410c" }, + variable: { color: "#44403c" }, + "attr-name": { color: "#1d4ed8" }, + "attr-value": { color: "#15803d" }, + tag: { color: "#b91c1c" }, + deleted: { color: "#b91c1c", background: "rgba(185, 28, 28, 0.1)" }, + inserted: { color: "#15803d", background: "rgba(21, 128, 61, 0.1)" }, +}; + +// Copy button component for code blocks +function CodeCopyButton({ code }: { code: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + await navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + ); +} + +// Inline copy button for copy-command spans +function InlineCopyButton({ command }: { command: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + await navigator.clipboard.writeText(command); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + ); +} + // Local storage key for view mode preference const VIEW_MODE_KEY = "featured-view-mode"; @@ -87,6 +290,8 @@ function HeadingAnchor({ id }: { id: string }) { } export default function Home() { + const { theme } = useTheme(); + // Fetch published posts from Convex (only if showing on home) const posts = useQuery( api.posts.getAllPosts, @@ -108,6 +313,20 @@ export default function Home() { siteConfig.featuredViewMode, ); + // Get code theme based on current theme + const getCodeTheme = () => { + switch (theme) { + case "dark": + return cursorDarkTheme; + case "light": + return cursorLightTheme; + case "tan": + return cursorTanTheme; + default: + return cursorDarkTheme; + } + }; + // Load saved view mode preference from localStorage useEffect(() => { const saved = localStorage.getItem(VIEW_MODE_KEY); @@ -189,6 +408,7 @@ export default function Home() { > ( @@ -280,6 +500,90 @@ export default function Home() { hr() { return
; }, + // Code blocks with syntax highlighting + code(codeProps) { + const { className, children, style, ...restProps } = + codeProps as { + className?: string; + children?: React.ReactNode; + style?: React.CSSProperties; + }; + const match = /language-(\w+)/.exec(className || ""); + + // Detect inline code vs code blocks + const codeContent = String(children); + const hasNewlines = codeContent.includes("\n"); + const isShort = codeContent.length < 80; + const hasLanguage = !!match || !!className; + + // It's inline only if: no language, short content, no newlines + const isInline = !hasLanguage && isShort && !hasNewlines; + + if (isInline) { + return ( + + {children} + + ); + } + + const codeString = String(children).replace(/\n$/, ""); + const language = match ? match[1] : "text"; + + // Route diff/patch to DiffCodeBlock for enhanced diff rendering + if (language === "diff" || language === "patch") { + return ( + + ); + } + + const isTextBlock = language === "text"; + + // Custom styles for text blocks to enable wrapping + const textBlockStyle = isTextBlock + ? { + whiteSpace: "pre-wrap" as const, + wordWrap: "break-word" as const, + overflowWrap: "break-word" as const, + } + : {}; + + return ( +
+ {match && {match[1]}} + + + {codeString} + +
+ ); + }, + // Span component with copy-command support + span({ className, children }) { + if (className === "copy-command") { + const command = getTextContent(children); + return ( + + {command} + + + ); + } + return {children}; + }, }} > {stripHtmlComments(homeIntro.content)} diff --git a/src/pages/Post.tsx b/src/pages/Post.tsx index 2a30fd0..f3eb673 100644 --- a/src/pages/Post.tsx +++ b/src/pages/Post.tsx @@ -15,9 +15,12 @@ import { useSidebar } from "../context/SidebarContext"; import { format, parseISO } from "date-fns"; import { ArrowLeft, Link as LinkIcon, Rss, Tag } from "lucide-react"; import { XLogo, LinkedinLogo } from "@phosphor-icons/react"; -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import siteConfig from "../config/siteConfig"; +// Local storage key for related posts view mode preference +const RELATED_POSTS_VIEW_MODE_KEY = "related-posts-view-mode"; + // Site configuration - update these for your site (or run npm run configure) const SITE_URL = "https://www.markdown.fast"; const SITE_NAME = "markdown sync framework"; @@ -87,6 +90,28 @@ export default function Post({ const [copied, setCopied] = useState(false); + // State for related posts view mode toggle (list or thumbnails) + const [relatedPostsViewMode, setRelatedPostsViewMode] = useState<"list" | "thumbnails">( + siteConfig.relatedPosts?.defaultViewMode ?? "thumbnails", + ); + + // Load saved related posts view mode preference from localStorage + useEffect(() => { + const saved = localStorage.getItem(RELATED_POSTS_VIEW_MODE_KEY); + if (saved === "list" || saved === "thumbnails") { + setRelatedPostsViewMode(saved); + } + }, []); + + // Toggle related posts view mode and save preference + const toggleRelatedPostsViewMode = useCallback(() => { + setRelatedPostsViewMode((prev) => { + const newMode = prev === "list" ? "thumbnails" : "list"; + localStorage.setItem(RELATED_POSTS_VIEW_MODE_KEY, newMode); + return newMode; + }); + }, []); + // Scroll to hash anchor after content loads // Skip if there's a search query - let the highlighting hook handle scroll useEffect(() => { @@ -859,26 +884,125 @@ export default function Post({ {/* Related posts section - only shown for blog posts with shared tags */} {relatedPosts && relatedPosts.length > 0 && (
-

Related Posts

-
    - {relatedPosts.map((relatedPost) => ( -
  • +
    +

    Related Posts

    + {siteConfig.relatedPosts?.showViewToggle !== false && ( + + )} +
    + + {/* Thumbnail view - shows image, title, description, author */} + {relatedPostsViewMode === "thumbnails" ? ( +
    + {relatedPosts.map((relatedPost) => ( - - {relatedPost.title} - - {relatedPost.readTime && ( - - {relatedPost.readTime} - + {relatedPost.image && ( +
    + {relatedPost.title} +
    )} +
    +

    + {relatedPost.title} +

    + {(relatedPost.excerpt || relatedPost.description) && ( +

    + {relatedPost.excerpt || relatedPost.description} +

    + )} +
    + {relatedPost.authorImage && ( + {relatedPost.authorName + )} + {relatedPost.authorName && ( + + {relatedPost.authorName} + + )} + {relatedPost.date && ( + + {format(parseISO(relatedPost.date), "MMM d, yyyy")} + + )} +
    +
    -
  • - ))} -
+ ))} +
+ ) : ( + /* List view - simple list with title and read time */ +
    + {relatedPosts.map((relatedPost) => ( +
  • + + + {relatedPost.title} + + {relatedPost.readTime && ( + + {relatedPost.readTime} + + )} + +
  • + ))} +
+ )}
)} diff --git a/src/styles/global.css b/src/styles/global.css index 90216bb..1403c01 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1774,6 +1774,33 @@ body { font-size: var(--font-size-inline-code); } +/* Diff fallback styles for invalid diff format */ +.diff-fallback { + background: var(--code-bg); + padding: 1.25em; + margin: 0; + overflow: auto; + border-radius: 0.5rem; + font-family: var(--font-mono); + font-size: 0.9em; + line-height: 1.6; +} + +.diff-fallback code { + background: none; + padding: 0; +} + +.diff-added { + color: var(--color-success, #86efac); + background: rgba(134, 239, 172, 0.1); +} + +.diff-removed { + color: var(--color-error, #f87171); + background: rgba(248, 113, 113, 0.1); +} + /* Copy command inline styles */ .copy-command { display: inline-flex; @@ -2180,6 +2207,128 @@ body { white-space: nowrap; } +/* Related posts header with toggle */ +.related-posts-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.related-posts-header .related-posts-title { + margin-bottom: 0; +} + +/* Related posts thumbnail view */ +.related-posts-thumbnails { + display: flex; + flex-direction: column; + gap: 24px; +} + +.related-post-thumbnail { + display: flex; + gap: 16px; + text-decoration: none; + color: inherit; + transition: opacity 0.2s ease; +} + +.related-post-thumbnail:hover { + opacity: 0.85; +} + +.related-post-thumbnail-image { + flex-shrink: 0; + width: 120px; + height: 120px; + border-radius: 8px; + overflow: hidden; + background-color: var(--bg-secondary); +} + +.related-post-thumbnail-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.related-post-thumbnail-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.related-post-thumbnail-title { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); + margin: 0; + line-height: 1.3; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.related-post-thumbnail-excerpt { + font-size: 0.9375rem; + color: var(--text-secondary); + margin: 0; + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.related-post-thumbnail-meta { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.8125rem; + color: var(--text-muted); + margin-top: auto; +} + +.related-post-thumbnail-author-image { + width: 24px; + height: 24px; + border-radius: 50%; + object-fit: cover; +} + +.related-post-thumbnail-author { + font-weight: 500; + color: var(--text-secondary); + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.02em; +} + +.related-post-thumbnail-date { + color: var(--text-muted); +} + +/* Mobile responsive styles for related posts thumbnails */ +@media (max-width: 480px) { + .related-post-thumbnail-image { + width: 100px; + height: 100px; + } + + .related-post-thumbnail-title { + font-size: 1rem; + } + + .related-post-thumbnail-excerpt { + font-size: 0.875rem; + -webkit-line-clamp: 2; + } +} + /* Loading and error states */ .loading, .no-posts, @@ -9286,6 +9435,12 @@ body { color: var(--text-primary); } +.dashboard-action-btn:disabled { + opacity: 0.4; + cursor: not-allowed; + pointer-events: none; +} + .dashboard-action-btn.primary { background: var(--text-primary); color: var(--bg-primary); @@ -9439,6 +9594,30 @@ body { max-height: 100vh; } +/* Hide scrollbars but keep scroll functionality for dashboard */ +.dashboard-sidebar-left, +.dashboard-nav, +.dashboard-content, +.dashboard-sidebar-right, +.dashboard-frontmatter-fields { + -ms-overflow-style: none; /* IE/old Edge */ + scrollbar-width: none; /* Firefox */ +} + +.dashboard-sidebar-left::-webkit-scrollbar, +.dashboard-nav::-webkit-scrollbar, +.dashboard-content::-webkit-scrollbar, +.dashboard-sidebar-right::-webkit-scrollbar, +.dashboard-frontmatter-fields::-webkit-scrollbar { + width: 0; + height: 0; +} + +/* Prevent outer page scroll on dashboard pages */ +body:has(.dashboard-layout) { + overflow: hidden; +} + .dashboard-frontmatter { display: flex; flex-direction: column; @@ -10968,7 +11147,7 @@ body { border-right: none; } -/* Quill Editor Styles */ +/* React Quill Editor Styles */ .dashboard-quill-container { flex: 1; display: flex; @@ -10985,25 +11164,6 @@ body { overflow: hidden; } -.dashboard-quill-container .ql-container { - flex: 1; - overflow: auto; - font-family: var(--font-family-base); - font-size: var(--font-size-base); -} - -.dashboard-quill-container .ql-editor { - min-height: 100%; - padding: 1rem; - color: var(--text-primary); - background: var(--bg-secondary); -} - -.dashboard-quill-container .ql-editor.ql-blank::before { - color: var(--text-tertiary); - font-style: normal; -} - .dashboard-quill-container .ql-toolbar { background: var(--bg-primary); border: none; @@ -11024,29 +11184,146 @@ body { } .dashboard-quill-container .ql-toolbar button:hover .ql-stroke, -.dashboard-quill-container .ql-toolbar button.ql-active .ql-stroke { +.dashboard-quill-container .ql-toolbar .ql-picker:hover .ql-stroke { stroke: var(--text-primary); } .dashboard-quill-container .ql-toolbar button:hover .ql-fill, -.dashboard-quill-container .ql-toolbar button.ql-active .ql-fill { +.dashboard-quill-container .ql-toolbar .ql-picker:hover .ql-fill { fill: var(--text-primary); } -.dashboard-quill-container .ql-toolbar .ql-picker-label:hover, -.dashboard-quill-container .ql-toolbar .ql-picker-label.ql-active { - color: var(--text-primary); +.dashboard-quill-container .ql-toolbar button.ql-active .ql-stroke, +.dashboard-quill-container .ql-toolbar .ql-picker.ql-active .ql-stroke { + stroke: var(--accent-color); } -.dashboard-quill-container .ql-toolbar .ql-picker-options { - background: var(--bg-primary); - border-color: var(--border-color); +.dashboard-quill-container .ql-toolbar button.ql-active .ql-fill, +.dashboard-quill-container .ql-toolbar .ql-picker.ql-active .ql-fill { + fill: var(--accent-color); } .dashboard-quill-container .ql-container { + flex: 1; + overflow: auto; + font-family: var(--font-family-base); + font-size: var(--font-size-base); border: none; } +.dashboard-quill-container .ql-editor { + min-height: 100%; + padding: 1rem; + color: var(--text-primary); + background: var(--bg-secondary); +} + +.dashboard-quill-container .ql-editor.ql-blank::before { + color: var(--text-tertiary); + font-style: normal; +} + +.dashboard-quill-container .ql-editor h1 { + font-size: 1.75rem; + font-weight: 700; + margin: 1.5rem 0 0.75rem; + line-height: 1.2; +} + +.dashboard-quill-container .ql-editor h2 { + font-size: 1.5rem; + font-weight: 600; + margin: 1.25rem 0 0.625rem; + line-height: 1.3; +} + +.dashboard-quill-container .ql-editor h3 { + font-size: 1.25rem; + font-weight: 600; + margin: 1rem 0 0.5rem; + line-height: 1.4; +} + +.dashboard-quill-container .ql-editor p { + margin: 0 0 1rem; + line-height: 1.6; +} + +.dashboard-quill-container .ql-editor blockquote { + border-left: 3px solid var(--border-color); + margin: 1rem 0; + padding-left: 1rem; + color: var(--text-secondary); + font-style: italic; +} + +.dashboard-quill-container .ql-editor pre.ql-syntax { + background: var(--bg-tertiary); + color: var(--text-primary); + border-radius: var(--border-radius-sm); + padding: 1rem; + overflow-x: auto; +} + +.dashboard-quill-container .ql-editor ul, +.dashboard-quill-container .ql-editor ol { + margin: 0.5rem 0 1rem 1.5rem; + padding: 0; +} + +.dashboard-quill-container .ql-editor li { + margin: 0.25rem 0; + line-height: 1.6; +} + +.dashboard-quill-container .ql-editor a { + color: var(--accent-color); + text-decoration: underline; +} + +.dashboard-quill-container .ql-editor img { + max-width: 100%; + height: auto; + border-radius: var(--border-radius-sm); + margin: 1rem 0; +} + +/* Quill picker dropdown styling */ +.dashboard-quill-container .ql-picker-options { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); +} + +.dashboard-quill-container .ql-picker-item { + color: var(--text-secondary); +} + +.dashboard-quill-container .ql-picker-item:hover { + color: var(--text-primary); +} + +/* Quill tooltip styling */ +.dashboard-quill-container .ql-tooltip { + background: var(--bg-primary); + border: 1px solid var(--border-color); + color: var(--text-primary); + box-shadow: var(--shadow-lg); + border-radius: var(--border-radius-sm); +} + +.dashboard-quill-container .ql-tooltip input[type="text"] { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + color: var(--text-primary); + border-radius: var(--border-radius-sm); +} + +.dashboard-quill-container .ql-tooltip a.ql-action, +.dashboard-quill-container .ql-tooltip a.ql-remove { + color: var(--accent-color); +} + .dashboard-write-type-selector { display: flex; gap: 0.5rem; @@ -11720,7 +11997,8 @@ body { ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: var(--font-size-sm); color: var(--text-primary); - word-break: break-all; + white-space: nowrap; + overflow-x: auto; } .dashboard-modal-copy-btn { @@ -11841,6 +12119,69 @@ body { line-height: 1.5; } +.dashboard-modal-copy-prompt { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 1rem; + margin-top: 1rem; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-md); +} + +.dashboard-modal-copy-prompt-text { + display: flex; + align-items: flex-start; + gap: 0.5rem; + font-size: var(--font-size-sm); + color: var(--text-secondary); + line-height: 1.4; +} + +.dashboard-modal-copy-prompt-text svg { + flex-shrink: 0; + color: var(--accent-color); + margin-top: 0.125rem; +} + +.dashboard-modal-copy-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + width: 100%; + padding: 0.625rem 1rem; + background: var(--bg-primary); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); + font-size: var(--font-size-sm); + font-weight: 500; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease; +} + +.dashboard-modal-copy-btn:hover:not(:disabled) { + background: var(--bg-tertiary); + border-color: var(--text-tertiary); +} + +.dashboard-modal-copy-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.dashboard-modal-copy-btn.copied { + background: #22c55e; + border-color: #22c55e; + color: #fff; +} + +.dashboard-modal-copy-btn svg { + flex-shrink: 0; +} + .dashboard-modal-btn.danger { display: flex; align-items: center;