Preskoči na vsebino

Postavitev in uvedba Astro Starlight

Izvorna Markdown koda.md
---
title: Postavitev in uvedba Astro Starlight
description: Celovit vodnik po korakih za namestitev, večjezično konfiguracijo, prilagoditev postavitve in uvedbo dokumentacije Astro Starlight na Vercel.
sidebar:
  order: 10
---

Ta priročnik vas korak za korakom vodi skozi vzpostavitev, nastavitev in uvedbo visoko zmogljive večjezične dokumentacije s pomočjo ogrodij Astro in Starlight ter platforme Vercel. Na koncu boste imeli produkcijsko pripravljeno dokumentacijsko spletno mesto z interaktivno navigacijo, samodejno optimizacijo slik, lokalnim iskalnikom Pagefind in avtomatizirano uvedbo v oblak.

---

## Načela, ki jim sledi ta vodnik

V skladu s standardi tehničnega pisanja:
1. **Brez izpuščanja korakov**: Vsak ukaz, ustvarjena datoteka in konfiguracijska vrstica so natančno navedeni.
2. **Kontekst pred izvedbo**: Tehnični izrazi in arhitekturni razlogi so pojasnjeni pred vnašanjem ukazov.
3. **Prikaži, ne le povej**: Vključene so celotne, delujoče konfiguracijske datoteke namesto nepopolnih izsekov.
4. **Preizkušeno in preverjeno**: Vsa navodila neposredno odražajo preverjeno postavitev, ki poganja prav to spletno mesto.

---

## Predpogoji

Pred začetkom se prepričajte, da imate pripravljeno naslednje:

- **Node.js**: Nameščena različica 20.x ali novejša (`node -v`)
- **Upravitelj paketov**: npm (različica 10.x ali novejša) ali pnpm
- **Račun Vercel**: Brezplačen račun na [vercel.com](https://vercel.com)
- **Vercel CLI**: Dostopen prek `npx vercel` ali nameščen globalno (`npm install -g vercel`)
- **Terminalski dostop**: Lupina v operacijskih sistemih Linux (Debian/Ubuntu), macOS ali Windows WSL2
- **Urejevalnik besedila**: VS Code, Cursor, Nano ali vaš priljubljeni urejevalnik

---

## Terminologija in ograde

V tem vodniku uporabljamo naslednje ograde za spremenljivke:

| Ograda | Pomen | Primer |
| :--- | :--- | :--- |
| `<ime_projekta>` | Ime mape ali repozitorija dokumentacije | `antigravity-docs` |
| `<produkcijski_url>` | Dodeljen produkcijski naslov na Vercelu | `https://antigravity-docs-teal.vercel.app` |
| `<koda_jezika>` | Dvočrkovna koda jezika po standardu ISO | `sl`, `en`, `hr` |
| `<github_repo_url>` | Povezava do vašega Git repozitorija | `https://github.com/google-gemini` |

---

## 1. korak – Nastavitev ogrodja projekta (Scaffolding)

Nov projekt Astro inicializiramo z uradno predlogo Starlight. Da se izognemo interaktivnim terminalskim vprašanjem v avtomatiziranih ali agentnih okoljih, uporabimo neinteraktivna stikala.

V terminalu zaženite:

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

### Pomen stikal:
* `--template starlight`: Inicializira projekt z uradno predlogo in vtičniki Starlight.
* `--yes`: Samodejno sprejme privzete nastavitve brez ustavljanja.
* `--install`: Takoj namesti vse osnovne odvisnosti prek upravitelja npm.
* `--no-git`: Prepreči ustvarjanje praznega repozitorija Git, če že delate znotraj obstoječega repozitorija.
* `--typescript strict`: Omogoči strogo preverjanje tipov TypeScript za varno delo s shemami vsebin.

Premaknite se v mapo projekta:

```bash
cd antigravity-docs
```

---

## 2. korak – Namestitev produkcijskih odvisnosti

Poleg jedra Starlight namestimo knjižnico **Sharp**. Astro jo uporablja za hitro procesiranje, stiskanje in samodejno pretvorbo slik v sodobni format `.webp` med gradnjo.

Namestite Sharp:

```bash
npm install sharp
```

### Celotna datoteka `package.json`

Preverite, da vaša datoteka `package.json` vsebuje ustrezne zagonske skripte in odvisnosti:

```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"
  }
}
```

---

## 3. korak – Nastavitev sheme vsebin (`src/content.config.ts`)

Astro 5 uvaja nov mehanizem Content Layer API. V mapi `src/content.config.ts` definiramo zbirko `docs` in zbirko `i18n` za prevode vmesnika.

Ustvarite datoteko `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() }),
};
```

---

## 4. korak – Glavna konfiguracija (`astro.config.mjs`)

Datoteka `astro.config.mjs` upravlja identiteto spletnega mesta, večjezične poti, navigacijo v stranski vrstici in sloge CSS.

### 4.1 – Večjezična arhitektura usmerjanja
V naši nastavitvi uporabljamo tri jezike:
1. **Koren (`/`)**: Slovenščina (`sl`) je privzeti jezik v korenu.
2. **Angleščina (`/en/`)**: Angleška dokumentacija.
3. **Hrvaščina (`/hr/`)**: Hrvaška dokumentacija.

### 4.2 – Celotna datoteka `astro.config.mjs`

Ustvarite ali posodobite `astro.config.mjs`:

```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. Življenjski kavlji', slug: 'hooks', translations: { en: '8. Lifecycle Hooks', hr: '8. Kuke životnog ciklusa (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' } },
					],
				},
			],
		}),
	],
});
```

---

## 5. korak – Prevod uporabniškega vmesnika (`src/content/i18n/`)

Za slovenski in hrvaški jezik ustvarimo slovarja v mapi `src/content/i18n/` z lokaliziranimi nizi za iskalnik, kazalo vsebine, izbirnik teme in navigacijo.

### 5.1 – Slovenski slovar (`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}}”"
}
```

### 5.2 – Hrvaški slovar (`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}}”"
}
```

---

## 6. korak – Prilagoditev slogov CSS (`src/styles/custom.css`)

V privzeti postavitvi Starlight postavi izbirnik jezika pred ikono GitHub. V naši prilagoditvi izbirnik jezika (`SI`, `EN`, `HR`) premaknemo na sredino med ikono GitHub in stikalom za temo s pomočjo lastnosti `order` v Flexboxu:

```css
/* Postavitev izbire jezika med ikone socialnih omrežij (GitHub) in izbiro teme */
.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;
}

/* Prilagoditev za mobilni meni */
.mobile-preferences .social-icons {
	order: 1;
}

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

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

---

## 7. korak – Vercel komponenta za kopiranje strani in ogled Markdown kode (`src/components/PageTitle.astro`)

Sodobne dokumentacijske strani (kot je uradna dokumentacija Vercel) imajo na vrhu vsake strani interaktivni gumb, ki obiskovalcem in razvijalcem omogoča takojšnje kopiranje izvorne kode Markdown v odložišče ali ogled surove kode v pojavnem oknu (modal dialog).

Starlight omogoča prepis privzetih komponent uporabniškega vmesnika prek polja `components` v `astro.config.mjs`. S prepisom komponente `PageTitle` ohranimo standardni naslov `<h1>`, hkrati pa v glavo dodamo interaktivni gumb za kopiranje in predogled brez potrebe po klicanju zunanjih vmesnikov API ali nepotrebnem klientskem prenosu JavaScripta (zero-hydration).

### Arhitekturne posebnosti:
1. **Branje na strežniku v času gradnje (Build-time extraction)**: V času statične gradnje (`npm run build`) Astrova komponenta prek `Astro.locals.starlightRoute.entry.filePath` in Node.js modula `node:fs` prebere datoteko neposredno z diska ter surovo Markdown kodo vstavi v neaktiven HTML element `<template class="raw-markdown-source">`.
2. **Dostopno modalno okno `<dialog>`**: Za prikaz Markdown kode uporabimo standardni HTML element `<dialog>` s klicem `dialog.showModal()`, ki zagotavlja dostopnost, navigacijo s tipkovnico in preprečuje premikanje ozadja.
3. **Zanesljivo centriranje na sredino zaslona**: Da pojavno okno ne obtiči v zgornjem levem kotu flexbnih elementov, uporabimo fiksno centriranje glede na celotno vidno polje (viewport):
   ```css
   position: fixed;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
   margin: 0;
   width: min(90vw, 52rem);
   max-height: 85vh;
   ```
