Add description exports and collapse characters from same user

This commit is contained in:
Bad Manners 2024-03-21 22:24:58 -03:00
parent 2990644f87
commit d4a9dc9dbc
78 changed files with 693 additions and 247 deletions

View file

@ -7,5 +7,5 @@
```bash ```bash
npm install npm install
npm run build npm run build
scp -r dist/ my-ssh-server:./gallery.badmanners.xyz scp -r ./dist/* my-ssh-server:./gallery.badmanners.xyz/
``` ```

27
package-lock.json generated
View file

@ -13,9 +13,12 @@
"@astrojs/tailwind": "^5.1.0", "@astrojs/tailwind": "^5.1.0",
"@astropub/md": "^0.4.0", "@astropub/md": "^0.4.0",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@types/he": "^1.2.3",
"astro": "^4.5.4", "astro": "^4.5.4",
"date-fns": "^3.5.0", "date-fns": "^3.5.0",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"he": "^1.2.0",
"marked": "^12.0.1",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
@ -1278,6 +1281,11 @@
"@types/unist": "*" "@types/unist": "*"
} }
}, },
"node_modules/@types/he": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/he/-/he-1.2.3.tgz",
"integrity": "sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA=="
},
"node_modules/@types/mdast": { "node_modules/@types/mdast": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz",
@ -3040,6 +3048,14 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"bin": {
"he": "bin/he"
}
},
"node_modules/html-escaper": { "node_modules/html-escaper": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
@ -3519,6 +3535,17 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/marked": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.1.tgz",
"integrity": "sha512-Y1/V2yafOcOdWQCX0XpAKXzDakPOpn6U0YLxTJs3cww6VxOzZV1BTOOYWLvH3gX38cq+iLwljHHTnMtlDfg01Q==",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mdast-util-definitions": { "node_modules/mdast-util-definitions": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz",

View file

@ -16,9 +16,12 @@
"@astrojs/tailwind": "^5.1.0", "@astrojs/tailwind": "^5.1.0",
"@astropub/md": "^0.4.0", "@astropub/md": "^0.4.0",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@types/he": "^1.2.3",
"astro": "^4.5.4", "astro": "^4.5.4",
"date-fns": "^3.5.0", "date-fns": "^3.5.0",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"he": "^1.2.0",
"marked": "^12.0.1",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },

View file

@ -1,9 +1,10 @@
--- ---
import { type Lang, type User } from "../content/config"; import { type CollectionEntry } from "astro:content";
import { type Lang } from "../content/config";
import UserComponent from "./UserComponent.astro"; import UserComponent from "./UserComponent.astro";
type Props = { type Props = {
authors: User | User[]; authors: CollectionEntry<"users"> | CollectionEntry<"users">[];
lang: Lang; lang: Lang;
}; };
@ -20,18 +21,19 @@ const authorsArray = [authors].flat();
by{" "} by{" "}
{authorsArray.slice(0, authorsArray.length - 1).map((author) => ( {authorsArray.slice(0, authorsArray.length - 1).map((author) => (
<Fragment> <Fragment>
<UserComponent user={author} />, <UserComponent lang="eng" user={author} />,
</Fragment> </Fragment>
))} ))}
and <UserComponent user={authorsArray[authorsArray.length - 1]} /> and <UserComponent lang="eng" user={authorsArray[authorsArray.length - 1]} />
</span> </span>
) : authorsArray.length > 1 ? ( ) : authorsArray.length > 1 ? (
<span> <span>
by <UserComponent user={authorsArray[0]} /> and <UserComponent user={authorsArray[1]} /> by <UserComponent lang="eng" user={authorsArray[0]} /> and{" "}
<UserComponent lang="eng" user={authorsArray[1]} />
</span> </span>
) : ( ) : (
<span> <span>
by <UserComponent user={authorsArray[0]} /> by <UserComponent lang="eng" user={authorsArray[0]} />
</span> </span>
))} ))}
{lang === "tok" && {lang === "tok" &&
@ -40,15 +42,15 @@ const authorsArray = [authors].flat();
lipu ni li tan ni:{" "} lipu ni li tan ni:{" "}
{authorsArray.slice(0, authorsArray.length - 1).map((author) => ( {authorsArray.slice(0, authorsArray.length - 1).map((author) => (
<Fragment> <Fragment>
<UserComponent user={author} /> <UserComponent lang="tok" user={author} />
{" en "} {" en "}
</Fragment> </Fragment>
))} ))}
<UserComponent user={authorsArray[authorsArray.length - 1]} /> <UserComponent lang="tok" user={authorsArray[authorsArray.length - 1]} />
</span> </span>
) : ( ) : (
<span> <span>
lipu ni li tan <UserComponent user={authorsArray[0]} /> lipu ni li tan <UserComponent lang="tok" user={authorsArray[0]} />
</span> </span>
))} ))}
</p> </p>

View file

@ -1,27 +1,67 @@
--- ---
import { type Lang, type User } from "../content/config"; import { type CollectionEntry } from "astro:content";
import { type Lang } from "../content/config";
import UserComponent from "./UserComponent.astro"; import UserComponent from "./UserComponent.astro";
type Props = { type Props = {
copyrightedCharacters?: Record<string, User>; copyrightedCharacters?: Record<string, CollectionEntry<"users">>;
lang: Lang; lang: Lang;
}; };
const { copyrightedCharacters, lang } = Astro.props; const { copyrightedCharacters, lang } = Astro.props;
if (copyrightedCharacters && "" in copyrightedCharacters && Object.keys(copyrightedCharacters).length > 1) {
throw new Error("copyrightedCharacter cannot use empty key (catch-all) with other keys");
}
const charactersPerUser =
copyrightedCharacters &&
Object.keys(copyrightedCharacters).reduce(
(acc, character) => {
const key = copyrightedCharacters[character].id;
if (!(key in acc)) {
acc[key] = [];
}
acc[key].push(character);
return acc;
},
{} as Record<
CollectionEntry<"users">["id"],
(typeof copyrightedCharacters extends Record<infer K, any> ? K : never)[]
>,
);
--- ---
{ {
copyrightedCharacters ? ( charactersPerUser ? (
<section id="copyrighted-characters"> <section id="copyrighted-characters">
{lang === "eng" && ( {lang === "eng" ? (
<ul> <ul>
{Object.entries(copyrightedCharacters).map(([character, user]) => ( {Object.values(charactersPerUser).map((characterList) => (
<li> <li>
{character} is &copy; <UserComponent user={user} /> {characterList[0] === "" ? (
<span>
All characters are &copy; <UserComponent lang={lang} user={copyrightedCharacters[""]} />
</span>
) : characterList.length > 2 ? (
<span>
{characterList.slice(0, characterList.length - 1).join(", ")}, and{" "}
{characterList[characterList.length - 1]} are &copy;{" "}
<UserComponent lang={lang} user={copyrightedCharacters[characterList[0]]} />
</span>
) : characterList.length > 1 ? (
<span>
{characterList[0]} and {characterList[1]} are &copy;{" "}
<UserComponent lang={lang} user={copyrightedCharacters[characterList[0]]} />
</span>
) : (
<span>
{characterList[0]} is &copy;{" "}
<UserComponent lang={lang} user={copyrightedCharacters[characterList[0]]} />
</span>
)}
</li> </li>
))} ))}
</ul> </ul>
)} ) : null}
</section> </section>
) : null ) : null
} }

View file

@ -1,21 +1,35 @@
--- ---
import { type User } from "../content/config"; import { type CollectionEntry } from "astro:content";
import { type Lang } from "../content/config";
type Props = { type Props = {
user: User; lang: Lang;
user: CollectionEntry<"users">;
}; };
const { user } = Astro.props; const { user, lang } = Astro.props;
const username = user.data.nameLang[lang] || user.data.name;
let link: string | null = null;
if (user.data.preferredLink) {
if (user.data.preferredLink in user.data.links) {
const preferredLink = user.data.links[user.data.preferredLink] as string | [string, string];
if (typeof preferredLink === "string") {
link = preferredLink;
} else {
link = preferredLink[0];
}
} else {
throw new Error(`No preferredLink "${user.data.preferredLink}" for user ${user.id}`);
}
}
--- ---
{ {
typeof user === "string" ? ( user.data.preferredLink == null ? (
<span>{user}</span> <span>{username}</span>
) : ( ) : (
Object.entries(user).map(([k, v]) => ( <a href={link} class="text-link underline" target="_blank">
<a href={v} class="text-link underline" target="_blank"> {username}
<span>{k}</span> </a>
</a>
))[0]
) )
} }

View file

@ -1,39 +1,52 @@
import { defineCollection, reference, z } from "astro:content"; import { defineCollection, reference, z } from "astro:content";
const user = z.union([z.string(), z.record(z.string().url())]); export const WEBSITE_LIST = [
"website",
"eka",
"furaffinity",
"weasyl",
"inkbunny",
"sofurry",
"twitter",
"mastodon",
"bluesky",
] as const;
const lang = z.enum(["eng", "tok"]).default("eng"); const lang = z.enum(["eng", "tok"]).default("eng");
const website = z.enum(WEBSITE_LIST);
export type User = z.output<typeof user>;
export type Lang = z.output<typeof lang>; export type Lang = z.output<typeof lang>;
export type Website = z.infer<typeof website>;
const storiesCollection = defineCollection({ const storiesCollection = defineCollection({
type: "content", type: "content",
schema: ({ image }) => schema: ({ image }) =>
z.object({ z.object({
// Required
title: z.string(), title: z.string(),
shortTitle: z.string().optional(),
pubDate: z.date(), pubDate: z.date(),
isDraft: z.boolean().default(false),
authors: z.union([user, z.array(user)]).default("Bad Manners"),
wordCount: z.number().int(), wordCount: z.number().int(),
contentWarning: z.string(), contentWarning: z.string(),
description: z.string(), description: z.string(),
tags: z.array(z.string()),
// Optional
isDraft: z.boolean().default(false),
shortTitle: z.string().optional(),
authors: z.union([reference("users"), z.array(reference("users"))]).default("bad-manners"),
descriptionPlaintext: z.string().optional(), descriptionPlaintext: z.string().optional(),
summary: z.string().optional(), summary: z.string().optional(),
thumbnail: image().optional(), thumbnail: image().optional(),
thumbnailWidth: z.number().int().optional(), thumbnailWidth: z.number().int().optional(),
thumbnailHeight: z.number().int().optional(), thumbnailHeight: z.number().int().optional(),
tags: z.array(z.string()),
series: z.record(z.string(), z.string()).optional(), series: z.record(z.string(), z.string()).optional(),
commissioner: user.optional(), commissioner: reference("users").optional(),
requester: user.optional(), requester: reference("users").optional(),
copyrightedCharacters: z.record(z.string(), user).optional(), copyrightedCharacters: z.record(z.string(), reference("users")).default({}),
lang, lang,
prev: reference("stories").nullable().optional(), prev: reference("stories").nullable().optional(),
next: reference("stories").nullable().optional(), next: reference("stories").nullable().optional(),
relatedStories: z.array(reference("stories")).optional(), relatedStories: z.array(reference("stories")).default([]),
relatedGames: z.array(reference("games")).optional(), relatedGames: z.array(reference("games")).default([]),
}), }),
}); });
@ -41,26 +54,43 @@ const gamesCollection = defineCollection({
type: "content", type: "content",
schema: ({ image }) => schema: ({ image }) =>
z.object({ z.object({
// Required
title: z.string(), title: z.string(),
pubDate: z.date(), pubDate: z.date(),
isDraft: z.boolean().default(false),
authors: z.union([user, z.array(user)]).default("Bad Manners"),
contentWarning: z.string(), contentWarning: z.string(),
description: z.string(), description: z.string(),
tags: z.array(z.string()),
// Optional
isDraft: z.boolean().default(false),
authors: z.union([reference("users"), z.array(reference("users"))]).default("bad-manners"),
descriptionPlaintext: z.string().optional(), descriptionPlaintext: z.string().optional(),
thumbnail: image().optional(), thumbnail: image().optional(),
thumbnailWidth: z.number().int().optional(), thumbnailWidth: z.number().int().optional(),
thumbnailHeight: z.number().int().optional(), thumbnailHeight: z.number().int().optional(),
tags: z.array(z.string()),
series: z.record(z.string(), z.string()).optional(), series: z.record(z.string(), z.string()).optional(),
copyrightedCharacters: z.record(z.string(), user).optional(), copyrightedCharacters: z.record(z.string(), reference("users")).default({}),
lang, lang,
relatedStories: z.array(reference("stories")).optional(), relatedStories: z.array(reference("stories")).default([]),
relatedGames: z.array(reference("games")).optional(), relatedGames: z.array(reference("games")).default([]),
}),
});
const usersCollection = defineCollection({
type: "data",
schema: ({ image }) =>
z.object({
// Required
name: z.string(),
links: z.record(website, z.union([z.string().url(), z.tuple([z.string().url(), z.string()])])),
preferredLink: website.nullable(),
// Optional
nameLang: z.record(lang, z.string()).default({}),
avatar: image().optional(),
}), }),
}); });
export const collections = { export const collections = {
stories: storiesCollection, stories: storiesCollection,
games: gamesCollection, games: gamesCollection,
users: usersCollection,
}; };

View file

@ -1,7 +1,7 @@
--- ---
title: Crossing Over title: Crossing Over
pubDate: 2024-02-28 pubDate: 2024-02-28
authors: Bad Manners authors: bad-manners
contentWarning: > contentWarning: >
This visual novel is a game about death, fishing, and vore. It contains purely fictional content deemed inappropriate for minors. It also deals with heavy subject matters like depression, abuse, and suicide, which may be unsuitable for some audiences. If you continue, you acknowledge that you're an adult, and accept responsibility for your actions. This visual novel is a game about death, fishing, and vore. It contains purely fictional content deemed inappropriate for minors. It also deals with heavy subject matters like depression, abuse, and suicide, which may be unsuitable for some audiences. If you continue, you acknowledge that you're an adult, and accept responsibility for your actions.
thumbnail: /src/assets/thumbnails/game_crossing_over_cover.png thumbnail: /src/assets/thumbnails/game_crossing_over_cover.png

View file

@ -1,7 +1,7 @@
--- ---
title: Accommodation title: Accommodation
pubDate: 2023-01-03 pubDate: 2023-01-03
authors: Bad Manners authors: bad-manners
wordCount: 4800 wordCount: 4800
contentWarning: > contentWarning: >
Contains: Non-fatal size difference anal vore, with unwilling to willing female okapi predator, unwilling to willing male gray wolf prey, and long-term endosoma. Also includes straight sexual situations. Contains: Non-fatal size difference anal vore, with unwilling to willing female okapi predator, unwilling to willing male gray wolf prey, and long-term endosoma. Also includes straight sexual situations.

View file

@ -1,7 +1,7 @@
--- ---
title: Addictive Additions title: Addictive Additions
pubDate: 2022-12-27 pubDate: 2022-12-27
authors: Bad Manners authors: bad-manners
wordCount: 11200 wordCount: 11200
contentWarning: > contentWarning: >
Contains: Non-fatal oral vore and anal vore, with willing pred and multiple consensual similar sized prey (both willing, and semi-willing to unwilling in partial vore), and implied perma endo with trait theft. Also includes consensual sexual situations (M/M, M/F), hyper, male pregnancy worship, marriage play, clothing play, and semi-public lewdness. Contains: Non-fatal oral vore and anal vore, with willing pred and multiple consensual similar sized prey (both willing, and semi-willing to unwilling in partial vore), and implied perma endo with trait theft. Also includes consensual sexual situations (M/M, M/F), hyper, male pregnancy worship, marriage play, clothing play, and semi-public lewdness.
@ -31,9 +31,9 @@ tags:
"netorare", "netorare",
"commission", "commission",
] ]
commissioner: { "Scion": "https://www.furaffinity.net/user/scionic" } commissioner: scion
copyrightedCharacters: copyrightedCharacters:
"": { "Scion": "https://www.furaffinity.net/user/scionic" } "": scion
--- ---
"A'ight, this place should be quiet enough." A resounding, confident manly voice boomed in the dark backroom. Music from a party kept playing far away, only the beats being discernible at this distance. "A'ight, this place should be quiet enough." A resounding, confident manly voice boomed in the dark backroom. Music from a party kept playing far away, only the beats being discernible at this distance.

View file

@ -1,7 +1,7 @@
--- ---
title: Annivoresary title: Annivoresary
pubDate: 2022-08-08 pubDate: 2022-08-08
authors: Bad Manners authors: bad-manners
wordCount: 3000 wordCount: 3000
contentWarning: > contentWarning: >
Contains: willing, non-fatal oral vore, with smaller male anthro fox pred, and larger male anthro wolf prey. Also includes bondage and sexual situations. Contains: willing, non-fatal oral vore, with smaller male anthro fox pred, and larger male anthro wolf prey. Also includes bondage and sexual situations.

View file

@ -1,7 +1,7 @@
--- ---
title: Better in Bully Batter title: Better in Bully Batter
pubDate: 2023-02-20 pubDate: 2023-02-20
authors: Bad Manners authors: bad-manners
wordCount: 19100 wordCount: 19100
contentWarning: > contentWarning: >
Contains: Non-fatal similar size cock vore, with willing pred, multiple unwilling/semi-willing prey, and implied perma endo with trait theft. Also includes consensual sexual situations (M/M, M/F), hyper, cock sizeplay, netorare/cuckoldry and marriage play, cum inflation and weight gain, auto-fellatio, public sexual situations, and public vore. Contains: Non-fatal similar size cock vore, with willing pred, multiple unwilling/semi-willing prey, and implied perma endo with trait theft. Also includes consensual sexual situations (M/M, M/F), hyper, cock sizeplay, netorare/cuckoldry and marriage play, cum inflation and weight gain, auto-fellatio, public sexual situations, and public vore.
@ -29,9 +29,9 @@ tags:
"netorare", "netorare",
"commission", "commission",
] ]
commissioner: { "Scion": "https://www.furaffinity.net/user/scionic" } commissioner: scion
copyrightedCharacters: copyrightedCharacters:
"": { "Scion": "https://www.furaffinity.net/user/scionic" } "": scion
--- ---
## The first day ## The first day

View file

@ -1,7 +1,7 @@
--- ---
title: Big Haul title: Big Haul
pubDate: 2023-11-20 pubDate: 2023-11-20
authors: Bad Manners authors: bad-manners
wordCount: 9100 wordCount: 9100
contentWarning: > contentWarning: >
Contains: Non-fatal size difference unbirth, with semi-willing trans male anthro lemur pred, and unwilling cis male anthro sergal prey. Also includes gay sex with trans and cis characters, fatfur, daddy play, implied long-term endosoma, booze, and rudeness. Contains: Non-fatal size difference unbirth, with semi-willing trans male anthro lemur pred, and unwilling cis male anthro sergal prey. Also includes gay sex with trans and cis characters, fatfur, daddy play, implied long-term endosoma, booze, and rudeness.

View file

@ -1,7 +1,7 @@
--- ---
title: Birdroom title: Birdroom
pubDate: 2023-05-17 pubDate: 2023-05-17
authors: Bad Manners authors: bad-manners
wordCount: 3000 wordCount: 3000
contentWarning: > contentWarning: >
Contains: non-fatal similar size anal vore, willing male feral gryphon pred, willing male anthro mimic hybrid prey, gay sex. Contains: non-fatal similar size anal vore, willing male feral gryphon pred, willing male anthro mimic hybrid prey, gay sex.
@ -31,8 +31,8 @@ tags:
"gay sex", "gay sex",
] ]
copyrightedCharacters: copyrightedCharacters:
Beetle: Bad Manners Beetle: bad-manners
"Sam Brendan": Bad Manners "Sam Brendan": bad-manners
--- ---
"Staaaaan!" The gryphon's voice squawked across rooms, as Beetle happily strutted with his lion paws. "Where areeee yooooou~?" He sang his question, letting his wagging tail follow his orange body as he checked room after room. "Oh, there you are, Stan!" "Staaaaan!" The gryphon's voice squawked across rooms, as Beetle happily strutted with his lion paws. "Where areeee yooooou~?" He sang his question, letting his wagging tail follow his orange body as he checked room after room. "Oh, there you are, Stan!"

View file

@ -1,7 +1,7 @@
--- ---
title: Bladder Filler title: Bladder Filler
pubDate: 2023-11-08 pubDate: 2023-11-08
authors: Bad Manners authors: bad-manners
wordCount: 1900 wordCount: 1900
contentWarning: > contentWarning: >
Contains: non-fatal size difference cock vore to clean bladder, with willing male feral gryphon pred, and willing 2nd person PoV feral dragon prey. Also includes implied long-term endosoma. Contains: non-fatal size difference cock vore to clean bladder, with willing male feral gryphon pred, and willing 2nd person PoV feral dragon prey. Also includes implied long-term endosoma.
@ -35,7 +35,7 @@ tags:
"flash fiction", "flash fiction",
] ]
copyrightedCharacters: copyrightedCharacters:
Beetle: Bad Manners Beetle: bad-manners
--- ---
"...You wanna go WHERE?" The gryphon looming over you squawked. It was hard to tell from his high-pitched tone if he was being incredulous, or making sure that he'd heard you right. "...You wanna go WHERE?" The gryphon looming over you squawked. It was hard to tell from his high-pitched tone if he was being incredulous, or making sure that he'd heard you right.

View file

@ -1,7 +1,7 @@
--- ---
title: Bottom of the Food Chain title: Bottom of the Food Chain
pubDate: 2023-09-27 pubDate: 2023-09-27
authors: Bad Manners authors: bad-manners
wordCount: 4500 wordCount: 4500
contentWarning: > contentWarning: >
Contains: Non-fatal size difference oral vore, with semi-willing male feral snake pred and semi-willing feral vole prey. Contains: Non-fatal size difference oral vore, with semi-willing male feral snake pred and semi-willing feral vole prey.
@ -28,7 +28,7 @@ tags:
] ]
prev: ruffling-some-feathers prev: ruffling-some-feathers
copyrightedCharacters: copyrightedCharacters:
Muno: Bad Manners Muno: bad-manners
--- ---
Dark gray plates slithered across the track. It was hard for an outside observer to tell, but the snake that the plates belonged to was in a hurry. Muno's destination was still a couple of days away, but his recent altercation with a rather hungry predator pressed him even more for time. He lamented how his mint-hued scales still smelled like owl breath even after a quick bath down the river, but he could only blame himself and he had a job to worry about instead. Dark gray plates slithered across the track. It was hard for an outside observer to tell, but the snake that the plates belonged to was in a hurry. Muno's destination was still a couple of days away, but his recent altercation with a rather hungry predator pressed him even more for time. He lamented how his mint-hued scales still smelled like owl breath even after a quick bath down the river, but he could only blame himself and he had a job to worry about instead.

View file

@ -1,7 +1,7 @@
--- ---
title: Butting into Their Plans title: Butting into Their Plans
pubDate: 2023-02-18 pubDate: 2023-02-18
authors: Bad Manners authors: bad-manners
wordCount: 1400 wordCount: 1400
contentWarning: > contentWarning: >
Contains: first person point-of-view, non-fatal size difference anal vore, willing male feral drake pred, willing PoV ambiguous anthro prey, rimming. Contains: first person point-of-view, non-fatal size difference anal vore, willing male feral drake pred, willing PoV ambiguous anthro prey, rimming.

View file

@ -1,7 +1,7 @@
--- ---
title: Delicacy's Dare title: Delicacy's Dare
pubDate: 2023-01-10 pubDate: 2023-01-10
authors: Bad Manners authors: bad-manners
wordCount: 8000 wordCount: 8000
contentWarning: > contentWarning: >
Contains: Non-fatal micro oral vore, with willing male deer predator, willing to unwilling male dragon prey, and messy stomach with food digestion. Contains: Non-fatal micro oral vore, with willing male deer predator, willing to unwilling male dragon prey, and messy stomach with food digestion.

View file

@ -1,7 +1,7 @@
--- ---
title: Eggs for Months title: Eggs for Months
pubDate: 2022-07-20 pubDate: 2022-07-20
authors: Bad Manners authors: bad-manners
wordCount: 7700 wordCount: 7700
contentWarning: > contentWarning: >
Contains: size difference, non-fatal sheath vore, with male feral gryphon pred and female anthro crow prey. Also includes dubious consent sex scenes, and lots of egg play and insertion (oral, cock, vaginal). Contains: size difference, non-fatal sheath vore, with male feral gryphon pred and female anthro crow prey. Also includes dubious consent sex scenes, and lots of egg play and insertion (oral, cock, vaginal).

View file

@ -1,7 +1,7 @@
--- ---
title: Engaging Contacts title: Engaging Contacts
pubDate: 2023-08-08 pubDate: 2023-08-08
authors: Bad Manners authors: bad-manners
wordCount: 6000 wordCount: 6000
contentWarning: > contentWarning: >
Contains: Non-fatal size difference oral vore and unbirth, with willing female wyvern pred and multiple unwilling/semi-willing/willing female anthro prey. Also includes lesbian sex, pet play, and implied full tour. Contains: Non-fatal size difference oral vore and unbirth, with willing female wyvern pred and multiple unwilling/semi-willing/willing female anthro prey. Also includes lesbian sex, pet play, and implied full tour.

View file

@ -1,7 +1,7 @@
--- ---
title: Flavorful Favor title: Flavorful Favor
pubDate: 2023-04-14 pubDate: 2023-04-14
authors: Bad Manners authors: bad-manners
wordCount: 9400 wordCount: 9400
contentWarning: > contentWarning: >
Contains: Non-fatal oral vore, cock vore, and unbirth, with willing male gryphon predator, willing/semi-willing smaller female kobold predator/prey, and unwilling/semi-willing micro male mouse prey. Also includes full tour, prey transfer (cock to womb), and straight/gay sexual situations. Contains: Non-fatal oral vore, cock vore, and unbirth, with willing male gryphon predator, willing/semi-willing smaller female kobold predator/prey, and unwilling/semi-willing micro male mouse prey. Also includes full tour, prey transfer (cock to womb), and straight/gay sexual situations.
@ -37,7 +37,7 @@ tags:
"gay sex", "gay sex",
] ]
copyrightedCharacters: copyrightedCharacters:
Beetle: Bad Manners Beetle: bad-manners
--- ---
Rondo scampered towards his friend's cave. The mouse's cheeks were still blushing from his determination to profess his love for Sonatina. Sure, he may have been a tiny rodent, as big as the already small kobold's fist... But still! Love had flourished within stranger pairings, before it certainly did in all sorts of romance novels he'd devour. And he and Tina had known each other for years, spending some time together in this woods whenever they had the chance...but just as friends. The orange mouse hoped to rectify that. Rondo scampered towards his friend's cave. The mouse's cheeks were still blushing from his determination to profess his love for Sonatina. Sure, he may have been a tiny rodent, as big as the already small kobold's fist... But still! Love had flourished within stranger pairings, before it certainly did in all sorts of romance novels he'd devour. And he and Tina had known each other for years, spending some time together in this woods whenever they had the chance...but just as friends. The orange mouse hoped to rectify that.

View file

@ -1,7 +1,7 @@
--- ---
title: For the Night title: For the Night
pubDate: 2022-12-26 pubDate: 2022-12-26
authors: Bad Manners authors: bad-manners
wordCount: 1500 wordCount: 1500
contentWarning: > contentWarning: >
Contains: non-fatal same size anal vore, willing anthro male dog pred, willing anthro female pony prey, straight anal sex, threesome, sexuality play. Contains: non-fatal same size anal vore, willing anthro male dog pred, willing anthro female pony prey, straight anal sex, threesome, sexuality play.

View file

@ -1,7 +1,7 @@
--- ---
title: Gentle and Cruel title: Gentle and Cruel
pubDate: 2023-10-31 pubDate: 2023-10-31
authors: Bad Manners authors: bad-manners
wordCount: 5200 wordCount: 5200
contentWarning: > contentWarning: >
Contains: Non-fatal size difference oral vore, with gentle male anthro badger pred, cruel monster pred, and willing to unwilling male anthro lynx prey. Also includes regurgitation, aftercare, thriller/horror scenes, and implied transformation. Contains: Non-fatal size difference oral vore, with gentle male anthro badger pred, cruel monster pred, and willing to unwilling male anthro lynx prey. Also includes regurgitation, aftercare, thriller/horror scenes, and implied transformation.

View file

@ -1,7 +1,7 @@
--- ---
title: Hate to Sea It title: Hate to Sea It
pubDate: 2023-02-18 pubDate: 2023-02-18
authors: Bad Manners authors: bad-manners
wordCount: 1200 wordCount: 1200
contentWarning: > contentWarning: >
Contains: non-fatal size difference unbirth, willing female feral orca pred, unwilling to semi-willing male feral dolphin prey, straight sex, hate sex. Contains: non-fatal size difference unbirth, willing female feral orca pred, unwilling to semi-willing male feral dolphin prey, straight sex, hate sex.

View file

@ -1,7 +1,7 @@
--- ---
title: Hungry for Love title: Hungry for Love
pubDate: 2023-02-14 pubDate: 2023-02-14
authors: Bad Manners authors: bad-manners
wordCount: 5900 wordCount: 5900
contentWarning: > contentWarning: >
Contains: Non-fatal size difference oral vore, with willing male spider predator, and willing female badger prey. Also includes straight sexual situations. Contains: Non-fatal size difference oral vore, with willing male spider predator, and willing female badger prey. Also includes straight sexual situations.

View file

@ -1,7 +1,7 @@
--- ---
title: Hyper Hunger title: Hyper Hunger
pubDate: 2022-12-05 pubDate: 2022-12-05
authors: Bad Manners authors: bad-manners
wordCount: 1300 wordCount: 1300
contentWarning: > contentWarning: >
Contains: non-fatal size difference oral vore, willing anthro ambiguous male pred, unwilling feral dog prey, food stuffing, messy stomach with smells, hyper cock, auto-fellatio. Contains: non-fatal size difference oral vore, willing anthro ambiguous male pred, unwilling feral dog prey, food stuffing, messy stomach with smells, hyper cock, auto-fellatio.

View file

@ -1,7 +1,7 @@
--- ---
title: Insistence and Assistance title: Insistence and Assistance
pubDate: 2022-12-05 pubDate: 2022-12-05
authors: Bad Manners authors: bad-manners
wordCount: 1200 wordCount: 1200
contentWarning: > contentWarning: >
Contains: non-fatal same size oral vore, semi-willing anthro male cat pred, unwilling anthro male mouse prey, burping, regurgitation, force feeding, voyeurism. Contains: non-fatal same size oral vore, semi-willing anthro male cat pred, unwilling anthro male mouse prey, burping, regurgitation, force feeding, voyeurism.

View file

@ -1,7 +1,7 @@
--- ---
title: Lactation Action title: Lactation Action
pubDate: 2022-12-26 pubDate: 2022-12-26
authors: Bad Manners authors: bad-manners
wordCount: 1400 wordCount: 1400
contentWarning: > contentWarning: >
Contains: non-fatal micro nipple vore and oral vore, willing anthro female ferret pred, willing anthro male brown bear prey/pred, willing/asleep anthro female seagull prey, breast play, shrinking and growing, lactation, breastfeeding, prey transfer, growing in stomach to same size, burping. Contains: non-fatal micro nipple vore and oral vore, willing anthro female ferret pred, willing anthro male brown bear prey/pred, willing/asleep anthro female seagull prey, breast play, shrinking and growing, lactation, breastfeeding, prey transfer, growing in stomach to same size, burping.

View file

@ -1,7 +1,7 @@
--- ---
title: Latest Catch title: Latest Catch
pubDate: 2022-12-26 pubDate: 2022-12-26
authors: Bad Manners authors: bad-manners
wordCount: 1500 wordCount: 1500
contentWarning: > contentWarning: >
Contains: non-fatal size difference cock vore, willing to semi-willing anthro non-binary rabbit pred, willing feral snake prey, masturbation, mouthplay, implied perma endo. Contains: non-fatal size difference cock vore, willing to semi-willing anthro non-binary rabbit pred, willing feral snake prey, masturbation, mouthplay, implied perma endo.

View file

@ -1,7 +1,7 @@
--- ---
title: Never Too Late title: Never Too Late
pubDate: 2022-12-05 pubDate: 2022-12-05
authors: Bad Manners authors: bad-manners
wordCount: 1100 wordCount: 1100
contentWarning: > contentWarning: >
Contains: non-fatal same size cock vore, asleep anthro male horse pred, willing anthro female aardwolf prey, masturbation, fellatio, alcohol. Contains: non-fatal same size cock vore, asleep anthro male horse pred, willing anthro female aardwolf prey, masturbation, fellatio, alcohol.

View file

@ -1,7 +1,7 @@
--- ---
title: Noble Fire title: Noble Fire
pubDate: 2023-09-20 pubDate: 2023-09-20
authors: Bad Manners authors: bad-manners
wordCount: 6900 wordCount: 6900
contentWarning: > contentWarning: >
Contains: Non-fatal same size oral vore, with willing male anthro lion pred and semi-willing male anthro dog prey. Also includes sexual nudity and heavy themes like violence, blood, trauma, abuse, and fear. Contains: Non-fatal same size oral vore, with willing male anthro lion pred and semi-willing male anthro dog prey. Also includes sexual nudity and heavy themes like violence, blood, trauma, abuse, and fear.

View file

@ -1,7 +1,7 @@
--- ---
title: Overzealous Zenko title: Overzealous Zenko
pubDate: 2023-08-08 pubDate: 2023-08-08
authors: Bad Manners authors: bad-manners
wordCount: 4900 wordCount: 4900
contentWarning: > contentWarning: >
Contains: Non-fatal size difference chest maw vore, with willing male kitsune-human centaur pred, unwilling female human prey, and implied perma endo. Contains: Non-fatal size difference chest maw vore, with willing male kitsune-human centaur pred, unwilling female human prey, and implied perma endo.
@ -24,9 +24,9 @@ tags:
"perma endo", "perma endo",
"request", "request",
] ]
requester: { "Dee Lumeni": "https://aryion.com/g4/user/KeeperofLillies" } requester: dee-lumeni
copyrightedCharacters: copyrightedCharacters:
Kuronosuke: { "Dee Lumeni": "https://aryion.com/g4/user/KeeperofLillies" } Kuronosuke: dee-lumeni
--- ---
"Come on, open up." Kuronosuke banged on the apartment's door again a few times. "I know you're in there. You don't wanna get on my bad side." "Come on, open up." Kuronosuke banged on the apartment's door again a few times. "I know you're in there. You don't wanna get on my bad side."

View file

@ -1,7 +1,7 @@
--- ---
title: Part of the Show title: Part of the Show
pubDate: 2023-06-13 pubDate: 2023-06-13
authors: Bad Manners authors: bad-manners
wordCount: 2000 wordCount: 2000
contentWarning: > contentWarning: >
Contains: non-fatal same size public oral vore, with willing male anthro mimic/maned wolf hybrid pred, semi-willing 2nd person PoV anthro prey. Also includes pole-dancing and mentions of alcohol. Contains: non-fatal same size public oral vore, with willing male anthro mimic/maned wolf hybrid pred, semi-willing 2nd person PoV anthro prey. Also includes pole-dancing and mentions of alcohol.
@ -29,7 +29,7 @@ tags:
"flash fiction", "flash fiction",
] ]
copyrightedCharacters: copyrightedCharacters:
"Sam Brendan": Bad Manners "Sam Brendan": bad-manners
--- ---
You make up your mind, and finally decide to visit this nightclub you've been told about. It was a 'one-in-a-lifetime experience', according to your friend. And everyone else who went there seems to agree with that sentiment. But honestly, what can be so interesting about people dancing around a pole? It isn't even a strip club performance... No matter. You already cleared up your agenda for tonight, and your interest is mildly piqued. You might as well visit the place. Alone, of course you don't need any nosy witnesses; and if it turns out to be as boring as expected, you can just leave, no strings attached. You make up your mind, and finally decide to visit this nightclub you've been told about. It was a 'one-in-a-lifetime experience', according to your friend. And everyone else who went there seems to agree with that sentiment. But honestly, what can be so interesting about people dancing around a pole? It isn't even a strip club performance... No matter. You already cleared up your agenda for tonight, and your interest is mildly piqued. You might as well visit the place. Alone, of course you don't need any nosy witnesses; and if it turns out to be as boring as expected, you can just leave, no strings attached.

View file

@ -1,7 +1,7 @@
--- ---
title: Pet-Sit Saturday title: Pet-Sit Saturday
pubDate: 2022-07-30 pubDate: 2022-07-30
authors: Bad Manners authors: bad-manners
wordCount: 11000 wordCount: 11000
contentWarning: > contentWarning: >
Contains: same size, non-fatal anal vore, with female anthro elephant pred, female anthro anteater unwilling prey, and female feral zorgoia pred/willing prey. Also includes object vore (anal), prey transfer, and masturbation. Contains: same size, non-fatal anal vore, with female anthro elephant pred, female anthro anteater unwilling prey, and female feral zorgoia pred/willing prey. Also includes object vore (anal), prey transfer, and masturbation.

View file

@ -1,7 +1,7 @@
--- ---
title: Reaching for the Full Moon title: Reaching for the Full Moon
pubDate: 2023-02-18 pubDate: 2023-02-18
authors: Bad Manners authors: bad-manners
wordCount: 1300 wordCount: 1300
contentWarning: > contentWarning: >
Contains: non-fatal oral vore, smaller unwilling anthro male rabbit pred, bigger willing werewolf prey, forced vore, role reversal. Contains: non-fatal oral vore, smaller unwilling anthro male rabbit pred, bigger willing werewolf prey, forced vore, role reversal.

View file

@ -1,7 +1,7 @@
--- ---
title: Ruffling Some Feathers title: Ruffling Some Feathers
pubDate: 2023-02-18 pubDate: 2023-02-18
authors: Bad Manners authors: bad-manners
wordCount: 1000 wordCount: 1000
contentWarning: > contentWarning: >
Contains: non-fatal size difference oral vore, willing feral male owl pred, semi-willing feral male snake prey. Contains: non-fatal size difference oral vore, willing feral male owl pred, semi-willing feral male snake prey.
@ -29,7 +29,7 @@ tags:
] ]
next: bottom-of-the-food-chain next: bottom-of-the-food-chain
copyrightedCharacters: copyrightedCharacters:
Muno: Bad Manners Muno: bad-manners
--- ---
Sovinne shifted in his sleep, feeling some minor discomfort. The brown saw-whet owl had been slumbering inside of the tree trunk hollow for the day, standing up with puffed up feathers. But he woke up when he felt something slick brushing against his skin, and yawned. Sovinne shifted in his sleep, feeling some minor discomfort. The brown saw-whet owl had been slumbering inside of the tree trunk hollow for the day, standing up with puffed up feathers. But he woke up when he felt something slick brushing against his skin, and yawned.

View file

@ -1,7 +1,7 @@
--- ---
title: Spontaneous Sleepover title: Spontaneous Sleepover
pubDate: 2022-12-26 pubDate: 2022-12-26
authors: Bad Manners authors: bad-manners
wordCount: 1300 wordCount: 1300
contentWarning: > contentWarning: >
Contains: non-fatal same size tail vore, willing anthro male squirrel pred, willing anthro female stoat prey, unwilling anthro female pangolin prey, public vore. Contains: non-fatal same size tail vore, willing anthro male squirrel pred, willing anthro female stoat prey, unwilling anthro female pangolin prey, public vore.

View file

@ -1,8 +1,7 @@
--- ---
title: Taken In title: Taken In
pubDate: 2024-01-22 pubDate: 2024-01-22
authors: authors: bad-manners
- Bad Manners
wordCount: 5900 wordCount: 5900
contentWarning: > contentWarning: >
Contains: Non-fatal same size oral vore, with willing male feral hybrid pred (mimic x maned wolf), unwilling PoV anthro prey, and full tour. Contains: Non-fatal same size oral vore, with willing male feral hybrid pred (mimic x maned wolf), unwilling PoV anthro prey, and full tour.
@ -23,7 +22,7 @@ tags:
"point of view", "point of view",
] ]
copyrightedCharacters: copyrightedCharacters:
"Sam Brendan": Bad Manners "Sam Brendan": bad-manners
--- ---
Clank! Shuffle! Crunch! The sounds outside are too loud to be stopped by the walls in your room, and you jolt awake. Clank! Shuffle! Crunch! The sounds outside are too loud to be stopped by the walls in your room, and you jolt awake.

View file

@ -1,7 +1,7 @@
--- ---
title: Tasting High Consequences title: Tasting High Consequences
pubDate: 2023-04-20 pubDate: 2023-04-20
authors: Bad Manners authors: bad-manners
wordCount: 6000 wordCount: 6000
contentWarning: > contentWarning: >
Contains: non-fatal oral vore, with willing feral female boar pred, unwilling similar size anthro female moth-dragon hybrid prey, and unwilling micro anthro female serpent prey. Also includes object vore, fantasy combat, and cannabis. Contains: non-fatal oral vore, with willing feral female boar pred, unwilling similar size anthro female moth-dragon hybrid prey, and unwilling micro anthro female serpent prey. Also includes object vore, fantasy combat, and cannabis.

View file

@ -1,7 +1,7 @@
--- ---
title: Team Building title: Team Building
pubDate: 2024-01-07 pubDate: 2024-01-07
authors: Bad Manners authors: bad-manners
wordCount: 15100 wordCount: 15100
contentWarning: > contentWarning: >
Contains: Non-fatal same size cock vore and anal vore, with willing male anthro monkey pred, willing male anthro gorilla pred, multiple same-size willing male anthro prey. Also includes casual public mass vore, prey transfer, long-term endosoma, hyper cock and balls, hyper and muscle growth, hyper cum inflation, cock worship, casual public gay sex, size difference play, bench-pressing, and voyeurism. Contains: Non-fatal same size cock vore and anal vore, with willing male anthro monkey pred, willing male anthro gorilla pred, multiple same-size willing male anthro prey. Also includes casual public mass vore, prey transfer, long-term endosoma, hyper cock and balls, hyper and muscle growth, hyper cum inflation, cock worship, casual public gay sex, size difference play, bench-pressing, and voyeurism.
@ -33,9 +33,9 @@ tags:
"gay sex", "gay sex",
"commission", "commission",
] ]
commissioner: { "YolkMonkey": "https://furaffinity.net/user/Vampire101" } commissioner: yolkmonkey
copyrightedCharacters: copyrightedCharacters:
Yolk: { "YolkMonkey": "https://furaffinity.net/user/Vampire101" } Yolk: yolkmonkey
prev: team-effort prev: team-effort
--- ---

View file

@ -1,7 +1,7 @@
--- ---
title: Team Effort title: Team Effort
pubDate: 2023-08-08 pubDate: 2023-08-08
authors: Bad Manners authors: bad-manners
wordCount: 11600 wordCount: 11600
contentWarning: > contentWarning: >
Contains: Non-fatal same size cock vore, with semi-willing to willing male anthro monkey pred, multiple willing male anthro prey, and long-term endosoma. Also includes hyper cock growth, cock worship, hyper cum inflation, public vore, casual gay sex (oral and anal sex; same size, size difference), and public sex. Contains: Non-fatal same size cock vore, with semi-willing to willing male anthro monkey pred, multiple willing male anthro prey, and long-term endosoma. Also includes hyper cock growth, cock worship, hyper cum inflation, public vore, casual gay sex (oral and anal sex; same size, size difference), and public sex.
@ -27,9 +27,9 @@ tags:
"gay sex", "gay sex",
"request", "request",
] ]
requester: { "YolkMonkey": "https://furaffinity.net/user/Vampire101" } requester: yolkmonkey
copyrightedCharacters: copyrightedCharacters:
Yolk: { "YolkMonkey": "https://furaffinity.net/user/Vampire101" } Yolk: yolkmonkey
next: team-building next: team-building
--- ---

View file

@ -1,7 +1,7 @@
--- ---
title: The Last Livestream title: The Last Livestream
pubDate: 2022-12-05 pubDate: 2022-12-05
authors: Bad Manners authors: bad-manners
wordCount: 1400 wordCount: 1400
contentWarning: > contentWarning: >
Contains: non-fatal similar size unbirth, willing anthro female coatimundi pred, willing anthro female fennec fox prey, masturbation, livestreamed vore. Contains: non-fatal similar size unbirth, willing anthro female coatimundi pred, willing anthro female fennec fox prey, masturbation, livestreamed vore.

View file

@ -2,7 +2,7 @@
title: "The Lost of the Marshes Bonus: Quince's Fantasy" title: "The Lost of the Marshes Bonus: Quince's Fantasy"
shortTitle: "Bonus Quince's Fantasy" shortTitle: "Bonus Quince's Fantasy"
pubDate: 2023-01-18 pubDate: 2023-01-18
authors: Bad Manners authors: bad-manners
wordCount: 5800 wordCount: 5800
contentWarning: > contentWarning: >
Contains: macro and size difference, non-fatal oral vore. Also includes dream scenarios, role reversal, and self-vore. Contains: macro and size difference, non-fatal oral vore. Also includes dream scenarios, role reversal, and self-vore.

View file

@ -2,7 +2,7 @@
title: "The Lost of the Marshes Chapter 1: Found" title: "The Lost of the Marshes Chapter 1: Found"
shortTitle: "Chapter 1 Found" shortTitle: "Chapter 1 Found"
pubDate: 2022-06-04 pubDate: 2022-06-04
authors: Bad Manners authors: bad-manners
wordCount: 7300 wordCount: 7300
contentWarning: > contentWarning: >
Contains: macro, non-fatal oral vore. Contains: macro, non-fatal oral vore.

View file

@ -2,7 +2,7 @@
title: "The Lost of the Marshes Chapter 10: Memory" title: "The Lost of the Marshes Chapter 10: Memory"
shortTitle: "Chapter 10 Memory" shortTitle: "Chapter 10 Memory"
pubDate: 2023-05-27 pubDate: 2023-05-27
authors: Bad Manners authors: bad-manners
wordCount: 14600 wordCount: 14600
contentWarning: > contentWarning: >
Contains: macro with non-fatal oral vore, anal vore, cock vore, and slit vore. Also includes sexual situations, slight blood, and heavy themes. Contains: macro with non-fatal oral vore, anal vore, cock vore, and slit vore. Also includes sexual situations, slight blood, and heavy themes.

View file

@ -2,7 +2,7 @@
title: "The Lost of the Marshes Chapter 11: Familiar" title: "The Lost of the Marshes Chapter 11: Familiar"
shortTitle: "Chapter 11 Familiar" shortTitle: "Chapter 11 Familiar"
pubDate: 2023-11-15 pubDate: 2023-11-15
authors: Bad Manners authors: bad-manners
wordCount: 13600 wordCount: 13600
contentWarning: > contentWarning: >
Contains: non-fatal oral vore and cock vore, with size difference and macro. Also includes gay sexual situations. Contains: non-fatal oral vore and cock vore, with size difference and macro. Also includes gay sexual situations.

View file

@ -2,7 +2,7 @@
title: "The Lost of the Marshes Chapter 2: Trust" title: "The Lost of the Marshes Chapter 2: Trust"
shortTitle: "Chapter 2 Trust" shortTitle: "Chapter 2 Trust"
pubDate: 2022-06-09 pubDate: 2022-06-09
authors: Bad Manners authors: bad-manners
wordCount: 6900 wordCount: 6900
contentWarning: > contentWarning: >
Contains: macro, non-fatal oral vore, minor nudity. Contains: macro, non-fatal oral vore, minor nudity.

View file

@ -2,7 +2,7 @@
title: "The Lost of the Marshes Chapter 3: Home" title: "The Lost of the Marshes Chapter 3: Home"
shortTitle: "Chapter 3 Home" shortTitle: "Chapter 3 Home"
pubDate: 2022-06-19 pubDate: 2022-06-19
authors: Bad Manners authors: bad-manners
wordCount: 10800 wordCount: 10800
contentWarning: > contentWarning: >
Contains: macro and size difference, non-fatal oral vore, role reversal. Contains: macro and size difference, non-fatal oral vore, role reversal.

View file

@ -2,7 +2,7 @@
title: "The Lost of the Marshes Chapter 4: Change" title: "The Lost of the Marshes Chapter 4: Change"
shortTitle: "Chapter 4 Change" shortTitle: "Chapter 4 Change"
pubDate: 2022-07-12 pubDate: 2022-07-12
authors: Bad Manners authors: bad-manners
wordCount: 12000 wordCount: 12000
contentWarning: > contentWarning: >
Contains: size difference, non-fatal oral vore, live feeding, sexual nudity. Contains: size difference, non-fatal oral vore, live feeding, sexual nudity.

View file

@ -2,7 +2,7 @@
title: "The Lost of the Marshes Chapter 5: Intersection" title: "The Lost of the Marshes Chapter 5: Intersection"
shortTitle: "Chapter 5 Intersection" shortTitle: "Chapter 5 Intersection"
pubDate: 2022-08-04 pubDate: 2022-08-04
authors: Bad Manners authors: bad-manners
wordCount: 8600 wordCount: 8600
contentWarning: > contentWarning: >
Contains: size difference, non-fatal oral vore. Also includes heavy themes. Contains: size difference, non-fatal oral vore. Also includes heavy themes.

View file

@ -2,7 +2,7 @@
title: "The Lost of the Marshes Chapter 6: Pleasure" title: "The Lost of the Marshes Chapter 6: Pleasure"
shortTitle: "Chapter 6 Pleasure" shortTitle: "Chapter 6 Pleasure"
pubDate: 2022-10-22 pubDate: 2022-10-22
authors: Bad Manners authors: bad-manners
wordCount: 9000 wordCount: 9000
contentWarning: > contentWarning: >
Contains: size difference, non-fatal oral vore and unbirth, gay sex, masturbation. Contains: size difference, non-fatal oral vore and unbirth, gay sex, masturbation.

View file

@ -2,7 +2,7 @@
title: "The Lost of the Marshes Chapter 7: Honesty" title: "The Lost of the Marshes Chapter 7: Honesty"
shortTitle: "Chapter 7 Honesty" shortTitle: "Chapter 7 Honesty"
pubDate: 2022-11-23 pubDate: 2022-11-23
authors: Bad Manners authors: bad-manners
wordCount: 6500 wordCount: 6500
contentWarning: > contentWarning: >
Contains: macro and size difference, non-fatal oral and anal vore, gay sex. Contains: macro and size difference, non-fatal oral and anal vore, gay sex.

View file

@ -2,7 +2,7 @@
title: "The Lost of the Marshes Chapter 8: Estranged" title: "The Lost of the Marshes Chapter 8: Estranged"
shortTitle: "Chapter 8 Estranged" shortTitle: "Chapter 8 Estranged"
pubDate: 2022-12-08 pubDate: 2022-12-08
authors: Bad Manners authors: bad-manners
wordCount: 7500 wordCount: 7500
contentWarning: > contentWarning: >
Contains: macro and size difference, non-fatal oral and anal vore. Contains: macro and size difference, non-fatal oral and anal vore.

View file

@ -2,7 +2,7 @@
title: "The Lost of the Marshes Chapter 9: Stuck" title: "The Lost of the Marshes Chapter 9: Stuck"
shortTitle: "Chapter 9 Stuck" shortTitle: "Chapter 9 Stuck"
pubDate: 2023-01-31 pubDate: 2023-01-31
authors: Bad Manners authors: bad-manners
wordCount: 11100 wordCount: 11100
contentWarning: > contentWarning: >
Contains: macro and size difference, non-fatal oral vore and slit vore. Also includes gay sexual situations, slight vomit, slight blood, and heavy themes. Contains: macro and size difference, non-fatal oral vore and slit vore. Also includes gay sexual situations, slight vomit, slight blood, and heavy themes.

View file

@ -1,7 +1,7 @@
--- ---
title: tomo moku title: tomo moku
pubDate: 2023-04-01 pubDate: 2023-04-01
authors: nasin ike Pemene authors: bad-manners
wordCount: 1200 wordCount: 1200
contentWarning: > contentWarning: >
nanpa nimi li mute li kijetesantakalu kijetesantakalu kijetesantakalu kijetesantakalu kijetesantakalu. lipu li jo e ijo tu tu ni: moku musi pi moli ala kepeken uta; akesi li moku musi e soweli; akesi li wile e ni; soweli li wile ala e ni. nanpa nimi li mute li kijetesantakalu kijetesantakalu kijetesantakalu kijetesantakalu kijetesantakalu. lipu li jo e ijo tu tu ni: moku musi pi moli ala kepeken uta; akesi li moku musi e soweli; akesi li wile e ni; soweli li wile ala e ni.

View file

@ -1,7 +1,7 @@
--- ---
title: Trouble Sleeping title: Trouble Sleeping
pubDate: 2023-09-20 pubDate: 2023-09-20
authors: Bad Manners authors: bad-manners
wordCount: 4800 wordCount: 4800
contentWarning: > contentWarning: >
Contains: Non-fatal size difference unbirth, with asleep female anthro wolf pred and willing male feral sparrow prey. Also includes straight sex and accidental long-term endo. Contains: Non-fatal size difference unbirth, with asleep female anthro wolf pred and willing male feral sparrow prey. Also includes straight sex and accidental long-term endo.

View file

@ -1,7 +1,7 @@
--- ---
title: Warped Friendship title: Warped Friendship
pubDate: 2023-08-08 pubDate: 2023-08-08
authors: Bad Manners authors: bad-manners
wordCount: 7800 wordCount: 7800
contentWarning: > contentWarning: >
Contains: Non-fatal same size oral vore, with willing male anthro fennec fox pred, unwilling to willing male anthro red panda prey, and long-term endo. Contains: Non-fatal same size oral vore, with willing male anthro fennec fox pred, unwilling to willing male anthro red panda prey, and long-term endo.
@ -24,10 +24,10 @@ tags:
"long-term endo", "long-term endo",
"request", "request",
] ]
requester: { "Avour Inden": "https://furaffinity.net/user/pppp0000" } requester: avour-inden
copyrightedCharacters: copyrightedCharacters:
Avour: { "Avour Inden": "https://furaffinity.net/user/pppp0000" } Avour: avour-inden
Buster: { "Holi": "https://furaffinity.net/user/CinnamonStars" } Buster: holi
--- ---
This wasn't Avour's usual life-or-death assignment, where he had to hunt down supernatural entities. The red panda was skilled in both combat and magic, and had made a name for himself by facing off against formidable foes. But this time, it was just a simple side job. He was on his way to some town that he had never heard of, where there had been several complaints about some sort of pest that kept stealing people's foods and wreaking havoc. This wasn't Avour's usual life-or-death assignment, where he had to hunt down supernatural entities. The red panda was skilled in both combat and magic, and had made a name for himself by facing off against formidable foes. But this time, it was just a simple side job. He was on his way to some town that he had never heard of, where there had been several complaints about some sort of pest that kept stealing people's foods and wreaking havoc.

View file

@ -1,7 +1,7 @@
--- ---
title: Within Limits title: Within Limits
pubDate: 2023-12-05 pubDate: 2023-12-05
authors: Bad Manners authors: bad-manners
wordCount: 14500 wordCount: 14500
contentWarning: > contentWarning: >
Contains: non-fatal vore, with female taur unbirth (mass vore, hammerspace), male anthro cock vore, and multiple anthro + human willing prey. Also includes bigger prey, similar size prey, size difference, nested vore, prey transfer, hyper genitals, alien genitals, and a sci-fi orgy setting. Contains: non-fatal vore, with female taur unbirth (mass vore, hammerspace), male anthro cock vore, and multiple anthro + human willing prey. Also includes bigger prey, similar size prey, size difference, nested vore, prey transfer, hyper genitals, alien genitals, and a sci-fi orgy setting.
@ -35,9 +35,9 @@ tags:
"orgy", "orgy",
"commission", "commission",
] ]
commissioner: { "Asof Yeun": "https://www.furaffinity.net/user/AsofYeun/" } commissioner: asofyeun
copyrightedCharacters: copyrightedCharacters:
Ushitora: { "Asof Yeun": "https://www.furaffinity.net/user/AsofYeun/" } Ushitora: asofyeun
--- ---
Tonight was going to be Ushitora's big night, but she showed no signs of nervousness. She had no reason to. The holographic readings on her bracelet were all nominal, of course, but she checked on them to make sure that all sensors were on. The technician didn't want to lose a single bit of data for her research. Tonight was going to be Ushitora's big night, but she showed no signs of nervousness. She had no reason to. The holographic readings on her bracelet were all nominal, of course, but she checked on them to make sure that all sensors were on. The technician didn't want to lose a single bit of data for her research.

View file

@ -1,7 +1,7 @@
--- ---
title: You're Home title: You're Home
pubDate: 2022-11-10 pubDate: 2022-11-10
authors: Bad Manners authors: bad-manners
wordCount: 11300 wordCount: 11300
contentWarning: > contentWarning: >
Contains: Unwilling, non-fatal oral vore, with similar size preds/preys, and implied permanent endosoma. Also includes sexual situations and masturbation, slight description of vomit, and a bunch of social anxiety. Contains: Unwilling, non-fatal oral vore, with similar size preds/preys, and implied permanent endosoma. Also includes sexual situations and masturbation, slight description of vomit, and a bunch of social anxiety.

View file

@ -0,0 +1,11 @@
{
"name": "Asof Yeun",
"links": {
"eka": "https://aryion.com/g4/user/asofyeun",
"furaffinity": "https://www.furaffinity.net/user/asofyeun",
"inkbunny": "https://inkbunny.net/asofyeun",
"sofurry": "https://asofyeun.sofurry.com/",
"weasyl": "https://www.weasyl.com/~asofyeun"
},
"preferredLink": "furaffinity"
}

View file

@ -0,0 +1,7 @@
{
"name": "Avour Inden",
"links": {
"furaffinity": "https://furaffinity.net/user/pppp0000"
},
"preferredLink": "furaffinity"
}

View file

@ -0,0 +1,20 @@
{
"name": "Bad Manners",
"nameLang": {
"eng": "Bad Manners",
"tok": "nasin ike Pemene"
},
"avatar": "/src/assets/images/logo_bm.png",
"links": {
"website": "https://badmanners.xyz",
"eka": ["https://aryion.com/g4/user/BadManners", "BadManners"],
"furaffinity": ["https://www.furaffinity.net/user/BadManners", "BadManners"],
"inkbunny": ["https://inkbunny.net/BadManners", "BadManners"],
"sofurry": ["https://bad-manners.sofurry.com/", "Bad Manners"],
"weasyl": ["https://www.weasyl.com/~BadManners", "BadManners"],
"twitter": "https://twitter.com/BadManners__",
"mastodon": "https://meow.social/@BadManners",
"bluesky": "https://bsky.app/profile/badmanners.xyz"
},
"preferredLink": null
}

View file

@ -0,0 +1,7 @@
{
"name": "Dee Lumeni",
"links": {
"eka": ["https://aryion.com/g4/user/KeeperofLillies", "KeeperofLillies"]
},
"preferredLink": "eka"
}

View file

@ -0,0 +1,7 @@
{
"name": "Holi",
"links": {
"furaffinity": ["https://furaffinity.net/user/CinnamonStars", "CinnamonStars"]
},
"preferredLink": "furaffinity"
}

View file

@ -0,0 +1,8 @@
{
"name": "Scion",
"links": {
"eka": "https://aryion.com/g4/user/Scion",
"furaffinity": ["https://www.furaffinity.net/user/Scionic", "Scionic"]
},
"preferredLink": "eka"
}

View file

@ -0,0 +1,8 @@
{
"name": "YolkMonkey",
"links": {
"furaffinity": ["https://furaffinity.net/user/Vampire101", "Vampire101"],
"sofurry": ["https://vampire101.sofurry.com/", "Vampire101"]
},
"preferredLink": "furaffinity"
}

View file

@ -1,6 +1,6 @@
--- ---
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import { type CollectionEntry } from "astro:content"; import { type CollectionEntry, getEntry, getEntries } from "astro:content";
import { Markdown } from "@astropub/md"; import { Markdown } from "@astropub/md";
import { format as formatDate } from "date-fns"; import { format as formatDate } from "date-fns";
import { enUS as enUSLocale } from "date-fns/locale/en-US"; import { enUS as enUSLocale } from "date-fns/locale/en-US";
@ -13,8 +13,17 @@ import Prose from "../components/Prose.astro";
type Props = CollectionEntry<"games">["data"]; type Props = CollectionEntry<"games">["data"];
const { props } = Astro; const { props } = Astro;
//const relatedStories = (await Promise.all((props.relatedStories || []).map(story => getEntry(story)))).filter(story => !story.data.isDraft) const authors = await getEntries([props.authors].flat());
// const relatedGames = (await Promise.all((props.relatedGames || []).map(game => getEntry(game)))).filter(game => !game.data.isDraft) const copyrightedCharacters: Record<string, CollectionEntry<"users">> = {};
Object.keys(props.copyrightedCharacters).forEach(async (character) => {
copyrightedCharacters[character] = await getEntry(props.copyrightedCharacters[character]);
});
// const relatedStories = (await getEntries(props.relatedStories)).filter(
// (story) => !story.data.isDraft,
// );
// const relatedGames = (await getEntries(props.relatedGames)).filter(
// (game) => !game.data.isDraft,
// );
--- ---
<AgeRestrictedBaseLayout pageTitle={props.title}> <AgeRestrictedBaseLayout pageTitle={props.title}>
@ -74,7 +83,7 @@ const { props } = Astro;
id="game-information" id="game-information"
class="mt-1 space-y-2 px-2 font-serif font-light italic text-stone-600 dark:text-stone-200" class="mt-1 space-y-2 px-2 font-serif font-light italic text-stone-600 dark:text-stone-200"
> >
<Authors authors={props.authors} lang={props.lang} /> <Authors authors={authors} lang={props.lang} />
{ {
props.isDraft ? ( props.isDraft ? (
<p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600"> <p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
@ -131,7 +140,7 @@ const { props } = Astro;
</h2> </h2>
<Prose> <Prose>
<Markdown of={props.description} /> <Markdown of={props.description} />
<CopyrightedCharacters copyrightedCharacters={props.copyrightedCharacters} lang={props.lang} /> <CopyrightedCharacters copyrightedCharacters={copyrightedCharacters} lang={props.lang} />
</Prose> </Prose>
</section> </section>
<div class="pr-3 text-right print:hidden"> <div class="pr-3 text-right print:hidden">

View file

@ -1,6 +1,6 @@
--- ---
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import { getEntry, type CollectionEntry } from "astro:content"; import { type CollectionEntry, getEntry, getEntries } from "astro:content";
import { Markdown } from "@astropub/md"; import { Markdown } from "@astropub/md";
import { format as formatDate } from "date-fns"; import { format as formatDate } from "date-fns";
import { enUS as enUSLocale } from "date-fns/locale/en-US"; import { enUS as enUSLocale } from "date-fns/locale/en-US";
@ -22,10 +22,15 @@ let next = props.next && (await getEntry(props.next));
if (next && next.data.isDraft) { if (next && next.data.isDraft) {
next = undefined; next = undefined;
} }
const relatedStories = (await Promise.all((props.relatedStories || []).map((story) => getEntry(story)))).filter( const authors = await getEntries([props.authors].flat());
(story) => !story.data.isDraft, const commissioner = props.commissioner && (await getEntry(props.commissioner));
); const requester = props.requester && (await getEntry(props.requester));
// const relatedGames = (await Promise.all((props.relatedGames || []).map(game => getEntry(game)))).filter( const copyrightedCharacters: Record<string, CollectionEntry<"users">> = {};
Object.keys(props.copyrightedCharacters).forEach(async (character) => {
copyrightedCharacters[character] = await getEntry(props.copyrightedCharacters[character]);
});
const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
// const relatedGames = (await getEntries(props.relatedGames)).filter(
// (game) => !game.data.isDraft, // (game) => !game.data.isDraft,
// ); // );
--- ---
@ -122,7 +127,7 @@ const relatedStories = (await Promise.all((props.relatedStories || []).map((stor
id="story-information" id="story-information"
class="mt-1 space-y-2 px-2 font-serif font-light italic text-stone-600 dark:text-stone-200" class="mt-1 space-y-2 px-2 font-serif font-light italic text-stone-600 dark:text-stone-200"
> >
<Authors authors={props.authors} lang={props.lang} /> <Authors authors={authors} lang={props.lang} />
{ {
props.isDraft ? ( props.isDraft ? (
<p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600"> <p id="draft-warning" class="py-2 text-center text-2xl font-semibold not-italic text-red-600">
@ -131,16 +136,16 @@ const relatedStories = (await Promise.all((props.relatedStories || []).map((stor
) : null ) : null
} }
{ {
props.commissioner && ( commissioner && (
<p id="commissioner"> <p id="commissioner">
Commissioned by <UserComponent user={props.commissioner} /> Commissioned by <UserComponent user={commissioner} lang={props.lang} />
</p> </p>
) )
} }
{ {
props.requester && ( requester && (
<p id="requester"> <p id="requester">
Requested by <UserComponent user={props.requester} /> Requested by <UserComponent user={requester} lang={props.lang} />
</p> </p>
) )
} }
@ -196,7 +201,7 @@ const relatedStories = (await Promise.all((props.relatedStories || []).map((stor
</h2> </h2>
<Prose> <Prose>
<Markdown of={props.description} /> <Markdown of={props.description} />
<CopyrightedCharacters copyrightedCharacters={props.copyrightedCharacters} lang={props.lang} /> <CopyrightedCharacters copyrightedCharacters={copyrightedCharacters} lang={props.lang} />
</Prose> </Prose>
</section> </section>
<div class="pr-3 text-right print:hidden"> <div class="pr-3 text-right print:hidden">
@ -281,7 +286,9 @@ const relatedStories = (await Promise.all((props.relatedStories || []).map((stor
</main> </main>
<div class="pt-6 text-center text-xs text-black dark:text-white"> <div class="pt-6 text-center text-xs text-black dark:text-white">
<span>&copy; {formatDate(props.pubDate, "yyyy")} | </span> <span>&copy; {formatDate(props.pubDate, "yyyy")} | </span>
<a class="hover:underline focus:underline" href="/licenses.txt" target="_blank">Licenses</a> <a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
>{props.lang === "eng" ? "Licenses" : props.lang === "tok" ? "lipu lawa" : null}</a
>
</div> </div>
</div> </div>
</AgeRestrictedBaseLayout> </AgeRestrictedBaseLayout>

View file

@ -1,36 +0,0 @@
---
import { Image } from "astro:assets";
import { getCollection } from "astro:content";
import { getUnixTime } from "date-fns";
import GalleryLayout from "../layouts/GalleryLayout.astro";
const stories = (await getCollection("stories"))
.filter((story) => !story.data.isDraft)
.sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate));
---
<GalleryLayout pageTitle="Stories">
<h1>Stories</h1>
<p>Lorem ipsum.</p>
<ul>
{
stories.map((story) => (
<li>
<a href={`/stories/${story.slug}`}>
{story.data.thumbnail && (
<Image
src={story.data.thumbnail}
alt={`Thumbnail for ${story.data.title}`}
width={story.data.thumbnailWidth}
height={story.data.thumbnailHeight}
/>
)}
<span>
{story.data.pubDate} - {story.data.title}
</span>
</a>
</li>
))
}
</ul>
</GalleryLayout>

View file

@ -8,11 +8,11 @@ type FeedItem = RSSFeedItem & {
}; };
export const GET: APIRoute = async ({ site }) => { export const GET: APIRoute = async ({ site }) => {
const stories = (await getCollection("stories")).filter((story) => !story.data.isDraft); const stories = await getCollection("stories", (story) => !story.data.isDraft);
const games = (await getCollection("games")).filter((game) => !game.data.isDraft); const games = await getCollection("games", (game) => !game.data.isDraft);
return rss({ return rss({
title: "Gallery | Bad Manners", title: "Gallery | Bad Manners",
description: "Stories, games, and more by Bad Manners", description: "Stories, games, and (possibly) more by Bad Manners",
site: site as URL, site: site as URL,
items: [ items: [
stories.map<FeedItem>((story) => ({ stories.map<FeedItem>((story) => ({
@ -21,7 +21,7 @@ export const GET: APIRoute = async ({ site }) => {
link: `/stories/${story.slug}`, link: `/stories/${story.slug}`,
description: description:
`Word count: ${story.data.wordCount}. ${story.data.contentWarning} ${story.data.descriptionPlaintext || story.data.description}` `Word count: ${story.data.wordCount}. ${story.data.contentWarning} ${story.data.descriptionPlaintext || story.data.description}`
.replaceAll(/\n+| +/g, " ") .replaceAll(/[\n ]+/g, " ")
.trim(), .trim(),
categories: ["story"], categories: ["story"],
})), })),
@ -30,7 +30,7 @@ export const GET: APIRoute = async ({ site }) => {
pubDate: addHours(game.data.pubDate, 12), pubDate: addHours(game.data.pubDate, 12),
link: `/games/${game.slug}`, link: `/games/${game.slug}`,
description: `${game.data.contentWarning} ${game.data.descriptionPlaintext || game.data.description}` description: `${game.data.contentWarning} ${game.data.descriptionPlaintext || game.data.description}`
.replaceAll(/\n+| +/g, " ") .replaceAll(/[\n ]+/g, " ")
.trim(), .trim(),
categories: ["game"], categories: ["game"],
})), })),

View file

@ -5,9 +5,9 @@ import { getUnixTime, format as formatDate } from "date-fns";
import { enUS as enUSLocale } from "date-fns/locale/en-US"; import { enUS as enUSLocale } from "date-fns/locale/en-US";
import GalleryLayout from "../layouts/GalleryLayout.astro"; import GalleryLayout from "../layouts/GalleryLayout.astro";
const games = (await getCollection("games")) const games = (await getCollection("games", (game) => !game.data.isDraft)).sort(
.filter((game) => !game.data.isDraft) (a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate),
.sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate)); );
--- ---
<GalleryLayout pageTitle="Games"> <GalleryLayout pageTitle="Games">
@ -18,7 +18,7 @@ const games = (await getCollection("games"))
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}`}>
{game.data.thumbnail && ( {game.data.thumbnail ? (
<Image <Image
class="max-w-72" class="max-w-72"
src={game.data.thumbnail} src={game.data.thumbnail}
@ -26,7 +26,7 @@ const games = (await getCollection("games"))
width={game.data.thumbnailWidth} width={game.data.thumbnailWidth}
height={game.data.thumbnailHeight} height={game.data.thumbnailHeight}
/> />
)} ) : null}
<div class="max-w-72 text-sm"> <div class="max-w-72 text-sm">
<> <>
<span>{game.data.title}</span> <span>{game.data.title}</span>

View file

@ -1,20 +1,21 @@
--- ---
import type { GetStaticPaths } from "astro";
import { type CollectionEntry, getCollection } from "astro:content"; import { type CollectionEntry, getCollection } from "astro:content";
import GameLayout from "../../layouts/GameLayout.astro"; import GameLayout from "../../layouts/GameLayout.astro";
export async function getStaticPaths() { export const getStaticPaths: GetStaticPaths = async () => {
const games = await getCollection("games"); const games = await getCollection("games");
return games.map((story) => ({ return games.map((game) => ({
params: { slug: story.slug }, params: { slug: game.slug },
props: story, props: game,
})); }));
} };
type Props = CollectionEntry<"games">; type Props = CollectionEntry<"games">;
const story = Astro.props; const game = Astro.props;
const { Content } = await story.render(); const { Content } = await game.render();
--- ---
<GameLayout {...story.data}> <GameLayout {...game.data}>
<Content /> <Content />
</GameLayout> </GameLayout>

View file

@ -1,14 +1,15 @@
--- ---
import type { GetStaticPaths } from "astro";
import { type CollectionEntry, getCollection } from "astro:content"; import { type CollectionEntry, getCollection } from "astro:content";
import StoryLayout from "../../layouts/StoryLayout.astro"; import StoryLayout from "../../layouts/StoryLayout.astro";
export async function getStaticPaths() { export const getStaticPaths: GetStaticPaths = async () => {
const stories = await getCollection("stories"); const stories = await getCollection("stories");
return stories.map((story) => ({ return stories.map((story) => ({
params: { slug: story.slug }, params: { slug: story.slug },
props: story, props: story,
})); }));
} };
type Props = CollectionEntry<"stories">; type Props = CollectionEntry<"stories">;
const story = Astro.props; const story = Astro.props;

View file

@ -1,17 +1,23 @@
--- ---
import type { GetStaticPathsOptions } from "astro"; import type { GetStaticPaths, Page } from "astro";
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import { getUnixTime, format as formatDate } from "date-fns"; import { getUnixTime, format as formatDate } from "date-fns";
import { enUS as enUSLocale } from "date-fns/locale/en-US"; import { enUS as enUSLocale } from "date-fns/locale/en-US";
import GalleryLayout from "../../layouts/GalleryLayout.astro"; import GalleryLayout from "../../layouts/GalleryLayout.astro";
import type { CollectionEntry } from "astro:content";
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) { export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
const stories = (await getCollection("stories")) const stories = (await getCollection("stories", (story) => !story.data.isDraft)).sort(
.filter((story) => !story.data.isDraft) (a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate),
.sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate)); );
return paginate(stories, { pageSize: 30 }); return paginate(stories, { pageSize: 30 });
} };
type Props = {
page: Page<CollectionEntry<"stories">>;
};
const { page } = Astro.props; const { page } = Astro.props;
const totalPages = Math.ceil(page.total / page.size); const totalPages = Math.ceil(page.total / page.size);
--- ---
@ -60,7 +66,7 @@ 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}`}>
{story.data.thumbnail && ( {story.data.thumbnail ? (
<Image <Image
class="w-48" class="w-48"
src={story.data.thumbnail} src={story.data.thumbnail}
@ -68,7 +74,7 @@ const totalPages = Math.ceil(page.total / page.size);
width={story.data.thumbnailWidth} width={story.data.thumbnailWidth}
height={story.data.thumbnailHeight} height={story.data.thumbnailHeight}
/> />
)} ) : null}
<div class="max-w-48 text-sm"> <div class="max-w-48 text-sm">
<> <>
<span>{story.data.title}</span> <span>{story.data.title}</span>

View file

@ -0,0 +1,274 @@
import type { APIRoute, GetStaticPaths } from "astro";
import { getCollection, getEntry, type CollectionEntry, getEntries } from "astro:content";
import { marked, type RendererApi } from "marked";
import he from "he";
import { type Website } from "../../../../content/config";
const WEBSITE_LIST = ["eka", "furaffinity", "inkbunny", "sofurry", "weasyl"] as const satisfies Website[];
type ExportWebsite = typeof WEBSITE_LIST extends ReadonlyArray<infer K> ? 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 {
const link = user.data.links[website];
if (link) {
if (typeof link === "string") {
switch (website) {
case "website":
break;
case "eka":
const ekaMatch = link.match(/^.*\baryion\.com\/g4\/user\/([^\/]+)\/?$/);
if (ekaMatch && ekaMatch[1]) {
return ekaMatch[1];
}
break;
case "furaffinity":
const faMatch = link.match(/^.*\bfuraffinity\.net\/user\/([^\/]+)\/?$/);
if (faMatch && faMatch[1]) {
return faMatch[1];
}
break;
case "inkbunny":
const ibMatch = link.match(/^.*\binkbunny\.net\/([^\/]+)\/?$/);
if (ibMatch && ibMatch[1]) {
return ibMatch[1];
}
break;
case "sofurry":
const sfMatch = link.match(/^(?:https?:\/\/)?([^\.]+).sofurry.com\b.*$/);
if (sfMatch && sfMatch[1]) {
return sfMatch[1].replaceAll("-", " ");
}
break;
case "weasyl":
const weasylMatch = link.match(/^.*\bweasyl\.com\/\~([^\/]+)\/?$/);
if (weasylMatch && weasylMatch[1]) {
return weasylMatch[1];
}
break;
case "twitter":
const twitterMatch = link.match(/^.*(?:\btwitter\.com|\bx\.com)\/@?([^\/]+)\/?$/);
if (twitterMatch && twitterMatch[1]) {
return twitterMatch[1];
}
break;
case "mastodon":
const mastodonMatch = link.match(/^(?:https?\:\/\/)?([^\/]+)\/(?:@|users\/)([^\/]+)\/?$/);
if (mastodonMatch && mastodonMatch[1] && mastodonMatch[2]) {
return `${mastodonMatch[2]}@${mastodonMatch[1]}`;
}
break;
case "bluesky":
const bskyMatch = link.match(/^.*\bbsky\.app\/profile\/([^\/]+)\/?$/);
if (bskyMatch && bskyMatch[1]) {
return bskyMatch[1];
}
break;
default:
throw new Error(`Unhandled website "${website}" in getUsernameForWebsite`);
}
} else {
return link[1].replace(/^@/, "");
}
}
throw new Error(`Cannot get "${website}" username for user "${user.id}"`);
}
function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsite): string {
switch (website) {
case "eka":
if (user.data.links.eka) {
return `:icon${getUsernameForWebsite(user, "eka")}:`;
}
break;
case "furaffinity":
if (user.data.links.furaffinity) {
return `:icon${getUsernameForWebsite(user, "furaffinity")}:`;
}
break;
case "weasyl":
if (user.data.links.weasyl) {
return `<!~${getUsernameForWebsite(user, "weasyl").replaceAll(" ", "")}>`;
} else if (
user.data.links.furaffinity &&
!(["inkbunny", "sofurry"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
return `<fa:${getUsernameForWebsite(user, "furaffinity")}>`;
} else if (
user.data.links.inkbunny &&
!(["furaffinity", "sofurry"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
return `<ib:${getUsernameForWebsite(user, "inkbunny")}>`;
} else if (
user.data.links.sofurry &&
!(["furaffinity", "inkbunny"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
return `<sf:${getUsernameForWebsite(user, "sofurry")}>`;
}
break;
case "inkbunny":
if (user.data.links.inkbunny) {
return `[iconname]${getUsernameForWebsite(user, "inkbunny")}[/iconname]`;
} else if (
user.data.links.furaffinity &&
!(["sofurry", "weasyl"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
return `[fa]${getUsernameForWebsite(user, "furaffinity")}[/fa]`;
} else if (
user.data.links.sofurry &&
!(["furaffinity", "weasyl"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
return `[sf]${getUsernameForWebsite(user, "sofurry")}[/sf]`;
} else if (
user.data.links.weasyl &&
!(["furaffinity", "sofurry"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
return `[weasyl]${getUsernameForWebsite(user, "weasyl")}[/weasyl]`;
}
break;
case "sofurry":
if (user.data.links.sofurry) {
return `:icon${getUsernameForWebsite(user, "sofurry")}:`;
} else if (
user.data.links.furaffinity &&
!(["inkbunny"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
return `fa!${getUsernameForWebsite(user, "furaffinity")}`;
} else if (
user.data.links.inkbunny &&
!(["furaffinity"] satisfies Website[] as (Website | null)[]).includes(user.data.preferredLink)
) {
return `ib!${getUsernameForWebsite(user, "inkbunny")}`;
}
break;
default:
throw new Error(`Unhandled website "${website}" in getLinkForUser`);
}
if (user.data.preferredLink) {
if (user.data.preferredLink in user.data.links) {
const preferredLink = user.data.links[user.data.preferredLink] as string | [string, string];
return `[${user.data.name}](${typeof preferredLink === "string" ? preferredLink : preferredLink[0]})`;
} else {
throw new Error(`No preferredLink "${user.data.preferredLink}" for user ${user.id}`);
}
}
throw new Error(`No "${website}"-supported link for user "${user.id}" without preferredLink`);
}
type Props = {
story: CollectionEntry<"stories">;
};
type Params = {
website: ExportWebsite;
slug: CollectionEntry<"stories">["slug"];
};
export const getStaticPaths: GetStaticPaths = async () => {
if (import.meta.env.PROD) {
return [];
}
return (await getCollection("stories"))
.map((story) =>
WEBSITE_LIST.map((website) => ({
params: { website, slug: story.slug } satisfies Params,
props: { story } satisfies Props,
})),
)
.flat();
};
export const GET: APIRoute<Props, Params> = async ({ props: { story }, params: { website }, site }) => {
const u = (user: CollectionEntry<"users">) => getLinkForUser(user, website);
if (
story.data.copyrightedCharacters &&
"" in story.data.copyrightedCharacters &&
Object.keys(story.data.copyrightedCharacters).length > 1
) {
throw new Error("copyrightedCharacter cannot use empty key (catch-all) with other keys");
}
const charactersPerUser =
story.data.copyrightedCharacters &&
Object.keys(story.data.copyrightedCharacters).reduce(
(acc, character) => {
const key = story.data.copyrightedCharacters[character].id;
if (!(key in acc)) {
acc[key] = [];
}
acc[key].push(character);
return acc;
},
{} as Record<
CollectionEntry<"users">["id"],
(typeof story.data.copyrightedCharacters extends Record<infer K, any> ? K : never)[]
>,
);
let storyDescription = (
[
story.data.description,
`*Word count: ${story.data.wordCount}. ${story.data.contentWarning.trim()}*`,
"Writing: " + (await getEntries([story.data.authors].flat())).map((author) => u(author)).join(" , "),
story.data.requester && "Request for: " + u(await getEntry(story.data.requester)),
story.data.commissioner && "Commissioned by: " + u(await getEntry(story.data.commissioner)),
...(await Promise.all(
(Object.keys(charactersPerUser) as CollectionEntry<"users">["id"][]).map(async (id) => {
const user = u(await getEntry("users", id));
const characterList = charactersPerUser[id];
if (characterList[0] == "") {
return `All characters are © ${user}`;
} else if (characterList.length > 2) {
return `${characterList.slice(0, characterList.length - 1).join(", ")}, and ${characterList[characterList.length - 1]} are © ${user}`;
} else if (characterList.length > 1) {
return `${characterList[0]} and ${characterList[1]} are © ${user}`;
}
return `${characterList[0]} is © ${user}`;
}),
)),
].filter((data) => data) as string[]
)
.join("\n\n")
.replaceAll(
/\[([^\]]+)\]\((\.[^\)]+)\)/g,
(_, group1, group2) => `[${group1}](${new URL(group2, new URL(`/stories/${story.slug}`, site)).toString()})`,
);
const headers = { "Content-Type": "text/markdown; charset=utf-8" };
// BBCode exports
if ((["eka", "furaffinity", "inkbunny", "sofurry"] satisfies ExportWebsite[] as ExportWebsite[]).includes(website)) {
storyDescription = he.decode(await marked.use({ renderer: bbcodeRenderer }).parse(storyDescription));
headers["Content-Type"] = "text/plain; charset=utf-8";
// Markdown exports (no-op)
} else if (!(["weasyl"] satisfies ExportWebsite[] as ExportWebsite[]).includes(website)) {
return new Response(null, { status: 404 });
}
return new Response(`${storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()}\n`, { headers });
};

View file

@ -5,7 +5,8 @@ import { getUnixTime } from "date-fns";
import GalleryLayout from "../../layouts/GalleryLayout.astro"; import GalleryLayout from "../../layouts/GalleryLayout.astro";
import mapImage from "../../assets/images/tlotm_map.jpg"; import mapImage from "../../assets/images/tlotm_map.jpg";
const stories = (await getCollection("stories")).filter( const stories = await getCollection(
"stories",
(story) => !story.data.isDraft && story.slug.startsWith("the-lost-of-the-marshes/"), (story) => !story.data.isDraft && story.slug.startsWith("the-lost-of-the-marshes/"),
); );
const mainChapters = stories const mainChapters = stories
@ -50,7 +51,7 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ
mainChapters.map((story) => ( mainChapters.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}`}>
{story.data.thumbnail && ( {story.data.thumbnail ? (
<Image <Image
class="w-48" class="w-48"
src={story.data.thumbnail} src={story.data.thumbnail}
@ -58,7 +59,7 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ
width={story.data.thumbnailWidth} width={story.data.thumbnailWidth}
height={story.data.thumbnailHeight} height={story.data.thumbnailHeight}
/> />
)} ) : null}
<div class="max-w-48 text-sm">{story.data.title}</div> <div class="max-w-48 text-sm">{story.data.title}</div>
</a> </a>
</li> </li>
@ -73,7 +74,7 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ
bonusChapters.map((story) => ( bonusChapters.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}`}>
{story.data.thumbnail && ( {story.data.thumbnail ? (
<Image <Image
class="w-48" class="w-48"
src={story.data.thumbnail} src={story.data.thumbnail}
@ -81,7 +82,7 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ
width={story.data.thumbnailWidth} width={story.data.thumbnailWidth}
height={story.data.thumbnailHeight} height={story.data.thumbnailHeight}
/> />
)} ) : null}
<div class="max-w-48 text-sm">{story.data.title}</div> <div class="max-w-48 text-sm">{story.data.title}</div>
</a> </a>
</li> </li>

View file

@ -3,47 +3,46 @@ 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";
const [stories, games] = await Promise.all([getCollection("stories"), getCollection("games")]); const [stories, games] = await Promise.all([
getCollection("stories", (story) => !story.data.isDraft),
getCollection("games", (game) => !game.data.isDraft),
]);
const tagsSet = new Set<string>(); const tagsSet = new Set<string>();
const seriesList: Record<string, string> = {}; const seriesList: Record<string, string> = {};
stories stories.forEach((story) => {
.filter((story) => !story.data.isDraft) story.data.tags.forEach((tag) => {
.forEach((story) => { tagsSet.add(tag);
story.data.tags.forEach((tag) => {
tagsSet.add(tag);
});
if (story.data.series) {
const [series, url] = Object.entries(story.data.series)[0];
if (seriesList[series]) {
if (seriesList[series] !== url) {
throw new Error(
`Mismatched series "${series}": tried to assign different links "${seriesList[series]}" and "${url}"`,
);
}
} else {
seriesList[series] = url;
}
}
}); });
games if (story.data.series) {
.filter((game) => !game.data.isDraft) const [series, url] = Object.entries(story.data.series)[0];
.forEach((game) => { if (seriesList[series]) {
game.data.tags.forEach((tag) => { if (seriesList[series] !== url) {
tagsSet.add(tag); throw new Error(
}); `Mismatched series "${series}": tried to assign different links "${seriesList[series]}" and "${url}"`,
if (game.data.series) { );
const [series, url] = Object.entries(game.data.series)[0];
if (seriesList[series]) {
if (seriesList[series] !== url) {
throw new Error(
`Mismatched series "${series}": tried to assign different links "${seriesList[series]}" and "${url}"`,
);
}
} else {
seriesList[series] = url;
} }
} else {
seriesList[series] = url;
} }
}
});
games.forEach((game) => {
game.data.tags.forEach((tag) => {
tagsSet.add(tag);
}); });
if (game.data.series) {
const [series, url] = Object.entries(game.data.series)[0];
if (seriesList[series]) {
if (seriesList[series] !== url) {
throw new Error(
`Mismatched series "${series}": tried to assign different links "${seriesList[series]}" and "${url}"`,
);
}
} else {
seriesList[series] = url;
}
}
});
const categorizedTags: Record<string, string[]> = { const categorizedTags: Record<string, string[]> = {
"Types of vore": [ "Types of vore": [

View file

@ -1,11 +1,12 @@
--- ---
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 { slug } from "github-slugger"; import { slug } from "github-slugger";
import { getUnixTime } from "date-fns"; import { getUnixTime } from "date-fns";
import GalleryLayout from "../../layouts/GalleryLayout.astro"; import GalleryLayout from "../../layouts/GalleryLayout.astro";
export async function getStaticPaths() { export const getStaticPaths: GetStaticPaths = async () => {
const [stories, games] = await Promise.all([getCollection("stories"), getCollection("games")]); const [stories, games] = await Promise.all([getCollection("stories"), getCollection("games")]);
const tags = new Set<string>(); const tags = new Set<string>();
stories.forEach((story) => { stories.forEach((story) => {
@ -32,7 +33,7 @@ export async function getStaticPaths() {
.sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate)), .sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate)),
}, },
})); }));
} };
type Props = { type Props = {
tag: string; tag: string;
@ -55,7 +56,7 @@ const { tag, stories, games } = Astro.props;
{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}`}>
{story.data.thumbnail && ( {story.data.thumbnail ? (
<Image <Image
class="w-48" class="w-48"
src={story.data.thumbnail} src={story.data.thumbnail}
@ -63,7 +64,7 @@ const { tag, stories, games } = Astro.props;
width={story.data.thumbnailWidth} width={story.data.thumbnailWidth}
height={story.data.thumbnailHeight} height={story.data.thumbnailHeight}
/> />
)} ) : null}
<div class="max-w-48 text-sm">{story.data.title}</div> <div class="max-w-48 text-sm">{story.data.title}</div>
</a> </a>
</li> </li>
@ -82,7 +83,7 @@ const { tag, stories, games } = Astro.props;
{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}`}>
{game.data.thumbnail && ( {game.data.thumbnail ? (
<Image <Image
class="w-48" class="w-48"
src={game.data.thumbnail} src={game.data.thumbnail}
@ -90,7 +91,7 @@ const { tag, stories, games } = Astro.props;
width={game.data.thumbnailWidth} width={game.data.thumbnailWidth}
height={game.data.thumbnailHeight} height={game.data.thumbnailHeight}
/> />
)} ) : null}
<div class="max-w-48 text-sm">{game.data.title}</div> <div class="max-w-48 text-sm">{game.data.title}</div>
</a> </a>
</li> </li>