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:
Bad Manners 2024-06-16 19:24:25 -03:00
parent 837433364d
commit a9d5a88d0e
26 changed files with 254 additions and 70 deletions

View file

@ -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
View 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."

View file

@ -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.

View file

@ -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: >

View file

@ -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
View file

@ -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"
},

View file

@ -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",

View file

@ -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}

View 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}

View 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}

View file

@ -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: >

View file

@ -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...

View file

@ -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.

View file

@ -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: >

View file

@ -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...

View file

@ -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.

View file

@ -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...

View file

@ -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}"`);
}

View file

@ -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">

View file

@ -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
View 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 &ndash; Not Found</h1>
<p class="my-4">The requested link couldn't be found. Make sure that the URL is correct.</p>
</GalleryLayout>

View file

@ -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 [

View file

@ -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();

View file

@ -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();

View file

@ -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>
),

View file

@ -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 = "";