4. **Večjezična podpora**: Besedila na gumbih in v modalnem oknu se samodejno prilagajajo glede na trenutni jezik strani (`sl`, `en`, `hr`).
5. **Združljivost s prehodi med stranmi (View Transitions)**: Poslušalci dogodkov so registrirani na dogodek `document.addEventListener('astro:page-load', ...)`, kar zagotavlja delovanje gumbov tudi pri navigaciji brez celotnega ponovnega nalaganja strani.

### Implementacija:

Ustvarite komponento `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>
```

---

## 8. korak – Interaktivne vstopne strani (`index.mdx`)

Vsaka vstopna stran uporablja predlogo `splash` in komponente `LinkCard` znotraj `CardGrid` za neposreden skok na poglavja:

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

## Raziščite poglavja vodnika

<CardGrid>
	<LinkCard
		title="1. Večagentne ekipe"
		href="/subagents/"
		description="Sestavite specializirane ekipe podagentov in preprečite nasičenje kontekstnega okna."
	/>
</CardGrid>
```

---

## 9. korak – Struktura poglavij

Vsako poglavje je shranjeno kot samostojna datoteka Markdown v mapi `src/content/docs/`:
* `src/content/docs/<poglavje>.md` (korenski jezik - slovenščina)
* `src/content/docs/en/<poglavje>.md` (angleščina)
* `src/content/docs/hr/<poglavje>.md` (hrvaščina)

Vsaka datoteka vsebuje glavo YAML:
```markdown
---
title: Naslov poglavja
description: Kratek enovrstični povzetek vsebine.
sidebar:
  order: 1
---
```

---

## 10. korak – Lokalni razvojni strežnik

### Interaktivni zagon:
```bash
npm run dev
```

### Zagon v ozadju (za avtomatizirana okolja):
```bash
npx astro dev --background
```
* Preverjanje stanja: `npx astro dev status`
* Dnevniki v živo: `npx astro dev logs`
* Zaustavitev strežnika: `npx astro dev stop`

---

## 11. korak – Lokalno preverjanje gradnje

Pred objavo vedno zaženite produkcijsko gradnjo:

```bash
npm run build
```

Preverite, da se uspešno ustvarijo vse statične strani, optimizirajo slike in zgradi iskalni indeks Pagefind.

---

## 12. korak – Samodejna uvedba na Vercel

Spletno mesto uvedemo v produkcijo z enim samim ukazom prek Vercel CLI:

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

Po zaključku preverite odziv spletnega mesta:
```bash
curl -s -o /dev/null -w "%{http_code}\n" https://antigravity-docs-teal.vercel.app/starlight-setup/
```
Pričakovan izpis: `200`.

---

## 13. korak – Nastavitev lastne domene in samodejni certifikat SSL prek Vercel CLI

Privzeti naslovi Vercela (npr. `https://antigravity-docs-teal.vercel.app`) so dolgi in generični. Za profesionalno podobo dokumentacije povežemo lastno domeno ali poddomeno (npr. `antigravity.clawdie.si`).

Vercel upravlja globalno omrežje Anycast Edge s samodejnim nameščanjem certifikatov Let's Encrypt / ZeroSSL. Lastno domeno lahko povežete in preverite neposredno prek terminala, ne da bi morali premikati obstoječe imenske strežnike (DNS nameservers).

### 13.1 – Povezava lastne domene s projektom

Z ukazom `vercel domains add` domeno dodamo v svoj projekt:

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

To ustvari usmerjevalni vnos v Vercelovem globalnem omrežju CDN, ki zahteve za `antigravity.clawdie.si` preusmeri na aktivno produkcijsko gradnjo.

### 13.2 – Preverjanje zahtevane konfiguracije DNS

Z ukazom za preverjanje pridobimo natančna navodila za vnos DNS zapisov:

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

Vercel preveri javne zapise DNS in prikaže priporočene možnosti:
* **Možnost A (zapis A – priporočeno)**: Usmerite `antigravity` na Vercelov Anycast IP `76.76.21.21`.
* **Možnost B (zapis CNAME)**: Usmerite `antigravity` na `cname.vercel-dns.com.` (ali unikatni razpršeni zapis CNAME).

### 13.3 – Vnos zapisov DNS pri registrarju domene

Prijavite se v nadzorno ploščo ponudnika DNS, kjer gostujete domeno, in dodajte zapis:

| Tip zapisa | Ime / Gostitelj (Host) | Vrednost / Cilj | TTL |
| :--- | :--- | :--- | :--- |
| `A` | `antigravity` | `76.76.21.21` | `3600` (ali Samodejno) |
| *ali* `CNAME` | `antigravity` | `cname.vercel-dns.com.` | `3600` (ali Samodejno) |

### 13.4 – Samodejno preverjanje in izdaja certifikata SSL

Ko se zapisi DNS uveljavijo (običajno v 1 do 5 minutah), ponovno zaženite preverjanje:

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

Pričakovan izpis:
```text
Status:
  DNS Configuration   ✔ Valid Configuration
  Project             ✔ Verified for antigravity-docs
```

Vercel samodejno sproži overitev ACME, izda brezplačen varnostni certifikat SSL/TLS ter uveljavi varno povezavo HTTPS s protokolom HTTP/2 in HSTS.

### 13.5 – Preverjanje lastne domene v živo

Preverite veljavnost certifikata SSL in odziv robnega predpomnilnika z ukazom `curl`:

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

Pričakovane glave odgovora:
```http
HTTP/2 200
server: Vercel
strict-transport-security: max-age=63072000
x-vercel-cache: HIT
```

---

## 14. korak – Preverjanje zmogljivosti in optimizacije (Google PageSpeed Insights)

Za neodvisno preverjanje hitrosti, dostopnosti in metrik Core Web Vitals smo spletno mesto preizkusili z Googlovim uradnim orodjem PageSpeed Insights.

### Rezultat v živo: Popolna ocena 100/100

Preizkus na produkcijski domeni (`https://antigravity.clawdie.si/`) je potrdil **brezhibnih 100 točk v vseh štirih kategorijah**:

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

* **Zmogljivost (Performance): 100** — Bliskovit prikaz največjega vsebinskega elementa (LCP) in ničelno blokiranje zahvaljujoč temu, da Astro privzeto ne pošilja nepotrebne kode JavaScript.
* **Dostopnost (Accessibility): 100** — Vgrajeni visoki barvni kontrasti, standardni atributi ARIA in podpora za bralnike zaslona.
* **Dobre prakse (Best Practices): 100** — Sodobno stiskanje slik (Sharp `.webp`), prisilni HTTPS in nič zastarelih vmesnikov API.
* **Optimizacija za iskalnike (SEO): 100** — Samodejne metapodatkovne oznake, strukturirani zemljevidi spletnega mesta in večjezične oznake `hreflang`.
* **Agentno brskanje (Agentic Browsing): 2/2** — Popolnoma berljiva semantična struktura, prilagojena za bralnike umetne inteligence in iskalne pajke.

---

## Zaključek

Uspešno ste vzpostavili večjezični dokumentacijski portal Astro Starlight, ga prilagodili in objavili na platformi Vercel.

**Naslednji koraki in viri:**
- [Uradna dokumentacija Astro Starlight](https://starlight.astro.build)
- [Astro vodnik po usmerjanju](https://docs.astro.build/en/guides/routing/)
- [Vercel CLI dokumentacija](https://vercel.com/docs/cli)
- [Pregled orodja Antigravity](/overview/)

Ta priročnik vas korak za korakom vodi skozi vzpostavitev, nastavitev in uvedbo visoko zmogljive večjezične dokumentacije s pomočjo ogrodij Astro in Starlight ter platforme Vercel. Na koncu boste imeli produkcijsko pripravljeno dokumentacijsko spletno mesto z interaktivno navigacijo, samodejno optimizacijo slik, lokalnim iskalnikom Pagefind in avtomatizirano uvedbo v oblak.


V skladu s standardi tehničnega pisanja:

  1. Brez izpuščanja korakov: Vsak ukaz, ustvarjena datoteka in konfiguracijska vrstica so natančno navedeni.
  2. Kontekst pred izvedbo: Tehnični izrazi in arhitekturni razlogi so pojasnjeni pred vnašanjem ukazov.
  3. Prikaži, ne le povej: Vključene so celotne, delujoče konfiguracijske datoteke namesto nepopolnih izsekov.
  4. Preizkušeno in preverjeno: Vsa navodila neposredno odražajo preverjeno postavitev, ki poganja prav to spletno mesto.

Pred začetkom se prepričajte, da imate pripravljeno naslednje:

  • Node.js: Nameščena različica 20.x ali novejša (node -v)
  • Upravitelj paketov: npm (različica 10.x ali novejša) ali pnpm
  • Račun Vercel: Brezplačen račun na vercel.com
  • Vercel CLI: Dostopen prek npx vercel ali nameščen globalno (npm install -g vercel)
  • Terminalski dostop: Lupina v operacijskih sistemih Linux (Debian/Ubuntu), macOS ali Windows WSL2
  • Urejevalnik besedila: VS Code, Cursor, Nano ali vaš priljubljeni urejevalnik

V tem vodniku uporabljamo naslednje ograde za spremenljivke:

Ograda Pomen Primer
<ime_projekta> Ime mape ali repozitorija dokumentacije antigravity-docs
<produkcijski_url> Dodeljen produkcijski naslov na Vercelu https://antigravity-docs-teal.vercel.app
<koda_jezika> Dvočrkovna koda jezika po standardu ISO sl, en, hr
<github_repo_url> Povezava do vašega Git repozitorija https://github.com/google-gemini

1. korak – Nastavitev ogrodja projekta (Scaffolding)

Razdelek z naslovom “1. korak – Nastavitev ogrodja projekta (Scaffolding)”

Nov projekt Astro inicializiramo z uradno predlogo Starlight. Da se izognemo interaktivnim terminalskim vprašanjem v avtomatiziranih ali agentnih okoljih, uporabimo neinteraktivna stikala.

V terminalu zaženite:

Terminal window
npm create astro@latest antigravity-docs -- \
--template starlight \
--yes \
--install \
--no-git \
--typescript strict
  • --template starlight: Inicializira projekt z uradno predlogo in vtičniki Starlight.
  • --yes: Samodejno sprejme privzete nastavitve brez ustavljanja.
  • --install: Takoj namesti vse osnovne odvisnosti prek upravitelja npm.
  • --no-git: Prepreči ustvarjanje praznega repozitorija Git, če že delate znotraj obstoječega repozitorija.
  • --typescript strict: Omogoči strogo preverjanje tipov TypeScript za varno delo s shemami vsebin.

Premaknite se v mapo projekta:

Terminal window
cd antigravity-docs

Poleg jedra Starlight namestimo knjižnico Sharp. Astro jo uporablja za hitro procesiranje, stiskanje in samodejno pretvorbo slik v sodobni format .webp med gradnjo.

Namestite Sharp:

Terminal window
npm install sharp

Preverite, da vaša datoteka package.json vsebuje ustrezne zagonske skripte in odvisnosti:

{
"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"
}
}

3. korak – Nastavitev sheme vsebin (src/content.config.ts)

Razdelek z naslovom “3. korak – Nastavitev sheme vsebin (src/content.config.ts)”

Astro 5 uvaja nov mehanizem Content Layer API. V mapi src/content.config.ts definiramo zbirko docs in zbirko i18n za prevode vmesnika.

Ustvarite datoteko 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() }),
};

4. korak – Glavna konfiguracija (astro.config.mjs)

Razdelek z naslovom “4. korak – Glavna konfiguracija (astro.config.mjs)”

Datoteka astro.config.mjs upravlja identiteto spletnega mesta, večjezične poti, navigacijo v stranski vrstici in sloge CSS.

V naši nastavitvi uporabljamo tri jezike:

  1. Koren (/): Slovenščina (sl) je privzeti jezik v korenu.
  2. Angleščina (/en/): Angleška dokumentacija.
  3. Hrvaščina (/hr/): Hrvaška dokumentacija.

Ustvarite ali posodobite astro.config.mjs:

// @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. Življenjski kavlji', slug: 'hooks', translations: { en: '8. Lifecycle Hooks', hr: '8. Kuke životnog ciklusa (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' } },
],
},
],
}),
],
});

5. korak – Prevod uporabniškega vmesnika (src/content/i18n/)

Razdelek z naslovom “5. korak – Prevod uporabniškega vmesnika (src/content/i18n/)”

Za slovenski in hrvaški jezik ustvarimo slovarja v mapi src/content/i18n/ z lokaliziranimi nizi za iskalnik, kazalo vsebine, izbirnik teme in navigacijo.

{
"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}}”"
}
{
"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}}”"
}

6. korak – Prilagoditev slogov CSS (src/styles/custom.css)

Razdelek z naslovom “6. korak – Prilagoditev slogov CSS (src/styles/custom.css)”

V privzeti postavitvi Starlight postavi izbirnik jezika pred ikono GitHub. V naši prilagoditvi izbirnik jezika (SI, EN, HR) premaknemo na sredino med ikono GitHub in stikalom za temo s pomočjo lastnosti order v Flexboxu:

/* Postavitev izbire jezika med ikone socialnih omrežij (GitHub) in izbiro teme */
.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;
}
/* Prilagoditev za mobilni meni */
.mobile-preferences .social-icons {
order: 1;
}
.mobile-preferences starlight-lang-select {
order: 2;
}
.mobile-preferences starlight-theme-select {
order: 3;
}

7. korak – Vercel komponenta za kopiranje strani in ogled Markdown kode (src/components/PageTitle.astro)

Razdelek z naslovom “7. korak – Vercel komponenta za kopiranje strani in ogled Markdown kode (src/components/PageTitle.astro)”

Sodobne dokumentacijske strani (kot je uradna dokumentacija Vercel) imajo na vrhu vsake strani interaktivni gumb, ki obiskovalcem in razvijalcem omogoča takojšnje kopiranje izvorne kode Markdown v odložišče ali ogled surove kode v pojavnem oknu (modal dialog).

Starlight omogoča prepis privzetih komponent uporabniškega vmesnika prek polja components v astro.config.mjs. S prepisom komponente PageTitle ohranimo standardni naslov <h1>, hkrati pa v glavo dodamo interaktivni gumb za kopiranje in predogled brez potrebe po klicanju zunanjih vmesnikov API ali nepotrebnem klientskem prenosu JavaScripta (zero-hydration).

  1. Branje na strežniku v času gradnje (Build-time extraction): V času statične gradnje (npm run build) Astrova komponenta prek Astro.locals.starlightRoute.entry.filePath in Node.js modula node:fs prebere datoteko neposredno z diska ter surovo Markdown kodo vstavi v neaktiven HTML element <template class="raw-markdown-source">.
  2. Dostopno modalno okno <dialog>: Za prikaz Markdown kode uporabimo standardni HTML element <dialog> s klicem dialog.showModal(), ki zagotavlja dostopnost, navigacijo s tipkovnico in preprečuje premikanje ozadja.
  3. Zanesljivo centriranje na sredino zaslona: Da pojavno okno ne obtiči v zgornjem levem kotu flexbnih elementov, uporabimo fiksno centriranje glede na celotno vidno polje (viewport):
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    margin: 0;
    width: min(90vw, 52rem);
    max-height: 85vh;
  4. Večjezična podpora: Besedila na gumbih in v modalnem oknu se samodejno prilagajajo glede na trenutni jezik strani (sl, en, hr).
  5. Združljivost s prehodi med stranmi (View Transitions): Poslušalci dogodkov so registrirani na dogodek document.addEventListener('astro:page-load', ...), kar zagotavlja delovanje gumbov tudi pri navigaciji brez celotnega ponovnega nalaganja strani.

Ustvarite komponento 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>

8. korak – Interaktivne vstopne strani (index.mdx)

Razdelek z naslovom “8. korak – Interaktivne vstopne strani (index.mdx)”

Vsaka vstopna stran uporablja predlogo splash in komponente LinkCard znotraj CardGrid za neposreden skok na poglavja:

import { LinkCard, CardGrid } from '@astrojs/starlight/components';
## Raziščite poglavja vodnika
<CardGrid>
<LinkCard
title="1. Večagentne ekipe"
href="/subagents/"
description="Sestavite specializirane ekipe podagentov in preprečite nasičenje kontekstnega okna."
/>
</CardGrid>

Vsako poglavje je shranjeno kot samostojna datoteka Markdown v mapi src/content/docs/:

  • src/content/docs/<poglavje>.md (korenski jezik - slovenščina)
  • src/content/docs/en/<poglavje>.md (angleščina)
  • src/content/docs/hr/<poglavje>.md (hrvaščina)

Vsaka datoteka vsebuje glavo YAML:

---
title: Naslov poglavja
description: Kratek enovrstični povzetek vsebine.
sidebar:
order: 1
---

Terminal window
npm run dev
Terminal window
npx astro dev --background
  • Preverjanje stanja: npx astro dev status
  • Dnevniki v živo: npx astro dev logs
  • Zaustavitev strežnika: npx astro dev stop

Pred objavo vedno zaženite produkcijsko gradnjo:

Terminal window
npm run build

Preverite, da se uspešno ustvarijo vse statične strani, optimizirajo slike in zgradi iskalni indeks Pagefind.


Spletno mesto uvedemo v produkcijo z enim samim ukazom prek Vercel CLI:

Terminal window
npm run build && npx vercel --prod --yes

Po zaključku preverite odziv spletnega mesta:

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

Pričakovan izpis: 200.


13. korak – Nastavitev lastne domene in samodejni certifikat SSL prek Vercel CLI

Razdelek z naslovom “13. korak – Nastavitev lastne domene in samodejni certifikat SSL prek Vercel CLI”

Privzeti naslovi Vercela (npr. https://antigravity-docs-teal.vercel.app) so dolgi in generični. Za profesionalno podobo dokumentacije povežemo lastno domeno ali poddomeno (npr. antigravity.clawdie.si).

Vercel upravlja globalno omrežje Anycast Edge s samodejnim nameščanjem certifikatov Let’s Encrypt / ZeroSSL. Lastno domeno lahko povežete in preverite neposredno prek terminala, ne da bi morali premikati obstoječe imenske strežnike (DNS nameservers).

Z ukazom vercel domains add domeno dodamo v svoj projekt:

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

To ustvari usmerjevalni vnos v Vercelovem globalnem omrežju CDN, ki zahteve za antigravity.clawdie.si preusmeri na aktivno produkcijsko gradnjo.

Z ukazom za preverjanje pridobimo natančna navodila za vnos DNS zapisov:

Terminal window
npx vercel domains verify antigravity.clawdie.si

Vercel preveri javne zapise DNS in prikaže priporočene možnosti:

  • Možnost A (zapis A – priporočeno): Usmerite antigravity na Vercelov Anycast IP 76.76.21.21.
  • Možnost B (zapis CNAME): Usmerite antigravity na cname.vercel-dns.com. (ali unikatni razpršeni zapis CNAME).

Prijavite se v nadzorno ploščo ponudnika DNS, kjer gostujete domeno, in dodajte zapis:

Tip zapisa Ime / Gostitelj (Host) Vrednost / Cilj TTL
A antigravity 76.76.21.21 3600 (ali Samodejno)
ali CNAME antigravity cname.vercel-dns.com. 3600 (ali Samodejno)

13.4 – Samodejno preverjanje in izdaja certifikata SSL

Razdelek z naslovom “13.4 – Samodejno preverjanje in izdaja certifikata SSL”

Ko se zapisi DNS uveljavijo (običajno v 1 do 5 minutah), ponovno zaženite preverjanje:

Terminal window
npx vercel domains verify antigravity.clawdie.si

Pričakovan izpis:

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

Vercel samodejno sproži overitev ACME, izda brezplačen varnostni certifikat SSL/TLS ter uveljavi varno povezavo HTTPS s protokolom HTTP/2 in HSTS.

Preverite veljavnost certifikata SSL in odziv robnega predpomnilnika z ukazom curl:

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

Pričakovane glave odgovora:

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

14. korak – Preverjanje zmogljivosti in optimizacije (Google PageSpeed Insights)

Razdelek z naslovom “14. korak – Preverjanje zmogljivosti in optimizacije (Google PageSpeed Insights)”

Za neodvisno preverjanje hitrosti, dostopnosti in metrik Core Web Vitals smo spletno mesto preizkusili z Googlovim uradnim orodjem PageSpeed Insights.

Preizkus na produkcijski domeni (https://antigravity.clawdie.si/) je potrdil brezhibnih 100 točk v vseh štirih kategorijah:

Google PageSpeed Insights popolna ocena 100 na Antigravity Docs

  • Zmogljivost (Performance): 100 — Bliskovit prikaz največjega vsebinskega elementa (LCP) in ničelno blokiranje zahvaljujoč temu, da Astro privzeto ne pošilja nepotrebne kode JavaScript.
  • Dostopnost (Accessibility): 100 — Vgrajeni visoki barvni kontrasti, standardni atributi ARIA in podpora za bralnike zaslona.
  • Dobre prakse (Best Practices): 100 — Sodobno stiskanje slik (Sharp .webp), prisilni HTTPS in nič zastarelih vmesnikov API.
  • Optimizacija za iskalnike (SEO): 100 — Samodejne metapodatkovne oznake, strukturirani zemljevidi spletnega mesta in večjezične oznake hreflang.
  • Agentno brskanje (Agentic Browsing): 2/2 — Popolnoma berljiva semantična struktura, prilagojena za bralnike umetne inteligence in iskalne pajke.

Uspešno ste vzpostavili večjezični dokumentacijski portal Astro Starlight, ga prilagodili in objavili na platformi Vercel.

Naslednji koraki in viri: