docs: improve page metadata

This commit is contained in:
Nicolas Meienberger 2026-04-18 11:30:33 +02:00
parent e19776a2b8
commit cbc874df0d
No known key found for this signature in database
11 changed files with 84 additions and 38 deletions

View file

@ -0,0 +1,8 @@
/assets/*
Cache-Control: public, max-age=31556952, immutable
/robots.txt
Cache-Control: public, max-age=3600
/sitemap.xml
Cache-Control: public, max-age=3600

BIN
apps/docs/src/assets/og.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -23,6 +23,8 @@ import {
type LucideIcon,
} from "lucide-react";
import { GithubLogoIcon } from "@phosphor-icons/react";
import screenshot1440Url from "@/assets/screenshot-1440.webp";
import screenshot768Url from "@/assets/screenshot-768.webp";
import { CornerCard } from "./CornerCard";
import Footer from "./Footer";
@ -208,8 +210,14 @@ function BrowserMockup() {
</div>
<div className="aspect-video bg-background/80">
<img
src="/images/screenshot.png"
src={screenshot1440Url}
srcSet={`${screenshot768Url} 768w, ${screenshot1440Url} 1440w`}
sizes="(min-width: 1100px) 55vw, 100vw"
alt="Zerobyte backups dashboard"
width={1440}
height={810}
fetchPriority="high"
decoding="async"
className="h-full w-full object-cover object-top"
/>
</div>

View file

@ -0,0 +1,34 @@
import ogImageAssetUrl from "@/assets/og.jpg";
export const siteUrl = "https://zerobyte.app";
export const siteTitle = "Zerobyte | Backup automation for Restic";
export const siteDescription =
"Zerobyte is a web control plane for Restic backups with scheduling, encrypted repositories, monitoring, and restore workflows.";
export const ogImageUrl = new URL(ogImageAssetUrl, siteUrl).toString();
export function getCanonicalUrl(path: string) {
return new URL(path, siteUrl).toString();
}
export function buildSeoHead({ title, description, path }: { title: string; description: string; path: string }) {
const canonicalUrl = getCanonicalUrl(path);
return {
meta: [
{ title },
{ name: "description", content: description },
{ property: "og:title", content: title },
{ property: "og:description", content: description },
{ property: "og:url", content: canonicalUrl },
{ name: "twitter:title", content: title },
{ name: "twitter:description", content: description },
],
links: [{ rel: "canonical", href: canonicalUrl }],
};
}
export function formatDocsTitle(title: string) {
if (title.toLowerCase().includes("zerobyte")) return title;
return `${title} | Zerobyte Docs`;
}

View file

@ -2,12 +2,10 @@ import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";
import { RootProvider } from "fumadocs-ui/provider/tanstack";
import Header from "../components/Header";
import { ogImageUrl } from "@/lib/metadata";
import appCss from "../styles.css?url";
const siteUrl = "https://zerobyte.app";
const ogImageUrl = `${siteUrl}/images/og.png`;
export const Route = createRootRoute({
head: () => ({
meta: [
@ -18,31 +16,10 @@ export const Route = createRootRoute({
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
title: "Zerobyte | Backup automation for Restic",
},
{
name: "description",
content:
"Zerobyte is a web control plane for Restic backups with scheduling, encrypted repositories, monitoring, and restore workflows.",
},
{
property: "og:title",
content: "Zerobyte | Backup automation for Restic",
},
{
property: "og:description",
content:
"Zerobyte is a web control plane for Restic backups with scheduling, encrypted repositories, monitoring, and restore workflows.",
},
{
property: "og:type",
content: "website",
},
{
property: "og:url",
content: siteUrl,
},
{
property: "og:image",
content: ogImageUrl,
@ -63,15 +40,6 @@ export const Route = createRootRoute({
name: "twitter:card",
content: "summary_large_image",
},
{
name: "twitter:title",
content: "Zerobyte | Backup automation for Restic",
},
{
name: "twitter:description",
content:
"Zerobyte is a web control plane for Restic backups with scheduling, encrypted repositories, monitoring, and restore workflows.",
},
{
name: "twitter:image",
content: ogImageUrl,

View file

@ -12,12 +12,30 @@ import { useFumadocsLoader } from "fumadocs-core/source/client";
import { Suspense } from "react";
import { Card, Cards } from "@/components/DocsCard";
import { DocsMdxLink } from "@/components/DocsMdxLink";
import { buildSeoHead, formatDocsTitle, siteDescription } from "@/lib/metadata";
type DocsLoaderData = {
path: string;
title: string;
description: string;
url: string;
pageTree: Awaited<ReturnType<typeof source.serializePageTree>>;
};
export const Route = createFileRoute("/docs/$")({
head: ({ loaderData }) => {
const data = loaderData as DocsLoaderData | undefined;
const title = formatDocsTitle(data?.title ?? "Zerobyte Documentation");
const description = data?.description ?? siteDescription;
const path = data?.url ?? "/docs";
return buildSeoHead({ title, description, path });
},
component: Page,
loader: async ({ params }) => {
const slugs = params._splat?.split("/") ?? [];
const data = await serverLoader({ data: slugs });
if (!data) throw notFound();
await clientLoader.preload(data.path);
return data;
},
@ -32,6 +50,9 @@ const serverLoader = createServerFn({
if (!page) throw notFound();
return {
path: page.path,
title: page.data.title ?? "Zerobyte Documentation",
description: page.data.description ?? siteDescription,
url: page.url,
pageTree: await source.serializePageTree(source.getPageTree()),
};
});
@ -77,7 +98,8 @@ const clientLoader = browserCollections.docs.createClientLoader({
},
});
function Page() {
const data = useFumadocsLoader(Route.useLoaderData());
const data = useFumadocsLoader(Route.useLoaderData() as DocsLoaderData);
return (
<div data-docs-page className="relative">
<div aria-hidden className="landing-hero-docs-grid pointer-events-none absolute inset-0" />

View file

@ -1,8 +1,12 @@
import { createFileRoute } from "@tanstack/react-router";
import LandingPage from "../components/LandingPage";
import { buildSeoHead, siteDescription, siteTitle } from "@/lib/metadata";
export const Route = createFileRoute("/")({ component: App });
export const Route = createFileRoute("/")({
head: () => buildSeoHead({ title: siteTitle, description: siteDescription, path: "/" }),
component: App,
});
function App() {
return <LandingPage />;

View file

@ -1,9 +1,8 @@
import { createFileRoute } from "@tanstack/react-router";
import { siteUrl } from "@/lib/metadata";
import { source } from "@/lib/source";
const siteUrl = "https://zerobyte.app";
function escapeXml(value: string) {
return value
.replaceAll("&", "&amp;")

View file

@ -4,4 +4,7 @@
"compatibility_date": "2025-09-02",
"compatibility_flags": ["nodejs_compat"],
"main": "@tanstack/react-start/server-entry",
"assets": {
"run_worker_first": ["/sitemap.xml"],
},
}