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
.astro/
dist/

View file

@ -28,11 +28,12 @@ npm run prettier # Prettier formatting
### 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 |
|-|-|-|
| `APACHE_CONFIG` | boolean | Whether to generate an `.htaccess` Apache config file at the root of the output directory or not. |
| Name | Type | Description |
| ---------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `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

View file

@ -27,6 +27,7 @@ export default defineConfig({
env: {
schema: {
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",
"version": "1.7.4",
"version": "1.7.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gallery.badmanners.xyz",
"version": "1.7.4",
"version": "1.7.5",
"hasInstallScript": true,
"dependencies": {
"@astrojs/check": "^0.9.2",

View file

@ -1,7 +1,7 @@
{
"name": "gallery.badmanners.xyz",
"type": "module",
"version": "1.7.4",
"version": "1.7.5",
"scripts": {
"postinstall": "astro sync",
"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 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 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) => {
button.classList.remove("hidden");
button.removeAttribute("aria-hidden");
button.setAttribute("aria-hidden", "false");
button.addEventListener("click", (e) => {
e.preventDefault();
if (colorScheme === "dark") {

View file

@ -26,7 +26,7 @@ const { link, instance, user, postId } = Astro.props;
</h2>
<div class="text-stone-800 dark:text-stone-100" id="comments">
<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>
</div>
</section>
@ -46,7 +46,7 @@ const { link, instance, user, postId } = Astro.props;
class="-mt-1 mr-1 animate-spin"
fill="none"
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>
<path
@ -59,25 +59,32 @@ const { link, instance, user, postId } = Astro.props;
</template>
<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">
<a data-author class="text-link flex items-center text-lg hover:underline focus:underline" target="_blank">
<img data-avatar class="mr-2 w-10 rounded-full border border-stone-400 dark:border-stone-600" />
<span data-display-name></span>
<a
data-author
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
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"
>
<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>
</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="flex" aria-label="Favorites">
<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
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>
@ -85,14 +92,14 @@ const { link, instance, user, postId } = Astro.props;
</div>
<div class="ml-4 flex" aria-label="Reblogs">
<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
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>
</svg>
</div>
</div>
<div data-comment-thread class="-mb-2" aria-hidden></div>
<div data-comment-thread class="-mb-2" aria-hidden="true"></div>
</div>
</template>
@ -164,19 +171,21 @@ const { link, instance, user, postId } = Astro.props;
)!;
data.descendants.forEach((comment) => {
const commentBox = commentTemplate.content.cloneNode(true) as HTMLDivElement;
commentBox.id = `comment-${comment.id}`;
const commentBoxAuthor = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-author]")!;
commentBoxAuthor.href = comment.account.url;
const avatar = commentBoxAuthor.querySelector<HTMLElementTagNameMap["img"]>("img[data-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]")!;
displayName.innerHTML = replaceEmojis(comment.account.display_name, comment.account.emojis);
const commentBoxPostLink = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-post-link]")!;
commentBoxPostLink.href = comment.url;
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", {
month: "short",
day: "numeric",
@ -186,8 +195,10 @@ const { link, instance, user, postId } = Astro.props;
});
if (comment.edited_at) {
const edited = document.createElement("span");
edited.className = "italic";
const edited = document.createElement("time");
edited.className = "dt-updated italic";
edited.setAttribute("datetime", comment.edited_at);
edited.setAttribute("title", comment.edited_at);
edited.innerText = "(edited)";
commentBoxPostLink.appendChild(edited);
}
@ -209,7 +220,7 @@ const { link, instance, user, postId } = Astro.props;
commentMap[comment.id] = commentsIndex;
const parentThreadDiv =
commentsList[commentsIndex].querySelector<HTMLElementTagNameMap["div"]>("div[data-comment-thread]")!;
parentThreadDiv.removeAttribute("aria-hidden");
parentThreadDiv.setAttribute("aria-hidden", "false");
parentThreadDiv.setAttribute("aria-label", "Replies");
parentThreadDiv.appendChild(commentBox);
}

View file

@ -7,19 +7,20 @@ type Props = {
lang: Lang;
user: CollectionEntry<"users">;
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 link = user.data.preferredLink ? user.data.links[user.data.preferredLink]!.link : null;
---
{
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}
</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(),
// Optional parameters
shortTitle: z.string().optional(),
commissioner: userList.optional(),
requester: userList.optional(),
commissioners: userList.optional(),
requesters: userList.optional(),
summary: z.string().trim().optional(),
thumbnailWidth: z.number().int().optional(),
thumbnailHeight: z.number().int().optional(),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,7 +40,7 @@ tags:
- lesbian sex
- orgy
- commission
commissioner: asof-yeun
commissioners: asof-yeun
copyrightedCharacters:
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>
<Navigation />
<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>
</div>
<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">
<svg style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }} viewBox="0 0 576 512" aria-hidden>
<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="true"
>
<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"
></path>
@ -53,19 +67,23 @@ const currentYear = new Date().getFullYear().toString();
<span id="label-main-website" class="hidden">Main website</span>
</a>
<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
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>
</svg>
<span id="label-rss-feed" class="hidden">RSS feed</span>
</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
style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }}
viewBox="0 0 512 512"
class="hidden dark:block"
aria-hidden
aria-hidden="true"
>
<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"
@ -75,7 +93,7 @@ const currentYear = new Date().getFullYear().toString();
style={{ width: "1.5rem", height: "1.5rem", fill: "currentColor" }}
viewBox="0 0 512 512"
class="block dark:hidden"
aria-hidden
aria-hidden="true"
>
<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"

View file

@ -54,7 +54,7 @@ const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !ga
/>
<Fragment slot="section-information">
<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>
<div id="platforms">
<p>{t(props.lang, "game/platforms", props.platforms)}</p>

View file

@ -115,9 +115,13 @@ const thumbnail =
<a
href={series ? series.data.link : props.labelReturnTo.link}
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
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>
@ -131,9 +135,13 @@ const thumbnail =
<a
href="#description"
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
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>
@ -144,14 +152,14 @@ const thumbnail =
<button
data-dark-mode
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-hidden
aria-labelledby="label-toggle-dark-mode"
aria-hidden="true"
>
<svg
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
viewBox="0 0 512 512"
class="hidden dark:block"
aria-hidden
aria-hidden="true"
>
<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"
@ -161,7 +169,7 @@ const thumbnail =
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
viewBox="0 0 512 512"
class="block dark:hidden"
aria-hidden
aria-hidden="true"
>
<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"
@ -190,14 +198,14 @@ const thumbnail =
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
class="mr-1"
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" />
</svg>
<span>{props.prev.title}</span>
</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 ? (
<a
@ -210,13 +218,13 @@ const thumbnail =
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
class="ml-1"
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" />
</svg>
</a>
) : (
<div aria-hidden />
<div aria-hidden="true" />
)}
</div>
<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
id="publish-date"
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-description={
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" }}
class="mr-1"
viewBox="0 0 384 512"
aria-hidden
aria-hidden="true"
><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"
></path></svg
@ -345,7 +353,7 @@ const thumbnail =
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
class="mr-1"
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" />
</svg>
@ -365,7 +373,7 @@ const thumbnail =
style={{ width: "1.25rem", height: "1.25rem", fill: "currentColor" }}
class="ml-1"
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" />
</svg>
@ -441,7 +449,9 @@ const thumbnail =
class="pt-6 text-center text-xs text-black dark:text-white"
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"
>{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 series = props.series && (await getEntry(props.series));
const authorsList = await getEntries(props.authors);
const commissionersList = props.commissioner && (await getEntries(props.commissioner));
const requestersList = props.requester && (await getEntries(props.requester));
const commissionersList = props.commissioners && (await getEntries(props.commissioners));
const requestersList = props.requesters && (await getEntries(props.requesters));
const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
const wordCount = props.wordCount?.toString();
@ -65,7 +65,7 @@ const wordCount = props.wordCount?.toString();
/>
<Fragment slot="section-information">
<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>
{
requestersList && (

View file

@ -47,8 +47,8 @@ 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));
const commissionersList = story.data.commissioners && (await getEntries(story.data.commissioners));
const requestersList = story.data.requesters && (await getEntries(story.data.requesters));
const description = await Promise.all(
WEBSITE_LIST.map(async ({ website, exportFormat }) => {

View file

@ -52,18 +52,18 @@ async function storyFeedItem(
"story/authors",
(await getEntries(data.authors)).map((author) => getLinkForUser(author, data.lang)),
)}</p>` +
(data.requester
(data.requesters
? `<p>${t(
data.lang,
"story/requested_by",
(await getEntries(data.requester)).map((requester) => getLinkForUser(requester, data.lang)),
(await getEntries(data.requesters)).map((requester) => getLinkForUser(requester, data.lang)),
)}</p>`
: "") +
(data.commissioner
(data.commissioners
? `<p>${t(
data.lang,
"story/commissioned_by",
(await getEntries(data.commissioner)).map((commissioner) => getLinkForUser(commissioner, data.lang)),
(await getEntries(data.commissioners)).map((commissioner) => getLinkForUser(commissioner, data.lang)),
)}</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 { getCollection, type CollectionEntry } from "astro:content";
import { getCollection, getEntries, type CollectionEntry } from "astro:content";
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 } };
const games = (
(await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate)) as GameWithPubDate[]
).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
const games = await Promise.all(
((await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate)) as GameWithPubDate[])
.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">
@ -17,7 +23,7 @@ const games = (
<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">
{
games.map((game) => (
games.map((game, i) => (
<li class="h-entry">
<a
class="u-url text-link hover:underline focus:underline"
@ -26,7 +32,13 @@ const games = (
>
{game.data.thumbnail ? (
<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>
) : null}
<div class="max-w-[288px] text-sm">
@ -37,6 +49,11 @@ const games = (
</time>
</div>
</a>
<div style={{ display: "none" }}>
{game.authors.map((author) => (
<UserComponent rel="author" class="p-author" user={author} lang={DEFAULT_LANG} />
))}
</div>
</li>
))
}

View file

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

View file

@ -1,17 +1,20 @@
---
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 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 {
type: string;
thumbnail?: ImageMetadata;
href: string;
title: string;
lang: Lang;
authors: CollectionEntry<"users">[];
altText: string;
pubDate: Date;
}
@ -32,27 +35,42 @@ const games = (
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
.slice(0, MAX_ITEMS);
const latestItems: LatestItemsEntry[] = [
stories.map<LatestItemsEntry>((story) => ({
type: "Story",
thumbnail: story.data.thumbnail,
href: `/stories/${story.slug}`,
title: story.data.title,
altText: t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning),
pubDate: story.data.pubDate,
})),
games.map<LatestItemsEntry>((game) => ({
type: "Game",
thumbnail: game.data.thumbnail,
href: `/games/${game.slug}`,
title: game.data.title,
altText: t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning),
pubDate: game.data.pubDate,
})),
]
.flat()
.sort((a, b) => b.pubDate.getTime() - a.pubDate.getTime())
.slice(0, MAX_ITEMS);
const latestItems: LatestItemsEntry[] = await Promise.all(
[
stories.map((story) => ({
date: story.data.pubDate,
fn: async () =>
({
type: "Story",
thumbnail: story.data.thumbnail,
href: `/stories/${story.slug}`,
title: story.data.title,
authors: await getEntries(story.data.authors),
lang: story.data.lang,
altText: t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning),
pubDate: story.data.pubDate,
}) satisfies LatestItemsEntry,
})),
games.map((game) => ({
date: game.data.pubDate,
fn: async () =>
({
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">
@ -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>
<div class="p-summary">
<p class="my-4">
Welcome to my gallery! You can expect lots of safe vore/endosoma ahead. Use the navigation menu to navigate through
my content.
Welcome to my gallery! You can expect lots of safe vore/endosoma ahead. Use the navigation menu to navigate
through my content.
</p>
<ul class="list-disc pl-8">
<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}>
{entry.thumbnail ? (
<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>
) : null}
<div class="max-w-[192px] text-sm">
@ -93,10 +117,17 @@ const latestItems: LatestItemsEntry[] = [
<br />
<span class="italic">
<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>
</div>
</a>
<div style={{ display: "none" }}>
{entry.authors.map((author) => (
<UserComponent rel="author" class="p-author" user={author} lang={entry.lang} />
))}
</div>
</li>
))
}

View file

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

View file

@ -1,20 +1,26 @@
---
import type { GetStaticPaths, Page } from "astro";
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 { t } from "../../i18n";
import UserComponent from "../../components/UserComponent.astro";
type StoryWithPubDate = CollectionEntry<"stories"> & { data: { pubDate: Date } };
type Props = {
page: Page<StoryWithPubDate>;
page: Page<StoryWithPubDate & { authors: CollectionEntry<"users">[] }>;
};
export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
const stories = (
(await getCollection("stories", (story) => !story.data.isDraft && story.data.pubDate)) as StoryWithPubDate[]
).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
const stories = await Promise.all(
((await getCollection("stories", (story) => !story.data.isDraft && story.data.pubDate)) as StoryWithPubDate[])
.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 }[];
};
@ -44,20 +50,22 @@ const totalPages = Math.ceil(page.total / page.size);
)
}
{
[...Array(totalPages).keys()].map((p) =>
p + 1 == page.currentPage ? (
<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 + 1}
</span>
) : (
<a
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}`)}
>
{p + 1}
</a>
),
)
[...Array(totalPages).keys()]
.map((p) => p + 1)
.map((p) =>
p == page.currentPage ? (
<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}
</span>
) : (
<a
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}`)}
>
{p}
</a>
),
)
}
{
page.url.next && (
@ -69,7 +77,7 @@ const totalPages = Math.ceil(page.total / page.size);
</div>
<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">
<a
class="u-url text-link hover:underline focus:underline"
@ -79,6 +87,7 @@ const totalPages = Math.ceil(page.total / page.size);
{story.data.thumbnail ? (
<div class="flex aspect-square max-w-[192px] justify-center">
<Image
loading={i < 10 ? "eager" : "lazy"}
class="u-photo m-auto"
src={story.data.thumbnail}
alt={`Thumbnail for ${story.data.title}`}
@ -94,6 +103,11 @@ const totalPages = Math.ceil(page.total / page.size);
</time>
</div>
</a>
<div style={{ display: "none" }}>
{story.authors.map((author) => (
<UserComponent rel="author" class="p-author" user={author} lang={story.data.lang} />
))}
</div>
</li>
))
}
@ -107,20 +121,22 @@ const totalPages = Math.ceil(page.total / page.size);
)
}
{
[...Array(totalPages).keys()].map((p) =>
p + 1 == page.currentPage ? (
<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 + 1}
</span>
) : (
<a
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}`)}
>
{p + 1}
</a>
),
)
[...Array(totalPages).keys()]
.map((p) => p + 1)
.map((p) =>
p == page.currentPage ? (
<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}
</span>
) : (
<a
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}`)}
>
{p}
</a>
),
)
}
{
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."
/>
<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">
<h2
id="main-chapters"

View file

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