blog utils & helpers

This commit is contained in:
matthieu42morin 2024-04-28 01:18:49 +02:00
parent f691a6b377
commit 34f70f0412
7 changed files with 187 additions and 155 deletions

View File

@ -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<Post>('./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<Post & MarkdownMetadata> = await import(`./blog/${slug}.md`);
return {
post: { ...data.metadata, slug },
Component: data.default
};
} catch {
throw error(404, `Unable to find blog post "${slug}"`);
}
}

View File

@ -1,16 +0,0 @@
export interface MarkdownHeading {
title: string;
slug: string;
level: number;
children: MarkdownHeading[];
}
export interface MarkdownMetadata {
headings: MarkdownHeading[];
}
export interface MdsvexImport<T extends MarkdownMetadata = MarkdownMetadata> {
// Technically not correct but needed to make language-tools happy
default: ConstructorOfATypedSvelteComponent;
metadata: T;
}

View File

@ -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<T extends { date?: string }>(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<typeof create_ssr_component>).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<T extends { date?: string }>(data: Record<string, T>): 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'
});
};

View File

@ -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<T extends MarkdownMetadata = MarkdownMetadata> {
// 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;
}

96
src/lib/utils/blog.ts Normal file
View File

@ -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<MdsvexImport<Post & MarkdownMetadata>>('$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<Post> = 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.';
}
};

View File

@ -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<T extends { date?: string }>(data: Record<string, T>): 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<T extends { date?: string }>(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<typeof create_ssr_component>).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}`;
};

View File

@ -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';
/**