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
|
```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
27
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 © <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>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
) : null
|
) : 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 = {
|
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]
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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!"
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
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 { 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">
|
||||||
|
|
|
@ -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>© {formatDate(props.pubDate, "yyyy")} | </span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</AgeRestrictedBaseLayout>
|
</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 }) => {
|
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"],
|
||||||
})),
|
})),
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
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 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>
|
||||||
|
|
|
@ -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": [
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue