This commit is contained in:
parent
4b3aa3022e
commit
632543db4d
55 changed files with 432 additions and 117 deletions
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
5
sites/art/src/lib/typography.ts
Normal file
5
sites/art/src/lib/typography.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
const EM_DASH_PATTERN = /---/g;
|
||||||
|
|
||||||
|
export function typographicText(value: string | null | undefined) {
|
||||||
|
return (value ?? "").replace(EM_DASH_PATTERN, "—");
|
||||||
|
}
|
||||||
|
|
@ -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$/, "")}/`,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
22
sites/art/src/remark/typography.mjs
Normal file
22
sites/art/src/remark/typography.mjs
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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")}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
5
sites/poetry/src/lib/typography.ts
Normal file
5
sites/poetry/src/lib/typography.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
const EM_DASH_PATTERN = /---/g;
|
||||||
|
|
||||||
|
export function typographicText(value: string | null | undefined) {
|
||||||
|
return (value ?? "").replace(EM_DASH_PATTERN, "—");
|
||||||
|
}
|
||||||
|
|
@ -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$/, "")}/`,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
22
sites/poetry/src/remark/typography.mjs
Normal file
22
sites/poetry/src/remark/typography.mjs
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
5
sites/puzzles/src/lib/typography.ts
Normal file
5
sites/puzzles/src/lib/typography.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
const EM_DASH_PATTERN = /---/g;
|
||||||
|
|
||||||
|
export function typographicText(value: string | null | undefined) {
|
||||||
|
return (value ?? "").replace(EM_DASH_PATTERN, "—");
|
||||||
|
}
|
||||||
|
|
@ -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$/, "")}/`,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
22
sites/puzzles/src/remark/typography.mjs
Normal file
22
sites/puzzles/src/remark/typography.mjs
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
5
sites/reflections/src/lib/typography.ts
Normal file
5
sites/reflections/src/lib/typography.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
const EM_DASH_PATTERN = /---/g;
|
||||||
|
|
||||||
|
export function typographicText(value: string | null | undefined) {
|
||||||
|
return (value ?? "").replace(EM_DASH_PATTERN, "—");
|
||||||
|
}
|
||||||
|
|
@ -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$/, "")}/`,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
22
sites/reflections/src/remark/typography.mjs
Normal file
22
sites/reflections/src/remark/typography.mjs
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
5
sites/research/src/lib/typography.ts
Normal file
5
sites/research/src/lib/typography.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
const EM_DASH_PATTERN = /---/g;
|
||||||
|
|
||||||
|
export function typographicText(value: string | null | undefined) {
|
||||||
|
return (value ?? "").replace(EM_DASH_PATTERN, "—");
|
||||||
|
}
|
||||||
|
|
@ -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$/, "")}/`,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
22
sites/research/src/remark/typography.mjs
Normal file
22
sites/research/src/remark/typography.mjs
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
5
sites/reviews/src/lib/typography.ts
Normal file
5
sites/reviews/src/lib/typography.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
const EM_DASH_PATTERN = /---/g;
|
||||||
|
|
||||||
|
export function typographicText(value: string | null | undefined) {
|
||||||
|
return (value ?? "").replace(EM_DASH_PATTERN, "—");
|
||||||
|
}
|
||||||
|
|
@ -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$/, "")}/`,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
22
sites/reviews/src/remark/typography.mjs
Normal file
22
sites/reviews/src/remark/typography.mjs
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|

|
||||||
<figure>
|
|
||||||
<img src="image003-a.png" />
|
_The initial configuration_
|
||||||
<figcaption>The initial configuration</figcaption>
|
|
||||||
</figure>
|

|
||||||
<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>
|

|
||||||
</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,
|
||||||
|
|
|
||||||
5
sites/vibes/src/lib/typography.ts
Normal file
5
sites/vibes/src/lib/typography.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
const EM_DASH_PATTERN = /---/g;
|
||||||
|
|
||||||
|
export function typographicText(value: string | null | undefined) {
|
||||||
|
return (value ?? "").replace(EM_DASH_PATTERN, "—");
|
||||||
|
}
|
||||||
|
|
@ -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$/, "")}/`,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
22
sites/vibes/src/remark/typography.mjs
Normal file
22
sites/vibes/src/remark/typography.mjs
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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'}`} />
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
5
src/lib/typography.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
const EM_DASH_PATTERN = /---/g;
|
||||||
|
|
||||||
|
export function typographicText(value: string | null | undefined) {
|
||||||
|
return (value ?? "").replace(EM_DASH_PATTERN, "—");
|
||||||
|
}
|
||||||
|
|
@ -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$/, "")}/`,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue