perf: image loading, smaller assets, server-rendered code blocks
- Only preload the post cover and latest-post image instead of every image - Drop in-post image quality from 100 to 90 and add responsive sizes - Downscale 2940px screenshots to 1800px; convert linkboard-old to WebP (3.1 MB -> 60 KB) - Make CodeWrapper a server component; only CopyButton ships to the client - Pin the Mermaid CDN script to an exact version for reproducible builds Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
@ -109,7 +109,9 @@ export default async function Post({ params }: { params: Promise<{ slug: string
|
|||
for SEO.
|
||||
</span>
|
||||
)}
|
||||
{meta.coverImage && <ImageWrapper src={meta.coverImage} alt={meta.coverImageAlt} />}
|
||||
{meta.coverImage && (
|
||||
<ImageWrapper src={meta.coverImage} alt={meta.coverImageAlt} priority />
|
||||
)}
|
||||
<div className="prose">{content}</div>
|
||||
|
||||
{meta.tags && (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
'use client'
|
||||
|
||||
import { CopyButton } from '@/components/ui/CopyButton'
|
||||
import { getTextContent } from '@/lib/react-node-text'
|
||||
|
||||
|
|
@ -13,12 +11,9 @@ export function CodeWrapper({ language, children }: CodeWrapperProps) {
|
|||
const lineCount = text.length === 0 ? 1 : text.split('\n').length
|
||||
|
||||
return (
|
||||
<div className="bg-foreground/10 rounded-md overflow-x-auto border border-border bg-fg/10">
|
||||
<div
|
||||
className="flex items-center gap-2 px-2 h-6 border-b border-border w-full"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="space-x-1">
|
||||
<div className="rounded-md overflow-x-auto border border-border bg-fg/10">
|
||||
<div className="flex items-center gap-2 px-2 h-6 border-b border-border w-full">
|
||||
<div className="space-x-1" aria-hidden="true">
|
||||
<span className="w-2 h-2 bg-border rounded-full inline-block"></span>
|
||||
<span className="w-2 h-2 bg-border rounded-full inline-block"></span>
|
||||
<span className="w-2 h-2 bg-border rounded-full inline-block"></span>
|
||||
|
|
@ -28,7 +23,10 @@ export function CodeWrapper({ language, children }: CodeWrapperProps) {
|
|||
</div>
|
||||
|
||||
<div className="flex">
|
||||
<div className="select-none text-right py-2 px-3 border-r border-border text-white font-mono text-sm leading-6 min-w-6">
|
||||
<div
|
||||
className="select-none text-right py-2 px-3 border-r border-border font-mono text-sm leading-6 min-w-6"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{Array.from({ length: lineCount }, (_, i) => (
|
||||
<span className="block text-fg-dim" key={i}>
|
||||
{i + 1}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,19 @@ import Image from 'next/image'
|
|||
type ImageWrapperProps = {
|
||||
src: string
|
||||
alt?: string
|
||||
/** Visible caption. Falls back to the alt text when omitted. */
|
||||
caption?: string
|
||||
isListing?: boolean
|
||||
/** Preload the image. Only set this for above-the-fold images (covers, latest post). */
|
||||
priority?: boolean
|
||||
}
|
||||
|
||||
export function ImageWrapper({ src, alt, isListing }: ImageWrapperProps) {
|
||||
export function ImageWrapper({ src, alt, caption, isListing, priority }: ImageWrapperProps) {
|
||||
const captionText = caption ?? alt
|
||||
// When the caption just repeats the alt text, hide it from screen readers so it isn't
|
||||
// announced twice
|
||||
const captionIsAlt = caption === undefined
|
||||
|
||||
return (
|
||||
<figure className={cn('flex flex-col items-center gap-1 mb-4 w-full', isListing && 'h-full')}>
|
||||
<div className="border border-border rounded-xl w-full overflow-hidden flex-1">
|
||||
|
|
@ -27,13 +36,19 @@ export function ImageWrapper({ src, alt, isListing }: ImageWrapperProps) {
|
|||
alt={alt ?? ''}
|
||||
width={isListing ? 400 : 900}
|
||||
height={isListing ? 240 : 600}
|
||||
quality={isListing ? 75 : 100}
|
||||
priority={true}
|
||||
sizes={isListing ? '(min-width: 768px) 400px, 100vw' : '(min-width: 896px) 900px, 100vw'}
|
||||
quality={isListing ? 75 : 90}
|
||||
priority={priority}
|
||||
className="w-full mx-auto object-contain h-full max-h-150"
|
||||
/>
|
||||
</div>
|
||||
{!isListing && alt && (
|
||||
<figcaption className="block text-xs text-fg-dim font-mono text-center">{alt}</figcaption>
|
||||
{!isListing && captionText && (
|
||||
<figcaption
|
||||
className="block text-xs text-fg-dim font-mono text-center"
|
||||
aria-hidden={captionIsAlt || undefined}
|
||||
>
|
||||
{captionText}
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,14 @@ export function Post({ post, isLatest, showImageSlot }: PostProps) {
|
|||
!coverImage && 'hidden md:block',
|
||||
)}
|
||||
>
|
||||
{coverImage && <ImageWrapper src={coverImage} alt={coverImageAlt ?? title} isListing />}
|
||||
{coverImage && (
|
||||
<ImageWrapper
|
||||
src={coverImage}
|
||||
alt={coverImageAlt ?? title}
|
||||
isListing
|
||||
priority={isLatest}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={cn('flex flex-col gap-1 w-full', isLatest && 'flex-1')}>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ The new version has focus on ease of use for not tech-savvy users, with a simple
|
|||
**Status:** Active / Maintained
|
||||
|
||||
**Screenshots:**
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export async function renderMermaidToSvg(chart: string): Promise<string> {
|
|||
<html><body><div id="target"></div></body></html>
|
||||
`)
|
||||
await page.addScriptTag({
|
||||
url: 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js',
|
||||
url: 'https://cdn.jsdelivr.net/npm/mermaid@11.17.2/dist/mermaid.min.js',
|
||||
})
|
||||
|
||||
const svg = await page.evaluate(async (source) => {
|
||||
|
|
|
|||
|
|
@ -96,7 +96,9 @@ export async function getPostBySlug(slug: string, postDirectory: string = postsD
|
|||
typeof props['data-language'] === 'string' ? props['data-language'] : undefined
|
||||
return <CodeWrapper language={language}>{children}</CodeWrapper>
|
||||
},
|
||||
img: ({ src, alt }: { src: string; alt: string }) => <ImageWrapper src={src} alt={alt} />,
|
||||
img: ({ src, alt, title }: { src: string; alt: string; title?: string }) => (
|
||||
<ImageWrapper src={src} alt={alt} caption={title} />
|
||||
),
|
||||
a: ({ href, children, ...props }: { href?: string; children: ReactNode }) => {
|
||||
if (href?.startsWith('http')) return <ExternalLink href={href}>{children}</ExternalLink>
|
||||
// Only internal routes go through next/link; anchors, mailto:, footnotes etc. stay plain
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { NextConfig } from 'next'
|
|||
|
||||
const nextConfig: NextConfig = {
|
||||
images: {
|
||||
qualities: [75, 100],
|
||||
qualities: [75, 90],
|
||||
},
|
||||
env: {
|
||||
GIT_COMMIT_HASH: process.env.SOURCE_COMMIT,
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 471 KiB After Width: | Height: | Size: 267 KiB |
|
Before Width: | Height: | Size: 708 KiB After Width: | Height: | Size: 419 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 3 MiB |
BIN
public/posts/linkboard-old.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |