Skip to content

Latest commit

 

History

History
907 lines (638 loc) · 40.8 KB

File metadata and controls

907 lines (638 loc) · 40.8 KB

Astro/Starlight CMS Customizations

This document explains the custom modifications made to the Astro/Starlight setup for the Strands Agents documentation site.

Overview

We're using Astro with the Starlight documentation theme. However, we've made several customizations to preserve compatibility with our existing MkDocs-based documentation structure and navigation.

Key Customizations

1. Sidebar Generation (src/sidebar.ts)

What it does: Reads the navigation structure from src/config/navigation.yml and converts it to Starlight's sidebar format. It does not apply any collapse behavior — that is handled entirely by the route middleware.

Why: Starlight can auto-generate sidebars from the file structure, but we have a specific navigation layout defined in navigation.yml that we want to preserve. The config file also contains navbar and GitHub dropdown configuration.

Collapse opt-in: Add collapsed: true to any group in navigation.yml to make it collapsed by default. This value is passed through to the middleware, which is the sole owner of collapse decisions.

Badges: Badges (like "new", "community", "experimental") come from page frontmatter, not the navigation config. This allows page authors to control badges directly.

2. Route Middleware (src/route-middleware.ts)

What it does: Filters the sidebar at buildtime so each page only shows items from its top-level group, and applies collapse behavior via applyCollapse(). For API pages (Python and TypeScript), it dynamically generates sidebars from the docs collection and computes pagination links.

Why: Our sidebar is organized into top-level groups (User Guide, Community, Examples, etc.). Without this middleware, every page would show the entire sidebar. This middleware scopes the sidebar to the current section, providing a cleaner navigation experience.

Python API sidebar: When viewing pages under docs/api/python/, the middleware uses buildPythonApiSidebar() from src/dynamic-sidebar.ts to generate a nested sidebar structure based on module names (e.g., strands.agent.agent becomes Agent > Agent).

TypeScript API sidebar: When viewing pages under docs/api/typescript/, the middleware uses buildTypeScriptApiSidebar() to generate a category-grouped sidebar (Classes, Interfaces, Type Aliases, Functions).

Pagination: For API pages, the middleware also updates starlightRoute.pagination using getPrevNextLinks() from src/dynamic-sidebar.ts. This ensures the previous/next navigation links at the bottom of pages work correctly with the dynamically generated sidebar. Pagination labels use actual page titles (from the docs collection) rather than sidebar nav labels.

Non-matching pages: Pages that don't belong to any nav section (e.g., the landing page) now show an empty sidebar instead of the full sidebar.

Pagination pruning for regular pages: Starlight pre-computes prev/next links from the full sidebar before middleware runs. The middleware now prunes any links that fall outside the current nav section and overrides labels with actual page titles.

3. MkDocs Snippets Plugin (src/plugins/remark-mkdocs-snippets.ts)

What it does: Processes MkDocs-style code snippet references in markdown files.

Why: Our existing documentation uses MkDocs' snippet syntax to include code from external files. This plugin provides compatibility so we don't need to rewrite all our code examples.

Syntax supported:

```typescript
--8<-- "path/to/file.ts:section_name"

**Source file markers:**
```typescript
// --8<-- [start:section_name]
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 strips the site's base path from the current URL before resolving, then re-applies it to the result — this ensures correct behavior when the site is deployed under a sub-path
  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.

The collection base is src/content (not src/content/docs), so all doc slugs include a docs/ prefix (e.g., docs/user-guide/concepts/agents/state). The generateId function strips this prefix from path-based slugs so that URLs remain clean (e.g., /docs/user-guide/...). Files with an explicit slug frontmatter field (such as generated API docs) use that value directly and must include the docs/ prefix themselves.

5. API Reference Links (@api shorthand)

What it does: Provides a shorthand format for linking to API reference pages that's cleaner than relative paths.

Syntax:

<!-- Python API -->
[@api/python/strands.agent.agent](link text)
[@api/python/strands.agent.agent#AgentResult](link text with anchor)

<!-- TypeScript API -->
[@api/typescript/Agent](link text)
[@api/typescript/Agent#constructor](link text with anchor)

How it works:

  1. Links starting with @api/ are detected by isApiShorthand() in src/util/links.ts
  2. resolveApiShorthand() converts them to absolute paths (e.g., /docs/api/python/strands.agent.agent/)
  3. PageLink.astro applies the site's base path for correct URL generation

Why use this format:

  • Cleaner than relative paths with ../api-reference/python/...
  • Doesn't break when the linking page moves to a different directory
  • Matches the actual URL structure of the generated API docs
  • Validated against the content collection at build time

Examples:

<!-- Instead of this (fragile, verbose): -->
[AgentResult](../api-reference/python/agent/agent_result.md#strands.agent.agent_result.AgentResult)

<!-- Use this (clean, stable): -->
[AgentResult](@api/python/strands.agent.agent_result#AgentResult)

Configuration (astro.config.mjs)

The main config ties everything together:

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

const sidebar = loadSidebarFromConfig(
  path.resolve('./src/config/navigation.yml'),
  path.resolve('./src/content')  // base is src/content, not src/content/docs
)

export default defineConfig({
  markdown: {
    remarkPlugins: [remarkMkdocsSnippets],
  },
  integrations: [
    astroExpressiveCode({
      themes: ['github-light', 'github-dark'],
      // Follow Starlight's data-theme attribute instead of prefers-color-scheme
      themeCssSelector: (theme) => `[data-theme='${theme.type}']`,
    }),
    starlight({
      markdown: {
        // Ensures Starlight's rehype plugins run on API docs symlinked from .build/api-docs
        processedDirs: [path.resolve('.build/api-docs')],
      },
      sidebar: sidebar,
      routeMiddleware: './src/route-middleware.ts',
      // ...
    }),
    AutoImport({
      imports: [/* ... */],
      defaultComponents: {
        // Override anchor elements for relative link resolution
        a: './src/components/PageLink.astro'
      }
    })
  ],
})

Notable config details:

  • themeCssSelector on Expressive Code makes code block themes follow Starlight's [data-theme] attribute rather than the browser's prefers-color-scheme, keeping syntax highlighting in sync with the site's theme toggle.
  • processedDirs tells Starlight to run its rehype plugins (e.g. heading anchor links) on the real resolved paths of the API docs symlinks.

Custom Frontmatter Fields

The documentation extends Starlight's default schema with custom fields that automatically render contextual banners at the top of pages.

languages

Indicates a feature is only available in specific SDK languages.

---
title: My Feature
languages: Python
---

Renders a note aside: "This provider is only supported in {languages}."

community

Marks a page as community-contributed content.

---
title: Community Tool
community: true
---

Renders a tip aside explaining the package is community-maintained, not officially supported.

experimental

Marks a feature as experimental.

---
title: Experimental Feature
experimental: true
---

Renders a tip aside warning the feature may change in future versions.

Rendering Order

When multiple fields are set, banners render top to bottom: experimental → community → languages.

Sidebar Badges

Pages can display badges in sidebar navigation:

---
title: My Page
sidebar:
  label: "AWS Lambda"
  badge:
    text: New
    variant: note
---

Available variants: note, tip, caution, danger, success, default

Badge sources: Badges like "Experimental" or "Community" are determined from page frontmatter. Add a sidebar.badge field to a page's frontmatter to display a badge in the sidebar.

MDX Components

Documentation pages use MDX format and can import Starlight components:

import { Tabs, TabItem } from '@astrojs/starlight/components';

<Tabs>
  <TabItem label="Python">Python code here</TabItem>
  <TabItem label="TypeScript">TypeScript code here</TabItem>
</Tabs>

Available: Tabs/TabItem, Aside, Card/CardGrid, LinkCard, Icon, Badge

Auto-Imported Components

We use astro-auto-import to make Tabs and Tab available globally without explicit imports. Since language tabs appear on nearly every page, this reduces boilerplate.

<!-- No import needed  just use directly -->
<Tabs>
  <Tab label="Python">pip install strands</Tab>
  <Tab label="TypeScript">npm install @strands-agents/sdk</Tab>
</Tabs>

Tabs maps to our AutoSyncTabs component (auto-syncs tabs with matching labels), and Tab maps to Starlight's TabItem.

For other components, use explicit imports.

Custom Components (src/components/)

AutoSyncTabs

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:

  • Head.astro: Adds Mermaid diagram support and loads SiteScripts (Shortbread + WebSDK).
  • Header.astro: Custom header with navigation tabs and theme-aware logos (see Header Navigation below).
  • Hero.astro: Suppresses the Starlight hero on /blog/ paths. Blog pages pass a dummy hero: { actions: [] } to collapse Starlight's two-panel layout, and this override ensures that dummy hero has no visual output.
  • MarkdownContent.astro: Injects the custom frontmatter banners (experimental, community, languages) at the top of page content.
  • PageFrame.astro: Extends Starlight's default PageFrame to add a full-width site footer containing the Copyright component. The footer spans the content area (respecting sidebar offset) with --sl-color-bg-nav background to match the header.
  • Sidebar.astro and SidebarSublist.astro: Custom sidebar navigation that mimics MkDocs Material theme's navigation.sections behavior.

Sidebar Navigation Style

The custom sidebar components provide a flatter navigation style.

How it works:

  1. Top-level groups render as non-collapsible section headers (uppercase labels), unless collapsed: true is set in navigation.yml, in which case they render as a collapsible with a caret icon
  2. Nested groups are collapsible with a caret icon
  3. Group labels link to their first child page (clickable navigation)
  4. Groups auto-expand when they contain the current page
  5. Indentation only starts at depth 2+ (first level under section headers has no indent)

Why: Starlight's default sidebar shows all groups as collapsible accordions. This override provides a cleaner hierarchy where top-level sections are always visible, and nested groups can be both navigated to and expanded.

Header Navigation

The custom header (src/components/overrides/Header.astro) replicates the navigation tabs from the MkDocs Material theme used on strandsagents.com.

Features:

  • Navigation tabs displayed below the main header row on desktop
  • Mobile dropdown menu next to the search bar for small screens
  • GitHub repository dropdown (src/components/GitHubDropdown.astro) replacing the default social icons
  • Theme-aware logos (logo-header-light.svg / logo-header-dark.svg)
  • Active state detection using longest-match path logic

Configuring Navigation Links:

Edit src/config/navbar.ts to add, remove, or reorder navigation links:

const rawNavLinks: NavLink[] = [
  { label: 'Home', href: '/' },
  {
    label: 'User Guide',
    href: '/user-guide/quickstart/overview/',
    basePath: '/user-guide/',  // Used for active state detection
  },
  {
    label: 'Contribute ❤️',
    href: 'https://github.com/strands-agents/sdk-python/blob/main/CONTRIBUTING.md',
    external: true,  // Opens in new tab with arrow icon
  },
]

GitHub dropdown: src/config/navbar.ts also exports githubSections — an array of grouped repository links shown in the GitHubDropdown component (desktop) and the mobile nav menu. Edit this to add or remove repos/orgs.

Active state logic: The header uses findCurrentNavSection() from src/route-middleware.ts to determine which tab is active. It finds the nav link with the longest matching basePath (or href if no basePath) that the current URL starts with.

Theme-aware logos: The header renders both logo-header-light.svg and logo-header-dark.svg, using CSS to show the appropriate one based on the [data-theme] attribute. Logo files are in src/assets/.

Internal Aside Components

Used by MarkdownContent.astro to render frontmatter banners:

  • ExperimentalAside.astro
  • CommunityContributionAside.astro
  • LanguageSupportAside.astro

These are not meant to be imported directly in MDX files—use the frontmatter fields instead.

Python API Reference Generation

The Python API reference documentation is auto-generated from the SDK source code using pydoc-markdown.

Generation Script (scripts/api-generation-python.py)

What it does: Parses Python source code from the SDK and generates MDX documentation files.

How to run:

uv run scripts/api-generation-python.py

Input: .build/sdk-python/src (cloned SDK repository) Output: .build/api-docs/python/*.mdx

Filtering:

  • Skips private modules (any module path containing _ prefix)
  • Skips explicitly excluded modules (e.g., strands.agent which just re-exports)

Output format: Each module becomes a flat MDX file named strands.module.name.mdx with frontmatter containing the title, slug, and editUrl: false (suppresses the "Edit this page" link on generated pages).

Symlink Setup

The generated docs are accessed via a committed symlink:

src/content/docs/api/python/_generated -> ../../../../../.build/api-docs/python

This symlink is checked into git, so no manual setup is required. The generation script outputs to .build/api-docs/python/, and the symlink makes those files available to the content collection.

The index page (src/content/docs/api/python/index.mdx) is a permanent file (not generated) that imports the PythonApiList component.

Dynamic Sidebar (src/dynamic-sidebar.ts)

What it does: Builds a hierarchical sidebar structure from Python API docs at runtime, and provides pagination utilities.

How it works:

  1. Filters docs collection for docs/api/python/* pages
  2. Parses module names from page titles (e.g., strands.agent.agent)
  3. Builds a nested tree structure based on module path segments
  4. Converts tree to Starlight sidebar entries with groups and links

Pagination utilities:

  • flattenSidebar() - Converts nested sidebar structure to a flat list of links
  • getPrevNextLinks(sidebar, titlesByHref?) - Finds the current page in the flattened sidebar and returns prev/next links. The optional titlesByHref map (href → title) overrides sidebar nav labels with actual page titles.

Sorting:

  • Alphabetical A-Z within each level
  • "Experimental" group always appears last
  • Groups at depth ≥2 are collapsed by default

Example transformation:

strands.agent.agent      → Agent > Agent
strands.agent.base       → Agent > Base
strands.experimental.bidi.types.events → Experimental > Bidi > Types > Events

Index Page Component (src/components/PythonApiList.astro)

What it does: Renders the API reference index page with a hierarchical list of all modules.

How it works: Uses the same buildPythonApiSidebar() function as the route middleware to ensure consistency between the sidebar navigation and the index page listing.

Path Alias

Components can be imported using the @components alias:

import PythonApiList from '@components/PythonApiList.astro'

This is configured in tsconfig.json under compilerOptions.paths.

TypeScript API Reference Generation

The TypeScript API reference documentation is auto-generated from the SDK source code using typedoc with typedoc-plugin-markdown.

Generation Script (scripts/api-generation-typescript.ts)

What it does: Runs typedoc to generate markdown files, then post-processes them to add frontmatter.

How to run:

npm run sdk:generate:ts
# or
npx tsx scripts/api-generation-typescript.ts

Input: .build/sdk-typescript/src (cloned SDK repository) Output: .build/api-docs/typescript/{classes,interfaces,type-aliases,functions,namespaces}/*.md

TypeDoc Configuration (typedoc.json)

Key settings:

  • outputFileStrategy: "members" - Creates separate files per class/interface/type/function
  • fileExtension: ".md" - Outputs standard markdown format
  • basePath: ".build/sdk-typescript" - Strips build path prefix from source links
  • hideBreadcrumbs: true, hidePageHeader: true - Cleaner output for Starlight integration
  • excludeExternals: true - Excludes re-exported symbols from external packages

Post-Processing

The generation script performs these transformations after typedoc runs:

  1. Adds frontmatter with title, slug, category, and editUrl: false (suppresses the "Edit this page" link since these files are generated):

    ---
    title: "Agent"
    slug: docs/api/typescript/Agent
    category: classes
    editUrl: false
    ---

    For namespace members, the slug includes the namespace as a prefix separated by a colon:

    ---
    title: "setupTracer"
    slug: docs/api/typescript/telemetry:setupTracer
    category: functions
    editUrl: false
    ---
  2. Fixes relative links to match the flat slug structure (e.g., ../interfaces/AgentData.md../AgentData.md) and updates .md extensions to .mdx. For namespace members and namespace index pages, cross-member links are rewritten to absolute slug paths (e.g., [TracerConfig](../interfaces/TracerConfig.md)[TracerConfig](/api/typescript/telemetry:TracerConfig)) to ensure correct resolution regardless of the page's own URL.

  3. Converts to MDX — runs content through a unified/remark-gfm pipeline with mdxToMarkdown() serialization, which escapes characters that are valid in markdown but invalid in MDX (e.g. {, } outside code blocks). Content inside code fences is left untouched. Files are written as .mdx instead of .md. A targeted replacement also handles the literal string <name>Data that typedoc emits in prose to describe the naming pattern for data interfaces.

  4. Deletes the generated index.md - We use our own custom index page instead

Flat Slugs with Category Grouping

Unlike Python API docs which use hierarchical slugs based on module paths, TypeScript API docs use flat slugs:

  • URL: /docs/api/typescript/Agent/ (not /docs/api/typescript/classes/Agent/)
  • The category frontmatter field is used for sidebar grouping

This keeps URLs clean while still organizing the sidebar by type (Classes, Interfaces, Type Aliases, Functions).

Namespace Exports

When the SDK exports a namespace (e.g., export * as telemetry from './telemetry/index.js'), typedoc generates a nested directory structure under namespaces/<ns>/. The generation script handles this specially:

  • The namespace index page (namespaces/<ns>/index.md) is kept and written as namespaces/<ns>.mdx with slug api/typescript/<ns> and category namespaces.
  • Members of the namespace (classes, interfaces, functions, etc.) are flattened into the same top-level category directories as regular exports, but their slugs are prefixed with the namespace name using a colon separator: docs/api/typescript/<ns>:<MemberName>.
  • All cross-member links within a namespace are rewritten to absolute slug paths to avoid broken relative links after flattening.

Symlink Setup

The generated docs are accessed via a committed symlink:

src/content/docs/api/typescript/_generated -> ../../../../../.build/api-docs/typescript

The index page (src/content/docs/api/typescript/index.mdx) is a permanent file that imports the TypeScriptApiList component.

Dynamic Sidebar (src/dynamic-sidebar.ts)

What it does: Builds a category-grouped sidebar structure from TypeScript API docs at runtime.

How it works:

  1. Filters docs collection for docs/api/typescript/* pages
  2. Groups docs by their category frontmatter field
  3. Creates sidebar groups for Classes, Interfaces, Type Aliases, and Functions
  4. Sorts entries alphabetically within each group

Example structure:

Namespaces
  └── telemetry
Classes
  ├── Agent
  ├── BedrockModel
  └── Tool
Interfaces
  ├── AgentConfig
  ├── TracerConfig       ← namespace member, slug: docs/api/typescript/telemetry:TracerConfig
  └── ToolSpec
Type Aliases
  ├── ContentBlock
  └── ToolChoice
Functions
  ├── configureLogging
  ├── setupTracer        ← namespace member, slug: docs/api/typescript/telemetry:setupTracer
  └── tool

Index Page Component (src/components/TypeScriptApiList.astro)

What it does: Renders the API reference index page with a categorized list of all exports.

How it works: Uses the same buildTypeScriptApiSidebar() function as the route middleware to ensure consistency between the sidebar navigation and the index page listing.

Content Collection Schema

The category field is defined in src/content.config.ts:

extend: z.object({
  // ...
  category: z.string().optional(),
})

This allows the content collection to validate and expose the category for sidebar generation.

Custom Landing Page

The landing page uses a custom layout that provides the Starlight header without the full documentation page structure, allowing for full-width marketing content.

Landing Layout (src/layouts/LandingLayout.astro)

What it does: Provides a minimal layout with the Starlight header, theme support, and CSS variables, but without the sidebar, table of contents, or content constraints of documentation pages.

Key features:

  • Mocks Astro.locals.starlightRoute with minimal data needed for the Header component
  • Mocks Astro.locals.t translation function (with .all() method for Search component)
  • Includes SiteScripts for Shortbread consent and WebSDK

Usage:

---
import LandingLayout from '../layouts/LandingLayout.astro'
---

<LandingLayout title="Page Title" description="Optional description">
  <!-- Full-width content here -->
</LandingLayout>

Landing Page (src/pages/index.astro)

The main landing page includes:

  • Animated parallax curves background (replicating strandsagents.com effect)
  • Hero section with frosted glass effect
  • Feature cards that expand on hover to show descriptions
  • Testimonials slider with fade transitions and auto-play
  • Footer with Copyright component (left-aligned, --sl-color-bg-nav background)

Assets:

  • src/assets/curve-primary.svg and src/assets/curve-secondary.svg - Animated strand patterns
  • src/assets/icons/icon-*.svg - Feature card icons

Testimonials Content Collection

Testimonials are managed as a content collection of Markdown files, with company logos stored alongside them.

Schema (src/content.config.ts)

testimonials: defineCollection({
  loader: glob({ base: 'src/content', pattern: 'testimonials/**/*.md' }),
  schema: ({ image }) => z.object({
    name: z.string(),
    title: z.string().optional(),
    logo: image().optional(),       // Light-mode company logo
    dark_logo: image().optional(),  // Dark-mode variant (falls back to logo)
    order: z.number().default(0),
  }),
})

Using Astro's image() helper ensures logos are processed through the asset pipeline (hashed, optimized) at build time.

Content Location

src/content/testimonials/ — each company has a .md file and its logo(s) stored alongside it:

src/content/testimonials/
├── smartsheet.md
├── smartsheet-logo.svg
├── smartsheet-logo-white.svg   ← dark-mode variant
├── landchecker.md
├── landchecker-logo.svg
└── ...

File Format

Each testimonial is a Markdown file with frontmatter metadata and the quote as the body:

---
name: JB Brown
title: VP Engineering, Smartsheet
logo: ./smartsheet-logo.svg
dark_logo: ./smartsheet-logo-white.svg
order: 1
---

At Smartsheet, we chose Strands...

The order field controls display sequence in the slider. Logo paths are relative to the file.

Dark/Light Logo Switching

The landing page renders both logo and dark_logo (falling back to logo when no dark variant exists) and uses CSS to show the appropriate one based on Starlight's [data-theme] attribute:

.logo-dark { display: none; }
[data-theme='dark'] .logo-light { display: none; }
[data-theme='dark'] .logo-dark { display: block; }

Temporary Migration Files

The following files were created to support the MkDocs → Astro migration and should be deleted once migration is complete:

Link Conversion Utilities

These files handle converting old MkDocs-style API reference links to the new @api shorthand format:

  • src/util/api-link-converter.ts - Utility functions to detect and convert old API links
  • test/api-link-converter.test.ts - Tests for the link converter

Migration Scripts

These scripts assist with documentation maintenance:

  • scripts/update-quickstart.ts - Quickstart-specific transformations
  • scripts/update-language-index.ts - Updates language index pages
  • test/update-docs.test.ts - Tests for API link conversion utilities

URL Redirects (Old MkDocs URLs → New CMS URLs)

The old MkDocs site used versioned URLs like /latest/documentation/docs/<path>/ and /1.x/documentation/docs/<path>/. The new CMS uses clean paths like /docs/<path>/. Some pages also moved or were renamed. The redirect system handles both cases client-side via the 404 page.

How It Works

  1. 404 page (src/content/404.mdx) renders Redirect404.astro, which runs a client-side script on every 404.
  2. Redirect404.astro (src/components/Redirect404.astro) builds a redirectFromMap at build time (via src/util/redirect.build.ts) and passes it to the client-side script, which calls resolveRedirectFromUrl() with the current URL and map. If a target is found, window.location.replace() fires without adding a history entry.
  3. src/util/redirect.ts contains the redirect logic:
    • resolveRedirectFromUrl(url, redirectFromMap?) — strips the version prefix (/latest/, /1.x/, /1.5.x/, etc.) and the /documentation/ segment, then delegates to resolveRedirect().
    • resolveRedirect(slug, redirectFromMap?) — checks SLUG_RULES first (highest priority), then falls back to redirectFromMap for frontmatter-based redirects.
  4. src/util/redirect.build.ts — shared helper that calls getCollection('docs') and builds the redirectFromMap from all redirectFrom frontmatter arrays. Used by both Redirect404.astro and the sitemap coverage test.

Page-Level Redirects (redirectFrom frontmatter)

Individual pages can declare old slugs that should redirect to them:

---
title: My Page
redirectFrom:
  - docs/old/path/to/page
---

This is useful when a page moves to a new URL. The redirectFrom slugs are collected at build time into a map and passed to the client-side redirect script. They must also be registered in test/known-routes.json — the sitemap coverage test enforces this.

Adding New Redirect Rules

Edit SLUG_RULES in src/util/redirect.ts for structural renames affecting many pages. For single-page moves, prefer redirectFrom frontmatter instead. Each SLUG_RULES entry has a match regex and a to string or function:

// Static rename
{ match: exactly('docs/old/path'), to: 'docs/new/path' },

// Pattern-based rename (capture group 1 = everything after the prefix)
{ match: startsWith('docs/old-prefix'), to: (m) => `docs/new-prefix/${m[1]}` },

Helper builders from src/utils/regex.ts:

  • startsWith(prefix) — matches slugs starting with prefix/, captures the rest in m[1]
  • exactly(s) — matches the slug exactly

Testing

  • test/redirect.test.ts — unit tests for resolveRedirect and resolveRedirectFromUrl covering slug transforms, URL normalisation, trailing-slash preservation, and redirectFromMap priority rules.
  • test/sitemap-coverage.test.ts — integration tests including:
    • Every redirectFrom slug declared in frontmatter has a corresponding entry in test/known-routes.json (fails with copy-paste-ready JSON if missing).
    • Every known route resolves to a valid CMS entry (uses buildRedirectFromMap() so frontmatter-based redirects are honoured).
    • Live sitemap coverage (controlled by VERIFY_LIVE_SITEMAP=true, skipped locally).

Run with:

npm test

LLM-Friendly Documentation (llms.txt)

We provide machine-readable documentation following the llms.txt specification, optimized for both humans and AI agents.

Why Custom Implementation

We evaluated existing Astro llms.txt plugins/integrations but found them lacking:

  • They generated HTML or poorly formatted markdown with navigation clutter
  • Links weren't properly resolved to our documentation structure
  • No support for our custom components (tabs, code snippets, etc.)

Our implementation renders documentation through Astro's container API, applies custom HTML-to-markdown transformations, and generates clean output with correct links.

Endpoints

  • /llms.txt - Index with links to all docs organized by sidebar structure
  • /llms-full.txt - Complete documentation content (excludes API reference)
  • /{slug}/index.md - Any doc page in raw markdown format

Implementation Files

File Purpose
src/pages/llms.txt.ts Generates index from sidebar structure
src/pages/llms-full.txt.ts Renders all docs inline
src/pages/[...slug]/index.md.ts Dynamic endpoint for individual pages
src/util/render-to-markdown.ts Renders MDX entries via AstroContainer
src/util/html-to-markdown.ts HTML→Markdown conversion with custom rules

HTML-to-Markdown Transformations

Uses Turndown with custom rules:

  • Tables: GFM plugin for proper markdown table syntax
  • Code blocks: Handles both standard and Expressive Code syntax highlighting
  • Tab panels: Wraps content with (( tab "Label" )) markers
  • Local links: Rewrites to /index.md format for LLM consumption
  • Cleanup: Removes screen-reader elements, empty anchors, tab navigation lists, scripts

Link Handling

The src/util/links.ts module was extended:

  • toRawMarkdownUrl() - Converts paths to index.md URLs, skips files with extensions
  • isLocalLink() - Identifies links that should be converted (excludes .txt, external, anchors)
  • resolveHref() - Special-cases llms.txt and llms-full.txt for proper resolution
  • getSiteOrigin() - Returns the value of the SITE_DOMAIN environment variable (trailing slash stripped), or an empty string if unset. Used by llms.txt and llms-full.txt to produce absolute URLs when a domain is known.

Absolute URLs via SITE_DOMAIN

By default, links in llms.txt and llms-full.txt are relative (path-only). Set the SITE_DOMAIN environment variable at build time to prefix all links with the full domain:

SITE_DOMAIN=https://strandsagents.com npm run build

Without SITE_DOMAIN, links remain relative (e.g. /user-guide/quickstart/). With it set, they become absolute (e.g. https://strandsagents.com/user-guide/quickstart/).

Blog

The blog is a standalone section at /blog/ with its own content collection, layouts, components, and routes — outside of Starlight's docs collection. It follows the same pattern as the custom landing page: reuses the Starlight header via BlogLayout.astro while opting out of the docs chrome (sidebar, table of contents, etc.).

Content Collections

Authors (src/content/authors.yaml):

- id: strands-team
  name: Strands Agents Team
  role: Core Team
  bio: The team behind the Strands Agents SDK.

Schema: { id, name, role, bio, avatar? } — all strings. The id field is used as the reference key from blog post frontmatter. Stored as a single YAML file (array of author objects) rather than individual JSON files per author.

Blog Posts (src/content/blog/*.mdx):

---
title: "Post Title"
date: 2026-02-20T00:00:00.000Z
description: "Short description for cards and meta tags."
authors: ["strands-team"]     # References author file IDs
tags: ["Open Source"]
draft: false                  # Excluded from production builds
coverImage: "/path/to/image"  # Optional
---

The readingTime field is injected automatically by the remark plugin (see below).

Both collections are registered in src/content.config.ts using glob loaders, following the same pattern as testimonials.

Reading Time Remark Plugin (src/plugins/remark-reading-time.ts)

Extracts text from the markdown AST and injects a readingTime string (e.g., "3 min read") into file.data.astro.frontmatter. Registered in astro.config.mjs under markdown.remarkPlugins.

Dependencies: reading-time, mdast-util-to-string.

Blog Utilities (src/util/blog.ts)

Helper functions used across all blog pages:

Function Purpose
getPublishedPosts() All posts sorted by date desc, excludes drafts in prod
getAllTags() Unique tags across all published posts
getPostsByTag(tag) Posts filtered by tag
getPostsByAuthor(authorId) Posts filtered by author ID
resolveAuthors(ids) Looks up author collection entries by ID
tagToSlug(tag) / slugToTag(slug) Bidirectional tag↔URL conversion
formatDate(date) Human-readable date (e.g., "February 20, 2026")

Layouts

BlogLayout.astro — Base layout for all blog pages. Uses Starlight's <StarlightPage> component to get the full page shell (head, styles, theme, header) for free. Passes hasSidebar={false} and template: 'splash' to suppress sidebar and doc-page chrome. Passes hero: { actions: [] } to collapse Starlight's two-panel layout into a single content panel (suppressing the auto-generated PageTitle). Extra head tags (canonical URL, OG/Twitter meta, RSS autodiscovery) are injected via the frontmatter.head array. A named <slot name="head" /> is forwarded for page-specific head content (e.g. JSON-LD). The Hero component override (src/components/overrides/Hero.astro) suppresses the hero on /blog/ paths so the dummy hero value has no visual effect.

BlogPostLayout.astro — Wraps BlogLayout with article-specific chrome: title, date, reading time, description, author byline, tags, cover image. Injects JSON-LD Article schema via the head slot. OG image URL: /blog/og/{slug}.png.

Components (src/components/blog/)

Component Purpose
BlogCard.astro Card for listing pages (cover, title, description, meta, tags). Glassmorphism styling matching landing page.
BlogAuthorByline.astro Author avatar + name + role, links to /blog/authors/[id]/
BlogTagList.astro Tag chips linking to /blog/tags/[tagSlug]/
BlogPostGrid.astro Reusable card grid (auto-fill, 320px min, 1200px max). Resolves authors for all posts.

Pages

Route File Description
/blog/ src/pages/blog/index.astro Index with tag filter bar + post grid
/blog/[slug] src/pages/blog/[slug].astro Individual post (via getStaticPaths)
/blog/tags/[tag]/ src/pages/blog/tags/[tag].astro Posts filtered by tag
/blog/authors/[author]/ src/pages/blog/authors/[author].astro Author page with bio + their posts

Navigation

Blog is added to the header nav in src/config/navbar.ts:

{ label: 'Blog', href: '/blog/', basePath: '/blog/' }

Active state is handled by the existing findCurrentNavSection() longest-match logic.

RSS Feeds

Endpoint File
/blog/feed.xml src/pages/blog/feed.xml.ts — Main feed (all posts)
/blog/feed/[tag].xml src/pages/blog/feed/[tag].xml.ts — Per-tag feeds

Uses @astrojs/rss. Currently includes description only (not full rendered content).

AEO (Agentic Engine Optimization)

The blog extends the existing llms.txt system:

  • /blog/[slug]/index.md — Raw markdown endpoint for each post (mirrors the [...slug]/index.md.ts pattern for docs). Uses renderEntryToMarkdown() with basePath: /blog/${post.id}/.
  • /llms.txt — Extended with a ## Blog section listing links to blog markdown endpoints.
  • /llms-full.txt — Extended to render blog posts inline after docs content.
  • src/util/render-to-markdown.ts — Generalized from CollectionEntry<'docs'> to CollectionEntry<'docs'> | CollectionEntry<'blog'> with an optional basePath parameter.

OG Images

Build-time OG image generation at /blog/og/[slug].png using astro-og-canvas:

  • 1200×630px images from post title + description
  • Strands branding: dark background (#0E0E0E), Strands green (#00CC5F) left border

Implementation: src/pages/blog/og/[slug].png.ts

robots.txt

public/robots.txt — Allows all crawlers including GPTBot, ClaudeBot, PerplexityBot. References sitemap.

Dependency Version Pinning

astro-broken-links-checker

This package is pinned to an exact version (1.0.6) rather than using a semver range. It's a low-popularity package, so we avoid automatic updates to prevent potentially pulling in malicious or breaking changes without an explicit review. Before upgrading, manually inspect the changelog and diff on the package's repository.

Known bug: The upstream plugin does not account for Astro's base path configuration, causing it to incorrectly flag all internal links as broken when the site is deployed under a sub-path. See imazen/astro-broken-link-checker#16.

Local fix: Rather than waiting for an upstream fix, the plugin source has been inlined into scripts/astro-broken-links-checker-index.js and scripts/astro-broken-links-checker-check-links.js. The fix captures config.base in the astro:config:setup hook and strips the base prefix from internal links before resolving them against the dist/ directory. astro.config.mjs imports from the local copy instead of the npm package.