Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 2 additions & 44 deletions web/app/docs/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import type { Metadata } from 'next';
import type React from 'react';
import { notFound, redirect } from 'next/navigation';
import { evaluate } from '@mdx-js/mdx';
import { Fragment } from 'react';
import { jsx, jsxs } from 'react/jsx-runtime';
import remarkGfm from 'remark-gfm';

import { Card } from '../../../components/docs/Card';
import { CardGroup } from '../../../components/docs/CardGroup';
import { BannerLink } from '../../../components/docs/BannerLink';
import { CodeGroup } from '../../../components/docs/CodeGroup';
import { DocsPageActions } from '../../../components/docs/DocsPageActions';
import { HighlightedPre } from '../../../components/docs/HighlightedCode';
import { LegacySpawnOptionsTable } from '../../../components/docs/LegacySpawnOptionsTable';
import { Note } from '../../../components/docs/Note';
import { Warning } from '../../../components/docs/Warning';
import { mdxComponents } from '../../../components/docs/mdx-components';
import { TableOfContents } from '../../../components/docs/TableOfContents';
import styles from '../../../components/docs/docs.module.css';
import { getDoc } from '../../../lib/docs';
Expand All @@ -24,40 +16,6 @@ import { getDefaultDocsVersionForSlug } from '../../../lib/docs-versions';
import { ogImage } from '../../../lib/og-meta';
import { absoluteUrl } from '../../../lib/site';

function slugify(text: string): string {
return text
.toLowerCase()
.replace(/`([^`]+)`/g, '$1')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
}

function HeadingWithId(level: 2 | 3) {
return function Heading({ children, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
const text = typeof children === 'string' ? children : String(children);
const id = slugify(text);
const Tag = `h${level}` as const;
return (
<Tag id={id} {...props}>
{children}
</Tag>
);
};
}

const components = {
CodeGroup,
Card,
CardGroup,
BannerLink,
Note,
Warning,
SpawnOptionsTable: LegacySpawnOptionsTable,
pre: HighlightedPre,
h2: HeadingWithId(2),
h3: HeadingWithId(3),
};

type PageProps = {
params: Promise<{ slug: string }>;
};
Expand Down Expand Up @@ -152,7 +110,7 @@ export default async function DocsPage({ params }: PageProps) {
<p className={styles.articleDescription}>{doc.frontmatter.description}</p>
)}
<div className={styles.articleBody}>
<MDXContent components={components} />
<MDXContent components={mdxComponents} />
</div>
</article>
<aside className={styles.tocSidebar}>
Expand Down
43 changes: 43 additions & 0 deletions web/app/docs/file/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Metadata } from 'next';

import { ProductDocPage } from '../../../../components/docs/ProductDocPage';
import {
fileSection,
getProductDoc,
getProductDocMarkdownUrl,
getProductDocSlugs,
} from '../../../../lib/product-docs';
import { absoluteUrl } from '../../../../lib/site';

const SECTION_ID = fileSection.id;

type PageProps = {
params: Promise<{ slug: string }>;
};

export function generateStaticParams() {
return getProductDocSlugs(fileSection).map((slug) => ({ slug }));
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const doc = getProductDoc(SECTION_ID, slug);

if (!doc) {
return { title: 'Not Found' };
}

return {
title: `${doc.frontmatter.title} — Relayfile`,
description: doc.frontmatter.description,
alternates: {
canonical: absoluteUrl(`/docs/${SECTION_ID}/${slug}`),
types: { 'text/markdown': getProductDocMarkdownUrl(SECTION_ID, slug) },
},
};
}

export default async function RelayfileDocPage({ params }: PageProps) {
const { slug } = await params;
return <ProductDocPage sectionId={SECTION_ID} slug={slug} />;
}
30 changes: 30 additions & 0 deletions web/app/docs/file/markdown/[slug]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { fileSection, getProductDocMarkdown, getProductDocSlugs } from '../../../../../lib/product-docs';

export const revalidate = 86400;
export const dynamicParams = false;

export function generateStaticParams() {
return getProductDocSlugs(fileSection).map((slug) => ({ slug: `${slug}.md` }));
}

type RouteProps = {
params: Promise<{ slug: string }>;
};

export async function GET(_request: Request, { params }: RouteProps) {
const { slug: rawSlug } = await params;
const slug = rawSlug.replace(/\.md$/, '');

const doc = getProductDocMarkdown(fileSection.id, slug);

if (!doc) {
return new Response('Not found', { status: 404 });
}

return new Response(doc.markdown, {
headers: {
'Content-Type': 'text/markdown; charset=utf-8',
'Cache-Control': 'public, max-age=86400',
},
});
}
5 changes: 5 additions & 0 deletions web/app/docs/file/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation';

export default function RelayfileDocsIndexPage() {
redirect('/docs/file/introduction');
}
9 changes: 8 additions & 1 deletion web/app/docs/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,22 @@ import { SiteFooter } from '../../components/SiteFooter';
import { SiteNav } from '../../components/SiteNav';
import styles from '../../components/docs/docs.module.css';
import { getSearchIndex } from '../../lib/docs';
import { productBasePath, productSections, getProductSearchIndex } from '../../lib/product-docs';

const searchIndex = getSearchIndex();
const productScopes = productSections.map((section) => ({
id: section.id,
label: section.label,
basePath: productBasePath(section),
index: getProductSearchIndex(section),
}));

export default function DocsLayout({ children }: { children: ReactNode }) {
return (
<DocsLanguageProvider>
<div className={styles.docsPage}>
<SiteNav
center={<DocsSearch index={searchIndex} />}
center={<DocsSearch index={searchIndex} productScopes={productScopes} />}
actions={<GitHubStarsBadge />}
mobileMenuContent={<DocsNav variant="mobileMenu" />}
hideMobileDocsLink
Expand Down
43 changes: 43 additions & 0 deletions web/app/docs/loop/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Metadata } from 'next';

import { ProductDocPage } from '../../../../components/docs/ProductDocPage';
import {
getProductDoc,
getProductDocMarkdownUrl,
getProductDocSlugs,
loopSection,
} from '../../../../lib/product-docs';
import { absoluteUrl } from '../../../../lib/site';

const SECTION_ID = loopSection.id;

type PageProps = {
params: Promise<{ slug: string }>;
};

export function generateStaticParams() {
return getProductDocSlugs(loopSection).map((slug) => ({ slug }));
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const doc = getProductDoc(SECTION_ID, slug);

if (!doc) {
return { title: 'Not Found' };
}

return {
title: `${doc.frontmatter.title} — Relayloop`,
description: doc.frontmatter.description,
alternates: {
canonical: absoluteUrl(`/docs/${SECTION_ID}/${slug}`),
types: { 'text/markdown': getProductDocMarkdownUrl(SECTION_ID, slug) },
},
};
}

export default async function RelayloopDocPage({ params }: PageProps) {
const { slug } = await params;
return <ProductDocPage sectionId={SECTION_ID} slug={slug} />;
}
30 changes: 30 additions & 0 deletions web/app/docs/loop/markdown/[slug]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getProductDocMarkdown, getProductDocSlugs, loopSection } from '../../../../../lib/product-docs';

export const revalidate = 86400;
export const dynamicParams = false;

export function generateStaticParams() {
return getProductDocSlugs(loopSection).map((slug) => ({ slug: `${slug}.md` }));
}

type RouteProps = {
params: Promise<{ slug: string }>;
};

export async function GET(_request: Request, { params }: RouteProps) {
const { slug: rawSlug } = await params;
const slug = rawSlug.replace(/\.md$/, '');

const doc = getProductDocMarkdown(loopSection.id, slug);

if (!doc) {
return new Response('Not found', { status: 404 });
}

return new Response(doc.markdown, {
headers: {
'Content-Type': 'text/markdown; charset=utf-8',
'Cache-Control': 'public, max-age=86400',
},
});
}
5 changes: 5 additions & 0 deletions web/app/docs/loop/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation';

export default function RelayloopDocsIndexPage() {
redirect('/docs/loop/introduction');
}
77 changes: 72 additions & 5 deletions web/components/docs/DocsNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,47 @@ import { SiClaude, SiPython, SiTypescript } from 'react-icons/si';

import { docsNav, legacyDocsNav } from '../../lib/docs-nav';
import { getDocsVersionForPath, legacyDocsBasePath, v8DocsBasePath } from '../../lib/docs-versions';
import { getProductSectionForPath, productBasePath } from '../../lib/product-docs-nav';
import { DocsVersionSelect } from './DocsVersionSelect';
import { FolderOpen as FolderOpenIcon, Repeat } from 'lucide-react';
import styles from './docs.module.css';

type NavIcon = ComponentType<{ className?: string; 'aria-hidden'?: boolean | 'true' | 'false' }>;

const productSectionIcons: Record<string, NavIcon> = {
file: FolderOpenIcon,
loop: Repeat,
};

const productNavIcons: Record<string, NavIcon> = {
introduction: Compass,
quickstart: Rocket,
install: Rocket,
'why-files': BookOpen,
'mount-layout': FolderOpen,
'reads-and-writes': Send,
acls: Shield,
'realtime-sync': Activity,
'run-locally': Terminal,
'local-development': Terminal,
mounting: FolderOpen,
sdk: SiTypescript,
agents: Bot,
'adapters-and-providers': Plug,
comparison: BookOpen,
cloud: Cloud,
'api-reference': Network,
cli: Terminal,
sync: Activity,
search: Compass,
sessions: BsChatRightText,
sources: Network,
stats: Zap,
'cloud-architecture': Network,
privacy: PiLockKeyDuotone,
teams: Users,
};

const navIcons: Record<string, NavIcon> = {
introduction: Compass,
quickstart: Rocket,
Expand Down Expand Up @@ -89,9 +125,18 @@ export function DocsNav({ variant = 'sidebar' }: { variant?: 'sidebar' | 'mobile
const pathname = usePathname();
const navRef = useRef<HTMLElement | null>(null);
const isSidebar = variant === 'sidebar';
const productSection = getProductSectionForPath(pathname ?? '/docs');
const docsVersion = getDocsVersionForPath(pathname ?? '/docs');
const navGroups = docsVersion === 'v7.1.1' ? legacyDocsNav : docsNav;
const docsBasePath = docsVersion === 'v8' ? v8DocsBasePath : legacyDocsBasePath;
const navGroups = productSection
? productSection.nav
: docsVersion === 'v7.1.1'
? legacyDocsNav
: docsNav;
const docsBasePath = productSection
? productBasePath(productSection)
: docsVersion === 'v8'
? v8DocsBasePath
: legacyDocsBasePath;

useEffect(() => {
if (!isSidebar) return;
Expand Down Expand Up @@ -140,7 +185,23 @@ export function DocsNav({ variant = 'sidebar' }: { variant?: 'sidebar' | 'mobile
className={`${styles.sidebar} ${!isSidebar ? styles.mobileSidebar : ''}`}
aria-label="Documentation"
>
{!isSidebar && <DocsVersionSelect />}
{!isSidebar && !productSection && <DocsVersionSelect />}
{productSection &&
(() => {
const ProductIcon = productSectionIcons[productSection.id] ?? BookOpen;
return (
<div className={styles.productHeader}>
<Link href="/docs/introduction" className={styles.productBackLink}>
← Agent Relay docs
</Link>
<Link href={docsBasePath} className={styles.productHeaderName}>
<ProductIcon className={styles.productHeaderIcon} aria-hidden="true" />
<span>{productSection.label}</span>
</Link>
<p className={styles.productHeaderTagline}>{productSection.tagline}</p>
</div>
);
})()}
{navGroups.map((group) => (
<div key={group.title} className={styles.navGroup}>
<h4 className={styles.navGroupTitle}>{group.title}</h4>
Expand All @@ -150,8 +211,14 @@ export function DocsNav({ variant = 'sidebar' }: { variant?: 'sidebar' | 'mobile
const isActive =
pathname === href ||
(item.slug === 'introduction' &&
(pathname === '/docs' || pathname === legacyDocsBasePath || pathname === v8DocsBasePath));
const Icon = navIcons[item.slug] ?? BookOpen;
(productSection
? pathname === docsBasePath
: pathname === '/docs' ||
pathname === legacyDocsBasePath ||
pathname === v8DocsBasePath));
const Icon = productSection
? (productNavIcons[item.slug] ?? BookOpen)
: (navIcons[item.slug] ?? BookOpen);
return (
<li key={item.slug}>
<Link href={href} className={`${styles.navLink} ${isActive ? styles.navLinkActive : ''}`}>
Expand Down
Loading
Loading