feat(seo): add JSON-LD, OG images and RSS feed for news
CI / check (push) Successful in 29s
CI / tests-unit (push) Successful in 2m17s
CI / tests-integration (push) Successful in 2m29s
CI / tests-imaging-browsers (push) Successful in 2m32s
CI / tests-quality (push) Failing after 2m18s
CI / tests-ui (push) Successful in 5m14s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 2m41s

This commit is contained in:
openhands committed 2026-10-22 18:22:32 +02:00
1 parent 97454f361b
commit 30583b8e2e
7 files changed
+328 -11

No files matched your search

File diff suppressed because it is too large. Load diff
+25
View File
@@ -0,0 +1,25 @@
import { resolveHotelName } from "@/lib/hotel-name";
import { buildNewsRss } from "@/lib/news-rss";
import { appBaseUrl } from "@/lib/seo";
import { getNewsList } from "@/lib/services/news-list";
// Built per request so the feed never needs a database connection during
// `next build`, matching the sitemap and robots routes.
export const dynamic = "force-dynamic";
export async function GET() {
const [hotelName, items] = await Promise.all([
resolveHotelName(),
getNewsList(30),
]);
return new Response(
buildNewsRss(items, { hotelName, appUrl: appBaseUrl() }),
{
headers: {
"Content-Type": "application/rss+xml; charset=utf-8",
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
},
},
);
}
+20 -1
View File
@@ -11,6 +11,7 @@ import { AuthTopBar } from "@/components/auth/auth-top-bar";
import { LoginForm } from "@/components/auth/login-form";
import Link from "@/components/link";
import { Reveal } from "@/components/motion-reveal";
import { JsonLd } from "@/components/public/json-ld";
import { PublicLoadError } from "@/components/public/load-error";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { SpotlightCard } from "@/components/spotlight-card";
@@ -21,6 +22,11 @@ import { CameraWeb, db, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
import { resolveHotelName } from "@/lib/hotel-name";
import { withPublicPagePerformance } from "@/lib/public-page-performance";
import {
absoluteAssetUrl,
appBaseUrl,
buildWebsiteStructuredData,
} from "@/lib/seo";
import { captchaConfig } from "@/lib/services/captcha";
import { getNewsList } from "@/lib/services/news-list";
import { loadProfilePrivacyMap } from "@/lib/services/profile-privacy";
@@ -48,10 +54,15 @@ export async function generateMetadata(): Promise<Metadata> {
const tp = await getTranslations("pages.home");
const title = tn("home");
const description = tp("welcomeBody");
const appUrl = appBaseUrl();
return {
title,
description,
openGraph: { title, description, type: "website" },
alternates: {
canonical: appUrl,
types: { "application/rss+xml": `${appUrl}/news/rss.xml` },
},
openGraph: { title, description, type: "website", url: appUrl },
};
}
@@ -442,6 +453,7 @@ async function Home() {
const exploreLinks = [
{ href: "/community", label: tn("community") },
{ href: "/news", label: tn("articles") },
{ href: "/events", label: tn("events") },
{ href: "/photos", label: tn("photos") },
{ href: "/radio", label: tn("radio") },
{ href: "/shop", label: tn("shop") },
@@ -460,8 +472,15 @@ async function Home() {
{ value: articleCount, label: tp("statsArticles"), icon: ICON_ARTICLE },
];
const structuredData = buildWebsiteStructuredData({
hotelName,
url: appBaseUrl(),
logo: absoluteAssetUrl(logo, appBaseUrl()),
});
return (
<div className="flex w-full flex-col gap-14 pb-16 pt-2 md:gap-20 md:pb-24">
<JsonLd data={structuredData} />
{/* ── Top bar: logo + live online + language/clock/theme ── */}
<div className="mx-auto w-full max-w-7xl">
<AuthTopBar hotelName={hotelName} logo={logo} online={online} />
+19 -3
View File
@@ -38,7 +38,10 @@ const pixelify = Pixelify_Sans({
// export const instant = false;
export async function generateMetadata(): Promise<Metadata> {
const appUrl = process.env.APP_URL ?? "http://localhost:3000";
const appUrl = (process.env.APP_URL ?? "http://localhost:3000").replace(
/\/+$/,
"",
);
const hotel = await resolveHotelName();
const customFavicon = await siteSettings.get("cms_favicon");
const faviconUrl = customFavicon ?? getDefaultFaviconDataUri();
@@ -50,15 +53,28 @@ export async function generateMetadata(): Promise<Metadata> {
title: { default: `${hotel} - Hotel`, template: `%s · ${hotel}` },
description: `${hotel} — a Habbo retro hotel. Hang out, collect furni and meet friends.`,
metadataBase: new URL(appUrl),
alternates: { canonical: appUrl },
alternates: {
canonical: appUrl,
types: { "application/rss+xml": `${appUrl}/news/rss.xml` },
},
icons,
openGraph: {
title: `${hotel} - Hotel`,
description: `${hotel} — a Habbo retro hotel. Hang out, collect furni and meet friends.`,
type: "website",
siteName: hotel,
images: [
{
url: `${appUrl}/assets/images/next_header.jpg`,
alt: `${hotel} - Hotel`,
},
],
},
twitter: {
card: "summary_large_image",
title: `${hotel} - Hotel`,
images: [`${appUrl}/assets/images/next_header.jpg`],
},
twitter: { card: "summary_large_image", title: `${hotel} - Hotel` },
};
}