mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-11 20:08:57 +00:00
feat: add /write page with three-column layout, font switcher, frontmatter reference, and localStorage persistence
This commit is contained in:
40
README.md
40
README.md
@@ -29,6 +29,7 @@ npm run sync:prod # production
|
||||
- Logo gallery with continuous marquee scroll
|
||||
- Static raw markdown files at `/raw/{slug}.md`
|
||||
- Dedicated blog page with configurable navigation order
|
||||
- Markdown writing page at `/write` with frontmatter reference
|
||||
|
||||
### SEO and Discovery
|
||||
|
||||
@@ -571,6 +572,45 @@ body {
|
||||
|
||||
Replace the `font-family` property with your preferred font stack.
|
||||
|
||||
### Font Sizes
|
||||
|
||||
All font sizes use CSS variables defined in `:root`. Customize sizes by editing the variables:
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Base size scale */
|
||||
--font-size-base: 16px;
|
||||
--font-size-sm: 13px;
|
||||
--font-size-lg: 17px;
|
||||
--font-size-xl: 18px;
|
||||
--font-size-2xl: 20px;
|
||||
--font-size-3xl: 24px;
|
||||
|
||||
/* Component-specific (examples) */
|
||||
--font-size-blog-content: 17px;
|
||||
--font-size-post-title: 32px;
|
||||
--font-size-nav-link: 14px;
|
||||
}
|
||||
```
|
||||
|
||||
Mobile responsive sizes are defined in a `@media (max-width: 768px)` block with smaller values.
|
||||
|
||||
## Write Page
|
||||
|
||||
A public markdown writing page at `/write` (not linked in navigation). Features:
|
||||
|
||||
- Three-column Cursor docs-style layout
|
||||
- Content type selector (Blog Post or Page) with dynamic frontmatter templates
|
||||
- Frontmatter reference panel with copy buttons for each field
|
||||
- Font switcher (Serif/Sans-serif) with localStorage persistence
|
||||
- Theme toggle matching the site themes (Moon, Sun, Half2Icon, Cloud)
|
||||
- Word, line, and character counts
|
||||
- localStorage persistence for content, content type, and font preference
|
||||
- Works with Grammarly and browser spellcheck
|
||||
- Warning message about refresh losing content
|
||||
|
||||
Access directly at `yourdomain.com/write`. Content is stored in localStorage only (not synced to database). Use it to draft posts, then copy the content to a markdown file in `content/blog/` or `content/pages/` and run `npm run sync`.
|
||||
|
||||
## Source
|
||||
|
||||
Fork this project: [github.com/waynesutton/markdown-site](https://github.com/waynesutton/markdown-site)
|
||||
|
||||
32
TASK.md
32
TASK.md
@@ -2,17 +2,43 @@
|
||||
|
||||
## To Do
|
||||
|
||||
- [ ] Add markdown write page with copy option
|
||||
- [ ] add github code block
|
||||
- [ ] create a ui site config page
|
||||
- [ ] create a prompt formator or skill or agent to change everything at once after forking
|
||||
- [ ] create a prompt formator or checklidst or skill or agent to change everything at once after forking
|
||||
|
||||
## Current Status
|
||||
|
||||
v1.12.1 deployed. OG images now use post/page image from frontmatter instead of always defaulting.
|
||||
v1.16.0 deployed. Added public /write page with three-column Cursor docs-style layout, font switcher, theme toggle, and localStorage persistence for markdown writing.
|
||||
|
||||
## Completed
|
||||
|
||||
- [x] Public /write page with three-column layout (not linked in nav)
|
||||
- [x] Left sidebar: Home link, content type selector, actions (Clear, Theme, Font)
|
||||
- [x] Center: Writing area with Copy All button and borderless textarea
|
||||
- [x] Right sidebar: Frontmatter reference with per-field copy buttons
|
||||
- [x] Font switcher to toggle between Serif and Sans-serif fonts
|
||||
- [x] Font preference persistence in localStorage
|
||||
- [x] Theme toggle icons matching ThemeToggle.tsx (Moon, Sun, Half2Icon, Cloud)
|
||||
- [x] Content type switching (Blog Post/Page) updates writing area template
|
||||
- [x] Word, line, and character counts in status bar
|
||||
- [x] Warning banner about refresh losing content
|
||||
- [x] localStorage persistence for content, type, and font
|
||||
- [x] Redesign /write page with three-column Cursor docs-style layout
|
||||
- [x] Add per-field copy icons to frontmatter reference panel
|
||||
- [x] Add refresh warning message in left sidebar
|
||||
- [x] Left sidebar with home link, content type selector, and actions
|
||||
- [x] Right sidebar with frontmatter fields and copy buttons
|
||||
- [x] Center area with title, Copy All button, and borderless textarea
|
||||
- [x] Theme toggle with matching icons for all four themes
|
||||
- [x] Redesign /write page with wider layout and modern Notion-like UI
|
||||
- [x] Remove header from /write page (standalone writing experience)
|
||||
- [x] Add inline theme toggle and home link to Write page toolbar
|
||||
- [x] Collapsible frontmatter fields panel
|
||||
- [x] Add markdown write page with copy option at /write
|
||||
- [x] Centralized font-size CSS variables in global.css
|
||||
- [x] Base size scale with semantic naming (3xs to hero)
|
||||
- [x] Component-specific font-size variables
|
||||
- [x] Mobile responsive font-size overrides
|
||||
- [x] Open Graph image fix for posts and pages with frontmatter images
|
||||
- [x] Dedicated blog page with configurable display options
|
||||
- [x] Blog page navigation order via siteConfig.blogPage.order
|
||||
|
||||
146
changelog.md
146
changelog.md
@@ -4,6 +4,152 @@ 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.16.0] - 2025-12-21
|
||||
|
||||
### Added
|
||||
|
||||
- Public markdown writing page at `/write` (not linked in navigation)
|
||||
- Three-column Cursor docs-style layout
|
||||
- Left sidebar: Home link, content type selector (Blog Post/Page), actions (Clear, Theme, Font)
|
||||
- Center: Full-height writing area with title, Copy All button, and borderless textarea
|
||||
- Right sidebar: Frontmatter reference with copy icon for each field
|
||||
- Font switcher in Actions section
|
||||
- Toggle between Serif and Sans-serif fonts
|
||||
- Font preference saved to localStorage
|
||||
- Theme toggle matching the rest of the app (Moon, Sun, Half2Icon, Cloud)
|
||||
- localStorage persistence for content, type, and font preference
|
||||
- Word, line, and character counts in status bar
|
||||
- Warning banner: "Refresh loses content"
|
||||
- Grammarly and browser spellcheck compatible
|
||||
- Works with all four themes (dark, light, tan, cloud)
|
||||
|
||||
### Technical
|
||||
|
||||
- New component: `src/pages/Write.tsx`
|
||||
- Route: `/write` (added to `src/App.tsx`)
|
||||
- Three localStorage keys: `markdown_write_content`, `markdown_write_type`, `markdown_write_font`
|
||||
- CSS Grid layout (220px | 1fr | 280px)
|
||||
- Uses Phosphor icons: House, Article, File, Trash, CopySimple, Warning, Check
|
||||
- Uses lucide-react and radix-ui icons for theme toggle (consistent with ThemeToggle.tsx)
|
||||
|
||||
## [1.15.1] - 2025-12-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- Theme toggle icons on `/write` page now match `ThemeToggle.tsx` component
|
||||
- dark: Moon icon (lucide-react)
|
||||
- light: Sun icon (lucide-react)
|
||||
- tan: Half2Icon (radix-ui) - consistent with rest of app
|
||||
- cloud: Cloud icon (lucide-react)
|
||||
- Content type switching (Blog Post/Page) now always updates writing area template
|
||||
|
||||
### Technical
|
||||
|
||||
- Replaced Phosphor icons (Moon, Sun, Leaf, CloudSun) with lucide-react and radix-ui icons
|
||||
- `handleTypeChange` now always regenerates template when switching types
|
||||
|
||||
## [1.15.0] - 2025-12-21
|
||||
|
||||
### Changed
|
||||
|
||||
- Redesigned `/write` page with three-column Cursor docs-style layout
|
||||
- Left sidebar: Home link, content type selector (Blog Post/Page), actions (Clear, Theme)
|
||||
- Center: Full-height writing area with title, Copy All button, and borderless textarea
|
||||
- Right sidebar: Frontmatter reference with copy icon for each field
|
||||
- Frontmatter fields panel with per-field copy buttons
|
||||
- Each frontmatter field shows name, example value, and copy icon
|
||||
- Click to copy individual field syntax to clipboard
|
||||
- Required fields marked with red asterisk
|
||||
- Fields update dynamically when switching between Blog Post and Page
|
||||
- Warning banner for unsaved content
|
||||
- "Refresh loses content" warning in left sidebar with warning icon
|
||||
- Helps users remember localStorage persistence limitations
|
||||
- Enhanced status bar
|
||||
- Word, line, and character counts in sticky footer
|
||||
- Save hint with content directory path
|
||||
|
||||
### Technical
|
||||
|
||||
- Three-column CSS Grid layout (220px sidebar | 1fr main | 280px right sidebar)
|
||||
- Theme toggle cycles through dark, light, tan, cloud with matching icons
|
||||
- Collapsible sidebars on mobile (stacked layout)
|
||||
- Uses Phosphor icons: House, Article, File, Trash, CopySimple, Warning, Check
|
||||
|
||||
## [1.14.0] - 2025-12-20
|
||||
|
||||
### Changed
|
||||
|
||||
- Redesigned `/write` page with Notion-like minimal UI
|
||||
- Full-screen distraction-free writing experience
|
||||
- Removed site header for focused writing environment
|
||||
- Wider writing area (900px max-width centered)
|
||||
- Borderless textarea with transparent background
|
||||
- Own minimal header with home link, type selector, and icon buttons
|
||||
- Improved toolbar design
|
||||
- Home icon link to return to main site
|
||||
- Clean dropdown for content type selection (no borders)
|
||||
- Collapsible frontmatter fields panel (hidden by default)
|
||||
- Theme toggle in toolbar (cycles through dark, light, tan, cloud)
|
||||
- Icon buttons with subtle hover states
|
||||
- Copy button with inverted theme colors
|
||||
- Enhanced status bar
|
||||
- Sticky footer with word/line/character counts
|
||||
- Save hint with content directory path
|
||||
- Dot separators between stats
|
||||
|
||||
### Technical
|
||||
|
||||
- Write page now renders without Layout component wrapper
|
||||
- Added Phosphor icons: House, Sun, Moon, CloudSun, Leaf, Info, X
|
||||
- CSS restructured for minimal aesthetic (`.write-wrapper`, `.write-header`, etc.)
|
||||
- Mobile responsive with hidden copy text and save hint on small screens
|
||||
|
||||
## [1.13.0] - 2025-12-20
|
||||
|
||||
### Added
|
||||
|
||||
- Public markdown writing page at `/write` (not linked in navigation)
|
||||
- Dropdown to select between "Blog Post" and "Page" content types
|
||||
- Frontmatter fields reference panel with required/optional indicators
|
||||
- Copy button using Phosphor CopySimple icon
|
||||
- Clear button to reset content to template
|
||||
- Status bar showing lines, words, and characters count
|
||||
- Usage hint with instructions for saving content
|
||||
- localStorage persistence for writing session
|
||||
- Content persists across page refreshes within same browser
|
||||
- Each browser has isolated content (session privacy)
|
||||
- Content type selection saved separately
|
||||
- Auto-generated frontmatter templates
|
||||
- Blog post template with all common fields
|
||||
- Page template with navigation fields
|
||||
- Current date auto-populated in templates
|
||||
|
||||
### Technical
|
||||
|
||||
- New component: `src/pages/Write.tsx`
|
||||
- Route: `/write` (added to `src/App.tsx`)
|
||||
- CSS styles added to `src/styles/global.css`
|
||||
- Works with all four themes (dark, light, tan, cloud)
|
||||
- Plain textarea for Grammarly and browser spellcheck compatibility
|
||||
- Mobile responsive design with adjusted layout for smaller screens
|
||||
- No Convex backend required (localStorage only)
|
||||
|
||||
## [1.12.2] - 2025-12-20
|
||||
|
||||
### Added
|
||||
|
||||
- Centralized font-size configuration using CSS variables in `global.css`
|
||||
- Base size scale from 10px to 64px with semantic names
|
||||
- Component-specific variables for consistent sizing
|
||||
- Mobile responsive overrides at 768px breakpoint
|
||||
- All hardcoded font sizes converted to CSS variables for easier customization
|
||||
|
||||
### Technical
|
||||
|
||||
- Font sizes defined in `:root` selector with `--font-size-*` naming convention
|
||||
- Mobile breakpoint uses same variables with smaller values
|
||||
- Base scale: 3xs (10px), 2xs (11px), xs (12px), sm (13px), md (14px), base (16px), lg (17px), xl (18px), 2xl (20px), 3xl (24px), 4xl (28px), 5xl (32px), 6xl (36px), hero (64px)
|
||||
|
||||
## [1.12.1] - 2025-12-20
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -703,6 +703,29 @@ body {
|
||||
}
|
||||
```
|
||||
|
||||
### Change Font Sizes
|
||||
|
||||
All font sizes use CSS variables defined in `:root`. Customize sizes by editing these variables in `src/styles/global.css`:
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Base size scale */
|
||||
--font-size-base: 16px;
|
||||
--font-size-sm: 13px;
|
||||
--font-size-lg: 17px;
|
||||
--font-size-xl: 18px;
|
||||
--font-size-2xl: 20px;
|
||||
--font-size-3xl: 24px;
|
||||
|
||||
/* Component-specific (examples) */
|
||||
--font-size-blog-content: 17px;
|
||||
--font-size-post-title: 32px;
|
||||
--font-size-nav-link: 14px;
|
||||
}
|
||||
```
|
||||
|
||||
Mobile responsive sizes are defined in a `@media (max-width: 768px)` block.
|
||||
|
||||
### Add Static Pages (Optional)
|
||||
|
||||
Create optional pages like About, Projects, or Contact. These appear as navigation links in the top right corner.
|
||||
@@ -944,6 +967,32 @@ markdown-site/
|
||||
└── package.json # Dependencies
|
||||
```
|
||||
|
||||
## Write Page
|
||||
|
||||
A markdown writing page is available at `/write` (not linked in navigation). Use it to draft content before saving to your markdown files.
|
||||
|
||||
**Features:**
|
||||
|
||||
- Three-column Cursor docs-style layout
|
||||
- Content type selector (Blog Post or Page) with dynamic frontmatter templates
|
||||
- Frontmatter field reference with individual copy buttons
|
||||
- Font switcher (Serif/Sans-serif)
|
||||
- Theme toggle matching site themes
|
||||
- Word, line, and character counts
|
||||
- localStorage persistence for content, type, and font preference
|
||||
- Works with Grammarly and browser spellcheck
|
||||
|
||||
**Workflow:**
|
||||
|
||||
1. Go to `yourdomain.com/write`
|
||||
2. Select content type (Blog Post or Page)
|
||||
3. Write your content using the frontmatter reference
|
||||
4. Click "Copy All" to copy the markdown
|
||||
5. Save to `content/blog/` or `content/pages/`
|
||||
6. Run `npm run sync` or `npm run sync:prod`
|
||||
|
||||
Content is stored in localStorage only and not synced to the database. Refreshing the page preserves your content, but clearing browser data will lose it.
|
||||
|
||||
## Next Steps
|
||||
|
||||
After deploying:
|
||||
|
||||
@@ -63,6 +63,7 @@ It's a hybrid: developer workflow for publishing + real-time delivery like a dyn
|
||||
- Copy to ChatGPT, Claude, and Perplexity sharing
|
||||
- Generate Skill option for AI agent training
|
||||
- View as Markdown option in share dropdown
|
||||
- Markdown writing page at `/write` with frontmatter reference
|
||||
|
||||
## Who this is for
|
||||
|
||||
|
||||
@@ -7,6 +7,82 @@ order: 5
|
||||
|
||||
All notable changes to this project.
|
||||
|
||||
## v1.15.2
|
||||
|
||||
Released December 20, 2025
|
||||
|
||||
**Write page font switcher**
|
||||
|
||||
- Font switcher in `/write` page Actions section
|
||||
- Toggle between Serif and Sans-serif fonts in the writing area
|
||||
- Font preference saved to localStorage and persists across sessions
|
||||
- Uses same font families defined in global.css
|
||||
|
||||
## v1.15.1
|
||||
|
||||
Released December 20, 2025
|
||||
|
||||
**Write page theme and content fixes**
|
||||
|
||||
- Fixed theme toggle icons on `/write` page to match `ThemeToggle.tsx` (Moon, Sun, Half2Icon, Cloud)
|
||||
- Content type switching now always updates the template in the writing area
|
||||
|
||||
## v1.15.0
|
||||
|
||||
Released December 20, 2025
|
||||
|
||||
**Write page three-column layout**
|
||||
|
||||
- Redesigned `/write` page with Cursor docs-style three-column layout
|
||||
- Left sidebar: content type selector (Blog Post/Page) and action buttons (Clear, Theme)
|
||||
- Center: full-screen writing area with Copy All button
|
||||
- Right sidebar: frontmatter field reference with individual copy buttons for each field
|
||||
- Warning message about refresh losing content
|
||||
- Stats bar showing words, lines, and characters
|
||||
|
||||
## v1.14.0
|
||||
|
||||
Released December 20, 2025
|
||||
|
||||
**Write page Notion-like UI**
|
||||
|
||||
- Redesigned `/write` page with full-screen, distraction-free writing experience
|
||||
- Floating header with home link, type selector, and action buttons
|
||||
- Collapsible frontmatter panel on the right
|
||||
- Removed borders from writing area for cleaner look
|
||||
- Improved typography and spacing
|
||||
|
||||
## v1.13.0
|
||||
|
||||
Released December 20, 2025
|
||||
|
||||
**Markdown write page**
|
||||
|
||||
- New `/write` page for drafting markdown content (not linked in navigation)
|
||||
- Content type selector for Blog Post or Page with appropriate frontmatter templates
|
||||
- Frontmatter reference with copy buttons for each field
|
||||
- Theme toggle matching site themes
|
||||
- Word, line, and character counts
|
||||
- localStorage persistence for content, type, and font preference
|
||||
- Works with Grammarly and browser spellcheck
|
||||
- Copy all button for easy content transfer
|
||||
- Clear button to reset content
|
||||
|
||||
Access at `yourdomain.com/write`. Content stored in localStorage only.
|
||||
|
||||
## v1.12.2
|
||||
|
||||
Released December 20, 2025
|
||||
|
||||
**Centralized font-size CSS variables**
|
||||
|
||||
- All font sizes now use CSS variables for easier customization
|
||||
- Base scale from `--font-size-3xs` (10px) to `--font-size-hero` (64px)
|
||||
- Component-specific variables for blog headings, navigation, search, stats, and more
|
||||
- Mobile responsive overrides at 768px breakpoint
|
||||
|
||||
Edit `src/styles/global.css` to customize font sizes across the entire site by changing the `:root` variables.
|
||||
|
||||
## v1.12.1
|
||||
|
||||
Released December 20, 2025
|
||||
|
||||
@@ -394,6 +394,22 @@ body {
|
||||
}
|
||||
```
|
||||
|
||||
### Font Sizes
|
||||
|
||||
All font sizes use CSS variables in `:root`. Customize by editing:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--font-size-base: 16px;
|
||||
--font-size-sm: 13px;
|
||||
--font-size-lg: 17px;
|
||||
--font-size-blog-content: 17px;
|
||||
--font-size-post-title: 32px;
|
||||
}
|
||||
```
|
||||
|
||||
Mobile sizes defined in `@media (max-width: 768px)` block.
|
||||
|
||||
### Images
|
||||
|
||||
| Image | Location | Size |
|
||||
|
||||
7
files.md
7
files.md
@@ -41,6 +41,7 @@ A brief description of each file in the codebase.
|
||||
| `Blog.tsx` | Dedicated blog page with post list (configurable via siteConfig.blogPage) |
|
||||
| `Post.tsx` | Individual blog post view (update SITE_URL/SITE_NAME when forking) |
|
||||
| `Stats.tsx` | Real-time analytics dashboard with visitor stats |
|
||||
| `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) |
|
||||
|
||||
### Components (`src/components/`)
|
||||
|
||||
@@ -71,9 +72,9 @@ A brief description of each file in the codebase.
|
||||
|
||||
### Styles (`src/styles/`)
|
||||
|
||||
| File | Description |
|
||||
| ------------ | ---------------------------------------------------------------- |
|
||||
| `global.css` | Global CSS with theme variables, font config for all four themes |
|
||||
| File | Description |
|
||||
| ------------ | ------------------------------------------------------------------------------------ |
|
||||
| `global.css` | Global CSS with theme variables, centralized font-size CSS variables for all themes |
|
||||
|
||||
## Convex Backend (`convex/`)
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ It's a hybrid: developer workflow for publishing + real-time delivery like a dyn
|
||||
- Copy to ChatGPT, Claude, and Perplexity sharing
|
||||
- Generate Skill option for AI agent training
|
||||
- View as Markdown option in share dropdown
|
||||
- Markdown writing page at `/write` with frontmatter reference
|
||||
|
||||
## Who this is for
|
||||
|
||||
|
||||
@@ -7,6 +7,82 @@ Date: 2025-12-21
|
||||
|
||||
All notable changes to this project.
|
||||
|
||||
## v1.15.2
|
||||
|
||||
Released December 20, 2025
|
||||
|
||||
**Write page font switcher**
|
||||
|
||||
- Font switcher in `/write` page Actions section
|
||||
- Toggle between Serif and Sans-serif fonts in the writing area
|
||||
- Font preference saved to localStorage and persists across sessions
|
||||
- Uses same font families defined in global.css
|
||||
|
||||
## v1.15.1
|
||||
|
||||
Released December 20, 2025
|
||||
|
||||
**Write page theme and content fixes**
|
||||
|
||||
- Fixed theme toggle icons on `/write` page to match `ThemeToggle.tsx` (Moon, Sun, Half2Icon, Cloud)
|
||||
- Content type switching now always updates the template in the writing area
|
||||
|
||||
## v1.15.0
|
||||
|
||||
Released December 20, 2025
|
||||
|
||||
**Write page three-column layout**
|
||||
|
||||
- Redesigned `/write` page with Cursor docs-style three-column layout
|
||||
- Left sidebar: content type selector (Blog Post/Page) and action buttons (Clear, Theme)
|
||||
- Center: full-screen writing area with Copy All button
|
||||
- Right sidebar: frontmatter field reference with individual copy buttons for each field
|
||||
- Warning message about refresh losing content
|
||||
- Stats bar showing words, lines, and characters
|
||||
|
||||
## v1.14.0
|
||||
|
||||
Released December 20, 2025
|
||||
|
||||
**Write page Notion-like UI**
|
||||
|
||||
- Redesigned `/write` page with full-screen, distraction-free writing experience
|
||||
- Floating header with home link, type selector, and action buttons
|
||||
- Collapsible frontmatter panel on the right
|
||||
- Removed borders from writing area for cleaner look
|
||||
- Improved typography and spacing
|
||||
|
||||
## v1.13.0
|
||||
|
||||
Released December 20, 2025
|
||||
|
||||
**Markdown write page**
|
||||
|
||||
- New `/write` page for drafting markdown content (not linked in navigation)
|
||||
- Content type selector for Blog Post or Page with appropriate frontmatter templates
|
||||
- Frontmatter reference with copy buttons for each field
|
||||
- Theme toggle matching site themes
|
||||
- Word, line, and character counts
|
||||
- localStorage persistence for content, type, and font preference
|
||||
- Works with Grammarly and browser spellcheck
|
||||
- Copy all button for easy content transfer
|
||||
- Clear button to reset content
|
||||
|
||||
Access at `yourdomain.com/write`. Content stored in localStorage only.
|
||||
|
||||
## v1.12.2
|
||||
|
||||
Released December 20, 2025
|
||||
|
||||
**Centralized font-size CSS variables**
|
||||
|
||||
- All font sizes now use CSS variables for easier customization
|
||||
- Base scale from `--font-size-3xs` (10px) to `--font-size-hero` (64px)
|
||||
- Component-specific variables for blog headings, navigation, search, stats, and more
|
||||
- Mobile responsive overrides at 768px breakpoint
|
||||
|
||||
Edit `src/styles/global.css` to customize font sizes across the entire site by changing the `:root` variables.
|
||||
|
||||
## v1.12.1
|
||||
|
||||
Released December 20, 2025
|
||||
|
||||
@@ -394,6 +394,22 @@ body {
|
||||
}
|
||||
```
|
||||
|
||||
### Font Sizes
|
||||
|
||||
All font sizes use CSS variables in `:root`. Customize by editing:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--font-size-base: 16px;
|
||||
--font-size-sm: 13px;
|
||||
--font-size-lg: 17px;
|
||||
--font-size-blog-content: 17px;
|
||||
--font-size-post-title: 32px;
|
||||
}
|
||||
```
|
||||
|
||||
Mobile sizes defined in `@media (max-width: 768px)` block.
|
||||
|
||||
### Images
|
||||
|
||||
| Image | Location | Size |
|
||||
|
||||
@@ -700,6 +700,29 @@ body {
|
||||
}
|
||||
```
|
||||
|
||||
### Change Font Sizes
|
||||
|
||||
All font sizes use CSS variables defined in `:root`. Customize sizes by editing these variables in `src/styles/global.css`:
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Base size scale */
|
||||
--font-size-base: 16px;
|
||||
--font-size-sm: 13px;
|
||||
--font-size-lg: 17px;
|
||||
--font-size-xl: 18px;
|
||||
--font-size-2xl: 20px;
|
||||
--font-size-3xl: 24px;
|
||||
|
||||
/* Component-specific (examples) */
|
||||
--font-size-blog-content: 17px;
|
||||
--font-size-post-title: 32px;
|
||||
--font-size-nav-link: 14px;
|
||||
}
|
||||
```
|
||||
|
||||
Mobile responsive sizes are defined in a `@media (max-width: 768px)` block.
|
||||
|
||||
### Add Static Pages (Optional)
|
||||
|
||||
Create optional pages like About, Projects, or Contact. These appear as navigation links in the top right corner.
|
||||
@@ -941,6 +964,32 @@ markdown-site/
|
||||
└── package.json # Dependencies
|
||||
```
|
||||
|
||||
## Write Page
|
||||
|
||||
A markdown writing page is available at `/write` (not linked in navigation). Use it to draft content before saving to your markdown files.
|
||||
|
||||
**Features:**
|
||||
|
||||
- Three-column Cursor docs-style layout
|
||||
- Content type selector (Blog Post or Page) with dynamic frontmatter templates
|
||||
- Frontmatter field reference with individual copy buttons
|
||||
- Font switcher (Serif/Sans-serif)
|
||||
- Theme toggle matching site themes
|
||||
- Word, line, and character counts
|
||||
- localStorage persistence for content, type, and font preference
|
||||
- Works with Grammarly and browser spellcheck
|
||||
|
||||
**Workflow:**
|
||||
|
||||
1. Go to `yourdomain.com/write`
|
||||
2. Select content type (Blog Post or Page)
|
||||
3. Write your content using the frontmatter reference
|
||||
4. Click "Copy All" to copy the markdown
|
||||
5. Save to `content/blog/` or `content/pages/`
|
||||
6. Run `npm run sync` or `npm run sync:prod`
|
||||
|
||||
Content is stored in localStorage only and not synced to the database. Refreshing the page preserves your content, but clearing browser data will lose it.
|
||||
|
||||
## Next Steps
|
||||
|
||||
After deploying:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { Routes, Route, useLocation } from "react-router-dom";
|
||||
import Home from "./pages/Home";
|
||||
import Post from "./pages/Post";
|
||||
import Stats from "./pages/Stats";
|
||||
import Blog from "./pages/Blog";
|
||||
import Write from "./pages/Write";
|
||||
import Layout from "./components/Layout";
|
||||
import { usePageTracking } from "./hooks/usePageTracking";
|
||||
import siteConfig from "./config/siteConfig";
|
||||
@@ -10,6 +11,12 @@ import siteConfig from "./config/siteConfig";
|
||||
function App() {
|
||||
// Track page views and active sessions
|
||||
usePageTracking();
|
||||
const location = useLocation();
|
||||
|
||||
// Write page renders without Layout (no header, full-screen writing)
|
||||
if (location.pathname === "/write") {
|
||||
return <Write />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
|
||||
411
src/pages/Write.tsx
Normal file
411
src/pages/Write.tsx
Normal file
@@ -0,0 +1,411 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
CopySimple,
|
||||
Check,
|
||||
Trash,
|
||||
House,
|
||||
Article,
|
||||
File,
|
||||
Warning,
|
||||
TextAa,
|
||||
} from "@phosphor-icons/react";
|
||||
import { Moon, Sun, Cloud } from "lucide-react";
|
||||
import { Half2Icon } from "@radix-ui/react-icons";
|
||||
import { useTheme } from "../context/ThemeContext";
|
||||
|
||||
// Frontmatter field definitions for blog posts
|
||||
const POST_FIELDS = [
|
||||
{ name: "title", required: true, example: '"Your Post Title"' },
|
||||
{
|
||||
name: "description",
|
||||
required: true,
|
||||
example: '"A brief description for SEO"',
|
||||
},
|
||||
{ name: "date", required: true, example: '"2025-01-20"' },
|
||||
{ name: "slug", required: true, example: '"your-post-url"' },
|
||||
{ name: "published", required: true, example: "true" },
|
||||
{ name: "tags", required: true, example: '["tag1", "tag2"]' },
|
||||
{ name: "readTime", required: false, example: '"5 min read"' },
|
||||
{ name: "image", required: false, example: '"/images/my-image.png"' },
|
||||
{
|
||||
name: "excerpt",
|
||||
required: false,
|
||||
example: '"Short description for cards"',
|
||||
},
|
||||
{ name: "featured", required: false, example: "true" },
|
||||
{ name: "featuredOrder", required: false, example: "1" },
|
||||
];
|
||||
|
||||
// Frontmatter field definitions for pages
|
||||
const PAGE_FIELDS = [
|
||||
{ name: "title", required: true, example: '"Page Title"' },
|
||||
{ name: "slug", required: true, example: '"page-url"' },
|
||||
{ name: "published", required: true, example: "true" },
|
||||
{ name: "order", required: false, example: "1" },
|
||||
{ name: "excerpt", required: false, example: '"Short description"' },
|
||||
{ name: "image", required: false, example: '"/images/thumbnail.png"' },
|
||||
{ name: "featured", required: false, example: "true" },
|
||||
{ name: "featuredOrder", required: false, example: "1" },
|
||||
];
|
||||
|
||||
// Generate frontmatter template based on content type
|
||||
function generateTemplate(type: "post" | "page"): string {
|
||||
if (type === "post") {
|
||||
return `---
|
||||
title: "Your Post Title"
|
||||
description: "A brief description for SEO and social sharing"
|
||||
date: "${new Date().toISOString().split("T")[0]}"
|
||||
slug: "your-post-url"
|
||||
published: true
|
||||
tags: ["tag1", "tag2"]
|
||||
readTime: "5 min read"
|
||||
---
|
||||
|
||||
# Your Post Title
|
||||
|
||||
Start writing your content here...
|
||||
|
||||
## Section Heading
|
||||
|
||||
Add your markdown content. You can use:
|
||||
|
||||
- **Bold text** and *italic text*
|
||||
- [Links](https://example.com)
|
||||
- Code blocks with syntax highlighting
|
||||
|
||||
\`\`\`typescript
|
||||
const greeting = "Hello, world";
|
||||
console.log(greeting);
|
||||
\`\`\`
|
||||
|
||||
## Conclusion
|
||||
|
||||
Wrap up your thoughts here.
|
||||
`;
|
||||
}
|
||||
|
||||
return `---
|
||||
title: "Page Title"
|
||||
slug: "page-url"
|
||||
published: true
|
||||
order: 1
|
||||
---
|
||||
|
||||
# Page Title
|
||||
|
||||
Your page content goes here...
|
||||
|
||||
## Section
|
||||
|
||||
Add your markdown content.
|
||||
`;
|
||||
}
|
||||
|
||||
// localStorage keys
|
||||
const STORAGE_KEY_CONTENT = "markdown_write_content";
|
||||
const STORAGE_KEY_TYPE = "markdown_write_type";
|
||||
const STORAGE_KEY_FONT = "markdown_write_font";
|
||||
|
||||
// Font family definitions (matches global.css options)
|
||||
type FontType = "serif" | "sans";
|
||||
const FONTS: Record<FontType, string> = {
|
||||
serif:
|
||||
'"New York", -apple-system-ui-serif, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
|
||||
sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif',
|
||||
};
|
||||
|
||||
// Get the appropriate icon for current theme (matches ThemeToggle.tsx)
|
||||
function getThemeIcon(theme: string) {
|
||||
switch (theme) {
|
||||
case "dark":
|
||||
return <Moon size={18} />;
|
||||
case "light":
|
||||
return <Sun size={18} />;
|
||||
case "tan":
|
||||
return <Half2Icon style={{ width: 18, height: 18 }} />;
|
||||
case "cloud":
|
||||
return <Cloud size={18} />;
|
||||
default:
|
||||
return <Sun size={18} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default function Write() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const [contentType, setContentType] = useState<"post" | "page">("post");
|
||||
const [content, setContent] = useState("");
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [copiedField, setCopiedField] = useState<string | null>(null);
|
||||
const [font, setFont] = useState<FontType>("serif");
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// Load from localStorage on mount
|
||||
useEffect(() => {
|
||||
const savedContent = localStorage.getItem(STORAGE_KEY_CONTENT);
|
||||
const savedType = localStorage.getItem(STORAGE_KEY_TYPE) as
|
||||
| "post"
|
||||
| "page"
|
||||
| null;
|
||||
const savedFont = localStorage.getItem(STORAGE_KEY_FONT) as FontType | null;
|
||||
|
||||
if (savedContent) {
|
||||
setContent(savedContent);
|
||||
} else {
|
||||
setContent(generateTemplate("post"));
|
||||
}
|
||||
|
||||
if (savedType) {
|
||||
setContentType(savedType);
|
||||
}
|
||||
|
||||
if (savedFont && (savedFont === "serif" || savedFont === "sans")) {
|
||||
setFont(savedFont);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Save to localStorage on content change
|
||||
useEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEY_CONTENT, content);
|
||||
}, [content]);
|
||||
|
||||
// Save type to localStorage
|
||||
useEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEY_TYPE, contentType);
|
||||
}, [contentType]);
|
||||
|
||||
// Save font preference to localStorage
|
||||
useEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEY_FONT, font);
|
||||
}, [font]);
|
||||
|
||||
// Toggle font between serif and sans-serif
|
||||
const toggleFont = useCallback(() => {
|
||||
setFont((prev) => (prev === "serif" ? "sans" : "serif"));
|
||||
}, []);
|
||||
|
||||
// Handle type change and update content template
|
||||
const handleTypeChange = (newType: "post" | "page") => {
|
||||
if (newType === contentType) return;
|
||||
|
||||
setContentType(newType);
|
||||
// Always update to the new template when switching types
|
||||
setContent(generateTemplate(newType));
|
||||
};
|
||||
|
||||
// Copy content to clipboard
|
||||
const handleCopy = useCallback(async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = content;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
// Copy a single frontmatter field to clipboard
|
||||
const handleCopyField = useCallback(
|
||||
async (fieldName: string, example: string) => {
|
||||
const fieldText = `${fieldName}: ${example}`;
|
||||
try {
|
||||
await navigator.clipboard.writeText(fieldText);
|
||||
setCopiedField(fieldName);
|
||||
setTimeout(() => setCopiedField(null), 1500);
|
||||
} catch {
|
||||
// Fallback
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = fieldText;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
setCopiedField(fieldName);
|
||||
setTimeout(() => setCopiedField(null), 1500);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Clear content and reset to template
|
||||
const handleClear = useCallback(() => {
|
||||
setContent(generateTemplate(contentType));
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
}
|
||||
}, [contentType]);
|
||||
|
||||
// Calculate stats
|
||||
const lines = content.split("\n").length;
|
||||
const characters = content.length;
|
||||
const words = content.trim() ? content.trim().split(/\s+/).length : 0;
|
||||
|
||||
const fields = contentType === "post" ? POST_FIELDS : PAGE_FIELDS;
|
||||
|
||||
return (
|
||||
<div className="write-layout">
|
||||
{/* Left Sidebar: Type selector */}
|
||||
<aside className="write-sidebar-left">
|
||||
<div className="write-sidebar-header">
|
||||
<Link to="/" className="write-logo-link" title="Back to home">
|
||||
<House size={20} weight="regular" />
|
||||
<span>Home</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<nav className="write-nav">
|
||||
<div className="write-nav-section">
|
||||
<span className="write-nav-label">Content Type</span>
|
||||
<button
|
||||
onClick={() => handleTypeChange("post")}
|
||||
className={`write-nav-item ${contentType === "post" ? "active" : ""}`}
|
||||
>
|
||||
<Article
|
||||
size={18}
|
||||
weight={contentType === "post" ? "fill" : "regular"}
|
||||
/>
|
||||
<span>Blog Post</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTypeChange("page")}
|
||||
className={`write-nav-item ${contentType === "page" ? "active" : ""}`}
|
||||
>
|
||||
<File
|
||||
size={18}
|
||||
weight={contentType === "page" ? "fill" : "regular"}
|
||||
/>
|
||||
<span>Page</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="write-nav-section">
|
||||
<span className="write-nav-label">Actions</span>
|
||||
<button onClick={handleClear} className="write-nav-item">
|
||||
<Trash size={18} />
|
||||
<span>Clear</span>
|
||||
</button>
|
||||
<button onClick={toggleTheme} className="write-nav-item">
|
||||
{getThemeIcon(theme)}
|
||||
<span>Theme</span>
|
||||
</button>
|
||||
<button onClick={toggleFont} className="write-nav-item">
|
||||
<TextAa size={18} />
|
||||
<span>{font === "serif" ? "Serif" : "Sans"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Warning about refresh */}
|
||||
<div className="write-warning">
|
||||
<Warning size={14} />
|
||||
<span>Refresh loses content</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main writing area */}
|
||||
<main className="write-main">
|
||||
<div className="write-main-header">
|
||||
<h1 className="write-main-title">
|
||||
{contentType === "post" ? "Blog Post" : "Page"}
|
||||
</h1>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className={`write-copy-btn ${copied ? "copied" : ""}`}
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check size={16} weight="bold" />
|
||||
<span>Copied</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CopySimple size={16} />
|
||||
<span>Copy All</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
className="write-textarea"
|
||||
placeholder="Start writing your markdown..."
|
||||
spellCheck={true}
|
||||
autoComplete="off"
|
||||
autoCapitalize="sentences"
|
||||
autoFocus
|
||||
style={{ fontFamily: FONTS[font] }}
|
||||
/>
|
||||
|
||||
{/* Footer with stats */}
|
||||
<div className="write-main-footer">
|
||||
<div className="write-stats">
|
||||
<span>{words} words</span>
|
||||
<span className="write-stats-divider" />
|
||||
<span>{lines} lines</span>
|
||||
<span className="write-stats-divider" />
|
||||
<span>{characters} chars</span>
|
||||
</div>
|
||||
<div className="write-save-hint">
|
||||
Save to{" "}
|
||||
<code>content/{contentType === "post" ? "blog" : "pages"}/</code>{" "}
|
||||
then <code>npm run sync</code>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Right Sidebar: Frontmatter fields */}
|
||||
<aside className="write-sidebar-right">
|
||||
<div className="write-sidebar-header">
|
||||
<span className="write-sidebar-title">Frontmatter</span>
|
||||
</div>
|
||||
|
||||
<div className="write-fields">
|
||||
<div className="write-fields-section">
|
||||
<span className="write-fields-label">
|
||||
{contentType === "post" ? "Blog Post" : "Page"} Fields
|
||||
</span>
|
||||
{fields.map((field) => (
|
||||
<div key={field.name} className="write-field-row">
|
||||
<div className="write-field-info">
|
||||
<code className="write-field-name">
|
||||
{field.name}
|
||||
{field.required && (
|
||||
<span className="write-field-required">*</span>
|
||||
)}
|
||||
</code>
|
||||
<span className="write-field-example">{field.example}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleCopyField(field.name, field.example)}
|
||||
className={`write-field-copy ${copiedField === field.name ? "copied" : ""}`}
|
||||
title={`Copy ${field.name}`}
|
||||
>
|
||||
{copiedField === field.name ? (
|
||||
<Check size={14} weight="bold" />
|
||||
) : (
|
||||
<CopySimple size={14} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="write-fields-note">
|
||||
<span className="write-field-required">*</span> Required fields
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user