From bfdd9709470ca15f67a9c75608b3f626df81cf6c Mon Sep 17 00:00:00 2001 From: Justin Mclean Date: Thu, 3 Sep 2026 00:05:25 +0000 Subject: [PATCH 1/4] feat(site): add sitemap, robots, llms.txt and structured data Nothing on the site enumerated its own pages: /sitemap.xml, /robots.txt and /llms.txt all returned 404. This adds all three, generated from the same Fumadocs source the site already builds from, plus the metadata that search engines and agents read first. - src/app/sitemap.ts: every docs page, blog post and hand-written route - src/app/robots.ts: allow all, pointing at the sitemap - src/app/llms.txt/route.ts: an llmstxt.org index built with fumadocs-core's llms helper, prefaced by a short section on what the site is for - src/app/layout.tsx: metadataBase, per-route canonical, Open Graph and Twitter cards, and Organization/WebSite/SoftwareApplication JSON-LD - src/app/not-found.tsx: recovery links, so a 404 is not a dead end - src/app/(home)/page.tsx: the "How it works" steps were h4 under an h2, so the heading hierarchy skipped a level New routes outside docs and blog need adding to STATIC_ROUTES in src/lib/site.ts to appear in the sitemap. --- src/app/(home)/page.tsx | 4 +- src/app/layout.tsx | 80 ++++++++++++++++++++++++++++++++++++++- src/app/llms.txt/route.ts | 65 +++++++++++++++++++++++++++++++ src/app/not-found.tsx | 33 ++++++++++++++++ src/app/robots.ts | 31 +++++++++++++++ src/app/sitemap.ts | 48 +++++++++++++++++++++++ src/lib/blog.ts | 48 +++++++++++++++++++++++ src/lib/site.ts | 52 +++++++++++++++++++++++++ 8 files changed, 357 insertions(+), 4 deletions(-) create mode 100644 src/app/llms.txt/route.ts create mode 100644 src/app/robots.ts create mode 100644 src/app/sitemap.ts create mode 100644 src/lib/blog.ts create mode 100644 src/lib/site.ts diff --git a/src/app/(home)/page.tsx b/src/app/(home)/page.tsx index 3ac9abd9c1..951f371f0c 100644 --- a/src/app/(home)/page.tsx +++ b/src/app/(home)/page.tsx @@ -358,9 +358,9 @@ export default function HomePage() { {item.step}
-

+

{item.title} -

+

{item.desc}

diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f0c4be7483..04eebed4aa 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -21,22 +21,98 @@ import { RootProvider } from "fumadocs-ui/provider/next"; import type { Metadata } from "next"; import Script from "next/script"; import { Matomo } from "@/components/matomo"; +import { SITE_DESCRIPTION, SITE_URL } from "@/lib/site"; import "./global.css"; +const OG_IMAGE = "/img/apache-iggy-color-darkbg0.5x.png"; + export const metadata: Metadata = { + metadataBase: new URL(SITE_URL), title: { default: "Apache Iggy", template: "%s | Apache Iggy", }, - description: - "Apache Iggy is a persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second.", + description: SITE_DESCRIPTION, + // Relative values resolve against the current route, so every page gets its own canonical. + alternates: { canonical: "./" }, + // No title/description here on purpose: Next fills og:title and og:description + // from each page's own metadata, so per-page titles survive. + openGraph: { + type: "website", + siteName: "Apache Iggy", + url: "./", + images: [ + { + url: OG_IMAGE, + width: 1951, + height: 652, + alt: "Apache Iggy", + }, + ], + }, + twitter: { + card: "summary_large_image", + images: [OG_IMAGE], + }, icons: { icon: "/img/favicon.png" }, }; +// Structured data. Kept deliberately small: only claims the site already makes elsewhere. +const structuredData = { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "Organization", + "@id": `${SITE_URL}/#organization`, + name: "The Apache Software Foundation", + url: "https://www.apache.org/", + logo: `${SITE_URL}/img/asf_logo.svg`, + address: { + "@type": "PostalAddress", + streetAddress: "1000 N West Street, Suite 1200", + addressLocality: "Wilmington", + addressRegion: "DE", + postalCode: "19801", + addressCountry: "US", + }, + contactPoint: { + "@type": "ContactPoint", + contactType: "technical support", + email: "dev@iggy.apache.org", + url: `${SITE_URL}/community/`, + }, + }, + { + "@type": "WebSite", + "@id": `${SITE_URL}/#website`, + name: "Apache Iggy", + url: `${SITE_URL}/`, + description: SITE_DESCRIPTION, + inLanguage: "en", + publisher: { "@id": `${SITE_URL}/#organization` }, + }, + { + "@type": "SoftwareApplication", + "@id": `${SITE_URL}/#software`, + name: "Apache Iggy", + applicationCategory: "DeveloperApplication", + description: SITE_DESCRIPTION, + url: `${SITE_URL}/`, + license: "https://www.apache.org/licenses/LICENSE-2.0", + sameAs: ["https://github.com/apache/iggy"], + publisher: { "@id": `${SITE_URL}/#organization` }, + }, + ], +}; + export default function Layout({ children }: { children: React.ReactNode }) { return ( + " would close the tag early. Every value in the payload is a constant today, but the escape makes that independent of what the object later carries. --- src/app/layout.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e397455c4c..06d6e17916 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -97,13 +97,23 @@ const structuredData = { ], }; +/** + * JSON-LD has to be injected as raw text: React would HTML-escape it as a child + * and the quotes would end up as entities. The object above is built from + * constants in this file, so nothing here is user input, but JSON.stringify does + * not escape "<" -- so escape it, and a stray "" can never close the tag. + */ +function serializeJsonLd(data: unknown): string { + return JSON.stringify(data).replace(/