From 9e429966770827600700ec06dcbbec592767943a Mon Sep 17 00:00:00 2001 From: thuanbui Date: Wed, 25 Mar 2026 09:15:12 +0900 Subject: [PATCH] feat: add YouTube embedding support with lite-youtube-embed and create remarkYouTube plugin --- astro.config.mjs | 2 ++ package-lock.json | 31 +++++++++++++++++++++++++++++++ package.json | 1 + src/pages/[...slug].astro | 4 ++++ src/plugins/remark-youtube.mjs | 24 ++++++++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 src/plugins/remark-youtube.mjs diff --git a/astro.config.mjs b/astro.config.mjs index 132abf0..c59dd17 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -7,6 +7,7 @@ import remarkReadingTime from "remark-reading-time"; import remarkUnwrapImages from "remark-unwrap-images"; import { remarkR2Images } from "./src/plugins/remark-r2-images.mjs"; import { rehypePictureWebp } from "./src/plugins/rehype-picture-webp.mjs"; +import { remarkYouTube } from "./src/plugins/remark-youtube.mjs"; export default defineConfig({ site: "https://balodeplao.com/", @@ -22,6 +23,7 @@ export default defineConfig({ }, remarkR2Images, remarkUnwrapImages, + remarkYouTube, ], rehypePlugins: [rehypePictureWebp], }, diff --git a/package-lock.json b/package-lock.json index 4049a90..cdd4090 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@tailwindcss/vite": "^4.1.18", "astro": "^6.0.4", "astro-icon": "^1.1.5", + "lite-youtube-embed": "^0.3.4", "remark-reading-time": "^2.0.2", "remark-unwrap-images": "^4.0.1", "tailwindcss": "^4.1.18", @@ -1300,6 +1301,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1322,6 +1324,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1344,6 +1347,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1360,6 +1364,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1376,6 +1381,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1392,6 +1398,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1408,6 +1415,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1424,6 +1432,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1440,6 +1449,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1456,6 +1466,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1472,6 +1483,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1488,6 +1500,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1504,6 +1517,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1526,6 +1540,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1548,6 +1563,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1570,6 +1586,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1592,6 +1609,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1614,6 +1632,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1636,6 +1655,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1658,6 +1678,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1680,6 +1701,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -1699,6 +1721,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1718,6 +1741,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1737,6 +1761,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -7389,6 +7414,12 @@ "node": ">=20.0.0" } }, + "node_modules/lite-youtube-embed": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/lite-youtube-embed/-/lite-youtube-embed-0.3.4.tgz", + "integrity": "sha512-aXgxpwK7AIW58GEbRzA8EYaY4LWvF3FKak6B9OtSJmuNyLhX2ouD4cMTxz/yR5HFInhknaYd2jLWOTRTvT8oAw==", + "license": "Apache-2.0" + }, "node_modules/local-pkg": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", diff --git a/package.json b/package.json index 5678e80..50013c0 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@tailwindcss/vite": "^4.1.18", "astro": "^6.0.4", "astro-icon": "^1.1.5", + "lite-youtube-embed": "^0.3.4", "remark-reading-time": "^2.0.2", "remark-unwrap-images": "^4.0.1", "tailwindcss": "^4.1.18", diff --git a/src/pages/[...slug].astro b/src/pages/[...slug].astro index 0957672..41908f8 100644 --- a/src/pages/[...slug].astro +++ b/src/pages/[...slug].astro @@ -6,6 +6,7 @@ import Categories from "@/components/ui/Categories.astro"; import Destinations from "@/components/ui/Destinations.astro"; import { toR2Url } from "@/utils/r2"; import Picture from "@/components/ui/Picture.astro"; +import "lite-youtube-embed/src/lite-yt-embed.css"; import { getCollection, render, type CollectionEntry } from "astro:content"; @@ -119,4 +120,7 @@ const metadata = { } + diff --git a/src/plugins/remark-youtube.mjs b/src/plugins/remark-youtube.mjs new file mode 100644 index 0000000..ad572b1 --- /dev/null +++ b/src/plugins/remark-youtube.mjs @@ -0,0 +1,24 @@ +import { visit } from "unist-util-visit"; + +const YT_REGEX = + /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/; + +export function remarkYouTube() { + return (tree) => { + visit(tree, "paragraph", (node, index, parent) => { + if (node.children.length !== 1) return; + const child = node.children[0]; + if (child.type !== "link" && child.type !== "text") return; + + const url = child.type === "link" ? child.url : child.value; + const match = url?.match(YT_REGEX); + if (!match) return; + + const videoId = match[1]; + parent.children.splice(index, 1, { + type: "html", + value: `Play Video`, + }); + }); + }; +}