From cae3deb6e7b9f2b59645f37986008639b78a9734 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 21 May 2026 10:07:41 +0700 Subject: [PATCH] fix(website): SEO canonicals, host redirects, sitemap, app noindex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 and a Disallow-all robots.txt so the SPA shell stops polluting the crawl with H1-missing / no-OG / low-word-count warnings. --- packages/app/public/index.html | 1 + packages/app/public/robots.txt | 2 ++ .../website/src/components/agent-route.tsx | 2 +- packages/website/src/meta.ts | 23 +++++++++++++------ packages/website/src/routes/agents.tsx | 10 ++++---- packages/website/src/routes/blog/$.tsx | 7 +++--- packages/website/src/routes/blog/index.tsx | 22 ++++++++++++------ packages/website/src/routes/changelog.tsx | 8 +++---- packages/website/src/routes/cloud.tsx | 8 +++---- packages/website/src/routes/docs/$.tsx | 7 +++--- packages/website/src/routes/docs/index.tsx | 11 +++++---- packages/website/src/routes/download.tsx | 10 ++++---- packages/website/src/routes/index.tsx | 10 ++++---- packages/website/src/routes/privacy.tsx | 8 +++---- packages/website/src/server-entry.ts | 18 +++++++++++++++ packages/website/vite.config.ts | 12 ++++++++++ packages/website/wrangler.toml | 2 +- 17 files changed, 106 insertions(+), 55 deletions(-) create mode 100644 packages/app/public/robots.txt create mode 100644 packages/website/src/server-entry.ts diff --git a/packages/app/public/index.html b/packages/app/public/index.html index b55e6f172..1d4a4084c 100644 --- a/packages/app/public/index.html +++ b/packages/app/public/index.html @@ -3,6 +3,7 @@ + ({ meta: pageMeta(page.metaTitle, page.metaDescription) }), + head: () => pageMeta(page.metaTitle, page.metaDescription, `/${slug}`), component: function AgentLandingPage() { return ; }, diff --git a/packages/website/src/meta.ts b/packages/website/src/meta.ts index 2289bbe13..a604f256f 100644 --- a/packages/website/src/meta.ts +++ b/packages/website/src/meta.ts @@ -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 }], + }; } diff --git a/packages/website/src/routes/agents.tsx b/packages/website/src/routes/agents.tsx index 05a4d67c8..884752b62 100644 --- a/packages/website/src/routes/agents.tsx +++ b/packages/website/src/routes/agents.tsx @@ -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, }); diff --git a/packages/website/src/routes/blog/$.tsx b/packages/website/src/routes/blog/$.tsx index 6c32fb3d6..814586b4c 100644 --- a/packages/website/src/routes/blog/$.tsx +++ b/packages/website/src/routes/blog/$.tsx @@ -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, }); diff --git a/packages/website/src/routes/blog/index.tsx b/packages/website/src/routes/blog/index.tsx index 870abbc1c..b241b3ce0 100644 --- a/packages/website/src/routes/blog/index.tsx +++ b/packages/website/src/routes/blog/index.tsx @@ -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) => ({ - 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): 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 (
diff --git a/packages/website/src/routes/changelog.tsx b/packages/website/src/routes/changelog.tsx index 302552fed..1b76a95f1 100644 --- a/packages/website/src/routes/changelog.tsx +++ b/packages/website/src/routes/changelog.tsx @@ -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, }); diff --git a/packages/website/src/routes/cloud.tsx b/packages/website/src/routes/cloud.tsx index eb6d514db..7de7036d6 100644 --- a/packages/website/src/routes/cloud.tsx +++ b/packages/website/src/routes/cloud.tsx @@ -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, }); diff --git a/packages/website/src/routes/docs/$.tsx b/packages/website/src/routes/docs/$.tsx index fa8bc0082..8dd16902a 100644 --- a/packages/website/src/routes/docs/$.tsx +++ b/packages/website/src/routes/docs/$.tsx @@ -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, }); diff --git a/packages/website/src/routes/docs/index.tsx b/packages/website/src/routes/docs/index.tsx index 98652e360..b850d07a1 100644 --- a/packages/website/src/routes/docs/index.tsx +++ b/packages/website/src/routes/docs/index.tsx @@ -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, }); diff --git a/packages/website/src/routes/download.tsx b/packages/website/src/routes/download.tsx index 4b32f0e75..8bf2cfaff 100644 --- a/packages/website/src/routes/download.tsx +++ b/packages/website/src/routes/download.tsx @@ -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, }); diff --git a/packages/website/src/routes/index.tsx b/packages/website/src/routes/index.tsx index 263e11d0d..e88ba9807 100644 --- a/packages/website/src/routes/index.tsx +++ b/packages/website/src/routes/index.tsx @@ -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, }); diff --git a/packages/website/src/routes/privacy.tsx b/packages/website/src/routes/privacy.tsx index 2032d52e5..0f793eeaf 100644 --- a/packages/website/src/routes/privacy.tsx +++ b/packages/website/src/routes/privacy.tsx @@ -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, }); diff --git a/packages/website/src/server-entry.ts b/packages/website/src/server-entry.ts new file mode 100644 index 000000000..c6eed927c --- /dev/null +++ b/packages/website/src/server-entry.ts @@ -0,0 +1,18 @@ +import startEntry from "@tanstack/react-start/server-entry"; + +const CANONICAL_HOST = "paseo.sh"; + +type FetchArgs = Parameters; + +export default { + async fetch(...args: FetchArgs): Promise { + 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); + }, +}; diff --git a/packages/website/vite.config.ts b/packages/website/vite.config.ts index 056054eb1..d6c680621 100644 --- a/packages/website/vite.config.ts +++ b/packages/website/vite.config.ts @@ -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, })); diff --git a/packages/website/wrangler.toml b/packages/website/wrangler.toml index 64aa1dfd7..089a22089 100644 --- a/packages/website/wrangler.toml +++ b/packages/website/wrangler.toml @@ -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 },