This is an automated email from the ASF dual-hosted git repository.

kparisa pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iggy-website.git


The following commit(s) were added to refs/heads/main by this push:
     new 4d91901b feat(docs): publish a Markdown copy of every docs page (#79)
4d91901b is described below

commit 4d91901b5f3128db44a48d28cb6ff0880d3b4db1
Author: Justin Mclean <[email protected]>
AuthorDate: Thu Sep 3 16:17:02 2026 +1000

    feat(docs): publish a Markdown copy of every docs page (#79)
    
    > Stacked on #77 as it edits `llms.txt`, so it needs that one merged
    first.
    
    An agent that wants a docs page has to parse the rendered HTML today.
    This writes a Markdown copy next to each page at build time, so
    `/docs/server/security/` is also available as
    `/docs/server/security.md`.
    
    - `scripts/generate-docs-markdown.mjs` runs in `prebuild` after the
    stars fetch and rewrites the copies from `content/docs` on every build.
    Generated files are gitignored, like `src/github-stars.json`, so they
    can't drift from the source.
    - Each docs page advertises its copy with `<link rel="alternate"
    type="text/markdown">`, and `llms.txt` states the convention.
    - The `.md` files aren't in the sitemap. They're page alternates, not
    pages.
    
    `npm run build`, then: 76 `.md` files in `out/`, the `rel="alternate"`
    and `rel="canonical"` links both present on a docs page, `index.html`
    count unchanged, and `git status` clean.
    
    ---------
    
    Co-authored-by: Kranti Parisa <[email protected]>
---
 .gitignore                              |   2 +
 package.json                            |   4 +-
 scripts/generate-docs-markdown.mjs      | 109 ++++++++++++++++++++++++++++++++
 src/app/(home)/page.tsx                 |   6 +-
 src/app/(site)/blogs/[...slug]/page.tsx |  10 +++
 src/app/docs/[[...slug]]/page.tsx       |   6 ++
 src/app/layout.tsx                      |  82 +++++++++++++++++++++++-
 src/app/llms.txt/route.ts               |  76 ++++++++++++++++++++++
 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 +++++++++++++++
 13 files changed, 500 insertions(+), 7 deletions(-)

diff --git a/.gitignore b/.gitignore
index 043d44f7..69ecf6f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,8 @@ tsconfig.tsbuildinfo
 
 # Build-time generated
 src/github-stars.json
+public/docs.md
+public/docs/**/*.md
 
 # System files
 .DS_Store
diff --git a/package.json b/package.json
index 04633f5e..1bca9fdb 100644
--- a/package.json
+++ b/package.json
@@ -3,9 +3,9 @@
   "version": "0.1.0",
   "private": true,
   "scripts": {
-    "predev": "node scripts/fetch-github-stars.mjs",
+    "predev": "node scripts/fetch-github-stars.mjs && node 
scripts/generate-docs-markdown.mjs",
     "dev": "next dev",
-    "prebuild": "node scripts/fetch-github-stars.mjs",
+    "prebuild": "node scripts/fetch-github-stars.mjs && node 
scripts/generate-docs-markdown.mjs",
     "build": "next build",
     "start": "next start",
     "postinstall": "fumadocs-mdx"
diff --git a/scripts/generate-docs-markdown.mjs 
b/scripts/generate-docs-markdown.mjs
new file mode 100644
index 00000000..9b5628c3
--- /dev/null
+++ b/scripts/generate-docs-markdown.mjs
@@ -0,0 +1,109 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Emits a Markdown copy of every docs page next to its HTML route, so an agent
+ * can fetch `/docs/server/security.md` instead of parsing the rendered page.
+ *
+ * The site is a static export served by ASF infrastructure, so it cannot do
+ * Accept-header content negotiation; a sibling file is the version of this 
that
+ * needs no server. Output goes to public/, is gitignored, and is rebuilt from
+ * content/docs on every build.
+ */
+
+import { readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync, statSync 
} from "fs";
+import { join, relative, dirname, basename } from "path";
+
+const CONTENT = "content/docs";
+const PUBLIC = "public";
+const SITE_URL = "https://iggy.apache.org";;
+const REPO_BLOB = "https://github.com/apache/iggy-website/blob/main";;
+
+function walk(dir) {
+  return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
+    const path = join(dir, entry.name);
+    if (entry.isDirectory()) return walk(path);
+    return entry.name.endsWith(".mdx") ? [path] : [];
+  });
+}
+
+/** content/docs/server/security.mdx -> public/docs/server/security.md
+ *  content/docs/binary-protocol/index.mdx -> public/docs/binary-protocol.md
+ *  content/docs/index.mdx -> public/docs.md */
+function outputPath(source) {
+  const rel = relative(CONTENT, source).replace(/\.mdx$/, "");
+  const stem = basename(rel) === "index" ? dirname(rel) : rel;
+  return join(PUBLIC, "docs" + (stem === "." ? "" : `/${stem}`) + ".md");
+}
+
+/** Frontmatter is YAML, but only two scalar keys are read here, so it is not
+ *  worth a parser dependency. */
+function splitFrontmatter(raw) {
+  if (!raw.startsWith("---\n")) return { meta: {}, body: raw };
+  const end = raw.indexOf("\n---", 3);
+  if (end === -1) return { meta: {}, body: raw };
+  const meta = {};
+  for (const line of raw.slice(4, end).split("\n")) {
+    const match = /^(title|description):\s*(.*)$/.exec(line);
+    if (match) meta[match[1]] = match[2].trim().replace(/^["']|["']$/g, "");
+  }
+  return { meta, body: raw.slice(end + 4).replace(/^\n+/, "") };
+}
+
+function render(source, raw) {
+  const { meta, body } = splitFrontmatter(raw);
+  const url = `${SITE_URL}/${outputPath(source).replace(/^public\//, 
"").replace(/\.md$/, "")}/`;
+  const head = [
+    `# ${meta.title ?? basename(source, ".mdx")}`,
+    ...(meta.description ? [`> ${meta.description}`] : []),
+    `Rendered page: ${url}`,
+    `Source: ${REPO_BLOB}/${source}`,
+  ].join("\n\n");
+  // The body keeps its MDX as authored: custom components appear as tags, 
which
+  // is honest about what the page contains and needs no MDX pipeline here.
+  return `${head}\n\n${body.trimEnd()}\n`;
+}
+
+/** Everything this script wrote last time. Nothing else under public/ is .md. 
*/
+function clean(dir) {
+  for (const entry of readdirSync(dir, { withFileTypes: true })) {
+    const path = join(dir, entry.name);
+    if (entry.isDirectory()) clean(path);
+    else if (entry.name.endsWith(".md")) rmSync(path);
+  }
+}
+
+function main() {
+  const sources = walk(CONTENT).sort();
+  try {
+    if (statSync(join(PUBLIC, "docs")).isDirectory()) clean(join(PUBLIC, 
"docs"));
+  } catch {
+    // No public/docs yet; nothing to clean.
+  }
+  rmSync(join(PUBLIC, "docs.md"), { force: true });
+
+  for (const source of sources) {
+    const target = outputPath(source);
+    mkdirSync(dirname(target), { recursive: true });
+    writeFileSync(target, render(source, readFileSync(source, "utf8")));
+  }
+  console.log(`Generated ${sources.length} Markdown copies of docs pages.`);
+}
+
+main();
diff --git a/src/app/(home)/page.tsx b/src/app/(home)/page.tsx
index 3ea052f7..9c5c08ba 100644
--- a/src/app/(home)/page.tsx
+++ b/src/app/(home)/page.tsx
@@ -25,7 +25,7 @@ import { BenchmarkSection } from 
"@/components/benchmark-chart";
 import { LATENCY_MS } from "@/lib/benchmark";
 
 export const metadata: Metadata = {
-  title: "Apache Iggy | Hyper-Efficient Message Streaming written in Rust.",
+  title: "Hyper-Efficient Message Streaming written in Rust",
   description:
     "Apache Iggy is a high-performance, persistent message streaming platform 
written in Rust, capable of processing millions of messages per second with 
ultra-low latency.",
 };
@@ -359,9 +359,9 @@ export default function HomePage() {
                     {item.step}
                   </div>
                   <div className="min-w-0">
-                    <h4 className="text-base font-semibold text-[#fffaeb] 
mb-1">
+                    <h3 className="text-base font-semibold text-[#fffaeb] 
mb-1">
                       {item.title}
-                    </h4>
+                    </h3>
                     <p className="text-sm font-light leading-relaxed 
text-[#aeb5bd] m-0">
                       {item.desc}
                     </p>
diff --git a/src/app/(site)/blogs/[...slug]/page.tsx 
b/src/app/(site)/blogs/[...slug]/page.tsx
index 70c9699a..9a7c563a 100644
--- a/src/app/(site)/blogs/[...slug]/page.tsx
+++ b/src/app/(site)/blogs/[...slug]/page.tsx
@@ -148,8 +148,18 @@ export async function generateMetadata(props: {
   const page = findPostByDateSlug(params.slug);
   if (!page) notFound();
 
+  const date = new Date(page.date);
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, "0");
+  const day = String(date.getDate()).padStart(2, "0");
+
   return {
     title: page.title,
     description: page.description,
+    // Set explicitly: the inherited relative canonical loses the trailing 
slash
+    // on this catch-all route, so it would not match the URL actually served.
+    alternates: {
+      canonical: `/blogs/${year}/${month}/${day}/${getSlug(page.info.path)}/`,
+    },
   };
 }
diff --git a/src/app/docs/[[...slug]]/page.tsx 
b/src/app/docs/[[...slug]]/page.tsx
index efb85f5c..bd43140b 100644
--- a/src/app/docs/[[...slug]]/page.tsx
+++ b/src/app/docs/[[...slug]]/page.tsx
@@ -79,6 +79,12 @@ export async function generateMetadata(props: {
   return {
     title: page.data.title,
     description: page.data.description,
+    alternates: {
+      // Set both: defining alternates here replaces the root's, canonical 
included.
+      canonical: `${page.url}/`,
+      // The Markdown copy written by scripts/generate-docs-markdown.mjs.
+      types: { "text/markdown": `${page.url}.md` },
+    },
   };
 }
 
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f0c4be74..06d6e179 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -21,22 +21,100 @@ 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`,
+      contactPoint: {
+        "@type": "ContactPoint",
+        contactType: "technical support",
+        email: "[email protected]",
+        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` },
+    },
+  ],
+};
+
+/**
+ * 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 "</script>" can never close the 
tag.
+ */
+function serializeJsonLd(data: unknown): string {
+  return JSON.stringify(data).replace(/</g, "\\u003c");
+}
+
 export default function Layout({ children }: { children: React.ReactNode }) {
   return (
     <html lang="en" suppressHydrationWarning>
       <body className="flex min-h-screen flex-col font-sans antialiased">
+        <script
+          type="application/ld+json"
+          dangerouslySetInnerHTML={{ __html: serializeJsonLd(structuredData) }}
+        />
         <RootProvider
           search={{ options: { type: "static" as const } }}
           theme={{ defaultTheme: "dark" }}
diff --git a/src/app/llms.txt/route.ts b/src/app/llms.txt/route.ts
new file mode 100644
index 00000000..5fe8a4d0
--- /dev/null
+++ b/src/app/llms.txt/route.ts
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { llms } from "fumadocs-core/source/llms";
+import { source } from "@/lib/source";
+import { publishedPosts } from "@/lib/blog";
+import { SITE_DESCRIPTION, SITE_URL, absoluteUrl } from "@/lib/site";
+
+export const dynamic = "force-static";
+
+const INTRO = `# Apache Iggy
+
+> ${SITE_DESCRIPTION}
+
+Apache Iggy is a message streaming server, not a hosted service: there is no
+API on this domain to call. This site carries the documentation, blog and
+community pages. The source, releases and issue tracker live at
+https://github.com/apache/iggy.
+
+## When to use this site
+
+- Running or configuring the server, including storage, networking and 
clustering: ${absoluteUrl("/docs/server")} and 
${absoluteUrl("/docs/clustering")}
+- Writing a producer or consumer in a given language: 
${absoluteUrl("/docs/sdk")}
+- Talking to the server directly over QUIC, TCP or HTTP: 
${absoluteUrl("/docs/binary-protocol")}
+- Moving data in or out of Iggy without writing code: 
${absoluteUrl("/docs/connectors")}
+- Command line and web administration: ${absoluteUrl("/docs/cli")} and 
${absoluteUrl("/docs/web_ui")}
+- Downloading a release: ${absoluteUrl("/downloads")}
+- Contributing, or reaching the project's mailing lists and Discord: 
${absoluteUrl("/community")}
+
+Every documentation page has a Markdown copy at the same path with a .md
+suffix, so ${absoluteUrl("/docs/server/security")} is also served as
+${SITE_URL}/docs/server/security.md.
+`;
+
+export function GET(): Response {
+  const posts = publishedPosts()
+    .slice(0, 10)
+    .map(({ post, href, date }) => {
+      const day = date.toISOString().slice(0, 10);
+      return `- [${post.title}](${absoluteUrl(href)}): ${day}`;
+    })
+    .join("\n");
+
+  // The helper emits site-relative links; llms.txt consumers want absolute 
ones.
+  const docsIndex = llms(source)
+    .index()
+    .replace(/\]\((\/[^)\s]*)\)/g, (_match, path: string) => 
`](${absoluteUrl(path)})`)
+    // One H1 per file: the helper's top-level heading becomes a section.
+    .replace(/^# /gm, "## ");
+
+  const body = [
+    INTRO,
+    docsIndex,
+    `## Blog\n\nThe ten most recent posts. The full list is at 
${absoluteUrl("/blogs")}.\n\n${posts}\n`,
+  ].join("\n");
+
+  return new Response(body, {
+    headers: { "Content-Type": "text/plain; charset=utf-8" },
+  });
+}
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
index 271125c8..4ca911dd 100644
--- a/src/app/not-found.tsx
+++ b/src/app/not-found.tsx
@@ -24,6 +24,14 @@ export const metadata: Metadata = {
   title: "404: Page not found",
 };
 
+const RECOVERY_LINKS = [
+  { href: "/", label: "Home" },
+  { href: "/docs", label: "Documentation" },
+  { href: "/blogs", label: "Blog" },
+  { href: "/downloads", label: "Downloads" },
+  { href: "/community", label: "Community" },
+];
+
 export default function NotFound() {
   return (
     <main className="flex min-h-screen flex-col items-center justify-center 
gap-4 px-6 text-center">
@@ -42,6 +50,31 @@ export default function NotFound() {
       >
         Back to documentation
       </Link>
+      <nav
+        aria-label="Where to go next"
+        className="mt-6 flex flex-wrap items-center justify-center gap-x-6 
gap-y-2 text-sm text-fd-muted-foreground"
+      >
+        {RECOVERY_LINKS.map((link) => (
+          <Link
+            key={link.href}
+            href={link.href}
+            className="transition-colors hover:text-fd-primary"
+          >
+            {link.label}
+          </Link>
+        ))}
+      </nav>
+      <p className="mt-6 max-w-md text-xs text-fd-muted-foreground">
+        Every page on this site is listed in{" "}
+        <a href="/sitemap.xml" className="underline hover:text-fd-primary">
+          /sitemap.xml
+        </a>
+        , and{" "}
+        <a href="/llms.txt" className="underline hover:text-fd-primary">
+          /llms.txt
+        </a>{" "}
+        summarises the documentation.
+      </p>
     </main>
   );
 }
diff --git a/src/app/robots.ts b/src/app/robots.ts
new file mode 100644
index 00000000..24b58ab5
--- /dev/null
+++ b/src/app/robots.ts
@@ -0,0 +1,31 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { MetadataRoute } from "next";
+import { SITE_URL } from "@/lib/site";
+
+export const dynamic = "force-static";
+
+export default function robots(): MetadataRoute.Robots {
+  return {
+    rules: [{ userAgent: "*", allow: "/" }],
+    sitemap: `${SITE_URL}/sitemap.xml`,
+    host: SITE_URL,
+  };
+}
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
new file mode 100644
index 00000000..69ab9a9c
--- /dev/null
+++ b/src/app/sitemap.ts
@@ -0,0 +1,48 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { MetadataRoute } from "next";
+import { source } from "@/lib/source";
+import { publishedPosts } from "@/lib/blog";
+import { STATIC_ROUTES, absoluteUrl } from "@/lib/site";
+
+export const dynamic = "force-static";
+
+export default function sitemap(): MetadataRoute.Sitemap {
+  const staticPages = STATIC_ROUTES.map((route) => ({
+    url: absoluteUrl(route),
+    changeFrequency: "weekly" as const,
+    priority: route === "/" ? 1 : 0.7,
+  }));
+
+  const docsPages = source.getPages().map((page) => ({
+    url: absoluteUrl(page.url),
+    changeFrequency: "weekly" as const,
+    priority: 0.8,
+  }));
+
+  const blogPages = publishedPosts().map(({ href, date }) => ({
+    url: absoluteUrl(href),
+    lastModified: date,
+    changeFrequency: "yearly" as const,
+    priority: 0.6,
+  }));
+
+  return [...staticPages, ...docsPages, ...blogPages];
+}
diff --git a/src/lib/blog.ts b/src/lib/blog.ts
new file mode 100644
index 00000000..ec55ca18
--- /dev/null
+++ b/src/lib/blog.ts
@@ -0,0 +1,48 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { blogPosts } from "@/lib/source";
+
+/** Filename stem of a blog post, which is the last segment of its URL. */
+export function blogSlug(filePath: string): string {
+  return (
+    filePath
+      .replace(/\.mdx?$/, "")
+      .split("/")
+      .pop() ?? filePath
+  );
+}
+
+/** Published, non-draft posts, newest first, with their site-relative URLs. */
+export function publishedPosts() {
+  return [...blogPosts]
+    .filter((post) => !post.draft)
+    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
+    .map((post) => {
+      const date = new Date(post.date);
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, "0");
+      const day = String(date.getDate()).padStart(2, "0");
+      return {
+        post,
+        date,
+        href: `/blogs/${year}/${month}/${day}/${blogSlug(post.info.path)}`,
+      };
+    });
+}
diff --git a/src/lib/site.ts b/src/lib/site.ts
new file mode 100644
index 00000000..8e067b46
--- /dev/null
+++ b/src/lib/site.ts
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/** Canonical origin of the published site. Used for canonical URLs, Open 
Graph, sitemap and llms.txt. */
+export const SITE_URL = "https://iggy.apache.org";;
+
+/** Short description reused across metadata, structured data and llms.txt. */
+export const SITE_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.";
+
+/**
+ * Pages outside the docs and blog collections.
+ *
+ * Static export means these are the only hand-written routes, so they are 
listed
+ * here rather than discovered. Add a route here when you add a page under
+ * `src/app/(site)/`, or it will be missing from the sitemap.
+ */
+export const STATIC_ROUTES = [
+  "/",
+  "/blogs",
+  "/downloads",
+  "/community",
+  "/community/become-a-committer",
+  "/community/brand-kit",
+  "/community/graduation-checklist",
+  "/community/how-to-contribute",
+  "/community/how-to-release",
+  "/community/team",
+] as const;
+
+/** `trailingSlash: true` in next.config.mjs, so every emitted URL ends in a 
slash. */
+export function absoluteUrl(path: string): string {
+  const withLeading = path.startsWith("/") ? path : `/${path}`;
+  const withTrailing = withLeading.endsWith("/") ? withLeading : 
`${withLeading}/`;
+  return `${SITE_URL}${withTrailing}`;
+}

Reply via email to