feat(frontend): rich OG metadata + dynamic page titles across all routes
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Has been cancelled
test / unit (pull_request) Has been cancelled
test / integration (pull_request) Has been cancelled
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Has been cancelled
test / unit (pull_request) Has been cancelled
test / integration (pull_request) Has been cancelled
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
// app/(auth)/layout.tsx
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
openGraph: { type: "website" },
|
||||
};
|
||||
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
|
||||
10
thoughts-frontend/app/(auth)/login/layout.tsx
Normal file
10
thoughts-frontend/app/(auth)/login/layout.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Sign in",
|
||||
description: "Sign in to your Thoughts account",
|
||||
};
|
||||
|
||||
export default function LoginLayout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
10
thoughts-frontend/app/(auth)/register/layout.tsx
Normal file
10
thoughts-frontend/app/(auth)/register/layout.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Join Thoughts",
|
||||
description: "Create an account on Thoughts and connect across the Fediverse",
|
||||
};
|
||||
|
||||
export default function RegisterLayout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -7,8 +7,25 @@ import localFont from "next/font/local";
|
||||
import InstallPrompt from "@/components/install-prompt";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Thoughts",
|
||||
description: "A social network for sharing thoughts",
|
||||
title: {
|
||||
default: "Thoughts",
|
||||
template: "%s · Thoughts",
|
||||
},
|
||||
description:
|
||||
"A federated social network for short-form thoughts. Follow people across Mastodon, Pixelfed, and the wider Fediverse.",
|
||||
openGraph: {
|
||||
type: "website",
|
||||
siteName: "Thoughts",
|
||||
title: "Thoughts",
|
||||
description:
|
||||
"A federated social network for short-form thoughts. Follow people across the Fediverse.",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
title: "Thoughts",
|
||||
description:
|
||||
"A federated social network for short-form thoughts. Follow people across the Fediverse.",
|
||||
},
|
||||
};
|
||||
|
||||
const frutiger = localFont({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import {
|
||||
getFeed,
|
||||
@@ -26,6 +27,11 @@ import {
|
||||
} from "@/components/ui/pagination";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Home",
|
||||
description: "Your home timeline — thoughts from people you follow",
|
||||
};
|
||||
|
||||
export default async function Home({
|
||||
searchParams,
|
||||
}: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { cookies } from "next/headers";
|
||||
import { getMe, lookupRemoteActor, getRemoteActorPosts, Me } from "@/lib/api";
|
||||
@@ -7,6 +8,43 @@ interface RemoteActorPageProps {
|
||||
searchParams: Promise<{ handle?: string }>;
|
||||
}
|
||||
|
||||
function stripHtml(html: string) {
|
||||
return html.replace(/<[^>]*>/g, "").trim();
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
searchParams,
|
||||
}: RemoteActorPageProps): Promise<Metadata> {
|
||||
const { handle } = await searchParams;
|
||||
if (!handle) return { title: "Profile" };
|
||||
|
||||
const token = (await cookies()).get("auth_token")?.value ?? null;
|
||||
const actor = await lookupRemoteActor(handle, token).catch(() => null);
|
||||
if (!actor) return { title: handle };
|
||||
|
||||
const name = actor.displayName || actor.handle;
|
||||
const description = actor.bio
|
||||
? stripHtml(actor.bio).slice(0, 160)
|
||||
: `${name} on the Fediverse. Follow from Thoughts.`;
|
||||
|
||||
return {
|
||||
title: `${name} (${actor.handle})`,
|
||||
description,
|
||||
openGraph: {
|
||||
type: "profile",
|
||||
title: `${name} (${actor.handle})`,
|
||||
description,
|
||||
images: actor.avatarUrl ? [{ url: actor.avatarUrl }] : [],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
title: `${name} · Thoughts`,
|
||||
description,
|
||||
images: actor.avatarUrl ? [actor.avatarUrl] : [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RemoteActorPage({
|
||||
searchParams,
|
||||
}: RemoteActorPageProps) {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import { getMe, search, lookupRemoteActor, User } from "@/lib/api";
|
||||
|
||||
export async function generateMetadata({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ q?: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { q } = await searchParams;
|
||||
const title = q ? `Search: "${q}"` : "Search";
|
||||
return {
|
||||
title,
|
||||
description: q
|
||||
? `Search results for "${q}" on Thoughts`
|
||||
: "Search for people and thoughts on Thoughts",
|
||||
};
|
||||
}
|
||||
import { UserListCard } from "@/components/user-list-card";
|
||||
import { RemoteUserCard } from "@/components/remote-user-card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
// app/settings/profile/page.tsx
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Edit profile",
|
||||
description: "Update your Thoughts profile",
|
||||
};
|
||||
import { redirect } from "next/navigation";
|
||||
import { getMe } from "@/lib/api";
|
||||
import { EditProfileForm } from "@/components/edit-profile-form";
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
// app/tags/[tagName]/page.tsx
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import { getThoughtsByTag, getUserProfile, getMe, Me, User } from "@/lib/api";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ tagName: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { tagName } = await params;
|
||||
return {
|
||||
title: `#${tagName}`,
|
||||
description: `Thoughts tagged with #${tagName}`,
|
||||
openGraph: {
|
||||
title: `#${tagName} · Thoughts`,
|
||||
description: `Thoughts tagged with #${tagName}`,
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
title: `#${tagName} · Thoughts`,
|
||||
description: `Thoughts tagged with #${tagName}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
import { buildThoughtThreads } from "@/lib/utils";
|
||||
import { ThoughtThread } from "@/components/thought-thread";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import {
|
||||
getThoughtById,
|
||||
getThoughtThread,
|
||||
getUserProfile,
|
||||
getMe,
|
||||
@@ -14,6 +16,42 @@ interface ThoughtPageProps {
|
||||
params: Promise<{ thoughtId: string }>;
|
||||
}
|
||||
|
||||
function stripHtml(html: string) {
|
||||
return html.replace(/<[^>]*>/g, "").trim();
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: ThoughtPageProps): Promise<Metadata> {
|
||||
const { thoughtId } = await params;
|
||||
const thought = await getThoughtById(thoughtId, null).catch(() => null);
|
||||
if (!thought) return { title: "Thought" };
|
||||
|
||||
const author = thought.author.displayName || thought.author.username;
|
||||
const preview = stripHtml(thought.content).slice(0, 120);
|
||||
const description = preview || `A thought by ${author}`;
|
||||
|
||||
return {
|
||||
title: `${author}: "${preview.slice(0, 60)}${preview.length > 60 ? "…" : ""}"`,
|
||||
description,
|
||||
openGraph: {
|
||||
type: "article",
|
||||
title: `${author} on Thoughts`,
|
||||
description,
|
||||
images: thought.author.avatarUrl
|
||||
? [{ url: thought.author.avatarUrl }]
|
||||
: [],
|
||||
publishedTime: thought.createdAt.toISOString(),
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
title: `${author} on Thoughts`,
|
||||
description,
|
||||
images: thought.author.avatarUrl ? [thought.author.avatarUrl] : [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function collectAuthors(thread: ThoughtThreadType): string[] {
|
||||
const authors = new Set<string>([thread.author.username]);
|
||||
for (const reply of thread.replies) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Metadata } from "next";
|
||||
import {
|
||||
getFollowersList,
|
||||
getFollowingList,
|
||||
@@ -7,6 +8,40 @@ import {
|
||||
getUserThoughts,
|
||||
Me,
|
||||
} from "@/lib/api";
|
||||
|
||||
interface ProfilePageProps {
|
||||
params: Promise<{ username: string }>;
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: ProfilePageProps): Promise<Metadata> {
|
||||
const { username } = await params;
|
||||
const user = await getUserProfile(username, null).catch(() => null);
|
||||
if (!user) return { title: username };
|
||||
|
||||
const name = user.displayName || user.username;
|
||||
const description =
|
||||
user.bio ||
|
||||
`Follow ${name} on Thoughts and across the Fediverse.`;
|
||||
|
||||
return {
|
||||
title: `${name} (@${user.username})`,
|
||||
description,
|
||||
openGraph: {
|
||||
type: "profile",
|
||||
title: `${name} (@${user.username})`,
|
||||
description,
|
||||
images: user.avatarUrl ? [{ url: user.avatarUrl }] : [],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
title: `${name} (@${user.username})`,
|
||||
description,
|
||||
images: user.avatarUrl ? [user.avatarUrl] : [],
|
||||
},
|
||||
};
|
||||
}
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { Calendar, Settings } from "lucide-react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
Reference in New Issue
Block a user