mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-11 20:08:57 +00:00
Export as PDF, Core Web Vitals performance optimizations, Enhanced diff code block rendering and blog post example on codeblocks
This commit is contained in:
27
TASK.md
27
TASK.md
@@ -4,10 +4,35 @@
|
|||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
v2.12.0 ready. Canonical URL fix for GitHub Issue #6 implemented.
|
v2.15.0 ready. Export as PDF feature added to CopyPageDropdown.
|
||||||
|
|
||||||
## Completed
|
## Completed
|
||||||
|
|
||||||
|
- [x] Export as PDF option in CopyPageDropdown
|
||||||
|
- [x] Added browser print dialog for saving pages as PDF
|
||||||
|
- [x] Clean formatted output with markdown syntax stripped
|
||||||
|
- [x] Title as heading, metadata on single line, readable content
|
||||||
|
- [x] Uses Phosphor FilePdf icon (already installed)
|
||||||
|
- [x] Positioned at end of dropdown menu
|
||||||
|
- [x] Added formatForPrint function and handleExportPDF handler
|
||||||
|
- [x] Updated files.md, changelog.md, task.md documentation
|
||||||
|
|
||||||
|
- [x] Core Web Vitals performance optimizations
|
||||||
|
- [x] Fixed non-composited animations in visitor map (SVG r to transform: scale)
|
||||||
|
- [x] Removed 5 duplicate @keyframes spin definitions
|
||||||
|
- [x] Added will-change hints to animated elements
|
||||||
|
- [x] Inlined critical CSS in index.html for faster first paint
|
||||||
|
- [x] Added preconnect hints for convex.site
|
||||||
|
|
||||||
|
- [x] Enhanced diff code block rendering with @pierre/diffs
|
||||||
|
- [x] Added @pierre/diffs package for Shiki-based diff visualization
|
||||||
|
- [x] Created DiffCodeBlock component with unified/split view toggle
|
||||||
|
- [x] Updated BlogPost.tsx to route diff/patch blocks to new renderer
|
||||||
|
- [x] Added theme-aware CSS styles for diff blocks
|
||||||
|
- [x] Added vendor-diffs chunk to Vite config for code splitting
|
||||||
|
- [x] Created "How to Use Code Blocks" blog post with examples
|
||||||
|
- [x] Updated files.md with DiffCodeBlock documentation
|
||||||
|
|
||||||
- [x] Canonical URL mismatch fix (GitHub Issue #6)
|
- [x] Canonical URL mismatch fix (GitHub Issue #6)
|
||||||
- [x] Raw HTML was serving homepage canonical instead of page-specific canonical
|
- [x] Raw HTML was serving homepage canonical instead of page-specific canonical
|
||||||
- [x] Added SEARCH_ENGINE_BOTS array to botMeta.ts for search engine crawler detection
|
- [x] Added SEARCH_ENGINE_BOTS array to botMeta.ts for search engine crawler detection
|
||||||
|
|||||||
71
changelog.md
71
changelog.md
@@ -4,6 +4,77 @@ 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/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## [2.15.0] - 2026-01-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Export as PDF option in CopyPageDropdown
|
||||||
|
- Browser-based print dialog for saving pages as PDF
|
||||||
|
- Clean formatted output (no markdown syntax visible)
|
||||||
|
- Title displayed as proper heading
|
||||||
|
- Metadata shown as clean line (date, read time, tags)
|
||||||
|
- Content with markdown stripped for readable document
|
||||||
|
- Uses Phosphor FilePdf icon
|
||||||
|
- Positioned at end of dropdown menu
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Added `formatForPrint` function to strip markdown syntax from content
|
||||||
|
- Added `handleExportPDF` handler with styled print window
|
||||||
|
- Imports `FilePdf` from `@phosphor-icons/react` (already installed)
|
||||||
|
|
||||||
|
## [2.14.0] - 2026-01-07
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Core Web Vitals performance optimizations
|
||||||
|
- Fixed non-composited animations in visitor map (SVG `r` attribute changed to `transform: scale()`)
|
||||||
|
- Removed 5 duplicate `@keyframes spin` definitions from global.css
|
||||||
|
- Added `will-change` hints to animated elements for GPU compositing
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Critical CSS inlined in index.html for faster first paint
|
||||||
|
- Theme variables (dark/light/tan/cloud)
|
||||||
|
- Reset and base body styles
|
||||||
|
- Layout skeleton and navigation styles
|
||||||
|
- Additional resource hints in index.html
|
||||||
|
- Preconnect to convex.site for faster API calls
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Updated `src/styles/global.css`:
|
||||||
|
- Converted visitor-pulse animations from SVG `r` to `transform: scale()` (GPU-composited)
|
||||||
|
- Added `transform-origin`, `transform-box`, and `will-change` to pulse ring elements
|
||||||
|
- Added `will-change` to `.theme-toggle`, `.copy-page-menu`, `.search-modal-backdrop`, `.scroll-to-top`
|
||||||
|
- Removed duplicate `@keyframes spin` at lines 9194, 10091, 10243, 10651, 13726
|
||||||
|
- Updated `src/components/VisitorMap.tsx`:
|
||||||
|
- Changed pulse ring `r` values from 12/8 to base value 5 (scaling handled by CSS)
|
||||||
|
- Updated `index.html`:
|
||||||
|
- Added inline critical CSS (~2KB) for instant first contentful paint
|
||||||
|
- Added preconnect/dns-prefetch for convex.site
|
||||||
|
|
||||||
|
## [2.13.0] - 2026-01-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Enhanced diff code block rendering with @pierre/diffs library
|
||||||
|
- Diff and patch code blocks now render with Shiki-based syntax highlighting
|
||||||
|
- Unified and split (side-by-side) view modes with toggle button
|
||||||
|
- Theme-aware colors (dark/light/tan/cloud support)
|
||||||
|
- Copy button for diff content
|
||||||
|
- Automatic routing: ```diff and ```patch blocks use enhanced renderer
|
||||||
|
- New blog post: "How to Use Code Blocks" with syntax highlighting and diff examples
|
||||||
|
- DiffCodeBlock component (`src/components/DiffCodeBlock.tsx`)
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Added `@pierre/diffs` package for enhanced diff visualization
|
||||||
|
- Updated `BlogPost.tsx` to route diff/patch language blocks to DiffCodeBlock
|
||||||
|
- Added diff block CSS styles to `global.css`
|
||||||
|
- Added `vendor-diffs` chunk to Vite config for code splitting
|
||||||
|
- Updated `files.md` with DiffCodeBlock documentation
|
||||||
|
|
||||||
## [2.12.0] - 2026-01-07
|
## [2.12.0] - 2026-01-07
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
222
content/blog/how-to-use-code-blocks.md
Normal file
222
content/blog/how-to-use-code-blocks.md
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
---
|
||||||
|
title: "How to Use Code Blocks"
|
||||||
|
description: "A guide to syntax highlighting, diff rendering, and code formatting in your markdown posts."
|
||||||
|
date: "2026-01-07"
|
||||||
|
slug: "how-to-use-code-blocks"
|
||||||
|
published: true
|
||||||
|
tags: ["tutorial", "markdown", "code", "syntax-highlighting"]
|
||||||
|
readTime: "4 min read"
|
||||||
|
featured: false
|
||||||
|
authorName: "Markdown"
|
||||||
|
authorImage: "/images/authors/markdown.png"
|
||||||
|
excerpt: "Learn how to add syntax-highlighted code blocks and enhanced diff views to your posts."
|
||||||
|
docsSection: true
|
||||||
|
docsSectionGroup: "Publishing"
|
||||||
|
docsSectionOrder: 4
|
||||||
|
docsSectionGroupOrder: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# How to Use Code Blocks
|
||||||
|
|
||||||
|
Code blocks are essential for technical writing. This guide covers standard syntax highlighting and enhanced diff rendering.
|
||||||
|
|
||||||
|
## Basic code blocks
|
||||||
|
|
||||||
|
Wrap code in triple backticks with a language identifier:
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
```javascript
|
||||||
|
function greet(name) {
|
||||||
|
return `Hello, ${name}!`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
This renders with syntax highlighting:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function greet(name) {
|
||||||
|
return `Hello, ${name}!`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported languages
|
||||||
|
|
||||||
|
Common languages with syntax highlighting:
|
||||||
|
|
||||||
|
| Language | Identifier |
|
||||||
|
| ---------- | -------------------- |
|
||||||
|
| JavaScript | `javascript` or `js` |
|
||||||
|
| TypeScript | `typescript` or `ts` |
|
||||||
|
| Python | `python` or `py` |
|
||||||
|
| Bash | `bash` or `shell` |
|
||||||
|
| JSON | `json` |
|
||||||
|
| CSS | `css` |
|
||||||
|
| SQL | `sql` |
|
||||||
|
| Go | `go` |
|
||||||
|
| Rust | `rust` |
|
||||||
|
| Markdown | `markdown` or `md` |
|
||||||
|
|
||||||
|
## Code block features
|
||||||
|
|
||||||
|
Every code block includes:
|
||||||
|
|
||||||
|
- **Language label** in the top right corner
|
||||||
|
- **Copy button** that appears on hover
|
||||||
|
- **Theme-aware colors** matching your selected theme
|
||||||
|
|
||||||
|
## Diff code blocks
|
||||||
|
|
||||||
|
For showing code changes, use the `diff` or `patch` language identifier. These render with enhanced diff visualization powered by @pierre/diffs.
|
||||||
|
|
||||||
|
### Basic diff example
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
```diff
|
||||||
|
--- a/config.js
|
||||||
|
+++ b/config.js
|
||||||
|
@@ -1,5 +1,5 @@
|
||||||
|
const config = {
|
||||||
|
- debug: true,
|
||||||
|
+ debug: false,
|
||||||
|
port: 3000
|
||||||
|
};
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
This renders as:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
--- a/config.js
|
||||||
|
+++ b/config.js
|
||||||
|
@@ -1,5 +1,5 @@
|
||||||
|
const config = {
|
||||||
|
- debug: true,
|
||||||
|
+ debug: false,
|
||||||
|
port: 3000
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-line changes
|
||||||
|
|
||||||
|
```diff
|
||||||
|
--- a/utils.ts
|
||||||
|
+++ b/utils.ts
|
||||||
|
@@ -10,12 +10,15 @@
|
||||||
|
export function formatDate(date: Date): string {
|
||||||
|
- return date.toLocaleDateString();
|
||||||
|
+ return date.toLocaleDateString('en-US', {
|
||||||
|
+ year: 'numeric',
|
||||||
|
+ month: 'long',
|
||||||
|
+ day: 'numeric'
|
||||||
|
+ });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseDate(str: string): Date {
|
||||||
|
- return new Date(str);
|
||||||
|
+ const parsed = new Date(str);
|
||||||
|
+ if (isNaN(parsed.getTime())) {
|
||||||
|
+ throw new Error('Invalid date string');
|
||||||
|
+ }
|
||||||
|
+ return parsed;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Diff view modes
|
||||||
|
|
||||||
|
Diff blocks include a view toggle button:
|
||||||
|
|
||||||
|
- **Unified view** (default): Shows changes in a single column with +/- indicators
|
||||||
|
- **Split view**: Shows old and new code side by side
|
||||||
|
|
||||||
|
Click the toggle button in the diff header to switch between views.
|
||||||
|
|
||||||
|
## Adding new functions
|
||||||
|
|
||||||
|
```diff
|
||||||
|
--- a/api.ts
|
||||||
|
+++ b/api.ts
|
||||||
|
@@ -5,6 +5,14 @@ export async function fetchUser(id: string) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
+export async function updateUser(id: string, data: UserUpdate) {
|
||||||
|
+ const response = await fetch(`/api/users/${id}`, {
|
||||||
|
+ method: 'PATCH',
|
||||||
|
+ body: JSON.stringify(data)
|
||||||
|
+ });
|
||||||
|
+ return response.json();
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
export async function deleteUser(id: string) {
|
||||||
|
return fetch(`/api/users/${id}`, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Removing code
|
||||||
|
|
||||||
|
```diff
|
||||||
|
--- a/legacy.js
|
||||||
|
+++ b/legacy.js
|
||||||
|
@@ -1,15 +1,8 @@
|
||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
-// Old middleware - no longer needed
|
||||||
|
-app.use((req, res, next) => {
|
||||||
|
- console.log('Request:', req.method, req.url);
|
||||||
|
- next();
|
||||||
|
-});
|
||||||
|
-
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.send('Hello World');
|
||||||
|
});
|
||||||
|
|
||||||
|
-// Deprecated route
|
||||||
|
-app.get('/old', (req, res) => res.redirect('/'));
|
||||||
|
-
|
||||||
|
app.listen(3000);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inline code
|
||||||
|
|
||||||
|
For inline code, use single backticks:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Run `npm install` to install dependencies.
|
||||||
|
```
|
||||||
|
|
||||||
|
Renders as: Run `npm install` to install dependencies.
|
||||||
|
|
||||||
|
Inline code is detected automatically when the content is short (under 80 characters) and has no newlines.
|
||||||
|
|
||||||
|
## Plain text blocks
|
||||||
|
|
||||||
|
Code blocks without a language identifier render as plain text with word wrapping:
|
||||||
|
|
||||||
|
```
|
||||||
|
This is a plain text block. It wraps long lines automatically
|
||||||
|
instead of requiring horizontal scrolling. Useful for logs,
|
||||||
|
output, or any text that isn't code.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
**Choose the right language**: Use the correct language identifier for accurate highlighting. TypeScript files should use `typescript`, not `javascript`.
|
||||||
|
|
||||||
|
**Keep examples focused**: Show only the relevant code. Long blocks lose readers.
|
||||||
|
|
||||||
|
**Use diffs for changes**: When explaining modifications to existing code, diff blocks clearly show what changed.
|
||||||
|
|
||||||
|
**Test your blocks**: Preview your post to verify syntax highlighting works correctly.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Block type | Use case |
|
||||||
|
| ------------------ | ---------------------------------------------- |
|
||||||
|
| Regular code block | Showing code snippets with syntax highlighting |
|
||||||
|
| Diff block | Showing code changes with additions/deletions |
|
||||||
|
| Plain text block | Logs, output, or non-code text |
|
||||||
|
| Inline code | Commands, function names, short references |
|
||||||
|
|
||||||
|
Code blocks make technical content readable. Use the right format for your content type.
|
||||||
@@ -11,6 +11,94 @@ docsSectionOrder: 4
|
|||||||
|
|
||||||
All notable changes to this project.
|
All notable changes to this project.
|
||||||
|
|
||||||
|
## v2.15.0
|
||||||
|
|
||||||
|
Released January 7, 2026
|
||||||
|
|
||||||
|
**Export as PDF**
|
||||||
|
|
||||||
|
Added PDF export option to CopyPageDropdown. Users can now export any blog post or page as a clean, formatted PDF document using the browser's native print dialog.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- Export as PDF button in Copy page dropdown (positioned at end of menu)
|
||||||
|
- Clean formatted output without markdown syntax
|
||||||
|
- Title displayed as proper heading
|
||||||
|
- Metadata shown on single line (date, read time, tags)
|
||||||
|
- Content with markdown stripped for readable document
|
||||||
|
- Uses Phosphor FilePdf icon
|
||||||
|
|
||||||
|
**Technical:**
|
||||||
|
|
||||||
|
- Added `formatForPrint` function to strip markdown syntax (headings, bold, italic, code, links, blockquotes)
|
||||||
|
- Added `handleExportPDF` handler that opens styled print window
|
||||||
|
- Imports `FilePdf` from `@phosphor-icons/react` (already installed in project)
|
||||||
|
|
||||||
|
**Files changed:**
|
||||||
|
|
||||||
|
- `src/components/CopyPageDropdown.tsx` - New PDF export functionality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.14.0
|
||||||
|
|
||||||
|
Released January 7, 2026
|
||||||
|
|
||||||
|
**Core Web Vitals performance optimizations**
|
||||||
|
|
||||||
|
Fixes for PageSpeed Insights failures on mobile and desktop. These changes improve Largest Contentful Paint (LCP) and eliminate non-composited animation warnings.
|
||||||
|
|
||||||
|
**Fixes:**
|
||||||
|
|
||||||
|
- Non-composited animations: Visitor map pulse animations now use GPU-composited `transform: scale()` instead of animating SVG `r` attribute
|
||||||
|
- Duplicate keyframes: Removed 5 redundant `@keyframes spin` definitions from CSS
|
||||||
|
- GPU compositing hints: Added `will-change` to animated elements (theme toggle, dropdown menus, modals, scroll-to-top button)
|
||||||
|
|
||||||
|
**Performance additions:**
|
||||||
|
|
||||||
|
- Critical CSS inlined in index.html (~2KB) for instant first paint
|
||||||
|
- Theme variables, reset styles, layout skeleton, and navigation pre-loaded
|
||||||
|
- Additional preconnect hints for Convex site endpoints
|
||||||
|
|
||||||
|
**Files changed:**
|
||||||
|
|
||||||
|
- `src/styles/global.css` - Animation fixes, will-change hints, removed duplicates
|
||||||
|
- `src/components/VisitorMap.tsx` - Updated SVG circle radius for transform-based animation
|
||||||
|
- `index.html` - Inline critical CSS, resource hints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.13.0
|
||||||
|
|
||||||
|
Released January 7, 2026
|
||||||
|
|
||||||
|
**Enhanced diff code block rendering**
|
||||||
|
|
||||||
|
Diff and patch code blocks now render with enhanced visualization powered by @pierre/diffs. This brings Shiki-based syntax highlighting specifically designed for showing code changes.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- Unified view (default): Single column with +/- indicators
|
||||||
|
- Split view: Side-by-side comparison of old and new code
|
||||||
|
- View toggle button to switch between modes
|
||||||
|
- Theme-aware colors matching dark/light/tan/cloud themes
|
||||||
|
- Copy button for copying raw diff content
|
||||||
|
- Automatic routing: Use ```diff or ```patch in markdown
|
||||||
|
|
||||||
|
**New documentation:**
|
||||||
|
|
||||||
|
- Blog post: "How to Use Code Blocks" with examples of regular code blocks and diff rendering
|
||||||
|
|
||||||
|
**Technical:**
|
||||||
|
|
||||||
|
- Added `@pierre/diffs` package
|
||||||
|
- Created `DiffCodeBlock` component (`src/components/DiffCodeBlock.tsx`)
|
||||||
|
- Updated `BlogPost.tsx` to route diff/patch blocks to new renderer
|
||||||
|
- Added diff block CSS styles to `global.css`
|
||||||
|
- Added `vendor-diffs` chunk to Vite config
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.12.0
|
## v2.12.0
|
||||||
|
|
||||||
Released January 7, 2026
|
Released January 7, 2026
|
||||||
|
|||||||
11
files.md
11
files.md
@@ -9,7 +9,7 @@ A brief description of each file in the codebase.
|
|||||||
| `package.json` | Dependencies and scripts for the blog |
|
| `package.json` | Dependencies and scripts for the blog |
|
||||||
| `tsconfig.json` | TypeScript configuration |
|
| `tsconfig.json` | TypeScript configuration |
|
||||||
| `vite.config.ts` | Vite bundler configuration |
|
| `vite.config.ts` | Vite bundler configuration |
|
||||||
| `index.html` | Main HTML entry with SEO meta tags and JSON-LD |
|
| `index.html` | Main HTML entry with SEO meta tags, JSON-LD, critical CSS inline, and resource hints |
|
||||||
| `netlify.toml` | Netlify deployment and Convex HTTP redirects |
|
| `netlify.toml` | Netlify deployment and Convex HTTP redirects |
|
||||||
| `README.md` | Project documentation |
|
| `README.md` | Project documentation |
|
||||||
| `AGENTS.md` | AI coding agent instructions (agents.md spec) |
|
| `AGENTS.md` | AI coding agent instructions (agents.md spec) |
|
||||||
@@ -60,8 +60,9 @@ A brief description of each file in the codebase.
|
|||||||
| `ThemeToggle.tsx` | Theme switcher (dark/light/tan/cloud) |
|
| `ThemeToggle.tsx` | Theme switcher (dark/light/tan/cloud) |
|
||||||
| `PostList.tsx` | Year-grouped blog post list or card grid (supports list/cards view modes, columns prop for 2/3 column grids, showExcerpts prop to control excerpt visibility) |
|
| `PostList.tsx` | Year-grouped blog post list or card grid (supports list/cards view modes, columns prop for 2/3 column grids, showExcerpts prop to control excerpt visibility) |
|
||||||
| `BlogHeroCard.tsx` | Hero card component for the first blogFeatured post on blog page. Displays landscape image, tags, date, title, excerpt, author info, and read more link |
|
| `BlogHeroCard.tsx` | Hero card component for the first blogFeatured post on blog page. Displays landscape image, tags, date, title, excerpt, author info, and read more link |
|
||||||
| `BlogPost.tsx` | Markdown renderer with syntax highlighting, collapsible sections (details/summary), text wrapping for plain text code blocks, image lightbox support (click images to magnify in full-screen overlay), and iframe embed support with domain whitelisting (YouTube and Twitter/X only). SEO: H1 headings in markdown demoted to H2 (`.blog-h1-demoted` class) for single H1 per page compliance. |
|
| `BlogPost.tsx` | Markdown renderer with syntax highlighting, collapsible sections (details/summary), text wrapping for plain text code blocks, image lightbox support (click images to magnify in full-screen overlay), and iframe embed support with domain whitelisting (YouTube and Twitter/X only). Routes diff/patch code blocks to DiffCodeBlock for enhanced diff rendering. SEO: H1 headings in markdown demoted to H2 (`.blog-h1-demoted` class) for single H1 per page compliance. |
|
||||||
| `CopyPageDropdown.tsx` | Share dropdown with Copy page (markdown to clipboard), View as Markdown (opens raw .md file), Download as SKILL.md (Anthropic Agent Skills format), and Open in AI links (ChatGPT, Claude, Perplexity) using local /raw URLs with simplified prompt |
|
| `DiffCodeBlock.tsx` | Enhanced diff/patch code block renderer using @pierre/diffs library. Supports unified and split (side-by-side) view modes with toggle button. Theme-aware (dark/light) with copy button. Used automatically for ```diff and ```patch code blocks in markdown. |
|
||||||
|
| `CopyPageDropdown.tsx` | Share dropdown with Copy page (markdown to clipboard), View as Markdown (opens raw .md file), Download as SKILL.md (Anthropic Agent Skills format), Open in AI links (ChatGPT, Claude, Perplexity) using local /raw URLs, and Export as PDF (browser print with clean formatting) |
|
||||||
| `Footer.tsx` | Footer component that renders markdown content from frontmatter footer field or siteConfig.defaultContent. Can be enabled/disabled globally and per-page via frontmatter showFooter field. Renders inside article at bottom for posts/pages, and in current position on homepage. Supports images with size control via HTML attributes (width, height, style, class) |
|
| `Footer.tsx` | Footer component that renders markdown content from frontmatter footer field or siteConfig.defaultContent. Can be enabled/disabled globally and per-page via frontmatter showFooter field. Renders inside article at bottom for posts/pages, and in current position on homepage. Supports images with size control via HTML attributes (width, height, style, class) |
|
||||||
| `SearchModal.tsx` | Full text search modal with keyboard navigation. Supports keyword and semantic search modes (toggle with Tab). Semantic mode conditionally shown when `siteConfig.semanticSearch.enabled: true`. When semantic disabled (default), shows keyword search only without mode toggle. |
|
| `SearchModal.tsx` | Full text search modal with keyboard navigation. Supports keyword and semantic search modes (toggle with Tab). Semantic mode conditionally shown when `siteConfig.semanticSearch.enabled: true`. When semantic disabled (default), shows keyword search only without mode toggle. |
|
||||||
| `FeaturedCards.tsx` | Card grid for featured posts/pages with excerpts |
|
| `FeaturedCards.tsx` | Card grid for featured posts/pages with excerpts |
|
||||||
@@ -69,7 +70,7 @@ A brief description of each file in the codebase.
|
|||||||
| `MobileMenu.tsx` | Slide-out drawer menu for mobile navigation with hamburger button, includes sidebar table of contents when page has sidebar layout |
|
| `MobileMenu.tsx` | Slide-out drawer menu for mobile navigation with hamburger button, includes sidebar table of contents when page has sidebar layout |
|
||||||
| `ScrollToTop.tsx` | Configurable scroll-to-top button with Phosphor ArrowUp icon |
|
| `ScrollToTop.tsx` | Configurable scroll-to-top button with Phosphor ArrowUp icon |
|
||||||
| `GitHubContributions.tsx` | GitHub activity graph with theme-aware colors and year navigation |
|
| `GitHubContributions.tsx` | GitHub activity graph with theme-aware colors and year navigation |
|
||||||
| `VisitorMap.tsx` | Real-time visitor location map with dotted world display and theme-aware colors |
|
| `VisitorMap.tsx` | Real-time visitor location map with dotted world display, theme-aware colors, and GPU-composited pulse animations using transform: scale() |
|
||||||
| `PageSidebar.tsx` | Collapsible table of contents sidebar for pages/posts with sidebar layout, extracts headings (H1-H6), active heading highlighting, smooth scroll navigation, localStorage persistence for expanded/collapsed state |
|
| `PageSidebar.tsx` | Collapsible table of contents sidebar for pages/posts with sidebar layout, extracts headings (H1-H6), active heading highlighting, smooth scroll navigation, localStorage persistence for expanded/collapsed state |
|
||||||
| `RightSidebar.tsx` | Right sidebar component that displays CopyPageDropdown or AI chat on posts/pages at 1135px+ viewport width, controlled by siteConfig.rightSidebar.enabled and frontmatter rightSidebar/aiChat fields |
|
| `RightSidebar.tsx` | Right sidebar component that displays CopyPageDropdown or AI chat on posts/pages at 1135px+ viewport width, controlled by siteConfig.rightSidebar.enabled and frontmatter rightSidebar/aiChat fields |
|
||||||
| `AIChatView.tsx` | AI chat interface component (Agent) using Anthropic Claude API. Supports per-page chat history, page content context, markdown rendering, and copy functionality. Used in Write page (replaces textarea when enabled) and optionally in RightSidebar. Requires ANTHROPIC_API_KEY environment variable in Convex. System prompt configurable via CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES, or CLAUDE_SYSTEM_PROMPT environment variables. Includes error handling for missing API keys. |
|
| `AIChatView.tsx` | AI chat interface component (Agent) using Anthropic Claude API. Supports per-page chat history, page content context, markdown rendering, and copy functionality. Used in Write page (replaces textarea when enabled) and optionally in RightSidebar. Requires ANTHROPIC_API_KEY environment variable in Convex. System prompt configurable via CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES, or CLAUDE_SYSTEM_PROMPT environment variables. Includes error handling for missing API keys. |
|
||||||
@@ -104,7 +105,7 @@ A brief description of each file in the codebase.
|
|||||||
|
|
||||||
| File | Description |
|
| File | Description |
|
||||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `global.css` | Global CSS with theme variables, centralized font-size CSS variables for all themes, sidebar styling with alternate background colors, hidden scrollbar, and consistent borders using box-shadow for docs-style layout. Left sidebar (`.post-sidebar-wrapper`) and right sidebar (`.post-sidebar-right`) have separate, independent styles. Footer image styles (`.site-footer-image-wrapper`, `.site-footer-image`, `.site-footer-image-caption`) for responsive image display. Write page layout uses viewport height constraints (100vh) with overflow hidden to prevent page scroll, and AI chat uses flexbox with min-height: 0 for proper scrollable message area. Image lightbox styles (`.image-lightbox-backdrop`, `.image-lightbox-img`, `.image-lightbox-close`, `.image-lightbox-caption`) for full-screen image magnification with backdrop, close button, and caption display. SEO: `.blog-h1-demoted` class for demoted H1s (semantic H2 with H1 styling), CSS `order` properties for article/sidebar DOM order optimization |
|
| `global.css` | Global CSS with theme variables, centralized font-size CSS variables for all themes, sidebar styling with alternate background colors, hidden scrollbar, and consistent borders using box-shadow for docs-style layout. Left sidebar (`.post-sidebar-wrapper`) and right sidebar (`.post-sidebar-right`) have separate, independent styles. Footer image styles (`.site-footer-image-wrapper`, `.site-footer-image`, `.site-footer-image-caption`) for responsive image display. Write page layout uses viewport height constraints (100vh) with overflow hidden to prevent page scroll, and AI chat uses flexbox with min-height: 0 for proper scrollable message area. Image lightbox styles (`.image-lightbox-backdrop`, `.image-lightbox-img`, `.image-lightbox-close`, `.image-lightbox-caption`) for full-screen image magnification with backdrop, close button, and caption display. SEO: `.blog-h1-demoted` class for demoted H1s (semantic H2 with H1 styling), CSS `order` properties for article/sidebar DOM order optimization. Core Web Vitals: GPU-composited visitor-pulse animations with `transform: scale()`, `will-change` hints on animated elements (theme-toggle, copy-page-menu, search-modal-backdrop, scroll-to-top) |
|
||||||
|
|
||||||
## Convex Backend (`convex/`)
|
## Convex Backend (`convex/`)
|
||||||
|
|
||||||
|
|||||||
22
index.html
22
index.html
@@ -7,6 +7,9 @@
|
|||||||
<!-- Preconnect for faster API calls -->
|
<!-- Preconnect for faster API calls -->
|
||||||
<link rel="preconnect" href="https://convex.cloud" crossorigin />
|
<link rel="preconnect" href="https://convex.cloud" crossorigin />
|
||||||
<link rel="dns-prefetch" href="https://convex.cloud" />
|
<link rel="dns-prefetch" href="https://convex.cloud" />
|
||||||
|
<!-- Preconnect for Convex site endpoints -->
|
||||||
|
<link rel="preconnect" href="https://convex.site" crossorigin />
|
||||||
|
<link rel="dns-prefetch" href="https://convex.site" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
<!-- SEO Meta Tags -->
|
<!-- SEO Meta Tags -->
|
||||||
@@ -31,6 +34,25 @@
|
|||||||
<!-- Theme -->
|
<!-- Theme -->
|
||||||
<meta name="theme-color" content="#faf8f5" />
|
<meta name="theme-color" content="#faf8f5" />
|
||||||
|
|
||||||
|
<!-- Critical CSS - inlined for fast first paint -->
|
||||||
|
<style>
|
||||||
|
/* Theme variables */
|
||||||
|
:root[data-theme="dark"]{--bg-primary:#111;--bg-secondary:#1a1a1a;--bg-hover:#252525;--text-primary:#fafafa;--text-secondary:#a1a1a1;--text-muted:#6b6b6b;--border-color:#2a2a2a;--accent:#fafafa}
|
||||||
|
:root[data-theme="light"]{--bg-primary:#fff;--bg-secondary:#fafafa;--bg-hover:#f5f5f5;--text-primary:#111;--text-secondary:#6b6b6b;--text-muted:#a1a1a1;--border-color:#e5e5e5;--accent:#111}
|
||||||
|
:root[data-theme="tan"]{--bg-primary:#faf8f5;--bg-secondary:#f5f3f0;--bg-hover:#ebe9e6;--text-primary:#1a1a1a;--text-secondary:#6b6b6b;--text-muted:#999;--border-color:#e6e4e1;--accent:#8b7355}
|
||||||
|
:root[data-theme="cloud"]{--bg-primary:#f5f5f5;--bg-secondary:#ebebeb;--bg-hover:#e0e0e0;--text-primary:#171717;--text-secondary:#525252;--text-muted:#737373;--border-color:#d4d4d4;--accent:#171717}
|
||||||
|
/* Reset and base */
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
html{font-size:16px;-webkit-font-smoothing:antialiased}
|
||||||
|
body{font-family:"New York",-apple-system-ui-serif,ui-serif,Georgia,serif;background-color:var(--bg-primary);color:var(--text-primary);line-height:1.6}
|
||||||
|
/* Layout skeleton */
|
||||||
|
.layout{min-height:100vh;display:flex;flex-direction:column;padding-top:60px}
|
||||||
|
.main-content{flex:1;max-width:800px;width:100%;margin:0 auto}
|
||||||
|
/* Fixed nav */
|
||||||
|
.top-nav{position:fixed;top:0;left:13px;right:13px;z-index:100;display:flex;align-items:center;gap:16px;background-color:var(--bg-primary);padding:8px 12px;border-radius:8px}
|
||||||
|
@media(max-width:1024px){.layout{padding:60px 15px 15px}}
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- Theme initialization - runs before any rendering to prevent FOUC -->
|
<!-- Theme initialization - runs before any rendering to prevent FOUC -->
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
|
|||||||
183
package-lock.json
generated
183
package-lock.json
generated
@@ -16,6 +16,7 @@
|
|||||||
"@mendable/firecrawl-js": "^1.21.1",
|
"@mendable/firecrawl-js": "^1.21.1",
|
||||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
|
"@pierre/diffs": "^1.0.4",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@workos-inc/authkit-react": "^0.15.0",
|
"@workos-inc/authkit-react": "^0.15.0",
|
||||||
"@workos-inc/widgets": "^1.6.1",
|
"@workos-inc/widgets": "^1.6.1",
|
||||||
@@ -1355,6 +1356,25 @@
|
|||||||
"react-dom": ">= 16.8"
|
"react-dom": ">= 16.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@pierre/diffs": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pierre/diffs/-/diffs-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-jH0uzP3syMFcz0rp4uDDGTkV8sbMeh0W8Gq+e3P1rIiJ+gdGISfG+Qhwpe//W2W5Ac9G5vBAbMflywz+lLaJlQ==",
|
||||||
|
"license": "apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@shikijs/core": "^3.0.0",
|
||||||
|
"@shikijs/engine-javascript": "^3.0.0",
|
||||||
|
"@shikijs/transformers": "^3.0.0",
|
||||||
|
"diff": "8.0.2",
|
||||||
|
"hast-util-to-html": "9.0.5",
|
||||||
|
"lru_map": "0.4.1",
|
||||||
|
"shiki": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.3.1 || ^19.0.0",
|
||||||
|
"react-dom": "^18.3.1 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@@ -3415,6 +3435,83 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@shikijs/core": {
|
||||||
|
"version": "3.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.21.0.tgz",
|
||||||
|
"integrity": "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@shikijs/types": "3.21.0",
|
||||||
|
"@shikijs/vscode-textmate": "^10.0.2",
|
||||||
|
"@types/hast": "^3.0.4",
|
||||||
|
"hast-util-to-html": "^9.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@shikijs/engine-javascript": {
|
||||||
|
"version": "3.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.21.0.tgz",
|
||||||
|
"integrity": "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@shikijs/types": "3.21.0",
|
||||||
|
"@shikijs/vscode-textmate": "^10.0.2",
|
||||||
|
"oniguruma-to-es": "^4.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@shikijs/engine-oniguruma": {
|
||||||
|
"version": "3.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.21.0.tgz",
|
||||||
|
"integrity": "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@shikijs/types": "3.21.0",
|
||||||
|
"@shikijs/vscode-textmate": "^10.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@shikijs/langs": {
|
||||||
|
"version": "3.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.21.0.tgz",
|
||||||
|
"integrity": "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@shikijs/types": "3.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@shikijs/themes": {
|
||||||
|
"version": "3.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.21.0.tgz",
|
||||||
|
"integrity": "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@shikijs/types": "3.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@shikijs/transformers": {
|
||||||
|
"version": "3.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.21.0.tgz",
|
||||||
|
"integrity": "sha512-CZwvCWWIiRRiFk9/JKzdEooakAP8mQDtBOQ1TKiCaS2E1bYtyBCOkUzS8akO34/7ufICQ29oeSfkb3tT5KtrhA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@shikijs/core": "3.21.0",
|
||||||
|
"@shikijs/types": "3.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@shikijs/types": {
|
||||||
|
"version": "3.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.21.0.tgz",
|
||||||
|
"integrity": "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@shikijs/vscode-textmate": "^10.0.2",
|
||||||
|
"@types/hast": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@shikijs/vscode-textmate": {
|
||||||
|
"version": "10.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
|
||||||
|
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tanstack/query-core": {
|
"node_modules/@tanstack/query-core": {
|
||||||
"version": "5.90.15",
|
"version": "5.90.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.15.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.15.tgz",
|
||||||
@@ -6500,6 +6597,29 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hast-util-to-html": {
|
||||||
|
"version": "9.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
|
||||||
|
"integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/hast": "^3.0.0",
|
||||||
|
"@types/unist": "^3.0.0",
|
||||||
|
"ccount": "^2.0.0",
|
||||||
|
"comma-separated-tokens": "^2.0.0",
|
||||||
|
"hast-util-whitespace": "^3.0.0",
|
||||||
|
"html-void-elements": "^3.0.0",
|
||||||
|
"mdast-util-to-hast": "^13.0.0",
|
||||||
|
"property-information": "^7.0.0",
|
||||||
|
"space-separated-tokens": "^2.0.0",
|
||||||
|
"stringify-entities": "^4.0.0",
|
||||||
|
"zwitch": "^2.0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/unified"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hast-util-to-jsx-runtime": {
|
"node_modules/hast-util-to-jsx-runtime": {
|
||||||
"version": "2.3.6",
|
"version": "2.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
||||||
@@ -7248,6 +7368,12 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lru_map": {
|
||||||
|
"version": "0.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.4.1.tgz",
|
||||||
|
"integrity": "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -8396,6 +8522,23 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/oniguruma-parser": {
|
||||||
|
"version": "0.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
|
||||||
|
"integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/oniguruma-to-es": {
|
||||||
|
"version": "4.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz",
|
||||||
|
"integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"oniguruma-parser": "^0.12.1",
|
||||||
|
"regex": "^6.0.1",
|
||||||
|
"regex-recursion": "^6.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/openai": {
|
"node_modules/openai": {
|
||||||
"version": "4.104.0",
|
"version": "4.104.0",
|
||||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
|
"resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
|
||||||
@@ -9303,6 +9446,30 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/regex": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"regex-utilities": "^2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/regex-recursion": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"regex-utilities": "^2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/regex-utilities": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/regexp.prototype.flags": {
|
"node_modules/regexp.prototype.flags": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
||||||
@@ -9833,6 +10000,22 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/shiki": {
|
||||||
|
"version": "3.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shiki/-/shiki-3.21.0.tgz",
|
||||||
|
"integrity": "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@shikijs/core": "3.21.0",
|
||||||
|
"@shikijs/engine-javascript": "3.21.0",
|
||||||
|
"@shikijs/engine-oniguruma": "3.21.0",
|
||||||
|
"@shikijs/langs": "3.21.0",
|
||||||
|
"@shikijs/themes": "3.21.0",
|
||||||
|
"@shikijs/types": "3.21.0",
|
||||||
|
"@shikijs/vscode-textmate": "^10.0.2",
|
||||||
|
"@types/hast": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/showdown": {
|
"node_modules/showdown": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/showdown/-/showdown-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/showdown/-/showdown-2.1.0.tgz",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"@mendable/firecrawl-js": "^1.21.1",
|
"@mendable/firecrawl-js": "^1.21.1",
|
||||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
|
"@pierre/diffs": "^1.0.4",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@workos-inc/authkit-react": "^0.15.0",
|
"@workos-inc/authkit-react": "^0.15.0",
|
||||||
"@workos-inc/widgets": "^1.6.1",
|
"@workos-inc/widgets": "^1.6.1",
|
||||||
|
|||||||
@@ -7,6 +7,94 @@ Date: 2026-01-08
|
|||||||
|
|
||||||
All notable changes to this project.
|
All notable changes to this project.
|
||||||
|
|
||||||
|
## v2.15.0
|
||||||
|
|
||||||
|
Released January 7, 2026
|
||||||
|
|
||||||
|
**Export as PDF**
|
||||||
|
|
||||||
|
Added PDF export option to CopyPageDropdown. Users can now export any blog post or page as a clean, formatted PDF document using the browser's native print dialog.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- Export as PDF button in Copy page dropdown (positioned at end of menu)
|
||||||
|
- Clean formatted output without markdown syntax
|
||||||
|
- Title displayed as proper heading
|
||||||
|
- Metadata shown on single line (date, read time, tags)
|
||||||
|
- Content with markdown stripped for readable document
|
||||||
|
- Uses Phosphor FilePdf icon
|
||||||
|
|
||||||
|
**Technical:**
|
||||||
|
|
||||||
|
- Added `formatForPrint` function to strip markdown syntax (headings, bold, italic, code, links, blockquotes)
|
||||||
|
- Added `handleExportPDF` handler that opens styled print window
|
||||||
|
- Imports `FilePdf` from `@phosphor-icons/react` (already installed in project)
|
||||||
|
|
||||||
|
**Files changed:**
|
||||||
|
|
||||||
|
- `src/components/CopyPageDropdown.tsx` - New PDF export functionality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.14.0
|
||||||
|
|
||||||
|
Released January 7, 2026
|
||||||
|
|
||||||
|
**Core Web Vitals performance optimizations**
|
||||||
|
|
||||||
|
Fixes for PageSpeed Insights failures on mobile and desktop. These changes improve Largest Contentful Paint (LCP) and eliminate non-composited animation warnings.
|
||||||
|
|
||||||
|
**Fixes:**
|
||||||
|
|
||||||
|
- Non-composited animations: Visitor map pulse animations now use GPU-composited `transform: scale()` instead of animating SVG `r` attribute
|
||||||
|
- Duplicate keyframes: Removed 5 redundant `@keyframes spin` definitions from CSS
|
||||||
|
- GPU compositing hints: Added `will-change` to animated elements (theme toggle, dropdown menus, modals, scroll-to-top button)
|
||||||
|
|
||||||
|
**Performance additions:**
|
||||||
|
|
||||||
|
- Critical CSS inlined in index.html (~2KB) for instant first paint
|
||||||
|
- Theme variables, reset styles, layout skeleton, and navigation pre-loaded
|
||||||
|
- Additional preconnect hints for Convex site endpoints
|
||||||
|
|
||||||
|
**Files changed:**
|
||||||
|
|
||||||
|
- `src/styles/global.css` - Animation fixes, will-change hints, removed duplicates
|
||||||
|
- `src/components/VisitorMap.tsx` - Updated SVG circle radius for transform-based animation
|
||||||
|
- `index.html` - Inline critical CSS, resource hints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.13.0
|
||||||
|
|
||||||
|
Released January 7, 2026
|
||||||
|
|
||||||
|
**Enhanced diff code block rendering**
|
||||||
|
|
||||||
|
Diff and patch code blocks now render with enhanced visualization powered by @pierre/diffs. This brings Shiki-based syntax highlighting specifically designed for showing code changes.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- Unified view (default): Single column with +/- indicators
|
||||||
|
- Split view: Side-by-side comparison of old and new code
|
||||||
|
- View toggle button to switch between modes
|
||||||
|
- Theme-aware colors matching dark/light/tan/cloud themes
|
||||||
|
- Copy button for copying raw diff content
|
||||||
|
- Automatic routing: Use ```diff or ```patch in markdown
|
||||||
|
|
||||||
|
**New documentation:**
|
||||||
|
|
||||||
|
- Blog post: "How to Use Code Blocks" with examples of regular code blocks and diff rendering
|
||||||
|
|
||||||
|
**Technical:**
|
||||||
|
|
||||||
|
- Added `@pierre/diffs` package
|
||||||
|
- Created `DiffCodeBlock` component (`src/components/DiffCodeBlock.tsx`)
|
||||||
|
- Updated `BlogPost.tsx` to route diff/patch blocks to new renderer
|
||||||
|
- Added diff block CSS styles to `global.css`
|
||||||
|
- Added `vendor-diffs` chunk to Vite config
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.12.0
|
## v2.12.0
|
||||||
|
|
||||||
Released January 7, 2026
|
Released January 7, 2026
|
||||||
|
|||||||
215
public/raw/how-to-use-code-blocks.md
Normal file
215
public/raw/how-to-use-code-blocks.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# How to Use Code Blocks
|
||||||
|
|
||||||
|
> A guide to syntax highlighting, diff rendering, and code formatting in your markdown posts.
|
||||||
|
|
||||||
|
---
|
||||||
|
Type: post
|
||||||
|
Date: 2026-01-07
|
||||||
|
Reading time: 4 min read
|
||||||
|
Tags: tutorial, markdown, code, syntax-highlighting
|
||||||
|
---
|
||||||
|
|
||||||
|
# How to Use Code Blocks
|
||||||
|
|
||||||
|
Code blocks are essential for technical writing. This guide covers standard syntax highlighting and enhanced diff rendering.
|
||||||
|
|
||||||
|
## Basic code blocks
|
||||||
|
|
||||||
|
Wrap code in triple backticks with a language identifier:
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
```javascript
|
||||||
|
function greet(name) {
|
||||||
|
return `Hello, ${name}!`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
This renders with syntax highlighting:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function greet(name) {
|
||||||
|
return `Hello, ${name}!`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported languages
|
||||||
|
|
||||||
|
Common languages with syntax highlighting:
|
||||||
|
|
||||||
|
| Language | Identifier |
|
||||||
|
| ---------- | -------------------- |
|
||||||
|
| JavaScript | `javascript` or `js` |
|
||||||
|
| TypeScript | `typescript` or `ts` |
|
||||||
|
| Python | `python` or `py` |
|
||||||
|
| Bash | `bash` or `shell` |
|
||||||
|
| JSON | `json` |
|
||||||
|
| CSS | `css` |
|
||||||
|
| SQL | `sql` |
|
||||||
|
| Go | `go` |
|
||||||
|
| Rust | `rust` |
|
||||||
|
| Markdown | `markdown` or `md` |
|
||||||
|
|
||||||
|
## Code block features
|
||||||
|
|
||||||
|
Every code block includes:
|
||||||
|
|
||||||
|
- **Language label** in the top right corner
|
||||||
|
- **Copy button** that appears on hover
|
||||||
|
- **Theme-aware colors** matching your selected theme
|
||||||
|
|
||||||
|
## Diff code blocks
|
||||||
|
|
||||||
|
For showing code changes, use the `diff` or `patch` language identifier. These render with enhanced diff visualization powered by @pierre/diffs.
|
||||||
|
|
||||||
|
### Basic diff example
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
```diff
|
||||||
|
--- a/config.js
|
||||||
|
+++ b/config.js
|
||||||
|
@@ -1,5 +1,5 @@
|
||||||
|
const config = {
|
||||||
|
- debug: true,
|
||||||
|
+ debug: false,
|
||||||
|
port: 3000
|
||||||
|
};
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
This renders as:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
--- a/config.js
|
||||||
|
+++ b/config.js
|
||||||
|
@@ -1,5 +1,5 @@
|
||||||
|
const config = {
|
||||||
|
- debug: true,
|
||||||
|
+ debug: false,
|
||||||
|
port: 3000
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-line changes
|
||||||
|
|
||||||
|
```diff
|
||||||
|
--- a/utils.ts
|
||||||
|
+++ b/utils.ts
|
||||||
|
@@ -10,12 +10,15 @@
|
||||||
|
export function formatDate(date: Date): string {
|
||||||
|
- return date.toLocaleDateString();
|
||||||
|
+ return date.toLocaleDateString('en-US', {
|
||||||
|
+ year: 'numeric',
|
||||||
|
+ month: 'long',
|
||||||
|
+ day: 'numeric'
|
||||||
|
+ });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseDate(str: string): Date {
|
||||||
|
- return new Date(str);
|
||||||
|
+ const parsed = new Date(str);
|
||||||
|
+ if (isNaN(parsed.getTime())) {
|
||||||
|
+ throw new Error('Invalid date string');
|
||||||
|
+ }
|
||||||
|
+ return parsed;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Diff view modes
|
||||||
|
|
||||||
|
Diff blocks include a view toggle button:
|
||||||
|
|
||||||
|
- **Unified view** (default): Shows changes in a single column with +/- indicators
|
||||||
|
- **Split view**: Shows old and new code side by side
|
||||||
|
|
||||||
|
Click the toggle button in the diff header to switch between views.
|
||||||
|
|
||||||
|
## Adding new functions
|
||||||
|
|
||||||
|
```diff
|
||||||
|
--- a/api.ts
|
||||||
|
+++ b/api.ts
|
||||||
|
@@ -5,6 +5,14 @@ export async function fetchUser(id: string) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
+export async function updateUser(id: string, data: UserUpdate) {
|
||||||
|
+ const response = await fetch(`/api/users/${id}`, {
|
||||||
|
+ method: 'PATCH',
|
||||||
|
+ body: JSON.stringify(data)
|
||||||
|
+ });
|
||||||
|
+ return response.json();
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
export async function deleteUser(id: string) {
|
||||||
|
return fetch(`/api/users/${id}`, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Removing code
|
||||||
|
|
||||||
|
```diff
|
||||||
|
--- a/legacy.js
|
||||||
|
+++ b/legacy.js
|
||||||
|
@@ -1,15 +1,8 @@
|
||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
-// Old middleware - no longer needed
|
||||||
|
-app.use((req, res, next) => {
|
||||||
|
- console.log('Request:', req.method, req.url);
|
||||||
|
- next();
|
||||||
|
-});
|
||||||
|
-
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.send('Hello World');
|
||||||
|
});
|
||||||
|
|
||||||
|
-// Deprecated route
|
||||||
|
-app.get('/old', (req, res) => res.redirect('/'));
|
||||||
|
-
|
||||||
|
app.listen(3000);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inline code
|
||||||
|
|
||||||
|
For inline code, use single backticks:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Run `npm install` to install dependencies.
|
||||||
|
```
|
||||||
|
|
||||||
|
Renders as: Run `npm install` to install dependencies.
|
||||||
|
|
||||||
|
Inline code is detected automatically when the content is short (under 80 characters) and has no newlines.
|
||||||
|
|
||||||
|
## Plain text blocks
|
||||||
|
|
||||||
|
Code blocks without a language identifier render as plain text with word wrapping:
|
||||||
|
|
||||||
|
```
|
||||||
|
This is a plain text block. It wraps long lines automatically
|
||||||
|
instead of requiring horizontal scrolling. Useful for logs,
|
||||||
|
output, or any text that isn't code.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
**Choose the right language**: Use the correct language identifier for accurate highlighting. TypeScript files should use `typescript`, not `javascript`.
|
||||||
|
|
||||||
|
**Keep examples focused**: Show only the relevant code. Long blocks lose readers.
|
||||||
|
|
||||||
|
**Use diffs for changes**: When explaining modifications to existing code, diff blocks clearly show what changed.
|
||||||
|
|
||||||
|
**Test your blocks**: Preview your post to verify syntax highlighting works correctly.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Block type | Use case |
|
||||||
|
| ------------------ | ---------------------------------------------- |
|
||||||
|
| Regular code block | Showing code snippets with syntax highlighting |
|
||||||
|
| Diff block | Showing code changes with additions/deletions |
|
||||||
|
| Plain text block | Logs, output, or non-code text |
|
||||||
|
| Inline code | Commands, function names, short references |
|
||||||
|
|
||||||
|
Code blocks make technical content readable. Use the right format for your content type.
|
||||||
@@ -30,8 +30,10 @@ agents. -->
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Blog Posts (18)
|
## Blog Posts (19)
|
||||||
|
|
||||||
|
- **[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
|
||||||
- **[How I added WorkOS to my Convex app with Cursor](/raw/workos-with-convex-cursor.md)** - A timeline of adding WorkOS AuthKit authentication to my markdown blog dashboard using Cursor, prompt engineering, and vibe coding. From PRD import to published feature.
|
- **[How I added WorkOS to my Convex app with Cursor](/raw/workos-with-convex-cursor.md)** - A timeline of adding WorkOS AuthKit authentication to my markdown blog dashboard using Cursor, prompt engineering, and vibe coding. From PRD import to published feature.
|
||||||
- Date: 2025-12-30 | Reading time: 8 min read | Tags: cursor, workos, convex, prompt-engineering, ai-coding
|
- Date: 2025-12-30 | Reading time: 8 min read | Tags: cursor, workos, convex, prompt-engineering, ai-coding
|
||||||
- **[How to setup WorkOS with Markdown Sync](/raw/how-to-setup-workos.md)** - Step-by-step guide to configure WorkOS AuthKit authentication for your markdown blog dashboard. WorkOS is optional and can be enabled in siteConfig.ts.
|
- **[How to setup WorkOS with Markdown Sync](/raw/how-to-setup-workos.md)** - Step-by-step guide to configure WorkOS AuthKit authentication for your markdown blog dashboard. WorkOS is optional and can be enabled in siteConfig.ts.
|
||||||
@@ -90,7 +92,7 @@ agents. -->
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Total Content:** 18 posts, 16 pages
|
**Total Content:** 19 posts, 16 pages
|
||||||
|
|
||||||
All content is available as raw markdown files at `/raw/{slug}.md`
|
All content is available as raw markdown files at `/raw/{slug}.md`
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import { Copy, Check, X } from "lucide-react";
|
|||||||
import { useTheme } from "../context/ThemeContext";
|
import { useTheme } from "../context/ThemeContext";
|
||||||
import NewsletterSignup from "./NewsletterSignup";
|
import NewsletterSignup from "./NewsletterSignup";
|
||||||
import ContactForm from "./ContactForm";
|
import ContactForm from "./ContactForm";
|
||||||
|
import DiffCodeBlock from "./DiffCodeBlock";
|
||||||
import siteConfig from "../config/siteConfig";
|
import siteConfig from "../config/siteConfig";
|
||||||
import { useSearchHighlighting } from "../hooks/useSearchHighlighting";
|
import { useSearchHighlighting } from "../hooks/useSearchHighlighting";
|
||||||
|
|
||||||
@@ -611,6 +612,17 @@ export default function BlogPost({
|
|||||||
|
|
||||||
const codeString = String(children).replace(/\n$/, "");
|
const codeString = String(children).replace(/\n$/, "");
|
||||||
const language = match ? match[1] : "text";
|
const language = match ? match[1] : "text";
|
||||||
|
|
||||||
|
// Route diff/patch to DiffCodeBlock for enhanced diff rendering
|
||||||
|
if (language === "diff" || language === "patch") {
|
||||||
|
return (
|
||||||
|
<DiffCodeBlock
|
||||||
|
code={codeString}
|
||||||
|
language={language as "diff" | "patch"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const isTextBlock = language === "text";
|
const isTextBlock = language === "text";
|
||||||
|
|
||||||
// Custom styles for text blocks to enable wrapping
|
// Custom styles for text blocks to enable wrapping
|
||||||
@@ -899,6 +911,17 @@ export default function BlogPost({
|
|||||||
|
|
||||||
const codeString = String(children).replace(/\n$/, "");
|
const codeString = String(children).replace(/\n$/, "");
|
||||||
const language = match ? match[1] : "text";
|
const language = match ? match[1] : "text";
|
||||||
|
|
||||||
|
// Route diff/patch to DiffCodeBlock for enhanced diff rendering
|
||||||
|
if (language === "diff" || language === "patch") {
|
||||||
|
return (
|
||||||
|
<DiffCodeBlock
|
||||||
|
code={codeString}
|
||||||
|
language={language as "diff" | "patch"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const isTextBlock = language === "text";
|
const isTextBlock = language === "text";
|
||||||
|
|
||||||
// Custom styles for text blocks to enable wrapping
|
// Custom styles for text blocks to enable wrapping
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { FilePdf } from "@phosphor-icons/react";
|
||||||
|
|
||||||
// Maximum URL length for query parameters (conservative limit)
|
// Maximum URL length for query parameters (conservative limit)
|
||||||
const MAX_URL_LENGTH = 6000;
|
const MAX_URL_LENGTH = 6000;
|
||||||
@@ -110,6 +111,38 @@ function formatAsSkill(props: CopyPageDropdownProps): string {
|
|||||||
return skill;
|
return skill;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format content for print/PDF export (clean, readable document)
|
||||||
|
function formatForPrint(props: CopyPageDropdownProps): {
|
||||||
|
title: string;
|
||||||
|
metadata: string[];
|
||||||
|
description: string;
|
||||||
|
content: string;
|
||||||
|
} {
|
||||||
|
const { title, content, description, date, tags, readTime } = props;
|
||||||
|
|
||||||
|
const metadata: string[] = [];
|
||||||
|
if (date) metadata.push(date);
|
||||||
|
if (readTime) metadata.push(readTime);
|
||||||
|
if (tags && tags.length > 0) metadata.push(tags.join(", "));
|
||||||
|
|
||||||
|
// Strip common markdown syntax for cleaner display
|
||||||
|
const cleanContent = content
|
||||||
|
.replace(/^#{1,6}\s+/gm, "") // Remove heading markers
|
||||||
|
.replace(/\*\*([^*]+)\*\*/g, "$1") // Bold to plain
|
||||||
|
.replace(/\*([^*]+)\*/g, "$1") // Italic to plain
|
||||||
|
.replace(/`([^`]+)`/g, "$1") // Inline code to plain
|
||||||
|
.replace(/^\s*[-*+]\s+/gm, "- ") // Normalize list markers
|
||||||
|
.replace(/^\s*>\s+/gm, "") // Remove blockquote markers
|
||||||
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); // Links to text only
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
metadata,
|
||||||
|
description: description || "",
|
||||||
|
content: cleanContent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Check if URL length exceeds safe limits
|
// Check if URL length exceeds safe limits
|
||||||
function isUrlTooLong(url: string): boolean {
|
function isUrlTooLong(url: string): boolean {
|
||||||
return url.length > MAX_URL_LENGTH;
|
return url.length > MAX_URL_LENGTH;
|
||||||
@@ -280,6 +313,67 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
|||||||
setTimeout(() => setIsOpen(false), 1500);
|
setTimeout(() => setIsOpen(false), 1500);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle export as PDF (browser print dialog)
|
||||||
|
const handleExportPDF = () => {
|
||||||
|
const printData = formatForPrint(props);
|
||||||
|
const printWindow = window.open("", "_blank");
|
||||||
|
if (printWindow) {
|
||||||
|
const escapeHtml = (str: string) =>
|
||||||
|
str.replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
|
||||||
|
printWindow.document.write(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>${escapeHtml(printData.title)}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 20px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.metadata {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #444;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
body { padding: 20px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>${escapeHtml(printData.title)}</h1>
|
||||||
|
${printData.metadata.length > 0 ? `<div class="metadata">${printData.metadata.join(" | ")}</div>` : ""}
|
||||||
|
${printData.description ? `<div class="description">${escapeHtml(printData.description)}</div>` : ""}
|
||||||
|
<div class="content">${escapeHtml(printData.content)}</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
printWindow.document.close();
|
||||||
|
printWindow.print();
|
||||||
|
}
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
// Get feedback icon
|
// Get feedback icon
|
||||||
const getFeedbackIcon = () => {
|
const getFeedbackIcon = () => {
|
||||||
switch (feedback) {
|
switch (feedback) {
|
||||||
@@ -515,6 +609,22 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Export as PDF option */}
|
||||||
|
<button
|
||||||
|
className="copy-page-item"
|
||||||
|
onClick={handleExportPDF}
|
||||||
|
role="menuitem"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<FilePdf size={16} className="copy-page-icon" aria-hidden="true" />
|
||||||
|
<div className="copy-page-item-content">
|
||||||
|
<span className="copy-page-item-title">Export as PDF</span>
|
||||||
|
<span className="copy-page-item-desc">
|
||||||
|
Print or save as PDF
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
74
src/components/DiffCodeBlock.tsx
Normal file
74
src/components/DiffCodeBlock.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { PatchDiff } from "@pierre/diffs/react";
|
||||||
|
import { Copy, Check, Columns2, AlignJustify } from "lucide-react";
|
||||||
|
import { useTheme } from "../context/ThemeContext";
|
||||||
|
|
||||||
|
// Map app themes to @pierre/diffs themeType
|
||||||
|
const THEME_MAP: Record<string, "dark" | "light"> = {
|
||||||
|
dark: "dark",
|
||||||
|
light: "light",
|
||||||
|
tan: "light",
|
||||||
|
cloud: "light",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DiffCodeBlockProps {
|
||||||
|
code: string;
|
||||||
|
language: "diff" | "patch";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DiffCodeBlock({ code, language }: DiffCodeBlockProps) {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const [viewMode, setViewMode] = useState<"split" | "unified">("unified");
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
// Get theme type for @pierre/diffs
|
||||||
|
const themeType = THEME_MAP[theme] || "dark";
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
await navigator.clipboard.writeText(code);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="diff-block-wrapper" data-theme-type={themeType}>
|
||||||
|
<div className="diff-block-header">
|
||||||
|
<span className="diff-language">{language}</span>
|
||||||
|
<div className="diff-block-controls">
|
||||||
|
<button
|
||||||
|
className="diff-view-toggle"
|
||||||
|
onClick={() =>
|
||||||
|
setViewMode(viewMode === "split" ? "unified" : "split")
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
viewMode === "split"
|
||||||
|
? "Switch to unified view"
|
||||||
|
: "Switch to split view"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{viewMode === "split" ? (
|
||||||
|
<AlignJustify size={14} />
|
||||||
|
) : (
|
||||||
|
<Columns2 size={14} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="diff-copy-button"
|
||||||
|
onClick={handleCopy}
|
||||||
|
aria-label={copied ? "Copied!" : "Copy code"}
|
||||||
|
title={copied ? "Copied!" : "Copy code"}
|
||||||
|
>
|
||||||
|
{copied ? <Check size={14} /> : <Copy size={14} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PatchDiff
|
||||||
|
patch={code}
|
||||||
|
options={{
|
||||||
|
themeType,
|
||||||
|
diffStyle: viewMode,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -116,21 +116,21 @@ export default function VisitorMap({ locations, title }: VisitorMapProps) {
|
|||||||
{/* Visitor location dots with pulse animation */}
|
{/* Visitor location dots with pulse animation */}
|
||||||
{visitorDots.map((dot, i) => (
|
{visitorDots.map((dot, i) => (
|
||||||
<g key={`visitor-${i}`}>
|
<g key={`visitor-${i}`}>
|
||||||
{/* Outer pulse ring */}
|
{/* Outer pulse ring - base r=5, scaled via CSS transform */}
|
||||||
<circle
|
<circle
|
||||||
cx={dot.x}
|
cx={dot.x}
|
||||||
cy={dot.y}
|
cy={dot.y}
|
||||||
r="12"
|
r="5"
|
||||||
fill="var(--visitor-map-dot)"
|
fill="var(--visitor-map-dot)"
|
||||||
opacity="0"
|
opacity="0"
|
||||||
className="visitor-pulse-ring"
|
className="visitor-pulse-ring"
|
||||||
style={{ animationDelay: `${i * 0.2}s` }}
|
style={{ animationDelay: `${i * 0.2}s` }}
|
||||||
/>
|
/>
|
||||||
{/* Middle pulse ring */}
|
{/* Middle pulse ring - base r=5, scaled via CSS transform */}
|
||||||
<circle
|
<circle
|
||||||
cx={dot.x}
|
cx={dot.x}
|
||||||
cy={dot.y}
|
cy={dot.y}
|
||||||
r="8"
|
r="5"
|
||||||
fill="var(--visitor-map-dot)"
|
fill="var(--visitor-map-dot)"
|
||||||
opacity="0.2"
|
opacity="0.2"
|
||||||
className="visitor-pulse-ring-mid"
|
className="visitor-pulse-ring-mid"
|
||||||
|
|||||||
@@ -455,6 +455,7 @@ body {
|
|||||||
transition:
|
transition:
|
||||||
color 0.2s ease,
|
color 0.2s ease,
|
||||||
background-color 0.2s ease;
|
background-color 0.2s ease;
|
||||||
|
will-change: color, background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle:hover {
|
.theme-toggle:hover {
|
||||||
@@ -820,6 +821,7 @@ body {
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
animation: dropdownFadeIn 0.15s ease;
|
animation: dropdownFadeIn 0.15s ease;
|
||||||
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
@@ -1800,6 +1802,79 @@ body {
|
|||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Diff block styles for @pierre/diffs */
|
||||||
|
.diff-block-wrapper {
|
||||||
|
position: relative;
|
||||||
|
margin: 24px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-block-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-language {
|
||||||
|
font-size: var(--font-size-code-language);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-block-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-view-toggle,
|
||||||
|
.diff-copy-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background 0.15s ease,
|
||||||
|
color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-view-toggle:hover,
|
||||||
|
.diff-copy-button:hover {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-view-toggle:active,
|
||||||
|
.diff-copy-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme-specific styles for @pierre/diffs */
|
||||||
|
.diff-block-wrapper[data-theme-type="dark"] {
|
||||||
|
--diffs-bg-color: #1e1e1e;
|
||||||
|
--diffs-fg-color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-block-wrapper[data-theme-type="light"] {
|
||||||
|
--diffs-bg-color: #ffffff;
|
||||||
|
--diffs-fg-color: #24292e;
|
||||||
|
}
|
||||||
|
|
||||||
/* Image styles */
|
/* Image styles */
|
||||||
.blog-image-wrapper {
|
.blog-image-wrapper {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -3422,6 +3497,7 @@ body {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-top: 15vh;
|
padding-top: 15vh;
|
||||||
animation: backdropFadeIn 0.15s ease;
|
animation: backdropFadeIn 0.15s ease;
|
||||||
|
will-change: opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes backdropFadeIn {
|
@keyframes backdropFadeIn {
|
||||||
@@ -4376,44 +4452,50 @@ body {
|
|||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pulse animation for visitor dots */
|
/* Pulse animation for visitor dots - using transform for GPU compositing */
|
||||||
@keyframes visitor-pulse {
|
@keyframes visitor-pulse {
|
||||||
0% {
|
0% {
|
||||||
r: 5;
|
transform: scale(1);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
r: 18;
|
transform: scale(3.6);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
r: 5;
|
transform: scale(1);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes visitor-pulse-mid {
|
@keyframes visitor-pulse-mid {
|
||||||
0% {
|
0% {
|
||||||
r: 5;
|
transform: scale(1);
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
r: 12;
|
transform: scale(2.4);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
r: 5;
|
transform: scale(1);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.visitor-pulse-ring {
|
.visitor-pulse-ring {
|
||||||
animation: visitor-pulse 2.5s ease-out infinite;
|
animation: visitor-pulse 2.5s ease-out infinite;
|
||||||
|
transform-origin: center;
|
||||||
|
transform-box: fill-box;
|
||||||
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visitor-pulse-ring-mid {
|
.visitor-pulse-ring-mid {
|
||||||
animation: visitor-pulse-mid 2.5s ease-out infinite;
|
animation: visitor-pulse-mid 2.5s ease-out infinite;
|
||||||
animation-delay: 0.3s;
|
animation-delay: 0.3s;
|
||||||
|
transform-origin: center;
|
||||||
|
transform-box: fill-box;
|
||||||
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visitor-dot-center {
|
.visitor-dot-center {
|
||||||
@@ -5266,6 +5348,7 @@ body {
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
animation: scrollToTopFadeIn 0.2s ease;
|
animation: scrollToTopFadeIn 0.2s ease;
|
||||||
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes scrollToTopFadeIn {
|
@keyframes scrollToTopFadeIn {
|
||||||
@@ -9185,15 +9268,6 @@ body {
|
|||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-editor-container {
|
.dashboard-editor-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -10082,15 +10156,6 @@ body {
|
|||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* AI Image Input Container */
|
/* AI Image Input Container */
|
||||||
.ai-image-input-container {
|
.ai-image-input-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -10234,15 +10299,6 @@ body {
|
|||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-import-result {
|
.dashboard-import-result {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -10641,16 +10697,6 @@ body {
|
|||||||
border-color: var(--border-color);
|
border-color: var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Spinning animation for sync icon */
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinning {
|
.spinning {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
@@ -13717,15 +13763,6 @@ body {
|
|||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Streaming cursor */
|
/* Streaming cursor */
|
||||||
.ask-ai-cursor {
|
.ask-ai-cursor {
|
||||||
animation: blink 1s steps(1) infinite;
|
animation: blink 1s steps(1) infinite;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
"rehype-sanitize",
|
"rehype-sanitize",
|
||||||
],
|
],
|
||||||
"vendor-syntax": ["react-syntax-highlighter"],
|
"vendor-syntax": ["react-syntax-highlighter"],
|
||||||
|
"vendor-diffs": ["@pierre/diffs"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user