diff --git a/app/features/articles/articles-schemas.server.ts b/app/features/articles/articles-schemas.server.ts index f6c13b9be..7ae2847d0 100644 --- a/app/features/articles/articles-schemas.server.ts +++ b/app/features/articles/articles-schemas.server.ts @@ -1,7 +1,14 @@ import { z } from "zod"; +const authorName = z.string().min(1); + +const author = z.union([ + authorName, + z.object({ name: authorName, link: z.string().url() }), +]); + export const articleDataSchema = z.object({ title: z.string().min(1), - author: z.string().min(1), + author: z.union([author, z.array(author)]), date: z.date(), }); diff --git a/app/features/articles/core/bySlug.server.ts b/app/features/articles/core/bySlug.server.ts index 164b32d51..92029da0d 100644 --- a/app/features/articles/core/bySlug.server.ts +++ b/app/features/articles/core/bySlug.server.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import matter from "gray-matter"; -import { ZodError } from "zod"; +import { ZodError, type z } from "zod"; import { ARTICLES_FOLDER_PATH } from "../articles-constants"; import { articleDataSchema } from "../articles-schemas.server"; @@ -23,8 +23,8 @@ export function articleBySlug(slug: string) { month: "long", year: "numeric", }), - authorLink: authorToLink(restParsed.author), - ...restParsed, + authors: normalizeAuthors(restParsed.author), + title: restParsed.title, }; } catch (e) { if (!(e instanceof Error)) throw e; @@ -37,8 +37,20 @@ export function articleBySlug(slug: string) { } } -function authorToLink(author: string) { - if (author === "Riczi") return "https://twitter.com/Riczi2k"; +export function normalizeAuthors( + authors: z.infer["author"], +): Array<{ name: string; link: string | null }> { + if (Array.isArray(authors)) { + return authors.map((author) => { + if (typeof author === "string") { + return { name: author, link: null }; + } + return author; + }); + } - return; + if (typeof authors === "string") { + return [{ name: authors, link: null }]; + } + return [authors]; } diff --git a/app/features/articles/core/list.server.ts b/app/features/articles/core/list.server.ts index aa9641e82..a399853fc 100644 --- a/app/features/articles/core/list.server.ts +++ b/app/features/articles/core/list.server.ts @@ -1,15 +1,15 @@ import fs from "node:fs"; import path from "node:path"; import matter from "gray-matter"; -import type { z } from "zod"; import { ARTICLES_FOLDER_PATH } from "../articles-constants"; import { articleDataSchema } from "../articles-schemas.server"; +import { type articleBySlug, normalizeAuthors } from "./bySlug.server"; export async function mostRecentArticles(count: number) { const files = await fs.promises.readdir(ARTICLES_FOLDER_PATH); const articles: Array< - z.infer & { + Omit>, "content"> & { slug: string; dateString: string; } @@ -21,15 +21,17 @@ export async function mostRecentArticles(count: number) { ); const { data } = matter(rawMarkdown); - const articleData = articleDataSchema.parse(data); + const { date, ...restParsed } = articleDataSchema.parse(data); articles.push({ - ...articleData, + date, slug: file.replace(".md", ""), - dateString: articleData.date.toLocaleDateString("en-US", { + dateString: date.toLocaleDateString("en-US", { day: "2-digit", month: "long", year: "numeric", }), + authors: normalizeAuthors(restParsed.author), + title: restParsed.title, }); } diff --git a/app/features/articles/routes/a.$slug.tsx b/app/features/articles/routes/a.$slug.tsx index 9901671c5..07ae6e5b0 100644 --- a/app/features/articles/routes/a.$slug.tsx +++ b/app/features/articles/routes/a.$slug.tsx @@ -3,7 +3,7 @@ import type { MetaFunction, SerializeFrom, } from "@remix-run/node"; -import { useLoaderData } from "@remix-run/react"; +import { Link, useLoaderData } from "@remix-run/react"; import Markdown from "markdown-to-jsx"; import * as React from "react"; import { Main } from "~/components/Main"; @@ -88,9 +88,22 @@ export default function ArticlePage() { function Author() { const data = useLoaderData(); - if (data.authorLink) { - return {data.author}; - } + return data.authors.map((author, i) => { + if (!author.link) return author.name; - return <>{data.author}; + const authorLink = author.link.includes("https://sendou.ink") + ? author.link.replace("https://sendou.ink", "") + : author.link; + + return ( + + {author.link.includes("https://sendou.ink") ? ( + {author.name} + ) : ( + {author.name} + )} + {i < data.authors.length - 1 ? " & " : ""} + + ); + }); } diff --git a/app/features/articles/routes/a.tsx b/app/features/articles/routes/a.tsx index c7869cb25..41e75b73c 100644 --- a/app/features/articles/routes/a.tsx +++ b/app/features/articles/routes/a.tsx @@ -6,6 +6,7 @@ import { ARTICLES_MAIN_PAGE, articlePage, navIconUrl } from "~/utils/urls"; import { mostRecentArticles } from "../core/list.server"; import "~/styles/front.css"; +import { joinListToNaturalString } from "../../../utils/arrays"; const MAX_ARTICLES_COUNT = 100; @@ -39,8 +40,13 @@ export default function ArticlesMainPage() { {article.title}
- {t("common:articles.by", { author: article.author })} •{" "} - + {t("common:articles.by", { + author: joinListToNaturalString( + article.authors.map((a) => a.name), + "&", + ), + })}{" "} + •
))}