Fix local images and em dash rendering
Some checks are pending
Build / build (push) Waiting to run

This commit is contained in:
Neeldhara Misra 2026-06-25 05:21:34 +05:30
parent 4b3aa3022e
commit 632543db4d
55 changed files with 432 additions and 117 deletions

View file

@ -12,6 +12,7 @@ import tailwindcss from "@tailwindcss/vite";
import remarkCallouts from "./src/remark/callouts.mjs"; import remarkCallouts from "./src/remark/callouts.mjs";
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs"; import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs"; import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
import remarkTypography from "./src/remark/typography.mjs";
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js"; import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
const remarkPlugins = [ const remarkPlugins = [
@ -19,6 +20,7 @@ const remarkPlugins = [
remarkInlineFootnotes, remarkInlineFootnotes,
remarkCodeFenceLanguages, remarkCodeFenceLanguages,
remarkMath, remarkMath,
remarkTypography,
remarkCallouts, remarkCallouts,
]; ];
const rehypePlugins = [rehypeKatex]; const rehypePlugins = [rehypeKatex];

View file

@ -3,6 +3,7 @@
// all pages through the use of the <BaseHead /> component. // all pages through the use of the <BaseHead /> component.
import '../styles/global.css'; import '../styles/global.css';
import { SITE_TITLE, SITE_METADATA } from '../consts'; import { SITE_TITLE, SITE_METADATA } from '../consts';
import { typographicText } from '@/lib/typography';
interface Props { interface Props {
title: string; title: string;
@ -13,8 +14,8 @@ interface Props {
const canonicalURL = new URL(Astro.url.pathname, Astro.site); const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image } = Astro.props; const { title, description, image } = Astro.props;
const finalTitle = title || SITE_METADATA.title.default; const finalTitle = typographicText(title || SITE_METADATA.title.default);
const finalDescription = description || SITE_METADATA.description; const finalDescription = typographicText(description || SITE_METADATA.description);
const finalImage = image || SITE_METADATA.openGraph.images[0].url; const finalImage = image || SITE_METADATA.openGraph.images[0].url;
const imageURL = new URL(finalImage, Astro.url); const imageURL = new URL(finalImage, Astro.url);
--- ---

View file

@ -1,5 +1,6 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { format } from "date-fns"; import { format } from "date-fns";
import { typographicText } from "@/lib/typography";
const getReadingTime = (body: string | undefined) => { const getReadingTime = (body: string | undefined) => {
const words = (body ?? "") const words = (body ?? "")
@ -18,6 +19,7 @@ const BlogPost = ({
children: ReactNode; children: ReactNode;
}) => { }) => {
const { title, image, pubDate } = post.data; const { title, image, pubDate } = post.data;
const displayTitle = typographicText(title);
const readingTime = getReadingTime(post.body); const readingTime = getReadingTime(post.body);
return ( return (
@ -30,12 +32,12 @@ const BlogPost = ({
</time> </time>
<span>{readingTime} min read</span> <span>{readingTime} min read</span>
</div> </div>
<h1 className="blog-post-title">{title}</h1> <h1 className="blog-post-title">{displayTitle}</h1>
</header> </header>
{image ? ( {image ? (
<figure className="blog-post-hero-image"> <figure className="blog-post-hero-image">
<img src={image} alt={title} /> <img src={image} alt={displayTitle} />
</figure> </figure>
) : null} ) : null}

View file

@ -4,6 +4,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Calendar, Clock, User } from "lucide-react"; import { Calendar, Clock, User } from "lucide-react";
import { typographicText } from "@/lib/typography";
import { PlusSigns } from "../icons/plus-signs"; import { PlusSigns } from "../icons/plus-signs";
const BlogPosts = ({ const BlogPosts = ({
@ -27,11 +28,13 @@ const BlogPosts = ({
post: any; post: any;
featured?: boolean; featured?: boolean;
}) => { }) => {
const displayTitle = typographicText(post.data.title);
if (post.data.image) { if (post.data.image) {
return ( return (
<img <img
src={post.data.image} src={post.data.image}
alt={post.data.title} alt={displayTitle}
className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]" className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]"
/> />
); );
@ -44,7 +47,7 @@ const BlogPosts = ({
featured ? "font-fraunces text-2xl" : "font-fraunces text-lg" featured ? "font-fraunces text-2xl" : "font-fraunces text-lg"
} }
> >
{post.data.title} {displayTitle}
</span> </span>
</div> </div>
); );
@ -82,10 +85,10 @@ const BlogPosts = ({
Featured Post Featured Post
</Badge> </Badge>
<h2 className="blog-card-title-featured mb-3 group-hover:underline"> <h2 className="blog-card-title-featured mb-3 group-hover:underline">
{featuredPost.data.title} {typographicText(featuredPost.data.title)}
</h2> </h2>
<p className="text-muted-foreground mb-4 line-clamp-3 text-base"> <p className="text-muted-foreground mb-4 line-clamp-3 text-base">
{featuredPost.data.description} {typographicText(featuredPost.data.description)}
</p> </p>
<div className="mb-6 flex gap-4"> <div className="mb-6 flex gap-4">
@ -128,10 +131,10 @@ const BlogPosts = ({
</div> </div>
<div className="px-4 pb-5 pt-2"> <div className="px-4 pb-5 pt-2">
<h2 className="mb-2 text-xl font-semibold group-hover:underline"> <h2 className="mb-2 text-xl font-semibold group-hover:underline">
{post.data.title} {typographicText(post.data.title)}
</h2> </h2>
<p className="text-muted-foreground line-clamp-2 text-sm"> <p className="text-muted-foreground line-clamp-2 text-sm">
{post.data.description} {typographicText(post.data.description)}
</p> </p>
<div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs"> <div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs">

View file

@ -0,0 +1,5 @@
const EM_DASH_PATTERN = /---/g;
export function typographicText(value: string | null | undefined) {
return (value ?? "").replace(EM_DASH_PATTERN, "—");
}

View file

@ -2,6 +2,7 @@ import rss from "@astrojs/rss";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts"; import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
import { ACTIVE_BLOG_SITE } from "../blog-sites.js"; import { ACTIVE_BLOG_SITE } from "../blog-sites.js";
import { typographicText } from "@/lib/typography";
export async function GET(context) { export async function GET(context) {
const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort( const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort(
@ -10,10 +11,12 @@ export async function GET(context) {
return rss({ return rss({
title: SITE_TITLE, title: SITE_TITLE,
description: SITE_DESCRIPTION, description: typographicText(SITE_DESCRIPTION),
site: context.site, site: context.site,
items: posts.map((post) => ({ items: posts.map((post) => ({
...post.data, ...post.data,
title: typographicText(post.data.title),
description: typographicText(post.data.description),
link: `/${post.id.replace(/\/index$/, "")}/`, link: `/${post.id.replace(/\/index$/, "")}/`,
})), })),
}); });

View file

@ -0,0 +1,22 @@
const EM_DASH_PATTERN = /---/g;
function visitTextNodes(node) {
if (!node || typeof node !== "object") return;
if (node.type === "text" && typeof node.value === "string") {
node.value = node.value.replace(EM_DASH_PATTERN, "—");
return;
}
if (!Array.isArray(node.children)) return;
for (const child of node.children) {
visitTextNodes(child);
}
}
export default function remarkTypography() {
return (tree) => {
visitTextNodes(tree);
};
}

View file

@ -2,6 +2,7 @@
import { defineConfig } from "astro/config"; import { defineConfig } from "astro/config";
import mdx from "@astrojs/mdx"; import mdx from "@astrojs/mdx";
import rehypeKatex from "rehype-katex"; import rehypeKatex from "rehype-katex";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math"; import remarkMath from "remark-math";
import sitemap from "@astrojs/sitemap"; import sitemap from "@astrojs/sitemap";
@ -9,9 +10,19 @@ import react from "@astrojs/react";
// @ts-ignore - TailwindCSS v4 Vite plugin has type compatibility issues with Astro // @ts-ignore - TailwindCSS v4 Vite plugin has type compatibility issues with Astro
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import remarkCallouts from "./src/remark/callouts.mjs"; import remarkCallouts from "./src/remark/callouts.mjs";
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
import remarkTypography from "./src/remark/typography.mjs";
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js"; import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
const remarkPlugins = [remarkMath, remarkCallouts]; const remarkPlugins = [
remarkGfm,
remarkInlineFootnotes,
remarkCodeFenceLanguages,
remarkMath,
remarkTypography,
remarkCallouts,
];
const rehypePlugins = [rehypeKatex]; const rehypePlugins = [rehypeKatex];
// https://astro.build/config // https://astro.build/config

View file

@ -3,6 +3,7 @@
// all pages through the use of the <BaseHead /> component. // all pages through the use of the <BaseHead /> component.
import '../styles/global.css'; import '../styles/global.css';
import { SITE_TITLE, SITE_METADATA } from '../consts'; import { SITE_TITLE, SITE_METADATA } from '../consts';
import { typographicText } from '@/lib/typography';
interface Props { interface Props {
title: string; title: string;
@ -13,8 +14,8 @@ interface Props {
const canonicalURL = new URL(Astro.url.pathname, Astro.site); const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image } = Astro.props; const { title, description, image } = Astro.props;
const finalTitle = title || SITE_METADATA.title.default; const finalTitle = typographicText(title || SITE_METADATA.title.default);
const finalDescription = description || SITE_METADATA.description; const finalDescription = typographicText(description || SITE_METADATA.description);
const finalImage = image || SITE_METADATA.openGraph.images[0].url; const finalImage = image || SITE_METADATA.openGraph.images[0].url;
const imageURL = new URL(finalImage, Astro.url); const imageURL = new URL(finalImage, Astro.url);
--- ---

View file

@ -1,4 +1,5 @@
import { format } from "date-fns"; import { format } from "date-fns";
import { typographicText } from "@/lib/typography";
const getReadingTime = (body: string | undefined) => { const getReadingTime = (body: string | undefined) => {
const words = (body ?? "") const words = (body ?? "")
@ -17,12 +18,13 @@ const BlogPost = ({
children: React.ReactNode; children: React.ReactNode;
}) => { }) => {
const { title, pubDate } = post.data; const { title, pubDate } = post.data;
const displayTitle = typographicText(title);
const readingTime = getReadingTime(post.body); const readingTime = getReadingTime(post.body);
return ( return (
<section className="container px-6 py-2 md:py-6"> <section className="container px-6 py-2 md:py-6">
<article className="poetry-post mx-auto"> <article className="poetry-post mx-auto">
<h1 className="poetry-post-title">{title}</h1> <h1 className="poetry-post-title">{displayTitle}</h1>
<div className="poetry-post-meta"> <div className="poetry-post-meta">
<time dateTime={pubDate.toISOString()}> <time dateTime={pubDate.toISOString()}>
{format(pubDate, "MMMM d, yyyy")} {format(pubDate, "MMMM d, yyyy")}

View file

@ -4,6 +4,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Calendar, Clock, User } from "lucide-react"; import { Calendar, Clock, User } from "lucide-react";
import { typographicText } from "@/lib/typography";
import { PlusSigns } from "../icons/plus-signs"; import { PlusSigns } from "../icons/plus-signs";
const BlogPosts = ({ const BlogPosts = ({
@ -27,11 +28,13 @@ const BlogPosts = ({
post: any; post: any;
featured?: boolean; featured?: boolean;
}) => { }) => {
const displayTitle = typographicText(post.data.title);
if (post.data.image) { if (post.data.image) {
return ( return (
<img <img
src={post.data.image} src={post.data.image}
alt={post.data.title} alt={displayTitle}
className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]" className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]"
/> />
); );
@ -44,7 +47,7 @@ const BlogPosts = ({
featured ? "font-fraunces text-2xl" : "font-fraunces text-lg" featured ? "font-fraunces text-2xl" : "font-fraunces text-lg"
} }
> >
{post.data.title} {displayTitle}
</span> </span>
</div> </div>
); );
@ -82,10 +85,10 @@ const BlogPosts = ({
Featured Post Featured Post
</Badge> </Badge>
<h2 className="blog-card-title-featured mb-3 group-hover:underline"> <h2 className="blog-card-title-featured mb-3 group-hover:underline">
{featuredPost.data.title} {typographicText(featuredPost.data.title)}
</h2> </h2>
<p className="text-muted-foreground mb-4 line-clamp-3 text-base"> <p className="text-muted-foreground mb-4 line-clamp-3 text-base">
{featuredPost.data.description} {typographicText(featuredPost.data.description)}
</p> </p>
<div className="mb-6 flex gap-4"> <div className="mb-6 flex gap-4">
@ -128,10 +131,10 @@ const BlogPosts = ({
</div> </div>
<div className="px-4 pb-5 pt-2"> <div className="px-4 pb-5 pt-2">
<h2 className="mb-2 text-xl font-semibold group-hover:underline"> <h2 className="mb-2 text-xl font-semibold group-hover:underline">
{post.data.title} {typographicText(post.data.title)}
</h2> </h2>
<p className="text-muted-foreground line-clamp-2 text-sm"> <p className="text-muted-foreground line-clamp-2 text-sm">
{post.data.description} {typographicText(post.data.description)}
</p> </p>
<div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs"> <div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs">

View file

@ -0,0 +1,5 @@
const EM_DASH_PATTERN = /---/g;
export function typographicText(value: string | null | undefined) {
return (value ?? "").replace(EM_DASH_PATTERN, "—");
}

View file

@ -2,6 +2,7 @@ import rss from "@astrojs/rss";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts"; import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
import { ACTIVE_BLOG_SITE } from "../blog-sites.js"; import { ACTIVE_BLOG_SITE } from "../blog-sites.js";
import { typographicText } from "@/lib/typography";
export async function GET(context) { export async function GET(context) {
const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort( const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort(
@ -10,10 +11,12 @@ export async function GET(context) {
return rss({ return rss({
title: SITE_TITLE, title: SITE_TITLE,
description: SITE_DESCRIPTION, description: typographicText(SITE_DESCRIPTION),
site: context.site, site: context.site,
items: posts.map((post) => ({ items: posts.map((post) => ({
...post.data, ...post.data,
title: typographicText(post.data.title),
description: typographicText(post.data.description),
link: `/${post.id.replace(/\/index$/, "")}/`, link: `/${post.id.replace(/\/index$/, "")}/`,
})), })),
}); });

View file

@ -0,0 +1,22 @@
const EM_DASH_PATTERN = /---/g;
function visitTextNodes(node) {
if (!node || typeof node !== "object") return;
if (node.type === "text" && typeof node.value === "string") {
node.value = node.value.replace(EM_DASH_PATTERN, "—");
return;
}
if (!Array.isArray(node.children)) return;
for (const child of node.children) {
visitTextNodes(child);
}
}
export default function remarkTypography() {
return (tree) => {
visitTextNodes(tree);
};
}

View file

@ -12,6 +12,7 @@ import tailwindcss from "@tailwindcss/vite";
import remarkCallouts from "./src/remark/callouts.mjs"; import remarkCallouts from "./src/remark/callouts.mjs";
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs"; import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs"; import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
import remarkTypography from "./src/remark/typography.mjs";
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js"; import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
const remarkPlugins = [ const remarkPlugins = [
@ -19,6 +20,7 @@ const remarkPlugins = [
remarkInlineFootnotes, remarkInlineFootnotes,
remarkCodeFenceLanguages, remarkCodeFenceLanguages,
remarkMath, remarkMath,
remarkTypography,
remarkCallouts, remarkCallouts,
]; ];
const rehypePlugins = [rehypeKatex]; const rehypePlugins = [rehypeKatex];

View file

@ -3,6 +3,7 @@
// all pages through the use of the <BaseHead /> component. // all pages through the use of the <BaseHead /> component.
import '../styles/global.css'; import '../styles/global.css';
import { SITE_TITLE, SITE_METADATA } from '../consts'; import { SITE_TITLE, SITE_METADATA } from '../consts';
import { typographicText } from '@/lib/typography';
interface Props { interface Props {
title: string; title: string;
@ -13,8 +14,8 @@ interface Props {
const canonicalURL = new URL(Astro.url.pathname, Astro.site); const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image } = Astro.props; const { title, description, image } = Astro.props;
const finalTitle = title || SITE_METADATA.title.default; const finalTitle = typographicText(title || SITE_METADATA.title.default);
const finalDescription = description || SITE_METADATA.description; const finalDescription = typographicText(description || SITE_METADATA.description);
const finalImage = image || SITE_METADATA.openGraph.images[0].url; const finalImage = image || SITE_METADATA.openGraph.images[0].url;
const imageURL = new URL(finalImage, Astro.url); const imageURL = new URL(finalImage, Astro.url);
--- ---

View file

@ -1,5 +1,6 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { format } from "date-fns"; import { format } from "date-fns";
import { typographicText } from "@/lib/typography";
const getReadingTime = (body: string | undefined) => { const getReadingTime = (body: string | undefined) => {
const words = (body ?? "") const words = (body ?? "")
@ -18,6 +19,7 @@ const BlogPost = ({
children: ReactNode; children: ReactNode;
}) => { }) => {
const { title, image, pubDate } = post.data; const { title, image, pubDate } = post.data;
const displayTitle = typographicText(title);
const readingTime = getReadingTime(post.body); const readingTime = getReadingTime(post.body);
return ( return (
@ -30,12 +32,12 @@ const BlogPost = ({
</time> </time>
<span>{readingTime} min read</span> <span>{readingTime} min read</span>
</div> </div>
<h1 className="blog-post-title">{title}</h1> <h1 className="blog-post-title">{displayTitle}</h1>
</header> </header>
{image ? ( {image ? (
<figure className="blog-post-hero-image"> <figure className="blog-post-hero-image">
<img src={image} alt={title} /> <img src={image} alt={displayTitle} />
</figure> </figure>
) : null} ) : null}

View file

@ -4,6 +4,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Calendar, Clock, User } from "lucide-react"; import { Calendar, Clock, User } from "lucide-react";
import { typographicText } from "@/lib/typography";
import { PlusSigns } from "../icons/plus-signs"; import { PlusSigns } from "../icons/plus-signs";
const BlogPosts = ({ const BlogPosts = ({
@ -27,11 +28,13 @@ const BlogPosts = ({
post: any; post: any;
featured?: boolean; featured?: boolean;
}) => { }) => {
const displayTitle = typographicText(post.data.title);
if (post.data.image) { if (post.data.image) {
return ( return (
<img <img
src={post.data.image} src={post.data.image}
alt={post.data.title} alt={displayTitle}
className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]" className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]"
/> />
); );
@ -44,7 +47,7 @@ const BlogPosts = ({
featured ? "font-fraunces text-2xl" : "font-fraunces text-lg" featured ? "font-fraunces text-2xl" : "font-fraunces text-lg"
} }
> >
{post.data.title} {displayTitle}
</span> </span>
</div> </div>
); );
@ -82,10 +85,10 @@ const BlogPosts = ({
Featured Post Featured Post
</Badge> </Badge>
<h2 className="blog-card-title-featured mb-3 group-hover:underline"> <h2 className="blog-card-title-featured mb-3 group-hover:underline">
{featuredPost.data.title} {typographicText(featuredPost.data.title)}
</h2> </h2>
<p className="text-muted-foreground mb-4 line-clamp-3 text-base"> <p className="text-muted-foreground mb-4 line-clamp-3 text-base">
{featuredPost.data.description} {typographicText(featuredPost.data.description)}
</p> </p>
<div className="mb-6 flex gap-4"> <div className="mb-6 flex gap-4">
@ -128,10 +131,10 @@ const BlogPosts = ({
</div> </div>
<div className="px-4 pb-5 pt-2"> <div className="px-4 pb-5 pt-2">
<h2 className="mb-2 text-xl font-semibold group-hover:underline"> <h2 className="mb-2 text-xl font-semibold group-hover:underline">
{post.data.title} {typographicText(post.data.title)}
</h2> </h2>
<p className="text-muted-foreground line-clamp-2 text-sm"> <p className="text-muted-foreground line-clamp-2 text-sm">
{post.data.description} {typographicText(post.data.description)}
</p> </p>
<div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs"> <div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs">

View file

@ -0,0 +1,5 @@
const EM_DASH_PATTERN = /---/g;
export function typographicText(value: string | null | undefined) {
return (value ?? "").replace(EM_DASH_PATTERN, "—");
}

View file

@ -2,6 +2,7 @@ import rss from "@astrojs/rss";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts"; import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
import { ACTIVE_BLOG_SITE } from "../blog-sites.js"; import { ACTIVE_BLOG_SITE } from "../blog-sites.js";
import { typographicText } from "@/lib/typography";
export async function GET(context) { export async function GET(context) {
const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort( const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort(
@ -10,10 +11,12 @@ export async function GET(context) {
return rss({ return rss({
title: SITE_TITLE, title: SITE_TITLE,
description: SITE_DESCRIPTION, description: typographicText(SITE_DESCRIPTION),
site: context.site, site: context.site,
items: posts.map((post) => ({ items: posts.map((post) => ({
...post.data, ...post.data,
title: typographicText(post.data.title),
description: typographicText(post.data.description),
link: `/${post.id.replace(/\/index$/, "")}/`, link: `/${post.id.replace(/\/index$/, "")}/`,
})), })),
}); });

View file

@ -0,0 +1,22 @@
const EM_DASH_PATTERN = /---/g;
function visitTextNodes(node) {
if (!node || typeof node !== "object") return;
if (node.type === "text" && typeof node.value === "string") {
node.value = node.value.replace(EM_DASH_PATTERN, "—");
return;
}
if (!Array.isArray(node.children)) return;
for (const child of node.children) {
visitTextNodes(child);
}
}
export default function remarkTypography() {
return (tree) => {
visitTextNodes(tree);
};
}

View file

@ -12,6 +12,7 @@ import tailwindcss from "@tailwindcss/vite";
import remarkCallouts from "./src/remark/callouts.mjs"; import remarkCallouts from "./src/remark/callouts.mjs";
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs"; import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs"; import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
import remarkTypography from "./src/remark/typography.mjs";
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js"; import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
const remarkPlugins = [ const remarkPlugins = [
@ -19,6 +20,7 @@ const remarkPlugins = [
remarkInlineFootnotes, remarkInlineFootnotes,
remarkCodeFenceLanguages, remarkCodeFenceLanguages,
remarkMath, remarkMath,
remarkTypography,
remarkCallouts, remarkCallouts,
]; ];
const rehypePlugins = [rehypeKatex]; const rehypePlugins = [rehypeKatex];

View file

@ -3,6 +3,7 @@
// all pages through the use of the <BaseHead /> component. // all pages through the use of the <BaseHead /> component.
import '../styles/global.css'; import '../styles/global.css';
import { SITE_TITLE, SITE_METADATA } from '../consts'; import { SITE_TITLE, SITE_METADATA } from '../consts';
import { typographicText } from '@/lib/typography';
interface Props { interface Props {
title: string; title: string;
@ -13,8 +14,8 @@ interface Props {
const canonicalURL = new URL(Astro.url.pathname, Astro.site); const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image } = Astro.props; const { title, description, image } = Astro.props;
const finalTitle = title || SITE_METADATA.title.default; const finalTitle = typographicText(title || SITE_METADATA.title.default);
const finalDescription = description || SITE_METADATA.description; const finalDescription = typographicText(description || SITE_METADATA.description);
const finalImage = image || SITE_METADATA.openGraph.images[0].url; const finalImage = image || SITE_METADATA.openGraph.images[0].url;
const imageURL = new URL(finalImage, Astro.url); const imageURL = new URL(finalImage, Astro.url);
--- ---

View file

@ -1,5 +1,6 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { format } from "date-fns"; import { format } from "date-fns";
import { typographicText } from "@/lib/typography";
const getReadingTime = (body: string | undefined) => { const getReadingTime = (body: string | undefined) => {
const words = (body ?? "") const words = (body ?? "")
@ -18,6 +19,7 @@ const BlogPost = ({
children: ReactNode; children: ReactNode;
}) => { }) => {
const { title, image, pubDate } = post.data; const { title, image, pubDate } = post.data;
const displayTitle = typographicText(title);
const readingTime = getReadingTime(post.body); const readingTime = getReadingTime(post.body);
return ( return (
@ -30,12 +32,12 @@ const BlogPost = ({
</time> </time>
<span>{readingTime} min read</span> <span>{readingTime} min read</span>
</div> </div>
<h1 className="blog-post-title">{title}</h1> <h1 className="blog-post-title">{displayTitle}</h1>
</header> </header>
{image ? ( {image ? (
<figure className="blog-post-hero-image"> <figure className="blog-post-hero-image">
<img src={image} alt={title} /> <img src={image} alt={displayTitle} />
</figure> </figure>
) : null} ) : null}

View file

@ -4,6 +4,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Calendar, Clock, User } from "lucide-react"; import { Calendar, Clock, User } from "lucide-react";
import { typographicText } from "@/lib/typography";
import { PlusSigns } from "../icons/plus-signs"; import { PlusSigns } from "../icons/plus-signs";
const BlogPosts = ({ const BlogPosts = ({
@ -27,11 +28,13 @@ const BlogPosts = ({
post: any; post: any;
featured?: boolean; featured?: boolean;
}) => { }) => {
const displayTitle = typographicText(post.data.title);
if (post.data.image) { if (post.data.image) {
return ( return (
<img <img
src={post.data.image} src={post.data.image}
alt={post.data.title} alt={displayTitle}
className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]" className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]"
/> />
); );
@ -44,7 +47,7 @@ const BlogPosts = ({
featured ? "font-fraunces text-2xl" : "font-fraunces text-lg" featured ? "font-fraunces text-2xl" : "font-fraunces text-lg"
} }
> >
{post.data.title} {displayTitle}
</span> </span>
</div> </div>
); );
@ -82,10 +85,10 @@ const BlogPosts = ({
Featured Post Featured Post
</Badge> </Badge>
<h2 className="blog-card-title-featured mb-3 group-hover:underline"> <h2 className="blog-card-title-featured mb-3 group-hover:underline">
{featuredPost.data.title} {typographicText(featuredPost.data.title)}
</h2> </h2>
<p className="text-muted-foreground mb-4 line-clamp-3 text-base"> <p className="text-muted-foreground mb-4 line-clamp-3 text-base">
{featuredPost.data.description} {typographicText(featuredPost.data.description)}
</p> </p>
<div className="mb-6 flex gap-4"> <div className="mb-6 flex gap-4">
@ -128,10 +131,10 @@ const BlogPosts = ({
</div> </div>
<div className="px-4 pb-5 pt-2"> <div className="px-4 pb-5 pt-2">
<h2 className="mb-2 text-xl font-semibold group-hover:underline"> <h2 className="mb-2 text-xl font-semibold group-hover:underline">
{post.data.title} {typographicText(post.data.title)}
</h2> </h2>
<p className="text-muted-foreground line-clamp-2 text-sm"> <p className="text-muted-foreground line-clamp-2 text-sm">
{post.data.description} {typographicText(post.data.description)}
</p> </p>
<div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs"> <div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs">

View file

@ -0,0 +1,5 @@
const EM_DASH_PATTERN = /---/g;
export function typographicText(value: string | null | undefined) {
return (value ?? "").replace(EM_DASH_PATTERN, "—");
}

View file

@ -2,6 +2,7 @@ import rss from "@astrojs/rss";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts"; import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
import { ACTIVE_BLOG_SITE } from "../blog-sites.js"; import { ACTIVE_BLOG_SITE } from "../blog-sites.js";
import { typographicText } from "@/lib/typography";
export async function GET(context) { export async function GET(context) {
const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort( const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort(
@ -10,10 +11,12 @@ export async function GET(context) {
return rss({ return rss({
title: SITE_TITLE, title: SITE_TITLE,
description: SITE_DESCRIPTION, description: typographicText(SITE_DESCRIPTION),
site: context.site, site: context.site,
items: posts.map((post) => ({ items: posts.map((post) => ({
...post.data, ...post.data,
title: typographicText(post.data.title),
description: typographicText(post.data.description),
link: `/${post.id.replace(/\/index$/, "")}/`, link: `/${post.id.replace(/\/index$/, "")}/`,
})), })),
}); });

View file

@ -0,0 +1,22 @@
const EM_DASH_PATTERN = /---/g;
function visitTextNodes(node) {
if (!node || typeof node !== "object") return;
if (node.type === "text" && typeof node.value === "string") {
node.value = node.value.replace(EM_DASH_PATTERN, "—");
return;
}
if (!Array.isArray(node.children)) return;
for (const child of node.children) {
visitTextNodes(child);
}
}
export default function remarkTypography() {
return (tree) => {
visitTextNodes(tree);
};
}

View file

@ -12,6 +12,7 @@ import tailwindcss from "@tailwindcss/vite";
import remarkCallouts from "./src/remark/callouts.mjs"; import remarkCallouts from "./src/remark/callouts.mjs";
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs"; import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs"; import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
import remarkTypography from "./src/remark/typography.mjs";
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js"; import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
const remarkPlugins = [ const remarkPlugins = [
@ -19,6 +20,7 @@ const remarkPlugins = [
remarkInlineFootnotes, remarkInlineFootnotes,
remarkCodeFenceLanguages, remarkCodeFenceLanguages,
remarkMath, remarkMath,
remarkTypography,
remarkCallouts, remarkCallouts,
]; ];
const rehypePlugins = [rehypeKatex]; const rehypePlugins = [rehypeKatex];

View file

@ -3,6 +3,7 @@
// all pages through the use of the <BaseHead /> component. // all pages through the use of the <BaseHead /> component.
import '../styles/global.css'; import '../styles/global.css';
import { SITE_TITLE, SITE_METADATA } from '../consts'; import { SITE_TITLE, SITE_METADATA } from '../consts';
import { typographicText } from '@/lib/typography';
interface Props { interface Props {
title: string; title: string;
@ -13,8 +14,8 @@ interface Props {
const canonicalURL = new URL(Astro.url.pathname, Astro.site); const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image } = Astro.props; const { title, description, image } = Astro.props;
const finalTitle = title || SITE_METADATA.title.default; const finalTitle = typographicText(title || SITE_METADATA.title.default);
const finalDescription = description || SITE_METADATA.description; const finalDescription = typographicText(description || SITE_METADATA.description);
const finalImage = image || SITE_METADATA.openGraph.images[0].url; const finalImage = image || SITE_METADATA.openGraph.images[0].url;
const imageURL = new URL(finalImage, Astro.url); const imageURL = new URL(finalImage, Astro.url);
--- ---

View file

@ -1,5 +1,6 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { format } from "date-fns"; import { format } from "date-fns";
import { typographicText } from "@/lib/typography";
const getReadingTime = (body: string | undefined) => { const getReadingTime = (body: string | undefined) => {
const words = (body ?? "") const words = (body ?? "")
@ -18,6 +19,7 @@ const BlogPost = ({
children: ReactNode; children: ReactNode;
}) => { }) => {
const { title, image, pubDate } = post.data; const { title, image, pubDate } = post.data;
const displayTitle = typographicText(title);
const readingTime = getReadingTime(post.body); const readingTime = getReadingTime(post.body);
return ( return (
@ -30,12 +32,12 @@ const BlogPost = ({
</time> </time>
<span>{readingTime} min read</span> <span>{readingTime} min read</span>
</div> </div>
<h1 className="blog-post-title">{title}</h1> <h1 className="blog-post-title">{displayTitle}</h1>
</header> </header>
{image ? ( {image ? (
<figure className="blog-post-hero-image"> <figure className="blog-post-hero-image">
<img src={image} alt={title} /> <img src={image} alt={displayTitle} />
</figure> </figure>
) : null} ) : null}

View file

@ -4,6 +4,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Calendar, Clock, User } from "lucide-react"; import { Calendar, Clock, User } from "lucide-react";
import { typographicText } from "@/lib/typography";
import { PlusSigns } from "../icons/plus-signs"; import { PlusSigns } from "../icons/plus-signs";
const BlogPosts = ({ const BlogPosts = ({
@ -27,11 +28,13 @@ const BlogPosts = ({
post: any; post: any;
featured?: boolean; featured?: boolean;
}) => { }) => {
const displayTitle = typographicText(post.data.title);
if (post.data.image) { if (post.data.image) {
return ( return (
<img <img
src={post.data.image} src={post.data.image}
alt={post.data.title} alt={displayTitle}
className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]" className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]"
/> />
); );
@ -44,7 +47,7 @@ const BlogPosts = ({
featured ? "font-fraunces text-2xl" : "font-fraunces text-lg" featured ? "font-fraunces text-2xl" : "font-fraunces text-lg"
} }
> >
{post.data.title} {displayTitle}
</span> </span>
</div> </div>
); );
@ -82,10 +85,10 @@ const BlogPosts = ({
Featured Post Featured Post
</Badge> </Badge>
<h2 className="blog-card-title-featured mb-3 group-hover:underline"> <h2 className="blog-card-title-featured mb-3 group-hover:underline">
{featuredPost.data.title} {typographicText(featuredPost.data.title)}
</h2> </h2>
<p className="text-muted-foreground mb-4 line-clamp-3 text-base"> <p className="text-muted-foreground mb-4 line-clamp-3 text-base">
{featuredPost.data.description} {typographicText(featuredPost.data.description)}
</p> </p>
<div className="mb-6 flex gap-4"> <div className="mb-6 flex gap-4">
@ -128,10 +131,10 @@ const BlogPosts = ({
</div> </div>
<div className="px-4 pb-5 pt-2"> <div className="px-4 pb-5 pt-2">
<h2 className="mb-2 text-xl font-semibold group-hover:underline"> <h2 className="mb-2 text-xl font-semibold group-hover:underline">
{post.data.title} {typographicText(post.data.title)}
</h2> </h2>
<p className="text-muted-foreground line-clamp-2 text-sm"> <p className="text-muted-foreground line-clamp-2 text-sm">
{post.data.description} {typographicText(post.data.description)}
</p> </p>
<div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs"> <div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs">

View file

@ -0,0 +1,5 @@
const EM_DASH_PATTERN = /---/g;
export function typographicText(value: string | null | undefined) {
return (value ?? "").replace(EM_DASH_PATTERN, "—");
}

View file

@ -2,6 +2,7 @@ import rss from "@astrojs/rss";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts"; import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
import { ACTIVE_BLOG_SITE } from "../blog-sites.js"; import { ACTIVE_BLOG_SITE } from "../blog-sites.js";
import { typographicText } from "@/lib/typography";
export async function GET(context) { export async function GET(context) {
const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort( const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort(
@ -10,10 +11,12 @@ export async function GET(context) {
return rss({ return rss({
title: SITE_TITLE, title: SITE_TITLE,
description: SITE_DESCRIPTION, description: typographicText(SITE_DESCRIPTION),
site: context.site, site: context.site,
items: posts.map((post) => ({ items: posts.map((post) => ({
...post.data, ...post.data,
title: typographicText(post.data.title),
description: typographicText(post.data.description),
link: `/${post.id.replace(/\/index$/, "")}/`, link: `/${post.id.replace(/\/index$/, "")}/`,
})), })),
}); });

View file

@ -0,0 +1,22 @@
const EM_DASH_PATTERN = /---/g;
function visitTextNodes(node) {
if (!node || typeof node !== "object") return;
if (node.type === "text" && typeof node.value === "string") {
node.value = node.value.replace(EM_DASH_PATTERN, "—");
return;
}
if (!Array.isArray(node.children)) return;
for (const child of node.children) {
visitTextNodes(child);
}
}
export default function remarkTypography() {
return (tree) => {
visitTextNodes(tree);
};
}

View file

@ -12,6 +12,7 @@ import tailwindcss from "@tailwindcss/vite";
import remarkCallouts from "./src/remark/callouts.mjs"; import remarkCallouts from "./src/remark/callouts.mjs";
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs"; import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs"; import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
import remarkTypography from "./src/remark/typography.mjs";
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js"; import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
const remarkPlugins = [ const remarkPlugins = [
@ -19,6 +20,7 @@ const remarkPlugins = [
remarkInlineFootnotes, remarkInlineFootnotes,
remarkCodeFenceLanguages, remarkCodeFenceLanguages,
remarkMath, remarkMath,
remarkTypography,
remarkCallouts, remarkCallouts,
]; ];
const rehypePlugins = [rehypeKatex]; const rehypePlugins = [rehypeKatex];

View file

@ -3,6 +3,7 @@
// all pages through the use of the <BaseHead /> component. // all pages through the use of the <BaseHead /> component.
import '../styles/global.css'; import '../styles/global.css';
import { SITE_TITLE, SITE_METADATA } from '../consts'; import { SITE_TITLE, SITE_METADATA } from '../consts';
import { typographicText } from '@/lib/typography';
interface Props { interface Props {
title: string; title: string;
@ -13,8 +14,8 @@ interface Props {
const canonicalURL = new URL(Astro.url.pathname, Astro.site); const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image } = Astro.props; const { title, description, image } = Astro.props;
const finalTitle = title || SITE_METADATA.title.default; const finalTitle = typographicText(title || SITE_METADATA.title.default);
const finalDescription = description || SITE_METADATA.description; const finalDescription = typographicText(description || SITE_METADATA.description);
const finalImage = image || SITE_METADATA.openGraph.images[0].url; const finalImage = image || SITE_METADATA.openGraph.images[0].url;
const imageURL = new URL(finalImage, Astro.url); const imageURL = new URL(finalImage, Astro.url);
--- ---

View file

@ -1,5 +1,6 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { format } from "date-fns"; import { format } from "date-fns";
import { typographicText } from "@/lib/typography";
const getReadingTime = (body: string | undefined) => { const getReadingTime = (body: string | undefined) => {
const words = (body ?? "") const words = (body ?? "")
@ -18,6 +19,7 @@ const BlogPost = ({
children: ReactNode; children: ReactNode;
}) => { }) => {
const { title, image, pubDate } = post.data; const { title, image, pubDate } = post.data;
const displayTitle = typographicText(title);
const readingTime = getReadingTime(post.body); const readingTime = getReadingTime(post.body);
return ( return (
@ -30,12 +32,12 @@ const BlogPost = ({
</time> </time>
<span>{readingTime} min read</span> <span>{readingTime} min read</span>
</div> </div>
<h1 className="blog-post-title">{title}</h1> <h1 className="blog-post-title">{displayTitle}</h1>
</header> </header>
{image ? ( {image ? (
<figure className="blog-post-hero-image"> <figure className="blog-post-hero-image">
<img src={image} alt={title} /> <img src={image} alt={displayTitle} />
</figure> </figure>
) : null} ) : null}

View file

@ -4,6 +4,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Calendar, Clock, User } from "lucide-react"; import { Calendar, Clock, User } from "lucide-react";
import { typographicText } from "@/lib/typography";
import { PlusSigns } from "../icons/plus-signs"; import { PlusSigns } from "../icons/plus-signs";
const BlogPosts = ({ const BlogPosts = ({
@ -27,11 +28,13 @@ const BlogPosts = ({
post: any; post: any;
featured?: boolean; featured?: boolean;
}) => { }) => {
const displayTitle = typographicText(post.data.title);
if (post.data.image) { if (post.data.image) {
return ( return (
<img <img
src={post.data.image} src={post.data.image}
alt={post.data.title} alt={displayTitle}
className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]" className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]"
/> />
); );
@ -44,7 +47,7 @@ const BlogPosts = ({
featured ? "font-fraunces text-2xl" : "font-fraunces text-lg" featured ? "font-fraunces text-2xl" : "font-fraunces text-lg"
} }
> >
{post.data.title} {displayTitle}
</span> </span>
</div> </div>
); );
@ -82,10 +85,10 @@ const BlogPosts = ({
Featured Post Featured Post
</Badge> </Badge>
<h2 className="blog-card-title-featured mb-3 group-hover:underline"> <h2 className="blog-card-title-featured mb-3 group-hover:underline">
{featuredPost.data.title} {typographicText(featuredPost.data.title)}
</h2> </h2>
<p className="text-muted-foreground mb-4 line-clamp-3 text-base"> <p className="text-muted-foreground mb-4 line-clamp-3 text-base">
{featuredPost.data.description} {typographicText(featuredPost.data.description)}
</p> </p>
<div className="mb-6 flex gap-4"> <div className="mb-6 flex gap-4">
@ -128,10 +131,10 @@ const BlogPosts = ({
</div> </div>
<div className="px-4 pb-5 pt-2"> <div className="px-4 pb-5 pt-2">
<h2 className="mb-2 text-xl font-semibold group-hover:underline"> <h2 className="mb-2 text-xl font-semibold group-hover:underline">
{post.data.title} {typographicText(post.data.title)}
</h2> </h2>
<p className="text-muted-foreground line-clamp-2 text-sm"> <p className="text-muted-foreground line-clamp-2 text-sm">
{post.data.description} {typographicText(post.data.description)}
</p> </p>
<div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs"> <div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs">

View file

@ -0,0 +1,5 @@
const EM_DASH_PATTERN = /---/g;
export function typographicText(value: string | null | undefined) {
return (value ?? "").replace(EM_DASH_PATTERN, "—");
}

View file

@ -2,6 +2,7 @@ import rss from "@astrojs/rss";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts"; import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
import { ACTIVE_BLOG_SITE } from "../blog-sites.js"; import { ACTIVE_BLOG_SITE } from "../blog-sites.js";
import { typographicText } from "@/lib/typography";
export async function GET(context) { export async function GET(context) {
const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort( const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort(
@ -10,10 +11,12 @@ export async function GET(context) {
return rss({ return rss({
title: SITE_TITLE, title: SITE_TITLE,
description: SITE_DESCRIPTION, description: typographicText(SITE_DESCRIPTION),
site: context.site, site: context.site,
items: posts.map((post) => ({ items: posts.map((post) => ({
...post.data, ...post.data,
title: typographicText(post.data.title),
description: typographicText(post.data.description),
link: `/${post.id.replace(/\/index$/, "")}/`, link: `/${post.id.replace(/\/index$/, "")}/`,
})), })),
}); });

View file

@ -0,0 +1,22 @@
const EM_DASH_PATTERN = /---/g;
function visitTextNodes(node) {
if (!node || typeof node !== "object") return;
if (node.type === "text" && typeof node.value === "string") {
node.value = node.value.replace(EM_DASH_PATTERN, "—");
return;
}
if (!Array.isArray(node.children)) return;
for (const child of node.children) {
visitTextNodes(child);
}
}
export default function remarkTypography() {
return (tree) => {
visitTextNodes(tree);
};
}

View file

@ -12,6 +12,7 @@ import tailwindcss from "@tailwindcss/vite";
import remarkCallouts from "./src/remark/callouts.mjs"; import remarkCallouts from "./src/remark/callouts.mjs";
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs"; import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs"; import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
import remarkTypography from "./src/remark/typography.mjs";
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js"; import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
const remarkPlugins = [ const remarkPlugins = [
@ -19,6 +20,7 @@ const remarkPlugins = [
remarkInlineFootnotes, remarkInlineFootnotes,
remarkCodeFenceLanguages, remarkCodeFenceLanguages,
remarkMath, remarkMath,
remarkTypography,
remarkCallouts, remarkCallouts,
]; ];
const rehypePlugins = [rehypeKatex]; const rehypePlugins = [rehypeKatex];

View file

@ -3,6 +3,7 @@
// all pages through the use of the <BaseHead /> component. // all pages through the use of the <BaseHead /> component.
import '../styles/global.css'; import '../styles/global.css';
import { SITE_TITLE, SITE_METADATA } from '../consts'; import { SITE_TITLE, SITE_METADATA } from '../consts';
import { typographicText } from '@/lib/typography';
interface Props { interface Props {
title: string; title: string;
@ -13,8 +14,8 @@ interface Props {
const canonicalURL = new URL(Astro.url.pathname, Astro.site); const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image } = Astro.props; const { title, description, image } = Astro.props;
const finalTitle = title || SITE_METADATA.title.default; const finalTitle = typographicText(title || SITE_METADATA.title.default);
const finalDescription = description || SITE_METADATA.description; const finalDescription = typographicText(description || SITE_METADATA.description);
const finalImage = image || SITE_METADATA.openGraph.images[0].url; const finalImage = image || SITE_METADATA.openGraph.images[0].url;
const imageURL = new URL(finalImage, Astro.url); const imageURL = new URL(finalImage, Astro.url);
--- ---

View file

@ -1,5 +1,6 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { format } from "date-fns"; import { format } from "date-fns";
import { typographicText } from "@/lib/typography";
const getReadingTime = (body: string | undefined) => { const getReadingTime = (body: string | undefined) => {
const words = (body ?? "") const words = (body ?? "")
@ -18,6 +19,7 @@ const BlogPost = ({
children: ReactNode; children: ReactNode;
}) => { }) => {
const { title, image, pubDate } = post.data; const { title, image, pubDate } = post.data;
const displayTitle = typographicText(title);
const readingTime = getReadingTime(post.body); const readingTime = getReadingTime(post.body);
return ( return (
@ -30,12 +32,12 @@ const BlogPost = ({
</time> </time>
<span>{readingTime} min read</span> <span>{readingTime} min read</span>
</div> </div>
<h1 className="blog-post-title">{title}</h1> <h1 className="blog-post-title">{displayTitle}</h1>
</header> </header>
{image ? ( {image ? (
<figure className="blog-post-hero-image"> <figure className="blog-post-hero-image">
<img src={image} alt={title} /> <img src={image} alt={displayTitle} />
</figure> </figure>
) : null} ) : null}

View file

@ -4,6 +4,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Calendar, Clock, User } from "lucide-react"; import { Calendar, Clock, User } from "lucide-react";
import { typographicText } from "@/lib/typography";
import { PlusSigns } from "../icons/plus-signs"; import { PlusSigns } from "../icons/plus-signs";
const BlogPosts = ({ const BlogPosts = ({
@ -27,11 +28,13 @@ const BlogPosts = ({
post: any; post: any;
featured?: boolean; featured?: boolean;
}) => { }) => {
const displayTitle = typographicText(post.data.title);
if (post.data.image) { if (post.data.image) {
return ( return (
<img <img
src={post.data.image} src={post.data.image}
alt={post.data.title} alt={displayTitle}
className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]" className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]"
/> />
); );
@ -44,7 +47,7 @@ const BlogPosts = ({
featured ? "font-fraunces text-2xl" : "font-fraunces text-lg" featured ? "font-fraunces text-2xl" : "font-fraunces text-lg"
} }
> >
{post.data.title} {displayTitle}
</span> </span>
</div> </div>
); );
@ -82,10 +85,10 @@ const BlogPosts = ({
Featured Post Featured Post
</Badge> </Badge>
<h2 className="blog-card-title-featured mb-3 group-hover:underline"> <h2 className="blog-card-title-featured mb-3 group-hover:underline">
{featuredPost.data.title} {typographicText(featuredPost.data.title)}
</h2> </h2>
<p className="text-muted-foreground mb-4 line-clamp-3 text-base"> <p className="text-muted-foreground mb-4 line-clamp-3 text-base">
{featuredPost.data.description} {typographicText(featuredPost.data.description)}
</p> </p>
<div className="mb-6 flex gap-4"> <div className="mb-6 flex gap-4">
@ -128,10 +131,10 @@ const BlogPosts = ({
</div> </div>
<div className="px-4 pb-5 pt-2"> <div className="px-4 pb-5 pt-2">
<h2 className="mb-2 text-xl font-semibold group-hover:underline"> <h2 className="mb-2 text-xl font-semibold group-hover:underline">
{post.data.title} {typographicText(post.data.title)}
</h2> </h2>
<p className="text-muted-foreground line-clamp-2 text-sm"> <p className="text-muted-foreground line-clamp-2 text-sm">
{post.data.description} {typographicText(post.data.description)}
</p> </p>
<div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs"> <div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs">

View file

@ -397,22 +397,17 @@ For visual clarity we also follow up with the following suggestion.
> for smaller ones)? > for smaller ones)?
<figure id="fig:003"> ![The initial configuration](image003-a.png)
<figure>
<img src="image003-a.png" /> _The initial configuration_
<figcaption>The initial configuration</figcaption>
</figure> ![An example where the current cost is worse than the optimal cost.](image003-b.png)
<figure>
<img src="image003-b.png" /> _An example where the current cost is worse than the optimal cost._
<figcaption>An example where the current cost is worse than the optimal
cost.</figcaption> ![An example where the current and optimal costs are the same.](image003-c.png)
</figure>
<figure> _An example where the current and optimal costs are the same._
<img src="image003-c.png" />
<figcaption>An example where the current and optimal costs are the
same.</figcaption>
</figure>
</figure>
The current version of this implementation is [here](https://filestorage2.netlify.app/), The current version of this implementation is [here](https://filestorage2.netlify.app/),
and it's still a little quirky in terms of the first couple of times you drag the slider, and it's still a little quirky in terms of the first couple of times you drag the slider,

View file

@ -0,0 +1,5 @@
const EM_DASH_PATTERN = /---/g;
export function typographicText(value: string | null | undefined) {
return (value ?? "").replace(EM_DASH_PATTERN, "—");
}

View file

@ -2,6 +2,7 @@ import rss from "@astrojs/rss";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts"; import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
import { ACTIVE_BLOG_SITE } from "../blog-sites.js"; import { ACTIVE_BLOG_SITE } from "../blog-sites.js";
import { typographicText } from "@/lib/typography";
export async function GET(context) { export async function GET(context) {
const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort( const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort(
@ -10,10 +11,12 @@ export async function GET(context) {
return rss({ return rss({
title: SITE_TITLE, title: SITE_TITLE,
description: SITE_DESCRIPTION, description: typographicText(SITE_DESCRIPTION),
site: context.site, site: context.site,
items: posts.map((post) => ({ items: posts.map((post) => ({
...post.data, ...post.data,
title: typographicText(post.data.title),
description: typographicText(post.data.description),
link: `/${post.id.replace(/\/index$/, "")}/`, link: `/${post.id.replace(/\/index$/, "")}/`,
})), })),
}); });

View file

@ -0,0 +1,22 @@
const EM_DASH_PATTERN = /---/g;
function visitTextNodes(node) {
if (!node || typeof node !== "object") return;
if (node.type === "text" && typeof node.value === "string") {
node.value = node.value.replace(EM_DASH_PATTERN, "—");
return;
}
if (!Array.isArray(node.children)) return;
for (const child of node.children) {
visitTextNodes(child);
}
}
export default function remarkTypography() {
return (tree) => {
visitTextNodes(tree);
};
}

View file

@ -3,6 +3,7 @@
// all pages through the use of the <BaseHead /> component. // all pages through the use of the <BaseHead /> component.
import '../styles/global.css'; import '../styles/global.css';
import { SITE_TITLE, SITE_METADATA } from '../consts'; import { SITE_TITLE, SITE_METADATA } from '../consts';
import { typographicText } from '@/lib/typography';
interface Props { interface Props {
title: string; title: string;
@ -13,8 +14,8 @@ interface Props {
const canonicalURL = new URL(Astro.url.pathname, Astro.site); const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image } = Astro.props; const { title, description, image } = Astro.props;
const finalTitle = title || SITE_METADATA.title.default; const finalTitle = typographicText(title || SITE_METADATA.title.default);
const finalDescription = description || SITE_METADATA.description; const finalDescription = typographicText(description || SITE_METADATA.description);
const finalImage = image || SITE_METADATA.openGraph.images[0].url; const finalImage = image || SITE_METADATA.openGraph.images[0].url;
const imageURL = new URL(finalImage, Astro.url); const imageURL = new URL(finalImage, Astro.url);
--- ---
@ -27,7 +28,7 @@ const imageURL = new URL(finalImage, Astro.url);
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Load Google Fonts --> <!-- Load Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Hubot+Sans:wght@400;500;600;700&family=Mona+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Hubot+Sans:wght@400;500;600;700&family=Mona+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="robots" content={`${SITE_METADATA.robots.index ? 'index' : 'noindex'}, ${SITE_METADATA.robots.follow ? 'follow' : 'nofollow'}`} /> <meta name="robots" content={`${SITE_METADATA.robots.index ? 'index' : 'noindex'}, ${SITE_METADATA.robots.follow ? 'follow' : 'nofollow'}`} />

View file

@ -1,5 +1,6 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { format } from "date-fns"; import { format } from "date-fns";
import { typographicText } from "@/lib/typography";
const getReadingTime = (body: string | undefined) => { const getReadingTime = (body: string | undefined) => {
const words = (body ?? "") const words = (body ?? "")
@ -18,6 +19,7 @@ const BlogPost = ({
children: ReactNode; children: ReactNode;
}) => { }) => {
const { title, image, pubDate } = post.data; const { title, image, pubDate } = post.data;
const displayTitle = typographicText(title);
const readingTime = getReadingTime(post.body); const readingTime = getReadingTime(post.body);
return ( return (
@ -30,12 +32,12 @@ const BlogPost = ({
</time> </time>
<span>{readingTime} min read</span> <span>{readingTime} min read</span>
</div> </div>
<h1 className="blog-post-title">{title}</h1> <h1 className="blog-post-title">{displayTitle}</h1>
</header> </header>
{image ? ( {image ? (
<figure className="blog-post-hero-image"> <figure className="blog-post-hero-image">
<img src={image} alt={title} /> <img src={image} alt={displayTitle} />
</figure> </figure>
) : null} ) : null}

View file

@ -4,6 +4,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Calendar, Clock, User } from "lucide-react"; import { Calendar, Clock, User } from "lucide-react";
import { typographicText } from "@/lib/typography";
import { PlusSigns } from "../icons/plus-signs"; import { PlusSigns } from "../icons/plus-signs";
const BlogPosts = ({ const BlogPosts = ({
@ -16,11 +17,52 @@ const BlogPosts = ({
// Get the first post as the featured post // Get the first post as the featured post
const featuredPost = posts[0]; const featuredPost = posts[0];
const remainingPosts = posts.slice(1); const remainingPosts = posts.slice(1);
const slugFor = (post: any) => post.id.replace(/\/index$/, "");
const hrefFor = (post: any) => const hrefFor = (post: any) =>
collection ? `/${collection}/${post.id}/` : `/${post.id}/`; collection ? `/${collection}/${slugFor(post)}/` : `/${slugFor(post)}/`;
const PostVisual = ({
post,
featured = false,
}: {
post: any;
featured?: boolean;
}) => {
const displayTitle = typographicText(post.data.title);
if (post.data.image) {
return (
<img
src={post.data.image}
alt={displayTitle}
className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]"
/>
);
}
return ( return (
<div className="relative py-16 md:py-28 lg:py-32"> <div className="bg-accent/70 text-accent-foreground flex aspect-video w-full items-center justify-center rounded-lg border px-6 text-center">
<span
className={
featured ? "font-fraunces text-2xl" : "font-fraunces text-lg"
}
>
{displayTitle}
</span>
</div>
);
};
if (!featuredPost) {
return (
<div className="text-muted-foreground container max-w-3xl py-16">
No posts yet.
</div>
);
}
return (
<div className="relative py-10 md:py-16 lg:py-20">
<div className="absolute -inset-40 z-[-1] [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_75%)]"> <div className="absolute -inset-40 z-[-1] [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_75%)]">
<PlusSigns className="text-foreground/[0.05] h-full w-full" /> <PlusSigns className="text-foreground/[0.05] h-full w-full" />
</div> </div>
@ -35,22 +77,18 @@ const BlogPosts = ({
<div className="flex flex-col gap-6 lg:flex-row"> <div className="flex flex-col gap-6 lg:flex-row">
<div className="lg:w-1/2"> <div className="lg:w-1/2">
<div className="p-2 lg:p-4"> <div className="p-2 lg:p-4">
<img <PostVisual post={featuredPost} featured />
src={featuredPost.data.image}
alt={featuredPost.data.title}
className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]"
/>
</div> </div>
</div> </div>
<div className="flex flex-col justify-center p-4 pb-8 lg:w-1/2 lg:pr-8"> <div className="flex flex-col justify-center p-4 pb-8 lg:w-1/2 lg:pr-8">
<Badge variant="outline" className="mb-3 w-fit"> <Badge variant="outline" className="mb-3 w-fit">
Featured Post Featured Post
</Badge> </Badge>
<h2 className="mb-3 text-2xl font-bold group-hover:underline md:text-3xl"> <h2 className="blog-card-title-featured mb-3 group-hover:underline">
{featuredPost.data.title} {typographicText(featuredPost.data.title)}
</h2> </h2>
<p className="text-muted-foreground mb-4 line-clamp-3 text-base"> <p className="text-muted-foreground mb-4 line-clamp-3 text-base">
{featuredPost.data.description} {typographicText(featuredPost.data.description)}
</p> </p>
<div className="mb-6 flex gap-4"> <div className="mb-6 flex gap-4">
@ -89,18 +127,14 @@ const BlogPosts = ({
href={hrefFor(post)} href={hrefFor(post)}
> >
<div className="p-2"> <div className="p-2">
<img <PostVisual post={post} />
src={post.data.image}
alt={post.data.title}
className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.01]"
/>
</div> </div>
<div className="px-4 pb-5 pt-2"> <div className="px-4 pb-5 pt-2">
<h2 className="mb-2 text-xl font-semibold group-hover:underline"> <h2 className="mb-2 text-xl font-semibold group-hover:underline">
{post.data.title} {typographicText(post.data.title)}
</h2> </h2>
<p className="text-muted-foreground line-clamp-2 text-sm"> <p className="text-muted-foreground line-clamp-2 text-sm">
{post.data.description} {typographicText(post.data.description)}
</p> </p>
<div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs"> <div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs">

5
src/lib/typography.ts Normal file
View file

@ -0,0 +1,5 @@
const EM_DASH_PATTERN = /---/g;
export function typographicText(value: string | null | undefined) {
return (value ?? "").replace(EM_DASH_PATTERN, "—");
}

View file

@ -2,6 +2,7 @@ import rss from "@astrojs/rss";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts"; import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
import { ACTIVE_BLOG_SITE } from "../blog-sites.js"; import { ACTIVE_BLOG_SITE } from "../blog-sites.js";
import { typographicText } from "@/lib/typography";
export async function GET(context) { export async function GET(context) {
const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort( const posts = (await getCollection(ACTIVE_BLOG_SITE.key)).sort(
@ -10,11 +11,13 @@ export async function GET(context) {
return rss({ return rss({
title: SITE_TITLE, title: SITE_TITLE,
description: SITE_DESCRIPTION, description: typographicText(SITE_DESCRIPTION),
site: context.site, site: context.site,
items: posts.map((post) => ({ items: posts.map((post) => ({
...post.data, ...post.data,
link: `/${post.id}/`, title: typographicText(post.data.title),
description: typographicText(post.data.description),
link: `/${post.id.replace(/\/index$/, "")}/`,
})), })),
}); });
} }