From 193816f5eccd0d582ab9d79713b08af45673a93a Mon Sep 17 00:00:00 2001 From: Jacob Mathison Date: Sat, 11 Apr 2026 10:49:34 -0500 Subject: [PATCH] feat: SiteLayout template, World nav dropdown, lore/world/characters/music/gallery pages - Fix SiteLayout WORLD_LINKS to correct /world/* paths - Add Spotify frame-src to CSP (next.config.ts) - pages/world/characters.tsx: full character roster page (6 cards, Aaron Pressey credit) - pages/world/gallery.tsx: tabbed Official/Fan Art gallery with coming-soon states - pages/world/music.tsx: soundtrack page with Spotify album embed + YouTube CTA - pages/world/lore.tsx: atmospheric lore with Bone Kingdom, Ledger, Origins/Prophecy placeholders - pages/world/world.tsx: 3 region cards (Bone Kingdom, Bonefield, Pyre Gates) - pages/{lore,world,characters,music,gallery}.tsx: 301 redirects to /world/* routes - All world pages use SiteLayout, dark themed, proper SEO keywords --- next.config.ts | 1 + pages/characters.tsx | 85 ++--------------- pages/components/SiteLayout.tsx | 10 +- pages/gallery.tsx | 144 ++-------------------------- pages/lore.tsx | 71 ++------------ pages/music.tsx | 90 ++---------------- pages/world.tsx | 87 ++--------------- pages/world/characters.tsx | 155 ++++++++++++++++++++++++++++++ pages/world/gallery.tsx | 164 ++++++++++++++++++++++++++++++++ pages/world/music.tsx | 158 ++++++++++++++++++++++++++++++ 10 files changed, 533 insertions(+), 432 deletions(-) create mode 100644 pages/world/characters.tsx create mode 100644 pages/world/gallery.tsx create mode 100644 pages/world/music.tsx diff --git a/next.config.ts b/next.config.ts index 8e515e3..4a000da 100644 --- a/next.config.ts +++ b/next.config.ts @@ -14,6 +14,7 @@ const securityHeaders = [ "style-src 'self' 'unsafe-inline'", "img-src 'self' data: https:", "connect-src 'self' https://www.google-analytics.com https://analytics.google.com", + "frame-src 'self' https://open.spotify.com", "frame-ancestors 'none'", ].join("; "), }, diff --git a/pages/characters.tsx b/pages/characters.tsx index c3117e9..8201648 100644 --- a/pages/characters.tsx +++ b/pages/characters.tsx @@ -1,77 +1,12 @@ -import SiteLayout from "./components/SiteLayout"; -import layoutStyles from "@/styles/Layout.module.css"; -import styles from "@/styles/World.module.css"; +import { useEffect } from "react"; +import { useRouter } from "next/router"; -const CHARACTERS = [ - { - emoji: "ðŸĶī", - name: "Bone Lord Bob", - role: "The Protagonist", - desc: "A man cursed to walk between worlds — carrying the weight of the dead and the rage of the living. He answers to no one. He owes everyone.", - revealed: true, - }, - { - emoji: "ðŸ”Ĩ", - name: "???", - role: "Coming Soon", - desc: "A figure from the oldest part of the war. Their allegiance changes everything.", - revealed: false, - }, - { - emoji: "⚔ïļ", - name: "???", - role: "Coming Soon", - desc: "Every legend needs its foil. Every bone lord needs someone willing to break them.", - revealed: false, - }, - { - emoji: "🌑", - name: "???", - role: "Coming Soon", - desc: "They were there at the beginning. They will be there at the end. What happens in between is the story.", - revealed: false, - }, -]; - -export default function Characters() { - return ( - - {/* Hero */} -
- The Cast -

Characters

-

- Every soul in this world has a price. Meet the ones who pay it. -

-
- -
-
- {CHARACTERS.map((c, i) => ( -
-
- {c.emoji} - {!c.revealed &&
?
} -
-
- {c.role} -

{c.name}

-

{c.desc}

-
-
- ))} -
- -
- ðŸ‘Ī -

