blog utils & helpers
This commit is contained in:
parent
f691a6b377
commit
34f70f0412
|
@ -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}"`);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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'
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -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 {
|
export interface Post extends MarkdownMetadata {
|
||||||
type?: 'Blog' | 'projects' | string;
|
slug: string;
|
||||||
date?: string;
|
title: string;
|
||||||
|
postTitle: string;
|
||||||
|
type: 'blog' | 'projects';
|
||||||
excerpt: string;
|
excerpt: string;
|
||||||
image: string;
|
datePublished: string;
|
||||||
slug?: string;
|
lastUpdated: string;
|
||||||
|
seoMetaDescription: string;
|
||||||
|
focusKeyphrase: string;
|
||||||
|
featuredImage: string;
|
||||||
|
featuredImageAlt: string;
|
||||||
|
imagePublicId: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
tags?: Tag[];
|
tags?: Tag[];
|
||||||
subtitle?: string;
|
timeToRead: number;
|
||||||
teaserImage: string;
|
isAnExternalLink?: boolean;
|
||||||
title: string;
|
ogImage?: string;
|
||||||
isNotAnActualPost?: boolean;
|
ogSquareImage?: string;
|
||||||
|
twitterImage?: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.';
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,4 +1,55 @@
|
||||||
import { readable } from 'svelte/store';
|
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.
|
* 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'
|
behavior: mediaQuery.matches ? 'auto' : 'smooth'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateURL = (href?: string, type: string, slug?: string) => {
|
||||||
|
if (href) return href;
|
||||||
|
return `/${type}/${slug}`;
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type { MdsvexImport } from '$content/types';
|
import type { MdsvexImport, MarkdownMetadata } from '$lib/types/post';
|
||||||
import type { MarkdownMetadata } from '$content/types';
|
|
||||||
import type { Project } from '$lib/types/projects';
|
import type { Project } from '$lib/types/projects';
|
||||||
import { parseReadContent } from './utils';
|
import { parseReadContent } from '$lib/utils/blog';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
/**
|
/**
|
Loading…
Reference in New Issue