diff --git a/apps/website/content/docs/agent/concepts/agent-architecture.mdx b/apps/website/content/docs/agent/concepts/agent-architecture.mdx index 4ba9f1ba8..70bb547b7 100644 --- a/apps/website/content/docs/agent/concepts/agent-architecture.mdx +++ b/apps/website/content/docs/agent/concepts/agent-architecture.mdx @@ -609,7 +609,7 @@ export class DebugTimelineComponent { ``` -When you submit from a previous checkpoint, LangGraph creates a new branch from that point. The original timeline is preserved. The `branch()` signal tells you which branch is currently active. See the [Time Travel guide](/docs/guides/time-travel) for the full walkthrough. +When you submit from a previous checkpoint, LangGraph creates a new branch from that point. The original timeline is preserved. The `branch()` signal tells you which branch is currently active. See the [Time Travel guide](/docs/agent/guides/time-travel) for the full walkthrough. ## Choosing an Architecture @@ -682,22 +682,22 @@ Begin with a single agent and tools. Add human-in-the-loop when you need approva ## What's Next - + Learn the graph, node, and edge primitives that agents are built on. - + Stream token-by-token responses with multiple stream modes. - + Build human-in-the-loop approval flows that pause and resume agents. - + Compose multi-agent systems with orchestrators and specialist workers. - + Debug agents by stepping through checkpoint history and branching. - + How Signals power the reactive model behind agent(). diff --git a/apps/website/content/docs/agent/concepts/angular-signals.mdx b/apps/website/content/docs/agent/concepts/angular-signals.mdx index 5d9fc04d5..62b3aa65f 100644 --- a/apps/website/content/docs/agent/concepts/angular-signals.mdx +++ b/apps/website/content/docs/agent/concepts/angular-signals.mdx @@ -527,22 +527,22 @@ Signals use referential equality (`===`) by default. agent() creates new array r ## What's Next - + How LangGraph agent state flows into Angular Signals and how to structure complex state. - + Configure stream modes, handle token-by-token rendering, and manage concurrent streams. - + Understand the Python agent patterns that produce the events Signals consume. - + Full reference for every Signal, method, and option on LangGraphAgent. - + Build human-in-the-loop approval flows that pause and resume the agent. - + Deep dive into change detection optimization for streaming applications. diff --git a/apps/website/content/docs/agent/concepts/langgraph-basics.mdx b/apps/website/content/docs/agent/concepts/langgraph-basics.mdx index 639001c51..896fb426a 100644 --- a/apps/website/content/docs/agent/concepts/langgraph-basics.mdx +++ b/apps/website/content/docs/agent/concepts/langgraph-basics.mdx @@ -363,22 +363,22 @@ Both APIs produce the same output and work identically with agent(). Choose the ## What's Next - + Deep dive into the planning, tool-calling, and execution lifecycle - + Stream token-by-token responses with multiple stream modes - + Build human-in-the-loop approval flows - + Compose multi-agent systems with orchestrators - + Thread-based conversation persistence - + How Signals power agent's reactive model diff --git a/apps/website/content/docs/agent/concepts/state-management.mdx b/apps/website/content/docs/agent/concepts/state-management.mdx index a0414c0a8..49a9fe807 100644 --- a/apps/website/content/docs/agent/concepts/state-management.mdx +++ b/apps/website/content/docs/agent/concepts/state-management.mdx @@ -361,7 +361,7 @@ const branch = agent.branch(); // Signal — active branch const branchTree = agent.experimentalBranchTree(); ``` -For full checkpoint and time-travel patterns, see the [Persistence guide](/docs/guides/persistence) and [Time Travel guide](/docs/guides/time-travel). +For full checkpoint and time-travel patterns, see the [Persistence guide](/docs/agent/guides/persistence) and [Time Travel guide](/docs/agent/guides/time-travel). ## Custom State Fields @@ -520,22 +520,22 @@ readonly reportTitle = computed(() => this.agent.value().report?.title ?? ''); ## What's Next - + How agent() uses Angular Signals for zero-subscription reactive rendering. - + Configure stream modes — values, messages, events — for different use cases. - + Thread-based conversation persistence and checkpoint configuration. - + Fork threads at any checkpoint and replay with different input. - + Human-in-the-loop approval flows and how interrupt state surfaces in Angular. - + Nodes, edges, and the graph execution model behind the state machine. diff --git a/apps/website/content/docs/agent/getting-started/installation.mdx b/apps/website/content/docs/agent/getting-started/installation.mdx index d0cb89929..414893512 100644 --- a/apps/website/content/docs/agent/getting-started/installation.mdx +++ b/apps/website/content/docs/agent/getting-started/installation.mdx @@ -123,16 +123,16 @@ console.log(test.status()); // 'idle' - setup is correct ## Next steps - + Build your first chat component in 5 minutes - + Understand how Signals power agent - + Graphs, nodes, edges, and state for Angular developers - + Complete agent() function reference diff --git a/apps/website/content/docs/agent/getting-started/introduction.mdx b/apps/website/content/docs/agent/getting-started/introduction.mdx index 56d43dd28..50a452230 100644 --- a/apps/website/content/docs/agent/getting-started/introduction.mdx +++ b/apps/website/content/docs/agent/getting-started/introduction.mdx @@ -307,31 +307,31 @@ Your Angular app is a stateless client. All agent state — threads, checkpoints ## What's Next - + Detailed 5-minute walkthrough with a complete chat component - + Token-by-token updates, stream modes, and status tracking - + Thread persistence across sessions and reactive thread switching - + Human-in-the-loop approval and confirmation flows - + Deterministic testing with MockAgentTransport Subscribe to per-agent lifecycle signals via AGENT_LIFECYCLE - + Deep dive into how Signals power agent - + Graphs, nodes, edges, and state for Angular developers - + Complete agent() function reference diff --git a/apps/website/content/docs/agent/getting-started/quickstart.mdx b/apps/website/content/docs/agent/getting-started/quickstart.mdx index de19d6134..c449be131 100644 --- a/apps/website/content/docs/agent/getting-started/quickstart.mdx +++ b/apps/website/content/docs/agent/getting-started/quickstart.mdx @@ -3,7 +3,7 @@ Build a streaming chat component with agent() in 5 minutes. -Angular 20+ project with Node.js 18+. If you need setup help, see the [Installation](/docs/getting-started/installation) guide. +Angular 20+ project with Node.js 18+. If you need setup help, see the [Installation](/docs/agent/getting-started/installation) guide. @@ -129,22 +129,22 @@ Open `http://localhost:4200` and start chatting with your agent. ## Next steps - + Learn about token-by-token updates and stream modes - + Keep conversations alive across page refreshes - + Add human-in-the-loop approval flows - + Deep dive into how Signals power agent - + Graphs, nodes, edges, and state for Angular developers - + Complete agent() function reference diff --git a/apps/website/content/docs/agent/guides/deployment.mdx b/apps/website/content/docs/agent/guides/deployment.mdx index 242da1dc4..6f3cbeabf 100644 --- a/apps/website/content/docs/agent/guides/deployment.mdx +++ b/apps/website/content/docs/agent/guides/deployment.mdx @@ -408,22 +408,22 @@ Confirm LangSmith traces are arriving and set up alerts for error rate spikes an ## What's Next - + Test agent interactions deterministically before deploying to production. - + Store thread IDs so users can resume conversations across sessions. - + Tune streaming options like throttle and stream modes for production performance. - + Understand the agent patterns your deployment will serve. - + Full reference for provideAgent configuration options. - + Deep dive into error recovery patterns beyond basic error boundaries. diff --git a/apps/website/content/docs/agent/guides/interrupts.mdx b/apps/website/content/docs/agent/guides/interrupts.mdx index 6b19b50e8..33ab68507 100644 --- a/apps/website/content/docs/agent/guides/interrupts.mdx +++ b/apps/website/content/docs/agent/guides/interrupts.mdx @@ -546,16 +546,16 @@ Because interrupts are checkpointed, the user can close their browser, come back ## What's Next - + Give your agent short-term and long-term memory with the Store API. - + Configure checkpointers that keep interrupt state across deployments. - + Stream token-by-token responses alongside interrupt events. - + Script interrupt events deterministically with MockAgentTransport. diff --git a/apps/website/content/docs/agent/guides/memory.mdx b/apps/website/content/docs/agent/guides/memory.mdx index ad8ea128c..92c4989a2 100644 --- a/apps/website/content/docs/agent/guides/memory.mdx +++ b/apps/website/content/docs/agent/guides/memory.mdx @@ -397,16 +397,16 @@ Use hierarchical namespaces like `("memories", user_id)` or `("project", project ## What's Next - + Configure checkpointers and thread storage for production deployments. - + Replay and branch agent runs from any past checkpoint. - + Pause for human input before the agent acts on its memory. - + How agent state flows from LangGraph into Angular Signals. diff --git a/apps/website/content/docs/agent/guides/persistence.mdx b/apps/website/content/docs/agent/guides/persistence.mdx index 6386ebeb6..81f751991 100644 --- a/apps/website/content/docs/agent/guides/persistence.mdx +++ b/apps/website/content/docs/agent/guides/persistence.mdx @@ -336,16 +336,16 @@ Setting the `threadId` signal (or calling `switchThread()`) loads the target thr ## What's Next - + Pause agent execution and wait for human approval before continuing. - + Preserve long-term context across sessions with LangGraph's memory store. - + Stream token-by-token responses and tool progress in real time. - + Test thread persistence and switching deterministically with MockAgentTransport. diff --git a/apps/website/content/docs/agent/guides/streaming.mdx b/apps/website/content/docs/agent/guides/streaming.mdx index 71b9a28f9..883159944 100644 --- a/apps/website/content/docs/agent/guides/streaming.mdx +++ b/apps/website/content/docs/agent/guides/streaming.mdx @@ -240,16 +240,16 @@ Each call to `chat.submit()` opens a new SSE connection. Connections are automat ## What's Next - + Resume conversations across page reloads using thread IDs and checkpointers. - + Pause agent execution mid-stream to collect human input before continuing. - + Unit-test components that use agent with the built-in test harness. - + Full option reference for agent(), including all configuration keys. diff --git a/apps/website/content/docs/agent/guides/subgraphs.mdx b/apps/website/content/docs/agent/guides/subgraphs.mdx index 74342a37f..649cc8a6c 100644 --- a/apps/website/content/docs/agent/guides/subgraphs.mdx +++ b/apps/website/content/docs/agent/guides/subgraphs.mdx @@ -284,16 +284,16 @@ Use **subagents** when tasks are independent and can run in parallel, when each ## What's Next - + Understand how agent() surfaces tokens, status, and errors in real time. - + Inspect earlier states and replay alternate execution paths with checkpoint history. - + Write unit and integration tests for orchestrator graphs and subagent interactions. - + Full reference for agent() options, signals, and subagent configuration. diff --git a/apps/website/content/docs/agent/guides/testing.mdx b/apps/website/content/docs/agent/guides/testing.mdx index 0a3cc1c51..7ef013f95 100644 --- a/apps/website/content/docs/agent/guides/testing.mdx +++ b/apps/website/content/docs/agent/guides/testing.mdx @@ -515,16 +515,16 @@ Integration tests hit a real server and (potentially) a real LLM. Reserve them f ## What's Next - + Understand the SSE event model your tests simulate. - + Build human-in-the-loop approval flows tested with scripted interrupt events. - + Thread persistence patterns that pair with thread-switching tests. - + Full reference for MockAgentTransport options and methods. diff --git a/apps/website/content/docs/agent/guides/time-travel.mdx b/apps/website/content/docs/agent/guides/time-travel.mdx index db6947831..2b0d8d73e 100644 --- a/apps/website/content/docs/agent/guides/time-travel.mdx +++ b/apps/website/content/docs/agent/guides/time-travel.mdx @@ -297,16 +297,16 @@ Time travel is most useful during development. Inspect why an agent chose a part ## What's Next - + Configure thread storage so checkpoints survive page reloads and are available across sessions. - + Understand how agent() surfaces incremental updates and how history integrates with live streaming state. - + Compose multi-agent systems with orchestrators and track subagent execution. - + Full reference for agent() options, signals, and the submit() API including checkpoint parameters. diff --git a/apps/website/content/docs/render/a2ui/overview.mdx b/apps/website/content/docs/render/a2ui/overview.mdx index accdfca8b..82076c2f3 100644 --- a/apps/website/content/docs/render/a2ui/overview.mdx +++ b/apps/website/content/docs/render/a2ui/overview.mdx @@ -238,7 +238,7 @@ handlers = { Both paths render structured UI, but they optimize for different jobs. -| | A2UI | json-render | +| Dimension | A2UI | json-render | | --- | --- | --- | | Wire shape | JSONL message stream | Single JSON spec | | State | Surface data model | Spec state | diff --git a/apps/website/e2e/website.spec.ts b/apps/website/e2e/website.spec.ts index 6ccb862f9..bd2ee6604 100644 --- a/apps/website/e2e/website.spec.ts +++ b/apps/website/e2e/website.spec.ts @@ -68,6 +68,59 @@ test('/llms.txt returns plain text', async ({ page }) => { expect(response?.headers()['content-type']).toContain('text/plain'); }); +test('/llms-full.txt includes generated API reference content', async ({ request }) => { + const response = await request.get('/llms-full.txt'); + expect(response.ok()).toBe(true); + expect(response.headers()['content-type']).toContain('text/plain'); + + const body = await response.text(); + expect(body).toContain('## API Reference (TypeDoc)'); + expect(body).toContain('### agent'); + expect(body).toContain('### chat'); + expect(body).not.toContain('API reference not yet generated'); +}); + +test('robots.txt allows crawling and points at the sitemap', async ({ request }) => { + const response = await request.get('/robots.txt'); + expect(response.ok()).toBe(true); + + const body = await response.text(); + expect(body).toContain('User-Agent: *'); + expect(body).toContain('Allow: /'); + expect(body).toContain('Sitemap: https://cacheplane.ai/sitemap.xml'); +}); + +test('sitemap.xml includes configured docs pages', async ({ request }) => { + const response = await request.get('/sitemap.xml'); + expect(response.ok()).toBe(true); + + const body = await response.text(); + expect(body).toContain('https://cacheplane.ai/docs'); + expect(body).toContain('https://cacheplane.ai/docs/agent/getting-started/introduction'); + expect(body).toContain('https://cacheplane.ai/docs/render/a2ui/overview'); +}); + +test('docs pages render canonical and social metadata', async ({ page }) => { + await page.goto('/docs/agent/guides/streaming'); + + await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( + 'href', + 'https://cacheplane.ai/docs/agent/guides/streaming', + ); + await expect(page.locator('meta[property="og:title"]')).toHaveAttribute( + 'content', + 'Streaming - Agent Docs - Angular Agent Framework', + ); + await expect(page.locator('meta[property="og:url"]')).toHaveAttribute( + 'content', + 'https://cacheplane.ai/docs/agent/guides/streaming', + ); + await expect(page.locator('meta[name="twitter:title"]')).toHaveAttribute( + 'content', + 'Streaming - Agent Docs - Angular Agent Framework', + ); +}); + test('marketing pages link to downloadable whitepaper PDFs', async ({ page }) => { const expectedDownloads: Record = { '/': '/whitepaper.pdf', diff --git a/apps/website/src/app/docs/page.tsx b/apps/website/src/app/docs/page.tsx index c297bb609..83d2f87c4 100644 --- a/apps/website/src/app/docs/page.tsx +++ b/apps/website/src/app/docs/page.tsx @@ -6,11 +6,14 @@ import { Eyebrow } from '../../components/ui/Eyebrow'; import { Card } from '../../components/ui/Card'; import { Pill } from '../../components/ui/Pill'; import { docsConfig } from '../../lib/docs-config'; +import { createPageMetadata } from '../../lib/site-metadata'; -export const metadata = { +export const metadata = createPageMetadata({ title: 'Documentation — Angular Agent Framework', description: 'Learn the framework. Library guides, API reference, and production patterns for Angular Agent Framework.', -}; + pathname: '/docs', + type: 'website', +}); interface PopularTopic { title: string; @@ -78,7 +81,22 @@ export default function DocsLandingPage() { {/* Library grid */}
- Libraries +

+ Libraries +

- Popular topics +
fs.existsSync(root)); - if (!docsRoot) { - return '(API reference not yet generated — run npm run generate-api-docs)'; - } +const API_DOCS: Record = { + 'ag-ui': agUiApiDocs, + agent: agentApiDocs, + chat: chatApiDocs, + render: renderApiDocs, +}; - const sections = fs.readdirSync(docsRoot) - .sort() - .map((library) => { - const apiDocsPath = path.join(docsRoot, library, 'api', 'api-docs.json'); - if (!fs.existsSync(apiDocsPath)) return null; - const raw = fs.readFileSync(apiDocsPath, 'utf8'); - return `### ${library}\n\n${raw}`; - }) - .filter((section): section is string => section !== null); +function loadApiDocs(): string { + const sections = Object.entries(API_DOCS) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([library, docs]) => `### ${library}\n\n${JSON.stringify(docs, null, 2)}`); return sections.length > 0 ? sections.join('\n\n') @@ -29,8 +24,12 @@ function loadApiDocs(): string { } function loadAllPrompts(): string { - const dir = path.join(process.cwd(), 'content', 'prompts'); - if (!fs.existsSync(dir)) return '(no prompt recipes found)'; + const roots = [ + path.join(process.cwd(), 'apps', 'website', 'content', 'prompts'), + path.join(process.cwd(), 'content', 'prompts'), + ]; + const dir = roots.find((root) => fs.existsSync(root)); + if (!dir) return '(no prompt recipes found)'; return fs.readdirSync(dir) .filter((f) => f.endsWith('.md')) .map((f) => { diff --git a/apps/website/src/app/robots.ts b/apps/website/src/app/robots.ts new file mode 100644 index 000000000..0cdfcb355 --- /dev/null +++ b/apps/website/src/app/robots.ts @@ -0,0 +1,12 @@ +import type { MetadataRoute } from 'next'; +import { getCanonicalUrl } from '../lib/site-metadata'; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: '*', + allow: '/', + }, + sitemap: getCanonicalUrl('/sitemap.xml'), + }; +} diff --git a/apps/website/src/app/sitemap.ts b/apps/website/src/app/sitemap.ts new file mode 100644 index 000000000..94b3fc7bd --- /dev/null +++ b/apps/website/src/app/sitemap.ts @@ -0,0 +1,13 @@ +import type { MetadataRoute } from 'next'; +import { getCanonicalUrl, getSitemapRoutes } from '../lib/site-metadata'; + +export default function sitemap(): MetadataRoute.Sitemap { + const now = new Date(); + + return getSitemapRoutes().map((route) => ({ + url: getCanonicalUrl(route), + lastModified: now, + changeFrequency: route.startsWith('/docs') ? 'weekly' : 'monthly', + priority: route === '/' ? 1 : route.startsWith('/docs') ? 0.8 : 0.7, + })); +} diff --git a/apps/website/src/components/docs/MdxRenderer.tsx b/apps/website/src/components/docs/MdxRenderer.tsx index 4839db1aa..df306bb2f 100644 --- a/apps/website/src/components/docs/MdxRenderer.tsx +++ b/apps/website/src/components/docs/MdxRenderer.tsx @@ -25,6 +25,11 @@ const mdxComponents = { ArchFlowDiagram, FeatureChips, pre: Pre, + table: ({ children, ...rest }: React.HTMLAttributes) => ( +
+ {children}
+
+ ), h2: ({ id, children, ...rest }: React.HTMLAttributes) => (

{id ? # : null} diff --git a/apps/website/src/lib/docs.spec.ts b/apps/website/src/lib/docs.spec.ts index abb9811cb..6e193c165 100644 --- a/apps/website/src/lib/docs.spec.ts +++ b/apps/website/src/lib/docs.spec.ts @@ -1,6 +1,14 @@ import { describe, expect, it } from 'vitest'; import { getAllDocSlugs, getDocBySlug, getDocMetadata } from './docs'; import { allDocsPages } from './docs-config'; +import { getCanonicalUrl, getSitemapRoutes } from './site-metadata'; + +const internalDocsLinkPattern = /(?:href=["']|\]\()(?\/docs\/[^"')#\s]+)/g; + +function findInternalDocsLinks(content: string): string[] { + return Array.from(content.matchAll(internalDocsLinkPattern), (match) => match.groups?.href) + .filter((href): href is string => Boolean(href)); +} describe('website docs bindings', () => { it('lists all doc slugs from config', () => { @@ -29,11 +37,65 @@ describe('website docs bindings', () => { }); it('resolves page metadata for configured docs', () => { - expect(getDocMetadata('ag-ui', 'reference', 'event-mapping')).toEqual({ + const metadata = getDocMetadata('ag-ui', 'reference', 'event-mapping'); + + expect(metadata).toMatchObject({ title: 'Event Mapping - AG-UI Docs - Angular Agent Framework', - description: - 'Adapter for any AG-UI-compatible backend (CrewAI, Mastra, Microsoft AF, AG2, Pydantic AI, AWS Strands, CopilotKit runtime)', + alternates: { + canonical: '/docs/ag-ui/reference/event-mapping', + }, + openGraph: { + title: 'Event Mapping - AG-UI Docs - Angular Agent Framework', + url: '/docs/ag-ui/reference/event-mapping', + }, + twitter: { + card: 'summary_large_image', + title: 'Event Mapping - AG-UI Docs - Angular Agent Framework', + }, }); + expect(metadata?.description).toContain('AG-UI protocol events'); + expect(metadata?.description).not.toBe( + 'Adapter for any AG-UI-compatible backend (CrewAI, Mastra, Microsoft AF, AG2, Pydantic AI, AWS Strands, CopilotKit runtime)', + ); + }); + + it('derives mostly unique descriptions from page content', () => { + const descriptions = getAllDocSlugs() + .map(({ library, section, slug }) => getDocMetadata(library, section, slug)?.description) + .filter((description): description is string => Boolean(description)); + + const duplicateDescriptions = descriptions.filter((description, index) => descriptions.indexOf(description) !== index); + expect(duplicateDescriptions).toHaveLength(0); + }); + + it('includes every configured doc page in the sitemap routes', () => { + const sitemapRoutes = getSitemapRoutes(); + + for (const { library, section, slug } of getAllDocSlugs()) { + expect(sitemapRoutes).toContain(`/docs/${library}/${section}/${slug}`); + } + }); + + it('resolves canonical URLs against the production origin', () => { + expect(getCanonicalUrl('/docs/agent/guides/streaming')).toBe('https://cacheplane.ai/docs/agent/guides/streaming'); + }); + + it('does not contain stale or broken internal docs links', () => { + const validDocsRoutes = new Set(['/docs', ...getSitemapRoutes().filter((route) => route.startsWith('/docs/'))]); + const brokenLinks: string[] = []; + + for (const { library, section, slug } of getAllDocSlugs()) { + const doc = getDocBySlug(library, section, slug); + if (!doc) continue; + + for (const href of findInternalDocsLinks(doc.content)) { + if (!validDocsRoutes.has(href)) { + brokenLinks.push(`${library}/${section}/${slug} -> ${href}`); + } + } + } + + expect(brokenLinks).toEqual([]); }); it('returns null for non-existent doc', () => { diff --git a/apps/website/src/lib/docs.ts b/apps/website/src/lib/docs.ts index 266b3b23f..22bfebc5c 100644 --- a/apps/website/src/lib/docs.ts +++ b/apps/website/src/lib/docs.ts @@ -1,6 +1,8 @@ import fs from 'fs'; import path from 'path'; +import type { Metadata } from 'next'; import { docsConfig, type DocsPage, getLibraryConfig, getLibraryPages } from './docs-config'; +import { createPageMetadata } from './site-metadata'; const resolveContentDir = (library: string): string => { const workspacePath = path.join(process.cwd(), 'apps', 'website', 'content', 'docs', library); @@ -14,9 +16,46 @@ export interface ResolvedDoc { title: string; } -export interface ResolvedDocMetadata { - title: string; - description: string; +export type ResolvedDocMetadata = Metadata; + +const FRONTMATTER_DESCRIPTION_PATTERN = /^---\s*\n[\s\S]*?\ndescription:\s*['"]?(?[^'"\n]+)['"]?\s*\n[\s\S]*?\n---/; + +function normalizeDescription(description: string): string { + return description + .replace(/`([^`]+)`/g, '$1') + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + .replace(/<[^>]+>/g, '') + .replace(/\s+/g, ' ') + .trim(); +} + +function extractFirstParagraph(content: string): string | null { + const withoutFrontmatter = content.replace(/^---\s*\n[\s\S]*?\n---\s*/, ''); + const withoutImports = withoutFrontmatter.replace(/^import\s.+$/gm, ''); + const paragraphs = withoutImports.split(/\n{2,}/); + + for (const paragraph of paragraphs) { + const normalized = normalizeDescription(paragraph); + if ( + normalized.length < 40 || + normalized.startsWith('#') || + normalized.startsWith('|') || + normalized.startsWith('```') || + normalized.startsWith('<') + ) { + continue; + } + + return normalized.length > 180 ? `${normalized.slice(0, 177).trimEnd()}...` : normalized; + } + + return null; +} + +function getDocDescription(content: string, fallback: string): string { + const frontmatterDescription = content.match(FRONTMATTER_DESCRIPTION_PATTERN)?.groups?.description; + if (frontmatterDescription) return normalizeDescription(frontmatterDescription); + return extractFirstParagraph(content) ?? fallback; } export function getDocBySlug(library: string, section: string, slug: string): ResolvedDoc | null { @@ -47,10 +86,11 @@ export function getDocMetadata( const lib = getLibraryConfig(library); const libraryTitle = lib?.title ?? 'Docs'; - return { - title: `${doc.title} - ${libraryTitle} Docs - Angular Agent Framework`, - description: lib?.description ?? 'Angular Agent Framework documentation', - }; + const title = `${doc.title} - ${libraryTitle} Docs - Angular Agent Framework`; + const description = getDocDescription(doc.content, lib?.description ?? 'Angular Agent Framework documentation'); + const pathname = `/docs/${library}/${section}/${slug}`; + + return createPageMetadata({ title, description, pathname }); } export function getAllDocSlugs(): { library: string; section: string; slug: string }[] { diff --git a/apps/website/src/lib/site-metadata.ts b/apps/website/src/lib/site-metadata.ts new file mode 100644 index 000000000..d237a6e63 --- /dev/null +++ b/apps/website/src/lib/site-metadata.ts @@ -0,0 +1,64 @@ +import type { Metadata } from 'next'; +import { getAllSolutionSlugs } from './solutions-data'; +import { docsConfig } from './docs-config'; + +export const SITE_ORIGIN = 'https://cacheplane.ai'; +export const SITE_NAME = 'Angular Agent Framework'; +export const DEFAULT_SOCIAL_IMAGE = '/opengraph-image'; + +export function getCanonicalPath(pathname: string): string { + if (pathname === '/') return '/'; + return `/${pathname.replace(/^\/+|\/+$/g, '')}`; +} + +export function getCanonicalUrl(pathname: string): string { + return new URL(getCanonicalPath(pathname), SITE_ORIGIN).toString(); +} + +export function createPageMetadata({ + title, + description, + pathname, + type = 'article', +}: { + title: string; + description: string; + pathname: string; + type?: 'article' | 'website'; +}): Metadata { + const canonicalPath = getCanonicalPath(pathname); + + return { + title, + description, + alternates: { + canonical: canonicalPath, + }, + openGraph: { + title, + description, + url: canonicalPath, + siteName: SITE_NAME, + type, + images: [DEFAULT_SOCIAL_IMAGE], + }, + twitter: { + card: 'summary_large_image', + title, + description, + images: [DEFAULT_SOCIAL_IMAGE], + }, + }; +} + +export function getSitemapRoutes(): string[] { + const staticRoutes = ['/', '/angular', '/render', '/chat', '/pricing', '/solutions', '/pilot-to-prod', '/docs']; + const solutionRoutes = getAllSolutionSlugs().map((slug) => `/solutions/${slug}`); + const docsRoutes = docsConfig.flatMap((library) => + library.sections.flatMap((section) => + section.pages.map((page) => `/docs/${library.id}/${page.section}/${page.slug}`), + ), + ); + + return [...staticRoutes, ...solutionRoutes, ...docsRoutes]; +}