From a9d5a88d0e862a8d3f8fffe0af9a4fa9e63e3adc Mon Sep 17 00:00:00 2001 From: Bad Manners <me@badmanners.xyz> Date: Sun, 16 Jun 2024 19:24:25 -0300 Subject: [PATCH] Migrate to LFTP deployment and improve templates - Add `deploy-lftp` command - Add 404 page - Change relative links to absolute links - Fix pagination links - Remove drafts from Pagefind indexing - Fix OpenGraph descriptions for i18n - Add Commissioners and Requesters components - Add consistent type-checking for getStaticPaths --- README.md | 16 ++++- deploy_lftp.sh | 37 +++++++++++ examples/game.md | 6 +- examples/story.md | 4 +- examples/user.yaml | 2 +- package-lock.json | 64 ++++++++++++++++--- package.json | 4 +- src/components/Authors.astro | 6 +- src/components/Commissioners.astro | 15 +++++ src/components/Requesters.astro | 15 +++++ src/content/stories/bladder-filler.md | 2 +- .../stories/bottom-of-the-food-chain.md | 2 +- src/content/stories/team-building.md | 2 +- .../bonus-1-quince-s-fantasy.md | 2 +- .../the-lost-of-the-marshes/chapter-4.md | 2 +- .../the-lost-of-the-marshes/chapter-5.md | 2 +- src/content/stories/tiny-accident.md | 2 +- src/i18n/index.ts | 39 ++++++++--- src/layouts/GameLayout.astro | 2 +- src/layouts/StoryLayout.astro | 21 +++--- src/pages/404.astro | 9 +++ src/pages/api/export-story/[...slug].ts | 8 +-- src/pages/games/[...slug].astro | 11 +++- src/pages/stories/[...slug].astro | 11 +++- src/pages/stories/[page].astro | 20 ++++-- src/pages/tags/[slug].astro | 20 +++--- 26 files changed, 254 insertions(+), 70 deletions(-) create mode 100755 deploy_lftp.sh create mode 100644 src/components/Commissioners.astro create mode 100644 src/components/Requesters.astro create mode 100644 src/pages/404.astro diff --git a/README.md b/README.md index 3598f63..721c3b3 100644 --- a/README.md +++ b/README.md @@ -32,5 +32,19 @@ npm run export-story -- --output-dir ~/Documents/TO_UPLOAD slug-for-story-to-exp ```bash npm run build -scp -r ./dist/* my-ssh-server:./gallery.badmanners.xyz/ ``` + +Then, if you're using LFTP: + +1. Create a new `.env` file at the root of the project: + +```env +DEPLOY_LFTP_HOST=https://example-webdav-server.com +DEPLOY_LFTP_USER=example_user +DEPLOY_LFTP_PASSWORD=sup3r_s3cr3t_password +DEPLOY_LFTP_TARGETFOLDER=sites/gallery.badmanners.xyz/ +``` + +2. Run the following command: `npm run deploy-lftp` + +Otherwise, to deploy over SSH: `scp -r ./dist/* my-ssh-server:./gallery.badmanners.xyz/` diff --git a/deploy_lftp.sh b/deploy_lftp.sh new file mode 100755 index 0000000..3ab40e9 --- /dev/null +++ b/deploy_lftp.sh @@ -0,0 +1,37 @@ +#!/bin/env bash +set -e + +HOST="${DEPLOY_LFTP_HOST}" +if [ -z "$HOST" ]; then + echo "ERROR: Missing envvar \$DEPLOY_LFTP_HOST" + exit 1 +fi +USER="${DEPLOY_LFTP_USER}" +if [ -z "$USER" ]; then + echo "ERROR: Missing envvar \$DEPLOY_LFTP_USER" + exit 1 +fi +PASSWORD="${DEPLOY_LFTP_PASSWORD}" +if [ -z "$PASSWORD" ]; then + echo "ERROR: Missing envvar \$DEPLOY_LFTP_PASSWORD" + exit 1 +fi +TARGETFOLDER="${DEPLOY_LFTP_TARGETFOLDER}" +if [ -z "$TARGETFOLDER" ]; then + echo "ERROR: Missing envvar \$DEPLOY_LFTP_TARGETFOLDER" + exit 1 +fi +SOURCEFOLDER="${DEPLOY_LFTP_SOURCEFOLDER:-dist/}" +if [ -z "$SOURCEFOLDER" ]; then + echo "ERROR: Missing envvar \$DEPLOY_LFTP_SOURCEFOLDER" + exit 1 +fi + +lftp -f " +open -u $USER,$PASSWORD $HOST +mirror --reverse --include-glob _astro/* --delete --only-missing --no-perms --verbose $SOURCEFOLDER $TARGETFOLDER +mirror --reverse --exclude-glob _astro/* --delete --no-perms --verbose $SOURCEFOLDER $TARGETFOLDER +bye +" + +echo "Done." diff --git a/examples/game.md b/examples/game.md index b07c1ad..8dc0ad1 100644 --- a/examples/game.md +++ b/examples/game.md @@ -1,13 +1,13 @@ --- # slug: some-custom-slug -title: Example Draft +title: Example Game # shortTitle: Example pubDate: 2024-01-01 isDraft: true authors: bad-manners contentWarning: > This game contains some stuff. -# thumbnail: /src/assets/thumbnails/game_crossing_over_cover.png +# thumbnail: /src/assets/thumbnails/game_thumbnail.png description: | Some funny text. # descriptionPlaintext: > @@ -24,4 +24,4 @@ tags: [] # lang: eng --- -The game content goes here. +The game content (i.e. embed) goes here. diff --git a/examples/story.md b/examples/story.md index f3c56a6..a99ace9 100644 --- a/examples/story.md +++ b/examples/story.md @@ -1,6 +1,6 @@ --- # slug: some-custom-slug -title: Example Draft +title: Example Story # shortTitle: Example pubDate: 2024-01-01 isDraft: true @@ -8,7 +8,7 @@ authors: bad-manners wordCount: 1000 contentWarning: > Contains: Non-fatal same size oral vore, with willing anthro male fox predator, and unwilling feral female wolf prey. Also includes other stuff. -# thumbnail: /src/assets/thumbnails/tlotm_ch1.png +# thumbnail: /src/assets/thumbnails/story_thumbnail.png description: | Some funny text. # descriptionPlaintext: > diff --git a/examples/user.yaml b/examples/user.yaml index b180ede..ba7eadf 100644 --- a/examples/user.yaml +++ b/examples/user.yaml @@ -1,7 +1,7 @@ name: Nameless User nameLang: eng: Nameless - tok: jan Nenlesuse pi nimi ala + tok: jan Nenle pi nimi ala # avatar: /src/assets/images/logo_bm.png links: website: https://example.com diff --git a/package-lock.json b/package-lock.json index 12df10b..36448c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gallery-badmanners-xyz", - "version": "1.3.1", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gallery-badmanners-xyz", - "version": "1.3.1", + "version": "1.4.0", "dependencies": { "@astrojs/check": "^0.5.10", "@astrojs/rss": "^4.0.5", @@ -26,6 +26,7 @@ }, "devDependencies": { "commander": "^12.0.0", + "dotenv-cli": "^7.4.2", "fetch-retry": "^6.0.0", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.13.0", @@ -2040,11 +2041,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2669,6 +2671,45 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-cli": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.4.2.tgz", + "integrity": "sha512-SbUj8l61zIbzyhIbg0FwPJq6+wjbzdn9oEtozQpZ6kW2ihCcapKVZj49oCT3oPM+mgQm+itgvUQcG5szxVrZTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "dotenv": "^16.3.0", + "dotenv-expand": "^10.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "dotenv": "cli.js" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/dset": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.3.tgz", @@ -2908,9 +2949,10 @@ "dev": true }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3546,6 +3588,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -4688,7 +4731,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "optional": true, + "devOptional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6915,6 +6958,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, diff --git a/package.json b/package.json index 4848f28..e3acf7e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gallery-badmanners-xyz", "type": "module", - "version": "1.3.1", + "version": "1.4.0", "scripts": { "dev": "astro dev", "start": "astro dev", @@ -9,6 +9,7 @@ "preview": "astro preview", "astro": "astro", "prettier": "prettier . --write", + "deploy-lftp": "dotenv ./deploy_lftp.sh", "export-story": "tsx scripts/export-story.ts" }, "dependencies": { @@ -30,6 +31,7 @@ }, "devDependencies": { "commander": "^12.0.0", + "dotenv-cli": "^7.4.2", "fetch-retry": "^6.0.0", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.13.0", diff --git a/src/components/Authors.astro b/src/components/Authors.astro index 0e8347c..7e74761 100644 --- a/src/components/Authors.astro +++ b/src/components/Authors.astro @@ -8,8 +8,8 @@ type Props = { const { lang } = Astro.props; const authors = Astro.slots.has("default") - ? (await Astro.slots.render("default")).replaceAll(/\<\/(a|span)\>\</g, "</$1><br><") - : ""; + ? (await Astro.slots.render("default")).replaceAll(/\<\/(a|span)\>\</g, "</$1><br><").split("<br>") + : []; --- -{authors ? <p id="authors" set:html={t(lang, "story/authors", authors.split("<br>"))} /> : null} +{authors.length ? <p id="authors" set:html={t(lang, "story/authors", authors)} /> : null} diff --git a/src/components/Commissioners.astro b/src/components/Commissioners.astro new file mode 100644 index 0000000..bd4400e --- /dev/null +++ b/src/components/Commissioners.astro @@ -0,0 +1,15 @@ +--- +import { type Lang } from "../content/config"; +import { t } from "../i18n"; + +type Props = { + lang: Lang; +}; + +const { lang } = Astro.props; +const commissioners = Astro.slots.has("default") + ? (await Astro.slots.render("default")).replaceAll(/\<\/(a|span)\>\</g, "</$1><br><").split("<br>") + : []; +--- + +{commissioners.length ? <p id="commissioners" set:html={t(lang, "story/commissioned_by", commissioners)} /> : null} diff --git a/src/components/Requesters.astro b/src/components/Requesters.astro new file mode 100644 index 0000000..4725719 --- /dev/null +++ b/src/components/Requesters.astro @@ -0,0 +1,15 @@ +--- +import { type Lang } from "../content/config"; +import { t } from "../i18n"; + +type Props = { + lang: Lang; +}; + +const { lang } = Astro.props; +const requesters = Astro.slots.has("default") + ? (await Astro.slots.render("default")).replaceAll(/\<\/(a|span)\>\</g, "</$1><br><").split("<br>") + : []; +--- + +{requesters.length ? <p id="requesters" set:html={t(lang, "story/requested_by", requesters)} /> : null} diff --git a/src/content/stories/bladder-filler.md b/src/content/stories/bladder-filler.md index 026456c..af521c4 100644 --- a/src/content/stories/bladder-filler.md +++ b/src/content/stories/bladder-filler.md @@ -9,7 +9,7 @@ thumbnail: /src/assets/thumbnails/bm_ff_15_bladder_filler.png description: | Always watch what you wish for... Blather the right thing to the right gryphon, and you might wash up in a bladder! - The return of Beetle! Swallowing multiple creatures, through multiple holes – [myself included!](./birdroom) –, has done little to quell his unusual appetites. But this time, he's set his eyes on you! Yes, you – a small dragon, who has been itching to try some bladder vore. An unusual request, but hey, I won't judge you! + The return of Beetle! Swallowing multiple creatures, through multiple holes – [myself included!](/stories/birdroom) –, has done little to quell his unusual appetites. But this time, he's set his eyes on you! Yes, you – a small dragon, who has been itching to try some bladder vore. An unusual request, but hey, I won't judge you! ...In all seriousness, I wanted to do something more daring for a PoV story. With a dragon, instead of a generic anthro, this time. And not to mention bladder vore, which is also a first for me. I'll still stick to third person narration for most of my stories, but let me know if PoV is something that you want to see more of. descriptionPlaintext: > diff --git a/src/content/stories/bottom-of-the-food-chain.md b/src/content/stories/bottom-of-the-food-chain.md index 85839e8..d9fc41b 100644 --- a/src/content/stories/bottom-of-the-food-chain.md +++ b/src/content/stories/bottom-of-the-food-chain.md @@ -9,7 +9,7 @@ thumbnail: /src/assets/thumbnails/bm_14_bottom_of_the_food_chain.png description: | A unique opportunity falls onto Muno's coils, but he's not too pleased about it... - I know everyone is already thinking about October and Halloween, but I still have a Snektember addition to share! This story follows Muno the snake after the events of ["Ruffling Some Feathers"](./ruffling-some-feathers), although you don't need to read that story to enjoy this one. + I know everyone is already thinking about October and Halloween, but I still have a Snektember addition to share! This story follows Muno the snake after the events of ["Ruffling Some Feathers"](/stories/ruffling-some-feathers), although you don't need to read that story to enjoy this one. descriptionPlaintext: > A unique opportunity falls onto Muno's coils, but he's not too pleased about it... diff --git a/src/content/stories/team-building.md b/src/content/stories/team-building.md index bbf3c88..cf4aad5 100644 --- a/src/content/stories/team-building.md +++ b/src/content/stories/team-building.md @@ -9,7 +9,7 @@ thumbnail: /src/assets/thumbnails/bm_comm_4_team_building.png description: | After another semester in college, Yolk finds new opportunities to surpass his limits and grow closer to his friends than ever. - This commission is a sequel to the raffle request, [Team Effort](./team-effort)! + This commission is a sequel to the raffle request, [Team Effort](/stories/team-effort)! descriptionPlaintext: > After another semester in college, Yolk finds new opportunities to surpass his limits and grow closer to his friends than ever. diff --git a/src/content/stories/the-lost-of-the-marshes/bonus-1-quince-s-fantasy.md b/src/content/stories/the-lost-of-the-marshes/bonus-1-quince-s-fantasy.md index 9b4858e..0a4a19a 100644 --- a/src/content/stories/the-lost-of-the-marshes/bonus-1-quince-s-fantasy.md +++ b/src/content/stories/the-lost-of-the-marshes/bonus-1-quince-s-fantasy.md @@ -8,7 +8,7 @@ contentWarning: > Contains: macro and size difference, non-fatal oral vore. Also includes dream scenarios, role reversal, and self-vore. thumbnail: /src/assets/thumbnails/tlotm_bonus_1.png description: | - This is a bonus chapter of The Lost of the Marshes, set between [Chapter 4 – Change](./chapter-4) and [Chapter 5 – Intersection](./chapter-5). + This is a bonus chapter of The Lost of the Marshes, set between [Chapter 4 – Change](/stories/the-lost-of-the-marshes/chapter-4) and [Chapter 5 – Intersection](/stories/the-lost-of-the-marshes/chapter-5). I had this idea while working on the next chapter, and wanted to try something a bit more experimental involving these characters. It's self-indulgent and loose canon, and something of a departure from my usual stories! I don't even know how to describe it... Well, whatever it is, it's certainly a mash of different thoughts and ideas that had been boiling in my head, and simmering them down into something legible was an interesting exercise. Hope it's an enjoyable read too! descriptionPlaintext: > diff --git a/src/content/stories/the-lost-of-the-marshes/chapter-4.md b/src/content/stories/the-lost-of-the-marshes/chapter-4.md index 97183f6..59643e1 100644 --- a/src/content/stories/the-lost-of-the-marshes/chapter-4.md +++ b/src/content/stories/the-lost-of-the-marshes/chapter-4.md @@ -12,7 +12,7 @@ description: | This chapter took a lot longer to churn out than any other so far. It was still a bunch of fun to write, but each chapter so far has been a lot more work than the last... So before chapter 5 comes out (and don't worry, it will – there are plenty more chapters with these three still planned!), I might put out a few separate standalone stories that focus more on "hot sauce" and less on world-building, just so I can have a more regular posting schedule. - [BONUS CHAPTER! Quince's Fantasy, set **after** this one.](./bonus-1-quince-s-fantasy) + [BONUS CHAPTER! Quince's Fantasy, set **after** this one.](/stories/the-lost-of-the-marshes/bonus-1-quince-s-fantasy) descriptionPlaintext: > How hard is it to sneak a giant dragon into a village? Quince and Nikili might learn the answer sooner rather than later... diff --git a/src/content/stories/the-lost-of-the-marshes/chapter-5.md b/src/content/stories/the-lost-of-the-marshes/chapter-5.md index 915b8dc..cefb563 100644 --- a/src/content/stories/the-lost-of-the-marshes/chapter-5.md +++ b/src/content/stories/the-lost-of-the-marshes/chapter-5.md @@ -12,7 +12,7 @@ description: | This chapter turned out pretty heavy, both dialogue- and theme-wise... Regardless, I hope you enjoy it. I want to work on something short for the 8/8, and next will be chapter 6, which should hopefully be more positive and have more emphasis on vore. - [BONUS CHAPTER! Quince's Fantasy, set **before** this one.](./bonus-1-quince-s-fantasy) + [BONUS CHAPTER! Quince's Fantasy, set **before** this one.](/stories/the-lost-of-the-marshes/bonus-1-quince-s-fantasy) descriptionPlaintext: > In a distant place full of passing faces, our protagonists are forced to face their demons, both without and within. diff --git a/src/content/stories/tiny-accident.md b/src/content/stories/tiny-accident.md index 60bb623..7e2af9c 100644 --- a/src/content/stories/tiny-accident.md +++ b/src/content/stories/tiny-accident.md @@ -9,7 +9,7 @@ thumbnail: /src/assets/thumbnails/bm_18_tiny_accident.png description: | Kolo's day at the airship is nearly over, but a tiny stalker will unwittingly make his evening quite eventful... - Finally got around to finishing a story ever since I worked on [Crossing Over](../games/crossing-over)! I wanna get back into writing more stuff again, and this short story has finally broken my writer's block. My goal is to go back to working on commissions, but I feel I'm not quite in the headspace to tackle them just yet... Nevertheless, I hope you enjoy this! + Finally got around to finishing a story ever since I worked on [Crossing Over](/games/crossing-over)! I wanna get back into writing more stuff again, and this short story has finally broken my writer's block. My goal is to go back to working on commissions, but I feel I'm not quite in the headspace to tackle them just yet... Nevertheless, I hope you enjoy this! descriptionPlaintext: > Kolo's day at the airship is nearly over, but a tiny stalker will unwittingly make his evening quite eventful... diff --git a/src/i18n/index.ts b/src/i18n/index.ts index f11fa95..b8e1532 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -2,8 +2,10 @@ import { type Lang } from "../content/config"; export const DEFAULT_LANG = "eng" satisfies Lang; -export type TranslationRecord = { [DEFAULT_LANG]: string | ((...args: any[]) => string) } & { - [L in Exclude<Lang, typeof DEFAULT_LANG>]?: string | ((...args: any[]) => string); +type Translation = string | ((...args: any[]) => string); + +export type TranslationRecord = { [DEFAULT_LANG]: Translation } & { + [L in Exclude<Lang, typeof DEFAULT_LANG>]?: Translation; }; export const UI_STRINGS: Record<string, TranslationRecord> = { @@ -21,10 +23,20 @@ export const UI_STRINGS: Record<string, TranslationRecord> = { tok: (authorsList: string[]) => `lipu ni li tan jan ni: ${authorsList.join(" en ")}`, }, "export_story/request_for": { - eng: (requester: string) => `Request for: ${requester}`, + eng: (requesterList: string | string[]) => { + if (typeof requesterList === "string") { + requesterList = [requesterList]; + } + return `Request for: ${requesterList.join(" ")}`; + }, }, "export_story/commissioned_by": { - eng: (commissioner: string) => `Commissioned by: ${commissioner}`, + eng: (commissionerList: string | string[]) => { + if (typeof commissionerList === "string") { + commissionerList = [commissionerList]; + } + return `Commissioned by: ${commissionerList.join(" ")}`; + }, }, "story/return_to_stories": { eng: "Return to stories", @@ -86,10 +98,20 @@ export const UI_STRINGS: Record<string, TranslationRecord> = { : `lipu ni li tan ${authorsList[0]}`, }, "story/commissioned_by": { - eng: (arg: string) => `Commissioned by ${arg}`, + eng: (commissionersList: string | string[]) => { + if (typeof commissionersList === "string") { + commissionersList = [commissionersList]; + } + return `Commissioned by ${(UI_STRINGS["util/join_names"]!.eng as (arg: string[]) => string)(commissionersList)}`; + }, }, "story/requested_by": { - eng: (arg: string) => `Requested by ${arg}`, + eng: (requestersList: string | string[]) => { + if (typeof requestersList === "string") { + requestersList = [requestersList]; + } + return `Requested by ${(UI_STRINGS["util/join_names"]!.eng as (arg: string[]) => string)(requestersList)}`; + }, }, "story/draft_warning": { eng: "DRAFT VERSION – DO NOT REDISTRIBUTE", @@ -146,6 +168,7 @@ export function t(lang: Lang, stringOrSource: string | TranslationRecord, ...arg } return translation; } - console.warn(`No translation map found for "${stringOrSource}"`); - return stringOrSource; + // console.warn(`No translation map found for "${stringOrSource}"`); + // return stringOrSource; + throw new Error(`No translation map found for "${stringOrSource}"`); } diff --git a/src/layouts/GameLayout.astro b/src/layouts/GameLayout.astro index 96c443b..52d3201 100644 --- a/src/layouts/GameLayout.astro +++ b/src/layouts/GameLayout.astro @@ -129,7 +129,7 @@ const thumbnail = </div> <main class="mx-auto max-w-3xl rounded-lg bg-stone-50 px-2 pb-4 pt-1 shadow-sm dark:bg-stone-900 print:max-w-full print:bg-none print:shadow-none" - data-pagefind-body + data-pagefind-body={props.isDraft ? undefined : ""} data-pagefind-meta="type:game" > <h1 id="game-title" class="px-2 pt-2 font-serif text-3xl font-semibold text-stone-800 dark:text-stone-100"> diff --git a/src/layouts/StoryLayout.astro b/src/layouts/StoryLayout.astro index a86cbe1..c4e0554 100644 --- a/src/layouts/StoryLayout.astro +++ b/src/layouts/StoryLayout.astro @@ -6,6 +6,8 @@ import { slug } from "github-slugger"; import { DEFAULT_LANG, t } from "../i18n"; import BaseLayout from "./BaseLayout.astro"; import Authors from "../components/Authors.astro"; +import Commissioners from "../components/Commissioners.astro"; +import Requesters from "../components/Requesters.astro"; import UserComponent from "../components/UserComponent.astro"; import CopyrightedCharacters from "../components/CopyrightedCharacters.astro"; import Prose from "../components/Prose.astro"; @@ -69,7 +71,10 @@ const thumbnail = <BaseLayout pageTitle={props.title}> <Fragment slot="head"> <meta property="og:title" content={props.title} data-pagefind-meta="title[content]" /> - <meta property="og:description" content={`Word count: ${props.wordCount}. ${props.contentWarning}`} /> + <meta + property="og:description" + content={t(props.lang, "story/warnings", props.wordCount, props.contentWarning.trim())} + /> <meta property="og:url" content={Astro.url} data-pagefind-meta="url[content]" /> { thumbnail ? ( @@ -141,7 +146,7 @@ const thumbnail = </div> <main class="mx-auto max-w-3xl rounded-lg bg-stone-50 px-2 pb-4 pt-1 shadow-sm dark:bg-stone-900 print:max-w-full print:bg-none print:shadow-none" - data-pagefind-body + data-pagefind-body={props.isDraft ? undefined : ""} data-pagefind-meta="type:story" > { @@ -198,16 +203,16 @@ const thumbnail = } { commissioner && ( - <p id="commissioner"> - Commissioned by <UserComponent user={commissioner} lang={props.lang} /> - </p> + <Commissioners lang={props.lang}> + <UserComponent user={commissioner} lang={props.lang} /> + </Commissioners> ) } { requester && ( - <p id="requester"> - Requested by <UserComponent user={requester} lang={props.lang} /> - </p> + <Requesters lang={props.lang}> + <UserComponent user={requester} lang={props.lang} /> + </Requesters> ) } <div id="content-warning"> diff --git a/src/pages/404.astro b/src/pages/404.astro new file mode 100644 index 0000000..b59e487 --- /dev/null +++ b/src/pages/404.astro @@ -0,0 +1,9 @@ +--- +import GalleryLayout from "../layouts/GalleryLayout.astro"; +--- + +<GalleryLayout pageTitle="Gallery"> + <meta slot="head-description" property="og:description" content="Not found" /> + <h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">404 – Not Found</h1> + <p class="my-4">The requested link couldn't be found. Make sure that the URL is correct.</p> +</GalleryLayout> diff --git a/src/pages/api/export-story/[...slug].ts b/src/pages/api/export-story/[...slug].ts index aff8832..082d11a 100644 --- a/src/pages/api/export-story/[...slug].ts +++ b/src/pages/api/export-story/[...slug].ts @@ -1,8 +1,8 @@ -import { type APIRoute, type GetStaticPaths } from "astro"; +import type { APIRoute, GetStaticPaths } from "astro"; import { getCollection, getEntry, type CollectionEntry, getEntries } from "astro:content"; import { marked, type RendererApi } from "marked"; import { decode as tinyDecode } from "tiny-decode"; -import { type Lang, type Website } from "../../../content/config"; +import type { Lang, Website } from "../../../content/config"; import { t } from "../../../i18n"; type ExportFormat = "bbcode" | "markdown"; @@ -252,9 +252,9 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) = ) .join("\n\n") .replaceAll( - /\[([^\]]+)\]\((\.[^\)]+)\)/g, + /\[([^\]]+)\]\((\/[^\)]+)\)/g, (_, group1, group2) => - `[${group1}](${new URL(group2, new URL(`/stories/${story.slug}`, site)).toString()})`, + `[${group1}](${new URL(group2, site).toString()})`, ); if (exportFormat === "bbcode") { return [ diff --git a/src/pages/games/[...slug].astro b/src/pages/games/[...slug].astro index 3c8d7bd..4e3c03b 100644 --- a/src/pages/games/[...slug].astro +++ b/src/pages/games/[...slug].astro @@ -3,14 +3,19 @@ import type { GetStaticPaths } from "astro"; import { type CollectionEntry, getCollection } from "astro:content"; import GameLayout from "../../layouts/GameLayout.astro"; +type Props = CollectionEntry<"games">; + +type Params = { + slug: CollectionEntry<"games">["slug"]; +}; + export const getStaticPaths: GetStaticPaths = async () => { const games = await getCollection("games"); return games.map((game) => ({ - params: { slug: game.slug }, - props: game, + params: { slug: game.slug } satisfies Params, + props: game satisfies Props, })); }; -type Props = CollectionEntry<"games">; const game = Astro.props; const { Content } = await game.render(); diff --git a/src/pages/stories/[...slug].astro b/src/pages/stories/[...slug].astro index d66aff7..49a15ff 100644 --- a/src/pages/stories/[...slug].astro +++ b/src/pages/stories/[...slug].astro @@ -3,14 +3,19 @@ import type { GetStaticPaths } from "astro"; import { type CollectionEntry, getCollection } from "astro:content"; import StoryLayout from "../../layouts/StoryLayout.astro"; +type Props = CollectionEntry<"stories">; + +type Params = { + slug: CollectionEntry<"stories">["slug"]; +}; + export const getStaticPaths: GetStaticPaths = async () => { const stories = await getCollection("stories"); return stories.map((story) => ({ - params: { slug: story.slug }, - props: story, + params: { slug: story.slug } satisfies Params, + props: story satisfies Props, })); }; -type Props = CollectionEntry<"stories">; const story = Astro.props; const { Content } = await story.render(); diff --git a/src/pages/stories/[page].astro b/src/pages/stories/[page].astro index 46d81eb..b4001cd 100644 --- a/src/pages/stories/[page].astro +++ b/src/pages/stories/[page].astro @@ -5,15 +5,15 @@ import { getCollection } from "astro:content"; import GalleryLayout from "../../layouts/GalleryLayout.astro"; import type { CollectionEntry } from "astro:content"; +type Props = { + page: Page<CollectionEntry<"stories">>; +}; + export const getStaticPaths: GetStaticPaths = async ({ paginate }) => { const stories = (await getCollection("stories", (story) => !story.data.isDraft)).sort( (a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(), ); - return paginate(stories, { pageSize: 30 }); -}; - -type Props = { - page: Page<CollectionEntry<"stories">>; + return paginate(stories, { pageSize: 30 }) satisfies { props: Props }[]; }; const { page } = Astro.props; @@ -46,7 +46,10 @@ const totalPages = Math.ceil(page.total / page.size); {p + 1} </span> ) : ( - <a class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" href={`./${p + 1}`}> + <a + class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" + href={page.url.current.replace(`/${page.currentPage}`, `/${p + 1}`)} + > {p + 1} </a> ), @@ -102,7 +105,10 @@ const totalPages = Math.ceil(page.total / page.size); {p + 1} </span> ) : ( - <a class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" href={`./${p + 1}`}> + <a + class="text-link border-r border-stone-400 px-2 py-1 underline dark:border-stone-500" + href={page.url.current.replace(`/${page.currentPage}`, `/${p + 1}`)} + > {p + 1} </a> ), diff --git a/src/pages/tags/[slug].astro b/src/pages/tags/[slug].astro index e89e8af..673f7c5 100644 --- a/src/pages/tags/[slug].astro +++ b/src/pages/tags/[slug].astro @@ -5,6 +5,16 @@ import { type CollectionEntry, getCollection } from "astro:content"; import { slug } from "github-slugger"; import GalleryLayout from "../../layouts/GalleryLayout.astro"; +type Props = { + tag: string; + stories: CollectionEntry<"stories">[]; + games: CollectionEntry<"games">[]; +}; + +type Params = { + slug: string; +}; + export const getStaticPaths: GetStaticPaths = async () => { const [stories, games] = await Promise.all([getCollection("stories"), getCollection("games")]); const tags = new Set<string>(); @@ -21,7 +31,7 @@ export const getStaticPaths: GetStaticPaths = async () => { return [...tags] .filter((tag) => !["The Lost of the Marshes"].includes(tag)) .map((tag) => ({ - params: { slug: slug(tag) }, + params: { slug: slug(tag) } satisfies Params, props: { tag, stories: stories @@ -30,16 +40,10 @@ export const getStaticPaths: GetStaticPaths = async () => { games: games .filter((game) => !game.data.isDraft && game.data.tags.includes(tag)) .sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()), - }, + } satisfies Props, })); }; -type Props = { - tag: string; - stories: CollectionEntry<"stories">[]; - games: CollectionEntry<"games">[]; -}; - const { tag, stories, games } = Astro.props; const count = stories.length + games.length; let tagDescription: string = "";