Export as PDF, Core Web Vitals performance optimizations, Enhanced diff code block rendering and blog post example on codeblocks

This commit is contained in:
Wayne Sutton
2026-01-07 23:20:50 -08:00
parent 1257fa220f
commit cd696416d9
19 changed files with 1228 additions and 65 deletions

View File

@@ -47,6 +47,7 @@ import { Copy, Check, X } from "lucide-react";
import { useTheme } from "../context/ThemeContext";
import NewsletterSignup from "./NewsletterSignup";
import ContactForm from "./ContactForm";
import DiffCodeBlock from "./DiffCodeBlock";
import siteConfig from "../config/siteConfig";
import { useSearchHighlighting } from "../hooks/useSearchHighlighting";
@@ -611,6 +612,17 @@ export default function BlogPost({
const codeString = String(children).replace(/\n$/, "");
const language = match ? match[1] : "text";
// Route diff/patch to DiffCodeBlock for enhanced diff rendering
if (language === "diff" || language === "patch") {
return (
<DiffCodeBlock
code={codeString}
language={language as "diff" | "patch"}
/>
);
}
const isTextBlock = language === "text";
// Custom styles for text blocks to enable wrapping
@@ -899,6 +911,17 @@ export default function BlogPost({
const codeString = String(children).replace(/\n$/, "");
const language = match ? match[1] : "text";
// Route diff/patch to DiffCodeBlock for enhanced diff rendering
if (language === "diff" || language === "patch") {
return (
<DiffCodeBlock
code={codeString}
language={language as "diff" | "patch"}
/>
);
}
const isTextBlock = language === "text";
// Custom styles for text blocks to enable wrapping

View File

@@ -7,6 +7,7 @@ import {
Download,
ExternalLink,
} from "lucide-react";
import { FilePdf } from "@phosphor-icons/react";
// Maximum URL length for query parameters (conservative limit)
const MAX_URL_LENGTH = 6000;
@@ -110,6 +111,38 @@ function formatAsSkill(props: CopyPageDropdownProps): string {
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
function isUrlTooLong(url: string): boolean {
return url.length > MAX_URL_LENGTH;
@@ -280,6 +313,67 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
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, "&lt;").replace(/>/g, "&gt;");
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
const getFeedbackIcon = () => {
switch (feedback) {
@@ -515,6 +609,22 @@ export default function CopyPageDropdown(props: CopyPageDropdownProps) {
</span>
</div>
</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>

View 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>
);
}

View File

@@ -116,21 +116,21 @@ export default function VisitorMap({ locations, title }: VisitorMapProps) {
{/* Visitor location dots with pulse animation */}
{visitorDots.map((dot, i) => (
<g key={`visitor-${i}`}>
{/* Outer pulse ring */}
{/* Outer pulse ring - base r=5, scaled via CSS transform */}
<circle
cx={dot.x}
cy={dot.y}
r="12"
r="5"
fill="var(--visitor-map-dot)"
opacity="0"
className="visitor-pulse-ring"
style={{ animationDelay: `${i * 0.2}s` }}
/>
{/* Middle pulse ring */}
{/* Middle pulse ring - base r=5, scaled via CSS transform */}
<circle
cx={dot.x}
cy={dot.y}
r="8"
r="5"
fill="var(--visitor-map-dot)"
opacity="0.2"
className="visitor-pulse-ring-mid"