Full character profiles reveal with each episode

-
-
-
- ); +export default function Redirect() { + const router = useRouter(); + useEffect(() => { router.replace("/world/characters"); }, [router]); + return null; +} + +export async function getServerSideProps() { + return { redirect: { destination: "/world/characters", permanent: true } }; } diff --git a/pages/components/SiteLayout.tsx b/pages/components/SiteLayout.tsx index 538176a..3a77719 100644 --- a/pages/components/SiteLayout.tsx +++ b/pages/components/SiteLayout.tsx @@ -13,11 +13,11 @@ export interface SiteLayoutProps { } const WORLD_LINKS = [ - { href: "/lore", label: "Lore" }, - { href: "/world", label: "World" }, - { href: "/characters", label: "Characters" }, - { href: "/music", label: "Music" }, - { href: "/gallery", label: "Gallery" }, + { href: "/world/lore", label: "Lore" }, + { href: "/world/world", label: "World" }, + { href: "/world/characters", label: "Characters" }, + { href: "/world/music", label: "Music" }, + { href: "/world/gallery", label: "Gallery" }, ]; export default function SiteLayout({ diff --git a/pages/gallery.tsx b/pages/gallery.tsx index 92419fc..6af7c5e 100644 --- a/pages/gallery.tsx +++ b/pages/gallery.tsx @@ -1,136 +1,12 @@ -import { useState } from "react"; -import SiteLayout from "./components/SiteLayout"; -import layoutStyles from "@/styles/Layout.module.css"; -import styles from "@/styles/Gallery.module.css"; +import { useEffect } from "react"; +import { useRouter } from "next/router"; -type GalleryTab = "official" | "fan"; - -// Placeholder art items — swap src for real image paths when assets are ready -const OFFICIAL_ART: { src: string; alt: string; caption?: string }[] = [ - // { src: "/gallery/official/blb-key-art.jpg", alt: "Bone Lord Bob key art", caption: "Key Art — Season 1" }, -]; - -const FAN_ART: { src: string; alt: string; artist?: string; artistUrl?: string }[] = [ - // { src: "/gallery/fan/fan-art-1.jpg", alt: "Fan art by ...", artist: "Artist Name", artistUrl: "https://..." }, -]; - -export default function Gallery() { - const [tab, setTab] = useState("official"); - - return ( - - {/* Hero */} -
- Art of the Bone Kingdom -

Gallery

-

- Official art from the series — and the community that's building around it. -

-
- -
- {/* Tab switcher */} -
- - -
- - {/* Official art */} - {tab === "official" && ( -
- {OFFICIAL_ART.length > 0 ? ( -
- {OFFICIAL_ART.map((item, i) => ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {item.alt} - {item.caption &&

{item.caption}

} -
- ))} -
- ) : ( -
- ðŸŽĻ -

Official art gallery coming soon

-

- Character sheets, key art, and episode stills will live here. -

-
- )} -
- )} - - {/* Fan art */} - {tab === "fan" && ( -
- {FAN_ART.length > 0 ? ( -
- {FAN_ART.map((item, i) => ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {item.alt} - {item.artist && ( -

- Art by{" "} - {item.artistUrl ? ( - - {item.artist} - - ) : ( - item.artist - )} -

- )} -
- ))} -
- ) : ( -
- 💀 -

Fan art gallery — coming soon

-

- Create something. Tag us on social with{" "} - #BoneLordBob and we'll feature - it here. -

- -
- )} -
- )} -
-
- ); +export default function Redirect() { + const router = useRouter(); + useEffect(() => { router.replace("/world/gallery"); }, [router]); + return null; +} + +export async function getServerSideProps() { + return { redirect: { destination: "/world/gallery", permanent: true } }; } diff --git a/pages/lore.tsx b/pages/lore.tsx index dba85a0..c0de156 100644 --- a/pages/lore.tsx +++ b/pages/lore.tsx @@ -1,63 +1,12 @@ -import SiteLayout from "./components/SiteLayout"; -import layoutStyles from "@/styles/Layout.module.css"; -import styles from "@/styles/World.module.css"; +import { useEffect } from "react"; +import { useRouter } from "next/router"; -export default function Lore() { - return ( - - {/* Hero */} -
- The Deep Canon -

Lore

-

- Every world has rules. Every myth has teeth. Here begins the canon of Bone Lord Bob. -

-
- -
- {/* Placeholder lore entries */} -
-
- 💀 -

The Bone Covenant

-

- In the beginning there was only the great silence — and then the crack. From that first - fracture, the Bone Kingdom was born. Those who heard it gained dominion over death itself. -

- More coming soon -
- -
- ðŸ”Ĩ -

The Cursed Tongue

-

- There is a language older than memory. Those who speak it fluently enough can unmake a - living thing — or bind the dead to their will. Very few have lived long enough to master it. -

- More coming soon -
- -
- ⚔ïļ -

The Three Kingdoms

-

- The world is divided not by geography, but by allegiance to one of three ancient powers. - The war between them has lasted longer than any living creature can remember. -

- More coming soon -
-
- -
- 📜 -

Full lore archive unlocking as the series releases

-
-
-
- ); +export default function Redirect() { + const router = useRouter(); + useEffect(() => { router.replace("/world/lore"); }, [router]); + return null; +} + +export async function getServerSideProps() { + return { redirect: { destination: "/world/lore", permanent: true } }; } diff --git a/pages/music.tsx b/pages/music.tsx index ee5190a..fb39e60 100644 --- a/pages/music.tsx +++ b/pages/music.tsx @@ -1,82 +1,12 @@ -import SiteLayout from "./components/SiteLayout"; -import layoutStyles from "@/styles/Layout.module.css"; -import styles from "@/styles/World.module.css"; +import { useEffect } from "react"; +import { useRouter } from "next/router"; -// Placeholder — tracks will be added when official BLB soundtrack assets are available -const TRACKS: { title: string; vibe: string; file?: string }[] = [ - // { title: "Bone Kingdom Theme", vibe: "Dark orchestral", file: "/music/bone-kingdom-theme.mp3" }, -]; - -export default function Music() { - return ( - - {/* Hero */} -
- The Soundtrack -

Music

-

- The Bone Kingdom has a sound. Dark. Relentless. Yours. -

-
- -
- {TRACKS.length > 0 ? ( -
- {TRACKS.map((t, i) => ( -
- {String(i + 1).padStart(2, "0")} -
- {t.title} - {t.vibe} -
- {t.file && ( - - â–ķ - - )} -
- ))} -
- ) : null} - - {/* YouTube playlist embed placeholder */} -
-
- ðŸŽĩ -

Official soundtrack coming with Episode 1

- - Subscribe on YouTube for updates - -
-
- - {/* Patreon CTA */} -
-

Support the Soundtrack

-

- Patrons get early access to music, behind-the-scenes production content, and - the full score as it's composed. -

- - Support on Patreon - -
-
-
- ); +export default function Redirect() { + const router = useRouter(); + useEffect(() => { router.replace("/world/music"); }, [router]); + return null; +} + +export async function getServerSideProps() { + return { redirect: { destination: "/world/music", permanent: true } }; } diff --git a/pages/world.tsx b/pages/world.tsx index 5cb5b6e..e437dc2 100644 --- a/pages/world.tsx +++ b/pages/world.tsx @@ -1,79 +1,12 @@ -import SiteLayout from "./components/SiteLayout"; -import layoutStyles from "@/styles/Layout.module.css"; -import styles from "@/styles/World.module.css"; +import { useEffect } from "react"; +import { useRouter } from "next/router"; -export default function WorldPage() { - return ( - - {/* Hero */} -
- The Setting -

- The World -

-

- Not a land you'd want to be born into. But one you won't be able to look away from. -

-
- -
-
-

- The world of Bone Lord Bob exists somewhere between myth and memory — a place where - the rules of death are more like suggestions, and the landscape itself remembers every - war ever fought on it. -

-

- There are no safe cities. Only cities that haven't fallen yet. -

-
- - {/* Location cards */} -
-
-
- ðŸĶī -

The Bonefield

-
-

- A vast plain where the remains of an ancient battle stretch beyond the horizon. - Travelers say you can still hear the war if you press your ear to the ground at midnight. -

-
- -
-
- 🏰 -

The Throne of Ash

-
-

- No one sits there now. No one has for a hundred years. But something still answers - when the right name is spoken in its hall. -

-
- -
-
- 🌑 -

The Hollow Dark

-
-

- Below every kingdom is the Hollow Dark — the underworld that isn't quite the afterlife. - Bob knows this place better than anyone alive. -

-
-
- -
- 🗚ïļ -

World map and full region guide — coming with Episode 1

-
-
-
- ); +export default function Redirect() { + const router = useRouter(); + useEffect(() => { router.replace("/world/world"); }, [router]); + return null; +} + +export async function getServerSideProps() { + return { redirect: { destination: "/world/world", permanent: true } }; } diff --git a/pages/world/characters.tsx b/pages/world/characters.tsx new file mode 100644 index 0000000..387c955 --- /dev/null +++ b/pages/world/characters.tsx @@ -0,0 +1,155 @@ +import SiteLayout from "@/pages/components/SiteLayout"; + +const s = { + page: { + maxWidth: "1100px", + margin: "0 auto", + padding: "64px 32px 96px", + } as React.CSSProperties, + hero: { + marginBottom: "56px", + textAlign: "center" as const, + } as React.CSSProperties, + eyebrow: { + display: "block", + fontSize: "0.75rem", + fontWeight: 700, + letterSpacing: "0.18em", + textTransform: "uppercase" as const, + color: "#c0392b", + marginBottom: "16px", + } as React.CSSProperties, + h1: { + fontFamily: "'Georgia', 'Times New Roman', serif", + fontSize: "clamp(2.4rem, 6vw, 4rem)", + fontWeight: 700, + color: "#e8dcc8", + lineHeight: 1.1, + marginBottom: "20px", + } as React.CSSProperties, + heroSub: { + fontSize: "1.05rem", + color: "#9e9080", + maxWidth: "520px", + margin: "0 auto", + lineHeight: 1.7, + } as React.CSSProperties, + grid: { + display: "grid", + gridTemplateColumns: "repeat(3, 1fr)", + gap: "20px", + marginBottom: "40px", + } as React.CSSProperties, + card: { + background: "#1a1510", + border: "1px solid rgba(232,220,200,0.12)", + borderRadius: "8px", + padding: "36px 28px", + } as React.CSSProperties, + cardEmoji: { + fontSize: "2.4rem", + display: "block", + marginBottom: "20px", + } as React.CSSProperties, + cardName: { + fontFamily: "'Georgia', 'Times New Roman', serif", + fontSize: "1.3rem", + fontWeight: 700, + color: "#e8dcc8", + marginBottom: "6px", + } as React.CSSProperties, + cardRole: { + fontSize: "0.75rem", + fontWeight: 700, + letterSpacing: "0.12em", + textTransform: "uppercase" as const, + color: "#c0392b", + marginBottom: "16px", + } as React.CSSProperties, + cardDesc: { + fontSize: "0.92rem", + color: "#9e9080", + lineHeight: 1.65, + } as React.CSSProperties, + credit: { + textAlign: "center" as const, + fontSize: "0.82rem", + color: "#9e9080", + marginTop: "8px", + letterSpacing: "0.04em", + } as React.CSSProperties, +}; + +const CHARACTERS = [ + { + emoji: "ðŸĶī", + name: "Bone Lord Bob", + role: "The Protagonist", + desc: "A man cursed to walk between worlds — carrying the weight of the dead and the rage of the living. He answers to no one.", + }, + { + emoji: "ðŸ”Ĩ", + name: "???", + role: "Coming Soon", + desc: "More characters will be revealed as the story unfolds.", + }, + { + emoji: "⚔ïļ", + name: "???", + role: "Coming Soon", + desc: "Every legend needs its foil. Every bone lord needs someone willing to break them.", + }, + { + emoji: "💀", + name: "???", + role: "Coming Soon", + desc: "The dead keep their secrets. This one keeps more than most.", + }, + { + emoji: "🌑", + name: "???", + role: "Coming Soon", + desc: "Some figures are felt before they are seen. This one you feel first.", + }, + { + emoji: "ðŸ”Ū", + name: "???", + role: "Coming Soon", + desc: "Knowledge is power in the Bone Kingdom. This character has both — and charges accordingly.", + }, +]; + +export default function Characters() { + return ( + +
+ {/* Hero */} +
+ Characters +

The Cast

+

+ Every soul in this world has a price. Meet the ones who pay it. +

+
+ + {/* Grid */} +
+ {CHARACTERS.map((c, i) => ( +
+ {c.emoji} +
{c.name}
+
{c.role}
+

{c.desc}

+
+ ))} +
+ +

Character art by Aaron Pressey

+
+
+ ); +} diff --git a/pages/world/gallery.tsx b/pages/world/gallery.tsx new file mode 100644 index 0000000..c02308b --- /dev/null +++ b/pages/world/gallery.tsx @@ -0,0 +1,164 @@ +import { useState } from "react"; +import SiteLayout from "@/pages/components/SiteLayout"; + +type Tab = "official" | "fan"; + +const s = { + page: { + maxWidth: "960px", + margin: "0 auto", + padding: "64px 32px 96px", + } as React.CSSProperties, + hero: { + marginBottom: "48px", + textAlign: "center" as const, + } as React.CSSProperties, + eyebrow: { + display: "block", + fontSize: "0.75rem", + fontWeight: 700, + letterSpacing: "0.18em", + textTransform: "uppercase" as const, + color: "#c0392b", + marginBottom: "16px", + } as React.CSSProperties, + h1: { + fontFamily: "'Georgia', 'Times New Roman', serif", + fontSize: "clamp(2.4rem, 6vw, 4rem)", + fontWeight: 700, + color: "#e8dcc8", + lineHeight: 1.1, + marginBottom: "20px", + } as React.CSSProperties, + heroSub: { + fontSize: "1.05rem", + color: "#9e9080", + maxWidth: "480px", + margin: "0 auto", + lineHeight: 1.7, + } as React.CSSProperties, + tabs: { + display: "flex", + gap: "0", + borderBottom: "1px solid rgba(232,220,200,0.12)", + marginBottom: "40px", + } as React.CSSProperties, + tab: { + padding: "12px 28px", + fontSize: "0.9rem", + fontWeight: 600, + letterSpacing: "0.06em", + textTransform: "uppercase" as const, + background: "none", + border: "none", + cursor: "pointer", + color: "#9e9080", + borderBottom: "2px solid transparent", + marginBottom: "-1px", + transition: "color 0.2s, border-color 0.2s", + fontFamily: "inherit", + } as React.CSSProperties, + tabActive: { + color: "#e8dcc8", + borderBottom: "2px solid #c0392b", + } as React.CSSProperties, + card: { + background: "#1a1510", + border: "1px solid rgba(232,220,200,0.1)", + borderRadius: "8px", + padding: "60px 40px", + textAlign: "center" as const, + } as React.CSSProperties, + cardIcon: { + fontSize: "3rem", + display: "block", + marginBottom: "20px", + opacity: 0.5, + } as React.CSSProperties, + cardTitle: { + fontFamily: "'Georgia', 'Times New Roman', serif", + fontSize: "1.4rem", + fontWeight: 700, + color: "#e8dcc8", + marginBottom: "12px", + } as React.CSSProperties, + cardDesc: { + fontSize: "0.92rem", + color: "#9e9080", + lineHeight: 1.65, + maxWidth: "400px", + margin: "0 auto", + } as React.CSSProperties, +}; + +export default function Gallery() { + const [activeTab, setActiveTab] = useState("official"); + + return ( + +
+ {/* Hero */} +
+ Art & Gallery +

Gallery

+

+ The world made visible. Official art and fan submissions. +

+
+ + {/* Tabs */} +
+ + +
+ + {/* Tab content */} + {activeTab === "official" && ( +
+ ⚔ïļ +
Official Art
+

+ Official artwork coming soon. +
+ Art by Aaron Pressey. +

+
+ )} + + {activeTab === "fan" && ( +
+ ðŸŽĻ +
Fan Art
+

+ Fan art gallery coming soon. +
+ Tag us @BoneLordBob to be featured. +

+
+ )} +
+
+ ); +} diff --git a/pages/world/music.tsx b/pages/world/music.tsx new file mode 100644 index 0000000..40d3bd0 --- /dev/null +++ b/pages/world/music.tsx @@ -0,0 +1,158 @@ +import SiteLayout from "@/pages/components/SiteLayout"; + +const s = { + page: { + maxWidth: "860px", + margin: "0 auto", + padding: "64px 32px 96px", + } as React.CSSProperties, + hero: { + marginBottom: "56px", + textAlign: "center" as const, + } as React.CSSProperties, + eyebrow: { + display: "block", + fontSize: "0.75rem", + fontWeight: 700, + letterSpacing: "0.18em", + textTransform: "uppercase" as const, + color: "#c0392b", + marginBottom: "16px", + } as React.CSSProperties, + h1: { + fontFamily: "'Georgia', 'Times New Roman', serif", + fontSize: "clamp(2.4rem, 6vw, 4rem)", + fontWeight: 700, + color: "#e8dcc8", + lineHeight: 1.1, + marginBottom: "20px", + } as React.CSSProperties, + heroSub: { + fontSize: "1.05rem", + color: "#9e9080", + maxWidth: "540px", + margin: "0 auto", + lineHeight: 1.7, + } as React.CSSProperties, + introPara: { + fontSize: "1rem", + color: "#c8bfae", + lineHeight: 1.8, + marginBottom: "40px", + textAlign: "center" as const, + } as React.CSSProperties, + embedWrapper: { + marginBottom: "48px", + borderRadius: "12px", + overflow: "hidden", + border: "1px solid rgba(232,220,200,0.12)", + } as React.CSSProperties, + sectionLabel: { + display: "block", + fontSize: "0.72rem", + fontWeight: 700, + letterSpacing: "0.14em", + textTransform: "uppercase" as const, + color: "#c0392b", + marginBottom: "16px", + } as React.CSSProperties, + comingSoon: { + background: "#1a1510", + border: "1px solid rgba(232,220,200,0.1)", + borderRadius: "8px", + padding: "40px 32px", + textAlign: "center" as const, + marginBottom: "48px", + } as React.CSSProperties, + comingSoonIcon: { + fontSize: "2rem", + marginBottom: "12px", + display: "block", + opacity: 0.4, + } as React.CSSProperties, + comingSoonText: { + fontSize: "0.88rem", + color: "#9e9080", + letterSpacing: "0.08em", + textTransform: "uppercase" as const, + fontWeight: 600, + } as React.CSSProperties, + ytLink: { + display: "inline-flex", + alignItems: "center", + gap: "8px", + padding: "12px 24px", + background: "rgba(255, 0, 0, 0.08)", + border: "1px solid rgba(255,0,0,0.25)", + borderRadius: "6px", + color: "#e8dcc8", + textDecoration: "none", + fontSize: "0.9rem", + fontWeight: 600, + transition: "background 0.2s, border-color 0.2s", + } as React.CSSProperties, + ytSection: { + textAlign: "center" as const, + marginTop: "24px", + } as React.CSSProperties, +}; + +export default function Music() { + return ( + +
+ {/* Hero */} +
+ Official Soundtrack +

The Music

+

+ The world of Bone Lord Bob has a sound. Dark, brutal, and alive. +

+
+ +

+ These are the tracks that define it. From the Bone Kingdom to the + Pyre Gates — every territory has its own score. This is what it sounds + like when death has a melody. +

+ + {/* Spotify embed */} +
+