diff --git a/astro.config.mjs b/astro.config.mjs index 48d7e7f..be3b025 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -89,11 +89,16 @@ export default defineConfig({ if (page.startsWith("https://ayc0.github.io/posts/drafts/")) { return false; } - // Same for the listing of tags posts + // Same for the listing of posts tags if (page.startsWith("https://ayc0.github.io/posts/tags/")) { return false; } + // Same for the listing of posts series + if (page.startsWith("https://ayc0.github.io/posts/series/")) { + return false; + } + return true; }, }), diff --git a/src/components/LinkToPost.astro b/src/components/LinkToPost.astro index 8497686..981f0b6 100644 --- a/src/components/LinkToPost.astro +++ b/src/components/LinkToPost.astro @@ -2,13 +2,19 @@ import { getEntry, render } from "astro:content"; import { LinkCard } from "@astrojs/starlight/components"; -import { getCreatedDate, getTagsHtml } from "@components/get-posts"; +import { + getCreatedDate, + getSeriesHtml, + getTagsHtml, +} from "@components/get-posts"; export interface Props { slug: string; includeDate?: boolean; includeTags?: boolean; includeReadingTime?: boolean; + includeDescription?: boolean; + includeSeries?: boolean; } const { @@ -16,13 +22,15 @@ const { includeDate, includeTags, includeReadingTime = true, + includeDescription = true, + includeSeries = true, } = Astro.props; -const entry = await getEntry( +const post = await getEntry( "docs", slug.startsWith("posts/") ? slug : "posts/" + slug, ); -if (entry == null) { +if (post == null) { throw new Error(`slug ${slug} isn’t valid`); } @@ -31,12 +39,12 @@ if (entry == null) { let description: string | undefined; if (includeReadingTime) { - const { remarkPluginFrontmatter } = await render(entry); + const { remarkPluginFrontmatter } = await render(post); description = remarkPluginFrontmatter.readingTime.displayedText; } if (includeDate) { - const date = getCreatedDate(entry); + const date = getCreatedDate(post); if (!description) { description = date; } else { @@ -44,7 +52,7 @@ if (includeDate) { } } if (includeTags) { - const tags = getTagsHtml(entry); + const tags = getTagsHtml(post); if (tags) { if (!description) { description = tags; @@ -54,8 +62,8 @@ if (includeTags) { } } -if (entry.data.description) { - const descriptionToAdd = entry.data.description; +if (includeDescription && post.data.description) { + const descriptionToAdd = post.data.description; if (!description) { description = descriptionToAdd; } else { @@ -64,12 +72,20 @@ if (entry.data.description) { } const props: { href: string; title: string; description?: string } = { - href: import.meta.env.BASE_URL + entry.slug, - title: entry.data.title, + href: import.meta.env.BASE_URL + post.slug, + title: post.data.title, }; if (description) { props.description = description; } + +if (includeSeries) { + const seriesHtml = getSeriesHtml(post); + if (seriesHtml) { + // is here because these are nested links, and browsers don't like that + props.title += ` ${seriesHtml}`; + } +} ---
diff --git a/src/components/get-posts.ts b/src/components/get-posts.ts index b3a32ee..1c28c21 100644 --- a/src/components/get-posts.ts +++ b/src/components/get-posts.ts @@ -90,23 +90,23 @@ export const getDraftPosts = async () => { return posts.sort((postA, postB) => getTime(postB) - getTime(postA)); }; -export const getCreatedDate = (post: Pick): string => { - if (!post.data.createdAt) { +export const getCreatedDate = ({ data }: Pick): string => { + if (!data.createdAt) { return ""; } - let full = `Posted on ${post.data.createdAt.toLocaleDateString("en-US", { + let full = `Posted on ${data.createdAt.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", })}`; - if (post.data.lastUpdated instanceof Date) { + if (data.lastUpdated instanceof Date) { if ( - post.data.createdAt.toLocaleDateString("en-US") !== - post.data.lastUpdated.toLocaleDateString("en-US") + data.createdAt.toLocaleDateString("en-US") !== + data.lastUpdated.toLocaleDateString("en-US") ) { - full += ` (Edited on ${post.data.lastUpdated.toLocaleDateString("en-US", { + full += ` (Edited on ${data.lastUpdated.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", @@ -117,12 +117,74 @@ export const getCreatedDate = (post: Pick): string => { return full; }; -export const getTagsHtml = (post: Pick): string | null => { - if (!post.data.tags?.length) { +export const getTagsHtml = ({ + data: { tags }, +}: Pick): string | null => { + if (!tags?.length) { return null; } - // TODO: turn those into links, but links to where? - return post.data.tags - .map((tag) => `#${tag}`) - .join(" "); + // TODO: turn those into links & better styling, but links to where? + return tags.map((tag) => `#${tag}`).join(" "); +}; + +export const getSeriesData = ({ + data: { series }, +}: Pick): { name: string; order: number } | undefined => { + let name: string; + let order = 0; + if (!series) { + return; + } + if (typeof series === "string") { + name = series; + } else { + name = series.name; + if (series.order != null) { + order = series.order; + } + } + + return { name: name, order }; +}; + +export const getSeriesHtml = (post: Pick) => { + const seriesData = getSeriesData(post); + if (!seriesData) { + return null; + } + return `${seriesData.name}`; +}; + +export const getPublishablePostsMatchingSeries = async ( + series: string, +): Promise => { + const posts = await getPublishablePosts(); + const postsInSameSeries = posts.filter((post) => { + const postSeriesData = getSeriesData(post); + if (!postSeriesData) { + return false; + } + return postSeriesData.name === series; + }); + + postsInSameSeries.sort((postA, postB) => { + if (!postA.data.createdAt && !postB.data.createdAt) { + return 0; + } + if (!postA.data.createdAt) { + return -1; + } + if (!postB.data.createdAt) { + return 1; + } + if (postA.data.createdAt.getTime() !== postB.data.createdAt.getTime()) { + return postA.data.createdAt.getTime() - postB.data.createdAt.getTime(); + } + + const postASeriesData = getSeriesData(postA)!; + const postBSeriesData = getSeriesData(postB)!; + return postASeriesData.order - postBSeriesData.order; + }); + + return postsInSameSeries; }; diff --git a/src/components/starlight/PageFrame.astro b/src/components/starlight/PageFrame.astro index 1541051..52cbfca 100644 --- a/src/components/starlight/PageFrame.astro +++ b/src/components/starlight/PageFrame.astro @@ -35,6 +35,24 @@ Astro.locals.starlightRoute.hasSidebar = false; display: flex; /* On mobile, keep the buttons & theme in header */ margin-left: auto; /* and put it on the right, instead of centered */ } + + [data-series] { + /* TODO: more beautiful colors */ + background-color: var(--sl-color-bg-inline-code); + color: var(--sl-color-white); + + margin-block: -0.125em; + padding: 0.125em 0.375em; + border-radius: 0.375em; + font-size: max(var(--sl-text-code-sm), 0.5em); + font-style: italic; + vertical-align: middle; + + text-decoration: none; + } + [data-series]:hover { + color: var(--sl-color-text-accent); + } diff --git a/src/components/starlight/PageTitle.astro b/src/components/starlight/PageTitle.astro index 602f94f..908e7b6 100644 --- a/src/components/starlight/PageTitle.astro +++ b/src/components/starlight/PageTitle.astro @@ -1,20 +1,46 @@ --- import { render } from "astro:content"; -import Default from "@astrojs/starlight/components/PageTitle.astro"; +// Most of the HTML was copied from @astrojs/starlight/components/PageTitle.astro -import { getCreatedDate, getTagsHtml } from "@components/get-posts"; +import { + getCreatedDate, + getSeriesHtml, + getTagsHtml, +} from "@components/get-posts"; + +const post = Astro.locals.starlightRoute.entry; const { remarkPluginFrontmatter } = await render( // @ts-expect-error – this works, but for some reasons, it doesn’t seem to be compatible at the type level - Astro.locals.starlightRoute.entry, + post, ); -const createdDate = getCreatedDate(Astro.locals.starlightRoute.entry); -const tags = getTagsHtml(Astro.locals.starlightRoute.entry); +const createdDate = getCreatedDate(post); +const tags = getTagsHtml(post); + +// Copied from node_modules/@astrojs/starlight/constants.ts as not exported +const PAGE_TITLE_ID = "_top"; + +const series = getSeriesHtml(post); --- - +

+ {post.data.title} + {series ? : null} +

+ + { // Only display the reading time & last updated when it is a doc diff --git a/src/components/starlight/Pagination.astro b/src/components/starlight/Pagination.astro index 82f10e0..262e5d1 100644 --- a/src/components/starlight/Pagination.astro +++ b/src/components/starlight/Pagination.astro @@ -1,60 +1,19 @@ --- import Default from "@astrojs/starlight/components/Pagination.astro"; -import { getPublishablePosts, type Post } from "@components/get-posts"; +import { + getSeriesData, + getPublishablePostsMatchingSeries, +} from "@components/get-posts"; -const getSeriesData = ( - data: Post["data"], -): { name: string; order: number } | undefined => { - let name: string; - let order = 0; - if (!data.series) { - return; - } - if (typeof data.series === "string") { - name = data.series; - } else { - name = data.series.name; - if (data.series.order != null) { - order = data.series.order; - } - } - - return { name: name, order }; -}; - -const thisSeriesData = getSeriesData(Astro.locals.starlightRoute.entry.data); +const thisSeriesData = getSeriesData(Astro.locals.starlightRoute.entry); Astro.locals.starlightRoute.pagination.prev = undefined; Astro.locals.starlightRoute.pagination.next = undefined; if (thisSeriesData) { - const posts = await getPublishablePosts(); - const postsInSameSeries = posts.filter((post) => { - const postSeriesData = getSeriesData(post.data); - if (!postSeriesData) { - return false; - } - return postSeriesData.name === thisSeriesData.name; - }); - - postsInSameSeries.sort((postA, postB) => { - if (!postA.data.createdAt && !postB.data.createdAt) { - return 0; - } - if (!postA.data.createdAt) { - return -1; - } - if (!postB.data.createdAt) { - return 1; - } - if (postA.data.createdAt.getTime() !== postB.data.createdAt.getTime()) { - return postA.data.createdAt.getTime() - postB.data.createdAt.getTime(); - } - - const postASeriesData = getSeriesData(postA.data)!; - const postBSeriesData = getSeriesData(postB.data)!; - return postASeriesData.order - postBSeriesData.order; - }); + const postsInSameSeries = await getPublishablePostsMatchingSeries( + thisSeriesData.name, + ); const indexOfCurrentPost = postsInSameSeries.findIndex( (post) => post.id === Astro.locals.starlightRoute.id, diff --git a/src/content/config.ts b/src/content/config.ts index 956d87e..467cae4 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -23,7 +23,7 @@ const tags = [ "yarn", ] as const; -const series = ["Light/dark"] as const; +export const series = ["Light/dark"] as const; export const collections = { docs: defineCollection({ diff --git a/src/content/docs/posts/drafts.mdx b/src/content/docs/posts/drafts.mdx index 622ba33..1408c30 100644 --- a/src/content/docs/posts/drafts.mdx +++ b/src/content/docs/posts/drafts.mdx @@ -8,4 +8,7 @@ import { getDraftPosts } from "@components/get-posts"; List of all posts that are in the process, but not ready to be open to public: -{(await getDraftPosts()).slice(0, 10).map((post) => )} +<> +{(await getDraftPosts()).slice(0, 10).map((post) => ( + +))} diff --git a/src/content/docs/posts/full.mdx b/src/content/docs/posts/full.mdx index 249952c..106a6f0 100644 --- a/src/content/docs/posts/full.mdx +++ b/src/content/docs/posts/full.mdx @@ -6,9 +6,16 @@ template: splash import LinkToPost from "@components/LinkToPost.astro"; import { getPublishablePostsByYear } from "@components/get-posts"; -{ -Object.entries(await getPublishablePostsByYear()).reverse().map(([year, posts]) => { -return (<>

{year}

-{posts.map(post => )}) -}) -} +<> +{Object.entries(await getPublishablePostsByYear()) + .reverse() + .map(([year, posts]) => { + return ( + <> +

{year}

+ {posts.map((post) => ( + + ))} + + ); + })} diff --git a/src/content/docs/posts/series.mdx b/src/content/docs/posts/series.mdx new file mode 100644 index 0000000..3494c76 --- /dev/null +++ b/src/content/docs/posts/series.mdx @@ -0,0 +1,48 @@ +--- +title: "Series" +template: splash +hasHeader: false +--- + +import LinkToPost from "@components/LinkToPost.astro"; +import { getPublishablePostsMatchingSeries } from "@components/get-posts"; +import { series as seriesList } from "../../config"; + +<> +{await Promise.all( + seriesList.map(async (series) => { + const posts = await getPublishablePostsMatchingSeries(series); + return ( + <> +

+ {series} +

+
+ {posts.map((post) => ( + + ))} +
+ + ); + }), +)} + + diff --git a/src/content/docs/posts/tags.mdx b/src/content/docs/posts/tags.mdx index f952fcb..fccf9d8 100644 --- a/src/content/docs/posts/tags.mdx +++ b/src/content/docs/posts/tags.mdx @@ -6,17 +6,26 @@ template: splash import LinkToPost from "@components/LinkToPost.astro"; import { getPostsByTags } from "@components/get-posts"; -← View all -{ -Object.entries(await getPostsByTags()).map(([tag, posts]) => { -return (<>

#{tag}

- -
    -{posts.map(post =>
  • {post.data.title} | {post.data.tags.map(tag => `#${tag}`).join(' ')}
  • )} -
-) -}) -} + + ← View all + +{Object.entries(await getPostsByTags()).map(([tag, posts]) => { + return ( + <> +

+ #{tag} +

+
    + {posts.map((post) => ( +
  • + {post.data.title} |{" "} + {post.data.tags.map((tag) => `#${tag}`).join(" ")} +
  • + ))} +
+ + ); +})}