From 34f70f041250911874abd36f8bc96b849cce1cc6 Mon Sep 17 00:00:00 2001 From: matthieu42morin Date: Sun, 28 Apr 2024 01:18:49 +0200 Subject: [PATCH] blog utils & helpers --- src/content/blog.ts | 32 --------- src/content/types.d.ts | 16 ----- src/content/utils.ts | 94 ------------------------- src/lib/types/post.d.ts | 43 +++++++++--- src/lib/utils/blog.ts | 96 ++++++++++++++++++++++++++ src/lib/utils/helpers.ts | 56 +++++++++++++++ src/{content => lib/utils}/projects.ts | 5 +- 7 files changed, 187 insertions(+), 155 deletions(-) delete mode 100644 src/content/blog.ts delete mode 100644 src/content/types.d.ts delete mode 100644 src/content/utils.ts create mode 100644 src/lib/utils/blog.ts rename src/{content => lib/utils}/projects.ts (82%) diff --git a/src/content/blog.ts b/src/content/blog.ts deleted file mode 100644 index 339df8e..0000000 --- a/src/content/blog.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Post } from '$lib/types/post'; -import type { MarkdownMetadata } from '$content/types'; -import type { MdsvexImport } from './types'; -import { parseReadContent } from '$content/utils'; -import { error } from '@sveltejs/kit'; - -export function listPosts() { - const posts = import.meta.glob('./blog/*.md', { - eager: true, - import: 'metadata' - }); - - return parseReadContent(posts); -} - -export async function getPostMetadata(slug: string) { - const { post } = await getPost(slug); - return post; -} - -export async function getPost(slug: string) { - try { - const data: MdsvexImport = await import(`./blog/${slug}.md`); - - return { - post: { ...data.metadata, slug }, - Component: data.default - }; - } catch { - throw error(404, `Unable to find blog post "${slug}"`); - } -} diff --git a/src/content/types.d.ts b/src/content/types.d.ts deleted file mode 100644 index 9655b9f..0000000 --- a/src/content/types.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface MarkdownHeading { - title: string; - slug: string; - level: number; - children: MarkdownHeading[]; -} - -export interface MarkdownMetadata { - headings: MarkdownHeading[]; -} - -export interface MdsvexImport { - // Technically not correct but needed to make language-tools happy - default: ConstructorOfATypedSvelteComponent; - metadata: T; -} diff --git a/src/content/utils.ts b/src/content/utils.ts deleted file mode 100644 index 5ed792b..0000000 --- a/src/content/utils.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { create_ssr_component } from 'svelte/internal'; - -/** - * Sorts an array of objects by their `date` property in descending order. - * @param a - The first object to compare. - * @param b - The second object to compare. - * @returns A number that represents the difference between the parsed dates of the two objects. - */ -export function dateSort(a: T, b: T): number { - return Date.parse(b.date) - Date.parse(a.date); -} - -/** - * Renders an mdsvex component and returns the resulting HTML string. - * @param component - The mdsvex component to render. - * @returns The HTML string that was generated by rendering the component. - * @throws An error if the `render` property of the component is not a function. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function renderMdsvexComponent(component: any): string { - if (typeof component['render'] != 'function') { - throw new Error("Unable to render something that isn't a mdsvex component"); - } - - return (component as ReturnType).render().html; -} - -/** - * Converts an md file path to a slug by removing the last segment of the path and the `.md` extension. - * @param path - The path of the md file. - * @returns The slug of the md file. - */ -export function mdPathToSlug(path: string) { - return path.split('/').at(-1).slice(0, -3); -} - -/** - * Parses an object of data that has string keys and values of type `T` that have an optional `date` property. - * @param data - The object of data to parse. - * @param dateSort - A function that sorts an array of objects by their `date` property in descending order. - * @param mdPathToSlug - A function that converts an md file path to a slug. - * @returns An array of objects that have a `slug` property and the properties of the original data objects. - */ -export function parseReadContent(data: Record): T[] { - return Object.entries(data) - .map(([file, data]) => ({ - slug: mdPathToSlug(file), - ...data - })) - .sort(dateSort); -} - -// Old utils -// /** -// * Formats a date string using the specified date style and locale. -// * @param date - The date string to format. -// * @param dateStyle - The style to use when formatting the date. Defaults to 'medium'. -// * @param locales - The locale to use when formatting the date. Defaults to 'en'. -// * @returns The formatted date string. -// */ -// export function formatDate(date: string, dateStyle: DateStyle = 'medium', locales = 'en') { -// const formatter = new Intl.DateTimeFormat(locales, { dateStyle }); -// return formatter.format(new Date(date)); -// } -// type DateStyle = Intl.DateTimeFormatOptions['dateStyle']; -/** - * Formats a date string into a human-readable format. - * @param date - The date string to format. - * @returns A string representing the formatted date, or an empty string if the input is invalid. - */ -export const formatDate = (date) => { - try { - const d = new Date(date); - return `${d.toLocaleString('default', { - month: 'long' - })} ${d.getDate()}, ${d.getFullYear()}`; - } catch (e) { - return ''; - } -}; - -export const scrollIntoView = (selector: string) => { - const scrollToElement = document.querySelector(selector); - - if (!scrollToElement) return; - - const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); - - scrollToElement.scrollIntoView({ - block: 'nearest', - inline: 'start', - behavior: mediaQuery.matches ? 'auto' : 'smooth' - }); -}; diff --git a/src/lib/types/post.d.ts b/src/lib/types/post.d.ts index 296f347..ced0492 100644 --- a/src/lib/types/post.d.ts +++ b/src/lib/types/post.d.ts @@ -1,17 +1,40 @@ -import type { MarkdownMetadata } from '$content/types'; +export interface MarkdownHeading { + title: string; + slug: string; + level: number; + children: MarkdownHeading[]; +} -export type Tag = 'DevOps' | 'Philosophy' | 'Updates' | ''; +export interface MarkdownMetadata { + headings: MarkdownHeading[]; +} + +export interface MdsvexImport { + // Technically not correct but needed to make language-tools happy + default: ConstructorOfATypedSvelteComponent; + metadata: T; +} + +export type Tag = 'Projects' | 'Blog' | 'Updates' | ''; export interface Post extends MarkdownMetadata { - type?: 'Blog' | 'projects' | string; - date?: string; + slug: string; + title: string; + postTitle: string; + type: 'blog' | 'projects'; excerpt: string; - image: string; - slug?: string; + datePublished: string; + lastUpdated: string; + seoMetaDescription: string; + focusKeyphrase: string; + featuredImage: string; + featuredImageAlt: string; + imagePublicId: string; href?: string; tags?: Tag[]; - subtitle?: string; - teaserImage: string; - title: string; - isNotAnActualPost?: boolean; + timeToRead: number; + isAnExternalLink?: boolean; + ogImage?: string; + ogSquareImage?: string; + twitterImage?: string; } diff --git a/src/lib/utils/blog.ts b/src/lib/utils/blog.ts new file mode 100644 index 0000000..3405db3 --- /dev/null +++ b/src/lib/utils/blog.ts @@ -0,0 +1,96 @@ +import type { Post } from '$lib/types/post'; +import type { MdsvexImport, MarkdownMetadata } from '$lib/types/post'; +import { error } from '@sveltejs/kit'; +import readingTime from 'reading-time'; + +import { parseReadContent } from '$lib/utils/helpers'; + +const posts = import.meta.glob>('$content/blog/*.md', { + eager: true, + import: 'metadata' +}); + +export function listPosts() { + return parseReadContent(posts); +} + +export function getPost(slug: string) { + try { + const postKey = `$content/blog/${slug}.md`; + const post = posts[postKey]; + + if (!post) { + throw error(404, `Unable to find blog post "${slug}"`); + } + + const { imagePublicId } = data.metadata; + try { + return { + post: { + ...data.metadata, + slug, + timeToRead: Math.ceil(readingTime(Component).minutes), + featuredImage: getImageUrl(imagePublicId, { width: 1200, height: 630 }), + ogImage: getImageUrl(imagePublicId, { width: 1200, height: 630 }), + ogSquareImage: getImageUrl(imagePublicId, { width: 400, height: 400 }), + twitterImage: getImageUrl(imagePublicId, { width: 1200, height: 630 }) + }, + Component: data.default + }; + } catch (error) { + throw error(404, `Unable to return blog post "${slug}"`); + } + } catch (error) { + console.error('Error fetching blog post:', error); + throw error; + } +} + +// export async function getPost(slug: string) { +// try { +// const postPath = Object.keys(posts).find((path) => path.includes(`${slug}.md`)); +// if (!postPath) { +// throw error(404, `Unable to find blog post "${slug}"`); +// } +// const data = await posts[postPath](); + +// const { imagePublicId } = data.metadata; + +// const data: MdsvexImport = await import.meta.glob(`$content/blog/${slug}.md`); + +// return { +// post: { +// ...data.metadata, +// slug, +// timeToRead: Math.ceil(readingTime(data.default).minutes), +// featuredImage: getImageUrl(imagePublicId, { width: 1200, height: 630 }), +// ogImage: getImageUrl(imagePublicId, { width: 1200, height: 630 }), +// ogSquareImage: getImageUrl(imagePublicId, { width: 400, height: 400 }), +// twitterImage: getImageUrl(imagePublicId, { width: 1200, height: 630 }) +// }, +// Component: data.default +// }; +// } catch (error) { +// console.error('Error fetching blog post:', error); +// // +// } +// } + +/** + * Formats a date string into a human-readable format. + * @param date - The date string to format. + * @param locale - The locale to use when formatting the date. Defaults to 'en-US'. + * @returns A string representing the formatted date, or an error something is invalid. + */ +export const formatDate = (date: string, locale: Intl.Locale = 'en-US') => { + try { + const options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'long', + day: 'numeric' + }; + return new Date(date).toLocaleDateString(locale, options); + } catch (e) { + return 'Invalid date string provided or something failed in formatting date.'; + } +}; diff --git a/src/lib/utils/helpers.ts b/src/lib/utils/helpers.ts index afe8174..ba760fe 100644 --- a/src/lib/utils/helpers.ts +++ b/src/lib/utils/helpers.ts @@ -1,4 +1,55 @@ import { readable } from 'svelte/store'; +import type { create_ssr_component } from 'svelte/internal'; + +/** + * Parses an object of data that has string keys and values of type `T` that have an optional `date` property. + * @param data - The object of data to parse. + * @param dateSort - A function that sorts an array of objects by their `date` property in descending order. + * @param mdPathToSlug - A function that converts an md file path to a slug. + * @returns An array of objects that have a `slug` property and the properties of the original data objects. + */ +export function parseReadContent(data: Record): T[] { + return Object.entries(data) + .map(([file, data]) => ({ + slug: mdPathToSlug(file), + ...data + })) + .sort(dateSort); +} + +/** + * Sorts an array of objects by their `date` property in descending order. + * @param a - The first object to compare. + * @param b - The second object to compare. + * @returns A number that represents the difference between the parsed dates of the two objects. + */ +export function dateSort(a: T, b: T): number { + return Date.parse(b.date) - Date.parse(a.date); +} + +/** + * Renders an mdsvex component and returns the resulting HTML string. + * @param component - The mdsvex component to render. + * @returns The HTML string that was generated by rendering the component. + * @throws An error if the `render` property of the component is not a function. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function renderMdsvexComponent(component: any): string { + if (typeof component['render'] != 'function') { + throw new Error("Unable to render something that isn't a mdsvex component"); + } + + return (component as ReturnType).render().html; +} + +/** + * Converts an md file path to a slug by removing the last segment of the path and the `.md` extension. + * @param path - The path of the md file. + * @returns The slug of the md file. + */ +export function mdPathToSlug(path: string) { + return path.split('/').at(-1).slice(0, -3); +} /** * Determines if the current timezone is between 0 and +3 hours UTC. @@ -105,3 +156,8 @@ export const scrollIntoView = (selector: string) => { behavior: mediaQuery.matches ? 'auto' : 'smooth' }); }; + +export const generateURL = (href?: string, type: string, slug?: string) => { + if (href) return href; + return `/${type}/${slug}`; +}; diff --git a/src/content/projects.ts b/src/lib/utils/projects.ts similarity index 82% rename from src/content/projects.ts rename to src/lib/utils/projects.ts index bd46ce9..952f2a3 100644 --- a/src/content/projects.ts +++ b/src/lib/utils/projects.ts @@ -1,7 +1,6 @@ -import type { MdsvexImport } from '$content/types'; -import type { MarkdownMetadata } from '$content/types'; +import type { MdsvexImport, MarkdownMetadata } from '$lib/types/post'; import type { Project } from '$lib/types/projects'; -import { parseReadContent } from './utils'; +import { parseReadContent } from '$lib/utils/blog'; import { error } from '@sveltejs/kit'; /**