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,
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { GithubLogoIcon } from "@phosphor-icons/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 { CornerCard } from "./CornerCard";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
|
|
@ -208,8 +210,14 @@ function BrowserMockup() {
|
||||||
</div>
|
</div>
|
||||||
<div className="aspect-video bg-background/80">
|
<div className="aspect-video bg-background/80">
|
||||||
<img
|
<img
|
||||||
src="/images/screenshot.png"
|
src={screenshot1440Url}
|
||||||
|
srcSet={`${screenshot768Url} 768w, ${screenshot1440Url} 1440w`}
|
||||||
|
sizes="(min-width: 1100px) 55vw, 100vw"
|
||||||
alt="Zerobyte backups dashboard"
|
alt="Zerobyte backups dashboard"
|
||||||
|
width={1440}
|
||||||
|
height={810}
|
||||||
|
fetchPriority="high"
|
||||||
|
decoding="async"
|
||||||
className="h-full w-full object-cover object-top"
|
className="h-full w-full object-cover object-top"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { RootProvider } from "fumadocs-ui/provider/tanstack";
|
||||||
|
|
||||||
import Header from "../components/Header";
|
import Header from "../components/Header";
|
||||||
|
import { ogImageUrl } from "@/lib/metadata";
|
||||||
|
|
||||||
import appCss from "../styles.css?url";
|
import appCss from "../styles.css?url";
|
||||||
|
|
||||||
const siteUrl = "https://zerobyte.app";
|
|
||||||
const ogImageUrl = `${siteUrl}/images/og.png`;
|
|
||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
head: () => ({
|
head: () => ({
|
||||||
meta: [
|
meta: [
|
||||||
|
|
@ -18,31 +16,10 @@ export const Route = createRootRoute({
|
||||||
name: "viewport",
|
name: "viewport",
|
||||||
content: "width=device-width, initial-scale=1",
|
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",
|
property: "og:type",
|
||||||
content: "website",
|
content: "website",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
property: "og:url",
|
|
||||||
content: siteUrl,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
property: "og:image",
|
property: "og:image",
|
||||||
content: ogImageUrl,
|
content: ogImageUrl,
|
||||||
|
|
@ -63,15 +40,6 @@ export const Route = createRootRoute({
|
||||||
name: "twitter:card",
|
name: "twitter:card",
|
||||||
content: "summary_large_image",
|
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",
|
name: "twitter:image",
|
||||||
content: ogImageUrl,
|
content: ogImageUrl,
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,30 @@ import { useFumadocsLoader } from "fumadocs-core/source/client";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { Card, Cards } from "@/components/DocsCard";
|
import { Card, Cards } from "@/components/DocsCard";
|
||||||
import { DocsMdxLink } from "@/components/DocsMdxLink";
|
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/$")({
|
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,
|
component: Page,
|
||||||
loader: async ({ params }) => {
|
loader: async ({ params }) => {
|
||||||
const slugs = params._splat?.split("/") ?? [];
|
const slugs = params._splat?.split("/") ?? [];
|
||||||
const data = await serverLoader({ data: slugs });
|
const data = await serverLoader({ data: slugs });
|
||||||
|
if (!data) throw notFound();
|
||||||
await clientLoader.preload(data.path);
|
await clientLoader.preload(data.path);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
@ -32,6 +50,9 @@ const serverLoader = createServerFn({
|
||||||
if (!page) throw notFound();
|
if (!page) throw notFound();
|
||||||
return {
|
return {
|
||||||
path: page.path,
|
path: page.path,
|
||||||
|
title: page.data.title ?? "Zerobyte Documentation",
|
||||||
|
description: page.data.description ?? siteDescription,
|
||||||
|
url: page.url,
|
||||||
pageTree: await source.serializePageTree(source.getPageTree()),
|
pageTree: await source.serializePageTree(source.getPageTree()),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -77,7 +98,8 @@ const clientLoader = browserCollections.docs.createClientLoader({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
function Page() {
|
function Page() {
|
||||||
const data = useFumadocsLoader(Route.useLoaderData());
|
const data = useFumadocsLoader(Route.useLoaderData() as DocsLoaderData);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-docs-page className="relative">
|
<div data-docs-page className="relative">
|
||||||
<div aria-hidden className="landing-hero-docs-grid pointer-events-none absolute inset-0" />
|
<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 { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
import LandingPage from "../components/LandingPage";
|
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() {
|
function App() {
|
||||||
return <LandingPage />;
|
return <LandingPage />;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { siteUrl } from "@/lib/metadata";
|
||||||
import { source } from "@/lib/source";
|
import { source } from "@/lib/source";
|
||||||
|
|
||||||
const siteUrl = "https://zerobyte.app";
|
|
||||||
|
|
||||||
function escapeXml(value: string) {
|
function escapeXml(value: string) {
|
||||||
return value
|
return value
|
||||||
.replaceAll("&", "&")
|
.replaceAll("&", "&")
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,7 @@
|
||||||
"compatibility_date": "2025-09-02",
|
"compatibility_date": "2025-09-02",
|
||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"main": "@tanstack/react-start/server-entry",
|
"main": "@tanstack/react-start/server-entry",
|
||||||
|
"assets": {
|
||||||
|
"run_worker_first": ["/sitemap.xml"],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue