fix(website): smooth docs navigation and improve lighthouse

This commit is contained in:
Mohamed Boudra
2026-05-07 22:42:39 +07:00
parent 4a9c2450b7
commit f7eac82593
16 changed files with 153 additions and 49 deletions
+5
View File
@@ -0,0 +1,5 @@
/assets/*
Cache-Control: public, max-age=31536000, immutable
/phone-*.webp
Cache-Control: public, max-age=2592000
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

@@ -52,7 +52,7 @@ export function CommandDialog({
return ( return (
<div className="relative" ref={ref}> <div className="relative" ref={ref}>
<button type="button" onClick={handleToggle}> <button type="button" aria-label={title} onClick={handleToggle}>
{trigger} {trigger}
</button> </button>
<AnimatePresence> <AnimatePresence>
@@ -0,0 +1,81 @@
import * as React from "react";
import ReactMarkdown, { type Components } from "react-markdown";
import { getDocsHighlighter, docsRehypePlugins, docsRemarkPlugins } from "~/docs-rehype";
function getCodeText(children: React.ReactNode): string {
return React.Children.toArray(children).join("");
}
function getLanguage(className: string | undefined): string | undefined {
return className?.match(/language-([^\s]+)/)?.[1];
}
function DocsPre({
children,
node: _node,
...props
}: React.ComponentProps<"pre"> & { node?: unknown }) {
const codeElement = React.Children.only(children);
const codeProps = React.isValidElement<React.ComponentProps<"code">>(codeElement)
? codeElement.props
: undefined;
const code = getCodeText(codeProps?.children);
const language = getLanguage(codeProps?.className);
const [highlightedHtml, setHighlightedHtml] = React.useState<string | null>(null);
React.useEffect(() => {
let cancelled = false;
setHighlightedHtml(null);
if (!language) return;
const lang = language;
async function highlight() {
try {
const highlighter = await getDocsHighlighter();
if (cancelled) return;
setHighlightedHtml(
highlighter.codeToHtml(code.replace(/\n$/, ""), {
lang,
theme: "catppuccin-mocha",
}),
);
} catch {
if (!cancelled) setHighlightedHtml(null);
}
}
void highlight();
return () => {
cancelled = true;
};
}, [code, language]);
const highlightedMarkup = React.useMemo(
() => (highlightedHtml ? { __html: highlightedHtml } : undefined),
[highlightedHtml],
);
if (highlightedMarkup) {
return <div dangerouslySetInnerHTML={highlightedMarkup} />;
}
return <pre {...props}>{children}</pre>;
}
const docsMarkdownComponents: Components = {
pre: DocsPre,
};
export function DocsMarkdown({ children }: { children: string }) {
return (
<ReactMarkdown
remarkPlugins={docsRemarkPlugins}
rehypePlugins={docsRehypePlugins}
components={docsMarkdownComponents}
>
{children}
</ReactMarkdown>
);
}
@@ -894,12 +894,15 @@ function GetStarted() {
<ServerInstallButton /> <ServerInstallButton />
</div> </div>
<div className="pt-3"> <div className="pt-3">
<a href="/download" className="text-xs text-white/40 hover:text-white/70 transition-colors"> <a
href="/download"
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
All download options All download options
</a> </a>
</div> </div>
<div className="flex items-center gap-2 pt-6"> <div className="flex items-center gap-2 pt-6">
<span className="text-xs text-white/40">Supports</span> <span className="text-xs text-muted-foreground">Supports</span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<AgentBadge name="Claude Code" icon={CLAUDE_CODE_BADGE_ICON} /> <AgentBadge name="Claude Code" icon={CLAUDE_CODE_BADGE_ICON} />
<AgentBadge name="Codex" icon={CODEX_BADGE_ICON} /> <AgentBadge name="Codex" icon={CODEX_BADGE_ICON} />
@@ -1338,8 +1341,14 @@ function PhoneShowcase() {
{/* Left phone — rotated to face inward */} {/* Left phone — rotated to face inward */}
<motion.div style={leftPhoneStyle} className="w-[160px] md:w-[240px] absolute"> <motion.div style={leftPhoneStyle} className="w-[160px] md:w-[240px] absolute">
<img <img
src="/phone-1.png" src="/phone-1-480.webp"
srcSet="/phone-1-320.webp 320w, /phone-1-480.webp 480w"
sizes="(min-width: 768px) 240px, 160px"
alt="Paseo sessions list" alt="Paseo sessions list"
width={480}
height={1044}
loading="lazy"
decoding="async"
className="w-full rounded-[40px] shadow-2xl border-[3px] border-black outline-[3px] outline-white/20" className="w-full rounded-[40px] shadow-2xl border-[3px] border-black outline-[3px] outline-white/20"
/> />
</motion.div> </motion.div>
@@ -1352,8 +1361,14 @@ function PhoneShowcase() {
className="w-[220px] md:w-[240px] relative z-10" className="w-[220px] md:w-[240px] relative z-10"
> >
<img <img
src="/phone-2.png" src="/phone-2-480.webp"
srcSet="/phone-2-320.webp 320w, /phone-2-480.webp 480w"
sizes="(min-width: 768px) 240px, 220px"
alt="Paseo agent chat" alt="Paseo agent chat"
width={480}
height={1044}
loading="lazy"
decoding="async"
className="w-full rounded-[40px] shadow-2xl border-[3px] border-black outline-[3px] outline-white/20" className="w-full rounded-[40px] shadow-2xl border-[3px] border-black outline-[3px] outline-white/20"
/> />
</motion.div> </motion.div>
@@ -1361,8 +1376,14 @@ function PhoneShowcase() {
{/* Right phone — rotated to face inward */} {/* Right phone — rotated to face inward */}
<motion.div style={rightPhoneStyle} className="w-[160px] md:w-[240px] absolute"> <motion.div style={rightPhoneStyle} className="w-[160px] md:w-[240px] absolute">
<img <img
src="/phone-3.png" src="/phone-3-480.webp"
srcSet="/phone-3-320.webp 320w, /phone-3-480.webp 480w"
sizes="(min-width: 768px) 240px, 160px"
alt="Paseo diff view" alt="Paseo diff view"
width={480}
height={1044}
loading="lazy"
decoding="async"
className="w-full rounded-[40px] shadow-2xl border-[3px] border-black outline-[3px] outline-white/20" className="w-full rounded-[40px] shadow-2xl border-[3px] border-black outline-[3px] outline-white/20"
/> />
</motion.div> </motion.div>
@@ -1425,7 +1446,7 @@ function CLISection() {
<a <a
href="/docs/cli" href="/docs/cli"
className="inline-flex items-center gap-1.5 text-xs text-white/40 hover:text-white/70 transition-colors" className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
> >
Full CLI reference Full CLI reference
<svg <svg
+27 -15
View File
@@ -13,30 +13,39 @@ export function SiteFooter({ width = "default" }: SiteFooterProps) {
<div className="space-y-3"> <div className="space-y-3">
<p className="text-white/60 font-medium">Product</p> <p className="text-white/60 font-medium">Product</p>
<div className="space-y-2"> <div className="space-y-2">
<a href="/blog" className="block text-white/40 hover:text-white/60 transition-colors"> <a
href="/blog"
className="block text-muted-foreground hover:text-foreground transition-colors"
>
Blog Blog
</a> </a>
<a href="/docs" className="block text-white/40 hover:text-white/60 transition-colors"> <a
href="/docs"
className="block text-muted-foreground hover:text-foreground transition-colors"
>
Docs Docs
</a> </a>
<a <a
href="/changelog" href="/changelog"
className="block text-white/40 hover:text-white/60 transition-colors" className="block text-muted-foreground hover:text-foreground transition-colors"
> >
Changelog Changelog
</a> </a>
<a href="/cloud" className="block text-white/40 hover:text-white/60 transition-colors"> <a
href="/cloud"
className="block text-muted-foreground hover:text-foreground transition-colors"
>
Cloud Cloud
</a> </a>
<a <a
href="/docs/cli" href="/docs/cli"
className="block text-white/40 hover:text-white/60 transition-colors" className="block text-muted-foreground hover:text-foreground transition-colors"
> >
CLI CLI
</a> </a>
<a <a
href="/privacy" href="/privacy"
className="block text-white/40 hover:text-white/60 transition-colors" className="block text-muted-foreground hover:text-foreground transition-colors"
> >
Privacy Privacy
</a> </a>
@@ -47,16 +56,19 @@ export function SiteFooter({ width = "default" }: SiteFooterProps) {
<div className="space-y-2"> <div className="space-y-2">
<a <a
href="/claude-code" href="/claude-code"
className="block text-white/40 hover:text-white/60 transition-colors" className="block text-muted-foreground hover:text-foreground transition-colors"
> >
Claude Code Claude Code
</a> </a>
<a href="/codex" className="block text-white/40 hover:text-white/60 transition-colors"> <a
href="/codex"
className="block text-muted-foreground hover:text-foreground transition-colors"
>
Codex Codex
</a> </a>
<a <a
href="/opencode" href="/opencode"
className="block text-white/40 hover:text-white/60 transition-colors" className="block text-muted-foreground hover:text-foreground transition-colors"
> >
OpenCode OpenCode
</a> </a>
@@ -69,7 +81,7 @@ export function SiteFooter({ width = "default" }: SiteFooterProps) {
href="https://discord.gg/jz8T2uahpH" href="https://discord.gg/jz8T2uahpH"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors" className="block text-muted-foreground hover:text-foreground transition-colors"
> >
Discord Discord
</a> </a>
@@ -77,7 +89,7 @@ export function SiteFooter({ width = "default" }: SiteFooterProps) {
href="https://github.com/getpaseo/paseo" href="https://github.com/getpaseo/paseo"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors" className="block text-muted-foreground hover:text-foreground transition-colors"
> >
GitHub GitHub
</a> </a>
@@ -90,7 +102,7 @@ export function SiteFooter({ width = "default" }: SiteFooterProps) {
href={appStoreUrl} href={appStoreUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors" className="block text-muted-foreground hover:text-foreground transition-colors"
> >
App Store App Store
</a> </a>
@@ -98,7 +110,7 @@ export function SiteFooter({ width = "default" }: SiteFooterProps) {
href={playStoreUrl} href={playStoreUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors" className="block text-muted-foreground hover:text-foreground transition-colors"
> >
Google Play Google Play
</a> </a>
@@ -106,7 +118,7 @@ export function SiteFooter({ width = "default" }: SiteFooterProps) {
href="https://github.com/getpaseo/paseo/releases" href="https://github.com/getpaseo/paseo/releases"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors" className="block text-muted-foreground hover:text-foreground transition-colors"
> >
Desktop Desktop
</a> </a>
@@ -114,7 +126,7 @@ export function SiteFooter({ width = "default" }: SiteFooterProps) {
href={webAppUrl} href={webAppUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors" className="block text-muted-foreground hover:text-foreground transition-colors"
> >
Web App Web App
</a> </a>
@@ -48,7 +48,7 @@ export function SiteHeader() {
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center" className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
> >
<svg <svg
role="img" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="18" width="18"
height="18" height="18"
@@ -62,7 +62,7 @@ export function SiteHeader() {
href="https://github.com/getpaseo/paseo" href="https://github.com/getpaseo/paseo"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="GitHub" aria-label={stars ? `GitHub, ${stars} stars` : "GitHub"}
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center gap-1.5" className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center gap-1.5"
> >
<svg <svg
+1 -10
View File
@@ -1,5 +1,4 @@
import rehypeAutolinkHeadings from "rehype-autolink-headings"; import rehypeAutolinkHeadings from "rehype-autolink-headings";
import { rehypePrettyCode } from "rehype-pretty-code";
import rehypeSlug from "rehype-slug"; import rehypeSlug from "rehype-slug";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import { createHighlighterCore, type HighlighterCore } from "shiki/core"; import { createHighlighterCore, type HighlighterCore } from "shiki/core";
@@ -10,7 +9,7 @@ export const docsRemarkPlugins: PluggableList = [remarkGfm];
let docsHighlighter: Promise<HighlighterCore> | undefined; let docsHighlighter: Promise<HighlighterCore> | undefined;
function getDocsHighlighter(): Promise<HighlighterCore> { export function getDocsHighlighter(): Promise<HighlighterCore> {
docsHighlighter ??= Promise.all([ docsHighlighter ??= Promise.all([
import("shiki/themes/catppuccin-mocha.mjs"), import("shiki/themes/catppuccin-mocha.mjs"),
import("shiki/langs/bash.mjs"), import("shiki/langs/bash.mjs"),
@@ -39,14 +38,6 @@ function getDocsHighlighter(): Promise<HighlighterCore> {
} }
export const docsRehypePlugins: PluggableList = [ export const docsRehypePlugins: PluggableList = [
[
rehypePrettyCode,
{
theme: "catppuccin-mocha",
keepBackground: false,
getHighlighter: getDocsHighlighter,
},
],
rehypeSlug, rehypeSlug,
[ [
rehypeAutolinkHeadings, rehypeAutolinkHeadings,
+3 -6
View File
@@ -1,8 +1,7 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { MarkdownAsync } from "react-markdown"; import { DocsMarkdown } from "~/components/docs-markdown";
import { DocsSourceFooter } from "~/components/docs-source-footer"; import { DocsSourceFooter } from "~/components/docs-source-footer";
import { getDoc } from "~/docs"; import { getDoc } from "~/docs";
import { docsRehypePlugins, docsRemarkPlugins } from "~/docs-rehype";
import { pageMeta } from "~/meta"; import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/$")({ export const Route = createFileRoute("/docs/$")({
@@ -23,7 +22,7 @@ function DocsPage() {
return <RenderedDoc slug={slug} />; return <RenderedDoc slug={slug} />;
} }
async function RenderedDoc({ slug }: { slug: string }) { function RenderedDoc({ slug }: { slug: string }) {
const doc = getDoc(slug); const doc = getDoc(slug);
if (!doc) { if (!doc) {
@@ -32,9 +31,7 @@ async function RenderedDoc({ slug }: { slug: string }) {
return ( return (
<> <>
<MarkdownAsync remarkPlugins={docsRemarkPlugins} rehypePlugins={docsRehypePlugins}> <DocsMarkdown>{doc.content}</DocsMarkdown>
{doc.content}
</MarkdownAsync>
<DocsSourceFooter doc={doc} /> <DocsSourceFooter doc={doc} />
</> </>
); );
+3 -6
View File
@@ -1,8 +1,7 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { MarkdownAsync } from "react-markdown"; import { DocsMarkdown } from "~/components/docs-markdown";
import { DocsSourceFooter } from "~/components/docs-source-footer"; import { DocsSourceFooter } from "~/components/docs-source-footer";
import { getDoc } from "~/docs"; import { getDoc } from "~/docs";
import { docsRehypePlugins, docsRemarkPlugins } from "~/docs-rehype";
import { pageMeta } from "~/meta"; import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/")({ export const Route = createFileRoute("/docs/")({
@@ -16,14 +15,12 @@ export const Route = createFileRoute("/docs/")({
component: DocsIndex, component: DocsIndex,
}); });
async function DocsIndex() { function DocsIndex() {
const doc = getDoc(""); const doc = getDoc("");
if (!doc) return <p className="text-muted-foreground">Doc not found.</p>; if (!doc) return <p className="text-muted-foreground">Doc not found.</p>;
return ( return (
<> <>
<MarkdownAsync remarkPlugins={docsRemarkPlugins} rehypePlugins={docsRehypePlugins}> <DocsMarkdown>{doc.content}</DocsMarkdown>
{doc.content}
</MarkdownAsync>
<DocsSourceFooter doc={doc} /> <DocsSourceFooter doc={doc} />
</> </>
); );
+3 -3
View File
@@ -253,7 +253,7 @@
} }
.docs-prose pre { .docs-prose pre {
background-color: var(--color-card); background-color: var(--color-card) !important;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 1rem; padding: 1rem;
@@ -439,10 +439,10 @@
--color-mock-accent: #20744a; --color-mock-accent: #20744a;
--color-mock-green: #22c55e; --color-mock-green: #22c55e;
--color-mock-green-400: #4ade80; --color-mock-green-400: #4ade80;
--color-mock-red: #ef4444; --color-mock-red: #f87171;
--color-mock-amber: #f59e0b; --color-mock-amber: #f59e0b;
--color-mock-blue: #3b82f6; --color-mock-blue: #3b82f6;
--color-mock-zinc500: #71717a; --color-mock-zinc500: #a1a5a4;
--color-mock-zinc600: #52525b; --color-mock-zinc600: #52525b;
--color-mock-diff-add: rgba(46, 160, 67, 0.15); --color-mock-diff-add: rgba(46, 160, 67, 0.15);
--color-mock-diff-remove: rgba(248, 81, 73, 0.1); --color-mock-diff-remove: rgba(248, 81, 73, 0.1);