From 793c3f8028efa67f7cca7846f7966b04585f9555 Mon Sep 17 00:00:00 2001 From: Samuel Jensen <44519206+nichtsam@users.noreply.github.com> Date: Sun, 17 Mar 2024 10:40:42 +0000 Subject: [PATCH] blog --- app/components/image.tsx | 23 ++++++++ app/components/ui/aspect-ratio.tsx | 5 ++ app/routes/blog.tsx | 87 ++++++++++++++++++++++++++++ app/routes/blog_.$slug.tsx | 59 +++++++++++++++++++ app/utils/mdx/blog.server.ts | 75 ++++++++++++++++++++++++ app/utils/mdx/compile-mdx.server.ts | 18 ++++++ app/utils/mdx/mdx-meta.server.ts | 10 ++++ app/utils/mdx/mdx.server.ts | 89 +++++++++++++++++++++++++++++ app/utils/mdx/mdx.tsx | 25 ++++++++ app/utils/path.server.ts | 24 ++++++++ mdx/blog/test.mdx | 57 ++++++++++++++++++ package.json | 1 + pnpm-lock.yaml | 36 ++++++++++-- 13 files changed, 503 insertions(+), 6 deletions(-) create mode 100644 app/components/ui/aspect-ratio.tsx create mode 100644 app/routes/blog.tsx create mode 100644 app/routes/blog_.$slug.tsx create mode 100644 app/utils/mdx/blog.server.ts create mode 100644 app/utils/mdx/compile-mdx.server.ts create mode 100644 app/utils/mdx/mdx-meta.server.ts create mode 100644 app/utils/mdx/mdx.server.ts create mode 100644 app/utils/mdx/mdx.tsx create mode 100644 app/utils/path.server.ts create mode 100644 mdx/blog/test.mdx diff --git a/app/components/image.tsx b/app/components/image.tsx index 9910c52..d31bba4 100644 --- a/app/components/image.tsx +++ b/app/components/image.tsx @@ -57,3 +57,26 @@ export function CloudinaryImage({ /> ); } + +export function BlogImage({ + id, + alt, +}: { + id: CloudinaryImageProps["id"]; + alt: string; +}) { + return ( + + ); +} diff --git a/app/components/ui/aspect-ratio.tsx b/app/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..c4abbf3 --- /dev/null +++ b/app/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/app/routes/blog.tsx b/app/routes/blog.tsx new file mode 100644 index 0000000..89050fe --- /dev/null +++ b/app/routes/blog.tsx @@ -0,0 +1,87 @@ +import { json, type HeadersFunction, type MetaFunction } from "@remix-run/node"; +import { type PostInfo, getPostInfos } from "#app/utils/mdx/blog.server"; +import { Link, useLoaderData } from "@remix-run/react"; +import { AspectRatio } from "#app/components/ui/aspect-ratio"; +import { CloudinaryImage } from "#app/components/image"; + +export const meta: MetaFunction = () => { + return [ + { title: "Blog | nichtsam" }, + { name: "description", content: "nichtsam's blog" }, + ]; +}; + +export const headers: HeadersFunction = () => ({ + "Cache-Control": "private, max-age=3600", + Vary: "Cookie", +}); + +export const loader = async () => { + const posts = await getPostInfos(); + + return json( + { posts }, + { + headers: { + "Cache-Control": "public, max-age=3600", + }, + }, + ); +}; + +export default function Blog() { + const data = useLoaderData(); + return ( + + + {data.posts.map((post) => ( + + + + ))} + + + ); +} + +function PostCard({ post }: { post: PostInfo }) { + return ( + + + + + + + + + + + + {post.meta.readingTime.text} + + + {post.meta.matter.publishedDate} + + + + + {post.meta.matter.title} + + + + + ); +} diff --git a/app/routes/blog_.$slug.tsx b/app/routes/blog_.$slug.tsx new file mode 100644 index 0000000..df2a115 --- /dev/null +++ b/app/routes/blog_.$slug.tsx @@ -0,0 +1,59 @@ +import { getPostMeta } from "#app/utils/mdx/blog.server"; +import { bundleMDX } from "#app/utils/mdx/compile-mdx.server"; +import { useMdxComponent } from "#app/utils/mdx/mdx"; +import { getMdxBundleSource, getMdxEntry } from "#app/utils/mdx/mdx.server"; +import { json } from "@remix-run/node"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; + +export const meta: MetaFunction = ({ data }) => { + if (!data) { + return [ + { title: "Error | nichtsam" }, + { name: "description", content: "Some error occured" }, + ]; + } + return [ + { title: `${data.meta.matter.title} | nichtsam` }, + { name: "description", content: data.meta.matter.description }, + ]; +}; + +export const loader = async ({ params }: LoaderFunctionArgs) => { + if (!params.slug) { + throw new Error("params.slug is not defined"); + } + + const slug = params.slug; + + const mdxEntry = getMdxEntry("blog", slug); + const mdxBundle = await getMdxBundleSource(mdxEntry); + + const meta = getPostMeta(mdxBundle.source); + const { code } = await bundleMDX(mdxBundle); + + return json( + { + code, + meta, + }, + { + headers: { + "Cache-Control": "public, max-age=3600", + }, + }, + ); +}; + +export default function BlogPost() { + const data = useLoaderData(); + const Component = useMdxComponent(data.code); + + return ( + + + + + + ); +} diff --git a/app/utils/mdx/blog.server.ts b/app/utils/mdx/blog.server.ts new file mode 100644 index 0000000..a3ecbaf --- /dev/null +++ b/app/utils/mdx/blog.server.ts @@ -0,0 +1,75 @@ +import { resolve } from "path"; +import { readFile, readdir } from "fs/promises"; +import { rootPath } from "../path.server"; +import { getMdxEntry } from "./mdx.server"; +import { getMdxMeta } from "./mdx-meta.server"; +import { z } from "zod"; +import type { ReadTimeResults } from "reading-time"; +import dayjs from "dayjs"; + +export const ENTRY = "blog"; +const blogDirPath = resolve(rootPath, "mdx", ENTRY); + +export const matterSchema = z.object({ + title: z.string(), + description: z.string(), + publishedDate: z.date().transform((date) => dayjs(date).format("YYYY-MM-DD")), + thumbnailId: z.string(), +}); + +export type PostMeta = { + readingTime: ReadTimeResults; + matter: z.infer; +}; + +export type PostInfo = { + slug: string; + meta: PostMeta; +}; + +export function getPostMeta(file: string): PostMeta { + const { matter: rawMatter, ...restMeta } = getMdxMeta(file); + const matter = matterSchema.parse(rawMatter); + + return { + ...restMeta, + matter, + }; +} + +export async function getPostInfos(): Promise> { + const dir = await readdir(blogDirPath, { withFileTypes: true }); + const postInfos: Array = []; + await Promise.all( + dir.map(async (dirent) => { + const slug = dirent.name.replace(/.mdx?$/, ""); + + try { + const entry = getMdxEntry("blog", slug); + const file = await readFile(entry.mdxPath, "utf-8"); + const meta = getPostMeta(file); + + postInfos.push({ + slug, + meta, + }); + } catch (err) { + if (err instanceof z.ZodError) { + console.error( + `Error: skipping blog post with slug '${slug}', invalid frontMatter:`, + err.flatten().fieldErrors, + ); + } else if (err instanceof Error) { + console.error( + `Error: skipping blog post with slug '${slug}' => `, + err.message, + ); + } else { + console.error("Error: corrupted blog post, ", err); + } + } + }), + ); + + return postInfos; +} diff --git a/app/utils/mdx/compile-mdx.server.ts b/app/utils/mdx/compile-mdx.server.ts new file mode 100644 index 0000000..0ab2cf7 --- /dev/null +++ b/app/utils/mdx/compile-mdx.server.ts @@ -0,0 +1,18 @@ +import { bundleMDX as _bundleMDX } from "mdx-bundler"; +import type { MdxBundleSource } from "./mdx.server"; + +export async function bundleMDX({ source, files }: MdxBundleSource) { + const mdxBundle = await _bundleMDX({ + source, + files, + mdxOptions(options) { + options.remarkPlugins = [...(options.remarkPlugins ?? [])]; + options.rehypePlugins = [...(options.rehypePlugins ?? [])]; + return options; + }, + }); + + return { + code: mdxBundle.code, + }; +} diff --git a/app/utils/mdx/mdx-meta.server.ts b/app/utils/mdx/mdx-meta.server.ts new file mode 100644 index 0000000..204d392 --- /dev/null +++ b/app/utils/mdx/mdx-meta.server.ts @@ -0,0 +1,10 @@ +import getGrayMatter from "gray-matter"; +import calculateReadingTime from "reading-time"; + +export function getMdxMeta(file: string) { + const grayMatter = getGrayMatter(file); + + const readingTime = calculateReadingTime(grayMatter.content); + + return { readingTime, matter: grayMatter.data as Record }; +} diff --git a/app/utils/mdx/mdx.server.ts b/app/utils/mdx/mdx.server.ts new file mode 100644 index 0000000..86fe23e --- /dev/null +++ b/app/utils/mdx/mdx.server.ts @@ -0,0 +1,89 @@ +import { existsSync, statSync } from "fs"; +import { readFile, readdir } from "fs/promises"; +import { relative, resolve } from "path"; +import { rootPath, getFilePathInDirectoryByName } from "../path.server"; + +type Files = Record; +export type MdxBundleSource = { + source: string; + files?: Files; +}; + +type Entry = { + name: string; + isFile: boolean; + bundlePath: string; + mdxPath: string; +}; + +const mdxDirPath = resolve(rootPath, "mdx"); +const VALID_EXTENSION = ["md", "mdx"]; + +export function getMdxEntry(category: string, name: string): Entry { + const maybeDirPath = resolve(mdxDirPath, category, name); + if (existsSync(maybeDirPath)) { + const dirPath = maybeDirPath; + return { + name, + isFile: false, + bundlePath: dirPath, + mdxPath: getFilePathInDirectoryByName(dirPath, "index", VALID_EXTENSION), + }; + } + + const filePath = getFilePathInDirectoryByName( + resolve(mdxDirPath, category), + name, + VALID_EXTENSION, + ); + + return { + name, + isFile: true, + bundlePath: filePath, + mdxPath: filePath, + }; +} + +export async function getMdxBundleSource( + entry: Entry, +): Promise { + const sourcePromise = readFile(entry.mdxPath, "utf-8"); + const filesPromise = entry.isFile + ? undefined + : getFilesInDirectory(entry.bundlePath); + const [source, files] = await Promise.all([sourcePromise, filesPromise]); + + return { source, files }; +} + +async function getFilesInDirectory( + currentPath: string, + rootPath: string = currentPath, +): Promise { + if (!existsSync(currentPath)) { + return {}; + } + + if (statSync(currentPath).isFile()) { + const relativePath = relative(rootPath, currentPath); + + return { + [relativePath]: await readFile(currentPath, "utf-8"), + }; + } else { + const dir = await readdir(currentPath, { withFileTypes: true }); + + const fileSets = await Promise.all( + dir.map((dirent) => + getFilesInDirectory(resolve(dirent.path, dirent.name), rootPath), + ), + ); + + const files = fileSets.reduce((acc, files) => { + return Object.assign(acc, files); + }, {}); + + return files; + } +} diff --git a/app/utils/mdx/mdx.tsx b/app/utils/mdx/mdx.tsx new file mode 100644 index 0000000..265112a --- /dev/null +++ b/app/utils/mdx/mdx.tsx @@ -0,0 +1,25 @@ +import { useMemo } from "react"; +import * as mdxBundler from "mdx-bundler/client/index.js"; +import { BlogImage } from "#app/components/image"; + +const customMdxComponents = { BlogImage }; + +function getMdxComponent(code: string) { + const Component = mdxBundler.getMDXComponent(code); + function MdxComponent({ components, ...rest }: mdxBundler.MDXContentProps) { + return ( + + ); + } + return MdxComponent; +} + +export const useMdxComponent = (code: string) => { + return useMemo(() => { + const component = getMdxComponent(code); + return component; + }, [code]); +}; diff --git a/app/utils/path.server.ts b/app/utils/path.server.ts new file mode 100644 index 0000000..9b6db02 --- /dev/null +++ b/app/utils/path.server.ts @@ -0,0 +1,24 @@ +import { existsSync } from "fs"; +import { dirname, resolve } from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); // prod: build/server/index.js | dev: app/utils/path.server.ts +const __dirname = dirname(__filename); // prod: build/server | dev: app/utils +export const rootPath = resolve(__dirname, "../../"); + +export function getFilePathInDirectoryByName( + dirPath: string, + name: string, + extensions: string[], +) { + for (let ext of extensions) { + const indexPath = resolve(dirPath, `${name}.${ext}`); + if (existsSync(indexPath)) { + return indexPath; + } + } + + throw new Error( + `No file named ${name} with valid extension found in directory => ${dirPath}`, + ); +} diff --git a/mdx/blog/test.mdx b/mdx/blog/test.mdx new file mode 100644 index 0000000..5d14560 --- /dev/null +++ b/mdx/blog/test.mdx @@ -0,0 +1,57 @@ +--- +title: A Post testing out my blog system +description: Some fake article for the purpose of testing if my blog if working correctly. +publishedDate: 2024-03-21 +thumbnailId: cld-sample-2 +--- + +# {frontmatter.title} + +> Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat. + +Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis. + +## Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat. + + + +Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis. + +## Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat. + +Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis. + +### Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat. + +Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis. + +![Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.](https://res.cloudinary.com/nichtsam/image/upload/v1709918808/cld-sample-2.jpg) +_Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat._ + +![Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.](https://res.cloudinary.com/nichtsam/image/upload/v1709918808/cld-sample-2.jpg) +_Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat._ + +### Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat. + +Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis. + +![Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.](https://res.cloudinary.com/nichtsam/image/upload/v1709918808/cld-sample-2.jpg) +_Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat._ + +### Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat. + +Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis. + +![Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.](https://res.cloudinary.com/nichtsam/image/upload/v1709918808/cld-sample-2.jpg) +_Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat._ + +## Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat. + +Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis. + +## Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat. + +Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat [the Github repository of this project](https://github.com/nichtsam). diff --git a/package.json b/package.json index 7242f72..9c53357 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@conform-to/react": "1.0.2", "@conform-to/zod": "1.0.2", "@libsql/client": "0.4.3", + "@radix-ui/react-aspect-ratio": "1.0.3", "@radix-ui/react-avatar": "1.0.4", "@radix-ui/react-checkbox": "1.0.4", "@radix-ui/react-dialog": "1.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af4a0bf..55266c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ dependencies: '@libsql/client': specifier: 0.4.3 version: 0.4.3 + '@radix-ui/react-aspect-ratio': + specifier: 1.0.3 + version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-avatar': specifier: 1.0.4 version: 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) @@ -2077,7 +2080,28 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.5 + '@babel/runtime': 7.24.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.61 + '@types/react-dom': 18.2.19 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@radix-ui/react-aspect-ratio@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-fXR5kbMan9oQqMuacfzlGG/SQMcmMlZ4wrvpckv8SgUulD0MMpspxJrxg/Gp/ISV3JfV1AeSWTYK9GvxA4ySwA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.61 '@types/react-dom': 18.2.19 @@ -2528,7 +2552,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.5 + '@babel/runtime': 7.24.0 '@radix-ui/react-slot': 1.0.2(@types/react@18.2.61)(react@18.2.0) '@types/react': 18.2.61 '@types/react-dom': 18.2.19 @@ -2571,7 +2595,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.5 + '@babel/runtime': 7.24.0 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.61)(react@18.2.0) @@ -2733,7 +2757,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.5 + '@babel/runtime': 7.24.0 '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.61)(react@18.2.0) '@types/react': 18.2.61 react: 18.2.0 @@ -2776,7 +2800,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.5 + '@babel/runtime': 7.24.0 '@radix-ui/rect': 1.0.1 '@types/react': 18.2.61 react: 18.2.0 @@ -2821,7 +2845,7 @@ packages: /@radix-ui/rect@1.0.1: resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} dependencies: - '@babel/runtime': 7.23.5 + '@babel/runtime': 7.24.0 dev: false /@remix-run/css-bundle@2.8.0:
+ {post.meta.readingTime.text} +
+ {post.meta.matter.publishedDate} +