Skip to content
Closed
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
47 changes: 42 additions & 5 deletions CMS-README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,37 @@ const example = "This code will be included"
// --8<-- [end:section_name]
```

### 4. Relative Link Resolution (`src/util/links.ts`, `src/components/PageLink.astro`)

**What it does:** Converts MkDocs-style relative file links to Astro slug-based URLs at render time.

**Why:** MkDocs uses relative links to files (e.g., `../tools/custom-tools.md`), while Astro uses slugs by default and doesn't validate internal links. Rather than rewriting all links to use slugs, we override the default `<a>` element to resolve relative paths automatically. This provides a better authoring experience—linking to files feels more natural than memorizing slug paths.

**How it works:**

1. `PageLink.astro` replaces the default anchor element via `astro-auto-import`
2. When rendering a link, it checks if the href is relative (not absolute, not anchor-only)
3. For relative links, it resolves the path against the current page's location using `src/util/links.ts`
4. The resolved path is matched against the content collection to find the correct slug
5. If no match is found, a warning is logged during development

**Example resolution:**

From page `user-guide/concepts/agents/state.mdx`:
- `conversation-management.md` → `/user-guide/concepts/agents/conversation-management/`
- `../tools/custom-tools.md` → `/user-guide/concepts/tools/custom-tools/`
- `../tools/index.md` → `/user-guide/concepts/tools/`

**Slug generation:** The content collection uses a custom `generateId` function in `src/content.config.ts` that shares the same normalization logic (`normalizePathToSlug`) as link resolution. This ensures consistency between how pages are identified and how links resolve to them.

## Configuration (`astro.config.mjs`)

The main config ties everything together:

```javascript
import { loadSidebarFromMkdocs } from "./src/sidebar.ts"
import remarkMkdocsSnippets from './src/plugins/remark-mkdocs-snippets.ts'
import AutoImport from 'astro-auto-import'

const sidebar = loadSidebarFromMkdocs(
path.resolve('./mkdocs.yml'),
Expand All @@ -57,11 +81,20 @@ export default defineConfig({
markdown: {
remarkPlugins: [remarkMkdocsSnippets],
},
integrations: [starlight({
sidebar: sidebar,
routeMiddleware: './src/route-middleware.ts',
// ...
})],
integrations: [
starlight({
sidebar: sidebar,
routeMiddleware: './src/route-middleware.ts',
// ...
}),
AutoImport({
imports: [/* ... */],
defaultComponents: {
// Override anchor elements for relative link resolution
a: './src/components/PageLink.astro'
}
})
],
})
```

Expand Down Expand Up @@ -175,6 +208,10 @@ For other components, use [explicit imports](https://starlight.astro.build/compo

A wrapper around Starlight's `Tabs` that auto-generates a `syncKey` from tab labels. Tabs with identical label sets automatically sync together across the page. Auto-imported as `Tabs` (see above).

### `PageLink`

Replaces the default anchor element to enable MkDocs-style relative linking. Resolves relative hrefs against the current page's path and validates against the content collection. Logs warnings in development for broken links. Auto-imported as the default `a` element.

### Starlight Overrides (`src/components/overrides/`)

These override default Starlight components:
Expand Down
1 change: 1 addition & 0 deletions CMS-TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
- [ ] Remove Vite SSR workaround for zod in `astro.config.mjs` once CMS build is separated from TS verification (see https://github.com/withastro/astro/issues/14117)
- [ ] Fix relative links to pages (e.g., `../some-page.md` style links need to be converted to Starlight-compatible paths)
- [ ] Add API documentation generation/integration for Python and TypeScript SDKs
- [ ] Update astro-auto-import once https://github.com/delucis/astro-auto-import/pull/110 is merged
14 changes: 12 additions & 2 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import path from 'node:path'
import remarkMkdocsSnippets from './src/plugins/remark-mkdocs-snippets.ts'

import { loadSidebarFromMkdocs } from "./src/sidebar.ts"
import AutoImport from 'astro-auto-import'
import AutoImport from './src/plugins/astro-auto-import.ts'
import astroExpressiveCode from "astro-expressive-code"
import mdx from '@astrojs/mdx';

// Generate sidebar from mkdocs nav (validates against existing content files)
// Top-level groups will be rendered as tabs by the custom Sidebar component
Expand All @@ -17,6 +19,7 @@ const sidebar = loadSidebarFromMkdocs(
// https://astro.build/config
export default defineConfig({
site: 'https://strandsagents.com',
trailingSlash: 'always',
vite: {
// TODO once we separate out CMS build from TS verification, fix this
// https://github.com/withastro/astro/issues/14117
Expand All @@ -27,7 +30,10 @@ export default defineConfig({
markdown: {
remarkPlugins: [remarkMkdocsSnippets],
},
integrations: [starlight({
integrations: [
astroExpressiveCode(),
mdx(),
starlight({
title: 'Strands Agents SDK',
description: 'A model-driven approach to building AI agents in just a few lines of code.',
sidebar: sidebar,
Expand Down Expand Up @@ -67,6 +73,10 @@ export default defineConfig({
]
},
],
defaultComponents: {
// override a links so that we can use relative urls
a: './src/components/PageLink.astro'
}
})
],
})
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@
"devDependencies": {
"@astrojs/starlight": "^0.37.3",
"@types/js-yaml": "^4.0.9",
"@types/mdast": "^4.0.4",
"@types/node": "^24.10.1",
"astro": "^5.6.1",
"astro-auto-import": "^0.5.1",
"acorn": "^8.14.0",
"js-yaml": "^4.1.1",
"pino": "^9.7.0",
"pino-pretty": "^13.0.0",
Expand Down
10 changes: 9 additions & 1 deletion scripts/update-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { join } from "path";
import { updateQuickstart } from "./update-quickstart.js";
import { getCommunityLabeledFiles } from "../src/sidebar.js";

const DOCS_DIR = "src/content/docs";
const DOCS_DIR = "docs";
const MKDOCS_PATH = "mkdocs.yml";
const INFO_BLOCK_PATTERN = '!!! info "Language Support"';
const INFO_BLOCK_CONTENT = " This provider is only supported in Python.";
const COMMUNITY_BANNER = "{{ community_contribution_banner }}";
const SKIP_FILES: string[] = [];
// Skip index files in examples directory (they're not included in the content collection)
const SKIP_PATTERNS = [/examples\/.*\/index\.md$/];

// Files that need explicit titles because they don't have H1 headings
const EXPLICIT_TITLES: Record<string, string> = {
Expand Down Expand Up @@ -688,6 +690,12 @@ async function main() {
if (SKIP_FILES.some((skip) => file.endsWith(skip))) {
continue;
}

// Skip files matching skip patterns (e.g., index files in examples)
if (SKIP_PATTERNS.some((pattern) => pattern.test(file))) {
console.log(`⊘ Skipped (pattern): ${file}`);
continue;
}

const content = await readFile(file, "utf-8");

Expand Down
35 changes: 35 additions & 0 deletions src/components/PageLink.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
import { getCollection } from 'astro:content'
import { resolveHref } from '../util/links'

interface Props {
href: string
[key: string]: any
}

const { href, ...rest } = Astro.props

// Get all docs for lookup
const allDocs = await getCollection('docs')
const docSlugs = new Set(allDocs.map((doc) => doc.id))

// Resolve the href
const currentPath = Astro.url.pathname
const { resolvedHref, found } = resolveHref(href, currentPath, docSlugs)

// Log warning in development if link wasn't found
if (!found) {
if (!href.includes("/api-reference/")) {
const route = Astro.locals.starlightRoute
console.warn([
`[PageLink] On page "${route?.entry?.filePath}"`,
`could not resolve ${href}"`,
`currentPath: "${currentPath}"`,
].join(', '))
}
}
---

<a href={resolvedHref} {...rest}>
<slot />
</a>
50 changes: 46 additions & 4 deletions src/content.config.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,61 @@
import { defineCollection } from 'astro:content'
import { docsLoader } from '@astrojs/starlight/loaders'
import { z } from 'astro/zod'
import { docsSchema } from '@astrojs/starlight/schema'
// github-slugger is used by Astro internally for default slug generation.
// We use it here to maintain parity with Astro's default behavior while adding a docs/ prefix.
import { slug as githubSlug } from 'github-slugger'
import { glob } from 'astro/loaders'
import { normalizePathToSlug } from './util/links'

export const collections = {
docs: defineCollection({
loader: docsLoader(),
loader: glob({
base: "docs",
// We explicitly declare the folders we want to include, as otherwise it includes index.md files
// in examples which are not intended to be rendered on the site.
// Long-term we'll be moving examples into the sdk-python repository instead, solving this problem.
pattern: [
"README.mdx",
"user-guide/**/*.mdx",
"community/**/*.mdx",
"examples/**/[!index]*.mdx",
],
generateId: generateDocsId,
}),
schema: docsSchema({
// We have certain flags/behavior based on the following properties; see CMS-README.md for more info
extend: z.object({
// Can be a single value or an array of supported values
languages: z.union([z.string(), z.array(z.string())]).optional(),
languages: z.string().optional(),
community: z.boolean().default(false),
experimental: z.boolean().default(false),
}),
}),
}),
}

/**
* Custom generateId function for docs content collection.
* This mimics Astro's default slug generation (see node_modules/astro/dist/content/loaders/glob.js)
* but uses our shared normalizePathToSlug utility for consistency with link resolution.
*/
function generateDocsId({ entry, data }: { entry: string; data: Record<string, unknown> }): string {
// If frontmatter has a slug, use it directly
if (data.slug) {
return `${data.slug}`
}

// Normalize the entry path and slugify each segment using github-slugger (same as Astro default)
const normalized = normalizePathToSlug(entry)

// Handle root README/index -> use 'index' as the slug (Starlight convention for homepage)
if (!normalized) {
return 'index'
}

const slug = normalized
.split('/')
.map((segment) => githubSlug(segment))
.join('/')

return slug
}
Loading
Loading