Skip to content

Astro Starlight Setup & Deployment Guide

Raw Markdown Source.md
---
title: Astro Starlight Setup & Deployment Guide
description: Complete step-by-step tutorial on scaffolding, configuring multilingual routing, customizing layouts, and deploying Astro Starlight documentation to Vercel.
sidebar:
  order: 10
---

This tutorial guides you through building a high-performance, multilingual documentation website using Astro and Starlight, and deploying it to Vercel. By the end, you will have a production-grade documentation portal with interactive navigation, automated image optimization, Pagefind offline search, and continuous global deployment.

---

## Principles Followed in This Guide

As outlined in our technical writing standards:
1. **No skipped steps**: Every command, file creation, and configuration line is explicitly provided.
2. **Context first**: Technical terms and architectural rationale are explained before executing commands.
3. **Show, don't just tell**: Complete, functional configuration files are included rather than incomplete snippets.
4. **Reproducible and verified**: All instructions directly reflect the tested, live setup running this exact documentation site.

---

## Prerequisites

Before starting, ensure you have the following ready on your system:

- **Node.js**: Version 20.x or higher installed (`node -v`)
- **Package Manager**: npm (version 10.x or higher) or pnpm
- **Vercel Account**: A free account at [vercel.com](https://vercel.com)
- **Vercel CLI**: Accessible via `npx vercel` or globally installed (`npm install -g vercel`)
- **Terminal Access**: A Linux (Debian/Ubuntu), macOS, or Windows WSL2 shell
- **Text Editor**: VS Code, Cursor, Nano, or your preferred code editor

---

## Terminology & Placeholders

Throughout this guide, the following placeholders are used:

| Placeholder | Meaning | Example |
| :--- | :--- | :--- |
| `<project_name>` | The directory and name of your documentation project | `antigravity-docs` |
| `<production_url>` | The live production URL assigned by Vercel | `https://antigravity-docs-teal.vercel.app` |
| `<locale_code>` | Two-letter ISO language identifier | `sl`, `en`, `hr` |
| `<github_repo_url>` | URL of your GitHub or Forgejo repository | `https://github.com/google-gemini` |

---

## Step 1 - Project Scaffolding

We will scaffold a fresh Astro project using the official Starlight template. To ensure automated scripts and agentic environments run smoothly without blocking on interactive prompts, we pass non-interactive flags.

Run the following command in your terminal:

```bash
npm create astro@latest antigravity-docs -- \
  --template starlight \
  --yes \
  --install \
  --no-git \
  --typescript strict
```

### Explanation of flags:
* `--template starlight`: Initializes the project with the official Starlight documentation theme and plugins.
* `--yes`: Accepts all recommended defaults automatically without prompting.
* `--install`: Runs `npm install` immediately to download all core dependencies.
* `--no-git`: Prevents initializing an empty git repository if you already maintain a parent repository.
* `--typescript strict`: Enables strict TypeScript typing for rock-solid type safety in content loaders and configs.

Navigate into the newly created project directory:

```bash
cd antigravity-docs
```

---

## Step 2 - Installing Production Dependencies

In addition to core Starlight, we install **Sharp**. Astro uses Sharp for high-speed image processing, responsive image generation, and modern `.webp` conversion at build time.

Install Sharp:

```bash
npm install sharp
```

### Complete `package.json`

Verify that your `package.json` contains the necessary build scripts and dependencies:

```json
{
  "name": "antigravity-docs",
  "type": "module",
  "version": "0.0.1",
  "scripts": {
    "dev": "astro dev",
    "start": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "astro": "astro"
  },
  "dependencies": {
    "@astrojs/starlight": "^0.42.2",
    "astro": "^5.0.0",
    "sharp": "^0.35.3"
  }
}
```

---

## Step 3 - Content Collection Setup (`src/content.config.ts`)

Astro 5 uses the Content Layer API. We define both our `docs` collection and the `i18n` localization collection in `src/content.config.ts`.

Create or update `src/content.config.ts`:

```typescript
import { defineCollection } from 'astro:content';
import { docsLoader, i18nLoader } from '@astrojs/starlight/loaders';
import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';

export const collections = {
	docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
	i18n: defineCollection({ loader: i18nLoader(), schema: i18nSchema() }),
};
```

### What this does:
* `docsLoader()`: Automatically reads Markdown (`.md`) and MDX (`.mdx`) files located in `src/content/docs/`.
* `i18nLoader()`: Automatically loads custom interface dictionaries located in `src/content/i18n/`.
* `docsSchema()` & `i18nSchema()`: Validates YAML frontmatter (titles, descriptions, sidebar orders) at compile time.

---

## Step 4 - Core Configuration (`astro.config.mjs`)

The `astro.config.mjs` file controls site branding, multilingual locales, sidebar navigation hierarchy, and custom styling.

### Step 4.1 - Multilingual Routing Architecture
In our setup, we configure three languages:
1. **Root (`/`)**: Slovenian (`sl`) serves as the default root language.
2. **English (`/en/`)**: English (`en`) documentation pages.
3. **Croatian (`/hr/`)**: Croatian (`hr`) documentation pages.

### Step 4.2 - Complete `astro.config.mjs`

Create or replace `astro.config.mjs` with the full configuration:

```javascript
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';

// https://astro.build/config
export default defineConfig({
	site: 'https://antigravity.clawdie.si',
	integrations: [
		starlight({
			title: {
				sl: 'Antigravity Dokumentacija',
				en: 'Antigravity Docs',
				hr: 'Antigravity Dokumentacija',
			},
			defaultLocale: 'root',
			locales: {
				root: {
					label: 'SI',
					lang: 'sl',
				},
				en: {
					label: 'EN',
					lang: 'en',
				},
				hr: {
					label: 'HR',
					lang: 'hr',
				},
			},
			customCss: ['./src/styles/custom.css'],
			components: {
				PageTitle: './src/components/PageTitle.astro',
			},
			logo: {
				src: './src/assets/antigravity-icon.png',
			},
			social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/google-gemini' }],
			sidebar: [
				{
					label: 'Antigravity Vodnik',
					translations: {
						en: 'Antigravity Guide',
						hr: 'Antigravity Vodič',
					},
					items: [
						{ label: '1. Pregled in arhitektura', slug: 'overview', translations: { en: '1. Overview & Architecture', hr: '1. Pregled i arhitektura' } },
						{ label: '2. Pravila in AGENTS.md', slug: 'rules', translations: { en: '2. Rules & AGENTS.md', hr: '2. Pravila i AGENTS.md' } },
						{ label: '3. Prilagojena TUI vrstica stanja', slug: 'statusline', translations: { en: '3. Custom TUI Status Line', hr: '3. Prilagođena TUI traka statusa' } },
						{ label: '4. Ukazi in bližnjice', slug: 'commands-shortcuts', translations: { en: '4. Commands & Shortcuts', hr: '4. Naredbe i prečaci' } },
						{ label: '5. Dovoljenja in način YOLO', slug: 'permissions', translations: { en: '5. Permissions & YOLO Mode', hr: '5. Dozvole i YOLO način' } },
						{ label: '6. Večagentna arhitektura', slug: 'subagents', translations: { en: '6. Multi-Agent Architecture', hr: '6. Višeagentska arhitektura' } },
						{ label: '7. Veščine in priročniki', slug: 'skills', translations: { en: '7. Skills & Runbooks', hr: '7. Vještine i priručnici' } },
						{ label: '8. Samodejni sprožilci dogodkov (Hooks)', slug: 'hooks', translations: { en: '8. Lifecycle Hooks', hr: '8. Automatski okidači događaja (Hooks)' } },
						{ label: '9. Zunanji ponudniki in Vercel', slug: 'mcp-vercel', translations: { en: '9. External Providers & Vercel', hr: '9. Vanjski pružatelji i Vercel' } },
						{ label: '10. Postavitev Astro Starlight', slug: 'starlight-setup', translations: { en: '10. Astro Starlight Guide', hr: '10. Postavljanje Astro Starlighta' } },
					],
				},
			],
		}),
	],
});
```

---

## Step 5 - Localizing the User Interface (`src/content/i18n/`)

Starlight provides built-in English strings. For additional languages, provide localized translation files in `src/content/i18n/` to ensure native translations for the search modal, table of contents, theme toggle, and skip links.

### Step 5.1 - Slovenian Dictionary (`src/content/i18n/sl.json`)

Create `src/content/i18n/sl.json`:

```json
{
  "skipLink.label": "Preskoči na vsebino",
  "search.label": "Iskanje",
  "search.ctrlKey": "Ctrl",
  "search.cancelLabel": "Prekliči",
  "search.devWarning": "Iskanje je na voljo le v produkcijski gradnji.\nPoskusite zgraditi in zagnati predogled lokalno.",
  "themeSelect.accessibleLabel": "Izbira teme",
  "themeSelect.dark": "Temna",
  "themeSelect.light": "Svetla",
  "themeSelect.auto": "Samodejno",
  "languageSelect.accessibleLabel": "Izbira jezika",
  "menuButton.accessibleLabel": "Meni",
  "sidebarNav.accessibleLabel": "Glavna navigacija",
  "tableOfContents.onThisPage": "Na tej strani",
  "tableOfContents.overview": "Pregled",
  "i18n.untranslatedContent": "Ta vsebina še ni na voljo v vašem jeziku.",
  "page.editLink": "Uredi stran",
  "page.lastUpdated": "Zadnja posodobitev:",
  "page.previousLink": "Prejšnja",
  "page.nextLink": "Naslednja",
  "page.draft": "Ta vsebina je osnutek in ne bo vključena v produkcijsko gradnjo.",
  "404.text": "Stran ni bila najdena. Preverite spletni naslov ali uporabite iskalnik.",
  "aside.note": "Opomba",
  "aside.tip": "Nasvet",
  "aside.caution": "Pozor",
  "aside.danger": "Nevarnost",
  "fileTree.directory": "Mapa",
  "builtWithStarlight.label": "Zgrajeno s Starlight",
  "heading.anchorLabel": "Razdelek z naslovom “{{title}}”"
}
```

### Step 5.2 - Croatian Dictionary (`src/content/i18n/hr.json`)

Create `src/content/i18n/hr.json`:

```json
{
  "skipLink.label": "Preskoči na sadržaj",
  "search.label": "Pretraživanje",
  "search.ctrlKey": "Ctrl",
  "search.cancelLabel": "Odustani",
  "search.devWarning": "Pretraživanje je dostupno samo u produkcijskoj verziji.\nPokušajte izgraditi i pregledati lokalno.",
  "themeSelect.accessibleLabel": "Odabir teme",
  "themeSelect.dark": "Tamna",
  "themeSelect.light": "Svijetla",
  "themeSelect.auto": "Automatski",
  "languageSelect.accessibleLabel": "Odabir jezika",
  "menuButton.accessibleLabel": "Izbornik",
  "sidebarNav.accessibleLabel": "Glavna navigacija",
  "tableOfContents.onThisPage": "Na ovoj stranici",
  "tableOfContents.overview": "Pregled",
  "i18n.untranslatedContent": "Ovaj sadržaj još nije dostupan na vašem jeziku.",
  "page.editLink": "Uredi stranicu",
  "page.lastUpdated": "Zadnje ažurirano:",
  "page.previousLink": "Prethodna",
  "page.nextLink": "Sljedeća",
  "page.draft": "Ovaj sadržaj je skica i neće biti uključen u produkcijsku verziju.",
  "404.text": "Stranica nije pronađena. Provjerite URL ili upotrijebite pretraživanje.",
  "aside.note": "Napomena",
  "aside.tip": "Savjet",
  "aside.caution": "Oprez",
  "aside.danger": "Opasnost",
  "fileTree.directory": "Mapa",
  "builtWithStarlight.label": "Izgrađeno sa Starlight",
  "heading.anchorLabel": "Odjeljak s naslovom “{{title}}”"
}
```

---

## Step 6 - Custom CSS Layout & Header Ordering (`src/styles/custom.css`)

By default, Starlight places the Language Select dropdown before the Social Icons. In our design, we position the Language Select (`SI`, `EN`, `HR`) between the GitHub icon and the Light/Dark Theme toggle.

Create `src/styles/custom.css`:

```css
/* Position Language Select between Social Icons (GitHub) and Theme Select */
.right-group {
	display: flex;
	align-items: center;
	gap: 1rem;
}

.right-group .social-icons {
	order: 1;
}

.right-group starlight-lang-select {
	order: 2;
	display: flex;
	align-items: center;
	gap: 0.5rem;
}

.right-group starlight-lang-select::after {
	content: '';
	height: 1.75rem;
	border-inline-end: 1px solid var(--sl-color-gray-5);
	margin-inline-start: 0.5rem;
}

.right-group starlight-theme-select {
	order: 3;
}

/* Mobile menu footer layout */
.mobile-preferences .social-icons {
	order: 1;
}

.mobile-preferences starlight-lang-select {
	order: 2;
}

.mobile-preferences starlight-theme-select {
	order: 3;
}
```

---

## Step 7 - Vercel-Style "Copy Page" & "View Markdown" Component Override (`src/components/PageTitle.astro`)

Documentation platforms like Vercel provide an interactive dropdown at the top of every documentation page, allowing visitors and developers to instantly copy the raw Markdown source into their clipboard or inspect it inside an on-screen dialog.

Starlight allows overriding default UI components via the `components` map in `astro.config.mjs`. By overriding `PageTitle`, we retain the standard `<h1>` heading while injecting a copy/view dropdown button without client-side API requests or runtime hydration overhead.

### Architectural Highlights:
1. **Zero-Hydration Server-Side Extraction**: During static generation (`npm run build`), Astro's server component reads `Astro.locals.starlightRoute.entry.filePath` using Node's `node:fs`. The raw Markdown source is embedded directly into an inert `<template class="raw-markdown-source">` tag in the generated HTML.
2. **Accessible Native Dialog**: The Markdown inspection window uses the HTML `<dialog>` element with `dialog.showModal()`, keeping it accessible, lightweight, and isolated from document scroll.
3. **Robust Viewport Centering**: To prevent the `<dialog>` from being pinned to the top-left of the flex container, strict viewport centering CSS is applied:
   ```css
   position: fixed;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
   margin: 0;
   width: min(90vw, 52rem);
   max-height: 85vh;
   ```
4. **Multilingual Localized Labels**: Buttons and modal titles automatically adapt based on the page's current language (`en`, `sl`, `hr`).
5. **View Transition Resiliency**: Event listeners are hooked into `document.addEventListener('astro:page-load', ...)` so the buttons remain responsive during client-side navigation.

### Implementation:

Create `src/components/PageTitle.astro`:

```astro
---
import fs from 'node:fs';
import path from 'node:path';

const { entry, entryMeta } = Astro.locals.starlightRoute;
const title = entry.data.title;
const lang = entryMeta.lang || 'sl';

// Read full source markdown from disk at build time
let fullMarkdown = '';
try {
  if (entry.filePath) {
    fullMarkdown = fs.readFileSync(path.resolve(entry.filePath), 'utf-8');
  } else {
    fullMarkdown = entry.body || '';
  }
} catch {
  fullMarkdown = entry.body || '';
}

// Localized strings
const labels: Record<string, { copyPage: string; copied: string; viewMarkdown: string; rawModalTitle: string; close: string }> = {
  en: {
    copyPage: 'Copy page',
    copied: 'Copied!',
    viewMarkdown: 'View as Markdown',
    rawModalTitle: 'Raw Markdown Source',
    close: 'Close',
  },
  sl: {
    copyPage: 'Kopiraj stran',
    copied: 'Kopirano!',
    viewMarkdown: 'Poglej kot Markdown',
    rawModalTitle: 'Izvorna Markdown koda',
    close: 'Zapri',
  },
  hr: {
    copyPage: 'Kopiraj stranicu',
    copied: 'Kopirano!',
    viewMarkdown: 'Prikaži kao Markdown',
    rawModalTitle: 'Izvorni Markdown kôd',
    close: 'Zatvori',
  },
};

const t = labels[lang] || labels.en;
---

<div class="page-title-wrapper">
  <h1 id="_top">{title}</h1>

  <div class="page-actions-container">
    <div class="copy-dropdown" data-copied-label={t.copied}>
      <div class="copy-btn-group">
        <button
          type="button"
          class="action-btn copy-page-btn"
          aria-label={t.copyPage}
          title={t.copyPage}
        >
          <svg class="copy-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
            <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
          </svg>
          <svg class="check-icon" style="display: none;" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
            <polyline points="20 6 9 17 4 12"></polyline>
          </svg>
          <span class="btn-text">{t.copyPage}</span>
        </button>

        <button
          type="button"
          class="action-btn dropdown-toggle-btn"
          aria-haspopup="true"
          aria-expanded="false"
          aria-label="More options"
        >
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
            <polyline points="6 9 12 15 18 9"></polyline>
          </svg>
        </button>
      </div>

      <div class="dropdown-menu" role="menu">
        <button type="button" class="menu-item menu-copy" role="menuitem">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
            <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
          </svg>
          <span>{t.copyPage}</span>
        </button>

        <button type="button" class="menu-item menu-view" role="menuitem">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
            <circle cx="12" cy="12" r="3"></circle>
          </svg>
          <span>{t.viewMarkdown}</span>
        </button>
      </div>
    </div>
  </div>

  <template class="raw-markdown-source">{fullMarkdown}</template>

  <dialog class="markdown-viewer-dialog">
    <div class="dialog-content">
      <div class="dialog-header">
        <div class="dialog-title-group">
          <span class="dialog-title">{t.rawModalTitle}</span>
          <span class="dialog-badge">.md</span>
        </div>
        <div class="dialog-actions">
          <button type="button" class="dialog-copy-btn action-btn" data-copied-label={t.copied}>
            <svg class="dialog-copy-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
              <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
            </svg>
            <svg class="dialog-check-icon" style="display: none;" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
              <polyline points="20 6 9 17 4 12"></polyline>
            </svg>
            <span class="dialog-copy-text">{t.copyPage}</span>
          </button>
          <button type="button" class="dialog-close-btn" aria-label={t.close}>
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <line x1="18" y1="6" x2="6" y2="18"></line>
              <line x1="6" y1="6" x2="18" y2="18"></line>
            </svg>
          </button>
        </div>
      </div>
      <div class="dialog-body">
        <pre class="dialog-code"><code>{fullMarkdown}</code></pre>
      </div>
    </div>
  </dialog>
</div>

<script>
  function setupPageActions() {
    const wrappers = document.querySelectorAll('.page-title-wrapper');

    wrappers.forEach((wrapper) => {
      if (wrapper.getAttribute('data-actions-initialized')) return;
      wrapper.setAttribute('data-actions-initialized', 'true');

      const template = wrapper.querySelector('.raw-markdown-source') as HTMLTemplateElement | null;
      const rawMarkdown = template ? template.content.textContent || '' : '';

      const copyBtn = wrapper.querySelector('.copy-page-btn') as HTMLButtonElement | null;
      const copyIcon = copyBtn?.querySelector('.copy-icon') as HTMLElement | null;
      const checkIcon = copyBtn?.querySelector('.check-icon') as HTMLElement | null;
      const btnText = copyBtn?.querySelector('.btn-text') as HTMLElement | null;
      const originalText = btnText?.textContent || 'Copy page';

      const toggleBtn = wrapper.querySelector('.dropdown-toggle-btn') as HTMLButtonElement | null;
      const dropdownMenu = wrapper.querySelector('.dropdown-menu') as HTMLElement | null;
      const menuCopy = wrapper.querySelector('.menu-copy') as HTMLButtonElement | null;
      const menuView = wrapper.querySelector('.menu-view') as HTMLButtonElement | null;

      const dialog = wrapper.querySelector('.markdown-viewer-dialog') as HTMLDialogElement | null;
      const dialogClose = wrapper.querySelector('.dialog-close-btn') as HTMLButtonElement | null;
      const dialogCopy = wrapper.querySelector('.dialog-copy-btn') as HTMLButtonElement | null;
      const dialogCopyIcon = dialogCopy?.querySelector('.dialog-copy-icon') as HTMLElement | null;
      const dialogCheckIcon = dialogCopy?.querySelector('.dialog-check-icon') as HTMLElement | null;
      const dialogCopyText = dialogCopy?.querySelector('.dialog-copy-text') as HTMLElement | null;
      const dialogOriginalText = dialogCopyText?.textContent || 'Copy page';

      function copyToClipboard(onSuccess: () => void) {
        if (!rawMarkdown) return;
        navigator.clipboard.writeText(rawMarkdown).then(() => {
          onSuccess();
        }).catch(() => {
          const textarea = document.createElement('textarea');
          textarea.value = rawMarkdown;
          document.body.appendChild(textarea);
          textarea.select();
          document.execCommand('copy');
          document.body.removeChild(textarea);
          onSuccess();
        });
      }

      function triggerCopyFeedback() {
        if (!copyIcon || !checkIcon || !btnText) return;
        copyIcon.style.display = 'none';
        checkIcon.style.display = 'inline-block';
        btnText.textContent = wrapper.querySelector('.copy-dropdown')?.getAttribute('data-copied-label') || 'Copied!';

        setTimeout(() => {
          copyIcon.style.display = 'inline-block';
          checkIcon.style.display = 'none';
          btnText.textContent = originalText;
        }, 2000);
      }

      function triggerDialogCopyFeedback() {
        if (!dialogCopyIcon || !dialogCheckIcon || !dialogCopyText) return;
        dialogCopyIcon.style.display = 'none';
        dialogCheckIcon.style.display = 'inline-block';
        dialogCopyText.textContent = dialogCopy?.getAttribute('data-copied-label') || 'Copied!';

        setTimeout(() => {
          dialogCopyIcon.style.display = 'inline-block';
          dialogCheckIcon.style.display = 'none';
          dialogCopyText.textContent = dialogOriginalText;
        }, 2000);
      }

      copyBtn?.addEventListener('click', (e) => {
        e.stopPropagation();
        closeDropdown();
        copyToClipboard(() => {
          triggerCopyFeedback();
        });
      });

      toggleBtn?.addEventListener('click', (e) => {
        e.stopPropagation();
        const isOpen = dropdownMenu?.classList.contains('show');
        if (isOpen) {
          closeDropdown();
        } else {
          openDropdown();
        }
      });

      function openDropdown() {
        dropdownMenu?.classList.add('show');
        toggleBtn?.setAttribute('aria-expanded', 'true');
      }

      function closeDropdown() {
        dropdownMenu?.classList.remove('show');
        toggleBtn?.setAttribute('aria-expanded', 'false');
      }

      menuCopy?.addEventListener('click', (e) => {
        e.stopPropagation();
        closeDropdown();
        copyToClipboard(() => {
          triggerCopyFeedback();
        });
      });

      menuView?.addEventListener('click', (e) => {
        e.stopPropagation();
        closeDropdown();
        if (dialog) {
          dialog.showModal();
        }
      });

      dialogClose?.addEventListener('click', () => {
        dialog?.close();
      });

      dialog?.addEventListener('click', (e) => {
        const rect = dialog.getBoundingClientRect();
        const isInDialog = (
          rect.top <= e.clientY &&
          e.clientY <= rect.top + rect.height &&
          rect.left <= e.clientX &&
          e.clientX <= rect.left + rect.width
        );
        if (!isInDialog) {
          dialog.close();
        }
      });

      dialogCopy?.addEventListener('click', () => {
        copyToClipboard(() => {
          triggerDialogCopyFeedback();
        });
      });

      document.addEventListener('click', (e) => {
        if (dropdownMenu?.classList.contains('show')) {
          if (!wrapper.querySelector('.copy-dropdown')?.contains(e.target as Node)) {
            closeDropdown();
          }
        }
      });

      document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') {
          closeDropdown();
        }
      });
    });
  }

  setupPageActions();
  document.addEventListener('astro:page-load', setupPageActions);
</script>

<style>
  .page-title-wrapper {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    margin-top: 1rem;
    margin-bottom: 0.5rem;
  }

  h1 {
    margin: 0;
    font-size: var(--sl-text-h1);
    line-height: var(--sl-line-height-headings);
    font-weight: 600;
    color: var(--sl-color-white);
    flex: 1 1 20rem;
  }

  .page-actions-container {
    display: flex;
    align-items: center;
    position: relative;
    user-select: none;
    flex-shrink: 0;
  }

  .copy-dropdown {
    position: relative;
    display: inline-block;
  }

  .copy-btn-group {
    display: inline-flex;
    align-items: stretch;
    border: 1px solid var(--sl-color-gray-5);
    border-radius: 0.5rem;
    background-color: var(--sl-color-gray-6);
    transition: border-color 0.15s ease, background-color 0.15s ease;
  }

  .copy-btn-group:hover {
    border-color: var(--sl-color-gray-4);
    background-color: var(--sl-color-gray-5);
  }

  .action-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    background: transparent;
    border: none;
    color: var(--sl-color-gray-2);
    font-size: var(--sl-text-xs);
    font-weight: 500;
    padding: 0.35rem 0.65rem;
    cursor: pointer;
    line-height: 1;
    transition: color 0.15s ease;
  }

  .action-btn:hover {
    color: var(--sl-color-white);
  }

  .copy-page-btn {
    border-top-left-radius: 0.5rem;
    border-bottom-left-radius: 0.5rem;
  }

  .dropdown-toggle-btn {
    border-left: 1px solid var(--sl-color-gray-5);
    border-top-right-radius: 0.5rem;
    border-bottom-right-radius: 0.5rem;
    padding: 0.35rem 0.45rem;
  }

  .dropdown-menu {
    display: none;
    position: absolute;
    top: calc(100% + 0.35rem);
    right: 0;
    min-width: 11.5rem;
    background-color: var(--sl-color-gray-6);
    border: 1px solid var(--sl-color-gray-5);
    border-radius: 0.5rem;
    box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5), 0 8px 10px -6px rgba(0, 0, 0, 0.4);
    padding: 0.25rem;
    z-index: 100;
  }

  .dropdown-menu.show {
    display: block;
    animation: dropdownFadeIn 0.15s ease-out;
  }

  @keyframes dropdownFadeIn {
    from {
      opacity: 0;
      transform: translateY(-4px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }

  .menu-item {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    padding: 0.45rem 0.75rem;
    font-size: var(--sl-text-xs);
    color: var(--sl-color-gray-2);
    background: transparent;
    border: none;
    border-radius: 0.35rem;
    cursor: pointer;
    text-align: left;
    transition: background-color 0.12s ease, color 0.12s ease;
  }

  .menu-item:hover {
    background-color: var(--sl-color-gray-5);
    color: var(--sl-color-white);
  }

  .markdown-viewer-dialog {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    margin: 0;
    padding: 0;
    border: 1px solid var(--sl-color-gray-5);
    border-radius: 0.75rem;
    background-color: var(--sl-color-gray-6);
    color: var(--sl-color-white);
    max-width: 52rem;
    width: min(90vw, 52rem);
    max-height: 85vh;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.75);
    z-index: 1000;
  }

  .markdown-viewer-dialog:not([open]) {
    display: none;
  }

  .markdown-viewer-dialog[open] {
    display: flex;
    flex-direction: column;
    animation: modalFadeIn 0.2s cubic-bezier(0.16, 1, 0.3, 1);
  }

  @keyframes modalFadeIn {
    from {
      opacity: 0;
      transform: translate(-50%, -47%) scale(0.97);
    }
    to {
      opacity: 1;
      transform: translate(-50%, -50%) scale(1);
    }
  }

  .markdown-viewer-dialog::backdrop {
    background-color: rgba(0, 0, 0, 0.75);
    backdrop-filter: blur(4px);
  }

  .dialog-content {
    display: flex;
    flex-direction: column;
    max-height: 85vh;
    width: 100%;
    overflow: hidden;
  }

  .dialog-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.85rem 1.25rem;
    border-bottom: 1px solid var(--sl-color-gray-5);
    background-color: var(--sl-color-gray-6);
  }

  .dialog-title-group {
    display: flex;
    align-items: center;
    gap: 0.6rem;
  }

  .dialog-title {
    font-size: var(--sl-text-sm);
    font-weight: 600;
    color: var(--sl-color-white);
  }

  .dialog-badge {
    font-size: 0.7rem;
    padding: 0.1rem 0.4rem;
    background-color: var(--sl-color-accent-low);
    color: var(--sl-color-accent-high);
    border: 1px solid var(--sl-color-accent);
    border-radius: 0.25rem;
    font-family: var(--sl-font-mono);
  }

  .dialog-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
  }

  .dialog-copy-btn {
    border: 1px solid var(--sl-color-gray-5);
    border-radius: 0.375rem;
    background-color: var(--sl-color-gray-5);
    padding: 0.3rem 0.6rem;
  }

  .dialog-copy-btn:hover {
    background-color: var(--sl-color-gray-4);
  }

  .dialog-close-btn {
    background: transparent;
    border: none;
    color: var(--sl-color-gray-3);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0.3rem;
    border-radius: 0.375rem;
    transition: color 0.15s ease, background-color 0.15s ease;
  }

  .dialog-close-btn:hover {
    color: var(--sl-color-white);
    background-color: var(--sl-color-gray-5);
  }

  .dialog-body {
    overflow-y: auto;
    padding: 1.25rem;
    background-color: var(--sl-color-black);
    font-family: var(--sl-font-mono);
    font-size: 0.825rem;
    line-height: 1.6;
  }

  .dialog-code {
    margin: 0;
    white-space: pre-wrap;
    word-break: break-word;
    color: var(--sl-color-gray-2);
  }
</style>
```

---

## Step 8 - Interactive Landing Pages (`index.mdx`)

To guide readers directly into chapters, landing pages use Starlight's `splash` template and interactive `LinkCard` components inside a `CardGrid`.

### Landing Page Example (`src/content/docs/en/index.mdx`):

```mdx
---
title: Antigravity Docs
description: A beginner-friendly guide to Google Antigravity, rules, multi-agent squads, and Vercel MCP.
template: splash
hero:
  tagline: A beginner-friendly guide for mastering Antigravity CLI, subagents, and cloud integrations.
  image:
    file: ../../../assets/antigravity-logo.png
  actions:
    - text: Start Reading
      link: /en/overview/
      icon: right-arrow
    - text: Official Antigravity Site
      link: https://antigravity.google
      icon: external
      variant: minimal
---

import { LinkCard, CardGrid } from '@astrojs/starlight/components';

## Explore the Chapters

<CardGrid>
	<LinkCard
		title="1. Multi-Agent Squads"
		href="/en/subagents/"
		description="Build specialized subagent teams to eliminate context rot."
	/>
	<LinkCard
		title="2. Custom Status Line"
		href="/en/statusline/"
		description="Configure real-time token tracking in your terminal interface."
	/>
	<LinkCard
		title="3. Rules vs. Skills"
		href="/en/skills/"
		description="Always-on AGENTS.md guardrails vs. on-demand SKILL.md runbooks."
	/>
	<LinkCard
		title="4. Lifecycle Hooks"
		href="/en/hooks/"
		description="Zero-token deterministic automation: linters, security gates, and tests."
	/>
</CardGrid>
```

---

## Step 9 - Structuring Content Chapters

Organize each chapter into its own Markdown file inside `src/content/docs/`.

### Directory Tree Overview:
```text
src/content/docs/
├── commands-shortcuts.md       # Root (SI) chapter
├── hooks.md
├── index.mdx                   # Root (SI) splash landing page
├── mcp-vercel.md
├── overview.md
├── permissions.md
├── rules.md
├── skills.md
├── statusline.md
├── subagents.md
├── starlight-setup.md
├── en/                         # English documentation
│   ├── index.mdx
│   ├── overview.md
│   └── starlight-setup.md
└── hr/                         # Croatian documentation
    ├── index.mdx
    ├── overview.md
    └── starlight-setup.md
```

### Chapter Frontmatter Standard:
Each chapter file starts with structured YAML metadata:

```markdown
---
title: Chapter Title
description: Concise single-sentence summary of the chapter.
sidebar:
  order: 1
---
```

---

## Step 10 - Local Testing & Background Dev Server

To preview your documentation during active development, run Astro's development server.

### Interactive Terminal Dev:
```bash
npm run dev
```
Open your browser at `http://localhost:4321/`.

### Background Mode (For Agentic & Headless Environments):
If running inside an agent terminal, run the server in the background:

```bash
npx astro dev --background
```

Manage the running background server:
* Check status: `npx astro dev status`
* View live logs: `npx astro dev logs`
* Stop the server: `npx astro dev stop`

---

## Step 11 - Local Production Build Verification

Always test building static HTML assets before deploying to production:

```bash
npm run build
```

Verify the build output:
1. **Static Routes**: Confirms generation of all `/`, `/en/`, and `/hr/` routes.
2. **Optimized Images**: Sharp converts and caches assets as modern `.webp` files.
3. **Pagefind Search Index**: Indexes all pages for client-side search without external API dependencies.

Expected successful terminal output:
```text
[build] Complete!
[starlight:pagefind] Building search index with Pagefind...
[starlight:pagefind] Found 34 HTML files.
[starlight:pagefind] Finished building search index.
```

---

## Step 12 - Automated Production Deployment to Vercel

We deploy the documentation site to Vercel using the official Vercel CLI.

### Step 12.1 - Deploy to Production

Execute the non-interactive production build and deploy command:

```bash
npm run build && npx vercel --prod --yes
```

### What this does:
* `npm run build`: Generates the static production bundle in `dist/`.
* `npx vercel --prod --yes`: Authenticates with your linked Vercel project, uploads static assets, and promotes the deployment directly to the production domain alias (`https://antigravity-docs-teal.vercel.app`).

### Step 12.2 - Testing Live Deployment
Once deployment completes, test the live HTTP response code using `curl`:

```bash
curl -s -o /dev/null -w "%{http_code}\n" https://antigravity-docs-teal.vercel.app/en/starlight-setup/
```
Expected output:
```text
200
```

---

## Step 13 - Custom Domain Setup & Automated SSL via Vercel CLI

Default Vercel deployment URLs (such as `https://antigravity-docs-teal.vercel.app`) are long and project-specific. For a professional, branded documentation experience, connect a custom domain or subdomain (e.g. `antigravity.clawdie.si`).

Vercel operates an Anycast Edge Network with automated Let's Encrypt / ZeroSSL certificate management. You can configure and verify custom domains directly from the terminal without migrating your existing nameservers.

### Step 13.1 - Registering the Custom Domain to the Project

Run `vercel domains add` to link the custom domain or subdomain to your project:

```bash
npx vercel domains add antigravity.clawdie.si antigravity-docs
```

This creates an edge routing entry within Vercel's global CDN network pointing requests for `antigravity.clawdie.si` to your active production build.

### Step 13.2 - Inspecting Required DNS Configuration

Query Vercel to inspect the current DNS status and determine the exact records needed:

```bash
npx vercel domains verify antigravity.clawdie.si
```

Vercel checks the public DNS and outputs the recommended record configuration:
* **Option A (A Record - Recommended)**: Point `antigravity` to Vercel's Anycast IP `76.76.21.21`.
* **Option B (CNAME Record)**: Point `antigravity` to `cname.vercel-dns.com.` (or your project-specific Vercel CNAME hash).

### Step 13.3 - Adding DNS Records at Your Registrar

Log into your DNS provider or registrar control panel where your domain is managed and add the record:

| Record Type | Name / Host | Target / Value | TTL |
| :--- | :--- | :--- | :--- |
| `A` | `antigravity` | `76.76.21.21` | `3600` (or Auto) |
| *or* `CNAME` | `antigravity` | `cname.vercel-dns.com.` | `3600` (or Auto) |

### Step 13.4 - Automated Verification & SSL Provisioning

Once your DNS changes propagate (typically 1–5 minutes), re-run verification:

```bash
npx vercel domains verify antigravity.clawdie.si
```

Expected output:
```text
Status:
  DNS Configuration   ✔ Valid Configuration
  Project             ✔ Verified for antigravity-docs
```

Vercel automatically triggers an ACME certificate challenge, provisions a free SSL/TLS certificate, and forces HTTPS encryption with HTTP/2 and HSTS enabled.

### Step 13.5 - Live Custom Domain Verification

Verify that your custom domain is serving with a valid SSL certificate and edge cache:

```bash
curl -s -I https://antigravity.clawdie.si/
```

Expected response headers:
```http
HTTP/2 200
server: Vercel
strict-transport-security: max-age=63072000
x-vercel-cache: HIT
```

---

## Step 14 - Performance & Optimization Audit (Google PageSpeed Insights)

To verify the real-world performance, accessibility, and Core Web Vitals of the deployed documentation, we run a live audit using Google PageSpeed Insights.

### Real-World Audit Result (Perfect 100% Score)

Running the audit against our live production deployment (`https://antigravity.clawdie.si/`) achieves a **flawless 100/100 across all four audit categories**:

![Google PageSpeed Insights perfect 100 score on Antigravity Docs](../../../assets/pagespeed-perfect-score.png)

* **Performance: 100** — Near-instant Largest Contentful Paint (LCP) and zero blocking time due to Astro's zero-JavaScript static HTML default.
* **Accessibility: 100** — Built-in accessible color contrast, compliant ARIA attributes, and keyboard-navigable menus provided by Starlight.
* **Best Practices: 100** — Automatic modern image compression (Sharp `.webp`), HTTPS enforcement, and zero console errors or deprecated APIs.
* **SEO: 100** — Automated metadata tags, structured sitemaps, semantic headings, and multilingual alternate links (`hreflang`).
* **Agentic Browsing: 2/2** — Clean, semantic HTML structure perfectly parseable by AI agents and automated crawlers.

---

## Conclusion

You now have a fully operational, multilingual Astro Starlight documentation portal hosted on Vercel. The site includes automatic image compression, full-text client-side search across all languages, customizable CSS layout ordering, and instant automated production deployment.

**Next Steps & Resources:**
- [Astro Starlight Official Documentation](https://starlight.astro.build)
- [Astro Routing Guide](https://docs.astro.build/en/guides/routing/)
- [Vercel CLI Documentation](https://vercel.com/docs/cli)
- [Antigravity Architecture Overview](/en/overview/)

This tutorial guides you through building a high-performance, multilingual documentation website using Astro and Starlight, and deploying it to Vercel. By the end, you will have a production-grade documentation portal with interactive navigation, automated image optimization, Pagefind offline search, and continuous global deployment.


As outlined in our technical writing standards:

  1. No skipped steps: Every command, file creation, and configuration line is explicitly provided.
  2. Context first: Technical terms and architectural rationale are explained before executing commands.
  3. Show, don’t just tell: Complete, functional configuration files are included rather than incomplete snippets.
  4. Reproducible and verified: All instructions directly reflect the tested, live setup running this exact documentation site.

Before starting, ensure you have the following ready on your system:

  • Node.js: Version 20.x or higher installed (node -v)
  • Package Manager: npm (version 10.x or higher) or pnpm
  • Vercel Account: A free account at vercel.com
  • Vercel CLI: Accessible via npx vercel or globally installed (npm install -g vercel)
  • Terminal Access: A Linux (Debian/Ubuntu), macOS, or Windows WSL2 shell
  • Text Editor: VS Code, Cursor, Nano, or your preferred code editor

Throughout this guide, the following placeholders are used:

Placeholder Meaning Example
<project_name> The directory and name of your documentation project antigravity-docs
<production_url> The live production URL assigned by Vercel https://antigravity-docs-teal.vercel.app
<locale_code> Two-letter ISO language identifier sl, en, hr
<github_repo_url> URL of your GitHub or Forgejo repository https://github.com/google-gemini

We will scaffold a fresh Astro project using the official Starlight template. To ensure automated scripts and agentic environments run smoothly without blocking on interactive prompts, we pass non-interactive flags.

Run the following command in your terminal:

Terminal window
npm create astro@latest antigravity-docs -- \
--template starlight \
--yes \
--install \
--no-git \
--typescript strict
  • --template starlight: Initializes the project with the official Starlight documentation theme and plugins.
  • --yes: Accepts all recommended defaults automatically without prompting.
  • --install: Runs npm install immediately to download all core dependencies.
  • --no-git: Prevents initializing an empty git repository if you already maintain a parent repository.
  • --typescript strict: Enables strict TypeScript typing for rock-solid type safety in content loaders and configs.

Navigate into the newly created project directory:

Terminal window
cd antigravity-docs

Step 2 - Installing Production Dependencies

Section titled “Step 2 - Installing Production Dependencies”

In addition to core Starlight, we install Sharp. Astro uses Sharp for high-speed image processing, responsive image generation, and modern .webp conversion at build time.

Install Sharp:

Terminal window
npm install sharp

Verify that your package.json contains the necessary build scripts and dependencies:

{
"name": "antigravity-docs",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/starlight": "^0.42.2",
"astro": "^5.0.0",
"sharp": "^0.35.3"
}
}

Step 3 - Content Collection Setup (src/content.config.ts)

Section titled “Step 3 - Content Collection Setup (src/content.config.ts)”

Astro 5 uses the Content Layer API. We define both our docs collection and the i18n localization collection in src/content.config.ts.

Create or update src/content.config.ts:

import { defineCollection } from 'astro:content';
import { docsLoader, i18nLoader } from '@astrojs/starlight/loaders';
import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
i18n: defineCollection({ loader: i18nLoader(), schema: i18nSchema() }),
};
  • docsLoader(): Automatically reads Markdown (.md) and MDX (.mdx) files located in src/content/docs/.
  • i18nLoader(): Automatically loads custom interface dictionaries located in src/content/i18n/.
  • docsSchema() & i18nSchema(): Validates YAML frontmatter (titles, descriptions, sidebar orders) at compile time.

Step 4 - Core Configuration (astro.config.mjs)

Section titled “Step 4 - Core Configuration (astro.config.mjs)”

The astro.config.mjs file controls site branding, multilingual locales, sidebar navigation hierarchy, and custom styling.

Step 4.1 - Multilingual Routing Architecture

Section titled “Step 4.1 - Multilingual Routing Architecture”

In our setup, we configure three languages:

  1. Root (/): Slovenian (sl) serves as the default root language.
  2. English (/en/): English (en) documentation pages.
  3. Croatian (/hr/): Croatian (hr) documentation pages.

Create or replace astro.config.mjs with the full configuration:

// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
// https://astro.build/config
export default defineConfig({
site: 'https://antigravity.clawdie.si',
integrations: [
starlight({
title: {
sl: 'Antigravity Dokumentacija',
en: 'Antigravity Docs',
hr: 'Antigravity Dokumentacija',
},
defaultLocale: 'root',
locales: {
root: {
label: 'SI',
lang: 'sl',
},
en: {
label: 'EN',
lang: 'en',
},
hr: {
label: 'HR',
lang: 'hr',
},
},
customCss: ['./src/styles/custom.css'],
components: {
PageTitle: './src/components/PageTitle.astro',
},
logo: {
src: './src/assets/antigravity-icon.png',
},
social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/google-gemini' }],
sidebar: [
{
label: 'Antigravity Vodnik',
translations: {
en: 'Antigravity Guide',
hr: 'Antigravity Vodič',
},
items: [
{ label: '1. Pregled in arhitektura', slug: 'overview', translations: { en: '1. Overview & Architecture', hr: '1. Pregled i arhitektura' } },
{ label: '2. Pravila in AGENTS.md', slug: 'rules', translations: { en: '2. Rules & AGENTS.md', hr: '2. Pravila i AGENTS.md' } },
{ label: '3. Prilagojena TUI vrstica stanja', slug: 'statusline', translations: { en: '3. Custom TUI Status Line', hr: '3. Prilagođena TUI traka statusa' } },
{ label: '4. Ukazi in bližnjice', slug: 'commands-shortcuts', translations: { en: '4. Commands & Shortcuts', hr: '4. Naredbe i prečaci' } },
{ label: '5. Dovoljenja in način YOLO', slug: 'permissions', translations: { en: '5. Permissions & YOLO Mode', hr: '5. Dozvole i YOLO način' } },
{ label: '6. Večagentna arhitektura', slug: 'subagents', translations: { en: '6. Multi-Agent Architecture', hr: '6. Višeagentska arhitektura' } },
{ label: '7. Veščine in priročniki', slug: 'skills', translations: { en: '7. Skills & Runbooks', hr: '7. Vještine i priručnici' } },
{ label: '8. Samodejni sprožilci dogodkov (Hooks)', slug: 'hooks', translations: { en: '8. Lifecycle Hooks', hr: '8. Automatski okidači događaja (Hooks)' } },
{ label: '9. Zunanji ponudniki in Vercel', slug: 'mcp-vercel', translations: { en: '9. External Providers & Vercel', hr: '9. Vanjski pružatelji i Vercel' } },
{ label: '10. Postavitev Astro Starlight', slug: 'starlight-setup', translations: { en: '10. Astro Starlight Guide', hr: '10. Postavljanje Astro Starlighta' } },
],
},
],
}),
],
});

Step 5 - Localizing the User Interface (src/content/i18n/)

Section titled “Step 5 - Localizing the User Interface (src/content/i18n/)”

Starlight provides built-in English strings. For additional languages, provide localized translation files in src/content/i18n/ to ensure native translations for the search modal, table of contents, theme toggle, and skip links.

Step 5.1 - Slovenian Dictionary (src/content/i18n/sl.json)

Section titled “Step 5.1 - Slovenian Dictionary (src/content/i18n/sl.json)”

Create src/content/i18n/sl.json:

{
"skipLink.label": "Preskoči na vsebino",
"search.label": "Iskanje",
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Prekliči",
"search.devWarning": "Iskanje je na voljo le v produkcijski gradnji.\nPoskusite zgraditi in zagnati predogled lokalno.",
"themeSelect.accessibleLabel": "Izbira teme",
"themeSelect.dark": "Temna",
"themeSelect.light": "Svetla",
"themeSelect.auto": "Samodejno",
"languageSelect.accessibleLabel": "Izbira jezika",
"menuButton.accessibleLabel": "Meni",
"sidebarNav.accessibleLabel": "Glavna navigacija",
"tableOfContents.onThisPage": "Na tej strani",
"tableOfContents.overview": "Pregled",
"i18n.untranslatedContent": "Ta vsebina še ni na voljo v vašem jeziku.",
"page.editLink": "Uredi stran",
"page.lastUpdated": "Zadnja posodobitev:",
"page.previousLink": "Prejšnja",
"page.nextLink": "Naslednja",
"page.draft": "Ta vsebina je osnutek in ne bo vključena v produkcijsko gradnjo.",
"404.text": "Stran ni bila najdena. Preverite spletni naslov ali uporabite iskalnik.",
"aside.note": "Opomba",
"aside.tip": "Nasvet",
"aside.caution": "Pozor",
"aside.danger": "Nevarnost",
"fileTree.directory": "Mapa",
"builtWithStarlight.label": "Zgrajeno s Starlight",
"heading.anchorLabel": "Razdelek z naslovom “{{title}}”"
}

Step 5.2 - Croatian Dictionary (src/content/i18n/hr.json)

Section titled “Step 5.2 - Croatian Dictionary (src/content/i18n/hr.json)”

Create src/content/i18n/hr.json:

{
"skipLink.label": "Preskoči na sadržaj",
"search.label": "Pretraživanje",
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Odustani",
"search.devWarning": "Pretraživanje je dostupno samo u produkcijskoj verziji.\nPokušajte izgraditi i pregledati lokalno.",
"themeSelect.accessibleLabel": "Odabir teme",
"themeSelect.dark": "Tamna",
"themeSelect.light": "Svijetla",
"themeSelect.auto": "Automatski",
"languageSelect.accessibleLabel": "Odabir jezika",
"menuButton.accessibleLabel": "Izbornik",
"sidebarNav.accessibleLabel": "Glavna navigacija",
"tableOfContents.onThisPage": "Na ovoj stranici",
"tableOfContents.overview": "Pregled",
"i18n.untranslatedContent": "Ovaj sadržaj još nije dostupan na vašem jeziku.",
"page.editLink": "Uredi stranicu",
"page.lastUpdated": "Zadnje ažurirano:",
"page.previousLink": "Prethodna",
"page.nextLink": "Sljedeća",
"page.draft": "Ovaj sadržaj je skica i neće biti uključen u produkcijsku verziju.",
"404.text": "Stranica nije pronađena. Provjerite URL ili upotrijebite pretraživanje.",
"aside.note": "Napomena",
"aside.tip": "Savjet",
"aside.caution": "Oprez",
"aside.danger": "Opasnost",
"fileTree.directory": "Mapa",
"builtWithStarlight.label": "Izgrađeno sa Starlight",
"heading.anchorLabel": "Odjeljak s naslovom “{{title}}”"
}

Step 6 - Custom CSS Layout & Header Ordering (src/styles/custom.css)

Section titled “Step 6 - Custom CSS Layout & Header Ordering (src/styles/custom.css)”

By default, Starlight places the Language Select dropdown before the Social Icons. In our design, we position the Language Select (SI, EN, HR) between the GitHub icon and the Light/Dark Theme toggle.

Create src/styles/custom.css:

/* Position Language Select between Social Icons (GitHub) and Theme Select */
.right-group {
display: flex;
align-items: center;
gap: 1rem;
}
.right-group .social-icons {
order: 1;
}
.right-group starlight-lang-select {
order: 2;
display: flex;
align-items: center;
gap: 0.5rem;
}
.right-group starlight-lang-select::after {
content: '';
height: 1.75rem;
border-inline-end: 1px solid var(--sl-color-gray-5);
margin-inline-start: 0.5rem;
}
.right-group starlight-theme-select {
order: 3;
}
/* Mobile menu footer layout */
.mobile-preferences .social-icons {
order: 1;
}
.mobile-preferences starlight-lang-select {
order: 2;
}
.mobile-preferences starlight-theme-select {
order: 3;
}

Step 7 - Vercel-Style “Copy Page” & “View Markdown” Component Override (src/components/PageTitle.astro)

Section titled “Step 7 - Vercel-Style “Copy Page” & “View Markdown” Component Override (src/components/PageTitle.astro)”

Documentation platforms like Vercel provide an interactive dropdown at the top of every documentation page, allowing visitors and developers to instantly copy the raw Markdown source into their clipboard or inspect it inside an on-screen dialog.

Starlight allows overriding default UI components via the components map in astro.config.mjs. By overriding PageTitle, we retain the standard <h1> heading while injecting a copy/view dropdown button without client-side API requests or runtime hydration overhead.

  1. Zero-Hydration Server-Side Extraction: During static generation (npm run build), Astro’s server component reads Astro.locals.starlightRoute.entry.filePath using Node’s node:fs. The raw Markdown source is embedded directly into an inert <template class="raw-markdown-source"> tag in the generated HTML.
  2. Accessible Native Dialog: The Markdown inspection window uses the HTML <dialog> element with dialog.showModal(), keeping it accessible, lightweight, and isolated from document scroll.
  3. Robust Viewport Centering: To prevent the <dialog> from being pinned to the top-left of the flex container, strict viewport centering CSS is applied:
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    margin: 0;
    width: min(90vw, 52rem);
    max-height: 85vh;
  4. Multilingual Localized Labels: Buttons and modal titles automatically adapt based on the page’s current language (en, sl, hr).
  5. View Transition Resiliency: Event listeners are hooked into document.addEventListener('astro:page-load', ...) so the buttons remain responsive during client-side navigation.

Create src/components/PageTitle.astro:

---
import fs from 'node:fs';
import path from 'node:path';
const { entry, entryMeta } = Astro.locals.starlightRoute;
const title = entry.data.title;
const lang = entryMeta.lang || 'sl';
// Read full source markdown from disk at build time
let fullMarkdown = '';
try {
if (entry.filePath) {
fullMarkdown = fs.readFileSync(path.resolve(entry.filePath), 'utf-8');
} else {
fullMarkdown = entry.body || '';
}
} catch {
fullMarkdown = entry.body || '';
}
// Localized strings
const labels: Record<string, { copyPage: string; copied: string; viewMarkdown: string; rawModalTitle: string; close: string }> = {
en: {
copyPage: 'Copy page',
copied: 'Copied!',
viewMarkdown: 'View as Markdown',
rawModalTitle: 'Raw Markdown Source',
close: 'Close',
},
sl: {
copyPage: 'Kopiraj stran',
copied: 'Kopirano!',
viewMarkdown: 'Poglej kot Markdown',
rawModalTitle: 'Izvorna Markdown koda',
close: 'Zapri',
},
hr: {
copyPage: 'Kopiraj stranicu',
copied: 'Kopirano!',
viewMarkdown: 'Prikaži kao Markdown',
rawModalTitle: 'Izvorni Markdown kôd',
close: 'Zatvori',
},
};
const t = labels[lang] || labels.en;
---
<div class="page-title-wrapper">
<h1 id="_top">{title}</h1>
<div class="page-actions-container">
<div class="copy-dropdown" data-copied-label={t.copied}>
<div class="copy-btn-group">
<button
type="button"
class="action-btn copy-page-btn"
aria-label={t.copyPage}
title={t.copyPage}
>
<svg class="copy-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<svg class="check-icon" style="display: none;" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span class="btn-text">{t.copyPage}</span>
</button>
<button
type="button"
class="action-btn dropdown-toggle-btn"
aria-haspopup="true"
aria-expanded="false"
aria-label="More options"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
<div class="dropdown-menu" role="menu">
<button type="button" class="menu-item menu-copy" role="menuitem">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<span>{t.copyPage}</span>
</button>
<button type="button" class="menu-item menu-view" role="menuitem">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<span>{t.viewMarkdown}</span>
</button>
</div>
</div>
</div>
<template class="raw-markdown-source">{fullMarkdown}</template>
<dialog class="markdown-viewer-dialog">
<div class="dialog-content">
<div class="dialog-header">
<div class="dialog-title-group">
<span class="dialog-title">{t.rawModalTitle}</span>
<span class="dialog-badge">.md</span>
</div>
<div class="dialog-actions">
<button type="button" class="dialog-copy-btn action-btn" data-copied-label={t.copied}>
<svg class="dialog-copy-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<svg class="dialog-check-icon" style="display: none;" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span class="dialog-copy-text">{t.copyPage}</span>
</button>
<button type="button" class="dialog-close-btn" aria-label={t.close}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<div class="dialog-body">
<pre class="dialog-code"><code>{fullMarkdown}</code></pre>
</div>
</div>
</dialog>
</div>
<script>
function setupPageActions() {
const wrappers = document.querySelectorAll('.page-title-wrapper');
wrappers.forEach((wrapper) => {
if (wrapper.getAttribute('data-actions-initialized')) return;
wrapper.setAttribute('data-actions-initialized', 'true');
const template = wrapper.querySelector('.raw-markdown-source') as HTMLTemplateElement | null;
const rawMarkdown = template ? template.content.textContent || '' : '';
const copyBtn = wrapper.querySelector('.copy-page-btn') as HTMLButtonElement | null;
const copyIcon = copyBtn?.querySelector('.copy-icon') as HTMLElement | null;
const checkIcon = copyBtn?.querySelector('.check-icon') as HTMLElement | null;
const btnText = copyBtn?.querySelector('.btn-text') as HTMLElement | null;
const originalText = btnText?.textContent || 'Copy page';
const toggleBtn = wrapper.querySelector('.dropdown-toggle-btn') as HTMLButtonElement | null;
const dropdownMenu = wrapper.querySelector('.dropdown-menu') as HTMLElement | null;
const menuCopy = wrapper.querySelector('.menu-copy') as HTMLButtonElement | null;
const menuView = wrapper.querySelector('.menu-view') as HTMLButtonElement | null;
const dialog = wrapper.querySelector('.markdown-viewer-dialog') as HTMLDialogElement | null;
const dialogClose = wrapper.querySelector('.dialog-close-btn') as HTMLButtonElement | null;
const dialogCopy = wrapper.querySelector('.dialog-copy-btn') as HTMLButtonElement | null;
const dialogCopyIcon = dialogCopy?.querySelector('.dialog-copy-icon') as HTMLElement | null;
const dialogCheckIcon = dialogCopy?.querySelector('.dialog-check-icon') as HTMLElement | null;
const dialogCopyText = dialogCopy?.querySelector('.dialog-copy-text') as HTMLElement | null;
const dialogOriginalText = dialogCopyText?.textContent || 'Copy page';
function copyToClipboard(onSuccess: () => void) {
if (!rawMarkdown) return;
navigator.clipboard.writeText(rawMarkdown).then(() => {
onSuccess();
}).catch(() => {
const textarea = document.createElement('textarea');
textarea.value = rawMarkdown;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
onSuccess();
});
}
function triggerCopyFeedback() {
if (!copyIcon || !checkIcon || !btnText) return;
copyIcon.style.display = 'none';
checkIcon.style.display = 'inline-block';
btnText.textContent = wrapper.querySelector('.copy-dropdown')?.getAttribute('data-copied-label') || 'Copied!';
setTimeout(() => {
copyIcon.style.display = 'inline-block';
checkIcon.style.display = 'none';
btnText.textContent = originalText;
}, 2000);
}
function triggerDialogCopyFeedback() {
if (!dialogCopyIcon || !dialogCheckIcon || !dialogCopyText) return;
dialogCopyIcon.style.display = 'none';
dialogCheckIcon.style.display = 'inline-block';
dialogCopyText.textContent = dialogCopy?.getAttribute('data-copied-label') || 'Copied!';
setTimeout(() => {
dialogCopyIcon.style.display = 'inline-block';
dialogCheckIcon.style.display = 'none';
dialogCopyText.textContent = dialogOriginalText;
}, 2000);
}
copyBtn?.addEventListener('click', (e) => {
e.stopPropagation();
closeDropdown();
copyToClipboard(() => {
triggerCopyFeedback();
});
});
toggleBtn?.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = dropdownMenu?.classList.contains('show');
if (isOpen) {
closeDropdown();
} else {
openDropdown();
}
});
function openDropdown() {
dropdownMenu?.classList.add('show');
toggleBtn?.setAttribute('aria-expanded', 'true');
}
function closeDropdown() {
dropdownMenu?.classList.remove('show');
toggleBtn?.setAttribute('aria-expanded', 'false');
}
menuCopy?.addEventListener('click', (e) => {
e.stopPropagation();
closeDropdown();
copyToClipboard(() => {
triggerCopyFeedback();
});
});
menuView?.addEventListener('click', (e) => {
e.stopPropagation();
closeDropdown();
if (dialog) {
dialog.showModal();
}
});
dialogClose?.addEventListener('click', () => {
dialog?.close();
});
dialog?.addEventListener('click', (e) => {
const rect = dialog.getBoundingClientRect();
const isInDialog = (
rect.top <= e.clientY &&
e.clientY <= rect.top + rect.height &&
rect.left <= e.clientX &&
e.clientX <= rect.left + rect.width
);
if (!isInDialog) {
dialog.close();
}
});
dialogCopy?.addEventListener('click', () => {
copyToClipboard(() => {
triggerDialogCopyFeedback();
});
});
document.addEventListener('click', (e) => {
if (dropdownMenu?.classList.contains('show')) {
if (!wrapper.querySelector('.copy-dropdown')?.contains(e.target as Node)) {
closeDropdown();
}
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeDropdown();
}
});
});
}
setupPageActions();
document.addEventListener('astro:page-load', setupPageActions);
</script>
<style>
.page-title-wrapper {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-top: 1rem;
margin-bottom: 0.5rem;
}
h1 {
margin: 0;
font-size: var(--sl-text-h1);
line-height: var(--sl-line-height-headings);
font-weight: 600;
color: var(--sl-color-white);
flex: 1 1 20rem;
}
.page-actions-container {
display: flex;
align-items: center;
position: relative;
user-select: none;
flex-shrink: 0;
}
.copy-dropdown {
position: relative;
display: inline-block;
}
.copy-btn-group {
display: inline-flex;
align-items: stretch;
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.5rem;
background-color: var(--sl-color-gray-6);
transition: border-color 0.15s ease, background-color 0.15s ease;
}
.copy-btn-group:hover {
border-color: var(--sl-color-gray-4);
background-color: var(--sl-color-gray-5);
}
.action-btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: transparent;
border: none;
color: var(--sl-color-gray-2);
font-size: var(--sl-text-xs);
font-weight: 500;
padding: 0.35rem 0.65rem;
cursor: pointer;
line-height: 1;
transition: color 0.15s ease;
}
.action-btn:hover {
color: var(--sl-color-white);
}
.copy-page-btn {
border-top-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.dropdown-toggle-btn {
border-left: 1px solid var(--sl-color-gray-5);
border-top-right-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
padding: 0.35rem 0.45rem;
}
.dropdown-menu {
display: none;
position: absolute;
top: calc(100% + 0.35rem);
right: 0;
min-width: 11.5rem;
background-color: var(--sl-color-gray-6);
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.5rem;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5), 0 8px 10px -6px rgba(0, 0, 0, 0.4);
padding: 0.25rem;
z-index: 100;
}
.dropdown-menu.show {
display: block;
animation: dropdownFadeIn 0.15s ease-out;
}
@keyframes dropdownFadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.menu-item {
display: flex;
align-items: center;
gap: 0.6rem;
width: 100%;
padding: 0.45rem 0.75rem;
font-size: var(--sl-text-xs);
color: var(--sl-color-gray-2);
background: transparent;
border: none;
border-radius: 0.35rem;
cursor: pointer;
text-align: left;
transition: background-color 0.12s ease, color 0.12s ease;
}
.menu-item:hover {
background-color: var(--sl-color-gray-5);
color: var(--sl-color-white);
}
.markdown-viewer-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: 0;
padding: 0;
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.75rem;
background-color: var(--sl-color-gray-6);
color: var(--sl-color-white);
max-width: 52rem;
width: min(90vw, 52rem);
max-height: 85vh;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.75);
z-index: 1000;
}
.markdown-viewer-dialog:not([open]) {
display: none;
}
.markdown-viewer-dialog[open] {
display: flex;
flex-direction: column;
animation: modalFadeIn 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: translate(-50%, -47%) scale(0.97);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
.markdown-viewer-dialog::backdrop {
background-color: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px);
}
.dialog-content {
display: flex;
flex-direction: column;
max-height: 85vh;
width: 100%;
overflow: hidden;
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.85rem 1.25rem;
border-bottom: 1px solid var(--sl-color-gray-5);
background-color: var(--sl-color-gray-6);
}
.dialog-title-group {
display: flex;
align-items: center;
gap: 0.6rem;
}
.dialog-title {
font-size: var(--sl-text-sm);
font-weight: 600;
color: var(--sl-color-white);
}
.dialog-badge {
font-size: 0.7rem;
padding: 0.1rem 0.4rem;
background-color: var(--sl-color-accent-low);
color: var(--sl-color-accent-high);
border: 1px solid var(--sl-color-accent);
border-radius: 0.25rem;
font-family: var(--sl-font-mono);
}
.dialog-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.dialog-copy-btn {
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.375rem;
background-color: var(--sl-color-gray-5);
padding: 0.3rem 0.6rem;
}
.dialog-copy-btn:hover {
background-color: var(--sl-color-gray-4);
}
.dialog-close-btn {
background: transparent;
border: none;
color: var(--sl-color-gray-3);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0.3rem;
border-radius: 0.375rem;
transition: color 0.15s ease, background-color 0.15s ease;
}
.dialog-close-btn:hover {
color: var(--sl-color-white);
background-color: var(--sl-color-gray-5);
}
.dialog-body {
overflow-y: auto;
padding: 1.25rem;
background-color: var(--sl-color-black);
font-family: var(--sl-font-mono);
font-size: 0.825rem;
line-height: 1.6;
}
.dialog-code {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
color: var(--sl-color-gray-2);
}
</style>

