docs: improve page metadata
This commit is contained in:
parent
e19776a2b8
commit
cbc874df0d
11 changed files with 84 additions and 38 deletions
8
apps/docs/public/_headers
Normal file
8
apps/docs/public/_headers
Normal 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
BIN
apps/docs/src/assets/og.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
BIN
apps/docs/src/assets/screenshot-1440.webp
Normal file
BIN
apps/docs/src/assets/screenshot-1440.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
BIN
apps/docs/src/assets/screenshot-768.webp
Normal file
BIN
apps/docs/src/assets/screenshot-768.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -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>
|
||||
|
|
|
|||
34
apps/docs/src/lib/metadata.ts
Normal file
34
apps/docs/src/lib/metadata.ts
Normal 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`;
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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 />;
|
||||
|
|
|
|||
|
|
@ -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("&", "&")
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue