Add qualifyLocalURLsInMarkdown()
This will handle special links in the description and similar fields
This commit is contained in:
parent
c38275e2f2
commit
cd67f6a5c5
20 changed files with 982 additions and 296 deletions
|
|
@ -1,14 +1,16 @@
|
|||
import type { APIRoute, GetStaticPaths } from "astro";
|
||||
import { getCollection, type CollectionEntry, getEntries } from "astro:content";
|
||||
import type { Website } from "../../../content/config";
|
||||
import { t, type Lang } from "../../../i18n";
|
||||
import type { PostWebsite } from "../../../content/config";
|
||||
import { t } from "../../../i18n";
|
||||
import { formatCopyrightedCharacters } from "../../../utils/format_copyrighted_characters";
|
||||
import { markdownToBbcode } from "../../../utils/markdown_to_bbcode";
|
||||
import { getUsernameForLang } from "../../../utils/get_username_for_lang";
|
||||
import { isAnonymousUser } from "../../../utils/is_anonymous_user";
|
||||
import { qualifyLocalURLsInMarkdown } from "../../../utils/qualify_local_urls_in_markdown";
|
||||
import { getWebsiteLinkForUser } from "../../../utils/get_website_link_for_user";
|
||||
|
||||
interface ExportWebsiteInfo {
|
||||
website: Website;
|
||||
website: PostWebsite;
|
||||
exportFormat: "bbcode" | "markdown";
|
||||
}
|
||||
|
||||
|
|
@ -22,76 +24,6 @@ const WEBSITE_LIST = [
|
|||
|
||||
type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<{ website: infer K }> ? K : never;
|
||||
|
||||
function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website): string {
|
||||
const link = user.data.links[website];
|
||||
if (link && "username" in link && link.username) {
|
||||
return link.username;
|
||||
}
|
||||
throw new Error(`Cannot get "${website}" username for user "${user.id}"`);
|
||||
}
|
||||
|
||||
function isPreferredWebsite(user: CollectionEntry<"users">, website: Website): boolean {
|
||||
const { preferredLink } = user.data;
|
||||
return !preferredLink || preferredLink == website;
|
||||
}
|
||||
|
||||
function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteName, lang: Lang): string {
|
||||
const { links, preferredLink } = user.data;
|
||||
switch (website) {
|
||||
case "eka":
|
||||
if ("eka" in links) {
|
||||
return `:icon${getUsernameForWebsite(user, "eka")}:`;
|
||||
}
|
||||
break;
|
||||
case "furaffinity":
|
||||
if ("furaffinity" in links) {
|
||||
return `:icon${getUsernameForWebsite(user, "furaffinity")}:`;
|
||||
}
|
||||
break;
|
||||
case "weasyl":
|
||||
if ("weasyl" in links) {
|
||||
return `<!~${getUsernameForWebsite(user, "weasyl").replaceAll(" ", "")}>`;
|
||||
} else if (isPreferredWebsite(user, "furaffinity")) {
|
||||
return `<fa:${getUsernameForWebsite(user, "furaffinity").replaceAll("_", "")}>`;
|
||||
} else if (isPreferredWebsite(user, "inkbunny")) {
|
||||
return `<ib:${getUsernameForWebsite(user, "inkbunny")}>`;
|
||||
} else if (isPreferredWebsite(user, "sofurry")) {
|
||||
return `<sf:${getUsernameForWebsite(user, "sofurry")}>`;
|
||||
}
|
||||
break;
|
||||
case "inkbunny":
|
||||
if ("inkbunny" in links) {
|
||||
return `[iconname]${getUsernameForWebsite(user, "inkbunny")}[/iconname]`;
|
||||
} else if (isPreferredWebsite(user, "furaffinity")) {
|
||||
return `[fa]${getUsernameForWebsite(user, "furaffinity")}[/fa]`;
|
||||
} else if (isPreferredWebsite(user, "sofurry")) {
|
||||
return `[sf]${getUsernameForWebsite(user, "sofurry")}[/sf]`;
|
||||
} else if (isPreferredWebsite(user, "weasyl")) {
|
||||
return `[weasyl]${getUsernameForWebsite(user, "weasyl")}[/weasyl]`;
|
||||
}
|
||||
break;
|
||||
case "sofurry":
|
||||
if ("sofurry" in links) {
|
||||
return `:icon${getUsernameForWebsite(user, "sofurry")}:`;
|
||||
} else if (isPreferredWebsite(user, "furaffinity")) {
|
||||
return `fa!${getUsernameForWebsite(user, "furaffinity")}`;
|
||||
} else if (isPreferredWebsite(user, "inkbunny")) {
|
||||
return `ib!${getUsernameForWebsite(user, "inkbunny")}`;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
const unknown: never = website;
|
||||
throw new Error(`Unhandled export website "${unknown}"`);
|
||||
}
|
||||
if (preferredLink) {
|
||||
const preferred = links[preferredLink]!;
|
||||
return `[${getUsernameForLang(user, lang)}](${preferred.link})`;
|
||||
}
|
||||
throw new Error(
|
||||
`No matching "${website}" link for user "${user.id}" (consider setting their "preferredLink" property)`,
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
story: CollectionEntry<"stories">;
|
||||
};
|
||||
|
|
@ -111,18 +43,21 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||
};
|
||||
|
||||
export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) => {
|
||||
const { lang } = story.data;
|
||||
const copyrightedCharacters = await formatCopyrightedCharacters(story.data.copyrightedCharacters);
|
||||
const authorsList = await getEntries(story.data.authors);
|
||||
const commissionersList = story.data.commissioner && (await getEntries(story.data.commissioner));
|
||||
const requestersList = story.data.requester && (await getEntries(story.data.requester));
|
||||
try {
|
||||
const { lang } = story.data;
|
||||
const copyrightedCharacters = await formatCopyrightedCharacters(story.data.copyrightedCharacters);
|
||||
const authorsList = await getEntries(story.data.authors);
|
||||
const commissionersList = story.data.commissioner && (await getEntries(story.data.commissioner));
|
||||
const requestersList = story.data.requester && (await getEntries(story.data.requester));
|
||||
|
||||
const description = Object.fromEntries(
|
||||
WEBSITE_LIST.map<[ExportWebsiteName, string]>(({ website, exportFormat }) => {
|
||||
const u = (user: CollectionEntry<"users">) =>
|
||||
isAnonymousUser(user) ? getUsernameForLang(user, lang) : getLinkForUser(user, website, lang);
|
||||
const storyDescription = (
|
||||
[
|
||||
const description = await Promise.all(
|
||||
WEBSITE_LIST.map(async ({ website, exportFormat }) => {
|
||||
const exportWebsite: ExportWebsiteName = website;
|
||||
const u = (user: CollectionEntry<"users">) =>
|
||||
isAnonymousUser(user)
|
||||
? getUsernameForLang(user, lang)
|
||||
: getWebsiteLinkForUser(user, exportWebsite, (user) => getUsernameForLang(user, lang));
|
||||
const storyDescription = await [
|
||||
story.data.description,
|
||||
`*${t(lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}*`,
|
||||
t(
|
||||
|
|
@ -142,62 +77,76 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
|||
"export_story/commissioned_by",
|
||||
commissionersList.map((commissioner) => u(commissioner)),
|
||||
),
|
||||
...copyrightedCharacters.map(([user, characterList]) =>
|
||||
characterList[0] == ""
|
||||
? t(lang, "characters/all_characters_are_copyrighted_by", u(user))
|
||||
: t(lang, "characters/characters_are_copyrighted_by", u(user), characterList),
|
||||
...copyrightedCharacters.map(({ user, characters }) =>
|
||||
t(lang, "characters/characters_are_copyrighted_by", u(user), characters[0] == "" ? [] : characters),
|
||||
),
|
||||
].filter((data) => data) as string[]
|
||||
)
|
||||
.join("\n\n")
|
||||
.replaceAll(
|
||||
/\[([^\]]+)\]\((\/[^\)]+)\)/g,
|
||||
(_, group1, group2) => `[${group1}](${new URL(group2, site).toString()})`,
|
||||
);
|
||||
switch (exportFormat) {
|
||||
case "bbcode":
|
||||
return [website, markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n")];
|
||||
case "markdown":
|
||||
return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
|
||||
default:
|
||||
const unknown: never = exportFormat;
|
||||
throw new Error(`Unknown export format "${unknown}"`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
].reduce(async (promise, data) => {
|
||||
if (!data) {
|
||||
return promise;
|
||||
}
|
||||
const acc = await promise;
|
||||
const newData = await qualifyLocalURLsInMarkdown(data, lang, site, exportWebsite);
|
||||
return acc ? `${acc}\n\n${newData}` : newData;
|
||||
}, Promise.resolve(""));
|
||||
switch (exportFormat) {
|
||||
case "bbcode":
|
||||
return { exportWebsite, description: markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n") };
|
||||
case "markdown":
|
||||
return { exportWebsite, description: storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim() };
|
||||
default:
|
||||
const unknown: never = exportFormat;
|
||||
throw new Error(`Unknown export format "${unknown}"`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const storyHeader =
|
||||
`${story.data.title}\n` +
|
||||
`${t(
|
||||
lang,
|
||||
"story/authors",
|
||||
authorsList.map((author) => getUsernameForLang(author, lang)),
|
||||
)}\n` +
|
||||
(requestersList
|
||||
? `${t(
|
||||
lang,
|
||||
"story/requested_by",
|
||||
requestersList.map((requester) => getUsernameForLang(requester, lang)),
|
||||
)}\n`
|
||||
: "") +
|
||||
(commissionersList
|
||||
? `${t(
|
||||
lang,
|
||||
"story/commissioned_by",
|
||||
commissionersList.map((commissioner) => getUsernameForLang(commissioner, lang)),
|
||||
)}\n`
|
||||
: "");
|
||||
const storyHeader =
|
||||
`${story.data.title}\n` +
|
||||
`${t(
|
||||
lang,
|
||||
"story/authors",
|
||||
authorsList.map((author) => getUsernameForLang(author, lang)),
|
||||
)}\n` +
|
||||
(requestersList
|
||||
? `${t(
|
||||
lang,
|
||||
"story/requested_by",
|
||||
requestersList.map((requester) => getUsernameForLang(requester, lang)),
|
||||
)}\n`
|
||||
: "") +
|
||||
(commissionersList
|
||||
? `${t(
|
||||
lang,
|
||||
"story/commissioned_by",
|
||||
commissionersList.map((commissioner) => getUsernameForLang(commissioner, lang)),
|
||||
)}\n`
|
||||
: "");
|
||||
|
||||
const storyText = `${storyHeader}\n===\n\n${story.body.replaceAll(/\\([=*])/g, "$1")}`
|
||||
.replaceAll(/\n\n\n+/g, "\n\n")
|
||||
.trim();
|
||||
const storyText = `${storyHeader}\n===\n\n${story.body.replaceAll(/\\([=*])/g, "$1")}`
|
||||
.replaceAll(/\n\n\n+/g, "\n\n")
|
||||
.trim();
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
story: storyText,
|
||||
description,
|
||||
thumbnail: story.data.thumbnail ? story.data.thumbnail.src : null,
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json; charset=utf-8" } },
|
||||
);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
story: storyText,
|
||||
description: description.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.exportWebsite] = item.description;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<PostWebsite, string>,
|
||||
),
|
||||
thumbnail: story.data.thumbnail ? story.data.thumbnail.src : null,
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json; charset=utf-8" } },
|
||||
);
|
||||
} catch (e) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message: (e as Error).message ?? null,
|
||||
stack: (e as Error).stack ?? null,
|
||||
}),
|
||||
{ status: 500, headers: { "Content-Type": "application/json; charset=utf-8" } },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import sanitizeHtml from "sanitize-html";
|
|||
import { t, type Lang } from "../i18n";
|
||||
import { markdownToPlaintext } from "../utils/markdown_to_plaintext";
|
||||
import { getUsernameForLang } from "../utils/get_username_for_lang";
|
||||
import { qualifyLocalURLsInMarkdown } from "../utils/qualify_local_urls_in_markdown";
|
||||
|
||||
type FeedItem = RSSFeedItem &
|
||||
Required<Pick<RSSFeedItem, "title" | "pubDate" | "link" | "description" | "categories" | "content">>;
|
||||
|
|
@ -28,6 +29,81 @@ const getLinkForUser = (user: CollectionEntry<"users">, lang: Lang) => {
|
|||
return userName;
|
||||
};
|
||||
|
||||
async function storyFeedItem(
|
||||
site: URL | undefined,
|
||||
data: EntryWithPubDate<"stories">["data"],
|
||||
slug: CollectionEntry<"stories">["slug"],
|
||||
body: string,
|
||||
): Promise<FeedItem> {
|
||||
return {
|
||||
title: `New story! "${data.title}"`,
|
||||
pubDate: toNoonUTCDate(data.pubDate),
|
||||
link: `/stories/${slug}`,
|
||||
description:
|
||||
`${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}\n\n${markdownToPlaintext(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`.replaceAll(
|
||||
/[\n ]+/g,
|
||||
" ",
|
||||
),
|
||||
categories: ["story"],
|
||||
content: sanitizeHtml(
|
||||
`<h1>${data.title}</h1>` +
|
||||
`<p>${t(
|
||||
data.lang,
|
||||
"story/authors",
|
||||
(await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)),
|
||||
)}</p>` +
|
||||
(data.requester
|
||||
? `<p>${t(
|
||||
data.lang,
|
||||
"story/requested_by",
|
||||
(await getEntries(data.requester)).map((requester) => getLinkForUser(requester, data.lang)),
|
||||
)}</p>`
|
||||
: "") +
|
||||
(data.commissioner
|
||||
? `<p>${t(
|
||||
data.lang,
|
||||
"story/commissioned_by",
|
||||
(await getEntries(data.commissioner)).map((commissioner) => getLinkForUser(commissioner, data.lang)),
|
||||
)}</p>`
|
||||
: "") +
|
||||
`<hr><p><em>${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}</em></p>` +
|
||||
`<hr>${await markdown(body)}` +
|
||||
`<hr>${await markdown(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function gameFeedItem(
|
||||
site: URL | undefined,
|
||||
data: EntryWithPubDate<"games">["data"],
|
||||
slug: CollectionEntry<"games">["slug"],
|
||||
body: string,
|
||||
): Promise<FeedItem> {
|
||||
return {
|
||||
title: `New game! "${data.title}"`,
|
||||
pubDate: toNoonUTCDate(data.pubDate),
|
||||
link: `/games/${slug}`,
|
||||
description:
|
||||
`${t(data.lang, "game/warnings", data.platforms, data.contentWarning)}\n\n${markdownToPlaintext(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`.replaceAll(
|
||||
/[\n ]+/g,
|
||||
" ",
|
||||
),
|
||||
categories: ["game"],
|
||||
content: sanitizeHtml(
|
||||
`<h1>${data.title}</h1>` +
|
||||
`<p>${t(
|
||||
data.lang,
|
||||
"story/authors",
|
||||
(await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)),
|
||||
)}</p>` +
|
||||
`<hr><p>${t(data.lang, "game/platforms", data.platforms)}</p>` +
|
||||
`<hr><p><em>${data.contentWarning}</em></p>` +
|
||||
`<hr>${await markdown(body)}` +
|
||||
`<hr>${await markdown(await qualifyLocalURLsInMarkdown(data.description, data.lang, site))}`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export const GET: APIRoute = async ({ site }) => {
|
||||
const stories = (
|
||||
(await getCollection(
|
||||
|
|
@ -47,75 +123,21 @@ export const GET: APIRoute = async ({ site }) => {
|
|||
title: "Gallery | Bad Manners",
|
||||
description: "Stories, games, and (possibly) more by Bad Manners",
|
||||
site: site!,
|
||||
items: [
|
||||
await Promise.all(
|
||||
stories.map<Promise<FeedItem>>(async ({ data, slug, body }) => ({
|
||||
title: `New story! "${data.title}"`,
|
||||
pubDate: toNoonUTCDate(data.pubDate),
|
||||
link: `/stories/${slug}`,
|
||||
description:
|
||||
`${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}\n\n${markdownToPlaintext(data.description)}`.replaceAll(
|
||||
/[\n ]+/g,
|
||||
" ",
|
||||
),
|
||||
categories: ["story"],
|
||||
content: sanitizeHtml(
|
||||
`<h1>${data.title}</h1>` +
|
||||
`<p>${t(
|
||||
data.lang,
|
||||
"story/authors",
|
||||
(await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)),
|
||||
)}</p>` +
|
||||
(data.requester
|
||||
? `<p>${t(
|
||||
data.lang,
|
||||
"story/requested_by",
|
||||
(await getEntries(data.requester)).map((requester) => getLinkForUser(requester, data.lang)),
|
||||
)}</p>`
|
||||
: "") +
|
||||
(data.commissioner
|
||||
? `<p>${t(
|
||||
data.lang,
|
||||
"story/commissioned_by",
|
||||
(await getEntries(data.commissioner)).map((commissioner) =>
|
||||
getLinkForUser(commissioner, data.lang),
|
||||
),
|
||||
)}</p>`
|
||||
: "") +
|
||||
`<hr><p><em>${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}</em></p>` +
|
||||
`<hr>${await markdown(body)}` +
|
||||
`<hr>${await markdown(data.description)}`,
|
||||
),
|
||||
items: await Promise.all(
|
||||
[
|
||||
stories.map(({ data, slug, body }) => ({
|
||||
date: data.pubDate,
|
||||
fn: () => storyFeedItem(site, data, slug, body),
|
||||
})),
|
||||
),
|
||||
await Promise.all(
|
||||
games.map<Promise<FeedItem>>(async ({ data, slug, body }) => ({
|
||||
title: `New game! "${data.title}"`,
|
||||
pubDate: toNoonUTCDate(data.pubDate),
|
||||
link: `/games/${slug}`,
|
||||
description:
|
||||
`${t(data.lang, "game/warnings", data.platforms, data.contentWarning)}\n\n${markdownToPlaintext(data.description)}`.replaceAll(
|
||||
/[\n ]+/g,
|
||||
" ",
|
||||
),
|
||||
categories: ["game"],
|
||||
content: sanitizeHtml(
|
||||
`<h1>${data.title}</h1>` +
|
||||
`<p>${t(
|
||||
data.lang,
|
||||
"story/authors",
|
||||
(await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)),
|
||||
)}</p>` +
|
||||
`<hr><p>${t(data.lang, "game/platforms", data.platforms)}</p>` +
|
||||
`<hr><p><em>${data.contentWarning}</em></p>` +
|
||||
`<hr>${await markdown(body)}` +
|
||||
`<hr>${await markdown(data.description)}`,
|
||||
),
|
||||
games.map(({ data, slug, body }) => ({
|
||||
date: data.pubDate,
|
||||
fn: () => gameFeedItem(site, data, slug, body),
|
||||
})),
|
||||
),
|
||||
]
|
||||
.flat()
|
||||
.sort((a, b) => b.pubDate.getTime() - a.pubDate.getTime())
|
||||
.slice(0, MAX_ITEMS),
|
||||
]
|
||||
.flat()
|
||||
.sort((a, b) => b.date.getTime() - a.date.getTime())
|
||||
.slice(0, MAX_ITEMS)
|
||||
.map((value) => value.fn()),
|
||||
),
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ const story = Astro.props;
|
|||
const readingTime = getReadingTime(story.body);
|
||||
if (story.data.wordCount && Math.abs(story.data.wordCount - readingTime.words) >= 150) {
|
||||
console.warn(
|
||||
`"wordCount" differs greatly from actual word count for published story ${story.data.title} ("${story.slug}") ` +
|
||||
`WARNING: "wordCount" differs greatly from actual word count for published story ` +
|
||||
`${story.data.title} ("${story.slug}") ` +
|
||||
`(expected ~${story.data.wordCount}, found ${readingTime.words})`,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
import type { GetStaticPaths, Page } from "astro";
|
||||
import { Image } from "astro:assets";
|
||||
import { getCollection } from "astro:content";
|
||||
import { getCollection, type CollectionEntry } from "astro:content";
|
||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import { t } from "../../i18n";
|
||||
|
||||
type StoryWithPubDate = CollectionEntry<"stories"> & { data: { pubDate: Date } };
|
||||
|
|
@ -51,7 +50,7 @@ const totalPages = Math.ceil(page.total / page.size);
|
|||
) : (
|
||||
<a
|
||||
class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500"
|
||||
href={page.url.current.replace(`/${page.currentPage}`, `/${p + 1}`)}
|
||||
href={page.url.current.replace(`/stories/${page.currentPage}`, `/stories/${p + 1}`)}
|
||||
>
|
||||
{p + 1}
|
||||
</a>
|
||||
|
|
@ -114,7 +113,7 @@ const totalPages = Math.ceil(page.total / page.size);
|
|||
) : (
|
||||
<a
|
||||
class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500"
|
||||
href={page.url.current.replace(`/${page.currentPage}`, `/${p + 1}`)}
|
||||
href={page.url.current.replace(`/stories/${page.currentPage}`, `/stories/${p + 1}`)}
|
||||
>
|
||||
{p + 1}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ
|
|||
<meta
|
||||
slot="head-description"
|
||||
property="og:description"
|
||||
content="Bad Manners || The story of Quince, Nikili, and Suu."
|
||||
content="The Lost of the Marshes || The story of Quince, Nikili, and Suu."
|
||||
/>
|
||||
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">{series.data.name}</h1>
|
||||
<p class="my-4">This is the main hub for the story of Quince, Nikili, and Suu, as well as all bonus content.</p>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const categorizedTags = tagCategories
|
|||
|
||||
if (uncategorizedTagsSet.size > 0) {
|
||||
const tagList = [...uncategorizedTagsSet];
|
||||
console.warn("The following tags have no category:", tagList);
|
||||
console.warn("WARNING: The following tags have no category:", tagList);
|
||||
// categorizedTags.push(["Uncategorized tags", "others", tagList]);
|
||||
}
|
||||
---
|
||||
|
|
@ -83,7 +83,7 @@ if (uncategorizedTagsSet.size > 0) {
|
|||
{
|
||||
seriesCollection.map((series) => (
|
||||
<li>
|
||||
<a class="text-link underline" href={series.data.url}>
|
||||
<a class="text-link underline" href={series.data.link}>
|
||||
{series.data.name}
|
||||
</a>
|
||||
</li>
|
||||
|
|
@ -98,10 +98,14 @@ if (uncategorizedTagsSet.size > 0) {
|
|||
<h2 id={`category-${categoryId}`} class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
{category}
|
||||
</h2>
|
||||
<ul class="flex max-w-3xl flex-wrap gap-x-2 gap-y-2 px-2">
|
||||
<ul class="flex max-w-3xl flex-wrap gap-x-2 gap-y-3 px-3">
|
||||
{tagList.map(({ id, name, description }) => (
|
||||
<li class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm dark:bg-bm-600 dark:text-white">
|
||||
<a class="hover:underline focus:underline" href={`/tags/${id}`} title={description}>
|
||||
<li>
|
||||
<a
|
||||
class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm hover:underline focus:underline dark:bg-bm-600 dark:text-white"
|
||||
href={`/tags/${id}`}
|
||||
title={description}
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { Markdown } from "@astropub/md";
|
|||
import { slug } from "github-slugger";
|
||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||
import Prose from "../../components/Prose.astro";
|
||||
import { t } from "../../i18n";
|
||||
import { DEFAULT_LANG } from "../../i18n";
|
||||
import { t, DEFAULT_LANG } from "../../i18n";
|
||||
import { qualifyLocalURLsInMarkdown } from "../../utils/qualify_local_urls_in_markdown";
|
||||
|
||||
type EntryWithPubDate<C extends CollectionKey> = CollectionEntry<C> & { data: { pubDate: Date } };
|
||||
|
||||
|
|
@ -47,11 +47,11 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||
category.data.tags.forEach(({ name, description, related }) => {
|
||||
related = related.filter((relatedTag) => {
|
||||
if (relatedTag == name) {
|
||||
console.warn(`Tag "${name}" should not have itself as a related tag; removing`);
|
||||
console.warn(`WARNING: Tag "${name}" should not have itself as a related tag; removing...`);
|
||||
return false;
|
||||
}
|
||||
if (!tags.has(relatedTag)) {
|
||||
console.warn(`Tag "${name}" has an unknown related tag "${relatedTag}"; removing`);
|
||||
console.warn(`WARNING: Tag "${name}" has an unknown related tag "${relatedTag}"; removing...`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -88,23 +88,34 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||
}));
|
||||
};
|
||||
|
||||
const { tag, description, stories, games, related } = Astro.props;
|
||||
const { props } = Astro;
|
||||
const description = props.description && (await qualifyLocalURLsInMarkdown(props.description, DEFAULT_LANG));
|
||||
if (!description) {
|
||||
console.warn(`Tag "${tag}" has no description`);
|
||||
console.warn(`WARNING: Tag "${props.tag}" has no description`);
|
||||
}
|
||||
const totalWorksWithTag = t(DEFAULT_LANG, "tag/total_works_with_tag", tag, stories.length, games.length);
|
||||
const totalWorksWithTag = t(
|
||||
DEFAULT_LANG,
|
||||
"tag/total_works_with_tag",
|
||||
props.tag,
|
||||
props.stories.length,
|
||||
props.games.length,
|
||||
);
|
||||
---
|
||||
|
||||
<GalleryLayout pageTitle={`Works tagged "${tag}"`}>
|
||||
<meta slot="head-description" content={`Bad Manners || ${totalWorksWithTag || tag}`} property="og:description" />
|
||||
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{tag}"</h1>
|
||||
<GalleryLayout pageTitle={`Works tagged "${props.tag}"`}>
|
||||
<meta
|
||||
slot="head-description"
|
||||
content={`Bad Manners || ${totalWorksWithTag || props.tag}`}
|
||||
property="og:description"
|
||||
/>
|
||||
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{props.tag}"</h1>
|
||||
<div class="my-4">
|
||||
<Prose>
|
||||
{description ? <Markdown of={description} /> : null}
|
||||
{
|
||||
related?.length ? (
|
||||
props.related?.length ? (
|
||||
<p
|
||||
set:html={`See also: ${related.map((relatedTag) => `<a href="/tags/${slug(relatedTag)}">${relatedTag}</a>`).join(", ")}.`}
|
||||
set:html={`See also: ${props.related.map((relatedTag) => `<a href="/tags/${slug(relatedTag)}">${relatedTag}</a>`).join(", ")}.`}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
|
@ -112,13 +123,13 @@ const totalWorksWithTag = t(DEFAULT_LANG, "tag/total_works_with_tag", tag, stori
|
|||
</Prose>
|
||||
</div>
|
||||
{
|
||||
stories.length > 0 && (
|
||||
props.stories.length > 0 && (
|
||||
<section class="my-2" aria-labelledby="content-stories">
|
||||
<h2 id="content-stories" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
Stories
|
||||
</h2>
|
||||
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
||||
{stories.map((story) => (
|
||||
{props.stories.map((story) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a
|
||||
class="text-link hover:underline focus:underline"
|
||||
|
|
@ -135,7 +146,17 @@ const totalWorksWithTag = t(DEFAULT_LANG, "tag/total_works_with_tag", tag, stori
|
|||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div class="max-w-48 text-sm">{story.data.title}</div>
|
||||
<div class="max-w-[192px] text-sm">
|
||||
<span>{story.data.title}</span>
|
||||
<br />
|
||||
<span class="italic">
|
||||
{story.data.pubDate.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -144,13 +165,13 @@ const totalWorksWithTag = t(DEFAULT_LANG, "tag/total_works_with_tag", tag, stori
|
|||
)
|
||||
}
|
||||
{
|
||||
games.length > 0 && (
|
||||
props.games.length > 0 && (
|
||||
<section class="my-2" aria-labelledby="content-games">
|
||||
<h2 id="content-games" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">
|
||||
Games
|
||||
</h2>
|
||||
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
||||
{games.map((game) => (
|
||||
{props.games.map((game) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a
|
||||
class="text-link hover:underline focus:underline"
|
||||
|
|
@ -167,7 +188,13 @@ const totalWorksWithTag = t(DEFAULT_LANG, "tag/total_works_with_tag", tag, stori
|
|||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div class="max-w-48 text-sm">{game.data.title}</div>
|
||||
<div class="max-w-[192px] text-sm">
|
||||
<span>{game.data.title}</span>
|
||||
<br />
|
||||
<span class="italic">
|
||||
{game.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue