feat(site): webshops rewrite + demo gallery + Bloomword/Meatspace/Mushroom Capital

- /products/webshops: lead with per-tenant theming; add the real platform
  features (Printful POD, non-custodial crypto to own wallet, Stripe Connect
  with item-margin-only fee framing, SIWE/wallet login, Telegram + email
  notifications, automated tax); fix the incorrect "Vercel/Railway" hosting
  line to fully-managed self-hosting; remove PayPal/Coinbase (not live).
- Live Demos gallery (new component) with real screenshots of the 5 themed
  demo storefronts.
- Add three projects to the portfolio: Bloomword (now live, bloomword.earth),
  Meatspace (in dev, meatspace.place), Mushroom Capital (coming soon,
  mushroom.capital) with an icon, a detail page (VideoGame + FAQ JSON-LD),
  and a sitemap entry each.
- Copy: avoid em dashes (colon/semicolon/comma) across the products area.

Verified: tsc clean, next build green (50/50 static pages).
This commit is contained in:
jwinterm
2026-07-06 08:28:39 -04:00
parent c15dd74a8d
commit cbc8906715
15 changed files with 724 additions and 53 deletions
@@ -0,0 +1,144 @@
import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav";
import { Card } from "@repo/ui/components/card";
import Link from "next/link";
import Image from "next/image";
import { JsonLd, breadcrumbLd, faqLd } from "@/components/seo/json-ld";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Bloomword: Daily Word Game with a Living Tree | Such Software",
description:
"Bloomword is a daily word game where everyone gets the same letters and every word you find grows a living 3D typographic tree. Five biomes, streaks, and a bloom to share. Coming to iOS and Android.",
alternates: { canonical: "/products/bloomword" },
keywords: [
"daily word game",
"word puzzle game",
"typography game",
"3d word game",
"word tree game",
"wordle alternative",
"bloomword",
],
openGraph: {
title: "Bloomword: Find the Day's Words, Grow a Living Tree",
description:
"A daily word game where every word you find grows a biome-specific, living 3D typographic tree. Coming to iOS and Android from Such Software.",
type: "website",
url: "/products/bloomword",
},
};
const faqs = [
{ q: "What is Bloomword?", a: "A daily word game. Everyone gets the same set of letters each day and races to find words before the sunset, and every valid word you find grows a living 3D tree made of typography. It is designed as a calm daily habit: find your words, watch your tree bloom, keep your streak." },
{ q: "How does the tree work?", a: "Each word you find sprouts new growth on a 3D typographic tree rendered by our in-house typograph-engine. The tree's look changes with the day's biome, so your finished board is both a score and a little piece of generative art you can share." },
{ q: "Is it the same puzzle for everyone?", a: "Yes. Like the best daily games, the letters are shared worldwide each day, so leaderboards and streaks are apples-to-apples. Results are server-replayed for fairness." },
{ q: "Is it free?", a: "Yes, free to play, supported by light ads and optional in-app purchases. A privacy-conscious build is a priority." },
{ q: "What platforms will it be on?", a: "Built in Godot for iOS and Android. It is in final pre-launch polish (store review); follow Such Software for the release date." },
];
const jsonLd = {
"@context": "https://schema.org",
"@type": "VideoGame",
name: "Bloomword",
url: "https://such.software/products/bloomword",
description:
"A daily word game where everyone gets the same letters and every word you find grows a living 3D typographic tree. Five biomes, streaks, and a bloom to share.",
applicationCategory: "Game",
genre: ["Word", "Puzzle", "Casual"],
gamePlatform: ["iOS", "Android"],
author: { "@type": "Organization", name: "Such Software" },
publisher: { "@type": "Organization", name: "Such Software" },
};
const features = [
{ title: "One shared daily puzzle", body: "Everyone gets the same letters each day and races the sunset. Server-replayed results keep streaks and leaderboards honest." },
{ title: "Words become a living tree", body: "Every word you find grows a 3D typographic tree, rendered in real time by our in-house typograph-engine. Your finished board is a shareable piece of generative art." },
{ title: "Five biomes", body: "Forest, desert, mushroom, kelp, and prairie; each biome gives the day's tree its own palette and character, so no two blooms look alike." },
{ title: "Built for the daily habit", body: "Streaks and biome collections reward showing up. It is a calm, one-a-day ritual, not an attention trap." },
{ title: "Play with anyone", body: "A shared daily seed and Nakama-backed leaderboards mean you can compare with friends or the whole world on the exact same board." },
{ title: "Free and privacy-conscious", body: "Free to play with light ads and optional purchases, built to respect your data." },
];
export default function BloomwordPage() {
return (
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground">
<JsonLd data={breadcrumbLd([
{ name: "Home", path: "/" },
{ name: "Products", path: "/products" },
{ name: "Bloomword", path: "/products/bloomword" },
])} />
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<JsonLd data={faqLd(faqs)} />
<Header />
<div id="main-content" className="z-10 w-full max-w-4xl mx-auto py-20 px-4 pb-24 md:pb-20">
<Link href="/products" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">
← Back to Products
</Link>
<div className="flex items-center gap-4 mb-2">
<Image src="/images/products/bloomword.svg" alt="Bloomword icon" width={64} height={64} className="rounded-2xl" />
<div>
<span className="inline-flex items-center rounded-full bg-green-400/15 px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.2em] text-green-600 dark:text-green-400 mb-1">
Now Live
</span>
<h1 className="text-4xl md:text-5xl font-bold text-green-600 dark:text-green-400 leading-none">
Bloomword
</h1>
</div>
</div>
<p className="text-base text-muted-foreground mb-6">Find the day&rsquo;s words. Grow a living tree.</p>
<div className="prose dark:prose-invert max-w-none mb-12">
<p className="text-xl text-muted-foreground leading-relaxed">
Bloomword is a daily word game with a twist you can watch grow. Everyone gets the same
letters each day and races the sunset to find words, and every word you find sprouts new
growth on a living 3D typographic tree. Keep your streak, collect the biomes, and share the
bloom you grew today. It is live at{" "}
<a href="https://bloomword.earth" target="_blank" rel="noopener noreferrer" className="text-green-600 dark:text-green-400 hover:underline">bloomword.earth</a>{" "}
and coming to iOS and Android.
</p>
</div>
<h2 className="text-2xl font-bold mb-6">What makes it different</h2>
<div className="grid md:grid-cols-2 gap-6 mb-12">
{features.map((f) => (
<Card key={f.title} className="glass-card border-green-500/20">
<h3 className="text-lg font-bold mb-2">{f.title}</h3>
<p className="text-sm text-muted-foreground">{f.body}</p>
</Card>
))}
</div>
<h2 className="text-2xl font-bold mb-6">Under the Hood</h2>
<div className="section-container border-l-4 border-green-500 mb-12">
<ul className="space-y-2 text-muted-foreground">
<li><strong>Engine:</strong> Godot 4 with a Rust GDExtension, one codebase for iOS and Android.</li>
<li><strong>The tree:</strong> a custom typograph-engine renders words as living 3D type in real time.</li>
<li><strong>Fair daily play:</strong> a shared daily seed with Nakama-backed, server-replayed leaderboards.</li>
<li><strong>Free to play:</strong> light ads and optional in-app purchases, built privacy-first.</li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-6">FAQ</h2>
<div className="space-y-6 mb-12">
{faqs.map((f) => (
<div key={f.q}>
<h3 className="font-bold mb-1 text-green-600 dark:text-green-400">{f.q}</h3>
<p className="text-muted-foreground">{f.a}</p>
</div>
))}
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground flex-wrap">
<Link href="/products" className="hover:text-foreground transition-colors">All Products</Link>
<span>•</span>
<a href="https://bloomword.earth" target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">bloomword.earth</a>
<span>•</span>
<span>Contact: apps@such.software</span>
</div>
</div>
<MobileNav />
</main>
);
}
@@ -0,0 +1,143 @@
import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav";
import { Card } from "@repo/ui/components/card";
import Link from "next/link";
import Image from "next/image";
import { JsonLd, breadcrumbLd, faqLd } from "@/components/seo/json-ld";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Meatspace: A Deduction Game After the Upload | Such Software",
description:
"Meatspace is a deduction game set in the ruins of the Ingestion: humanity's failed mass upload into the Lattice. Reconstruct a lost person from fragments, through a living meat-vs-cyber crossfade. In development.",
alternates: { canonical: "/products/meatspace" },
keywords: [
"deduction game",
"narrative mystery game",
"mind upload game",
"sci-fi horror game",
"reconstruction game",
"multiplayer deduction",
"meatspace",
],
openGraph: {
title: "Meatspace: Find Who You Lost in the Wreck of the Upload",
description:
"A deduction game in the ruins of a failed mass mind-upload, with a living meat-vs-cyber crossfade and an optional multiplayer mode. In development by Such Software.",
type: "website",
url: "/products/meatspace",
},
};
const faqs = [
{ q: "What is Meatspace?", a: "A deduction game set after the Ingestion: humanity's failed attempt to upload itself en masse into a digital substrate called the Lattice. You sift through the fragments of the dead to reconstruct one specific lost person, and the way you piece them together shapes an ending you won't fully understand until you reach it." },
{ q: "What is the Ingestion?", a: "The fiction's central catastrophe: the mass upload that was supposed to save everyone and instead left a ruin of half-copied minds. The game lives in that wreckage: part meat, part cyber, neither settled." },
{ q: "Is it single-player or multiplayer?", a: "Primarily a single-player deduction experience, with an optional realtime multiplayer mode called The Wake, built on Nakama, for playing the reconstruction together." },
{ q: "What's the meat-vs-cyber look?", a: "The world renders in two layers, organic 'meat' and digital 'cyber', with a continuous post-process crossfade we call the Seam Director. The balance shifts as you play, so the image is never fully one or the other." },
{ q: "What platforms and when?", a: "Built in Godot for desktop and mobile; the marketing site is live at meatspace.place and the game is in active development. Follow Such Software for release news." },
];
const jsonLd = {
"@context": "https://schema.org",
"@type": "VideoGame",
name: "Meatspace",
url: "https://such.software/products/meatspace",
description:
"A deduction game set in the ruins of a failed mass mind-upload. Reconstruct a lost person from fragments, through a living meat-vs-cyber crossfade, with an optional multiplayer mode.",
applicationCategory: "Game",
genre: ["Adventure", "Mystery", "Narrative"],
gamePlatform: ["PC", "iOS", "Android"],
author: { "@type": "Organization", name: "Such Software" },
publisher: { "@type": "Organization", name: "Such Software" },
};
const features = [
{ title: "Deduction that dawns on you", body: "You reconstruct a lost person from unreliable fragments, and you get it wrong in ways you won't see until the ending makes you feel it." },
{ title: "The Seam Director", body: "A continuous meat-vs-cyber crossfade rendered as a post-process. The world is never fully organic or fully digital; the balance moves as you do." },
{ title: "The Wake", body: "An optional realtime multiplayer mode, on Nakama, for working through the reconstruction alongside others." },
{ title: "A world built to unsettle", body: "The ruins of the Ingestion: half-copied minds, a substrate called the Lattice, rendered with an intentionally uneasy, in-between aesthetic." },
{ title: "AI-assisted, human-directed pipeline", body: "3D (Meshy), audio (ElevenLabs), and imagery (nano-gpt) feed an engine that owns the composition, the same 'AI makes ingredients, the engine makes the content' approach we use across the studio." },
];
export default function MeatspacePage() {
return (
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground">
<JsonLd data={breadcrumbLd([
{ name: "Home", path: "/" },
{ name: "Products", path: "/products" },
{ name: "Meatspace", path: "/products/meatspace" },
])} />
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<JsonLd data={faqLd(faqs)} />
<Header />
<div id="main-content" className="z-10 w-full max-w-4xl mx-auto py-20 px-4 pb-24 md:pb-20">
<Link href="/products" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">
← Back to Products
</Link>
<div className="flex items-center gap-4 mb-2">
<Image src="/images/products/meatspace.svg" alt="Meatspace icon" width={64} height={64} className="rounded-2xl" />
<div>
<span className="inline-flex items-center rounded-full bg-pink-400/15 px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.2em] text-pink-600 dark:text-pink-400 mb-1">
In Development
</span>
<h1 className="text-4xl md:text-5xl font-bold text-pink-600 dark:text-pink-400 leading-none">
Meatspace
</h1>
</div>
</div>
<p className="text-base text-muted-foreground mb-6">Find who you lost in the wreck of the upload.</p>
<div className="prose dark:prose-invert max-w-none mb-12">
<p className="text-xl text-muted-foreground leading-relaxed">
Meatspace is a deduction game set in the ruins of the Ingestion: humanity&rsquo;s failed
mass upload into the Lattice. You sift a lost person&rsquo;s fragments out of the noise of
the dead, reconstructing who they were, and you get it wrong in ways you won&rsquo;t see
until the end. It renders in a continuous meat-vs-cyber crossfade, with an optional
multiplayer mode. Its home is at{" "}
<a href="https://meatspace.place" target="_blank" rel="noopener noreferrer" className="text-pink-600 dark:text-pink-400 hover:underline">meatspace.place</a>.
</p>
</div>
<h2 className="text-2xl font-bold mb-6">What makes it different</h2>
<div className="grid md:grid-cols-2 gap-6 mb-12">
{features.map((f) => (
<Card key={f.title} className="glass-card border-pink-500/20">
<h3 className="text-lg font-bold mb-2">{f.title}</h3>
<p className="text-sm text-muted-foreground">{f.body}</p>
</Card>
))}
</div>
<h2 className="text-2xl font-bold mb-6">Under the Hood</h2>
<div className="section-container border-l-4 border-pink-500 mb-12">
<ul className="space-y-2 text-muted-foreground">
<li><strong>Engine:</strong> Godot with a Rust GDExtension, one codebase for desktop and mobile.</li>
<li><strong>The Seam Director:</strong> a post-process that crossfades an organic and a digital render of the same world.</li>
<li><strong>Multiplayer:</strong> The Wake, a realtime mode on Nakama.</li>
<li><strong>Asset pipeline:</strong> Meshy (3D), ElevenLabs (audio), and nano-gpt (imagery) feed an engine that owns the composition.</li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-6">FAQ</h2>
<div className="space-y-6 mb-12">
{faqs.map((f) => (
<div key={f.q}>
<h3 className="font-bold mb-1 text-pink-600 dark:text-pink-400">{f.q}</h3>
<p className="text-muted-foreground">{f.a}</p>
</div>
))}
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground flex-wrap">
<Link href="/products" className="hover:text-foreground transition-colors">All Products</Link>
<span>•</span>
<a href="https://meatspace.place" target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">meatspace.place</a>
<span>•</span>
<span>Contact: apps@such.software</span>
</div>
</div>
<MobileNav />
</main>
);
}
@@ -0,0 +1,142 @@
import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav";
import { Card } from "@repo/ui/components/card";
import Link from "next/link";
import Image from "next/image";
import { JsonLd, breadcrumbLd, faqLd } from "@/components/seo/json-ld";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Mushroom Capital: Cozy 3D Idle Mushroom Game | Such Software",
description:
"Mushroom Capital is a cozy 3D idle and async-social game where you build and tend a living fungal ecosystem. Grow your colony, bank Capital, and prestige with Heirloom Spores. Coming to mobile, web, and desktop.",
alternates: { canonical: "/products/mushroom-capital" },
keywords: [
"idle game",
"cozy game",
"mushroom game",
"incremental game",
"async social game",
"tilt shift game",
"mushroom capital",
],
openGraph: {
title: "Mushroom Capital: Tend a Living Fungal Ecosystem",
description:
"A cozy 3D idle and async-social game with a tilt-shift toybox aesthetic. Grow a colony, earn Capital, prestige with Heirloom Spores. Coming soon from Such Software.",
type: "website",
url: "/products/mushroom-capital",
},
};
const faqs = [
{ q: "What is Mushroom Capital?", a: "A cozy 3D idle and async-social game. You build and tend a self-contained, living fungal ecosystem: a stylized, tilt-shift toybox that keeps growing while you're away. It's a calm, collect-and-prestige experience, not a hardcore optimizer." },
{ q: "What do you actually do?", a: "Grow and arrange your colony, keep the ecosystem healthy, and bank two currencies: Capital (the soft, moment-to-moment score) and Heirloom Spores (prestige you carry forward when you reset). The loop rewards curation and patience over grinding." },
{ q: "Is it multiplayer?", a: "It's async-social: your world is yours, but you connect with others between sessions. Cross-progression means one save follows you across mobile, web, and desktop." },
{ q: "What platforms?", a: "Mobile-first (iOS and Android), with web and desktop cross-progression. Built in Godot 4." },
{ q: "When is it out?", a: "A soft launch is planned for summer 2026, with a wider launch in September around the Kennett Square Mushroom Festival. Its home is mushroom.capital." },
];
const jsonLd = {
"@context": "https://schema.org",
"@type": "VideoGame",
name: "Mushroom Capital",
url: "https://such.software/products/mushroom-capital",
description:
"A cozy 3D idle and async-social game where you build and tend a living fungal ecosystem, with a tilt-shift toybox aesthetic and cross-progression.",
applicationCategory: "Game",
genre: ["Simulation", "Casual", "Idle"],
gamePlatform: ["iOS", "Android", "PC", "Web"],
author: { "@type": "Organization", name: "Such Software" },
publisher: { "@type": "Organization", name: "Such Software" },
};
const features = [
{ title: "A living fungal ecosystem", body: "Build and tend a self-contained colony that keeps growing while you're away. Curation and care matter more than clicking." },
{ title: "Two currencies, one calm loop", body: "Bank Capital as you grow, then prestige with Heirloom Spores to start richer. A tidy, legible economy with no dark patterns." },
{ title: "Tilt-shift toybox", body: "A stylized miniature aesthetic: everything reads like a warm, hand-built diorama you can turn in your fingers." },
{ title: "Async-social", body: "Your world is yours, but it's better with others between sessions: light, unpressured social hooks, not a leaderboard grind." },
{ title: "Cross-progression", body: "One save follows you across mobile, web, and desktop, so your colony is always where you are." },
];
export default function MushroomCapitalPage() {
return (
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground">
<JsonLd data={breadcrumbLd([
{ name: "Home", path: "/" },
{ name: "Products", path: "/products" },
{ name: "Mushroom Capital", path: "/products/mushroom-capital" },
])} />
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<JsonLd data={faqLd(faqs)} />
<Header />
<div id="main-content" className="z-10 w-full max-w-4xl mx-auto py-20 px-4 pb-24 md:pb-20">
<Link href="/products" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">
← Back to Products
</Link>
<div className="flex items-center gap-4 mb-2">
<Image src="/images/products/mushroom-capital.png" alt="Mushroom Capital icon" width={64} height={64} className="rounded-2xl" />
<div>
<span className="inline-flex items-center rounded-full bg-orange-400/15 px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.2em] text-orange-600 dark:text-orange-400 mb-1">
Coming Soon
</span>
<h1 className="text-4xl md:text-5xl font-bold text-orange-600 dark:text-orange-400 leading-none">
Mushroom Capital
</h1>
</div>
</div>
<p className="text-base text-muted-foreground mb-6">Grow a colony. Bank Capital. Prestige with spores.</p>
<div className="prose dark:prose-invert max-w-none mb-12">
<p className="text-xl text-muted-foreground leading-relaxed">
Mushroom Capital is a cozy 3D idle game where you build and tend a living fungal
ecosystem: a stylized, tilt-shift toybox that keeps growing while you&rsquo;re away. Bank
Capital as you go, prestige with Heirloom Spores, and carry one save across mobile, web, and
desktop. Its home is at{" "}
<a href="https://mushroom.capital" target="_blank" rel="noopener noreferrer" className="text-orange-600 dark:text-orange-400 hover:underline">mushroom.capital</a>.
</p>
</div>
<h2 className="text-2xl font-bold mb-6">What makes it different</h2>
<div className="grid md:grid-cols-2 gap-6 mb-12">
{features.map((f) => (
<Card key={f.title} className="glass-card border-orange-500/20">
<h3 className="text-lg font-bold mb-2">{f.title}</h3>
<p className="text-sm text-muted-foreground">{f.body}</p>
</Card>
))}
</div>
<h2 className="text-2xl font-bold mb-6">Under the Hood</h2>
<div className="section-container border-l-4 border-orange-500 mb-12">
<ul className="space-y-2 text-muted-foreground">
<li><strong>Engine:</strong> Godot 4 (Vulkan) with Rust (gdext) for the perf-critical client.</li>
<li><strong>Server:</strong> self-hosted Nakama with TypeScript RPC modules and PostgreSQL.</li>
<li><strong>Cross-progression:</strong> one account across mobile, web, and desktop.</li>
<li><strong>Economy:</strong> a clean two-currency prestige loop: Capital and Heirloom Spores.</li>
</ul>
</div>
<h2 className="text-2xl font-bold mb-6">FAQ</h2>
<div className="space-y-6 mb-12">
{faqs.map((f) => (
<div key={f.q}>
<h3 className="font-bold mb-1 text-orange-600 dark:text-orange-400">{f.q}</h3>
<p className="text-muted-foreground">{f.a}</p>
</div>
))}
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground flex-wrap">
<Link href="/products" className="hover:text-foreground transition-colors">All Products</Link>
<span>•</span>
<a href="https://mushroom.capital" target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">mushroom.capital</a>
<span>•</span>
<span>Contact: apps@such.software</span>
</div>
</div>
<MobileNav />
</main>
);
}
+28 -2
View File
@@ -90,7 +90,7 @@ const products: Product[] = [
{
slug: "barns-and-neutrons",
title: "Barns & Neutrons",
description: "A cozy exploration-puzzle game across the real Table of Nuclides—the anti-spreadsheet. Capture neutrons, coax decays, and light up the chart while learning honest nuclear physics. Desktop and mobile.",
description: "A cozy exploration-puzzle game across the real Table of Nuclides: the anti-spreadsheet. Capture neutrons, coax decays, and light up the chart while learning honest nuclear physics. Desktop and mobile.",
color: "yellow",
icon: "/images/products/barns-and-neutrons.svg",
releaseDate: null,
@@ -99,7 +99,7 @@ const products: Product[] = [
{
slug: "alpha-doc",
title: "Alpha Doc",
description: "A top-down academic-horror roguelike. You're the irradiated PhD student—master four kinds of radiation and descend through a warped physics facility to your thesis defense. Desktop and mobile.",
description: "A top-down academic-horror roguelike. You're the irradiated PhD student; master four kinds of radiation and descend through a warped physics facility to your thesis defense. Desktop and mobile.",
color: "orange",
icon: "/images/products/alpha-doc.svg",
releaseDate: null,
@@ -114,6 +114,32 @@ const products: Product[] = [
releaseDate: null,
comingSoon: true,
},
{
slug: "bloomword",
title: "Bloomword",
description: "A daily word game where everyone gets the same letters and races the sunset; every word you find grows a biome-specific, living 3D typographic tree. Build streaks, collect biomes, and share your bloom. Play free at bloomword.earth; launching on iOS and Android.",
color: "green",
icon: "/images/products/bloomword.svg",
releaseDate: "2026-07-06",
},
{
slug: "meatspace",
title: "Meatspace",
description: "A deduction game set in the ruins of the Ingestion: humanity's failed mass upload into the Lattice. Sift a lost person's fragments from the noise of the dead, through a living meat-vs-cyber crossfade, with an optional realtime multiplayer mode: The Wake.",
color: "pink",
icon: "/images/products/meatspace.svg",
releaseDate: null,
comingSoon: true,
},
{
slug: "mushroom-capital",
title: "Mushroom Capital",
description: "A cozy 3D idle game where you build and tend a living fungal ecosystem. Grow your colony, bank Capital, and prestige with Heirloom Spores. Tilt-shift toybox worlds, mobile-first with web and desktop cross-progression. Soft launch this summer.",
color: "orange",
icon: "/images/products/mushroom-capital.png",
releaseDate: null,
comingSoon: true,
},
];
export default function ProductsPage() {
+120 -51
View File
@@ -1,5 +1,6 @@
import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav";
import { DemosGallery } from "@/components/products/demos-gallery";
import { Card } from "@repo/ui/components/card";
import { Button } from "@repo/ui/components/button";
import { JsonLd, breadcrumbLd, faqLd } from "@/components/seo/json-ld";
@@ -8,11 +9,11 @@ import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Custom Online Stores & E-Commerce | Such Software",
description: "Custom online stores with no upfront cost and revenue-share pricing. Own your data, escape template limits, engineered for top Core Web Vitals. Built on Medusa.",
description: "A multi-tenant commerce platform: your own fully-themed storefront, print-on-demand, non-custodial crypto checkout, Stripe Connect, and automated tax, no upfront cost, revenue-share pricing, fully managed hosting. Built on Medusa v2.",
alternates: { canonical: "/products/webshops" },
openGraph: {
title: "Custom Online Stores & E-Commerce | Such Software",
description: "Custom online stores with no upfront cost and revenue-share pricing. Own your data and escape template limits. Built on Medusa.",
description: "Your own fully-themed storefront from one engine: print-on-demand, non-custodial crypto checkout, Stripe Connect, automated tax. No upfront cost, revenue-share, fully managed hosting. Built on Medusa v2.",
type: "website",
images: ["/images/og/webshops.png"],
},
@@ -23,11 +24,60 @@ export const metadata: Metadata = {
};
const faqs = [
{ q: "What makes each store unique if they share one platform?", a: "Every tenant gets a fully-themed storefront, its own colors, typography, layout, dark mode, and content, driven from a shared Medusa engine. One backend, many distinct-looking shops. See the five live demos on this page: each is the same platform wearing a completely different skin." },
{ q: "How does revenue-share pricing work?", a: "Instead of a large upfront build fee, we build and host your store at our own cost and take a small percentage of sales once you are live. We only make money when you do." },
{ q: "Who hosts the store?", a: "We do. Hosting is fully managed and included, the platform runs on our own infrastructure (VPS, pm2, Caddy), with deploys, TLS, backups, and monitoring handled by us. You do not need a Vercel or Railway account, and there is nothing for you to operate." },
{ q: "What is headless commerce, and why Medusa?", a: "Headless means the storefront is decoupled from the commerce backend. We use Medusa v2 (open-source, Node.js) for the backend and Next.js for a fast custom storefront, so you are not boxed in by a template or a hosted platform's limits." },
{ q: "Do I own my data?", a: "Yes. Unlike hosted platforms, you own your customer data, product catalog, and order history outright. There is no platform lock-in." },
{ q: "Which payment methods are supported?", a: "Stripe via Stripe Connect (you stay the merchant of record and settle directly) and non-custodial cryptocurrency, Bitcoin, Litecoin, Monero, Wownero, and Grin. Crypto payments land straight in your own wallet; we never take custody of funds." },
{ q: "Can you handle print-on-demand or dropshipping?", a: "Yes. We integrate Printful for print-on-demand and dropship fulfillment, so you can sell apparel and merch without holding inventory, orders route to production and ship automatically." },
{ q: "What about sales tax?", a: "Tax is automated. We support Stripe Tax for full multi-state calculation and filing, or a self-hosted single-state estimator for sub-nexus tenants, routed per tenant so you collect the right amount at checkout." },
{ q: "Can you migrate my existing store?", a: "Yes. We can migrate catalogs, customers, and order history from Shopify, WooCommerce, and other platforms." },
{ q: "Which payment methods are supported?", a: "Stripe and PayPal out of the box, with optional cryptocurrency payments if you want them." },
];
type Feature = { title: string; body: string };
const features: Feature[] = [
{
title: "Zero Upfront Cost",
body: "We invest our development time and infrastructure. You focus on your brand and products. We take a small percentage of sales once you are live.",
},
{
title: "Own Your Data",
body: "Unlike Shopify, you own your customer data, product catalog, and order history. No platform lock-in, ever.",
},
{
title: "Print-on-Demand & Dropship",
body: "Printful integration for apparel and merch: sell without holding inventory. Orders route to production and ship automatically, and the platform reconciles production cost for you.",
},
{
title: "Non-Custodial Crypto Checkout",
body: "Accept Bitcoin, Litecoin, Monero, Wownero, and Grin. Funds settle directly into your own wallet, we never take custody, so you stay in control of your money.",
},
{
title: "Stripe Connect Payments",
body: "Card payments run through Stripe Connect: you stay the merchant of record on the statement, and the platform fee comes out as a transparent revenue-share on item margin only, tax and shipping pass through to you uncut.",
},
{
title: "Wallet & Web3 Login",
body: "Optional Sign-In-With-Ethereum and wallet-based login alongside email, backed by a three-tier auth model, the operator chooses which methods each tenant offers, and each tenant's customers sign in their way.",
},
{
title: "Order Notifications",
body: "Get pinged the moment a sale lands. Telegram and email notifications keep you and your team on top of new orders without babysitting a dashboard.",
},
{
title: "Automated Sales Tax",
body: "Tax is calculated at checkout via Stripe Tax, or a self-hosted single-state estimator for sub-nexus tenants, routed per tenant so you collect the right amount in the right jurisdictions.",
},
{
title: "Flexible Architecture",
body: "Subscriptions, bundles, B2B pricing, multi-warehouse inventory. Model your exact business logic without platform constraints.",
},
{
title: "Performance First",
body: "Next.js storefronts engineered for Core Web Vitals, with fast load times and high conversion in mind.",
},
];
export default function WebshopsPage() {
@@ -40,66 +90,85 @@ export default function WebshopsPage() {
])} />
<JsonLd data={faqLd(faqs)} />
<Header />
<div id="main-content" className="z-10 w-full max-w-4xl mx-auto py-20 px-4 pb-24 md:pb-20">
<div id="main-content" className="z-10 w-full max-w-5xl mx-auto py-20 px-4 pb-24 md:pb-20">
<Link href="/products" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">
← Back to Products
</Link>
<h1 className="text-4xl md:text-5xl font-bold mb-2 text-emerald-600 dark:text-emerald-400">
Custom Online Stores
{/* Hero, lead with the differentiator: per-tenant theming */}
<p className="text-sm font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-3">
Multi-tenant commerce platform
</p>
<h1 className="text-4xl md:text-5xl font-bold mb-4 text-emerald-600 dark:text-emerald-400">
Your own fully-themed storefront, from one engine
</h1>
<p className="text-base text-muted-foreground mb-6">Webshops built and run on revenue share.</p>
<p className="text-xl text-muted-foreground leading-relaxed max-w-3xl mb-12">
We run a single Medusa v2 commerce platform that spins up a completely distinct,
fully-branded storefront for every tenant, your colors, your typography, your layout,
your dark mode. One battle-tested engine underneath; a store that looks and feels like
nobody else's on top. No large setup fee, no monthly retainer: we build, host, and
maintain it on revenue share, so we only win when you make sales.
</p>
<div className="prose dark:prose-invert max-w-none mb-12">
<p className="text-xl text-muted-foreground leading-relaxed">
We build custom online stores with a revenue-share model: no large setup fee, no monthly
retainer. We only win when you make sales. Under the hood it is a headless Medusa v2
backend with a Next.js storefront, so you own your data and your business logic.
{/* Theming spotlight, the headline story */}
<section aria-labelledby="theming-heading" className="section-container border-l-4 border-emerald-500 mb-16">
<h2 id="theming-heading" className="text-2xl md:text-3xl font-bold mb-4">
Per-tenant theming is the whole point
</h2>
<p className="text-muted-foreground leading-relaxed max-w-3xl mb-4">
Most "multi-store" platforms give every shop the same skeleton with a swapped logo.
Ours derives an entire visual system per tenant from your theme configuration:
palette, fonts, spacing, component styling, and a matched dark mode across both the
storefront and checkout. Two shops on the same backend can look nothing alike.
</p>
</div>
<p className="text-muted-foreground leading-relaxed max-w-3xl">
That means you get the economics of a shared platform, managed hosting, security
updates, and new features shipped to everyone, without the cookie-cutter look that
usually comes with it. The five live demos below are all the exact same platform.
</p>
</section>
<div className="grid md:grid-cols-2 gap-6 mb-12">
<Card className="glass-card border-emerald-500/20">
<h3 className="text-xl font-bold mb-3">Zero Upfront Cost</h3>
<p className="text-muted-foreground">
We invest our development time and infrastructure. You focus on your brand and products.
We take a small percentage of sales once you are live.
</p>
</Card>
<Card className="glass-card border-emerald-500/20">
<h3 className="text-xl font-bold mb-3">Own Your Data</h3>
<p className="text-muted-foreground">
Unlike Shopify, you own your customer data, product catalog, and order history.
No platform lock-in, ever.
</p>
</Card>
<Card className="glass-card border-emerald-500/20">
<h3 className="text-xl font-bold mb-3">Flexible Architecture</h3>
<p className="text-muted-foreground">
Subscriptions, bundles, B2B pricing, multi-warehouse inventory.
Model your exact business logic without platform constraints.
</p>
</Card>
<Card className="glass-card border-emerald-500/20">
<h3 className="text-xl font-bold mb-3">Performance First</h3>
<p className="text-muted-foreground">
Next.js storefronts engineered for Core Web Vitals, with fast load
times and high conversion in mind.
</p>
</Card>
</div>
{/* Live demos, proof of the theming story */}
<section aria-labelledby="demos-heading" className="mb-16">
<h2 id="demos-heading" className="text-2xl md:text-3xl font-bold mb-2">
Live demos
</h2>
<p className="text-muted-foreground mb-8 max-w-3xl">
Five differently-themed storefronts, one platform. Open them side by side, same
engine, five completely different looks.
</p>
<DemosGallery />
</section>
<div className="section-container border-l-4 border-emerald-500 mb-12">
<h2 className="text-2xl font-bold mb-4">Tech Stack</h2>
{/* Feature grid, the real platform capabilities */}
<section aria-labelledby="features-heading" className="mb-16">
<h2 id="features-heading" className="text-2xl md:text-3xl font-bold mb-8">
What's built in
</h2>
<div className="grid md:grid-cols-2 gap-6">
{features.map((f) => (
<Card key={f.title} className="glass-card border-emerald-500/20">
<h3 className="text-xl font-bold mb-3">{f.title}</h3>
<p className="text-muted-foreground">{f.body}</p>
</Card>
))}
</div>
</section>
{/* Tech stack, hosting line corrected to fully-managed self-hosting */}
<section aria-labelledby="stack-heading" className="section-container border-l-4 border-emerald-500 mb-16">
<h2 id="stack-heading" className="text-2xl font-bold mb-4">Tech Stack</h2>
<ul className="space-y-2 text-muted-foreground">
<li><strong>Backend:</strong> Medusa v2 (Node.js, PostgreSQL)</li>
<li><strong>Frontend:</strong> Next.js 15, React 18, Tailwind CSS</li>
<li><strong>Payments:</strong> Stripe, PayPal, crypto (optional)</li>
<li><strong>Hosting:</strong> Vercel, Railway, or your infrastructure</li>
<li><strong>Backend:</strong> Medusa v2 (Node.js, PostgreSQL), multi-tenant with per-store sales-channel isolation</li>
<li><strong>Frontend:</strong> Next.js 15, React 18, Tailwind CSS, per-tenant themed storefront</li>
<li><strong>Payments:</strong> Stripe Connect and non-custodial crypto (BTC, LTC, XMR, WOW, Grin)</li>
<li><strong>Fulfillment:</strong> Printful print-on-demand / dropship, plus your own inventory</li>
<li><strong>Tax:</strong> Stripe Tax or self-hosted single-state estimator, routed per tenant</li>
<li><strong>Hosting:</strong> Fully managed and included, we self-host on our own infrastructure (VPS, pm2, Caddy) with TLS, backups, and monitoring handled for you</li>
</ul>
</div>
</section>
<h2 className="text-2xl font-bold mb-6">FAQ</h2>
<h2 className="text-2xl md:text-3xl font-bold mb-6">FAQ</h2>
<div className="space-y-6 mb-12">
{faqs.map((f) => (
<div key={f.q}>
+3
View File
@@ -25,6 +25,9 @@ export default function sitemap(): MetadataRoute.Sitemap {
{ url: `${base}/products/barns-and-neutrons`, lastModified: updated, changeFrequency: "weekly", priority: 0.8 },
{ url: `${base}/products/alpha-doc`, lastModified: updated, changeFrequency: "weekly", priority: 0.7 },
{ url: `${base}/products/decay-theory`, lastModified: updated, changeFrequency: "weekly", priority: 0.7 },
{ url: `${base}/products/bloomword`, lastModified: updated, changeFrequency: "weekly", priority: 0.8 },
{ url: `${base}/products/meatspace`, lastModified: updated, changeFrequency: "weekly", priority: 0.7 },
{ url: `${base}/products/mushroom-capital`, lastModified: updated, changeFrequency: "weekly", priority: 0.7 },
{ url: `${base}/services`, lastModified: updated, changeFrequency: "monthly", priority: 0.9 },
{ url: `${base}/about`, lastModified: updated, changeFrequency: "monthly", priority: 0.7 },
{ url: `${base}/support`, lastModified: updated, changeFrequency: "monthly", priority: 0.5 },
@@ -0,0 +1,106 @@
import Image from "next/image";
import { Card } from "@repo/ui/components/card";
// Literal class strings per color so Tailwind's JIT keeps them (no dynamic interpolation).
const cardBorder: Record<string, string> = {
amber: "border-amber-500/20 hover:border-amber-500/50",
blue: "border-blue-500/20 hover:border-blue-500/50",
emerald: "border-emerald-500/20 hover:border-emerald-500/50",
violet: "border-violet-500/20 hover:border-violet-500/50",
cyan: "border-cyan-500/20 hover:border-cyan-500/50",
};
const titleColor: Record<string, string> = {
amber: "text-amber-700 dark:text-amber-400",
blue: "text-blue-700 dark:text-blue-400",
emerald: "text-emerald-700 dark:text-emerald-400",
violet: "text-violet-700 dark:text-violet-400",
cyan: "text-cyan-700 dark:text-cyan-400",
};
type Demo = {
name: string;
slug: string;
tagline: string;
color: keyof typeof titleColor;
href: string;
};
const demos: Demo[] = [
{
name: "Artisan",
slug: "artisan",
tagline: "Warm, handcrafted-goods storefront with editorial product pages.",
color: "amber",
href: "https://artisan-demo.such.software",
},
{
name: "Merch",
slug: "merch",
tagline: "Bold band / brand merch shop wired to print-on-demand fulfillment.",
color: "blue",
href: "https://merch-demo.such.software",
},
{
name: "Crypto",
slug: "crypto",
tagline: "Non-custodial crypto checkout: Bitcoin, Monero, Wownero, and Grin.",
color: "emerald",
href: "https://crypto-demo.such.software",
},
{
name: "Yoga",
slug: "yoga",
tagline: "Calm studio storefront for classes, passes, and lifestyle goods.",
color: "violet",
href: "https://yoga-demo.such.software",
},
{
name: "Coaching",
slug: "coaching",
tagline: "Service-led site for sessions, packages, and digital products.",
color: "cyan",
href: "https://coaching-demo.such.software",
},
];
function DemoCard({ demo }: { demo: Demo }) {
return (
<a
href={demo.href}
target="_blank"
rel="noopener noreferrer"
className="block group focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background rounded-[2rem]"
aria-label={`Open the ${demo.name} demo storefront in a new tab`}
>
<Card className={`glass-card h-full !p-0 overflow-hidden cursor-pointer transition-colors ${cardBorder[demo.color]}`}>
{/* Real screenshot of the live themed storefront — captured at 1280×800,
cropped to the hero/nav (the most brand-distinctive region). */}
<div className="h-40 w-full overflow-hidden">
<Image
src={`/images/demos/${demo.slug}.png`}
alt={`${demo.name} demo storefront`}
width={1280}
height={800}
className="h-40 w-full object-cover object-top transition-transform duration-700 group-hover:scale-[1.03]"
/>
</div>
<div className="p-6">
<h3 className={`text-xl font-bold mb-2 ${titleColor[demo.color]}`}>{demo.name}</h3>
<p className="text-sm text-muted-foreground leading-relaxed mb-3">{demo.tagline}</p>
<span className="text-sm font-medium text-foreground group-hover:underline">
Visit live demo →
</span>
</div>
</Card>
</a>
);
}
export function DemosGallery() {
return (
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
{demos.map((demo) => (
<DemoCard key={demo.slug} demo={demo} />
))}
</div>
);
}