Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions frontend/app/[lang]/exporter/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Metadata } from "next";
import ExporterBody from "@/app/exporter/ExporterBody";
import {
isValidLang,
LANG_GAME_NAME,
LANG_NAMES,
LANG_HREFLANG,
type LangCode,
} from "@/lib/languages";
import { buildLanguageAlternates, DEFAULT_OG_IMAGE, SITE_NAME, SITE_URL } from "@/lib/seo";
import { t } from "@/lib/ui-translations";

const CATEGORY = "exporter";

export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise<Metadata> {
const { lang } = await params;
if (!isValidLang(lang)) return {};

const langCode = lang as LangCode;
const gameName = LANG_GAME_NAME[langCode];
const nativeName = LANG_NAMES[langCode];

const title = `${gameName} ${t("Art Exporter", lang)} | Spire Codex (${nativeName})`;
const description = `The tool that generates every image on Spire Codex, free on the Steam Workshop for ${gameName}. Card renders, Spine character art, animations, and texture dumps. ${nativeName}.`;

return {
title,
description,
openGraph: {
type: "website",
siteName: SITE_NAME,
url: `${SITE_URL}/${lang}/${CATEGORY}`,
title,
description,
locale: LANG_HREFLANG[langCode],
images: [{ url: DEFAULT_OG_IMAGE }],
},
twitter: { card: "summary_large_image", title, description },
alternates: {
canonical: `/${lang}/${CATEGORY}`,
languages: buildLanguageAlternates(`/${CATEGORY}`),
},
};
}

export default async function LangExporterPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
if (!isValidLang(lang)) return null;
return <ExporterBody lang={lang} />;
}
3 changes: 2 additions & 1 deletion frontend/app/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const NAV_GROUPS: NavGroup[] = [
links: [
{ href: "/tier-list-maker", label: "Tier List Maker" },
{ href: "/mod", label: "Steam Mod" },
{ href: "/exporter", label: "Art Exporter" },
{ href: "/overlay", label: "Overlay (Overwolf)" },
{ href: "/showcase", label: "Showcase" },
{ href: "/knowledge-demon", label: "Knowledge Demon", badge: "discord-bot" },
Expand Down Expand Up @@ -174,7 +175,7 @@ const NAV_COLUMNS: Record<string, { title: string; labels: string[] }[]> = {
],
Tools: [
{ title: "Make & share", labels: ["Tier List Maker", "Showcase"] },
{ title: "Companion apps", labels: ["Steam Mod", "Overlay (Overwolf)", "Knowledge Demon"] },
{ title: "Companion apps", labels: ["Steam Mod", "Art Exporter", "Overlay (Overwolf)", "Knowledge Demon"] },
{ title: "Build with the data", labels: ["Developers", "API"] },
],
About: [
Expand Down
127 changes: 127 additions & 0 deletions frontend/app/exporter/ExporterBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { t } from "@/lib/ui-translations";

const WORKSHOP_URL = "https://steamcommunity.com/sharedfiles/filedetails/?id=3776092288";

const EXPORTS = [
"Cards: every card at every upgrade level, plus beta art variants, per-language card text, the enchant and affliction attachment matrix, and the card frame template pieces",
"Ancient cards: animated, 10 flame frames per card, same as the site",
"Characters and monsters: high-res Spine renders, including skin variants",
"Character form VFX: stills plus real-time animated frame sequences (needs a game build with forms, v0.110+)",
"Animation frames: per-frame PNGs for creature animations",
"Relics and potions",
"Backgrounds: fully composited scenes like the character select",
"Full texture dump: everything in the game files plus the atlases",
];

const STEPS = [
"Subscribe on the Steam Workshop and Steam downloads the mod on its own",
"Click Spire Codex Exporter on the game's main menu, tick what you want, and pick an output folder",
"Hit Extract. Animated exports write a manifest.json with the measured frame rate, so you can turn the frames into webp or gif with your own tooling",
];

const NOTES = [
"The big exports (attachment matrix, animation frames, texture dump) take a while and use a lot of disk. The defaults are the sane set.",
"All exported art belongs to MegaCrit. Exports are for personal use, wikis, and community content.",
"It runs entirely on your machine and touches nothing online. No dependencies, no gameplay changes.",
];

export default function ExporterBody({ lang }: { lang: string }) {
return (
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h1 className="text-3xl font-bold mb-2">
<span className="text-[var(--accent-gold)]">Spire Codex</span>{" "}
<span className="text-[var(--text-primary)]">{t("Art Exporter", lang)}</span>
</h1>
<p className="text-[var(--text-secondary)] text-lg leading-relaxed mb-3">
{t("The tool that generates every image on Spire Codex, released so anyone can use it. It renders the game's art straight out of the running engine and saves full-quality PNGs drawn by the game itself, not unpacked or datamined files.", lang)}
</p>
<p className="text-[var(--text-secondary)] leading-relaxed mb-8">
{t("Useful for wikis, content creators, thumbnail makers, and anyone who wants clean renders of the game's art.", lang)}
</p>

<div className="flex flex-wrap gap-3 mb-12">
<a
href={WORKSHOP_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center px-5 py-2.5 rounded-lg bg-[var(--accent-gold)] text-[var(--bg-primary)] font-semibold hover:opacity-90 transition-opacity"
>
{t("Subscribe on the Steam Workshop", lang)}
</a>
</div>

{/* What it exports */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-[var(--accent-gold)] mb-4">
{t("What it exports", lang)}
</h2>
<div className="bg-[var(--bg-card)] rounded-xl border border-[var(--border-subtle)] p-5">
<ul className="space-y-2.5 text-sm text-[var(--text-secondary)]">
{EXPORTS.map((item) => (
<li key={item} className="flex gap-3">
<span aria-hidden className="text-[var(--accent-gold)] shrink-0">
</span>
<span>{t(item, lang)}</span>
</li>
))}
</ul>
</div>
</section>

{/* How to use it */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-[var(--accent-gold)] mb-4">
{t("How to use it", lang)}
</h2>
<div className="bg-[var(--bg-card)] rounded-xl border border-[var(--border-subtle)] divide-y divide-[var(--border-subtle)]">
{STEPS.map((step, i) => (
<div key={step} className="flex items-start gap-4 px-4 py-3">
<span className="text-sm font-bold text-[var(--accent-gold)] tabular-nums shrink-0 w-5">
{i + 1}.
</span>
<span className="text-sm text-[var(--text-secondary)]">{t(step, lang)}</span>
</div>
))}
</div>
</section>

{/* Notes */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-[var(--accent-gold)] mb-4">
{t("Good to know", lang)}
</h2>
<div className="bg-[var(--bg-card)] rounded-xl border border-[var(--border-subtle)] p-5">
<ul className="space-y-2.5 text-sm text-[var(--text-secondary)]">
{NOTES.map((item) => (
<li key={item} className="flex gap-3">
<span aria-hidden className="text-[var(--accent-gold)] shrink-0">
</span>
<span>{t(item, lang)}</span>
</li>
))}
</ul>
</div>
</section>

{/* CTA repeated at bottom for long-page reads */}
<section>
<div className="bg-[var(--bg-card)] rounded-xl border border-[var(--border-subtle)] p-6 text-center">
<h2 className="text-xl font-semibold mb-2">{t("Get the exporter", lang)}</h2>
<p className="text-sm text-[var(--text-secondary)] mb-4">
{t("Free, open, and you can unsubscribe from the Workshop at any time.", lang)}
</p>
<a
href={WORKSHOP_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center px-5 py-2.5 rounded-lg bg-[var(--accent-gold)] text-[var(--bg-primary)] font-semibold hover:opacity-90 transition-opacity"
>
{t("Subscribe on the Steam Workshop", lang)}
</a>
</div>
</section>
</div>
);
}
27 changes: 27 additions & 0 deletions frontend/app/exporter/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Metadata } from "next";
import { DEFAULT_OG_IMAGE, SITE_NAME, SITE_URL } from "@/lib/seo";

const title = "Art Exporter - Slay the Spire 2 (sts2) | Spire Codex";
const ogDesc = "The tool that generates every image on Spire Codex, free on the Steam Workshop. Card renders, Spine character art, animations, and full texture dumps.";

export const metadata: Metadata = {
title,
description:
"The Spire Codex Art Exporter for Slay the Spire 2 (sts2), free on the Steam Workshop. It renders card art at every upgrade level, characters and monsters, animations, backgrounds, and full texture dumps straight from the running game.",
openGraph: {
type: "website",
siteName: SITE_NAME,
url: `${SITE_URL}/exporter`,
title,
description: ogDesc,
images: [{ url: DEFAULT_OG_IMAGE }],
},
twitter: { card: "summary_large_image", title, description: ogDesc },
alternates: {
canonical: "/exporter",
},
};

export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
5 changes: 5 additions & 0 deletions frontend/app/exporter/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import ExporterBody from "./ExporterBody";

export default function ExporterPage() {
return <ExporterBody lang="eng" />;
}
1 change: 1 addition & 0 deletions frontend/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const LANG_LIST_ROUTES = [
"images",
"about",
"mod",
"exporter",
"overlay",
"knowledge-demon",
"giveaway",
Expand Down
Loading
Loading