Step 8 - Interactive Landing Pages (index.mdx)

Section titled “Step 8 - Interactive Landing Pages (index.mdx)”

To guide readers directly into chapters, landing pages use Starlight’s splash template and interactive LinkCard components inside a CardGrid.

Landing Page Example (src/content/docs/en/index.mdx):

Section titled “Landing Page Example (src/content/docs/en/index.mdx):”
---
title: Antigravity Docs
description: A beginner-friendly guide to Google Antigravity, rules, multi-agent squads, and Vercel MCP.
template: splash
hero:
tagline: A beginner-friendly guide for mastering Antigravity CLI, subagents, and cloud integrations.
image:
file: ../../../assets/antigravity-logo.png
actions:
- text: Start Reading
link: /en/overview/
icon: right-arrow
- text: Official Antigravity Site
link: https://antigravity.google
icon: external
variant: minimal
---
import { LinkCard, CardGrid } from '@astrojs/starlight/components';
## Explore the Chapters
<CardGrid>
<LinkCard
title="1. Multi-Agent Squads"
href="/en/subagents/"
description="Build specialized subagent teams to eliminate context rot."
/>
<LinkCard
title="2. Custom Status Line"
href="/en/statusline/"
description="Configure real-time token tracking in your terminal interface."
/>
<LinkCard
title="3. Rules vs. Skills"
href="/en/skills/"
description="Always-on AGENTS.md guardrails vs. on-demand SKILL.md runbooks."
/>
<LinkCard
title="4. Lifecycle Hooks"
href="/en/hooks/"
description="Zero-token deterministic automation: linters, security gates, and tests."
/>
</CardGrid>

Organize each chapter into its own Markdown file inside src/content/docs/.

src/content/docs/
├── commands-shortcuts.md # Root (SI) chapter
├── hooks.md
├── index.mdx # Root (SI) splash landing page
├── mcp-vercel.md
├── overview.md
├── permissions.md
├── rules.md
├── skills.md
├── statusline.md
├── subagents.md
├── starlight-setup.md
├── en/ # English documentation
│ ├── index.mdx
│ ├── overview.md
│ └── starlight-setup.md
└── hr/ # Croatian documentation
├── index.mdx
├── overview.md
└── starlight-setup.md

Each chapter file starts with structured YAML metadata:

---
title: Chapter Title
description: Concise single-sentence summary of the chapter.
sidebar:
order: 1
---

Step 10 - Local Testing & Background Dev Server

Section titled “Step 10 - Local Testing & Background Dev Server”

To preview your documentation during active development, run Astro’s development server.

Terminal window
npm run dev

Open your browser at http://localhost:4321/.

Background Mode (For Agentic & Headless Environments):

Section titled “Background Mode (For Agentic & Headless Environments):”

If running inside an agent terminal, run the server in the background:

Terminal window
npx astro dev --background

Manage the running background server:

  • Check status: npx astro dev status
  • View live logs: npx astro dev logs
  • Stop the server: npx astro dev stop

Step 11 - Local Production Build Verification

Section titled “Step 11 - Local Production Build Verification”

Always test building static HTML assets before deploying to production:

Terminal window
npm run build

Verify the build output:

  1. Static Routes: Confirms generation of all /, /en/, and /hr/ routes.
  2. Optimized Images: Sharp converts and caches assets as modern .webp files.
  3. Pagefind Search Index: Indexes all pages for client-side search without external API dependencies.

Expected successful terminal output:

[build] Complete!
[starlight:pagefind] Building search index with Pagefind...
[starlight:pagefind] Found 34 HTML files.
[starlight:pagefind] Finished building search index.

Step 12 - Automated Production Deployment to Vercel

Section titled “Step 12 - Automated Production Deployment to Vercel”

We deploy the documentation site to Vercel using the official Vercel CLI.

Execute the non-interactive production build and deploy command:

Terminal window
npm run build && npx vercel --prod --yes
  • npm run build: Generates the static production bundle in dist/.
  • npx vercel --prod --yes: Authenticates with your linked Vercel project, uploads static assets, and promotes the deployment directly to the production domain alias (https://antigravity-docs-teal.vercel.app).

Once deployment completes, test the live HTTP response code using curl:

Terminal window
curl -s -o /dev/null -w "%{http_code}\n" https://antigravity-docs-teal.vercel.app/en/starlight-setup/

Expected output:

200

Step 13 - Custom Domain Setup & Automated SSL via Vercel CLI

Section titled “Step 13 - Custom Domain Setup & Automated SSL via Vercel CLI”

Default Vercel deployment URLs (such as https://antigravity-docs-teal.vercel.app) are long and project-specific. For a professional, branded documentation experience, connect a custom domain or subdomain (e.g. antigravity.clawdie.si).

Vercel operates an Anycast Edge Network with automated Let’s Encrypt / ZeroSSL certificate management. You can configure and verify custom domains directly from the terminal without migrating your existing nameservers.

Step 13.1 - Registering the Custom Domain to the Project

Section titled “Step 13.1 - Registering the Custom Domain to the Project”

Run vercel domains add to link the custom domain or subdomain to your project:

Terminal window
npx vercel domains add antigravity.clawdie.si antigravity-docs

This creates an edge routing entry within Vercel’s global CDN network pointing requests for antigravity.clawdie.si to your active production build.

Step 13.2 - Inspecting Required DNS Configuration

Section titled “Step 13.2 - Inspecting Required DNS Configuration”

Query Vercel to inspect the current DNS status and determine the exact records needed:

Terminal window
npx vercel domains verify antigravity.clawdie.si

Vercel checks the public DNS and outputs the recommended record configuration:

  • Option A (A Record - Recommended): Point antigravity to Vercel’s Anycast IP 76.76.21.21.
  • Option B (CNAME Record): Point antigravity to cname.vercel-dns.com. (or your project-specific Vercel CNAME hash).

Step 13.3 - Adding DNS Records at Your Registrar

Section titled “Step 13.3 - Adding DNS Records at Your Registrar”

Log into your DNS provider or registrar control panel where your domain is managed and add the record:

Record Type Name / Host Target / Value TTL
A antigravity 76.76.21.21 3600 (or Auto)
or CNAME antigravity cname.vercel-dns.com. 3600 (or Auto)

Step 13.4 - Automated Verification & SSL Provisioning

Section titled “Step 13.4 - Automated Verification & SSL Provisioning”

Once your DNS changes propagate (typically 1–5 minutes), re-run verification:

Terminal window
npx vercel domains verify antigravity.clawdie.si

Expected output:

Status:
DNS Configuration ✔ Valid Configuration
Project ✔ Verified for antigravity-docs

Vercel automatically triggers an ACME certificate challenge, provisions a free SSL/TLS certificate, and forces HTTPS encryption with HTTP/2 and HSTS enabled.

Step 13.5 - Live Custom Domain Verification

Section titled “Step 13.5 - Live Custom Domain Verification”

Verify that your custom domain is serving with a valid SSL certificate and edge cache:

Terminal window
curl -s -I https://antigravity.clawdie.si/

Expected response headers:

HTTP/2 200
server: Vercel
strict-transport-security: max-age=63072000
x-vercel-cache: HIT

Step 14 - Performance & Optimization Audit (Google PageSpeed Insights)

Section titled “Step 14 - Performance & Optimization Audit (Google PageSpeed Insights)”

To verify the real-world performance, accessibility, and Core Web Vitals of the deployed documentation, we run a live audit using Google PageSpeed Insights.

Real-World Audit Result (Perfect 100% Score)

Section titled “Real-World Audit Result (Perfect 100% Score)”

Running the audit against our live production deployment (https://antigravity.clawdie.si/) achieves a flawless 100/100 across all four audit categories:

Google PageSpeed Insights perfect 100 score on Antigravity Docs

  • Performance: 100 — Near-instant Largest Contentful Paint (LCP) and zero blocking time due to Astro’s zero-JavaScript static HTML default.
  • Accessibility: 100 — Built-in accessible color contrast, compliant ARIA attributes, and keyboard-navigable menus provided by Starlight.
  • Best Practices: 100 — Automatic modern image compression (Sharp .webp), HTTPS enforcement, and zero console errors or deprecated APIs.
  • SEO: 100 — Automated metadata tags, structured sitemaps, semantic headings, and multilingual alternate links (hreflang).
  • Agentic Browsing: 2/2 — Clean, semantic HTML structure perfectly parseable by AI agents and automated crawlers.

You now have a fully operational, multilingual Astro Starlight documentation portal hosted on Vercel. The site includes automatic image compression, full-text client-side search across all languages, customizable CSS layout ordering, and instant automated production deployment.

Next Steps & Resources: