Addresses the high-volume issues from the Ahrefs audit: - Worker entrypoint 301s every request not on https://paseo.sh (www subdomain and http variants), killing the duplicate-page errors and the HTTP→HTTPS internal-link notices. - pageMeta() emits <link rel="canonical"> and og:url per route; every head() call now passes its canonical path. - /blog stops rewriting itself to /blog?drafts=false; the search param only appears when explicitly true. - Sitemap includes /blog and each post. - Trim homepage title under SERP truncation, rewrite /agents meta description under 160 chars, pad short descriptions on /privacy, /download, /cloud, /changelog, /docs. - app.paseo.sh gets <meta robots=noindex,nofollow> and a Disallow-all robots.txt so the SPA shell stops polluting the crawl with H1-missing / no-OG / low-word-count warnings.
78 lines
2.2 KiB
TypeScript
78 lines
2.2 KiB
TypeScript
import { useMemo } from "react";
|
||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||
import { getPosts, formatDate } from "~/posts";
|
||
import { pageMeta } from "~/meta";
|
||
|
||
interface BlogSearch {
|
||
drafts?: true;
|
||
}
|
||
|
||
export const Route = createFileRoute("/blog/")({
|
||
validateSearch: (search: Record<string, unknown>): BlogSearch => {
|
||
const drafts = search.drafts === true || search.drafts === "" || search.drafts === "true";
|
||
return drafts ? { drafts: true } : {};
|
||
},
|
||
head: () =>
|
||
pageMeta(
|
||
"Blog – Updates and announcements from the Paseo team",
|
||
"Product updates, technical posts, and announcements from the Paseo team. Notes on building a self-hosted, multi-agent dev environment for your phone.",
|
||
"/blog",
|
||
),
|
||
component: BlogIndex,
|
||
});
|
||
|
||
interface PostRowProps {
|
||
slug: string;
|
||
title: string;
|
||
date: string;
|
||
draft?: boolean;
|
||
}
|
||
|
||
function PostRow({ slug, title, date, draft }: PostRowProps) {
|
||
const params = useMemo(() => ({ _splat: slug }), [slug]);
|
||
return (
|
||
<div className="flex flex-col-reverse items-start md:flex-row md:items-center gap-x-4">
|
||
<span className="text-lg text-muted-foreground tabular-nums">
|
||
{formatDate(new Date(date))}
|
||
</span>
|
||
<Link
|
||
to="/blog/$"
|
||
params={params}
|
||
className="text-lg text-foreground hover:text-primary transition-colors"
|
||
>
|
||
{title}
|
||
{draft && (
|
||
<span className="ml-2 text-xs px-2 py-1 bg-primary/20 text-primary rounded">DRAFT</span>
|
||
)}
|
||
</Link>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function BlogIndex() {
|
||
const { drafts } = Route.useSearch();
|
||
const posts = getPosts(drafts === true);
|
||
|
||
return (
|
||
<div>
|
||
{drafts && (
|
||
<div className="mb-6 p-4 bg-primary/10 rounded border-l-4 border-primary">
|
||
<p className="text-sm text-foreground/80">Showing draft posts</p>
|
||
</div>
|
||
)}
|
||
<div className="space-y-2">
|
||
{posts.map(({ slug, frontmatter }) => (
|
||
<PostRow
|
||
key={slug}
|
||
slug={slug}
|
||
title={frontmatter.title}
|
||
date={frontmatter.date}
|
||
draft={frontmatter.draft}
|
||
/>
|
||
))}
|
||
{posts.length === 0 && <p className="text-muted-foreground">No posts yet.</p>}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|