mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
@@ -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"
|
||||
|
||||
2
packages/app/public/robots.txt
Normal file
2
packages/app/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -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} />;
|
||||
},
|
||||
|
||||
@@ -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 }],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
18
packages/website/src/server-entry.ts
Normal file
18
packages/website/src/server-entry.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user