From dc38d2bc287ca917a6babb060aab5d79336bc3b3 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Fri, 25 Apr 2025 21:09:42 +0100 Subject: [PATCH] feat: page metadata --- .env.example | 3 +- src/app/create-username/page.tsx | 6 +++ src/app/edit/[slug]/page.tsx | 16 ++++++++ src/app/layout.tsx | 5 ++- src/app/login/page.tsx | 6 +++ src/app/mii/[slug]/page.tsx | 61 ++++++++++++++++++++++++++++++- src/app/not-found.tsx | 6 +++ src/app/privacy/page.tsx | 7 ++++ src/app/profile/[slug]/page.tsx | 57 +++++++++++++++++++++++++++++ src/app/profile/settings/page.tsx | 6 +++ src/app/submit/page.tsx | 6 +++ src/app/terms-of-service/page.tsx | 7 ++++ 12 files changed, 182 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index a58ffb5..b0d4ac0 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ DATABASE_URL="postgresql://postgres:frieren@localhost:5432/tomodachi-share?schema=public" +BASE_URL=https://tomodachi-share.trafficlunar.net -NEXTAUTH_URL=https://tomodachi-share.trafficlunar.net +NEXTAUTH_URL=https://tomodachi-share.trafficlunar.net # This should be the same as BASE_URL AUTH_SECRET=XXXXXXXXXXXXXXXX AUTH_DISCORD_ID=XXXXXXXXXXXXXXXX diff --git a/src/app/create-username/page.tsx b/src/app/create-username/page.tsx index 114f5bd..b7eab72 100644 --- a/src/app/create-username/page.tsx +++ b/src/app/create-username/page.tsx @@ -1,7 +1,13 @@ +import { Metadata } from "next"; import { redirect } from "next/navigation"; import { auth } from "@/lib/auth"; import UsernameForm from "@/components/username-form"; +export const metadata: Metadata = { + title: "Create your Username - TomodachiShare", + description: "Pick a unique username to start using TomodachiShare", +}; + export default async function CreateUsernamePage() { const session = await auth(); diff --git a/src/app/edit/[slug]/page.tsx b/src/app/edit/[slug]/page.tsx index f7c0f78..4167a29 100644 --- a/src/app/edit/[slug]/page.tsx +++ b/src/app/edit/[slug]/page.tsx @@ -1,3 +1,4 @@ +import { Metadata, ResolvingMetadata } from "next"; import { redirect } from "next/navigation"; import { auth } from "@/lib/auth"; @@ -8,6 +9,21 @@ interface Props { params: Promise<{ slug: string }>; } +export async function generateMetadata({ params }: Props, parent: ResolvingMetadata): Promise { + const { slug } = await params; + + const mii = await prisma.mii.findUnique({ + where: { + id: Number(slug), + }, + }); + + return { + title: `${mii?.name} - TomodachiShare`, + description: `Edit the name, tags, and images of '${mii?.name}'`, + }; +} + export default async function MiiPage({ params }: Props) { const { slug } = await params; const session = await auth(); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b3bb28f..693e333 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,9 +3,10 @@ import Script from "next/script"; import { Lexend } from "next/font/google"; import "./globals.css"; + +import Providers from "./provider"; import Header from "@/components/header"; import Footer from "@/components/footer"; -import Providers from "./provider"; const lexend = Lexend({ subsets: ["latin"], @@ -13,7 +14,7 @@ const lexend = Lexend({ export const metadata: Metadata = { title: "TomodachiShare", - description: "Share your Tomodachi Life Miis", + description: "Discover and share Mii residents for your Tomodachi Life island!", }; export default function RootLayout({ diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index f569cf2..0f26efd 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,7 +1,13 @@ +import { Metadata } from "next"; import { redirect } from "next/navigation"; import { auth } from "@/lib/auth"; import LoginButtons from "@/components/login-buttons"; +export const metadata: Metadata = { + title: "Login - TomodachiShare", + description: "Sign in with Discord or GitHub to upload Miis, and like others' creations", +}; + export default async function LoginPage() { const session = await auth(); diff --git a/src/app/mii/[slug]/page.tsx b/src/app/mii/[slug]/page.tsx index 4c94a17..997ef98 100644 --- a/src/app/mii/[slug]/page.tsx +++ b/src/app/mii/[slug]/page.tsx @@ -1,3 +1,4 @@ +import { Metadata, ResolvingMetadata } from "next"; import Link from "next/link"; import { redirect } from "next/navigation"; @@ -15,6 +16,65 @@ interface Props { params: Promise<{ slug: string }>; } +export async function generateMetadata({ params }: Props, parent: ResolvingMetadata): Promise { + const { slug } = await params; + + const mii = await prisma.mii.findUnique({ + where: { + id: Number(slug), + }, + include: { + user: { + select: { + username: true, + }, + }, + _count: { + select: { likedBy: true }, // Get total like count + }, + }, + }); + + // Bots get redirected anyways + if (!mii) return {}; + + const miiImageUrl = `/mii/${mii.id}/mii.webp`; + const qrCodeUrl = `/mii/${mii.id}/qrcode.webp`; + + const username = `@${mii.user.username}`; + + return { + metadataBase: new URL(process.env.BASE_URL!), + title: `${mii.name} - TomodachiShare`, + description: `Check out '${mii.name}', a Tomodachi Life Mii created by ${username} on TomodachiShare. From ${mii.islandName} Island with ${mii._count.likedBy} likes.`, + keywords: [`mii`, `tomodachi life`, `nintendo`, ...mii.tags], + creator: username, + category: "Gaming", + openGraph: { + locale: "en_US", + type: "article", + images: [miiImageUrl, qrCodeUrl], + siteName: "TomodachiShare", + publishedTime: mii.createdAt.toISOString(), + authors: username, + }, + twitter: { + card: "summary_large_image", + title: `${mii.name} - TomodachiShare`, + description: `Check out '${mii.name}', a Tomodachi Life Mii created by ${username} on TomodachiShare. From ${mii.islandName} Island with ${mii._count.likedBy} likes.`, + images: [miiImageUrl, qrCodeUrl], + creator: username, + }, + alternates: { + canonical: `/mii/${mii.id}`, + }, + robots: { + index: true, + follow: true, + }, + }; +} + export default async function MiiPage({ params }: Props) { const { slug } = await params; const session = await auth(); @@ -26,7 +86,6 @@ export default async function MiiPage({ params }: Props) { include: { user: { select: { - id: true, username: true, }, }, diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index e078649..4c34d69 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,5 +1,11 @@ import Link from "next/link"; import { Icon } from "@iconify/react"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Not Found - TomodachiShare", + description: "The requested page could not be found on TomodachiShare", +}; export default function NotFound() { return ( diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx index 9e73528..247544b 100644 --- a/src/app/privacy/page.tsx +++ b/src/app/privacy/page.tsx @@ -1,3 +1,10 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Privacy Policy - TomodachiShare", + description: "Learn how TomodachiShare collects, uses, and protects your data", +}; + export default function PrivacyPage() { return (
diff --git a/src/app/profile/[slug]/page.tsx b/src/app/profile/[slug]/page.tsx index 5614cab..d88b36b 100644 --- a/src/app/profile/[slug]/page.tsx +++ b/src/app/profile/[slug]/page.tsx @@ -1,3 +1,4 @@ +import { Metadata, ResolvingMetadata } from "next"; import { redirect } from "next/navigation"; import Image from "next/image"; import Link from "next/link"; @@ -13,6 +14,62 @@ interface Props { params: Promise<{ slug: string }>; } +export async function generateMetadata({ params }: Props, parent: ResolvingMetadata): Promise { + const { slug } = await params; + + const user = await prisma.user.findUnique({ + where: { + id: Number(slug), + }, + include: { + _count: { + select: { + miis: true, + }, + }, + }, + }); + + // Bots get redirected anyways + if (!user) return {}; + + const joinDate = user.createdAt.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + }); + + return { + metadataBase: new URL(process.env.BASE_URL!), + title: `${user.name} (@${user.username}) - TomodachiShare`, + description: `View ${user.name}'s profile on TomodachiShare. Creator of ${user._count.miis} Miis. Member since ${joinDate}.`, + keywords: [`tomodachi life`, `mii creator`, `nintendo`, `mii collection`, `profile`], + creator: user.username, + category: "Gaming", + openGraph: { + locale: "en_US", + type: "profile", + images: [user.image ?? "/missing.webp"], + siteName: "TomodachiShare", + username: user.username, + firstName: user.name, + }, + twitter: { + card: "summary", + title: `${user.name} (@${user.username}) - TomodachiShare`, + description: `View ${user.name}'s profile on TomodachiShare. Creator of ${user._count.miis} Miis. Member since ${joinDate}.`, + images: [user.image ?? "/missing.webp"], + creator: user.username!, + }, + alternates: { + canonical: `/profile/${user.id}`, + }, + robots: { + index: true, + follow: true, + }, + }; +} + export default async function ProfilePage({ params }: Props) { const session = await auth(); const { slug } = await params; diff --git a/src/app/profile/settings/page.tsx b/src/app/profile/settings/page.tsx index 3e75379..a0fb023 100644 --- a/src/app/profile/settings/page.tsx +++ b/src/app/profile/settings/page.tsx @@ -1,3 +1,4 @@ +import { Metadata } from "next"; import { redirect } from "next/navigation"; import Image from "next/image"; import Link from "next/link"; @@ -9,6 +10,11 @@ import { prisma } from "@/lib/prisma"; import ProfileSettings from "@/components/profile-settings"; +export const metadata: Metadata = { + title: "Profile Settings - TomodachiShare", + description: "Change your account info or delete it", +}; + export default async function ProfileSettingsPage() { const session = await auth(); diff --git a/src/app/submit/page.tsx b/src/app/submit/page.tsx index 701fb94..2b3bee2 100644 --- a/src/app/submit/page.tsx +++ b/src/app/submit/page.tsx @@ -1,7 +1,13 @@ +import { Metadata } from "next"; import { redirect } from "next/navigation"; import { auth } from "@/lib/auth"; import SubmitForm from "@/components/submit-form"; +export const metadata: Metadata = { + title: "Submit a Mii - TomodachiShare", + description: "Upload your Tomodachi Life Mii through its QR code and share it with others", +}; + export default async function SubmitPage() { const session = await auth(); diff --git a/src/app/terms-of-service/page.tsx b/src/app/terms-of-service/page.tsx index f21dc43..79c691c 100644 --- a/src/app/terms-of-service/page.tsx +++ b/src/app/terms-of-service/page.tsx @@ -1,3 +1,10 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Terms of Service - TomodachiShare", + description: "Review the rules and guidelines for using TomodachiShare", +}; + export default function PrivacyPage() { return (