Add Mastodon links to new stories, add title texts, and improve tags
- Added Mastodon links to "Woofer Exploration" and "Rose's Binge" - Add title texts to stories and games thumbnails on index, stories, games, and tag pages - Add descriptions and related tags to tag pages
This commit is contained in:
parent
efcfce1e06
commit
a713adc1ec
34 changed files with 493 additions and 239 deletions
package-lock.jsonpackage.jsontailwind.config.mjs
public
src
components
content
i18n
layouts
pages
utils
13
package-lock.json
generated
13
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "gallery-badmanners-xyz",
|
"name": "gallery-badmanners-xyz",
|
||||||
"version": "1.5.0",
|
"version": "1.5.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "gallery-badmanners-xyz",
|
"name": "gallery-badmanners-xyz",
|
||||||
"version": "1.5.0",
|
"version": "1.5.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.5.10",
|
"@astrojs/check": "^0.5.10",
|
||||||
"@astrojs/rss": "^4.0.5",
|
"@astrojs/rss": "^4.0.5",
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
"@tailwindcss/typography": "^0.5.12",
|
"@tailwindcss/typography": "^0.5.12",
|
||||||
"@types/sanitize-html": "^2.11.0",
|
"@types/sanitize-html": "^2.11.0",
|
||||||
"astro": "^4.5.16",
|
"astro": "^4.5.16",
|
||||||
"astro-pagefind": "^1.5.0",
|
"astro-pagefind": "^1.6.0",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"marked": "^12.0.1",
|
"marked": "^12.0.1",
|
||||||
"pagefind": "^1.1.0",
|
"pagefind": "^1.1.0",
|
||||||
|
@ -1800,9 +1800,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/astro-pagefind": {
|
"node_modules/astro-pagefind": {
|
||||||
"version": "1.5.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/astro-pagefind/-/astro-pagefind-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/astro-pagefind/-/astro-pagefind-1.6.0.tgz",
|
||||||
"integrity": "sha512-CN7Afe9qW640U1qliCQMXN259Dl6VPUnl8FneLfKE7STV4HLiif4PbNZejylC6yXYyP6uyNVDHNOfJfBBW5h6A==",
|
"integrity": "sha512-U/WuE0ktkZkoFJf6yopWO4DjIJ3+wrnopE2L3kUYiyqNTJpqmp13bFLR8gir6B+KzQ5dsXQtJZYTQtKJg1FxIA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pagefind/default-ui": "^1.0.3",
|
"@pagefind/default-ui": "^1.0.3",
|
||||||
"pagefind": "^1.0.3",
|
"pagefind": "^1.0.3",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "gallery-badmanners-xyz",
|
"name": "gallery-badmanners-xyz",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.5.0",
|
"version": "1.5.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
"@tailwindcss/typography": "^0.5.12",
|
"@tailwindcss/typography": "^0.5.12",
|
||||||
"@types/sanitize-html": "^2.11.0",
|
"@types/sanitize-html": "^2.11.0",
|
||||||
"astro": "^4.5.16",
|
"astro": "^4.5.16",
|
||||||
"astro-pagefind": "^1.5.0",
|
"astro-pagefind": "^1.6.0",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"marked": "^12.0.1",
|
"marked": "^12.0.1",
|
||||||
"pagefind": "^1.1.0",
|
"pagefind": "^1.1.0",
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
ErrorDocument 404 /404.html
|
ErrorDocument 404 /404.html
|
||||||
|
RedirectMatch 301 ^/stories(\/(index.html)?)?$ /stories/1/
|
||||||
|
|
|
@ -53,7 +53,7 @@ const { instance, user, postId } = Astro.props;
|
||||||
<span class="mr-1" data-publish-date></span>
|
<span class="mr-1" data-publish-date></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div data-content class="prose-a:text-link prose prose-story my-1 dark:prose-invert prose-img:my-0"></div>
|
<div data-content class="prose-a:text-link prose-story prose my-1 dark:prose-invert prose-img:my-0"></div>
|
||||||
<div class="ml-1 flex flex-row pb-2 pt-1">
|
<div class="ml-1 flex flex-row pb-2 pt-1">
|
||||||
<div class="flex" aria-label="Favorites">
|
<div class="flex" aria-label="Favorites">
|
||||||
<span data-favorites></span>
|
<span data-favorites></span>
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="prose-a:text-link prose prose-story max-w-none dark:prose-invert">
|
<div class="prose-a:text-link prose prose-bm max-w-none dark:prose-invert">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -185,10 +185,14 @@ const tagCategoriesCollection = defineCollection({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
index: z.number().int(),
|
index: z.number().int(),
|
||||||
tags: z.array(
|
tags: z.array(
|
||||||
z.union([
|
z.object({
|
||||||
z.string(),
|
name: z.union([
|
||||||
z.record(lang, z.string()).refine((tag) => "eng" in tag, `object-formatted tag must have an "eng" key`),
|
z.string(),
|
||||||
]),
|
z.record(lang, z.string()).refine((tag) => "eng" in tag, `object-formatted tag must have an "eng" key`),
|
||||||
|
]),
|
||||||
|
description: z.string().optional(),
|
||||||
|
related: z.array(z.string()).optional(),
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,9 +35,9 @@ platforms:
|
||||||
posts:
|
posts:
|
||||||
eka: https://aryion.com/g4/view/986297
|
eka: https://aryion.com/g4/view/986297
|
||||||
furaffinity: https://www.furaffinity.net/view/55712675/
|
furaffinity: https://www.furaffinity.net/view/55712675/
|
||||||
weasyl: https://www.weasyl.com/~badmanners/submissions/2356092/crossing-over-vore-game
|
|
||||||
inkbunny: https://inkbunny.net/s/3262911
|
inkbunny: https://inkbunny.net/s/3262911
|
||||||
sofurry: https://www.sofurry.com/view/2109688
|
sofurry: https://www.sofurry.com/view/2109688
|
||||||
|
weasyl: https://www.weasyl.com/~badmanners/submissions/2356092/crossing-over-vore-game
|
||||||
mastodon: https://meow.social/@BadManners/112009918919441027
|
mastodon: https://meow.social/@BadManners/112009918919441027
|
||||||
tags:
|
tags:
|
||||||
- oral vore
|
- oral vore
|
||||||
|
|
|
@ -10,6 +10,13 @@ description: |
|
||||||
A seemingly insatiable plushie makes short work of an entire town, and the last few people left will try their best to escape from their binge.
|
A seemingly insatiable plushie makes short work of an entire town, and the last few people left will try their best to escape from their binge.
|
||||||
|
|
||||||
This is a commission for Dee! As a disclaimer, all characters, implied or otherwise, are adults.
|
This is a commission for Dee! As a disclaimer, all characters, implied or otherwise, are adults.
|
||||||
|
posts:
|
||||||
|
eka: https://aryion.com/g4/view/1030875
|
||||||
|
furaffinity: https://www.furaffinity.net/view/57459926/
|
||||||
|
inkbunny: https://inkbunny.net/s/3377727
|
||||||
|
sofurry: https://www.sofurry.com/view/2156241
|
||||||
|
weasyl: https://www.weasyl.com/~badmanners/submissions/2396279/c-rose-s-binge
|
||||||
|
mastodon: https://meow.social/@BadManners/112827108738415986
|
||||||
tags:
|
tags:
|
||||||
- oral vore
|
- oral vore
|
||||||
- cock vore
|
- cock vore
|
||||||
|
|
|
@ -17,10 +17,10 @@ descriptionPlaintext: >
|
||||||
posts:
|
posts:
|
||||||
eka: https://aryion.com/g4/view/994229
|
eka: https://aryion.com/g4/view/994229
|
||||||
furaffinity: https://www.furaffinity.net/view/56026627/
|
furaffinity: https://www.furaffinity.net/view/56026627/
|
||||||
weasyl: https://www.weasyl.com/~badmanners/submissions/2363560/tiny-accident
|
|
||||||
inkbunny: https://inkbunny.net/s/3283508
|
inkbunny: https://inkbunny.net/s/3283508
|
||||||
mastodon: https://meow.social/@BadManners/112157812554023271
|
|
||||||
sofurry: https://www.sofurry.com/view/2118138
|
sofurry: https://www.sofurry.com/view/2118138
|
||||||
|
weasyl: https://www.weasyl.com/~badmanners/submissions/2363560/tiny-accident
|
||||||
|
mastodon: https://meow.social/@BadManners/112157812554023271
|
||||||
tags:
|
tags:
|
||||||
- anthro predator
|
- anthro predator
|
||||||
- anthro prey
|
- anthro prey
|
||||||
|
|
|
@ -25,6 +25,7 @@ tags:
|
||||||
- willing predator
|
- willing predator
|
||||||
- unwilling prey
|
- unwilling prey
|
||||||
- flash fiction
|
- flash fiction
|
||||||
|
- toki pona
|
||||||
summary: |
|
summary: |
|
||||||
Lijan, a red nondescript mammal, makes his way through town to a new 'tomo moku' ("eating place"), expecting it to be a restaurant. They can't find the building among the similar looking ones, and spot a colorful and spooked bird covered in some liquid, who they ask for directions.
|
Lijan, a red nondescript mammal, makes his way through town to a new 'tomo moku' ("eating place"), expecting it to be a restaurant. They can't find the building among the similar looking ones, and spot a colorful and spooked bird covered in some liquid, who they ask for directions.
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,13 @@ descriptionPlaintext: >
|
||||||
The Director wakes up in the middle of the night to a little intruder, and decides to have some fun with him.
|
The Director wakes up in the middle of the night to a little intruder, and decides to have some fun with him.
|
||||||
|
|
||||||
This was a gift for my friend Hans! This story is sort of a non-canon sequel to his game "Director Explorer" on Itch.io, which you should definitely try out if you haven't already!
|
This was a gift for my friend Hans! This story is sort of a non-canon sequel to his game "Director Explorer" on Itch.io, which you should definitely try out if you haven't already!
|
||||||
|
posts:
|
||||||
|
eka: https://aryion.com/g4/view/1030872
|
||||||
|
furaffinity: https://www.furaffinity.net/view/57459921/
|
||||||
|
inkbunny: https://inkbunny.net/s/3377722
|
||||||
|
sofurry: https://www.sofurry.com/view/2156237
|
||||||
|
weasyl: https://www.weasyl.com/~badmanners/submissions/2396278/woofer-exploration
|
||||||
|
mastodon: https://meow.social/@BadManners/112827104119035982
|
||||||
tags:
|
tags:
|
||||||
- Sam Brendan
|
- Sam Brendan
|
||||||
- oral vore
|
- oral vore
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
name: Body types
|
name: Body types
|
||||||
index: 1
|
index: 1
|
||||||
tags:
|
tags:
|
||||||
- anthro predator
|
- name: anthro predator
|
||||||
- feral predator
|
description: Scenarios where at least one of the predators is an anthropomorphic animal, i.e. generally regarded as a "furry".
|
||||||
- taur predator
|
- name: feral predator
|
||||||
- eng: ambiguous predator
|
description: Scenarios where at least one of the predators is an animal based on a real or mythological creature.
|
||||||
tok: sijelo pi jan pi wawa mute li ale
|
- name: taur predator
|
||||||
- human prey
|
description: Scenarios where at least one of the predators is a multi-legged centaur-like creature, with an animal lower body and anthropomorphic upper body.
|
||||||
- anthro prey
|
- name: { eng: ambiguous predator, tok: sijelo pi jan pi wawa mute li ale }
|
||||||
- feral prey
|
description: Scenarios where the body type of at least one of the predators is left ambiguous.
|
||||||
- eng: ambiguous prey
|
- name: human prey
|
||||||
tok: sijelo pi jan pi wawa lili li ale
|
description: Scenarios where at least one of the prey is a human person.
|
||||||
|
- name: anthro prey
|
||||||
|
description: Scenarios where at least one of the prey is an anthropomorphic animal, i.e. generally regarded as a "furry".
|
||||||
|
- name: feral prey
|
||||||
|
description: Scenarios where at least one of the predators is an animal based on a real or mythological creature.
|
||||||
|
- name: { eng: ambiguous prey, tok: sijelo pi jan pi wawa lili li ale }
|
||||||
|
description: Scenarios where the body type of at least one of the predators is left ambiguous.
|
||||||
|
|
|
@ -1,15 +1,31 @@
|
||||||
name: Genders
|
name: Genders
|
||||||
index: 2
|
index: 2
|
||||||
tags:
|
tags:
|
||||||
- male predator
|
- name: male predator
|
||||||
- trans male predator
|
description: Scenarios where at least one of the predators is a man and/or male-presenting.
|
||||||
- female predator
|
related:
|
||||||
- non-binary predator
|
- trans male predator
|
||||||
- eng: ambiguous gender predator
|
- name: trans male predator
|
||||||
tok: jan pi wawa mute li meli anu mije
|
description: Scenarios where at least one of the predators is a man and/or male-presenting; more specifically, undergoing or or having undergone gender transition.
|
||||||
- male prey
|
related:
|
||||||
- female prey
|
- male predator
|
||||||
- trans female prey
|
- name: female predator
|
||||||
- non-binary prey
|
description: Scenarios where at least one of the predators is a woman and/or female-presenting.
|
||||||
- eng: ambiguous gender prey
|
- name: non-binary predator
|
||||||
tok: jan pi wawa lili li meli anu mije
|
description: Scenarios where at least one of the predators has a non-binary gender expression, be they genderless/agender, intersex, androgynous, gender-fluid, non-binary and/or non-binary-presenting, et cetera, regardless of undergoing or having undergone gender transition or not.
|
||||||
|
- name: { eng: ambiguous gender predator, tok: jan pi wawa mute li meli anu mije }
|
||||||
|
description: Scenarios where the gender at least one of the predators is left ambiguous.
|
||||||
|
- name: male prey
|
||||||
|
description: Scenarios where at least one of the prey is a man and/or male-presenting.
|
||||||
|
- name: female prey
|
||||||
|
description: Scenarios where at least one of the prey is a woman and/or female-presenting.
|
||||||
|
related:
|
||||||
|
- trans female prey
|
||||||
|
- name: trans female prey
|
||||||
|
description: Scenarios where at least one of the prey is a woman and/or female-presenting; more specifically, undergoing or having undergone gender transition.
|
||||||
|
related:
|
||||||
|
- female prey
|
||||||
|
- name: non-binary prey
|
||||||
|
description: Scenarios where at least one of the predators has a non-binary gender expression, be they genderless/agender, intersex, androgynous, gender-fluid, non-binary and/or non-binary-presenting, et cetera, regardless of undergoing or having undergone gender transition or not.
|
||||||
|
- name: { eng: ambiguous gender prey, tok: jan pi wawa lili li meli anu mije }
|
||||||
|
description: Scenarios where the gender at least one of the predators is left ambiguous.
|
||||||
|
|
|
@ -1,13 +1,25 @@
|
||||||
name: Other kinks
|
name: Other kinks
|
||||||
index: 7
|
index: 7
|
||||||
tags:
|
tags:
|
||||||
- hyper
|
- name: hyper
|
||||||
- egg play
|
description: Scenarios where a character has abnormally large features compared to their body, usually genitalia.
|
||||||
- transformation
|
- name: egg play
|
||||||
- netorare
|
description: Scenarios with sexual action involving eggs.
|
||||||
- sizeplay
|
related:
|
||||||
- inflation
|
- object vore
|
||||||
- daddy play
|
- name: transformation
|
||||||
- BDSM
|
description: Scenarios where a character changes body and/or species.
|
||||||
- dubcon
|
- name: netorare
|
||||||
- plushie
|
description: Scenarios involving a character cheating on their partner with or without consent, and/or cuckoldry.
|
||||||
|
- name: sizeplay
|
||||||
|
description: Scenarios where a character changes size.
|
||||||
|
- name: inflation
|
||||||
|
description: Scenarios where a character expands out with the excessive intake of fluids, including semen.
|
||||||
|
- name: daddy play
|
||||||
|
description: Scenarios where a character refers to another as their "daddy" in a kinky manner as part of a sexual situation.
|
||||||
|
- name: BDSM
|
||||||
|
description: Scenarios related to bondage, sadism, masochism, dominance, and/or submission in a kinky manner.
|
||||||
|
- name: dubcon
|
||||||
|
description: Scenarios where sexual consent is dubious.
|
||||||
|
- name: plushie
|
||||||
|
description: Scenarios with sexual action involving stuffed toys or plushie-like characters.
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
name: Recurring characters
|
name: Recurring characters
|
||||||
index: 9
|
index: 9
|
||||||
tags:
|
tags:
|
||||||
- Sam Brendan
|
- name: Sam Brendan
|
||||||
- Beetle
|
description: Content that includes my character and fursona [Sam, the mimic x maned wolf hybrid](https://badmanners.xyz/sam_brendan).
|
||||||
- Muno
|
- name: Beetle
|
||||||
|
description: Content that includes my character [Beetle, the gryphon](https://booru.badmanners.xyz/index.php?q=post/view/3).
|
||||||
|
- name: Muno
|
||||||
|
description: Content that includes my character Muno, the snake.
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
name: Relative size
|
name: Relative size
|
||||||
index: 3
|
index: 3
|
||||||
tags:
|
tags:
|
||||||
- macro predator
|
- name: macro predator
|
||||||
- micro prey
|
description: Scenarios where at least one of the predators has a size/height one or more orders of magnitude larger than average.
|
||||||
- size difference
|
- name: micro prey
|
||||||
- similar size
|
description: Scenarios where at least one of the prey has a size/height one or more orders of magnitude smaller than average.
|
||||||
- same size
|
- name: size difference
|
||||||
- smaller predator
|
description: Scenarios where at least one of the predators has a notably larger size/height than one of their prey.
|
||||||
|
- name: similar size
|
||||||
|
description: Scenarios where at least one of the predators has a slightly larger size/height than one of their prey.
|
||||||
|
- name: same size
|
||||||
|
description: Scenarios where at least one of the predators has the same size/height as one of their prey, or their sizes are comparable.
|
||||||
|
- name: smaller predator
|
||||||
|
description: Scenarios where at least one of the predators has a notably smaller size/height than one of their prey.
|
||||||
|
related:
|
||||||
|
- role reversal
|
||||||
|
|
|
@ -1,9 +1,25 @@
|
||||||
name: Sexual content
|
name: Sexual content
|
||||||
index: 6
|
index: 6
|
||||||
tags:
|
tags:
|
||||||
- nudity
|
- name: nudity
|
||||||
- masturbation
|
description: Scenarios where a character is nude and displaying their sexual features, without necessarily participating in a sexual situation.
|
||||||
- straight sex
|
- name: masturbation
|
||||||
- gay sex
|
description: Scenarios where a character sexually stimulates themselves.
|
||||||
- lesbian sex
|
- name: straight sex
|
||||||
- orgy
|
description: Scenarios where two or more characters of different genders participate in sexual activity.
|
||||||
|
related:
|
||||||
|
- orgy
|
||||||
|
- name: gay sex
|
||||||
|
description: Scenarios where two or more characters of the same gender, both men and/or male-presenting, participate in sexual activity.
|
||||||
|
related:
|
||||||
|
- orgy
|
||||||
|
- name: lesbian sex
|
||||||
|
description: Scenarios where two or more characters of the same gender, both women and/or female-presenting, participate in sexual activity.
|
||||||
|
related:
|
||||||
|
- orgy
|
||||||
|
- name: orgy
|
||||||
|
description: Scenarios where more than two characters participate in sexual activity freely.
|
||||||
|
related:
|
||||||
|
- straight sex
|
||||||
|
- gay sex
|
||||||
|
- lesbian sex
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
name: Type of content
|
name: Type of content
|
||||||
index: 8
|
index: 8
|
||||||
tags:
|
tags:
|
||||||
- request
|
- name: request
|
||||||
- commission
|
description: Stories made as part of someone else's request as a gift.
|
||||||
- eng: flash fiction
|
- name: commission
|
||||||
tok: lipu lili
|
description: Stories made as part of a commission to someone else.
|
||||||
|
- name: { eng: flash fiction, tok: lipu lili }
|
||||||
|
description: Short-format stories of no more than 2,500 words.
|
||||||
|
- name: toki pona
|
||||||
|
description: Stories written in toki pona, the language of good.
|
||||||
|
|
|
@ -1,13 +1,21 @@
|
||||||
name: Types of vore
|
name: Types of vore
|
||||||
index: 0
|
index: 0
|
||||||
tags:
|
tags:
|
||||||
- eng: oral vore
|
- name: { eng: oral vore, tok: moku musi kepeken uta }
|
||||||
tok: moku musi kepeken uta
|
description: Scenarios where prey are consumed by the predator through their mouth.
|
||||||
- anal vore
|
- name: anal vore
|
||||||
- cock vore
|
description: Scenarios where prey are consumed by the predator through their butt/anus.
|
||||||
- unbirth
|
- name: cock vore
|
||||||
- tail vore
|
description: Scenarios where prey are consumed by the predator through their penis.
|
||||||
- slit vore
|
- name: unbirth
|
||||||
- sheath vore
|
description: Scenarios where prey are consumed by the predator through their vagina/vulva.
|
||||||
- nipple vore
|
- name: tail vore
|
||||||
- chest maw vore
|
description: Scenarios where prey are consumed by the predator through an opening or mouth in their tail.
|
||||||
|
- name: slit vore
|
||||||
|
description: Scenarios where prey are consumed by the predator through a genital slit.
|
||||||
|
- name: sheath vore
|
||||||
|
description: Scenarios where prey are consumed by the predator through a genital sheath, generally around their penis.
|
||||||
|
- name: nipple vore
|
||||||
|
description: Scenarios where prey are consumed by the predator through their nipple/breast.
|
||||||
|
- name: chest maw vore
|
||||||
|
description: Scenarios where prey are consumed by the predator through a mouth/maw on their chest cavity.
|
||||||
|
|
|
@ -1,20 +1,67 @@
|
||||||
name: Vore-related scenarios
|
name: Vore-related scenarios
|
||||||
index: 5
|
index: 5
|
||||||
tags:
|
tags:
|
||||||
- point of view
|
- name: point of view
|
||||||
- long-term endo
|
description: Scenarios where the narration takes the perspective of one of the characters, generally in first person.
|
||||||
- perma endo
|
- name: long-term endo
|
||||||
- implied perma endo
|
description: Scenarios where prey remains safely inside of their predator for an extended period of time.
|
||||||
- full tour
|
related:
|
||||||
- implied full tour
|
- perma endo
|
||||||
- regurgitation
|
- implied perma endo
|
||||||
- implied regurgitation
|
- name: perma endo
|
||||||
- prey transfer
|
description: Scenarios where prey remains safely inside of their predator for an indefinite period of time.
|
||||||
- object vore
|
related:
|
||||||
- role reversal
|
- long-term endo
|
||||||
- nested vore
|
- implied perma endo
|
||||||
- multiple prey
|
- name: implied perma endo
|
||||||
- messy stomach
|
description: Scenarios where prey is implied to remain safely inside of their predator for an indefinite period of time.
|
||||||
- hammerspace vore
|
related:
|
||||||
- bladder vore
|
- long-term endo
|
||||||
- soul vore
|
- perma endo
|
||||||
|
- name: full tour
|
||||||
|
description: Scenarios where prey passes all the way through their predator's gastrointestinal system, entering through one end and exiting through the opposite one.
|
||||||
|
related:
|
||||||
|
- implied full tour
|
||||||
|
- name: implied full tour
|
||||||
|
description: Scenarios where prey is implied to pass all the way through their predator's gastrointestinal system, entering through one end and supposedly exiting through the opposite one.
|
||||||
|
related:
|
||||||
|
- full tour
|
||||||
|
- name: regurgitation
|
||||||
|
description: Scenarios where prey are explicitly let out through the same spot they entered their predator, specifically their mouth.
|
||||||
|
related:
|
||||||
|
- implied regurgitation
|
||||||
|
- oral vore
|
||||||
|
- name: implied regurgitation
|
||||||
|
description: Scenarios where prey are implied to have been let out through the same spot they entered their predator, specifically their mouth.
|
||||||
|
related:
|
||||||
|
- regurgitation
|
||||||
|
- oral vore
|
||||||
|
- name: prey transfer
|
||||||
|
description: Scenarios where prey are transferred from one predator into another, or transferred from one part of their predator to another.
|
||||||
|
- name: object vore
|
||||||
|
description: Scenarios where predators consume objects, alongside their prey or in place of them.
|
||||||
|
- name: role reversal
|
||||||
|
description: Scenarios where predators become prey and vice-versa, where dominant characters become prey, or where submissive characters become predators.
|
||||||
|
related:
|
||||||
|
- smaller predator
|
||||||
|
- name: nested vore
|
||||||
|
description: Scenarios where a predator becomes prey to another predator, or a prey consumes another prey.
|
||||||
|
related:
|
||||||
|
- multiple prey
|
||||||
|
- role reversal
|
||||||
|
- name: multiple prey
|
||||||
|
description: Scenarios where one predator consumes multiple prey at once.
|
||||||
|
- name: messy stomach
|
||||||
|
description: Scenarios where a prey visits a messy stomach, usually with semi-digested food inside.
|
||||||
|
related:
|
||||||
|
- oral vore
|
||||||
|
- name: hammerspace vore
|
||||||
|
description: Scenarios where a predator's body doesn't grow proportionally around the prey they consume, either at a substantially lower rate or not at all.
|
||||||
|
related:
|
||||||
|
- multiple prey
|
||||||
|
- name: bladder vore
|
||||||
|
description: Scenarios where prey consumed through the predator's urethra end up in their bladder.
|
||||||
|
related:
|
||||||
|
- cock vore
|
||||||
|
- name: soul vore
|
||||||
|
description: Scenarios where predators consume a soul instead of their prey's body.
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
name: Willingness
|
name: Willingness
|
||||||
index: 4
|
index: 4
|
||||||
tags:
|
tags:
|
||||||
- eng: willing predator
|
- name: { eng: willing predator, tok: jan pi wawa mute li wile e moku musi }
|
||||||
tok: jan pi wawa mute li wile e moku musi
|
description: Scenarios where at least one of the predators participates in vore willingly.
|
||||||
- semi-willing predator
|
- name: semi-willing predator
|
||||||
- unwilling predator
|
description: Scenarios where the willingness of at least one of the predators might be partial, oscillating between willing and unwilling, somewhere in-between, or ambiguous.
|
||||||
- asleep predator
|
- name: unwilling predator
|
||||||
- willing prey
|
description: Scenarios where at least one of the predators participates in vore unwillingly.
|
||||||
- semi-willing prey
|
- name: asleep predator
|
||||||
- eng: unwilling prey
|
description: Scenarios where at least one of the predators participates in vore while asleep.
|
||||||
tok: jan pi wawa lili li wile ala e moku musi
|
- name: willing prey
|
||||||
- asleep prey
|
description: Scenarios where at least one of the prey participates in vore willingly.
|
||||||
|
- name: semi-willing prey
|
||||||
|
description: Scenarios where the willingness of at least one of the prey might be partial, oscillating between willing and unwilling, somewhere in-between, or ambiguous.
|
||||||
|
- name: { eng: unwilling prey, tok: jan pi wawa lili li wile ala e moku musi }
|
||||||
|
description: Scenarios where at least one of the prey participates in vore unwillingly.
|
||||||
|
- name: asleep prey
|
||||||
|
description: Scenarios where at least one of the predators participates in vore while asleep.
|
||||||
|
|
|
@ -151,6 +151,10 @@ export const UI_STRINGS: Record<string, TranslationRecord> = {
|
||||||
"game/platform_ios": {
|
"game/platform_ios": {
|
||||||
eng: "iOS",
|
eng: "iOS",
|
||||||
},
|
},
|
||||||
|
"game/warnings": {
|
||||||
|
eng: (platforms: string[], contentWarning: string) =>
|
||||||
|
`${(UI_STRINGS["game/platforms"]!.eng as (arg: string[]) => string)(platforms)}. ${contentWarning}`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function t(lang: Lang, stringOrSource: string | TranslationRecord, ...args: any[]): string {
|
export function t(lang: Lang, stringOrSource: string | TranslationRecord, ...args: any[]): string {
|
||||||
|
|
|
@ -22,12 +22,9 @@ const copyrightedCharacters = await formatCopyrightedCharacters(props.copyrighte
|
||||||
// const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
// const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
||||||
const categorizedTags = Object.fromEntries(
|
const categorizedTags = Object.fromEntries(
|
||||||
(await getCollection("tag-categories")).flatMap((category) =>
|
(await getCollection("tag-categories")).flatMap((category) =>
|
||||||
category.data.tags.map((tag) => {
|
category.data.tags.map<[string, string]>(({ name }) =>
|
||||||
if (typeof tag === "string") {
|
typeof name === "string" ? [name, name] : [t(DEFAULT_LANG, name as any), t(props.lang, name as any)],
|
||||||
return [tag, tag];
|
),
|
||||||
}
|
|
||||||
return [t(DEFAULT_LANG, tag as any), t(props.lang, tag as any)];
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const tags = props.tags.map<[string, string]>((tag) => {
|
const tags = props.tags.map<[string, string]>((tag) => {
|
||||||
|
|
|
@ -34,12 +34,9 @@ const relatedStories = (await getEntries(props.relatedStories)).filter((story) =
|
||||||
// const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
// const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
|
||||||
const categorizedTags = Object.fromEntries(
|
const categorizedTags = Object.fromEntries(
|
||||||
(await getCollection("tag-categories")).flatMap((category) =>
|
(await getCollection("tag-categories")).flatMap((category) =>
|
||||||
category.data.tags.map((tag) => {
|
category.data.tags.map<[string, string]>(({ name }) =>
|
||||||
if (typeof tag === "string") {
|
typeof name === "string" ? [name, name] : [t(DEFAULT_LANG, name as any), t(props.lang, name as any)],
|
||||||
return [tag, tag];
|
),
|
||||||
}
|
|
||||||
return [t(DEFAULT_LANG, tag as any), t(props.lang, tag as any)];
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const tags = props.tags.map<[string, string]>((tag) => {
|
const tags = props.tags.map<[string, string]>((tag) => {
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import type { APIRoute, GetStaticPaths } from "astro";
|
import type { APIRoute, GetStaticPaths } from "astro";
|
||||||
import { getCollection, getEntry, type CollectionEntry, getEntries } from "astro:content";
|
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, Website } from "../../../content/config";
|
import type { Lang, Website } from "../../../content/config";
|
||||||
import { t } from "../../../i18n";
|
import { t } from "../../../i18n";
|
||||||
import { formatCopyrightedCharacters } from "../../../utils/format_copyrighted_characters";
|
import { formatCopyrightedCharacters } from "../../../utils/format_copyrighted_characters";
|
||||||
|
import { markdownToBbcode } from "../../../utils/markdown_to_bbcode";
|
||||||
|
|
||||||
interface ExportWebsiteInfo {
|
interface ExportWebsiteInfo {
|
||||||
website: string;
|
website: Website;
|
||||||
exportFormat: "bbcode" | "markdown";
|
exportFormat: "bbcode" | "markdown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,37 +20,6 @@ const WEBSITE_LIST = [
|
||||||
|
|
||||||
type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<{ website: infer K }> ? K : never;
|
type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<{ website: infer K }> ? K : never;
|
||||||
|
|
||||||
//type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<[infer K, DescriptionExportFormat]> ? K : never;
|
|
||||||
|
|
||||||
const bbcodeRenderer: RendererApi = {
|
|
||||||
strong: (text) => `[b]${text}[/b]`,
|
|
||||||
em: (text) => `[i]${text}[/i]`,
|
|
||||||
codespan: (code) => code,
|
|
||||||
br: () => `\n\n`,
|
|
||||||
link: (href, _, text) => `[url=${href}]${text}[/url]`,
|
|
||||||
image: (href) => `[img]${href}[/img]`,
|
|
||||||
text: (text) => text,
|
|
||||||
paragraph: (text) => `\n${text}\n`,
|
|
||||||
list: (body, ordered) => (ordered ? `\n[ol]\n${body}[/ol]\n` : `\n[ul]\n${body}[/ul]\n`),
|
|
||||||
listitem: (text) => `[li]${text}[/li]\n`,
|
|
||||||
blockquote: (quote) => `\n[quote]${quote}[/quote]\n`,
|
|
||||||
code: (code) => `\n[code]${code}[/code]\n`,
|
|
||||||
heading: (heading) => `\n${heading}\n`,
|
|
||||||
table: (header, body) => `\n[table]\n${header}${body}[/table]\n`,
|
|
||||||
tablerow: (content) => `[tr]\n${content}[/tr]\n`,
|
|
||||||
tablecell: (content, { header }) => (header ? `[th]${content}[/th]\n` : `[td]${content}[/td]\n`),
|
|
||||||
hr: () => `\n===\n`,
|
|
||||||
del: () => {
|
|
||||||
throw new Error("Not supported by bbcodeRenderer: del");
|
|
||||||
},
|
|
||||||
html: () => {
|
|
||||||
throw new Error("Not supported by bbcodeRenderer: html");
|
|
||||||
},
|
|
||||||
checkbox: () => {
|
|
||||||
throw new Error("Not supported by bbcodeRenderer: checkbox");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website): string {
|
function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website): string {
|
||||||
const link = user.data.links[website];
|
const link = user.data.links[website];
|
||||||
if (link) {
|
if (link) {
|
||||||
|
@ -147,7 +115,7 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsiteNa
|
||||||
if ("weasyl" in user.data.links) {
|
if ("weasyl" in user.data.links) {
|
||||||
return `<!~${getUsernameForWebsite(user, "weasyl").replaceAll(" ", "")}>`;
|
return `<!~${getUsernameForWebsite(user, "weasyl").replaceAll(" ", "")}>`;
|
||||||
} else if (isPreferredWebsite(user, "furaffinity")) {
|
} else if (isPreferredWebsite(user, "furaffinity")) {
|
||||||
return `<fa:${getUsernameForWebsite(user, "furaffinity")}>`;
|
return `<fa:${getUsernameForWebsite(user, "furaffinity").replaceAll("_", "")}>`;
|
||||||
} else if (isPreferredWebsite(user, "inkbunny")) {
|
} else if (isPreferredWebsite(user, "inkbunny")) {
|
||||||
return `<ib:${getUsernameForWebsite(user, "inkbunny")}>`;
|
return `<ib:${getUsernameForWebsite(user, "inkbunny")}>`;
|
||||||
} else if (isPreferredWebsite(user, "sofurry")) {
|
} else if (isPreferredWebsite(user, "sofurry")) {
|
||||||
|
@ -220,47 +188,40 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
|
||||||
const anonymousUser = await getEntry("users", "anonymous");
|
const anonymousUser = await getEntry("users", "anonymous");
|
||||||
const anonymousFallback = getNameForUser(anonymousUser, anonymousUser, lang);
|
const anonymousFallback = getNameForUser(anonymousUser, anonymousUser, lang);
|
||||||
|
|
||||||
const description: Record<ExportWebsiteName, string> = Object.fromEntries(
|
const description = Object.fromEntries(
|
||||||
await Promise.all(
|
WEBSITE_LIST.map<[ExportWebsiteName, string]>(({ website, exportFormat }) => {
|
||||||
WEBSITE_LIST.map(async ({ website, exportFormat }) => {
|
const u = (user: CollectionEntry<"users">) => getLinkForUser(user, website, anonymousFallback);
|
||||||
const u = (user: CollectionEntry<"users">) => getLinkForUser(user, website, anonymousFallback);
|
const storyDescription = (
|
||||||
const storyDescription = (
|
[
|
||||||
[
|
story.data.description,
|
||||||
story.data.description,
|
`*${t(lang, "story/warnings", story.data.wordCount || "???", story.data.contentWarning.trim())}*`,
|
||||||
`*${t(lang, "story/warnings", story.data.wordCount || "???", story.data.contentWarning.trim())}*`,
|
t(
|
||||||
t(
|
lang,
|
||||||
lang,
|
"export_story/writing",
|
||||||
"export_story/writing",
|
authorsList.map((author) => u(author)),
|
||||||
authorsList.map((author) => u(author)),
|
),
|
||||||
),
|
requester && t(lang, "export_story/request_for", u(requester)),
|
||||||
requester && t(lang, "export_story/request_for", u(requester)),
|
commissioner && t(lang, "export_story/commissioned_by", u(commissioner)),
|
||||||
commissioner && t(lang, "export_story/commissioned_by", u(commissioner)),
|
...copyrightedCharacters.map(([user, characterList]) =>
|
||||||
...copyrightedCharacters.map(([user, characterList]) =>
|
characterList[0] == ""
|
||||||
characterList[0] == ""
|
? t(lang, "characters/all_characters_are_copyrighted_by", u(user))
|
||||||
? t(lang, "characters/all_characters_are_copyrighted_by", u(user))
|
: t(lang, "characters/characters_are_copyrighted_by", u(user), characterList),
|
||||||
: t(lang, "characters/characters_are_copyrighted_by", u(user), characterList),
|
),
|
||||||
),
|
].filter((data) => data) as string[]
|
||||||
].filter((data) => data) as string[]
|
)
|
||||||
)
|
.join("\n\n")
|
||||||
.join("\n\n")
|
.replaceAll(
|
||||||
.replaceAll(
|
/\[([^\]]+)\]\((\/[^\)]+)\)/g,
|
||||||
/\[([^\]]+)\]\((\/[^\)]+)\)/g,
|
(_, group1, group2) => `[${group1}](${new URL(group2, site).toString()})`,
|
||||||
(_, group1, group2) => `[${group1}](${new URL(group2, site).toString()})`,
|
);
|
||||||
);
|
if (exportFormat === "bbcode") {
|
||||||
if (exportFormat === "bbcode") {
|
return [website, markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n").trim()];
|
||||||
return [
|
} else if (exportFormat === "markdown") {
|
||||||
website,
|
return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
|
||||||
tinyDecode(await marked.use({ renderer: bbcodeRenderer }).parse(storyDescription))
|
} else {
|
||||||
.replaceAll(/\n\n\n+/g, "\n\n")
|
throw new Error(`Unhandled export format "${exportFormat}"`);
|
||||||
.trim(),
|
}
|
||||||
];
|
})
|
||||||
}
|
|
||||||
if (exportFormat === "markdown") {
|
|
||||||
return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
|
|
||||||
}
|
|
||||||
throw new Error(`Unhandled ExportFormat "${exportFormat}"`);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const storyHeader =
|
const storyHeader =
|
||||||
|
|
|
@ -84,7 +84,7 @@ export const GET: APIRoute = async ({ site }) => {
|
||||||
pubDate: toNoonUTCDate(data.pubDate!),
|
pubDate: toNoonUTCDate(data.pubDate!),
|
||||||
link: `/games/${slug}`,
|
link: `/games/${slug}`,
|
||||||
description:
|
description:
|
||||||
`${t(data.lang, "game/platforms", data.platforms)}. ${data.contentWarning} ${data.descriptionPlaintext || data.description}`
|
`${t(data.lang, "game/warnings", data.platforms, data.contentWarning)} ${data.descriptionPlaintext || data.description}`
|
||||||
.replaceAll(/[\n ]+/g, " ")
|
.replaceAll(/[\n ]+/g, " ")
|
||||||
.trim(),
|
.trim(),
|
||||||
categories: ["game"],
|
categories: ["game"],
|
||||||
|
@ -104,8 +104,8 @@ export const GET: APIRoute = async ({ site }) => {
|
||||||
)}</p>` +
|
)}</p>` +
|
||||||
`<hr><p>${t(data.lang, "game/platforms", data.platforms)}</p>` +
|
`<hr><p>${t(data.lang, "game/platforms", data.platforms)}</p>` +
|
||||||
`<hr><p><em>${data.contentWarning.trim()}</em></p>` +
|
`<hr><p><em>${data.contentWarning.trim()}</em></p>` +
|
||||||
`<hr>${tinyDecode(await marked(body))}` +
|
`<hr>${tinyDecode(marked.parse(body) as string)}` +
|
||||||
`<hr>${tinyDecode(await marked(data.description))}`,
|
`<hr>${tinyDecode(marked.parse(data.description) as string)}`,
|
||||||
),
|
),
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { Image } from "astro:assets";
|
import { Image } from "astro:assets";
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
|
||||||
const games = (await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate)).sort(
|
const games = (await getCollection("games", (game) => !game.data.isDraft && game.data.pubDate)).sort(
|
||||||
(a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime(),
|
(a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime(),
|
||||||
|
@ -16,7 +17,11 @@ const games = (await getCollection("games", (game) => !game.data.isDraft && game
|
||||||
{
|
{
|
||||||
games.map((game) => (
|
games.map((game) => (
|
||||||
<li>
|
<li>
|
||||||
<a class="text-link hover:underline focus:underline" href={`/games/${game.slug}`}>
|
<a
|
||||||
|
class="text-link hover:underline focus:underline"
|
||||||
|
href={`/games/${game.slug}`}
|
||||||
|
title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning.trim())}
|
||||||
|
>
|
||||||
{game.data.thumbnail ? (
|
{game.data.thumbnail ? (
|
||||||
<div class="flex aspect-[630/500] max-w-[288px] justify-center">
|
<div class="flex aspect-[630/500] max-w-[288px] justify-center">
|
||||||
<Image class="m-auto" src={game.data.thumbnail} alt={`Thumbnail for ${game.data.title}`} width={288} />
|
<Image class="m-auto" src={game.data.thumbnail} alt={`Thumbnail for ${game.data.title}`} width={288} />
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { type CollectionEntry, getCollection } from "astro:content";
|
import { type CollectionEntry, getCollection } from "astro:content";
|
||||||
import { Image } from "astro:assets";
|
import { Image } from "astro:assets";
|
||||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
|
||||||
const MAX_ITEMS = 8;
|
const MAX_ITEMS = 8;
|
||||||
|
|
||||||
|
@ -10,6 +11,7 @@ interface LatestItemsEntry {
|
||||||
thumbnail: CollectionEntry<"stories">["data"]["thumbnail"];
|
thumbnail: CollectionEntry<"stories">["data"]["thumbnail"];
|
||||||
href: string;
|
href: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
altText: string;
|
||||||
pubDate: Date;
|
pubDate: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +28,7 @@ const latestItems: LatestItemsEntry[] = [
|
||||||
thumbnail: story.data.thumbnail,
|
thumbnail: story.data.thumbnail,
|
||||||
href: `/stories/${story.slug}`,
|
href: `/stories/${story.slug}`,
|
||||||
title: story.data.title,
|
title: story.data.title,
|
||||||
|
altText: t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim()),
|
||||||
pubDate: story.data.pubDate!,
|
pubDate: story.data.pubDate!,
|
||||||
})),
|
})),
|
||||||
games.map<LatestItemsEntry>((game) => ({
|
games.map<LatestItemsEntry>((game) => ({
|
||||||
|
@ -33,6 +36,7 @@ const latestItems: LatestItemsEntry[] = [
|
||||||
thumbnail: game.data.thumbnail,
|
thumbnail: game.data.thumbnail,
|
||||||
href: `/games/${game.slug}`,
|
href: `/games/${game.slug}`,
|
||||||
title: game.data.title,
|
title: game.data.title,
|
||||||
|
altText: t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning.trim()),
|
||||||
pubDate: game.data.pubDate!,
|
pubDate: game.data.pubDate!,
|
||||||
})),
|
})),
|
||||||
]
|
]
|
||||||
|
@ -63,7 +67,7 @@ const latestItems: LatestItemsEntry[] = [
|
||||||
{
|
{
|
||||||
latestItems.map((entry) => (
|
latestItems.map((entry) => (
|
||||||
<li class="break-inside-avoid">
|
<li class="break-inside-avoid">
|
||||||
<a class="text-link hover:underline focus:underline" href={entry.href}>
|
<a class="text-link hover:underline focus:underline" href={entry.href} title={entry.altText}>
|
||||||
{entry.thumbnail ? (
|
{entry.thumbnail ? (
|
||||||
<div class="flex aspect-square max-w-[192px] justify-center">
|
<div class="flex aspect-square max-w-[192px] justify-center">
|
||||||
<Image class="m-auto" src={entry.thumbnail} alt={`Thumbnail for ${entry.title}`} width={192} />
|
<Image class="m-auto" src={entry.thumbnail} alt={`Thumbnail for ${entry.title}`} width={192} />
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Image } from "astro:assets";
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||||
import type { CollectionEntry } from "astro:content";
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
page: Page<CollectionEntry<"stories">>;
|
page: Page<CollectionEntry<"stories">>;
|
||||||
|
@ -67,7 +68,11 @@ const totalPages = Math.ceil(page.total / page.size);
|
||||||
{
|
{
|
||||||
page.data.map((story) => (
|
page.data.map((story) => (
|
||||||
<li class="break-inside-avoid">
|
<li class="break-inside-avoid">
|
||||||
<a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`}>
|
<a
|
||||||
|
class="text-link hover:underline focus:underline"
|
||||||
|
href={`/stories/${story.slug}`}
|
||||||
|
title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim())}
|
||||||
|
>
|
||||||
{story.data.thumbnail ? (
|
{story.data.thumbnail ? (
|
||||||
<div class="flex aspect-square max-w-[192px] justify-center">
|
<div class="flex aspect-square max-w-[192px] justify-center">
|
||||||
<Image
|
<Image
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
import { slug } from "github-slugger";
|
import { slug } from "github-slugger";
|
||||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||||
|
import { markdownToPlaintext } from "../utils/markdown_to_plaintext";
|
||||||
|
|
||||||
const [stories, games, tagCategories] = await Promise.all([
|
const [stories, games, tagCategories] = await Promise.all([
|
||||||
getCollection("stories"),
|
getCollection("stories"),
|
||||||
|
@ -30,35 +31,43 @@ const seriesCollection = await getCollection("series");
|
||||||
});
|
});
|
||||||
|
|
||||||
const uncategorizedTagsSet = new Set(tagsSet);
|
const uncategorizedTagsSet = new Set(tagsSet);
|
||||||
const categorizedTags: Array<[string, string, string[]]> = tagCategories
|
const categorizedTags: Array<[string, string, [string, string | undefined][]]> = await Promise.all(
|
||||||
.sort((a, b) => a.data.index - b.data.index)
|
tagCategories
|
||||||
.map((category) => {
|
.sort((a, b) => a.data.index - b.data.index)
|
||||||
const tagList = category.data.tags.map((tag) => (typeof tag === "string" ? tag : tag["eng"]!));
|
.map(async (category) => {
|
||||||
tagList.forEach((tag, index) => {
|
const tagList = category.data.tags.map(({ name, description }) => {
|
||||||
if (index !== tagList.findLastIndex((val) => tag == val)) {
|
description = description && markdownToPlaintext(description).replaceAll(/\n+/g, " ").trim();
|
||||||
throw new Error(`Duplicated tag "${tag}" found in multiple tag-categories`);
|
return (typeof name === "string" ? [name, description] : [name["eng"]!, description]) as [
|
||||||
}
|
string,
|
||||||
});
|
string | undefined,
|
||||||
return [
|
];
|
||||||
category.data.name,
|
});
|
||||||
category.id,
|
tagList.forEach(([tag, _], index) => {
|
||||||
tagList.filter((tag) => {
|
if (index !== tagList.findLastIndex(([otherTag, _]) => tag == otherTag)) {
|
||||||
if (draftOnlyTagsSet.has(tag)) {
|
throw new Error(`Duplicated tag "${tag}" found in multiple tag-categories lists`);
|
||||||
console.log(`Omitting draft-only tag "${tag}"`);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
if (tagsSet.has(tag)) {
|
});
|
||||||
uncategorizedTagsSet.delete(tag);
|
return [
|
||||||
}
|
category.data.name,
|
||||||
return true;
|
category.id,
|
||||||
}),
|
tagList.filter(([tag, _]) => {
|
||||||
];
|
if (draftOnlyTagsSet.has(tag)) {
|
||||||
});
|
console.log(`Omitting draft-only tag "${tag}"`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (tagsSet.has(tag)) {
|
||||||
|
uncategorizedTagsSet.delete(tag);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if (uncategorizedTagsSet.size > 0) {
|
if (uncategorizedTagsSet.size > 0) {
|
||||||
const tagList = [...uncategorizedTagsSet];
|
const tagList = [...uncategorizedTagsSet];
|
||||||
console.log("The following tags have no category:", tagList);
|
console.warn("The following tags have no category:", tagList);
|
||||||
categorizedTags.push(["Uncategorized tags", "others", tagList]);
|
// categorizedTags.push(["Uncategorized tags", "others", tagList]);
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -92,9 +101,9 @@ if (uncategorizedTagsSet.size > 0) {
|
||||||
{category}
|
{category}
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="flex max-w-3xl flex-wrap gap-x-2 gap-y-2 px-2">
|
<ul class="flex max-w-3xl flex-wrap gap-x-2 gap-y-2 px-2">
|
||||||
{tagList.map((tag) => (
|
{tagList.map(([tag, description]) => (
|
||||||
<li class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm dark:bg-bm-600 dark:text-white">
|
<li class="rounded-full bg-bm-300 px-3 py-1 text-sm text-black shadow-sm dark:bg-bm-600 dark:text-white">
|
||||||
<a class="hover:underline focus:underline" href={`/tags/${slug(tag)}`}>
|
<a class="hover:underline focus:underline" href={`/tags/${slug(tag)}`} title={description}>
|
||||||
{tag}
|
{tag}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -2,11 +2,16 @@
|
||||||
import type { GetStaticPaths } from "astro";
|
import type { GetStaticPaths } from "astro";
|
||||||
import { Image } from "astro:assets";
|
import { Image } from "astro:assets";
|
||||||
import { type CollectionEntry, getCollection } from "astro:content";
|
import { type CollectionEntry, getCollection } from "astro:content";
|
||||||
|
import { Markdown } from "@astropub/md";
|
||||||
import { slug } from "github-slugger";
|
import { slug } from "github-slugger";
|
||||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||||
|
import Prose from "../../components/Prose.astro";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tag: string;
|
tag: string;
|
||||||
|
description?: string;
|
||||||
|
related?: string[];
|
||||||
stories: CollectionEntry<"stories">[];
|
stories: CollectionEntry<"stories">[];
|
||||||
games: CollectionEntry<"games">[];
|
games: CollectionEntry<"games">[];
|
||||||
};
|
};
|
||||||
|
@ -16,10 +21,11 @@ type Params = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = async () => {
|
export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
const [stories, games, series] = await Promise.all([
|
const [stories, games, series, tagCategories] = await Promise.all([
|
||||||
getCollection("stories"),
|
getCollection("stories"),
|
||||||
getCollection("games"),
|
getCollection("games"),
|
||||||
getCollection("series"),
|
getCollection("series"),
|
||||||
|
getCollection("tag-categories"),
|
||||||
]);
|
]);
|
||||||
const seriesTags = new Set(series.map((s) => s.data.name));
|
const seriesTags = new Set(series.map((s) => s.data.name));
|
||||||
const tags = new Set<string>();
|
const tags = new Set<string>();
|
||||||
|
@ -33,12 +39,40 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
tags.add(tag);
|
tags.add(tag);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const tagDescriptions = Object.fromEntries(
|
||||||
|
tagCategories.flatMap((category) =>
|
||||||
|
category.data.tags.reduce(
|
||||||
|
(acc, { name, description, related }) => {
|
||||||
|
if (related) {
|
||||||
|
related = related.filter((relatedTag) => {
|
||||||
|
if (relatedTag == name) {
|
||||||
|
console.warn(`Tag "${name}" should not have itself as a related tag; removing`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!tags.has(relatedTag)) {
|
||||||
|
console.warn(`Tag "${name}" has an unknown related tag "${relatedTag}"; removing`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
acc.push(
|
||||||
|
typeof name === "string" ? [name, { description, related }] : [name["eng"]!, { description, related }],
|
||||||
|
);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[] as [string, { description: string | undefined; related: string[] | undefined }][],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
return [...tags]
|
return [...tags]
|
||||||
.filter((tag) => !seriesTags.has(tag))
|
.filter((tag) => !seriesTags.has(tag))
|
||||||
.map((tag) => ({
|
.map((tag) => ({
|
||||||
params: { slug: slug(tag) } satisfies Params,
|
params: { slug: slug(tag) } satisfies Params,
|
||||||
props: {
|
props: {
|
||||||
tag,
|
tag,
|
||||||
|
description: tagDescriptions[tag].description,
|
||||||
|
related: tagDescriptions[tag].related,
|
||||||
stories: stories
|
stories: stories
|
||||||
.filter((story) => !story.data.isDraft && story.data.pubDate && story.data.tags.includes(tag))
|
.filter((story) => !story.data.isDraft && story.data.pubDate && story.data.tags.includes(tag))
|
||||||
.sort((a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime()),
|
.sort((a, b) => b.data.pubDate!.getTime() - a.data.pubDate!.getTime()),
|
||||||
|
@ -49,28 +83,43 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const { tag, stories, games } = Astro.props;
|
const { tag, description, stories, games, related } = Astro.props;
|
||||||
|
if (!description) {
|
||||||
|
console.warn(`Tag "${tag}" has no description!`);
|
||||||
|
}
|
||||||
const count = stories.length + games.length;
|
const count = stories.length + games.length;
|
||||||
let tagDescription: string = "";
|
let totalWorksWithTag: string = "";
|
||||||
if (count == 1) {
|
if (count == 1) {
|
||||||
if (stories.length == 1) {
|
if (stories.length == 1) {
|
||||||
tagDescription = `One story tagged with "${tag}".`;
|
totalWorksWithTag = `One story tagged with "${tag}".`;
|
||||||
} else if (games.length == 1) {
|
} else if (games.length == 1) {
|
||||||
tagDescription = `One game tagged with "${tag}".`;
|
totalWorksWithTag = `One game tagged with "${tag}".`;
|
||||||
}
|
}
|
||||||
} else if (stories.length == 0) {
|
} else if (stories.length == 0) {
|
||||||
tagDescription = `${games.length} games tagged with "${tag}".`;
|
totalWorksWithTag = `${games.length} games tagged with "${tag}".`;
|
||||||
} else if (games.length == 0) {
|
} else if (games.length == 0) {
|
||||||
tagDescription = `${stories.length} stories tagged with "${tag}".`;
|
totalWorksWithTag = `${stories.length} stories tagged with "${tag}".`;
|
||||||
} else {
|
} else {
|
||||||
tagDescription = `${stories.length == 1 ? "One story" : `${stories.length} stories`} and ${games.length == 1 ? "one game" : `${games.length} games`} tagged with "${tag}".`;
|
totalWorksWithTag = `${stories.length == 1 ? "One story" : `${stories.length} stories`} and ${games.length == 1 ? "one game" : `${games.length} games`} tagged with "${tag}".`;
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<GalleryLayout pageTitle={`Works tagged "${tag}"`}>
|
<GalleryLayout pageTitle={`Works tagged "${tag}"`}>
|
||||||
<meta slot="head-description" content={`Bad Manners || ${tagDescription || tag}`} property="og:description" />
|
<meta slot="head-description" content={`Bad Manners || ${totalWorksWithTag || tag}`} property="og:description" />
|
||||||
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{tag}"</h1>
|
<h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{tag}"</h1>
|
||||||
{tagDescription ? <p class="my-4">{tagDescription}</p> : null}
|
<div class="my-4">
|
||||||
|
<Prose>
|
||||||
|
{description ? <Markdown of={description} /> : null}
|
||||||
|
{
|
||||||
|
related?.length ? (
|
||||||
|
<p
|
||||||
|
set:html={`See also: ${related.map((relatedTag) => `<a href="/tags/${slug(relatedTag)}">${relatedTag}</a>`).join(", ")}.`}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
<p>{totalWorksWithTag}</p>
|
||||||
|
</Prose>
|
||||||
|
</div>
|
||||||
{
|
{
|
||||||
stories.length > 0 && (
|
stories.length > 0 && (
|
||||||
<section class="my-2" aria-labelledby="content-stories">
|
<section class="my-2" aria-labelledby="content-stories">
|
||||||
|
@ -80,7 +129,11 @@ if (count == 1) {
|
||||||
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
||||||
{stories.map((story) => (
|
{stories.map((story) => (
|
||||||
<li class="break-inside-avoid">
|
<li class="break-inside-avoid">
|
||||||
<a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`}>
|
<a
|
||||||
|
class="text-link hover:underline focus:underline"
|
||||||
|
href={`/stories/${story.slug}`}
|
||||||
|
title={t(story.data.lang, "story/warnings", story.data.wordCount, story.data.contentWarning.trim())}
|
||||||
|
>
|
||||||
{story.data.thumbnail ? (
|
{story.data.thumbnail ? (
|
||||||
<div class="flex aspect-square max-w-[192px] justify-center">
|
<div class="flex aspect-square max-w-[192px] justify-center">
|
||||||
<Image
|
<Image
|
||||||
|
@ -108,7 +161,11 @@ if (count == 1) {
|
||||||
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
<ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
|
||||||
{games.map((game) => (
|
{games.map((game) => (
|
||||||
<li class="break-inside-avoid">
|
<li class="break-inside-avoid">
|
||||||
<a class="text-link hover:underline focus:underline" href={`/games/${game.slug}`}>
|
<a
|
||||||
|
class="text-link hover:underline focus:underline"
|
||||||
|
href={`/games/${game.slug}`}
|
||||||
|
title={t(game.data.lang, "game/warnings", game.data.platforms, game.data.contentWarning.trim())}
|
||||||
|
>
|
||||||
{game.data.thumbnail ? (
|
{game.data.thumbnail ? (
|
||||||
<div class="flex aspect-[630/500] max-w-[192px] justify-center">
|
<div class="flex aspect-[630/500] max-w-[192px] justify-center">
|
||||||
<Image
|
<Image
|
||||||
|
|
33
src/utils/markdown_to_bbcode.ts
Normal file
33
src/utils/markdown_to_bbcode.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { Marked, type RendererApi } from "marked";
|
||||||
|
import { decode } from "tiny-decode";
|
||||||
|
|
||||||
|
const renderer: RendererApi = {
|
||||||
|
strong: (text) => `[b]${text}[/b]`,
|
||||||
|
em: (text) => `[i]${text}[/i]`,
|
||||||
|
del: (text) => `[s]${text}[/s]`,
|
||||||
|
codespan: (code) => code,
|
||||||
|
br: () => `\n\n`,
|
||||||
|
link: (href, _, text) => `[url=${href}]${text}[/url]`,
|
||||||
|
image: (href) => `[img]${href}[/img]`,
|
||||||
|
text: (text) => text,
|
||||||
|
paragraph: (text) => `\n${text}\n`,
|
||||||
|
list: (body, ordered) => (ordered ? `\n[ol]\n${body}[/ol]\n` : `\n[ul]\n${body}[/ul]\n`),
|
||||||
|
listitem: (text) => `[li]${text}[/li]\n`,
|
||||||
|
blockquote: (quote) => `\n[quote]${quote}[/quote]\n`,
|
||||||
|
code: (code) => `\n[code]${code}[/code]\n`,
|
||||||
|
heading: (heading) => `\n${heading}\n`,
|
||||||
|
table: (header, body) => `\n[table]\n${header}${body}[/table]\n`,
|
||||||
|
tablerow: (content) => `[tr]\n${content}[/tr]\n`,
|
||||||
|
tablecell: (content, { header }) => (header ? `[th]${content}[/th]\n` : `[td]${content}[/td]\n`),
|
||||||
|
hr: () => `\n===\n`,
|
||||||
|
html: () => {
|
||||||
|
throw new Error("Not supported by BBCode: html");
|
||||||
|
},
|
||||||
|
checkbox: () => {
|
||||||
|
throw new Error("Not supported by BBCode: checkbox");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const bbcodeRenderer = new Marked({ renderer, async: false });
|
||||||
|
|
||||||
|
export const markdownToBbcode = (text: string) => decode((bbcodeRenderer.parse(text) as string).trim());
|
35
src/utils/markdown_to_plaintext.ts
Normal file
35
src/utils/markdown_to_plaintext.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { Marked, type RendererApi } from "marked";
|
||||||
|
import { decode } from "tiny-decode";
|
||||||
|
|
||||||
|
const renderer: RendererApi = {
|
||||||
|
strong: (text) => text,
|
||||||
|
em: (text) => `_${text}_`,
|
||||||
|
del: (text) => text,
|
||||||
|
codespan: (code) => code,
|
||||||
|
br: () => `\n\n`,
|
||||||
|
link: (_href, _title, text) => text,
|
||||||
|
text: (text) => text,
|
||||||
|
paragraph: (text) => `\n${text}\n`,
|
||||||
|
list: (body) => `\n${body}\n`,
|
||||||
|
listitem: (text) => `- ${text}\n`,
|
||||||
|
blockquote: (quote) => `\n> ${quote}\n`,
|
||||||
|
code: (code) => `\n${code}\n`,
|
||||||
|
heading: (heading) => `\n${heading}\n`,
|
||||||
|
table: (header, body) => `\n${header}\n---\n${body}\n`,
|
||||||
|
tablerow: (content) => `${content.slice(0, -3)}\n`,
|
||||||
|
tablecell: (content) => `${content} | `,
|
||||||
|
hr: () => `\n***\n`,
|
||||||
|
image: () => {
|
||||||
|
throw new Error("Not supported by plaintext: img");
|
||||||
|
},
|
||||||
|
html: () => {
|
||||||
|
throw new Error("Not supported by plaintext: html");
|
||||||
|
},
|
||||||
|
checkbox: () => {
|
||||||
|
throw new Error("Not supported by plaintext: checkbox");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const plaintextRenderer = new Marked({ renderer, async: false });
|
||||||
|
|
||||||
|
export const markdownToPlaintext = (text: string) => decode((plaintextRenderer.parse(text) as string).trim());
|
|
@ -22,7 +22,7 @@ export default {
|
||||||
radial: "radial-gradient(var(--tw-gradient-stops))",
|
radial: "radial-gradient(var(--tw-gradient-stops))",
|
||||||
},
|
},
|
||||||
typography: ({ theme }) => ({
|
typography: ({ theme }) => ({
|
||||||
story: {
|
bm: {
|
||||||
css: {
|
css: {
|
||||||
"--tw-prose-body": theme("colors.stone[800]"),
|
"--tw-prose-body": theme("colors.stone[800]"),
|
||||||
"--tw-prose-headings": theme("colors.stone[900]"),
|
"--tw-prose-headings": theme("colors.stone[900]"),
|
||||||
|
|
Loading…
Add table
Reference in a new issue