Better microformats support and add PUBLISH_DRAFTS envvar

This commit is contained in:
Bad Manners 2024-08-16 21:46:32 -03:00
parent 132b2b69f3
commit a335aff2d3
31 changed files with 269 additions and 153 deletions

View file

@ -1 +1,3 @@
src/components/DarkModeScript.astro src/components/DarkModeScript.astro
.astro/
dist/

View file

@ -28,11 +28,12 @@ npm run prettier # Prettier formatting
### Configuration ### Configuration
The following optional environment variables can be set with `.env`: The following optional environment variables can be set within a `.env` file:
| Name | Type | Description | | Name | Type | Description |
|-|-|-| | ---------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `APACHE_CONFIG` | boolean | Whether to generate an `.htaccess` Apache config file at the root of the output directory or not. | | `APACHE_CONFIG` | boolean | If set to true, generates an `.htaccess` Apache config file at the root of the output directory. |
| `PUBLISH_DRAFTS` | boolean | If set to true, includes drafts in the production build. Published drafts still won't be directly indexed by any other pages. |
### Export story for upload ### Export story for upload

View file

@ -27,6 +27,7 @@ export default defineConfig({
env: { env: {
schema: { schema: {
APACHE_CONFIG: envField.boolean({ context: "server", access: "public", default: false }), APACHE_CONFIG: envField.boolean({ context: "server", access: "public", default: false }),
PUBLISH_DRAFTS: envField.boolean({ context: "server", access: "public", default: false }),
}, },
}, },
}, },

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "gallery.badmanners.xyz", "name": "gallery.badmanners.xyz",
"version": "1.7.4", "version": "1.7.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "gallery.badmanners.xyz", "name": "gallery.badmanners.xyz",
"version": "1.7.4", "version": "1.7.5",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.2", "@astrojs/check": "^0.9.2",

View file

