Add description exports and collapse characters from same user
This commit is contained in:
parent
2990644f87
commit
d4a9dc9dbc
78 changed files with 693 additions and 247 deletions
README.mdpackage-lock.jsonpackage.json
src
components
content
config.ts
games
stories
accommodation.mdaddictive-additions.mdannivoresary.mdbetter-in-bully-batter.mdbig-haul.mdbirdroom.mdbladder-filler.mdbottom-of-the-food-chain.mdbutting-into-their-plans.mddelicacy-s-dare.mdeggs-for-months.mdengaging-contacts.mdflavorful-favor.mdfor-the-night.mdgentle-and-cruel.mdhate-to-sea-it.mdhungry-for-love.mdhyper-hunger.mdinsistence-and-assistance.mdlactation-action.mdlatest-catch.mdnever-too-late.mdnoble-fire.mdoverzealous-zenko.mdpart-of-the-show.mdpet-sit-saturday.mdreaching-for-the-full-moon.mdruffling-some-feathers.mdspontaneous-sleepover.mdtaken-in.mdtasting-high-consequences.mdteam-building.mdteam-effort.mdthe-last-livestream.md
the-lost-of-the-marshes
bonus-1-quince-s-fantasy.mdchapter-1.mdchapter-10.mdchapter-11.mdchapter-2.mdchapter-3.mdchapter-4.mdchapter-5.mdchapter-6.mdchapter-7.mdchapter-8.mdchapter-9.md
tomo-moku.mdtrouble-sleeping.mdwarped-friendship.mdwithin-limits.mdyou-re-home.mdusers
layouts
pages
|
@ -7,5 +7,5 @@
|
|||
```bash
|
||||
npm install
|
||||
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
27
package-lock.json
generated
|
@ -13,9 +13,12 @@
|
|||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@astropub/md": "^0.4.0",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/he": "^1.2.3",
|
||||
"astro": "^4.5.4",
|
||||
"date-fns": "^3.5.0",
|
||||
"github-slugger": "^2.0.0",
|
||||
"he": "^1.2.0",
|
||||
"marked": "^12.0.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
|
@ -1278,6 +1281,11 @@
|
|||
"@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": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz",
|
||||
|
@ -3040,6 +3048,14 @@
|
|||
"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": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
|
||||
|
@ -3519,6 +3535,17 @@
|
|||
"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": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz",
|
||||
|
|
|
@ -16,9 +16,12 @@
|
|||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@astropub/md": "^0.4.0",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/he": "^1.2.3",
|
||||
"astro": "^4.5.4",
|
||||
"date-fns": "^3.5.0",
|
||||
"github-slugger": "^2.0.0",
|
||||
"he": "^1.2.0",
|
||||
"marked": "^12.0.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
|
|
|
@ -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";
|
||||
|
||||
type Props = {
|
||||
authors: User | User[];
|
||||
authors: CollectionEntry<"users"> | CollectionEntry<"users">[];
|
||||
lang: Lang;
|
||||
};
|
||||
|
||||
|
@ -20,18 +21,19 @@ const authorsArray = [authors].flat();
|
|||
by{" "}
|
||||
{authorsArray.slice(0, authorsArray.length - 1).map((author) => (
|
||||
<Fragment>
|
||||
<UserComponent user={author} />,
|
||||
<UserComponent lang="eng" user={author} />,
|
||||
</Fragment>
|
||||
))}
|
||||
and <UserComponent user={authorsArray[authorsArray.length - 1]} />
|
||||
and <UserComponent lang="eng" user={authorsArray[authorsArray.length - 1]} />
|
||||
</span>
|
||||
) : authorsArray.length > 1 ? (
|
||||
<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>
|
||||
by <UserComponent user={authorsArray[0]} />
|
||||
by <UserComponent lang="eng" user={authorsArray[0]} />
|
||||
</span>
|
||||
))}
|
||||
{lang === "tok" &&
|
||||
|
@ -40,15 +42,15 @@ const authorsArray = [authors].flat();
|
|||
lipu ni li tan ni:{" "}
|
||||
{authorsArray.slice(0, authorsArray.length - 1).map((author) => (
|
||||
<Fragment>
|
||||
<UserComponent user={author} />
|
||||
<UserComponent lang="tok" user={author} />
|
||||
{" en "}
|
||||
</Fragment>
|
||||
))}
|
||||
<UserComponent user={authorsArray[authorsArray.length - 1]} />
|
||||
<UserComponent lang="tok" user={authorsArray[authorsArray.length - 1]} />
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
lipu ni li tan <UserComponent user={authorsArray[0]} />
|
||||
lipu ni li tan <UserComponent lang="tok" user={authorsArray[0]} />
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
|
|
|
@ -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";
|
||||
|
||||
type Props = {
|
||||
copyrightedCharacters?: Record<string, User>;
|
||||
copyrightedCharacters?: Record<string, CollectionEntry<"users">>;
|
||||
lang: Lang;
|
||||
};
|
||||
|
||||
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">
|
||||
{lang === "eng" && (
|
||||
{lang === "eng" ? (
|
||||
<ul>
|
||||
{Object.entries(copyrightedCharacters).map(([character, user]) => (
|
||||
{Object.values(charactersPerUser).map((characterList) => (
|
||||
<li>
|
||||
{character} is © <UserComponent user={user} />
|
||||
{characterList[0] === "" ? (
|
||||
<span>
|
||||
All characters are © <UserComponent lang={lang} user={copyrightedCharacters[""]} />
|
||||
</span>
|
||||
) : characterList.length > 2 ? (
|
||||
<span>
|
||||
{characterList.slice(0, characterList.length - 1).join(", ")}, and{" "}
|
||||
{characterList[characterList.length - 1]} are ©{" "}
|
||||
<UserComponent lang={lang} user={copyrightedCharacters[characterList[0]]} />
|
||||
</span>
|
||||
) : characterList.length > 1 ? (
|
||||
<span>
|
||||
{characterList[0]} and {characterList[1]} are ©{" "}
|
||||
<UserComponent lang={lang} user={copyrightedCharacters[characterList[0]]} />
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
{characterList[0]} is ©{" "}
|
||||
<UserComponent lang={lang} user={copyrightedCharacters[characterList[0]]} />
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
) : null}
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
|
|
|
@ -1,21 +1,35 @@
|
|||
---
|
||||
import { type User } from "../content/config";
|
||||
import { type CollectionEntry } from "astro:content";
|
||||
import { type Lang } from "../content/config";
|
||||
|
||||
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" ? (
|
||||
<span>{user}</span>
|
||||
user.data.preferredLink == null ? (
|
||||
<span>{username}</span>
|
||||
) : (
|
||||
Object.entries(user).map(([k, v]) => (
|
||||
<a href={v} class="text-link underline" target="_blank">
|
||||
<span>{k}</span>
|
||||
</a>
|
||||
))[0]
|
||||
<a href={link} class="text-link underline" target="_blank">
|
||||
{username}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,39 +1,52 @@
|
|||
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 website = z.enum(WEBSITE_LIST);
|
||||
|
||||
export type User = z.output<typeof user>;
|
||||
export type Lang = z.output<typeof lang>;
|
||||
export type Website = z.infer<typeof website>;
|
||||
|
||||
const storiesCollection = defineCollection({
|
||||
type: "content",
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
// Required
|
||||
title: z.string(),
|
||||
shortTitle: z.string().optional(),
|
||||
pubDate: z.date(),
|
||||
isDraft: z.boolean().default(false),
|
||||
authors: z.union([user, z.array(user)]).default("Bad Manners"),
|
||||
wordCount: z.number().int(),
|
||||
contentWarning: 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(),
|
||||
summary: z.string().optional(),
|
||||
thumbnail: image().optional(),
|
||||
thumbnailWidth: z.number().int().optional(),
|
||||
thumbnailHeight: z.number().int().optional(),
|
||||
tags: z.array(z.string()),
|
||||
series: z.record(z.string(), z.string()).optional(),
|
||||
commissioner: user.optional(),
|
||||
requester: user.optional(),
|
||||
copyrightedCharacters: z.record(z.string(), user).optional(),
|
||||
commissioner: reference("users").optional(),
|
||||
requester: reference("users").optional(),
|
||||
copyrightedCharacters: z.record(z.string(), reference("users")).default({}),
|
||||
lang,
|
||||
prev: reference("stories").nullable().optional(),
|
||||
next: reference("stories").nullable().optional(),
|
||||
relatedStories: z.array(reference("stories")).optional(),
|
||||
relatedGames: z.array(reference("games")).optional(),
|
||||
relatedStories: z.array(reference("stories")).default([]),
|
||||
relatedGames: z.array(reference("games")).default([]),
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -41,26 +54,43 @@ const gamesCollection = defineCollection({
|
|||
type: "content",
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
// Required
|
||||
title: z.string(),
|
||||
pubDate: z.date(),
|
||||
isDraft: z.boolean().default(false),
|
||||
authors: z.union([user, z.array(user)]).default("Bad Manners"),
|
||||
contentWarning: 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(),
|
||||
thumbnail: image().optional(),
|
||||
thumbnailWidth: z.number().int().optional(),
|
||||
thumbnailHeight: z.number().int().optional(),
|
||||
tags: z.array(z.string()),
|
||||
series: z.record(z.string(), z.string()).optional(),
|
||||
copyrightedCharacters: z.record(z.string(), user).optional(),
|
||||
copyrightedCharacters: z.record(z.string(), reference("users")).default({}),
|
||||
lang,
|
||||
relatedStories: z.array(reference("stories")).optional(),
|
||||
relatedGames: z.array(reference("games")).optional(),
|
||||
relatedStories: z.array(reference("stories")).default([]),
|
||||
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 = {
|
||||
stories: storiesCollection,
|
||||
games: gamesCollection,
|
||||
users: usersCollection,
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Crossing Over
|
||||
pubDate: 2024-02-28
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
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.
|
||||
thumbnail: /src/assets/thumbnails/game_crossing_over_cover.png
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Accommodation
|
||||
pubDate: 2023-01-03
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 4800
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Addictive Additions
|
||||
pubDate: 2022-12-27
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 11200
|
||||
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.
|
||||
|
@ -31,9 +31,9 @@ tags:
|
|||
"netorare",
|
||||
"commission",
|
||||
]
|
||||
commissioner: { "Scion": "https://www.furaffinity.net/user/scionic" }
|
||||
commissioner: scion
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Annivoresary
|
||||
pubDate: 2022-08-08
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 3000
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Better in Bully Batter
|
||||
pubDate: 2023-02-20
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 19100
|
||||
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.
|
||||
|
@ -29,9 +29,9 @@ tags:
|
|||
"netorare",
|
||||
"commission",
|
||||
]
|
||||
commissioner: { "Scion": "https://www.furaffinity.net/user/scionic" }
|
||||
commissioner: scion
|
||||
copyrightedCharacters:
|
||||
"": { "Scion": "https://www.furaffinity.net/user/scionic" }
|
||||
"": scion
|
||||
---
|
||||
|
||||
## The first day
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Big Haul
|
||||
pubDate: 2023-11-20
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 9100
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Birdroom
|
||||
pubDate: 2023-05-17
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 3000
|
||||
contentWarning: >
|
||||
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",
|
||||
]
|
||||
copyrightedCharacters:
|
||||
Beetle: Bad Manners
|
||||
"Sam Brendan": Bad Manners
|
||||
Beetle: 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!"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Bladder Filler
|
||||
pubDate: 2023-11-08
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 1900
|
||||
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.
|
||||
|
@ -35,7 +35,7 @@ tags:
|
|||
"flash fiction",
|
||||
]
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Bottom of the Food Chain
|
||||
pubDate: 2023-09-27
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 4500
|
||||
contentWarning: >
|
||||
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
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Butting into Their Plans
|
||||
pubDate: 2023-02-18
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 1400
|
||||
contentWarning: >
|
||||
Contains: first person point-of-view, non-fatal size difference anal vore, willing male feral drake pred, willing PoV ambiguous anthro prey, rimming.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Delicacy's Dare
|
||||
pubDate: 2023-01-10
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 8000
|
||||
contentWarning: >
|
||||
Contains: Non-fatal micro oral vore, with willing male deer predator, willing to unwilling male dragon prey, and messy stomach with food digestion.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Eggs for Months
|
||||
pubDate: 2022-07-20
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 7700
|
||||
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).
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Engaging Contacts
|
||||
pubDate: 2023-08-08
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 6000
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Flavorful Favor
|
||||
pubDate: 2023-04-14
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 9400
|
||||
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.
|
||||
|
@ -37,7 +37,7 @@ tags:
|
|||
"gay sex",
|
||||
]
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: For the Night
|
||||
pubDate: 2022-12-26
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 1500
|
||||
contentWarning: >
|
||||
Contains: non-fatal same size anal vore, willing anthro male dog pred, willing anthro female pony prey, straight anal sex, threesome, sexuality play.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Gentle and Cruel
|
||||
pubDate: 2023-10-31
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 5200
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Hate to Sea It
|
||||
pubDate: 2023-02-18
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 1200
|
||||
contentWarning: >
|
||||
Contains: non-fatal size difference unbirth, willing female feral orca pred, unwilling to semi-willing male feral dolphin prey, straight sex, hate sex.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Hungry for Love
|
||||
pubDate: 2023-02-14
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 5900
|
||||
contentWarning: >
|
||||
Contains: Non-fatal size difference oral vore, with willing male spider predator, and willing female badger prey. Also includes straight sexual situations.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Hyper Hunger
|
||||
pubDate: 2022-12-05
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 1300
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Insistence and Assistance
|
||||
pubDate: 2022-12-05
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 1200
|
||||
contentWarning: >
|
||||
Contains: non-fatal same size oral vore, semi-willing anthro male cat pred, unwilling anthro male mouse prey, burping, regurgitation, force feeding, voyeurism.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Lactation Action
|
||||
pubDate: 2022-12-26
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 1400
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Latest Catch
|
||||
pubDate: 2022-12-26
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 1500
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Never Too Late
|
||||
pubDate: 2022-12-05
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 1100
|
||||
contentWarning: >
|
||||
Contains: non-fatal same size cock vore, asleep anthro male horse pred, willing anthro female aardwolf prey, masturbation, fellatio, alcohol.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Noble Fire
|
||||
pubDate: 2023-09-20
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 6900
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Overzealous Zenko
|
||||
pubDate: 2023-08-08
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 4900
|
||||
contentWarning: >
|
||||
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",
|
||||
"request",
|
||||
]
|
||||
requester: { "Dee Lumeni": "https://aryion.com/g4/user/KeeperofLillies" }
|
||||
requester: dee-lumeni
|
||||
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."
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Part of the Show
|
||||
pubDate: 2023-06-13
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 2000
|
||||
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.
|
||||
|
@ -29,7 +29,7 @@ tags:
|
|||
"flash fiction",
|
||||
]
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Pet-Sit Saturday
|
||||
pubDate: 2022-07-30
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 11000
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Reaching for the Full Moon
|
||||
pubDate: 2023-02-18
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 1300
|
||||
contentWarning: >
|
||||
Contains: non-fatal oral vore, smaller unwilling anthro male rabbit pred, bigger willing werewolf prey, forced vore, role reversal.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Ruffling Some Feathers
|
||||
pubDate: 2023-02-18
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 1000
|
||||
contentWarning: >
|
||||
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
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Spontaneous Sleepover
|
||||
pubDate: 2022-12-26
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 1300
|
||||
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.
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
---
|
||||
title: Taken In
|
||||
pubDate: 2024-01-22
|
||||
authors:
|
||||
- Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 5900
|
||||
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.
|
||||
|
@ -23,7 +22,7 @@ tags:
|
|||
"point of view",
|
||||
]
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Tasting High Consequences
|
||||
pubDate: 2023-04-20
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 6000
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Team Building
|
||||
pubDate: 2024-01-07
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 15100
|
||||
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.
|
||||
|
@ -33,9 +33,9 @@ tags:
|
|||
"gay sex",
|
||||
"commission",
|
||||
]
|
||||
commissioner: { "YolkMonkey": "https://furaffinity.net/user/Vampire101" }
|
||||
commissioner: yolkmonkey
|
||||
copyrightedCharacters:
|
||||
Yolk: { "YolkMonkey": "https://furaffinity.net/user/Vampire101" }
|
||||
Yolk: yolkmonkey
|
||||
prev: team-effort
|
||||
---
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Team Effort
|
||||
pubDate: 2023-08-08
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 11600
|
||||
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.
|
||||
|
@ -27,9 +27,9 @@ tags:
|
|||
"gay sex",
|
||||
"request",
|
||||
]
|
||||
requester: { "YolkMonkey": "https://furaffinity.net/user/Vampire101" }
|
||||
requester: yolkmonkey
|
||||
copyrightedCharacters:
|
||||
Yolk: { "YolkMonkey": "https://furaffinity.net/user/Vampire101" }
|
||||
Yolk: yolkmonkey
|
||||
next: team-building
|
||||
---
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: The Last Livestream
|
||||
pubDate: 2022-12-05
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 1400
|
||||
contentWarning: >
|
||||
Contains: non-fatal similar size unbirth, willing anthro female coatimundi pred, willing anthro female fennec fox prey, masturbation, livestreamed vore.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "The Lost of the Marshes – Bonus: Quince's Fantasy"
|
||||
shortTitle: "Bonus – Quince's Fantasy"
|
||||
pubDate: 2023-01-18
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 5800
|
||||
contentWarning: >
|
||||
Contains: macro and size difference, non-fatal oral vore. Also includes dream scenarios, role reversal, and self-vore.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "The Lost of the Marshes – Chapter 1: Found"
|
||||
shortTitle: "Chapter 1 – Found"
|
||||
pubDate: 2022-06-04
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 7300
|
||||
contentWarning: >
|
||||
Contains: macro, non-fatal oral vore.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "The Lost of the Marshes – Chapter 10: Memory"
|
||||
shortTitle: "Chapter 10 – Memory"
|
||||
pubDate: 2023-05-27
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 14600
|
||||
contentWarning: >
|
||||
Contains: macro with non-fatal oral vore, anal vore, cock vore, and slit vore. Also includes sexual situations, slight blood, and heavy themes.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "The Lost of the Marshes – Chapter 11: Familiar"
|
||||
shortTitle: "Chapter 11 – Familiar"
|
||||
pubDate: 2023-11-15
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 13600
|
||||
contentWarning: >
|
||||
Contains: non-fatal oral vore and cock vore, with size difference and macro. Also includes gay sexual situations.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "The Lost of the Marshes – Chapter 2: Trust"
|
||||
shortTitle: "Chapter 2 – Trust"
|
||||
pubDate: 2022-06-09
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 6900
|
||||
contentWarning: >
|
||||
Contains: macro, non-fatal oral vore, minor nudity.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "The Lost of the Marshes – Chapter 3: Home"
|
||||
shortTitle: "Chapter 3 – Home"
|
||||
pubDate: 2022-06-19
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 10800
|
||||
contentWarning: >
|
||||
Contains: macro and size difference, non-fatal oral vore, role reversal.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "The Lost of the Marshes – Chapter 4: Change"
|
||||
shortTitle: "Chapter 4 – Change"
|
||||
pubDate: 2022-07-12
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 12000
|
||||
contentWarning: >
|
||||
Contains: size difference, non-fatal oral vore, live feeding, sexual nudity.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "The Lost of the Marshes – Chapter 5: Intersection"
|
||||
shortTitle: "Chapter 5 – Intersection"
|
||||
pubDate: 2022-08-04
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 8600
|
||||
contentWarning: >
|
||||
Contains: size difference, non-fatal oral vore. Also includes heavy themes.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "The Lost of the Marshes – Chapter 6: Pleasure"
|
||||
shortTitle: "Chapter 6 – Pleasure"
|
||||
pubDate: 2022-10-22
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 9000
|
||||
contentWarning: >
|
||||
Contains: size difference, non-fatal oral vore and unbirth, gay sex, masturbation.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "The Lost of the Marshes – Chapter 7: Honesty"
|
||||
shortTitle: "Chapter 7 – Honesty"
|
||||
pubDate: 2022-11-23
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 6500
|
||||
contentWarning: >
|
||||
Contains: macro and size difference, non-fatal oral and anal vore, gay sex.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "The Lost of the Marshes – Chapter 8: Estranged"
|
||||
shortTitle: "Chapter 8 – Estranged"
|
||||
pubDate: 2022-12-08
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 7500
|
||||
contentWarning: >
|
||||
Contains: macro and size difference, non-fatal oral and anal vore.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
title: "The Lost of the Marshes – Chapter 9: Stuck"
|
||||
shortTitle: "Chapter 9 – Stuck"
|
||||
pubDate: 2023-01-31
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 11100
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: tomo moku
|
||||
pubDate: 2023-04-01
|
||||
authors: nasin ike Pemene
|
||||
authors: bad-manners
|
||||
wordCount: 1200
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Trouble Sleeping
|
||||
pubDate: 2023-09-20
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 4800
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Warped Friendship
|
||||
pubDate: 2023-08-08
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 7800
|
||||
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.
|
||||
|
@ -24,10 +24,10 @@ tags:
|
|||
"long-term endo",
|
||||
"request",
|
||||
]
|
||||
requester: { "Avour Inden": "https://furaffinity.net/user/pppp0000" }
|
||||
requester: avour-inden
|
||||
copyrightedCharacters:
|
||||
Avour: { "Avour Inden": "https://furaffinity.net/user/pppp0000" }
|
||||
Buster: { "Holi": "https://furaffinity.net/user/CinnamonStars" }
|
||||
Avour: avour-inden
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Within Limits
|
||||
pubDate: 2023-12-05
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 14500
|
||||
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.
|
||||
|
@ -35,9 +35,9 @@ tags:
|
|||
"orgy",
|
||||
"commission",
|
||||
]
|
||||
commissioner: { "Asof Yeun": "https://www.furaffinity.net/user/AsofYeun/" }
|
||||
commissioner: asofyeun
|
||||
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.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: You're Home
|
||||
pubDate: 2022-11-10
|
||||
authors: Bad Manners
|
||||
authors: bad-manners
|
||||
wordCount: 11300
|
||||
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.
|
||||
|
|
11
src/content/users/asofyeun.json
Normal file
11
src/content/users/asofyeun.json
Normal 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"
|
||||
}
|
7
src/content/users/avour-inden.json
Normal file
7
src/content/users/avour-inden.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "Avour Inden",
|
||||
"links": {
|
||||
"furaffinity": "https://furaffinity.net/user/pppp0000"
|
||||
},
|
||||
"preferredLink": "furaffinity"
|
||||
}
|
20
src/content/users/bad-manners.json
Normal file
20
src/content/users/bad-manners.json
Normal 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
|
||||
}
|
7
src/content/users/dee-lumeni.json
Normal file
7
src/content/users/dee-lumeni.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "Dee Lumeni",
|
||||
"links": {
|
||||
"eka": ["https://aryion.com/g4/user/KeeperofLillies", "KeeperofLillies"]
|
||||
},
|
||||
"preferredLink": "eka"
|
||||
}
|
7
src/content/users/holi.json
Normal file
7
src/content/users/holi.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "Holi",
|
||||
"links": {
|
||||
"furaffinity": ["https://furaffinity.net/user/CinnamonStars", "CinnamonStars"]
|
||||
},
|
||||
"preferredLink": "furaffinity"
|
||||
}
|
8
src/content/users/scion.json
Normal file
8
src/content/users/scion.json
Normal 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"
|
||||
}
|
8
src/content/users/yolkmonkey.json
Normal file
8
src/content/users/yolkmonkey.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "YolkMonkey",
|
||||
"links": {
|
||||
"furaffinity": ["https://furaffinity.net/user/Vampire101", "Vampire101"],
|
||||
"sofurry": ["https://vampire101.sofurry.com/", "Vampire101"]
|
||||
},
|
||||
"preferredLink": "furaffinity"
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
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 { format as formatDate } from "date-fns";
|
||||
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"];
|
||||
|
||||
const { props } = Astro;
|
||||
//const relatedStories = (await Promise.all((props.relatedStories || []).map(story => getEntry(story)))).filter(story => !story.data.isDraft)
|
||||
// const relatedGames = (await Promise.all((props.relatedGames || []).map(game => getEntry(game)))).filter(game => !game.data.isDraft)
|
||||
const authors = await getEntries([props.authors].flat());
|
||||
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}>
|
||||
|
@ -74,7 +83,7 @@ const { props } = Astro;
|
|||
id="game-information"
|
||||
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 ? (
|
||||
<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>
|
||||
<Prose>
|
||||
<Markdown of={props.description} />
|
||||
<CopyrightedCharacters copyrightedCharacters={props.copyrightedCharacters} lang={props.lang} />
|
||||
<CopyrightedCharacters copyrightedCharacters={copyrightedCharacters} lang={props.lang} />
|
||||
</Prose>
|
||||
</section>
|
||||
<div class="pr-3 text-right print:hidden">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
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 { format as formatDate } from "date-fns";
|
||||
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) {
|
||||
next = undefined;
|
||||
}
|
||||
const relatedStories = (await Promise.all((props.relatedStories || []).map((story) => getEntry(story)))).filter(
|
||||
(story) => !story.data.isDraft,
|
||||
);
|
||||
// const relatedGames = (await Promise.all((props.relatedGames || []).map(game => getEntry(game)))).filter(
|
||||
const authors = await getEntries([props.authors].flat());
|
||||
const commissioner = props.commissioner && (await getEntry(props.commissioner));
|
||||
const requester = props.requester && (await getEntry(props.requester));
|
||||
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,
|
||||
// );
|
||||
---
|
||||
|
@ -122,7 +127,7 @@ const relatedStories = (await Promise.all((props.relatedStories || []).map((stor
|
|||
id="story-information"
|
||||
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 ? (
|
||||
<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
|
||||
}
|
||||
{
|
||||
props.commissioner && (
|
||||
commissioner && (
|
||||
<p id="commissioner">
|
||||
Commissioned by <UserComponent user={props.commissioner} />
|
||||
Commissioned by <UserComponent user={commissioner} lang={props.lang} />
|
||||
</p>
|
||||
)
|
||||
}
|
||||
{
|
||||
props.requester && (
|
||||
requester && (
|
||||
<p id="requester">
|
||||
Requested by <UserComponent user={props.requester} />
|
||||
Requested by <UserComponent user={requester} lang={props.lang} />
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
@ -196,7 +201,7 @@ const relatedStories = (await Promise.all((props.relatedStories || []).map((stor
|
|||
</h2>
|
||||
<Prose>
|
||||
<Markdown of={props.description} />
|
||||
<CopyrightedCharacters copyrightedCharacters={props.copyrightedCharacters} lang={props.lang} />
|
||||
<CopyrightedCharacters copyrightedCharacters={copyrightedCharacters} lang={props.lang} />
|
||||
</Prose>
|
||||
</section>
|
||||
<div class="pr-3 text-right print:hidden">
|
||||
|
@ -281,7 +286,9 @@ const relatedStories = (await Promise.all((props.relatedStories || []).map((stor
|
|||
</main>
|
||||
<div class="pt-6 text-center text-xs text-black dark:text-white">
|
||||
<span>© {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>
|
||||
</AgeRestrictedBaseLayout>
|
||||
|
|
|
@ -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>
|
|
@ -8,11 +8,11 @@ type FeedItem = RSSFeedItem & {
|
|||
};
|
||||
|
||||
export const GET: APIRoute = async ({ site }) => {
|
||||
const stories = (await getCollection("stories")).filter((story) => !story.data.isDraft);
|
||||
const games = (await getCollection("games")).filter((game) => !game.data.isDraft);
|
||||
const stories = await getCollection("stories", (story) => !story.data.isDraft);
|
||||
const games = await getCollection("games", (game) => !game.data.isDraft);
|
||||
return rss({
|
||||
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,
|
||||
items: [
|
||||
stories.map<FeedItem>((story) => ({
|
||||
|
@ -21,7 +21,7 @@ export const GET: APIRoute = async ({ site }) => {
|
|||
link: `/stories/${story.slug}`,
|
||||
description:
|
||||
`Word count: ${story.data.wordCount}. ${story.data.contentWarning} ${story.data.descriptionPlaintext || story.data.description}`
|
||||
.replaceAll(/\n+| +/g, " ")
|
||||
.replaceAll(/[\n ]+/g, " ")
|
||||
.trim(),
|
||||
categories: ["story"],
|
||||
})),
|
||||
|
@ -30,7 +30,7 @@ export const GET: APIRoute = async ({ site }) => {
|
|||
pubDate: addHours(game.data.pubDate, 12),
|
||||
link: `/games/${game.slug}`,
|
||||
description: `${game.data.contentWarning} ${game.data.descriptionPlaintext || game.data.description}`
|
||||
.replaceAll(/\n+| +/g, " ")
|
||||
.replaceAll(/[\n ]+/g, " ")
|
||||
.trim(),
|
||||
categories: ["game"],
|
||||
})),
|
||||
|
|
|
@ -5,9 +5,9 @@ import { getUnixTime, format as formatDate } from "date-fns";
|
|||
import { enUS as enUSLocale } from "date-fns/locale/en-US";
|
||||
import GalleryLayout from "../layouts/GalleryLayout.astro";
|
||||
|
||||
const games = (await getCollection("games"))
|
||||
.filter((game) => !game.data.isDraft)
|
||||
.sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate));
|
||||
const games = (await getCollection("games", (game) => !game.data.isDraft)).sort(
|
||||
(a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate),
|
||||
);
|
||||
---
|
||||
|
||||
<GalleryLayout pageTitle="Games">
|
||||
|
@ -18,7 +18,7 @@ const games = (await getCollection("games"))
|
|||
games.map((game) => (
|
||||
<li>
|
||||
<a class="text-link hover:underline focus:underline" href={`/games/${game.slug}`}>
|
||||
{game.data.thumbnail && (
|
||||
{game.data.thumbnail ? (
|
||||
<Image
|
||||
class="max-w-72"
|
||||
src={game.data.thumbnail}
|
||||
|
@ -26,7 +26,7 @@ const games = (await getCollection("games"))
|
|||
width={game.data.thumbnailWidth}
|
||||
height={game.data.thumbnailHeight}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<div class="max-w-72 text-sm">
|
||||
<>
|
||||
<span>{game.data.title}</span>
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
---
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import GameLayout from "../../layouts/GameLayout.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const games = await getCollection("games");
|
||||
return games.map((story) => ({
|
||||
params: { slug: story.slug },
|
||||
props: story,
|
||||
return games.map((game) => ({
|
||||
params: { slug: game.slug },
|
||||
props: game,
|
||||
}));
|
||||
}
|
||||
};
|
||||
type Props = CollectionEntry<"games">;
|
||||
|
||||
const story = Astro.props;
|
||||
const { Content } = await story.render();
|
||||
const game = Astro.props;
|
||||
const { Content } = await game.render();
|
||||
---
|
||||
|
||||
<GameLayout {...story.data}>
|
||||
<GameLayout {...game.data}>
|
||||
<Content />
|
||||
</GameLayout>
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
---
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import StoryLayout from "../../layouts/StoryLayout.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const stories = await getCollection("stories");
|
||||
return stories.map((story) => ({
|
||||
params: { slug: story.slug },
|
||||
props: story,
|
||||
}));
|
||||
}
|
||||
};
|
||||
type Props = CollectionEntry<"stories">;
|
||||
|
||||
const story = Astro.props;
|
||||
|
|
|
@ -1,17 +1,23 @@
|
|||
---
|
||||
import type { GetStaticPathsOptions } from "astro";
|
||||
import type { GetStaticPaths, Page } from "astro";
|
||||
import { Image } from "astro:assets";
|
||||
import { getCollection } from "astro:content";
|
||||
import { getUnixTime, format as formatDate } from "date-fns";
|
||||
import { enUS as enUSLocale } from "date-fns/locale/en-US";
|
||||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
|
||||
const stories = (await getCollection("stories"))
|
||||
.filter((story) => !story.data.isDraft)
|
||||
.sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate));
|
||||
export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
|
||||
const stories = (await getCollection("stories", (story) => !story.data.isDraft)).sort(
|
||||
(a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate),
|
||||
);
|
||||
return paginate(stories, { pageSize: 30 });
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
page: Page<CollectionEntry<"stories">>;
|
||||
};
|
||||
|
||||
const { page } = Astro.props;
|
||||
const totalPages = Math.ceil(page.total / page.size);
|
||||
---
|
||||
|
@ -60,7 +66,7 @@ const totalPages = Math.ceil(page.total / page.size);
|
|||
page.data.map((story) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`}>
|
||||
{story.data.thumbnail && (
|
||||
{story.data.thumbnail ? (
|
||||
<Image
|
||||
class="w-48"
|
||||
src={story.data.thumbnail}
|
||||
|
@ -68,7 +74,7 @@ const totalPages = Math.ceil(page.total / page.size);
|
|||
width={story.data.thumbnailWidth}
|
||||
height={story.data.thumbnailHeight}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<div class="max-w-48 text-sm">
|
||||
<>
|
||||
<span>{story.data.title}</span>
|
||||
|
|
274
src/pages/stories/export/[website]/[...slug].ts
Normal file
274
src/pages/stories/export/[website]/[...slug].ts
Normal 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 });
|
||||
};
|
|
@ -5,7 +5,8 @@ import { getUnixTime } from "date-fns";
|
|||
import GalleryLayout from "../../layouts/GalleryLayout.astro";
|
||||
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/"),
|
||||
);
|
||||
const mainChapters = stories
|
||||
|
@ -50,7 +51,7 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ
|
|||
mainChapters.map((story) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`}>
|
||||
{story.data.thumbnail && (
|
||||
{story.data.thumbnail ? (
|
||||
<Image
|
||||
class="w-48"
|
||||
src={story.data.thumbnail}
|
||||
|
@ -58,7 +59,7 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ
|
|||
width={story.data.thumbnailWidth}
|
||||
height={story.data.thumbnailHeight}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<div class="max-w-48 text-sm">{story.data.title}</div>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -73,7 +74,7 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ
|
|||
bonusChapters.map((story) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`}>
|
||||
{story.data.thumbnail && (
|
||||
{story.data.thumbnail ? (
|
||||
<Image
|
||||
class="w-48"
|
||||
src={story.data.thumbnail}
|
||||
|
@ -81,7 +82,7 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ
|
|||
width={story.data.thumbnailWidth}
|
||||
height={story.data.thumbnailHeight}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<div class="max-w-48 text-sm">{story.data.title}</div>
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -3,47 +3,46 @@ import { getCollection } from "astro:content";
|
|||
import { slug } from "github-slugger";
|
||||
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 seriesList: Record<string, string> = {};
|
||||
stories
|
||||
.filter((story) => !story.data.isDraft)
|
||||
.forEach((story) => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
stories.forEach((story) => {
|
||||
story.data.tags.forEach((tag) => {
|
||||
tagsSet.add(tag);
|
||||
});
|
||||
games
|
||||
.filter((game) => !game.data.isDraft)
|
||||
.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;
|
||||
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.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[]> = {
|
||||
"Types of vore": [
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
---
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { Image } from "astro:assets";
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import { slug } from "github-slugger";
|
||||
import { getUnixTime } from "date-fns";
|
||||
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 tags = new Set<string>();
|
||||
stories.forEach((story) => {
|
||||
|
@ -32,7 +33,7 @@ export async function getStaticPaths() {
|
|||
.sort((a, b) => getUnixTime(b.data.pubDate) - getUnixTime(a.data.pubDate)),
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
tag: string;
|
||||
|
@ -55,7 +56,7 @@ const { tag, stories, games } = Astro.props;
|
|||
{stories.map((story) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a class="text-link hover:underline focus:underline" href={`/stories/${story.slug}`}>
|
||||
{story.data.thumbnail && (
|
||||
{story.data.thumbnail ? (
|
||||
<Image
|
||||
class="w-48"
|
||||
src={story.data.thumbnail}
|
||||
|
@ -63,7 +64,7 @@ const { tag, stories, games } = Astro.props;
|
|||
width={story.data.thumbnailWidth}
|
||||
height={story.data.thumbnailHeight}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<div class="max-w-48 text-sm">{story.data.title}</div>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -82,7 +83,7 @@ const { tag, stories, games } = Astro.props;
|
|||
{games.map((game) => (
|
||||
<li class="break-inside-avoid">
|
||||
<a class="text-link hover:underline focus:underline" href={`/games/${game.slug}`}>
|
||||
{game.data.thumbnail && (
|
||||
{game.data.thumbnail ? (
|
||||
<Image
|
||||
class="w-48"
|
||||
src={game.data.thumbnail}
|
||||
|
@ -90,7 +91,7 @@ const { tag, stories, games } = Astro.props;
|
|||
width={game.data.thumbnailWidth}
|
||||
height={game.data.thumbnailHeight}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<div class="max-w-48 text-sm">{game.data.title}</div>
|
||||
</a>
|
||||
</li>
|
||||
|
|
Loading…
Add table
Reference in a new issue