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
This commit is contained in:
parent
837433364d
commit
a9d5a88d0e
26 changed files with 254 additions and 70 deletions
README.mddeploy_lftp.sh
examples
package-lock.jsonpackage.jsonsrc
components
content/stories
i18n
layouts
pages
16
README.md
16
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/`
|
||||
|
|
37
deploy_lftp.sh
Executable file
37
deploy_lftp.sh
Executable file
|
@ -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."
|
|
@ -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.
|
||||
|
|
|
@ -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: >
|
||||
|
|
|
@ -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
|
||||
|
|
64
package-lock.json
generated
64
package-lock.json
generated
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}
|
||||
|
|
15
src/components/Commissioners.astro
Normal file
15
src/components/Commissioners.astro
Normal file
|
@ -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}
|
15
src/components/Requesters.astro
Normal file
15
src/components/Requesters.astro
Normal file
|
@ -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}
|
|
@ -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: >
|
||||
|
|
|
@ -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...
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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: >
|
||||
|
|
|
@ -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...
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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...
|
||||
|
||||
|
|
|
@ -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}"`);
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
9
src/pages/404.astro
Normal file
9
src/pages/404.astro
Normal file
|
@ -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>
|
|
@ -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 [
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
),
|
||||
|
|
|
@ -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 = "";
|
||||
|
|
Loading…
Add table
Reference in a new issue