@ -1,7 +1,7 @@
{ {
"name": "gallery.badmanners.xyz", "name": "gallery.badmanners.xyz",
"type": "module", "type": "module",
"version": "1.7.4", "version": "1.7.5",
"scripts": { "scripts": {
"postinstall": "astro sync", "postinstall": "astro sync",
"dev": "astro dev", "dev": "astro dev",

View file

@ -2,10 +2,10 @@ The source code of this website is licensed under the MIT License: https://opens
The stories and games hosted on the website are copyrighted by me and licensed under CC-BY-NC-ND-4.0: https://creativecommons.org/licenses/by-nc-nd/4.0/ The stories and games hosted on the website are copyrighted by me and licensed under CC-BY-NC-ND-4.0: https://creativecommons.org/licenses/by-nc-nd/4.0/
The briefcase logo and any unattributed characters are copyrighted and trademarked by me. The briefcase logo, my characters, and any unattributed characters are copyrighted and trademarked by me, Bad Manners.
The Noto Sans and Noto Serif typefaces are copyrighted to the Noto Project Authors and distributed under the SIL Open Font License v1.1: https://opensource.org/license/ofl-1-1 The Noto Sans and Noto Serif typefaces are copyrighted to the Noto Project Authors and distributed under the SIL Open Font License v1.1: https://opensource.org/license/ofl-1-1
The generic SVG icons were created by Font Awesome and are distributed under CC-BY-4.0: https://creativecommons.org/licenses/by/4.0/ The generic SVG icons were created by Font Awesome and are distributed under CC-BY-4.0: https://creativecommons.org/licenses/by/4.0/
All third-party trademarks and attributed characters belong to their respective owners, and I'm not affiliated with any of them. All third-party trademarks and attributed characters belong to their respective owners, and I (Bad Manners) am not affiliated with any of them.

View file

@ -12,7 +12,7 @@
} }
document.querySelectorAll<HTMLElementTagNameMap["button"]>("button[data-dark-mode]").forEach((button) => { document.querySelectorAll<HTMLElementTagNameMap["button"]>("button[data-dark-mode]").forEach((button) => {
button.classList.remove("hidden"); button.classList.remove("hidden");
button.removeAttribute("aria-hidden"); button.setAttribute("aria-hidden", "false");
button.addEventListener("click", (e) => { button.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
if (colorScheme === "dark") { if (colorScheme === "dark") {

View file

@ -26,7 +26,7 @@ const { link, instance, user, postId } = Astro.props;
</h2> </h2>
<div class="text-stone-800 dark:text-stone-100" id="comments"> <div class="text-stone-800 dark:text-stone-100" id="comments">
<p class="my-1"> <p class="my-1">
<a class="text-link underline" href={link} target="_blank">View comments on Mastodon</a> <a class="text-link underline" href={link} target="_blank">View comments on Mastodon</a>.
</p> </p>
</div> </div>
</section> </section>
@ -46,7 +46,7 @@ const { link, instance, user, postId } = Astro.props;
class="-mt-1 mr-1 animate-spin" class="-mt-1 mr-1 animate-spin"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
aria-hidden aria-hidden="true"
> >
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path <path
@ -59,25 +59,32 @@ const { link, instance, user, postId } = Astro.props;
</template> </template>
<template id="template-comment-box"> <template id="template-comment-box">
<div class="my-2 rounded-md border-2 border-stone-400 bg-stone-200 p-2 dark:border-stone-600 dark:bg-stone-800"> <div
role="article"
class="p-comment h-entry my-2 rounded-md border-2 border-stone-400 bg-stone-100 p-2 dark:border-stone-600 dark:bg-stone-800"
>
<div class="ml-1"> <div class="ml-1">
<a data-author class="text-link flex items-center text-lg hover:underline focus:underline" target="_blank"> <a
<img data-avatar class="mr-2 w-10 rounded-full border border-stone-400 dark:border-stone-600" /> data-author
<span data-display-name></span> class="p-author h-card u-url text-link flex items-center text-lg hover:underline focus:underline"
target="_blank"
>
<img data-avatar class="u-photo mr-2 w-10 rounded-full border border-stone-400 dark:border-stone-600" />
<span data-display-name class="p-nickname"></span>
</a> </a>
<a <a
data-post-link data-post-link
class="text-link my-1 flex items-center text-sm font-light hover:underline focus:underline" class="u-url text-link my-1 flex items-center text-sm font-light hover:underline focus:underline"
target="_blank" target="_blank"
> >
<span class="mr-1" data-publish-date aria-label="Publish date"></span> <time class="dt-published mr-1" data-publish-date aria-label="Publish date"></time>
</a> </a>
</div> </div>
<div data-content class="prose-a:text-link prose-story prose my-1 dark:prose-invert prose-img:my-0"></div> <div data-content class="e-content prose-a:text-link prose-story prose my-1 dark:prose-invert prose-img:my-0"></div>
<div class="ml-1 flex flex-row pb-2 pt-1"> <div class="ml-1 flex flex-row pb-2 pt-1">
<div class="flex" aria-label="Favorites"> <div class="flex" aria-label="Favorites">
<span data-favorites></span> <span data-favorites></span>
<svg style={{ width: "1.25rem", fill: "currentColor" }} class="ml-2" viewBox="0 0 576 512" aria-hidden> <svg style={{ width: "1.25rem", fill: "currentColor" }} class="ml-2" viewBox="0 0 576 512" aria-hidden="true">
<path <path
d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z" d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"
></path> ></path>
@ -85,14 +92,14 @@ const { link, instance, user, postId } = Astro.props;
</div> </div>
<div class="ml-4 flex" aria-label="Reblogs"> <div class="ml-4 flex" aria-label="Reblogs">
<span data-reblogs></span> <span data-reblogs></span>
<svg style={{ width: "1.25rem", fill: "currentColor" }} class="ml-2" viewBox="0 0 512 512" aria-hidden> <svg style={{ width: "1.25rem", fill: "currentColor" }} class="ml-2" viewBox="0 0 512 512" aria-hidden="true">
<path <path
d="M0 224c0 17.7 14.3 32 32 32s32-14.3 32-32c0-53 43-96 96-96H320v32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S320 19.1 320 32V64H160C71.6 64 0 135.6 0 224zm512 64c0-17.7-14.3-32-32-32s-32 14.3-32 32c0 53-43 96-96 96H192V352c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V448H352c88.4 0 160-71.6 160-160z" d="M0 224c0 17.7 14.3 32 32 32s32-14.3 32-32c0-53 43-96 96-96H320v32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S320 19.1 320 32V64H160C71.6 64 0 135.6 0 224zm512 64c0-17.7-14.3-32-32-32s-32 14.3-32 32c0 53-43 96-96 96H192V352c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V448H352c88.4 0 160-71.6 160-160z"
></path> ></path>
</svg> </svg>
</div> </div>
</div> </div>
<div data-comment-thread class="-mb-2" aria-hidden></div> <div data-comment-thread class="-mb-2" aria-hidden="true"></div>
</div> </div>
</template> </template>
@ -164,19 +171,21 @@ const { link, instance, user, postId } = Astro.props;
)!; )!;
data.descendants.forEach((comment) => { data.descendants.forEach((comment) => {
const commentBox = commentTemplate.content.cloneNode(true) as HTMLDivElement; const commentBox = commentTemplate.content.cloneNode(true) as HTMLDivElement;
commentBox.id = `comment-${comment.id}`;
const commentBoxAuthor = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-author]")!; const commentBoxAuthor = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-author]")!;
commentBoxAuthor.href = comment.account.url; commentBoxAuthor.href = comment.account.url;
const avatar = commentBoxAuthor.querySelector<HTMLElementTagNameMap["img"]>("img[data-avatar]")!; const avatar = commentBoxAuthor.querySelector<HTMLElementTagNameMap["img"]>("img[data-avatar]")!;
avatar.src = comment.account.avatar; avatar.src = comment.account.avatar;
avatar.alt = `Profile picture of ${comment.account.username}`; avatar.alt = `Avatar of ${comment.account.username}`;
const displayName = commentBoxAuthor.querySelector<HTMLElementTagNameMap["span"]>("span[data-display-name]")!; const displayName = commentBoxAuthor.querySelector<HTMLElementTagNameMap["span"]>("span[data-display-name]")!;
displayName.innerHTML = replaceEmojis(comment.account.display_name, comment.account.emojis); displayName.innerHTML = replaceEmojis(comment.account.display_name, comment.account.emojis);
const commentBoxPostLink = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-post-link]")!; const commentBoxPostLink = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-post-link]")!;
commentBoxPostLink.href = comment.url; commentBoxPostLink.href = comment.url;
const publishDate = const publishDate =
commentBoxPostLink.querySelector<HTMLElementTagNameMap["span"]>("span[data-publish-date]")!; commentBoxPostLink.querySelector<HTMLElementTagNameMap["time"]>("time[data-publish-date]")!;
publishDate.setAttribute("datetime", comment.created_at);
publishDate.innerText = new Date(Date.parse(comment.created_at)).toLocaleString("en-US", { publishDate.innerText = new Date(Date.parse(comment.created_at)).toLocaleString("en-US", {
month: "short", month: "short",
day: "numeric", day: "numeric",
@ -186,8 +195,10 @@ const { link, instance, user, postId } = Astro.props;
}); });
if (comment.edited_at) { if (comment.edited_at) {
const edited = document.createElement("span"); const edited = document.createElement("time");
edited.className = "italic"; edited.className = "dt-updated italic";
edited.setAttribute("datetime", comment.edited_at);
edited.setAttribute("title", comment.edited_at);
edited.innerText = "(edited)"; edited.innerText = "(edited)";
commentBoxPostLink.appendChild(edited); commentBoxPostLink.appendChild(edited);
} }
@ -209,7 +220,7 @@ const { link, instance, user, postId } = Astro.props;
commentMap[comment.id] = commentsIndex; commentMap[comment.id] = commentsIndex;
const parentThreadDiv = const parentThreadDiv =
commentsList[commentsIndex].querySelector<HTMLElementTagNameMap["div"]>("div[data-comment-thread]")!; commentsList[commentsIndex].querySelector<HTMLElementTagNameMap["div"]>("div[data-comment-thread]")!;
parentThreadDiv.removeAttribute("aria-hidden"); parentThreadDiv.setAttribute("aria-hidden", "false");
parentThreadDiv.setAttribute("aria-label", "Replies"); parentThreadDiv.setAttribute("aria-label", "Replies");
parentThreadDiv.appendChild(commentBox); parentThreadDiv.appendChild(commentBox);
} }

View file

@ -7,19 +7,20 @@ type Props = {
lang: Lang; lang: Lang;
user: CollectionEntry<"users">; user: CollectionEntry<"users">;
class?: string; class?: string;
rel?: string;
}; };
const { user, lang, class: className } = Astro.props; const { user, lang, class: className, rel } = Astro.props;
const username = getUsernameForLang(user, lang); const username = getUsernameForLang(user, lang);
const link = user.data.preferredLink ? user.data.links[user.data.preferredLink]!.link : null; const link = user.data.preferredLink ? user.data.links[user.data.preferredLink]!.link : null;
--- ---
{ {
link ? ( link ? (
<a href={link} class:list={["h-card u-url text-link underline", className]} target="_blank"> <a rel={rel} href={link} class:list={[className, "h-card u-url text-link underline"]} target="_blank">
{username} {username}
</a> </a>
) : ( ) : (
<span class:list={["h-card", className]}>{username}</span> <span class:list={[className, "h-card"]}>{username}</span>
) )
} }

View file

@ -235,8 +235,8 @@ const storiesCollection = defineCollection({
thumbnail: image().optional(), thumbnail: image().optional(),
// Optional parameters // Optional parameters
shortTitle: z.string().optional(), shortTitle: z.string().optional(),
commissioner: userList.optional(), commissioners: userList.optional(),
requester: userList.optional(), requesters: userList.optional(),
summary: z.string().trim().optional(), summary: z.string().trim().optional(),
thumbnailWidth: z.number().int().optional(), thumbnailWidth: z.number().int().optional(),
thumbnailHeight: z.number().int().optional(), thumbnailHeight: z.number().int().optional(),

View file

@ -35,7 +35,7 @@ tags:
- hyper - hyper
- netorare - netorare
- commission - commission
commissioner: scion commissioners: scion
copyrightedCharacters: copyrightedCharacters:
"": scion "": scion
--- ---

View file

@ -34,7 +34,7 @@ tags:
- hyper - hyper
- netorare - netorare
- commission - commission
commissioner: scion commissioners: scion
copyrightedCharacters: copyrightedCharacters:
"": scion "": scion
--- ---

View file

@ -28,7 +28,7 @@ tags:
- size difference - size difference
- implied perma endo - implied perma endo
- request - request
requester: dee-lumeni requesters: dee-lumeni
copyrightedCharacters: copyrightedCharacters:
Kuronosuke: dee-lumeni Kuronosuke: dee-lumeni
--- ---

View file

@ -37,7 +37,7 @@ tags:
- plushie - plushie
- wardrobe malfunction - wardrobe malfunction
- commission - commission
commissioner: dee-lumeni commissioners: dee-lumeni
copyrightedCharacters: copyrightedCharacters:
Rose: dee-lumeni Rose: dee-lumeni
--- ---

View file

@ -34,7 +34,7 @@ tags:
- gay sex - gay sex
- netorare - netorare
- commission - commission
commissioner: yolkmonkey commissioners: yolkmonkey
copyrightedCharacters: copyrightedCharacters:
Yolk: yolkmonkey Yolk: yolkmonkey
prev: team-effort prev: team-effort

View file

@ -32,7 +32,7 @@ tags:
- inflation - inflation
- gay sex - gay sex
- request - request
requester: yolkmonkey requesters: yolkmonkey
copyrightedCharacters: copyrightedCharacters:
Yolk: yolkmonkey Yolk: yolkmonkey
next: team-building next: team-building

View file

@ -28,7 +28,7 @@ tags:
- same size - same size
- long-term endo - long-term endo
- request - request
requester: avour-inden requesters: avour-inden
copyrightedCharacters: copyrightedCharacters:
Avour: avour-inden Avour: avour-inden
Buster: holi Buster: holi

View file

@ -40,7 +40,7 @@ tags:
- lesbian sex - lesbian sex
- orgy - orgy
- commission - commission
commissioner: asof-yeun commissioners: asof-yeun
copyrightedCharacters: copyrightedCharacters:
Ushitora: asof-yeun Ushitora: asof-yeun
--- ---

View file

@ -40,12 +40,26 @@ const currentYear = new Date().getFullYear().toString();
<span class="my-2 text-2xl font-semibold">Bad Manners</span> <span class="my-2 text-2xl font-semibold">Bad Manners</span>
<Navigation /> <Navigation />
<div class="pt-4 text-center text-xs text-black dark:text-white"> <div class="pt-4 text-center text-xs text-black dark:text-white">
<span>&copy; {currentYear == "2024" ? <time datetime="2024">2024</time> : <><time datetime="2024">2024</time>&ndash;<time datetime={currentYear}>{currentYear}</time></>} | </span> <span
>&copy; {
currentYear == "2024" ? (
<time datetime="2024">2024</time>
) : (
<>
<time datetime="2024">2024</time>&ndash;<time datetime={currentYear}>{currentYear}</time>
</>
)
} |
</span>
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank">Licenses</a> <a class="hover:underline focus:underline" href="/licenses.txt" target="_blank">Licenses</a>
</div> </div>
<div class="mt-2 flex items-center gap-x-1 pb-10"> <div class="mt-2 flex items-center gap-x-1 pb-10">
<a class="text-link p-1" href="https://badmanners.xyz/" target="_blank" aria-labelled-by="label-main-website"> <a class="text-link p-1" href="https://badmanners.xyz/" target="_blank" aria-labelledby="label-main-website">
<svg style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }} viewBox="0 0 576 512" aria-hidden> <svg
style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }}
viewBox="0 0 576 512"
aria-hidden="true"
>
<path <path
d="M575.8 255.5c0 18-15 32.1-32 32.1h-32l.7 160.2c0 2.7-.2 5.4-.5 8.1V472c0 22.1-17.9 40-40 40H456c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1H416 392c-22.1 0-40-17.9-40-40V448 384c0-17.7-14.3-32-32-32H256c-17.7 0-32 14.3-32 32v64 24c0 22.1-17.9 40-40 40H160 128.1c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2H104c-22.1 0-40-17.9-40-40V360c0-.9 0-1.9 .1-2.8V287.6H32c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z" d="M575.8 255.5c0 18-15 32.1-32 32.1h-32l.7 160.2c0 2.7-.2 5.4-.5 8.1V472c0 22.1-17.9 40-40 40H456c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1H416 392c-22.1 0-40-17.9-40-40V448 384c0-17.7-14.3-32-32-32H256c-17.7 0-32 14.3-32 32v64 24c0 22.1-17.9 40-40 40H160 128.1c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2H104c-22.1 0-40-17.9-40-40V360c0-.9 0-1.9 .1-2.8V287.6H32c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z"
></path> ></path>
@ -53,19 +67,23 @@ const currentYear = new Date().getFullYear().toString();
<span id="label-main-website" class="hidden">Main website</span> <span id="label-main-website" class="hidden">Main website</span>
</a> </a>
<a class="text-link p-1" href="/feed.xml" target="_blank" aria-labelledby="label-rss-feed"> <a class="text-link p-1" href="/feed.xml" target="_blank" aria-labelledby="label-rss-feed">
<svg style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }} viewBox="0 0 448 512" aria-hidden> <svg
style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }}
viewBox="0 0 448 512"
aria-hidden="true"
>
<path <path
d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z" d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"
></path> ></path>
</svg> </svg>
<span id="label-rss-feed" class="hidden">RSS feed</span> <span id="label-rss-feed" class="hidden">RSS feed</span>
</a> </a>
<button data-dark-mode class="text-link hidden p-1" aria-labelledby="label-toggle-dark-mode" aria-hidden> <button data-dark-mode class="text-link hidden p-1" aria-labelledby="label-toggle-dark-mode" aria-hidden="true">
<svg <svg
style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }} style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }}
viewBox="0 0 512 512" viewBox="0 0 512 512"
class="hidden dark:block" class="hidden dark:block"
aria-hidden aria-hidden="true"
> >
<path <path
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z" d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
@ -75,7 +93,7 @@ const currentYear = new Date().getFullYear().toString();
style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }} style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }}
viewBox="0 0 512 512" viewBox="0 0 512 512"
class="block dark:hidden" class="block dark:hidden"
aria-hidden aria-hidden="true"
> >
<path <path
d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z" d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"

