Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions components/posts/MarkdownContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { isSafeSlug } from "@/config/paths";
import { getPostPreviewData } from "@/utils/content/preview";
import CopyButton from "./CopyButton";
import PostLinkPreview from "./PostLinkPreview";
import { baseComponents, remarkPlugins, rehypePlugins } from "./markdownConfig";
import { getImageMeta } from "@/utils/content/imageMeta";
import {
createBaseComponents,
remarkPlugins,
rehypePlugins,
} from "./markdownConfig";

// Internal post links get a hover preview card. Matches relative and absolute
// forms; anything else falls through to a plain anchor.
Expand Down Expand Up @@ -49,7 +54,7 @@ async function CodeBlock({
}

const components: Components = {
...baseComponents,
...createBaseComponents(getImageMeta),
a({ href, children }) {
const slug = href ? postSlugFromHref(href) : null;
const preview = slug ? getPostPreviewData(slug) : null;
Expand Down
26 changes: 22 additions & 4 deletions components/posts/PostImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,39 @@

import Image from "next/image";
import { useState } from "react";
import type { ImageMeta } from "@/utils/content/imageMeta";

export default function PostImage({ src, alt }: { src: string; alt: string }) {
export default function PostImage({
src,
alt,
meta,
}: {
src: string;
alt: string;
meta?: ImageMeta | null;
}) {
const [loaded, setLoaded] = useState(false);
// Without a blur placeholder (drafts, unknown images), fall back to the
// opacity fade so images never pop in abruptly.
const fadeIn = !meta?.blurDataURL;

return (
<figure className="my-1">
<div className="w-full overflow-hidden rounded-xs">
<Image
src={src}
alt={alt}
width={800}
height={600}
width={meta?.width ?? 800}
height={meta?.height ?? 600}
sizes="(max-width: 800px) 100vw, 800px"
placeholder={meta?.blurDataURL ? "blur" : "empty"}
blurDataURL={meta?.blurDataURL}
onLoad={() => setLoaded(true)}
className={`w-full h-auto transition-opacity duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] ${loaded ? "opacity-100" : "opacity-0"}`}
className={`w-full h-auto ${
fadeIn
? `transition-opacity duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] ${loaded ? "opacity-100" : "opacity-0"}`
: ""
}`}
/>
</div>
{alt && (
Expand Down
37 changes: 24 additions & 13 deletions components/posts/markdownConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import remarkGfm from "remark-gfm";
import PostImage from "./PostImage";
import type { ImageMeta } from "@/utils/content/imageMeta";
import "katex/dist/katex.min.css";

export const remarkPlugins: PluggableList = [remarkMath, remarkGfm];
Expand All @@ -13,16 +14,26 @@ export const rehypePlugins: PluggableList = [
[rehypeKatex, { strict: false }],
];

export const baseComponents: Components = {
p({ node, children }) {
const first = node?.children[0] as
| { tagName?: string; properties?: Record<string, unknown> }
| undefined;
if (first?.tagName === "img") {
const src = first.properties?.src as string;
const alt = (first.properties?.alt as string) ?? "";
return <PostImage src={src} alt={alt} />;
}
return <p>{children}</p>;
},
};
// `getImageMeta` is only available server-side (it reads the manifest from
// disk), so the server MarkdownContent passes it and the client admin
// preview renders without dimensions or blur.
export function createBaseComponents(
getImageMeta?: (src: string) => ImageMeta | null
): Components {
return {
p({ node, children }) {
const first = node?.children[0] as
| { tagName?: string; properties?: Record<string, unknown> }
| undefined;
if (first?.tagName === "img") {
const src = first.properties?.src as string;
const alt = (first.properties?.alt as string) ?? "";
const meta = getImageMeta?.(src) ?? null;
return <PostImage src={src} alt={alt} meta={meta} />;
}
return <p>{children}</p>;
},
};
}

export const baseComponents: Components = createBaseComponents();
8 changes: 6 additions & 2 deletions components/visualizations/Gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,20 @@ function Tile({
alt={post?.title ?? image.filename}
fill
sizes={sizes}
placeholder={image.meta ? "blur" : "empty"}
blurDataURL={image.meta?.blurDataURL}
className="object-cover"
/>
</div>
) : (
<Image
src={image.path}
alt={post?.title ?? image.filename}
width={800}
height={800}
width={image.meta?.width ?? 800}
height={image.meta?.height ?? 800}
sizes={sizes}
placeholder={image.meta ? "blur" : "empty"}
blurDataURL={image.meta?.blurDataURL}
className="w-full h-auto bg-japanese-shironeri dark:bg-dark-tag"
/>
)}
Expand Down
1 change: 1 addition & 0 deletions config/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const IMAGES_DIR = path.join(PUBLIC_DIR, "images");
export const IMAGES_DRAFTS_DIR = path.join(IMAGES_DIR, "drafts");
export const DATA_DIR = path.join(PUBLIC_DIR, "data");
export const KNOWLEDGE_MAP_JSON = path.join(DATA_DIR, "knowledge-map.json");
export const IMAGE_META_JSON = path.join(DATA_DIR, "image-meta.json");

export const LIBRARY_MD = path.join(
ROOT_DIR,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"format": "prettier --write .",
"format:check": "prettier --check .",
"generate:knowledge-map": "tsx scripts/generateKnowledgeMap.ts",
"generate:image-meta": "tsx scripts/generateImageMeta.ts",
"prebuild": "pnpm run generate:knowledge-map",
"postbuild": "next-sitemap",
"generate-embeddings": "tsx scripts/generateEmbeddings.ts",
Expand Down
Loading
Loading