feat: add R2 image handling and utility functions for image URLs

This commit is contained in:
Thuan Bui
2026-03-15 11:29:38 +07:00
parent 0be314edbe
commit 510c7f25d4
8 changed files with 46 additions and 55 deletions
+2
View File
@@ -4,6 +4,7 @@ import mdx from "@astrojs/mdx";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import icon from "astro-icon"; import icon from "astro-icon";
import remarkReadingTime from "remark-reading-time"; import remarkReadingTime from "remark-reading-time";
import { remarkR2Images } from './src/plugins/remark-r2-images.mjs';
export default defineConfig({ export default defineConfig({
site: "https://balodeplao.com/", site: "https://balodeplao.com/",
@@ -17,6 +18,7 @@ export default defineConfig({
file.data.readingTime.minutes; file.data.readingTime.minutes;
}; };
}, },
remarkR2Images
], ],
}, },
i18n: { i18n: {
+2 -49
View File
@@ -21,7 +21,8 @@
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"remark-reading-time": "^2.0.2", "remark-reading-time": "^2.0.2",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"typescript": "^5.9.3" "typescript": "^5.9.3",
"unist-util-visit": "^5.1.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
@@ -1249,9 +1250,6 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1268,9 +1266,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1287,9 +1282,6 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1306,9 +1298,6 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1325,9 +1314,6 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1344,9 +1330,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1363,9 +1346,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1382,9 +1362,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1401,9 +1378,6 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1426,9 +1400,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1451,9 +1422,6 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1476,9 +1444,6 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1501,9 +1466,6 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1526,9 +1488,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1551,9 +1510,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1576,9 +1532,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
+2 -1
View File
@@ -31,7 +31,8 @@
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"remark-reading-time": "^2.0.2", "remark-reading-time": "^2.0.2",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"typescript": "^5.9.3" "typescript": "^5.9.3",
"unist-util-visit": "^5.1.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
+4 -2
View File
@@ -2,6 +2,7 @@
import Tags from "@/components/ui/Tags.astro"; import Tags from "@/components/ui/Tags.astro";
import Categories from "@/components/ui/Categories.astro"; import Categories from "@/components/ui/Categories.astro";
import { type CollectionEntry, render } from "astro:content"; import { type CollectionEntry, render } from "astro:content";
import { toR2Url } from "@/utils/r2";
export interface Props { export interface Props {
post: CollectionEntry<"blog">; post: CollectionEntry<"blog">;
@@ -9,6 +10,7 @@ export interface Props {
const { post } = Astro.props; const { post } = Astro.props;
const { remarkPluginFrontmatter } = await render(post); const { remarkPluginFrontmatter } = await render(post);
const coverImage = toR2Url(post.data.image);
const readingTime = remarkPluginFrontmatter?.minutesRead const readingTime = remarkPluginFrontmatter?.minutesRead
? `${Math.ceil(remarkPluginFrontmatter.minutesRead)} phút đọc` ? `${Math.ceil(remarkPluginFrontmatter.minutesRead)} phút đọc`
@@ -20,9 +22,9 @@ const readingTime = remarkPluginFrontmatter?.minutesRead
> >
<div class="relative h-48 w-full overflow-hidden"> <div class="relative h-48 w-full overflow-hidden">
{ {
post.data.image && ( coverImage && (
<img <img
src={post.data.image} src={coverImage}
alt={post.data.title} alt={post.data.title}
class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
/> />
+1
View File
@@ -0,0 +1 @@
export const R2_BASE = 'https://media.balodeplao.com';
+9 -3
View File
@@ -3,6 +3,7 @@ import BaseLayout from "@/layouts/BaseLayout.astro";
import Schema from "@/components/seo/Schema.astro"; import Schema from "@/components/seo/Schema.astro";
import Tags from "@/components/ui/Tags.astro"; import Tags from "@/components/ui/Tags.astro";
import Categories from "@/components/ui/Categories.astro"; import Categories from "@/components/ui/Categories.astro";
import { toR2Url } from "@/utils/r2";
import { getCollection, render } from "astro:content"; import { getCollection, render } from "astro:content";
@@ -18,6 +19,7 @@ const { post } = Astro.props;
const { Content, remarkPluginFrontmatter } = await render(post); const { Content, remarkPluginFrontmatter } = await render(post);
const { title, description, pubDate, author, image, categories, tags } = const { title, description, pubDate, author, image, categories, tags } =
post.data; post.data;
const coverImage = toR2Url(image);
const formattedDate = pubDate.toLocaleDateString("vi-VN", { const formattedDate = pubDate.toLocaleDateString("vi-VN", {
year: "numeric", year: "numeric",
@@ -32,7 +34,7 @@ const readingTime = remarkPluginFrontmatter?.minutesRead
const metadata = { const metadata = {
title: title, title: title,
description: description, description: description,
ogImage: image, ogImage: coverImage,
}; };
--- ---
@@ -70,9 +72,13 @@ const metadata = {
</div> </div>
{ {
image && ( coverImage && (
<div class="reveal relative mb-8 h-96 w-full overflow-hidden rounded-xl shadow-lg"> <div class="reveal relative mb-8 h-96 w-full overflow-hidden rounded-xl shadow-lg">
<img src={image} alt={title} class="h-full w-full object-cover" /> <img
src={coverImage}
alt={title}
class="h-full w-full object-cover"
/>
</div> </div>
) )
} }
+19
View File
@@ -0,0 +1,19 @@
import { visit } from 'unist-util-visit';
import { R2_BASE } from '../config/r2.mjs';
export function remarkR2Images() {
return (tree) => {
visit(tree, 'image', (node) => {
if (node.url.startsWith('/images/')) {
node.url = `${R2_BASE}${node.url}`;
}
});
visit(tree, 'html', (node) => {
node.value = node.value.replace(
/src="(\/images\/[^"]+)"/g,
`src="${R2_BASE}$1"`
);
});
};
}
+7
View File
@@ -0,0 +1,7 @@
import { R2_BASE } from "@/config/r2.mjs";
export function toR2Url(path?: string): string | undefined {
if (!path) return undefined;
if (path.startsWith("http")) return path;
return `${R2_BASE}${path}`;
}