View file

@ -54,7 +54,7 @@ const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !ga
/> />
<Fragment slot="section-information"> <Fragment slot="section-information">
<Authors lang={props.lang}> <Authors lang={props.lang}>
{authorsList.map((author) => <UserComponent class="p-author" user={author} lang={props.lang} />)} {authorsList.map((author) => <UserComponent rel="author" class="p-author" user={author} lang={props.lang} />)}
</Authors> </Authors>
<div id="platforms"> <div id="platforms">
<p>{t(props.lang, "game/platforms", props.platforms)}</p> <p>{t(props.lang, "game/platforms", props.platforms)}</p>

View file

@ -115,9 +115,13 @@ const thumbnail =
<a <a
href={series ? series.data.link : props.labelReturnTo.link} href={series ? series.data.link : props.labelReturnTo.link}
class="text-link my-1 p-2" class="text-link my-1 p-2"
aria-labelled-by="label-return-to" aria-labelledby="label-return-to"
> >
<svg style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }} viewBox="0 0 512 512" aria-hidden> <svg
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
viewBox="0 0 512 512"
aria-hidden="true"
>
<path <path
d="M48.5 224H40c-13.3 0-24-10.7-24-24V72c0-9.7 5.8-18.5 14.8-22.2s19.3-1.7 26.2 5.2L98.6 96.6c87.6-86.5 228.7-86.2 315.8 1c87.5 87.5 87.5 229.3 0 316.8s-229.3 87.5-316.8 0c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0c62.5 62.5 163.8 62.5 226.3 0s62.5-163.8 0-226.3c-62.2-62.2-162.7-62.5-225.3-1L185 183c6.9 6.9 8.9 17.2 5.2 26.2s-12.5 14.8-22.2 14.8H48.5z" d="M48.5 224H40c-13.3 0-24-10.7-24-24V72c0-9.7 5.8-18.5 14.8-22.2s19.3-1.7 26.2 5.2L98.6 96.6c87.6-86.5 228.7-86.2 315.8 1c87.5 87.5 87.5 229.3 0 316.8s-229.3 87.5-316.8 0c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0c62.5 62.5 163.8 62.5 226.3 0s62.5-163.8 0-226.3c-62.2-62.2-162.7-62.5-225.3-1L185 183c6.9 6.9 8.9 17.2 5.2 26.2s-12.5 14.8-22.2 14.8H48.5z"
></path> ></path>
@ -131,9 +135,13 @@ const thumbnail =
<a <a
href="#description" href="#description"
class="text-link my-1 border-l border-stone-300 p-2 dark:border-stone-700" class="text-link my-1 border-l border-stone-300 p-2 dark:border-stone-700"
aria-labelled-by="label-go-to-description" aria-labelledby="label-go-to-description"
> >
<svg style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }} viewBox="0 0 512 512" aria-hidden> <svg
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
viewBox="0 0 512 512"
aria-hidden="true"
>
<path <path
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
></path> ></path>
@ -144,14 +152,14 @@ const thumbnail =
<button <button
data-dark-mode data-dark-mode
class="text-link my-1 hidden border-l border-stone-300 p-2 dark:border-stone-700" class="text-link my-1 hidden border-l border-stone-300 p-2 dark:border-stone-700"
aria-labelled-by="label-toggle-dark-mode" aria-labelledby="label-toggle-dark-mode"
aria-hidden aria-hidden="true"
> >
<svg <svg
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }} style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
viewBox="0 0 512 512" viewBox="0 0 512 512"
class="hidden dark:block" class="hidden dark:block"
aria-hidden aria-hidden="true"
> >
<path <path
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z" d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
@ -161,7 +169,7 @@ const thumbnail =
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }} style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
viewBox="0 0 512 512" viewBox="0 0 512 512"
class="block dark:hidden" class="block dark:hidden"
aria-hidden aria-hidden="true"
> >
<path <path
d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z" d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"
@ -190,14 +198,14 @@ const thumbnail =
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }} style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
class="mr-1" class="mr-1"
viewBox="0 0 320 512" viewBox="0 0 320 512"
aria-hidden aria-hidden="true"
> >
<path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" /> <path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" />
</svg> </svg>
<span>{props.prev.title}</span> <span>{props.prev.title}</span>
</a> </a>
) : ( ) : (
<div class="h-full border-r border-stone-400 dark:border-stone-600" aria-hidden /> <div class="h-full border-r border-stone-400 dark:border-stone-600" aria-hidden="true" />
)} )}
{props.next ? ( {props.next ? (
<a <a
@ -210,13 +218,13 @@ const thumbnail =
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }} style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
class="ml-1" class="ml-1"
viewBox="0 0 320 512" viewBox="0 0 320 512"
aria-hidden aria-hidden="true"
> >
<path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z" /> <path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z" />
</svg> </svg>
</a> </a>
) : ( ) : (
<div aria-hidden /> <div aria-hidden="true" />
)} )}
</div> </div>
<hr class="mx-auto mb-5 w-full max-w-2xl border-stone-400 dark:border-stone-600" /> <hr class="mx-auto mb-5 w-full max-w-2xl border-stone-400 dark:border-stone-600" />
@ -277,7 +285,7 @@ const thumbnail =
<time <time
id="publish-date" id="publish-date"
datetime={props.pubDate.toISOString().slice(0, 10)} datetime={props.pubDate.toISOString().slice(0, 10)}
class="dt-published block mt-2 px-2 text-center font-serif font-light text-stone-600 dark:text-stone-200" class="dt-published mt-2 block px-2 text-center font-serif font-light text-stone-600 dark:text-stone-200"
aria-label={t(props.lang, "published_content/publish_date_aria_label")} aria-label={t(props.lang, "published_content/publish_date_aria_label")}
aria-description={ aria-description={
t(props.lang, "published_content/publish_date_aria_description", props.pubDate) || undefined t(props.lang, "published_content/publish_date_aria_description", props.pubDate) || undefined
@ -323,7 +331,7 @@ const thumbnail =
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }} style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
class="mr-1" class="mr-1"
viewBox="0 0 384 512" viewBox="0 0 384 512"
aria-hidden aria-hidden="true"
><path ><path
d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z" d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
></path></svg ></path></svg
@ -345,7 +353,7 @@ const thumbnail =
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }} style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
class="mr-1" class="mr-1"
viewBox="0 0 320 512" viewBox="0 0 320 512"
aria-hidden aria-hidden="true"
> >
<path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" /> <path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" />
</svg> </svg>
@ -365,7 +373,7 @@ const thumbnail =
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }} style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
class="ml-1" class="ml-1"
viewBox="0 0 320 512" viewBox="0 0 320 512"
aria-hidden aria-hidden="true"
> >
<path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z" /> <path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z" />
</svg> </svg>
@ -441,7 +449,9 @@ const thumbnail =
class="pt-6 text-center text-xs text-black dark:text-white" class="pt-6 text-center text-xs text-black dark:text-white"
aria-label={t(props.lang, "published_content/copyright_aria_label")} aria-label={t(props.lang, "published_content/copyright_aria_label")}
> >
<span set:html={t(props.lang, "published_content/copyright_year", (props.pubDate || new Date()).getFullYear())}></span><span>&nbsp;|</span> <span
set:html={t(props.lang, "published_content/copyright_year", (props.pubDate || new Date()).getFullYear())}
/><span>&nbsp;|</span>
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank" <a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
>{t(props.lang, "published_content/licenses")}</a >{t(props.lang, "published_content/licenses")}</a
> >

View file

@ -15,8 +15,8 @@ const prev = props.prev && (await getEntry(props.prev));
const next = props.next && (await getEntry(props.next)); const next = props.next && (await getEntry(props.next));
const series = props.series && (await getEntry(props.series)); const series = props.series && (await getEntry(props.series));
const authorsList = await getEntries(props.authors); const authorsList = await getEntries(props.authors);
const commissionersList = props.commissioner && (await getEntries(props.commissioner)); const commissionersList = props.commissioners && (await getEntries(props.commissioners));
const requestersList = props.requester && (await getEntries(props.requester)); const requestersList = props.requesters && (await getEntries(props.requesters));
const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft); const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft); const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
const wordCount = props.wordCount?.toString(); const wordCount = props.wordCount?.toString();
@ -65,7 +65,7 @@ const wordCount = props.wordCount?.toString();
/> />
<Fragment slot="section-information"> <Fragment slot="section-information">
<Authors lang={props.lang}> <Authors lang={props.lang}>
{authorsList.map((author) => <UserComponent class="p-author" user={author} lang={props.lang} />)} {authorsList.map((author) => <UserComponent rel="author" class="p-author" user={author} lang={props.lang} />)}
</Authors> </Authors>
{ {
requestersList && ( requestersList && (

View file

@ -47,8 +47,8 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
const { lang } = story.data; const { lang } = story.data;
const copyrightedCharacters = await formatCopyrightedCharacters(story.data.copyrightedCharacters); const copyrightedCharacters = await formatCopyrightedCharacters(story.data.copyrightedCharacters);
const authorsList = await getEntries(story.data.authors); const authorsList = await getEntries(story.data.authors);
const commissionersList = story.data.commissioner && (await getEntries(story.data.commissioner)); const commissionersList = story.data.commissioners && (await getEntries(story.data.commissioners));
const requestersList = story.data.requester && (await getEntries(story.data.requester)); const requestersList = story.data.requesters && (await getEntries(story.data.requesters));
const description = await Promise.all( const description = await Promise.all(
WEBSITE_LIST.map(async ({ website, exportFormat }) => { WEBSITE_LIST.map(async ({ website, exportFormat }) => {

View file

@ -52,18 +52,18 @@ async function storyFeedItem(
"story/authors", "story/authors",
(await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)), (await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)),
)}</p>` + )}</p>` +
(data.requester (data.requesters
? `<p>${t( ? `<p>${t(
data.lang, data.lang,
"story/requested_by", "story/requested_by",
(await getEntries(data.requester)).map((requester) => getLinkForUser(requester, data.lang)), (await getEntries(data.requesters)).map((requester) => getLinkForUser(requester, data.lang)),
)}</p>` )}</p>`
: "") + : "") +
(data.commissioner (data.commissioners
? `<p>${t( ? `<p>${t(
data.lang, data.lang,
"story/commissioned_by", "story/commissioned_by",
(await getEntries(data.commissioner)).map((commissioner) => getLinkForUser(commissioner, data.lang)), (await getEntries(data.commissioners)).map((commissioner) => getLinkForUser(commissioner, data.lang)),
)}</p>` )}</p>`
: "") + : "") +
`<hr><p><em>${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}</em></p>` + `<hr><p><em>${t(data.lang, "story/warnings", data.wordCount, data.contentWarning)}</em></p>` +

View file

@ -1,14 +1,20 @@
--- ---
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import { getCollection, type CollectionEntry } from "astro:content"; import { getCollection, getEntries, type CollectionEntry } from "astro:content";
import GalleryLayout from "../layouts/GalleryLayout.astro"; import GalleryLayout from "../layouts/GalleryLayout.astro";
import { t } from "../i18n"; import { DEFAULT_LANG, t } from "../i18n";
import UserComponent from "../components/UserComponent.astro";
type GameWithPubDate = CollectionEntry<"games"> & { data: { pubDate: Date } }; type GameWithPubDate = CollectionEntry<"games"> & { data: { pubDate: Date } };
const games = ( const games = await Promise.all(
(await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate)) as GameWithPubDate[] ((await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate)) as GameWithPubDate[])
).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()); .sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
.map(async (game) => ({
...game,
authors: await getEntries(game.data.authors),
})),
);
--- ---
<GalleryLayout pageTitle="Games" class="h-feed"> <GalleryLayout pageTitle="Games" class="h-feed">
@ -17,7 +23,7 @@ const games = (
<p class="p-summary my-4">A game that I've gone and done.</p> <p class="p-summary my-4">A game that I've gone and done.</p>
<ul class="my-6 flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal"> <ul class="my-6 flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
{ {
games.map((game) => ( games.map((game, i) => (
<li class="h-entry"> <li class="h-entry">
<a <a
class="u-url text-link hover:underline focus:underline" class="u-url text-link hover:underline focus:underline"
@ -26,7 +32,13 @@ const games = (
> >
{game.data.thumbnail ? ( {game.data.thumbnail ? (
<div class="flex aspect-[630/500] max-w-[288px] justify-center"> <div class="flex aspect-[630/500] max-w-[288px] justify-center">
<Image class="u-photo m-auto" src={game.data.thumbnail} alt={`Thumbnail for ${game.data.title}`} width={288} /> <Image
loading={i < 10 ? "eager" : "lazy"}
class="u-photo m-auto"
src={game.data.thumbnail}
alt={`Thumbnail for ${game.data.title}`}
width={288}
/>
</div> </div>
) : null} ) : null}
<div class="max-w-[288px] text-sm"> <div class="max-w-[288px] text-sm">
@ -37,6 +49,11 @@ const games = (
</time> </time>
</div> </div>
</a> </a>
<div style={{ display: "none" }}>
{game.authors.map((author) => (
<UserComponent rel="author" class="p-author" user={author} lang={DEFAULT_LANG} />
))}
</div>
</li> </li>
)) ))
} }

View file

@ -2,6 +2,7 @@
import type { GetStaticPaths } from "astro"; import type { GetStaticPaths } from "astro";
import { type CollectionEntry, getCollection } from "astro:content"; import { type CollectionEntry, getCollection } from "astro:content";
import GameLayout from "../../layouts/GameLayout.astro"; import GameLayout from "../../layouts/GameLayout.astro";
import { PUBLISH_DRAFTS } from "astro:env/server";
type Props = CollectionEntry<"games">; type Props = CollectionEntry<"games">;
@ -11,10 +12,12 @@ type Params = {
export const getStaticPaths: GetStaticPaths = async () => { export const getStaticPaths: GetStaticPaths = async () => {
const games = await getCollection("games"); const games = await getCollection("games");
return games.map((game) => ({ return games
params: { slug: game.slug } satisfies Params, .filter((game) => import.meta.env.DEV || PUBLISH_DRAFTS || !game.data.isDraft)
props: game satisfies Props, .map((game) => ({
})); params: { slug: game.slug } satisfies Params,
props: game satisfies Props,
}));
}; };
const game = Astro.props; const game = Astro.props;

View file

@ -1,17 +1,20 @@
--- ---
import type { ImageMetadata } from "astro"; import type { ImageMetadata } from "astro";
import { type CollectionEntry, type CollectionKey, getCollection } from "astro:content"; import { type CollectionEntry, type CollectionKey, getCollection, getEntries } from "astro:content";
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import GalleryLayout from "../layouts/GalleryLayout.astro"; import GalleryLayout from "../layouts/GalleryLayout.astro";
import { t } from "../i18n"; import { DEFAULT_LANG, t, type Lang } from "../i18n";
import UserComponent from "../components/UserComponent.astro";
const MAX_ITEMS = 8; const MAX_ITEMS = 10;
interface LatestItemsEntry { interface LatestItemsEntry {
type: string; type: string;
thumbnail?: ImageMetadata; thumbnail?: ImageMetadata;
href: string; href: string;
title: string; title: string;
lang: Lang;
authors: CollectionEntry<"users">[];
altText: string; altText: string;
pubDate: Date; pubDate: Date;
} }
@ -32,27 +35,42 @@ const games = (
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()) .sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
.slice(0, MAX_ITEMS); .slice(0, MAX_ITEMS);
const latestItems: LatestItemsEntry[] = [ const latestItems: LatestItemsEntry[] = await Promise.all(
stories.map<LatestItemsEntry>((story) => ({ [
type: "Story", stories.map((story) => ({
thumbnail: story.data.thumbnail, date: story.data.pubDate,
href: `/stories/${story.slug}`, fn: async () =>
title: story.data.title, ({
altText: t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning), type: "Story",
pubDate: story.data.pubDate, thumbnail: story.data.thumbnail,
})), href: `/stories/${story.slug}`,
games.map<LatestItemsEntry>((game) => ({ title: story.data.title,
type: "Game", authors: await getEntries(story.data.authors),
thumbnail: game.data.thumbnail, lang: story.data.lang,
href: `/games/${game.slug}`, altText: t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning),
title: game.data.title, pubDate: story.data.pubDate,
altText: t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning), }) satisfies LatestItemsEntry,
pubDate: game.data.pubDate, })),
})), games.map((game) => ({
] date: game.data.pubDate,
.flat() fn: async () =>
.sort((a, b) => b.pubDate.getTime() - a.pubDate.getTime()) ({
.slice(0, MAX_ITEMS); type: "Game",
thumbnail: game.data.thumbnail,
href: `/games/${game.slug}`,
title: game.data.title,
authors: await getEntries(game.data.authors),
lang: DEFAULT_LANG,
altText: t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning),
pubDate: game.data.pubDate,
}) satisfies LatestItemsEntry,
})),
]
.flat()
.sort((a, b) => b.date.getTime() - a.date.getTime())
.slice(0, MAX_ITEMS)
.map(async (entry) => await entry.fn()),
);
--- ---
<GalleryLayout pageTitle="Gallery" class="h-feed"> <GalleryLayout pageTitle="Gallery" class="h-feed">
@ -60,8 +78,8 @@ const latestItems: LatestItemsEntry[] = [
<h1 class="p-name m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">My gallery</h1> <h1 class="p-name m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">My gallery</h1>
<div class="p-summary"> <div class="p-summary">
<p class="my-4"> <p class="my-4">
Welcome to my gallery! You can expect lots of safe vore/endosoma ahead. Use the navigation menu to navigate through Welcome to my gallery! You can expect lots of safe vore/endosoma ahead. Use the navigation menu to navigate
my content. through my content.
</p> </p>
<ul class="list-disc pl-8"> <ul class="list-disc pl-8">
<li><a class="text-link underline" href="/stories/1">Read my stories!</a></li> <li><a class="text-link underline" href="/stories/1">Read my stories!</a></li>
@ -85,7 +103,13 @@ const latestItems: LatestItemsEntry[] = [
<a class="u-url text-link hover:underline focus:underline" href={entry.href} title={entry.altText}> <a class="u-url text-link hover:underline focus:underline" href={entry.href} title={entry.altText}>
{entry.thumbnail ? ( {entry.thumbnail ? (
<div class="flex aspect-square max-w-[192px] justify-center"> <div class="flex aspect-square max-w-[192px] justify-center">
<Image class="u-photo m-auto" src={entry.thumbnail} alt={`Thumbnail for ${entry.title}`} width={192} /> <Image
loading="eager"
class="u-photo m-auto"
src={entry.thumbnail}
alt={`Thumbnail for ${entry.title}`}
width={192}
/>
</div> </div>
) : null} ) : null}
<div class="max-w-[192px] text-sm"> <div class="max-w-[192px] text-sm">
@ -93,10 +117,17 @@ const latestItems: LatestItemsEntry[] = [
<br /> <br />
<span class="italic"> <span class="italic">
<span class="p-category">{entry.type}</span> &ndash;{" "} <span class="p-category">{entry.type}</span> &ndash;{" "}
<time class="dt-published" datetime={entry.pubDate.toISOString().slice(0, 10)}>{entry.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}</time> <time class="dt-published" datetime={entry.pubDate.toISOString().slice(0, 10)}>
{entry.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
</time>
</span> </span>
</div> </div>
</a> </a>
<div style={{ display: "none" }}>
{entry.authors.map((author) => (
<UserComponent rel="author" class="p-author" user={author} lang={entry.lang} />
))}
</div>
</li> </li>
)) ))
} }

View file

@ -3,6 +3,7 @@ import type { GetStaticPaths } from "astro";
import { type CollectionEntry, getCollection } from "astro:content"; import { type CollectionEntry, getCollection } from "astro:content";
import getReadingTime from "reading-time"; import getReadingTime from "reading-time";
import StoryLayout from "../../layouts/StoryLayout.astro"; import StoryLayout from "../../layouts/StoryLayout.astro";
import { PUBLISH_DRAFTS } from "astro:env/server";
type Props = CollectionEntry<"stories">; type Props = CollectionEntry<"stories">;
@ -12,10 +13,12 @@ type Params = {
export const getStaticPaths: GetStaticPaths = async () => { export const getStaticPaths: GetStaticPaths = async () => {
const stories = await getCollection("stories"); const stories = await getCollection("stories");
return stories.map((story) => ({ return stories
params: { slug: story.slug } satisfies Params, .filter((story) => import.meta.env.DEV || PUBLISH_DRAFTS || !story.data.isDraft)
props: story satisfies Props, .map((story) => ({
})); params: { slug: story.slug } satisfies Params,
props: story satisfies Props,
}));
}; };
const story = Astro.props; const story = Astro.props;

View file

@ -1,20 +1,26 @@
--- ---
import type { GetStaticPaths, Page } from "astro"; import type { GetStaticPaths, Page } from "astro";
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import { getCollection, type CollectionEntry } from "astro:content"; import { getCollection, getEntries, type CollectionEntry } from "astro:content";
import GalleryLayout from "../../layouts/GalleryLayout.astro"; import GalleryLayout from "../../layouts/GalleryLayout.astro";
import { t } from "../../i18n"; import { t } from "../../i18n";
import UserComponent from "../../components/UserComponent.astro";
type StoryWithPubDate = CollectionEntry<"stories"> & { data: { pubDate: Date } }; type StoryWithPubDate = CollectionEntry<"stories"> & { data: { pubDate: Date } };
type Props = { type Props = {
page: Page<StoryWithPubDate>; page: Page<StoryWithPubDate & { authors: CollectionEntry<"users">[] }>;
}; };
export const getStaticPaths: GetStaticPaths = async ({ paginate }) => { export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
const stories = ( const stories = await Promise.all(
(await getCollection("stories", (story) => !story.data.isDraft && story.data.pubDate)) as StoryWithPubDate[] ((await getCollection("stories", (story) => !story.data.isDraft && story.data.pubDate)) as StoryWithPubDate[])
).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()); .sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
.map(async (story) => ({
...story,
authors: await getEntries(story.data.authors),
})),
);
return paginate(stories, { pageSize: 30 }) satisfies { props: Props }[]; return paginate(stories, { pageSize: 30 }) satisfies { props: Props }[];
}; };
@ -44,20 +50,22 @@ const totalPages = Math.ceil(page.total / page.size);
) )
} }
{ {
[...Array(totalPages).keys()].map((p) => [...Array(totalPages).keys()]
p + 1 == page.currentPage ? ( .map((p) => p + 1)
<span class="border-r border-stone-400 px-4 py-1 font-semibold text-stone-900 dark:border-stone-500 dark:text-stone-50"> .map((p) =>
{p + 1} p == page.currentPage ? (
</span> <span class="border-r border-stone-400 px-4 py-1 font-semibold text-stone-900 dark:border-stone-500 dark:text-stone-50">
) : ( {p}
<a </span>
class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" ) : (
href={page.url.current.replace(`/stories/${page.currentPage}`, `/stories/${p + 1}`)} <a
> class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500"
{p + 1} href={page.url.current.replace(`/stories/${page.currentPage}`, `/stories/${p}`)}
</a> >
), {p}
) </a>
),
)
} }
{ {
page.url.next && ( page.url.next && (
@ -69,7 +77,7 @@ const totalPages = Math.ceil(page.total / page.size);
</div> </div>
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal"> <ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
{ {
page.data.map((story) => ( page.data.map((story, i) => (
<li class="h-entry break-inside-avoid"> <li class="h-entry break-inside-avoid">
<a <a
class="u-url text-link hover:underline focus:underline" class="u-url text-link hover:underline focus:underline"
@ -79,6 +87,7 @@ const totalPages = Math.ceil(page.total / page.size);
{story.data.thumbnail ? ( {story.data.thumbnail ? (
<div class="flex aspect-square max-w-[192px] justify-center"> <div class="flex aspect-square max-w-[192px] justify-center">
<Image <Image
loading={i < 10 ? "eager" : "lazy"}
class="u-photo m-auto" class="u-photo m-auto"
src={story.data.thumbnail} src={story.data.thumbnail}
alt={`Thumbnail for ${story.data.title}`} alt={`Thumbnail for ${story.data.title}`}
@ -94,6 +103,11 @@ const totalPages = Math.ceil(page.total / page.size);
</time> </time>
</div> </div>
</a> </a>
<div style={{ display: "none" }}>
{story.authors.map((author) => (
<UserComponent rel="author" class="p-author" user={author} lang={story.data.lang} />
))}
</div>
</li> </li>
)) ))
} }
@ -107,20 +121,22 @@ const totalPages = Math.ceil(page.total / page.size);
) )
} }
{ {
[...Array(totalPages).keys()].map((p) => [...Array(totalPages).keys()]
p + 1 == page.currentPage ? ( .map((p) => p + 1)
<span class="border-r border-stone-400 px-4 py-1 font-semibold text-stone-900 dark:border-stone-500 dark:text-stone-50"> .map((p) =>
{p + 1} p == page.currentPage ? (
</span> <span class="border-r border-stone-400 px-4 py-1 font-semibold text-stone-900 dark:border-stone-500 dark:text-stone-50">
) : ( {p}
<a </span>
class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" ) : (
href={page.url.current.replace(`/stories/${page.currentPage}`, `/stories/${p + 1}`)} <a
> class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500"
{p + 1} href={page.url.current.replace(`/stories/${page.currentPage}`, `/stories/${p}`)}
</a> >
), {p}
) </a>
),
)
} }
{ {
page.url.next && ( page.url.next && (

View file

@ -27,7 +27,9 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ
content="The Lost of the Marshes || The story of Quince, Nikili, and Suu." content="The Lost of the Marshes || The story of Quince, Nikili, and Suu."
/> />
<h1 class="p-name m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">{series.data.name}</h1> <h1 class="p-name m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">{series.data.name}</h1>
<p class="p-summary my-4">This is the main hub for the story of Quince, Nikili, and Suu, as well as all bonus content.</p> <p class="p-summary my-4">
This is the main hub for the story of Quince, Nikili, and Suu, as well as all bonus content.
</p>
<section class="my-2" aria-labelledby="main-chapters"> <section class="my-2" aria-labelledby="main-chapters">
<h2 <h2
id="main-chapters" id="main-chapters"

View file

@ -155,7 +155,7 @@ const totalWorksWithTag = t(
day: "numeric", day: "numeric",
year: "numeric", year: "numeric",
})} })}
</span> </time>
</div> </div>
</a> </a>
</li> </li>