From 6b776733d5e726e33fa4f4fee8527ad056139179 Mon Sep 17 00:00:00 2001 From: Wayne Sutton Date: Thu, 25 Dec 2025 12:17:27 -0800 Subject: [PATCH] feat: add font family configuration system with monospace option --- README.md | 6 ++ TASK.md | 12 +++- changelog.md | 33 +++++++++ content/blog/setup-guide.md | 29 +++++++- content/pages/changelog-page.md | 24 +++++++ content/pages/docs.md | 16 ++++- convex/search.ts | 75 ++++++++++++++++++--- files.md | 5 +- fork-config.json.example | 3 +- public/raw/changelog.md | 1 + public/raw/docs.md | 16 ++++- public/raw/setup-guide.md | 29 +++++++- scripts/configure-fork.ts | 9 +++ src/components/SearchModal.tsx | 11 +-- src/config/siteConfig.ts | 14 +++- src/context/FontContext.tsx | 115 ++++++++++++++++++++++++++++++++ src/main.tsx | 5 +- src/pages/Write.tsx | 42 +++++++++--- src/styles/global.css | 13 ++-- 19 files changed, 421 insertions(+), 37 deletions(-) create mode 100644 src/context/FontContext.tsx diff --git a/README.md b/README.md index efde30f..94fd4fc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # markdown "sync" framework +![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) +![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg) +![React](https://img.shields.io/badge/React-18-61dafb.svg) +![Convex](https://img.shields.io/badge/Convex-enabled-ff6b6b.svg) +![Netlify](https://img.shields.io/badge/Netlify-hosted-00C7B7.svg) + An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify. Write markdown locally, run `npm run sync` (dev) or `npm run sync:prod` (production), and content appears instantly across all connected browsers. Built with React, Convex, and Vite. Optimized for AEO, GEO, and LLM discovery. diff --git a/TASK.md b/TASK.md index 5783e1e..c8e6ec7 100644 --- a/TASK.md +++ b/TASK.md @@ -8,10 +8,20 @@ ## Current Status -v1.28.2 ready. Fixed text wrapping for plain text code blocks. +v1.29.0 ready. Added font family configuration system with monospace option. ## Completed +- [x] Font family configuration system with siteConfig integration +- [x] Added FontContext.tsx for global font state management +- [x] Monospace font option added to FONT SWITCHER (IBM Plex Mono) +- [x] CSS variable --font-family for dynamic font updates +- [x] Write page font switcher updated to support serif/sans/monospace +- [x] Fork configuration support for fontFamily option +- [x] Documentation updated (setup-guide.md, docs.md) +- [x] Font preference persistence with localStorage +- [x] SiteConfig default font detection and override logic + - [x] Plain text code blocks now wrap text properly instead of horizontal overflow - [x] Updated inline vs block code detection logic in BlogPost.tsx - [x] Added `pre-wrap` styling for text blocks via SyntaxHighlighter props diff --git a/changelog.md b/changelog.md index a7e0571..d41542c 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,39 @@ 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/). +## [1.29.0] - 2025-12-25 + +### Added + +- Font family configuration system + - Added `fontFamily` option to `siteConfig.ts` with three options: "serif" (New York), "sans" (system fonts), "monospace" (IBM Plex Mono) + - Created `FontContext.tsx` for global font state management with localStorage persistence + - Font preference persists across page reloads + - SiteConfig default font is respected and overrides localStorage when siteConfig changes + - CSS variable `--font-family` dynamically updates based on selected font +- Monospace font option + - Added monospace font family to FONT SWITCHER options in `global.css` + - Monospace uses "IBM Plex Mono", "Liberation Mono", ui-monospace, monospace + - Write page font switcher now supports all three font options (serif/sans/monospace) +- Fork configuration support for fontFamily + - Added `fontFamily` field to `fork-config.json.example` + - Updated `configure-fork.ts` to handle fontFamily configuration + +### Changed + +- `src/styles/global.css`: Updated body font-family to use CSS variable `--font-family` with fallback +- `src/main.tsx`: Added FontProvider wrapper around app +- `src/pages/Write.tsx`: Updated font switcher to cycle through serif/sans/monospace options +- `content/blog/setup-guide.md`: Updated font configuration documentation with siteConfig option +- `content/pages/docs.md`: Updated font configuration documentation + +### Technical + +- New context: `src/context/FontContext.tsx` with `useFont()` hook +- Font detection logic compares siteConfig default with localStorage to detect changes +- CSS variable updates synchronously on mount for immediate font application +- Write page font state syncs with global font on initial load + ## [1.28.2] - 2025-12-25 ### Fixed diff --git a/content/blog/setup-guide.md b/content/blog/setup-guide.md index bf94096..128b3d8 100644 --- a/content/blog/setup-guide.md +++ b/content/blog/setup-guide.md @@ -938,7 +938,22 @@ const DEFAULT_THEME: Theme = "tan"; // Options: "dark", "light", "tan", "cloud" ### Change the Font -The blog uses a serif font by default. To switch to sans-serif, edit `src/styles/global.css`: +The blog uses a serif font by default. You can configure the font in two ways: + +**Option 1: Configure via siteConfig.ts (Recommended)** + +Edit `src/config/siteConfig.ts`: + +```typescript +export const siteConfig: SiteConfig = { + // ... other config + fontFamily: "serif", // Options: "serif", "sans", or "monospace" +}; +``` + +**Option 2: Edit global.css directly** + +Edit `src/styles/global.css`: ```css body { @@ -953,9 +968,21 @@ body { ui-serif, Georgia, serif; + + /* Monospace */ + font-family: + "IBM Plex Mono", + "Liberation Mono", + ui-monospace, + monospace; } ``` +Available font options: +- `serif`: New York serif font (default) +- `sans`: System sans-serif fonts +- `monospace`: IBM Plex Mono monospace font + ### Change Font Sizes All font sizes use CSS variables defined in `:root`. Customize sizes by editing these variables in `src/styles/global.css`: diff --git a/content/pages/changelog-page.md b/content/pages/changelog-page.md index 0c06ad9..b146224 100644 --- a/content/pages/changelog-page.md +++ b/content/pages/changelog-page.md @@ -7,6 +7,30 @@ layout: "sidebar" --- All notable changes to this project. +![](https://img.shields.io/badge/License-MIT-yellow.svg) + +## v1.29.0 + +Released December 25, 2025 + +**Font family configuration system** + +- Font family configuration via siteConfig.ts + - Three font options: "serif" (New York), "sans" (system fonts), "monospace" (IBM Plex Mono) + - Configure default font in `src/config/siteConfig.ts` with `fontFamily` option + - Font preference persists in localStorage across page reloads + - SiteConfig default font overrides localStorage when siteConfig changes +- Monospace font option added + - Added monospace to FONT SWITCHER options in global.css + - Uses "IBM Plex Mono", "Liberation Mono", ui-monospace, monospace + - Write page font switcher now cycles through all three options +- Fork configuration support + - Added fontFamily field to fork-config.json.example + - Automated fork configuration script supports fontFamily option + +Updated files: `src/config/siteConfig.ts`, `src/context/FontContext.tsx`, `src/main.tsx`, `src/pages/Write.tsx`, `src/styles/global.css`, `scripts/configure-fork.ts`, `fork-config.json.example` + +Documentation updated: `content/blog/setup-guide.md`, `content/pages/docs.md`, `files.md` ## v1.28.2 diff --git a/content/pages/docs.md b/content/pages/docs.md index 8ed9869..62ef9b1 100644 --- a/content/pages/docs.md +++ b/content/pages/docs.md @@ -627,7 +627,16 @@ const DEFAULT_THEME: Theme = "tan"; ### Font -Edit `src/styles/global.css`: +Configure the font in `src/config/siteConfig.ts`: + +```typescript +export const siteConfig: SiteConfig = { + // ... other config + fontFamily: "serif", // Options: "serif", "sans", or "monospace" +}; +``` + +Or edit `src/styles/global.css` directly: ```css body { @@ -637,9 +646,14 @@ body { /* Serif (default) */ font-family: "New York", ui-serif, Georgia, serif; + + /* Monospace */ + font-family: "IBM Plex Mono", "Liberation Mono", ui-monospace, monospace; } ``` +Available options: `serif` (default), `sans`, or `monospace`. + ### Font Sizes All font sizes use CSS variables in `:root`. Customize by editing: diff --git a/convex/search.ts b/convex/search.ts index de39933..c6fe9a9 100644 --- a/convex/search.ts +++ b/convex/search.ts @@ -9,6 +9,7 @@ const searchResultValidator = v.object({ title: v.string(), description: v.optional(v.string()), snippet: v.string(), + anchor: v.optional(v.string()), // Anchor ID for scrolling to exact match location }); // Search across posts and pages @@ -30,6 +31,7 @@ export const search = query({ title: string; description?: string; snippet: string; + anchor?: string; }> = []; // Search posts by title @@ -70,8 +72,8 @@ export const search = query({ if (seenPostIds.has(post._id)) continue; seenPostIds.add(post._id); - // Create snippet from content - const snippet = createSnippet(post.content, args.query, 120); + // Create snippet from content and find anchor + const { snippet, anchor } = createSnippet(post.content, args.query, 120); results.push({ _id: post._id, @@ -80,6 +82,7 @@ export const search = query({ title: post.title, description: post.description, snippet, + anchor: anchor || undefined, }); } @@ -89,8 +92,8 @@ export const search = query({ if (seenPageIds.has(page._id)) continue; seenPageIds.add(page._id); - // Create snippet from content - const snippet = createSnippet(page.content, args.query, 120); + // Create snippet from content and find anchor + const { snippet, anchor } = createSnippet(page.content, args.query, 120); results.push({ _id: page._id, @@ -98,6 +101,7 @@ export const search = query({ slug: page.slug, title: page.title, snippet, + anchor: anchor || undefined, }); } @@ -116,12 +120,63 @@ export const search = query({ }, }); -// Helper to create a snippet around the search term +// Generate slug from heading text (same as frontend) +function generateSlug(text: string): string { + return text + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") + .trim(); +} + +// Find the nearest heading before a match position in the original content +function findNearestHeading(content: string, matchPosition: number): string | null { + const lines = content.split("\n"); + const headings: Array<{ text: string; position: number; id: string }> = []; + let currentPosition = 0; + + // Find all headings with their positions + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); + + if (headingMatch) { + const text = headingMatch[2].trim(); + const id = generateSlug(text); + headings.push({ text, position: currentPosition, id }); + } + + // Add line length + newline to position + currentPosition += line.length + 1; + } + + // Find the last heading before the match position + let nearestHeading: typeof headings[0] | null = null; + for (const heading of headings) { + if (heading.position <= matchPosition) { + nearestHeading = heading; + } else { + break; + } + } + + return nearestHeading?.id || null; +} + +// Helper to create a snippet around the search term and find anchor function createSnippet( content: string, searchTerm: string, maxLength: number -): string { +): { snippet: string; anchor: string | null } { + const lowerSearchTerm = searchTerm.toLowerCase(); + + // Find the first occurrence in the original content for anchor lookup + // This finds the match position before we clean the content + const originalIndex = content.toLowerCase().indexOf(lowerSearchTerm); + const anchor = originalIndex !== -1 ? findNearestHeading(content, originalIndex) : null; + // Remove markdown syntax for cleaner snippets const cleanContent = content .replace(/#{1,6}\s/g, "") // Headers @@ -136,12 +191,14 @@ function createSnippet( .trim(); const lowerContent = cleanContent.toLowerCase(); - const lowerSearchTerm = searchTerm.toLowerCase(); const index = lowerContent.indexOf(lowerSearchTerm); if (index === -1) { // Term not found, return beginning of content - return cleanContent.slice(0, maxLength) + (cleanContent.length > maxLength ? "..." : ""); + return { + snippet: cleanContent.slice(0, maxLength) + (cleanContent.length > maxLength ? "..." : ""), + anchor: null, + }; } // Calculate start position to center the search term @@ -154,6 +211,6 @@ function createSnippet( if (start > 0) snippet = "..." + snippet; if (end < cleanContent.length) snippet = snippet + "..."; - return snippet; + return { snippet, anchor }; } diff --git a/files.md b/files.md index 6c3efa3..f045e78 100644 --- a/files.md +++ b/files.md @@ -33,7 +33,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, GitHub contributions, nav order, inner page logo settings, hardcoded navigation items for React routes, GitHub repository config for AI service raw URLs) | +| `siteConfig.ts` | Centralized site configuration (name, logo, blog page, posts display with homepage post limit and read more link, 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) | ### Pages (`src/pages/`) @@ -44,7 +44,7 @@ A brief description of each file in the codebase. | `Post.tsx` | Individual blog post or page view with optional sidebar layout. Includes back button, CopyPageDropdown, tag links, and related posts section in footer for blog posts (update SITE_URL/SITE_NAME when forking) | | `Stats.tsx` | Real-time analytics dashboard with visitor stats and GitHub stars | | `TagPage.tsx` | Tag archive page displaying posts filtered by a specific tag. Includes view mode toggle (list/cards) with localStorage persistence | -| `Write.tsx` | Three-column markdown writing page with Cursor docs-style UI, frontmatter reference with copy buttons, theme toggle, font switcher (serif/sans-serif), and localStorage persistence (not linked in nav) | +| `Write.tsx` | Three-column markdown writing page with Cursor docs-style UI, frontmatter reference with copy buttons, theme toggle, font switcher (serif/sans/monospace), and localStorage persistence (not linked in nav) | ### Components (`src/components/`) @@ -69,6 +69,7 @@ A brief description of each file in the codebase. | File | Description | | ------------------ | ---------------------------------------------------- | | `ThemeContext.tsx` | Theme state management with localStorage persistence | +| `FontContext.tsx` | Font family state management (serif/sans/monospace) with localStorage persistence and siteConfig integration | | `SidebarContext.tsx` | Shares sidebar headings and active ID between Post and Layout components for mobile menu integration | ### Utils (`src/utils/`) diff --git a/fork-config.json.example b/fork-config.json.example index 665d72e..57e0627 100644 --- a/fork-config.json.example +++ b/fork-config.json.example @@ -49,6 +49,7 @@ }, "featuredViewMode": "cards", "showViewToggle": true, - "theme": "tan" + "theme": "tan", + "fontFamily": "serif" } diff --git a/public/raw/changelog.md b/public/raw/changelog.md index 67c998c..a7505b7 100644 --- a/public/raw/changelog.md +++ b/public/raw/changelog.md @@ -6,6 +6,7 @@ Date: 2025-12-25 --- All notable changes to this project. +![](https://img.shields.io/badge/License-MIT-yellow.svg) ## v1.28.2 diff --git a/public/raw/docs.md b/public/raw/docs.md index 2bd6ed8..f936c18 100644 --- a/public/raw/docs.md +++ b/public/raw/docs.md @@ -626,7 +626,16 @@ const DEFAULT_THEME: Theme = "tan"; ### Font -Edit `src/styles/global.css`: +Configure the font in `src/config/siteConfig.ts`: + +```typescript +export const siteConfig: SiteConfig = { + // ... other config + fontFamily: "serif", // Options: "serif", "sans", or "monospace" +}; +``` + +Or edit `src/styles/global.css` directly: ```css body { @@ -636,9 +645,14 @@ body { /* Serif (default) */ font-family: "New York", ui-serif, Georgia, serif; + + /* Monospace */ + font-family: "IBM Plex Mono", "Liberation Mono", ui-monospace, monospace; } ``` +Available options: `serif` (default), `sans`, or `monospace`. + ### Font Sizes All font sizes use CSS variables in `:root`. Customize by editing: diff --git a/public/raw/setup-guide.md b/public/raw/setup-guide.md index 3e0852a..631b182 100644 --- a/public/raw/setup-guide.md +++ b/public/raw/setup-guide.md @@ -932,7 +932,22 @@ const DEFAULT_THEME: Theme = "tan"; // Options: "dark", "light", "tan", "cloud" ### Change the Font -The blog uses a serif font by default. To switch to sans-serif, edit `src/styles/global.css`: +The blog uses a serif font by default. You can configure the font in two ways: + +**Option 1: Configure via siteConfig.ts (Recommended)** + +Edit `src/config/siteConfig.ts`: + +```typescript +export const siteConfig: SiteConfig = { + // ... other config + fontFamily: "serif", // Options: "serif", "sans", or "monospace" +}; +``` + +**Option 2: Edit global.css directly** + +Edit `src/styles/global.css`: ```css body { @@ -947,9 +962,21 @@ body { ui-serif, Georgia, serif; + + /* Monospace */ + font-family: + "IBM Plex Mono", + "Liberation Mono", + ui-monospace, + monospace; } ``` +Available font options: +- `serif`: New York serif font (default) +- `sans`: System sans-serif fonts +- `monospace`: IBM Plex Mono monospace font + ### Change Font Sizes All font sizes use CSS variables defined in `:root`. Customize sizes by editing these variables in `src/styles/global.css`: diff --git a/scripts/configure-fork.ts b/scripts/configure-fork.ts index fb3bd9d..cea0c50 100644 --- a/scripts/configure-fork.ts +++ b/scripts/configure-fork.ts @@ -72,6 +72,7 @@ interface ForkConfig { featuredViewMode?: "cards" | "list"; showViewToggle?: boolean; theme?: "dark" | "light" | "tan" | "cloud"; + fontFamily?: "serif" | "sans" | "monospace"; } // Get project root directory @@ -252,6 +253,14 @@ function updateSiteConfig(config: ForkConfig): void { ); } + // Update fontFamily if specified + if (config.fontFamily) { + content = content.replace( + /fontFamily: ['"](?:serif|sans|monospace)['"],\s*\/\/ Options: "serif", "sans", or "monospace"/, + `fontFamily: "${config.fontFamily}", // Options: "serif", "sans", or "monospace"`, + ); + } + // Update gitHubRepo config (for AI service raw URLs) // Support both new gitHubRepoConfig and legacy githubUsername/githubRepo fields const gitHubRepoOwner = diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index 0259974..3304f9e 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -52,7 +52,9 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) { case "Enter": e.preventDefault(); if (results[selectedIndex]) { - navigate(`/${results[selectedIndex].slug}`); + const result = results[selectedIndex]; + const url = result.anchor ? `/${result.slug}#${result.anchor}` : `/${result.slug}`; + navigate(url); onClose(); } break; @@ -66,8 +68,9 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) { ); // Handle clicking on a result - const handleResultClick = (slug: string) => { - navigate(`/${slug}`); + const handleResultClick = (slug: string, anchor?: string) => { + const url = anchor ? `/${slug}#${anchor}` : `/${slug}`; + navigate(url); onClose(); }; @@ -133,7 +136,7 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
  • diff --git a/src/styles/global.css b/src/styles/global.css index f7d583d..6df0446 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -279,10 +279,12 @@ html { body { /* FONT SWITCHER: Replace the font-family below to change fonts - Sans-serif (default): -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif - Use "New York" for a serif font. - Serif (New York): "New York", -apple-system-ui-serif, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif */ - font-family: + Sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif + Serif (New York): "New York", -apple-system-ui-serif, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif + Monospace: "IBM Plex Mono", "Liberation Mono", ui-monospace, monospace + Or configure via siteConfig.ts fontFamily option */ + font-family: var( + --font-family, "New York", -apple-system-ui-serif, ui-serif, @@ -290,7 +292,8 @@ body { Cambria, "Times New Roman", Times, - serif; + serif + ); background-color: var(--bg-primary); color: var(--text-primary); line-height: 1.6;