Files
paseo/packages/website/src/routes/blog/index.tsx
T
Mohamed Boudra cae3deb6e7 fix(website): SEO canonicals, host redirects, sitemap, app noindex
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.
2026-05-21 10:07:41 +07:00

78 lines
2.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}