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.
This commit is contained in:
Mohamed Boudra
2026-05-21 10:07:41 +07:00
parent ea36f0879f
commit cae3deb6e7
17 changed files with 106 additions and 55 deletions

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="robots" content="noindex,nofollow" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -5,7 +5,7 @@ import { pageMeta } from "~/meta";
export function agentRouteOptions(slug: string) {
const page = getAgentPage(slug);
return {
head: () => ({ meta: pageMeta(page.metaTitle, page.metaDescription) }),
head: () => pageMeta(page.metaTitle, page.metaDescription, `/${slug}`),
component: function AgentLandingPage() {
return <LandingPage title={page.title} subtitle={page.subtitle} />;
},

View File

@@ -1,8 +1,17 @@
export function pageMeta(title: string, description: string) {
return [
{ title },
{ name: "description", content: description },
{ property: "og:title", content: title },
{ property: "og:description", content: description },
];
const SITE_ORIGIN = "https://paseo.sh";
export function pageMeta(title: string, description: string, path: string) {
const url = `${SITE_ORIGIN}${path}`;
return {
meta: [
{ title },
{ name: "description", content: description },
{ property: "og:title", content: title },
{ property: "og:description", content: description },
{ property: "og:url", content: url },
{ name: "twitter:title", content: title },
{ name: "twitter:description", content: description },
],
links: [{ rel: "canonical", href: url }],
};
}

View File

@@ -7,12 +7,12 @@ import { pageMeta } from "~/meta";
import "~/styles.css";
export const Route = createFileRoute("/agents")({
head: () => ({
meta: pageMeta(
"Supported agents Every coding agent Paseo runs | Paseo",
"Run Claude Code, Codex, OpenCode, Cursor CLI, Gemini CLI, Hermes Agent, Qwen Code, Kimi Code, DeepSeek TUI, and 28 more coding agents from your phone. Self-hosted, your code stays on your machine.",
head: () =>
pageMeta(
"Supported agents Every coding agent Paseo runs",
"Run Claude Code, Codex, Copilot, OpenCode, Cursor CLI, Gemini CLI, and dozens more coding agents from your phone. Self-hosted, your code stays on your machine.",
"/agents",
),
}),
component: AgentsPage,
});

View File

@@ -6,11 +6,10 @@ import { pageMeta } from "~/meta";
export const Route = createFileRoute("/blog/$")({
head: ({ params }) => {
const slug = params._splat ?? "";
const path = `/blog/${slug}`;
const post = getPost(slug);
if (!post) return { meta: pageMeta("Not Found - Paseo", "Post not found.") };
return {
meta: pageMeta(`${post.frontmatter.title} - Paseo`, post.frontmatter.description),
};
if (!post) return pageMeta("Not Found - Paseo", "Post not found.", path);
return pageMeta(`${post.frontmatter.title} - Paseo`, post.frontmatter.description, path);
},
component: BlogPost,
});

View File

@@ -3,13 +3,21 @@ 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>) => ({
drafts: search.drafts === true || search.drafts === "" || search.drafts === "true",
}),
head: () => ({
meta: pageMeta("Blog - Paseo", "Updates, thoughts, and announcements from the Paseo team."),
}),
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,
});
@@ -43,7 +51,7 @@ function PostRow({ slug, title, date, draft }: PostRowProps) {
function BlogIndex() {
const { drafts } = Route.useSearch();
const posts = getPosts(drafts);
const posts = getPosts(drafts === true);
return (
<div>

View File

@@ -5,12 +5,12 @@ import { SiteShell } from "~/components/site-shell";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/changelog")({
head: () => ({
meta: pageMeta(
head: () =>
pageMeta(
"Changelog - Paseo",
"Product updates, fixes, and improvements shipped in each Paseo release.",
"Product updates, bug fixes, and improvements shipped in each Paseo release. Track new agent providers, mobile features, and daemon changes over time.",
"/changelog",
),
}),
component: Changelog,
});

View File

@@ -6,12 +6,12 @@ import { SiteShell } from "~/components/site-shell";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/cloud")({
head: () => ({
meta: pageMeta(
head: () =>
pageMeta(
"Paseo Cloud - Design Partners",
"Paseo across machines, with a team, or inside a company. Looking for design partners.",
"Run Paseo across machines, with a team, or inside a company. We're onboarding design partners for the hosted, multi-machine, multi-user version of Paseo.",
"/cloud",
),
}),
component: Cloud,
});

View File

@@ -7,11 +7,10 @@ import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/$")({
head: ({ params }) => {
const slug = params._splat ?? "";
const path = `/docs/${slug}`;
const doc = getDoc(slug);
if (!doc) return { meta: pageMeta("Not Found - Paseo Docs", "Doc not found.") };
return {
meta: pageMeta(`${doc.frontmatter.title} - Paseo Docs`, doc.frontmatter.description),
};
if (!doc) return pageMeta("Not Found - Paseo Docs", "Doc not found.", path);
return pageMeta(`${doc.frontmatter.title} - Paseo Docs`, doc.frontmatter.description, path);
},
component: DocsPage,
});

View File

@@ -7,10 +7,13 @@ import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/")({
head: () => {
const doc = getDoc("");
if (!doc) return { meta: pageMeta("Docs - Paseo", "Paseo documentation.") };
return {
meta: pageMeta(`${doc.frontmatter.title} - Paseo Docs`, doc.frontmatter.description),
};
if (!doc)
return pageMeta(
"Docs - Paseo",
"Install Paseo and start running coding agents from your phone, desktop, and terminal.",
"/docs",
);
return pageMeta(`${doc.frontmatter.title} - Paseo Docs`, doc.frontmatter.description, "/docs");
},
component: DocsIndex,
});

View File

@@ -18,12 +18,12 @@ import { useRelease } from "~/routes/__root";
import "~/styles.css";
export const Route = createFileRoute("/download")({
head: () => ({
meta: pageMeta(
"Download - Paseo",
"Download Paseo for macOS, Windows, Linux, iOS, and Android. Your dev environment, in your pocket.",
head: () =>
pageMeta(
"Download Paseo for macOS, Windows, Linux, iOS, and Android",
"Install Paseo on every platform. Native desktop apps for macOS, Windows, and Linux. Mobile apps for iOS and Android. Self-hosted, open source, free to download.",
"/download",
),
}),
component: Download,
});

View File

@@ -3,12 +3,12 @@ import { LandingPage } from "~/components/landing-page";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/")({
head: () => ({
meta: pageMeta(
"Paseo Run Claude Code, Codex, Copilot, OpenCode, and Pi from everywhere",
"A self-hosted daemon for Claude Code, Codex, Copilot, OpenCode, and Pi. Agents run on your machine with your full dev environment. Connect from phone, desktop, or web.",
head: () =>
pageMeta(
"Paseo Run Claude Code, Codex, Copilot, OpenCode from anywhere",
"Self-hosted daemon for Claude Code, Codex, Copilot, OpenCode, and Pi. Agents run on your machine with your full dev environment. Connect from phone, desktop, or web.",
"/",
),
}),
component: Home,
});

View File

@@ -3,12 +3,12 @@ import { SiteShell } from "~/components/site-shell";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/privacy")({
head: () => ({
meta: pageMeta(
head: () =>
pageMeta(
"Privacy Policy - Paseo",
"Privacy policy for Paseo - a self-hosted agent manager with no tracking or analytics.",
"Privacy policy for Paseo, the self-hosted coding agent manager. No tracking, no analytics, no data collection. Your code stays on your machine.",
"/privacy",
),
}),
component: Privacy,
});

View File

@@ -0,0 +1,18 @@
import startEntry from "@tanstack/react-start/server-entry";
const CANONICAL_HOST = "paseo.sh";
type FetchArgs = Parameters<typeof startEntry.fetch>;
export default {
async fetch(...args: FetchArgs): Promise<Response> {
const [request] = args;
const url = new URL(request.url);
if (url.hostname !== CANONICAL_HOST || url.protocol !== "https:") {
url.protocol = "https:";
url.hostname = CANONICAL_HOST;
return Response.redirect(url.toString(), 301);
}
return startEntry.fetch(...args);
},
};

View File

@@ -53,6 +53,17 @@ function discoverAgentRoutes(): string[] {
.map((slug) => `/${slug}`);
}
function discoverBlogRoutes(): string[] {
const postsDir = path.join(__dirname, "posts");
if (!fs.existsSync(postsDir)) return ["/blog"];
const slugs = fs
.readdirSync(postsDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
.map((entry) => entry.name.replace(/\.md$/, ""))
.sort();
return ["/blog", ...slugs.map((slug) => `/blog/${slug}`)];
}
const sitemapPages = [
"/",
"/agents",
@@ -62,6 +73,7 @@ const sitemapPages = [
"/privacy",
...discoverAgentRoutes(),
...discoverDocsRoutes(),
...discoverBlogRoutes(),
].map((routePath) => ({
path: routePath,
}));

View File

@@ -2,7 +2,7 @@ name = "paseo-website"
account_id = "10ed39a1dbf316e30abd0c409bed40d6"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]
main = "@tanstack/react-start/server-entry"
main = "./src/server-entry.ts"
routes = [
{ pattern = "paseo.sh", custom_domain = true },