@@ -68,9 +99,28 @@ export async function generateMetadata(props: {
if (!page) notFound();
+ const url = page.url;
+ const image = getBlogImage(page).url;
+
return {
title: page.data.title,
description: page.data.description,
+ alternates: { canonical: url },
+ openGraph: {
+ type: "article",
+ url,
+ title: page.data.title,
+ description: page.data.description,
+ publishedTime: new Date(page.data.date).toISOString(),
+ authors: [page.data.author],
+ images: [{ url: image, width: 1200, height: 630, alt: page.data.title }],
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: page.data.title,
+ description: page.data.description,
+ images: [image],
+ },
};
}
diff --git a/docs/app/docs/[[...slug]]/page.tsx b/docs/app/docs/[[...slug]]/page.tsx
index 8d5c34f60..79dc1d38d 100644
--- a/docs/app/docs/[[...slug]]/page.tsx
+++ b/docs/app/docs/[[...slug]]/page.tsx
@@ -1,6 +1,7 @@
import { LLMCopyButton, ViewOptions } from "@/components/ai/page-actions";
+import { JsonLd } from "@/components/seo/JsonLd";
import { gitConfig } from "@/lib/layout.shared";
-import { getPageImage, source } from "@/lib/source";
+import { BASE_URL, getPageImage, source } from "@/lib/source";
import { getMDXComponents } from "@/mdx-components";
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/layouts/docs/page";
import { createRelativeLink } from "fumadocs-ui/mdx";
@@ -14,8 +15,48 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
const MDX = page.data.body;
+ const url = `${BASE_URL}${page.url}`;
+ const breadcrumbItems = [
+ { name: "Docs", url: `${BASE_URL}/docs` },
+ ...page.slugs.map((_, i) => {
+ const p = source.getPage(page.slugs.slice(0, i + 1));
+ return p ? { name: p.data.title, url: `${BASE_URL}${p.url}` } : null;
+ }),
+ ].filter((item): item is { name: string; url: string } => item !== null);
+ const jsonLd = [
+ {
+ "@context": "https://schema.org",
+ "@type": "TechArticle",
+ headline: page.data.title,
+ description: page.data.description,
+ image: `${BASE_URL}${getPageImage(page).url}`,
+ author: { "@type": "Organization", name: "OpenUI" },
+ publisher: {
+ "@type": "Organization",
+ name: "OpenUI",
+ logo: { "@type": "ImageObject", url: `${BASE_URL}/favicon.svg` },
+ },
+ ...(page.data.lastModified
+ ? { dateModified: new Date(page.data.lastModified).toISOString() }
+ : {}),
+ mainEntityOfPage: { "@type": "WebPage", "@id": url },
+ url,
+ },
+ {
+ "@context": "https://schema.org",
+ "@type": "BreadcrumbList",
+ itemListElement: breadcrumbItems.map((item, i) => ({
+ "@type": "ListItem",
+ position: i + 1,
+ name: item.name,
+ item: item.url,
+ })),
+ },
+ ];
+
return (
+
{page.data.title}
{page.data.description}
@@ -49,8 +90,19 @@ export async function generateMetadata(props: PageProps<"/docs/[[...slug]]">): P
return {
title: page.data.title,
description: page.data.description,
+ alternates: { canonical: page.url },
openGraph: {
+ type: "article",
+ url: page.url,
+ title: page.data.title,
+ description: page.data.description,
images: getPageImage(page).url,
},
+ twitter: {
+ card: "summary_large_image",
+ title: page.data.title,
+ description: page.data.description,
+ images: [getPageImage(page).url],
+ },
};
}
diff --git a/docs/app/layout.tsx b/docs/app/layout.tsx
index bcd8cf474..36e5071af 100644
--- a/docs/app/layout.tsx
+++ b/docs/app/layout.tsx
@@ -1,5 +1,5 @@
import { RootProvider } from "fumadocs-ui/provider/next";
-import type { Metadata } from "next";
+import type { Metadata, Viewport } from "next";
import { Geist_Mono, Inter } from "next/font/google";
import Script from "next/script";
import { BASE_URL } from "../lib/source";
@@ -47,9 +47,12 @@ export const metadata: Metadata = {
canonical: "/",
},
icons: {
- icon: "/favicon.svg",
+ icon: [
+ { url: "/favicon.svg", type: "image/svg+xml" },
+ { url: "/favicon-32.png", type: "image/png", sizes: "32x32" },
+ ],
shortcut: "/favicon.svg",
- apple: "/favicon.svg",
+ apple: "/apple-touch-icon.png",
},
openGraph: {
type: "website",
@@ -57,6 +60,7 @@ export const metadata: Metadata = {
siteName: SITE_TITLE,
title: SITE_TITLE,
description: SITE_DESCRIPTION,
+ locale: "en_US",
images: [
{
url: SITE_IMAGE,
@@ -68,6 +72,8 @@ export const metadata: Metadata = {
},
twitter: {
card: "summary_large_image",
+ site: "@thesysdev",
+ creator: "@thesysdev",
title: SITE_TITLE,
description: SITE_DESCRIPTION,
images: [SITE_IMAGE],
@@ -85,6 +91,13 @@ export const metadata: Metadata = {
},
};
+export const viewport: Viewport = {
+ themeColor: [
+ { media: "(prefers-color-scheme: light)", color: "#ffffff" },
+ { media: "(prefers-color-scheme: dark)", color: "#000000" },
+ ],
+};
+
export default function Layout({ children }: LayoutProps<"/">) {
return (
diff --git a/docs/app/manifest.ts b/docs/app/manifest.ts
new file mode 100644
index 000000000..0a306720a
--- /dev/null
+++ b/docs/app/manifest.ts
@@ -0,0 +1,19 @@
+import type { MetadataRoute } from "next";
+
+export default function manifest(): MetadataRoute.Manifest {
+ return {
+ name: "OpenUI - The Open Standard for Generative UI",
+ short_name: "OpenUI",
+ description:
+ "OpenUI is a full-stack Generative UI framework with a compact streaming-first language, a React runtime with built-in components, and ready-to-use chat interfaces.",
+ start_url: "/",
+ display: "standalone",
+ background_color: "#ffffff",
+ theme_color: "#ffffff",
+ icons: [
+ { src: "/favicon.svg", type: "image/svg+xml", sizes: "any" },
+ { src: "/icon-192.png", type: "image/png", sizes: "192x192" },
+ { src: "/icon-512.png", type: "image/png", sizes: "512x512" },
+ ],
+ };
+}
diff --git a/docs/app/og/blog/[...slug]/route.tsx b/docs/app/og/blog/[...slug]/route.tsx
new file mode 100644
index 000000000..2558d9ed5
--- /dev/null
+++ b/docs/app/og/blog/[...slug]/route.tsx
@@ -0,0 +1,27 @@
+import { blog, getBlogImage } from "@/lib/source";
+import { ImageResponse } from "@takumi-rs/image-response";
+import { generate as DefaultImage } from "fumadocs-ui/og/takumi";
+import { notFound } from "next/navigation";
+
+export const revalidate = false;
+
+export async function GET(_req: Request, { params }: RouteContext<"/og/blog/[...slug]">) {
+ const { slug } = await params;
+ const page = blog.getPage(slug.slice(0, -1));
+ if (!page) notFound();
+
+ return new ImageResponse(
+ ,
+ {
+ width: 1200,
+ height: 630,
+ format: "webp",
+ },
+ );
+}
+
+export function generateStaticParams() {
+ return blog.getPages().map((page) => ({
+ slug: getBlogImage(page).segments,
+ }));
+}
diff --git a/docs/app/playground/layout.tsx b/docs/app/playground/layout.tsx
index dc4dfe4b9..b93e546aa 100644
--- a/docs/app/playground/layout.tsx
+++ b/docs/app/playground/layout.tsx
@@ -1,7 +1,29 @@
import { WebsiteThemeProvider } from "@/components/website-theme-provider";
+import type { Metadata } from "next";
import type { ReactNode } from "react";
import "./layout.css";
+const TITLE = "Playground - Generate UI from a Prompt";
+const DESCRIPTION =
+ "Build and preview generative UI live in your browser. Prompt an LLM and watch OpenUI render interactive components in real time - no setup required.";
+
+export const metadata: Metadata = {
+ title: TITLE,
+ description: DESCRIPTION,
+ alternates: { canonical: "/playground" },
+ openGraph: {
+ type: "website",
+ url: "/playground",
+ title: TITLE,
+ description: DESCRIPTION,
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: TITLE,
+ description: DESCRIPTION,
+ },
+};
+
export default function PlaygroundLayout({ children }: { children: ReactNode }) {
return {children};
}
diff --git a/docs/app/sitemap.ts b/docs/app/sitemap.ts
index 8ab517728..ae1c13a32 100644
--- a/docs/app/sitemap.ts
+++ b/docs/app/sitemap.ts
@@ -1,6 +1,6 @@
import { BASE_URL, blog, source } from "@/lib/source";
-const STATIC_PATHS = ["/", "/playground", "/blog"];
+const STATIC_PATHS = ["/", "/playground", "/blog", "/openclaw-os"];
export default async function sitemap() {
const staticRoutes = STATIC_PATHS.map((path) => ({
@@ -17,8 +17,8 @@ export default async function sitemap() {
const blogRoutes = blog.getPages().map((page) => ({
url: `${BASE_URL}${page.url}`,
- lastModified: new Date(),
- changeFrequency: "weekly" as const,
+ lastModified: page.data.date ? new Date(page.data.date) : new Date(),
+ changeFrequency: "monthly" as const,
}));
return [...staticRoutes, ...docsRoutes, ...blogRoutes];
diff --git a/docs/components/seo/JsonLd.tsx b/docs/components/seo/JsonLd.tsx
new file mode 100644
index 000000000..cbf926c74
--- /dev/null
+++ b/docs/components/seo/JsonLd.tsx
@@ -0,0 +1,13 @@
+/**
+ * Renders a JSON-LD structured-data block. Server-rendered so crawlers see it
+ * in the initial HTML. Pass any schema.org object (or array) as `data`.
+ */
+export function JsonLd({ data }: { data: Record | Record[] }) {
+ return (
+
+ );
+}
diff --git a/docs/lib/source.ts b/docs/lib/source.ts
index bef5cc7e3..af4326c70 100644
--- a/docs/lib/source.ts
+++ b/docs/lib/source.ts
@@ -26,6 +26,15 @@ export function getPageImage(page: InferPageType) {
};
}
+export function getBlogImage(page: InferPageType) {
+ const segments = [...page.slugs, "image.webp"];
+
+ return {
+ segments,
+ url: `/og/blog/${segments.join("/")}`,
+ };
+}
+
export async function getLLMText(page: InferPageType) {
const processed = await page.data.getText("processed");
diff --git a/docs/public/apple-touch-icon.png b/docs/public/apple-touch-icon.png
new file mode 100644
index 000000000..5f44153a6
Binary files /dev/null and b/docs/public/apple-touch-icon.png differ
diff --git a/docs/public/favicon-32.png b/docs/public/favicon-32.png
new file mode 100644
index 000000000..f3ef82137
Binary files /dev/null and b/docs/public/favicon-32.png differ
diff --git a/docs/public/icon-192.png b/docs/public/icon-192.png
new file mode 100644
index 000000000..702336869
Binary files /dev/null and b/docs/public/icon-192.png differ
diff --git a/docs/public/icon-512.png b/docs/public/icon-512.png
new file mode 100644
index 000000000..7014e44bc
Binary files /dev/null and b/docs/public/icon-512.png differ