Preskoči na sadržaj

Postavljanje i implementacija Astro Starlighta

Izvorni Markdown kôd.md
---
title: Postavljanje i implementacija Astro Starlighta
description: Cjelovit vodič korak po korak za instalaciju, višejezičnu konfiguraciju, prilagodbu izgleda i implementaciju Astro Starlight dokumentacije na Vercel.
sidebar:
  order: 10
---

Ovaj vas vodič vodi korak po korak kroz uspostavu, konfiguraciju i implementaciju višejezičnog dokumentacijskog portala visokih performansi pomoću okvira Astro i Starlight te platforme Vercel. Na kraju ćete imati produkcijski spremnu stranicu s interaktivnom navigacijom, automatskom optimizacijom slika, Pagefind pretraživanjem i automatiziranom implementacijom u oblak.

---

## Načela kojima se vodi ovaj priručnik

U skladu sa standardima tehničkog pisanja:
1. **Bez preskakanja koraka**: Svaka naredba, stvaranje datoteke i konfiguracijski redak točno su navedeni.
2. **Kontekst prije izvođenja**: Tehnički pojmovi i arhitektonski razlozi objašnjeni su prije unošenja naredbi.
3. **Pokaži, a ne samo objasni**: Uključene su cjelovite, funkcionalne konfiguracijske datoteke umjesto nepotpunih isječaka.
4. **Provjereno i testirano**: Sve upute izravno odražavaju testiranu konfiguraciju koja pokreće upravo ovu dokumentacijsku web stranicu.

---

## Preduvjeti

Prije početka provjerite imate li spremno sljedeće:

- **Node.js**: Instalirana verzija 20.x ili novija (`node -v`)
- **Upravitelj paketa**: npm (verzija 10.x ili novija) ili pnpm
- **Vercel račun**: Besplatan račun na [vercel.com](https://vercel.com)
- **Vercel CLI**: Dostupan putem `npx vercel` ili instaliran globalno (`npm install -g vercel`)
- **Terminalski pristup**: Ljuska na operacijskim sustavima Linux (Debian/Ubuntu), macOS ili Windows WSL2
- **Uređivač teksta**: VS Code, Cursor, Nano ili vaš omiljeni uređivač

---

## Terminologija i zamjenske oznake

U ovom vodiču koristimo sljedeće zamjenske oznake:

| Oznaka | Značenje | Primjer |
| :--- | :--- | :--- |
| `<naziv_projekta>` | Naziv direktorija ili repozitorija dokumentacije | `antigravity-docs` |
| `<produkcijski_url>` | Dodijeljena produkcijska web adresa na Vercelu | `https://antigravity-docs-teal.vercel.app` |
| `<kod_jezika>` | Dvoslovni ISO identifikator jezika | `sl`, `en`, `hr` |
| `<github_repo_url>` | Poveznica na vaš Git repozitorij | `https://github.com/google-gemini` |

---

## Korak 1 – Postavljanje kostura projekta (Scaffolding)

Novi Astro projekt pokrećemo s pomoću službenog Starlight predloška. Kako bismo izbjegli interaktivna pitanja u terminalu tijekom automatiziranih ili agentskih procesa, prosljeđujemo neinteraktivne zastavice.

U terminalu pokrenite:

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

### Značenje zastavica:
* `--template starlight`: Inicijalizira projekt sa službenim predloškom i dodacima za Starlight.
* `--yes`: Automatski prihvaća sve preporučene zadane vrijednosti.
* `--install`: Odmah pokreće `npm install` za preuzimanje osnovnih ovisnosti.
* `--no-git`: Sprječava stvaranje praznog Git repozitorija ako već radite unutar postojećeg projekta.
* `--typescript strict`: Omogućuje strogu provjeru tipova za stabilan i siguran rad s definicijama sadržaja.

Prijeđite u direktorij projekta:

```bash
cd antigravity-docs
```

---

## Korak 2 – Instalacija produkcijskih ovisnosti

Uz osnovni paket Starlight, instaliramo knjižnicu **Sharp**. Astro koristi Sharp za brzo procesiranje, sažimanje i pretvaranje slika u moderan `.webp` format tijekom izgradnje.

Instalirajte Sharp:

```bash
npm install sharp
```

### Cjelovita datoteka `package.json`

Provjerite sadrži li vaša datoteka `package.json` odgovarajuće skripte i ovisnosti:

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

---

## Korak 3 – Postavljanje sheme sadržaja (`src/content.config.ts`)

Astro 5 koristi Content Layer API. U datoteci `src/content.config.ts` definiramo zbirku `docs` i zbirku `i18n` za lokalizaciju sučelja.

Stvorite datoteku `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() }),
};
```

---

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

Datoteka `astro.config.mjs` upravlja vizualnim identitetom, višejezičnim rutama, navigacijom u bočnoj traci i CSS stilovima.

### 4.1 – Višejezična arhitektura usmjeravanja
U našoj postavci konfiguriramo tri jezika:
1. **Korijen (`/`)**: Slovenski (`sl`) kao zadani jezik u korijenu.
2. **Engleski (`/en/`)**: Engleska dokumentacija.
3. **Hrvatski (`/hr/`)**: Hrvatska dokumentacija.

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

Stvorite ili ažurirajte datoteku `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. 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' } },
					],
				},
			],
		}),
	],
});
```

---

## Korak 5 – Prijevod korisničkog sučelja (`src/content/i18n/`)

U mapi `src/content/i18n/` definiramo rječnike sa specifičnim prijevodima za elemente sučelja (pretraživanje, izbornik teme, preskakanje na sadržaj).

### 5.1 – Slovenski rječnik (`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 – Hrvatski rječnik (`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}}”"
}
```

---

## Korak 6 – Prilagodba CSS stilova (`src/styles/custom.css`)

Starlight prema zadanim postavkama smješta odabir jezika ispred ikona društvenih mreža. U našem dizajnu premještamo odabir jezika (`SI`, `EN`, `HR`) u sredinu, između GitHub ikone i prekidača za temu pomoću svojstva `order` u Flexboxu:

```css
/* Pozicioniranje odabira jezika između ikona društvenih mreža (GitHub) i odabira 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;
}

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

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

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

---

## Korak 7 – Vercel komponenta za kopiranje stranice i prikaz Markdown kôda (`src/components/PageTitle.astro`)

Moderne dokumentacijske platforme (poput službene Vercel dokumentacije) na vrhu svake stranice nude interaktivni padajući izbornik koji posjetiteljima i razvojnim inženjerima omogućuje trenutačno kopiranje izvornog Markdown kôda u međuspremnik ili pregled čistog kôda u skočnom dijaloškom okviru (modal dialog).

Starlight omogućuje nadjačavanje (override) zadanih komponenti korisničkog sučelja putem svojstva `components` u `astro.config.mjs`. Nadjačavanjem komponente `PageTitle` zadržavamo standardni naslov `<h1>`, a istodobno ubacujemo padajući gumb za kopiranje i pregled bez vanjskih API zahtjeva ili klijentskog JavaScript opterećenja (zero-hydration).

### Arhitektonske značajke:
1. **Ekstrakcija na poslužitelju tijekom izgradnje (Build-time extraction)**: Tijekom statičke izgradnje (`npm run build`), Astro komponenta putem `Astro.locals.starlightRoute.entry.filePath` i Node.js modula `node:fs` čita datoteku izravno s diska i umeće izvorni Markdown unutar inertnog HTML elementa `<template class="raw-markdown-source">`.
2. **Pristupačan nativni dijalog `<dialog>`**: Za prikaz kôda koristi se standardni HTML element `<dialog>` s pozivom `dialog.showModal()`, što osigurava nativnu pristupačnost, upravljanje tipkovnicom (Escape za zatvaranje) i blokiranje pomicanja pozadine.
3. **Pravilno centriranje na sredinu zaslona**: Kako bi se izbjeglo fiksiranje dijaloga u gornji lijevi kut unutar flex kontejnera, primjenjuje se robusno centriranje:
   ```css
   position: fixed;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
   margin: 0;
   width: min(90vw, 52rem);
   max-height: 85vh;
   ```
4. **Višejezična lokalizacija natpisa**: Oznake na gumbima i u modalnom prozoru automatski se prilagođavaju aktivnom jeziku stranice (`hr`, `sl`, `en`).
5. **Otpornost na tranzicije stranica (View Transitions)**: Slušači događaja registrirani su na `document.addEventListener('astro:page-load', ...)`, omogućujući besprijekoran rad gumba tijekom klijentske navigacije.

### Implementacija:

Kreirajte komponentu `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-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>
```

---

## Korak 8 – Interaktivne početne stranice (`index.mdx`)

Početne stranice koriste predložak `splash` i komponente `LinkCard` unutar `CardGrid` za izravan skok na poglavlja dokumentacije:

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

## Istražite poglavlja vodiča

<CardGrid>
	<LinkCard
		title="1. Višeagentski timovi"
		href="/hr/subagents/"
		description="Sastavite specijalizirane timove podagenata i spriječite zasićenje kontekstnog prozora."
	/>
</CardGrid>
```

---

## Korak 9 – Struktura poglavlja

Svako poglavlje pohranjeno je kao zasebna Markdown datoteka unutar direktorija `src/content/docs/`:
* `src/content/docs/<poglavlje>.md` (slovenski - korijenski jezik)
* `src/content/docs/en/<poglavlje>.md` (engleski jezik)
* `src/content/docs/hr/<poglavlje>.md` (hrvatski jezik)

Zaglavlje svake datoteke definirano je standardnim YAML formatom:
```markdown
---
title: Naslov poglavlja
description: Kratak sažetak sadržaja u jednoj rečenici.
sidebar:
  order: 1
---
```

---

## Korak 10 – Lokalni razvojni poslužitelj

### Interaktivno pokretanje:
```bash
npm run dev
```

### Pokretanje u pozadini (za automatizirana i agentska okruženja):
```bash
npx astro dev --background
```
* Provjera statusa: `npx astro dev status`
* Pregled zapisa: `npx astro dev logs`
* Zaustavljanje poslužitelja: `npx astro dev stop`

---

## Korak 11 – Provjera produkcijske izgradnje

Prije objave uvijek pokrenite produkcijsku izgradnju:

```bash
npm run build
```

Provjerite jesu li sve statičke rute generirane, slike optimizirane te je li Pagefind indeks pretraživanja uspješno stvoren.

---

## Korak 12 – Automatizirana implementacija na Vercel

Implementaciju na produkciju provodimo jednom jedinom naredbom putem Vercel CLI-ja:

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

Nakon završetka provjerite HTTP status objavljene stranice:
```bash
curl -s -o /dev/null -w "%{http_code}\n" https://antigravity-docs-teal.vercel.app/hr/starlight-setup/
```
Očekivani izlaz: `200`.

---

## Korak 13 – Postavljanje vlastite domene i automatski SSL certifikat putem Vercel CLI-ja

Zadane Vercel URL adrese (poput `https://antigravity-docs-teal.vercel.app`) su dugačke i generičke. Za profesionalan dojam dokumentacije povezujemo vlastitu domenu ili poddomenu (npr. `antigravity.clawdie.si`).

Vercel upravlja globalnom Anycast Edge mrežom s automatskim izdavanjem Let's Encrypt / ZeroSSL certifikata. Vlastitu domenu možete povezati i verificirati izravno putem terminala bez migracije postojećih imenskih poslužitelja (DNS nameservers).

### 13.1 – Povezivanje vlastite domene s projektom

Pomoću naredbe `vercel domains add` domenu dodajemo u svoj projekt:

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

Ovo stvara usmjerivački unos unutar Vercelove globalne CDN mreže koji usmjerava zahtjeve za `antigravity.clawdie.si` na aktivnu produkcijsku izgradnju.

### 13.2 – Provjera potrebne DNS konfiguracije

Provjerite točne parametre koje je potrebno unijeti u DNS zapise:

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

Vercel provjerava javne DNS zapise i prikazuje preporučene opcije:
* **Opcija A (A zapis – preporučeno)**: Usmjerite `antigravity` na Vercelov Anycast IP `76.76.21.21`.
* **Opcija B (CNAME zapis)**: Usmjerite `antigravity` na `cname.vercel-dns.com.` (ili jedinstveni CNAME hash).

### 13.3 – Unos DNS zapisa kod registara domene

Prijavite se u kontrolnu ploču DNS pružatelja gdje upravljate domenom i unesite zapis:

| Vrsta zapisa | Ime / Poslužitelj (Host) | Vrijednost / Cilj | TTL |
| :--- | :--- | :--- | :--- |
| `A` | `antigravity` | `76.76.21.21` | `3600` (ili Automatski) |
| *ili* `CNAME` | `antigravity` | `cname.vercel-dns.com.` | `3600` (ili Automatski) |

### 13.4 – Automatska verifikacija i izdavanje SSL certifikata

Nakon propagacije DNS zapisa (obično 1 do 5 minuta), ponovno pokrenite provjeru:

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

Očekivani izlaz:
```text
Status:
  DNS Configuration   ✔ Valid Configuration
  Project             ✔ Verified for antigravity-docs
```

Vercel automatski pokreće ACME autorizaciju, izdaje besplatan SSL/TLS certifikat i forsira sigurnu HTTPS enkripciju uz HTTP/2 i HSTS.

### 13.5 – Provjera vlastite domene uživo

Potvrdite ispravnost certifikata i odziv rubnog predmeđuspremnika pomoću `curl`:

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

Očekivana zaglavlja odgovora:
```http
HTTP/2 200
server: Vercel
strict-transport-security: max-age=63072000
x-vercel-cache: HIT
```

---

## Korak 14 – Provjera performansi i optimizacije (Google PageSpeed Insights)

Za neovisnu provjeru brzine, pristupačnosti i Core Web Vitals metrika testirali smo stranicu Googleovim službenim alatom PageSpeed Insights.

### Rezultat uživo: Savršenih 100/100 bodova

Testiranje na produkcijskoj domeni (`https://antigravity.clawdie.si/`) potvrdilo je **besprijekornih 100 bodova u sve četiri kategorije**:

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

* **Performanse (Performance): 100** — Gotovo trenutačan prikaz najvećeg sadržajnog elementa (LCP) i nula milisekundi blokiranja zahvaljujući Astrovom pristupu bez klijentskog JavaScripta prema zadanim postavkama.
* **Pristupačnost (Accessibility): 100** — Ugrađeni visoki kontrasti boja, standardni ARIA atributi i podrška za čitače zaslona.
* **Dobre prakse (Best Practices): 100** — Suvremena kompresija slika (Sharp `.webp`), forsirani HTTPS i nula zastarjelih API sučelja.
* **SEO optimizacija: 100** — Automatske metapodatkovne oznake, strukturirane mape web stranice i višejezične oznake `hreflang`.
* **Agentno pregledavanje (Agentic Browsing): 2/2** — Potpuno čitljiva semantička struktura, optimizirana za AI agente i automatizirane indeksere.

---

## Zaključak

Uspješno ste postavili višejezični dokumentacijski portal Astro Starlight, prilagodili ga i objavili na platformi Vercel.

**Sljedeći koraci i resursi:**
- [Službena Astro Starlight dokumentacija](https://starlight.astro.build)
- [Astro vodič kroz usmjeravanje (Routing)](https://docs.astro.build/en/guides/routing/)
- [Vercel CLI dokumentacija](https://vercel.com/docs/cli)
- [Pregled alata Antigravity](/hr/overview/)

Ovaj vas vodič vodi korak po korak kroz uspostavu, konfiguraciju i implementaciju višejezičnog dokumentacijskog portala visokih performansi pomoću okvira Astro i Starlight te platforme Vercel. Na kraju ćete imati produkcijski spremnu stranicu s interaktivnom navigacijom, automatskom optimizacijom slika, Pagefind pretraživanjem i automatiziranom implementacijom u oblak.


U skladu sa standardima tehničkog pisanja:

  1. Bez preskakanja koraka: Svaka naredba, stvaranje datoteke i konfiguracijski redak točno su navedeni.
  2. Kontekst prije izvođenja: Tehnički pojmovi i arhitektonski razlozi objašnjeni su prije unošenja naredbi.
  3. Pokaži, a ne samo objasni: Uključene su cjelovite, funkcionalne konfiguracijske datoteke umjesto nepotpunih isječaka.
  4. Provjereno i testirano: Sve upute izravno odražavaju testiranu konfiguraciju koja pokreće upravo ovu dokumentacijsku web stranicu.

Prije početka provjerite imate li spremno sljedeće:

  • Node.js: Instalirana verzija 20.x ili novija (node -v)
  • Upravitelj paketa: npm (verzija 10.x ili novija) ili pnpm
  • Vercel račun: Besplatan račun na vercel.com
  • Vercel CLI: Dostupan putem npx vercel ili instaliran globalno (npm install -g vercel)
  • Terminalski pristup: Ljuska na operacijskim sustavima Linux (Debian/Ubuntu), macOS ili Windows WSL2
  • Uređivač teksta: VS Code, Cursor, Nano ili vaš omiljeni uređivač

U ovom vodiču koristimo sljedeće zamjenske oznake:

Oznaka Značenje Primjer
<naziv_projekta> Naziv direktorija ili repozitorija dokumentacije antigravity-docs
<produkcijski_url> Dodijeljena produkcijska web adresa na Vercelu https://antigravity-docs-teal.vercel.app
<kod_jezika> Dvoslovni ISO identifikator jezika sl, en, hr
<github_repo_url> Poveznica na vaš Git repozitorij https://github.com/google-gemini

Korak 1 – Postavljanje kostura projekta (Scaffolding)

Odjeljak s naslovom “Korak 1 – Postavljanje kostura projekta (Scaffolding)”

Novi Astro projekt pokrećemo s pomoću službenog Starlight predloška. Kako bismo izbjegli interaktivna pitanja u terminalu tijekom automatiziranih ili agentskih procesa, prosljeđujemo neinteraktivne zastavice.

U terminalu pokrenite:

Terminal window
npm create astro@latest antigravity-docs -- \
--template starlight \
--yes \
--install \
--no-git \
--typescript strict
  • --template starlight: Inicijalizira projekt sa službenim predloškom i dodacima za Starlight.
  • --yes: Automatski prihvaća sve preporučene zadane vrijednosti.
  • --install: Odmah pokreće npm install za preuzimanje osnovnih ovisnosti.
  • --no-git: Sprječava stvaranje praznog Git repozitorija ako već radite unutar postojećeg projekta.
  • --typescript strict: Omogućuje strogu provjeru tipova za stabilan i siguran rad s definicijama sadržaja.

Prijeđite u direktorij projekta:

Terminal window
cd antigravity-docs

Uz osnovni paket Starlight, instaliramo knjižnicu Sharp. Astro koristi Sharp za brzo procesiranje, sažimanje i pretvaranje slika u moderan .webp format tijekom izgradnje.

Instalirajte Sharp:

Terminal window
npm install sharp

Provjerite sadrži li vaša datoteka package.json odgovarajuće skripte i ovisnosti:

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

Korak 3 – Postavljanje sheme sadržaja (src/content.config.ts)

Odjeljak s naslovom “Korak 3 – Postavljanje sheme sadržaja (src/content.config.ts)”

Astro 5 koristi Content Layer API. U datoteci src/content.config.ts definiramo zbirku docs i zbirku i18n za lokalizaciju sučelja.

Stvorite datoteku 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() }),
};

Datoteka astro.config.mjs upravlja vizualnim identitetom, višejezičnim rutama, navigacijom u bočnoj traci i CSS stilovima.

U našoj postavci konfiguriramo tri jezika:

  1. Korijen (/): Slovenski (sl) kao zadani jezik u korijenu.
  2. Engleski (/en/): Engleska dokumentacija.
  3. Hrvatski (/hr/): Hrvatska dokumentacija.

Stvorite ili ažurirajte datoteku 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. 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' } },
],
},
],
}),
],
});

Korak 5 – Prijevod korisničkog sučelja (src/content/i18n/)

Odjeljak s naslovom “Korak 5 – Prijevod korisničkog sučelja (src/content/i18n/)”

U mapi src/content/i18n/ definiramo rječnike sa specifičnim prijevodima za elemente sučelja (pretraživanje, izbornik teme, preskakanje na sadržaj).

5.1 – Slovenski rječnik (src/content/i18n/sl.json)

Odjeljak s naslovom “5.1 – Slovenski rječnik (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}}”"
}

5.2 – Hrvatski rječnik (src/content/i18n/hr.json)

Odjeljak s naslovom “5.2 – Hrvatski rječnik (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}}”"
}

Korak 6 – Prilagodba CSS stilova (src/styles/custom.css)

Odjeljak s naslovom “Korak 6 – Prilagodba CSS stilova (src/styles/custom.css)”

Starlight prema zadanim postavkama smješta odabir jezika ispred ikona društvenih mreža. U našem dizajnu premještamo odabir jezika (SI, EN, HR) u sredinu, između GitHub ikone i prekidača za temu pomoću svojstva order u Flexboxu:

/* Pozicioniranje odabira jezika između ikona društvenih mreža (GitHub) i odabira 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;
}
/* Izgled za mobilni izbornik */
.mobile-preferences .social-icons {
order: 1;
}
.mobile-preferences starlight-lang-select {
order: 2;
}
.mobile-preferences starlight-theme-select {
order: 3;
}

Korak 7 – Vercel komponenta za kopiranje stranice i prikaz Markdown kôda (src/components/PageTitle.astro)

Odjeljak s naslovom “Korak 7 – Vercel komponenta za kopiranje stranice i prikaz Markdown kôda (src/components/PageTitle.astro)”

Moderne dokumentacijske platforme (poput službene Vercel dokumentacije) na vrhu svake stranice nude interaktivni padajući izbornik koji posjetiteljima i razvojnim inženjerima omogućuje trenutačno kopiranje izvornog Markdown kôda u međuspremnik ili pregled čistog kôda u skočnom dijaloškom okviru (modal dialog).

Starlight omogućuje nadjačavanje (override) zadanih komponenti korisničkog sučelja putem svojstva components u astro.config.mjs. Nadjačavanjem komponente PageTitle zadržavamo standardni naslov <h1>, a istodobno ubacujemo padajući gumb za kopiranje i pregled bez vanjskih API zahtjeva ili klijentskog JavaScript opterećenja (zero-hydration).

  1. Ekstrakcija na poslužitelju tijekom izgradnje (Build-time extraction): Tijekom statičke izgradnje (npm run build), Astro komponenta putem Astro.locals.starlightRoute.entry.filePath i Node.js modula node:fs čita datoteku izravno s diska i umeće izvorni Markdown unutar inertnog HTML elementa <template class="raw-markdown-source">.
  2. Pristupačan nativni dijalog <dialog>: Za prikaz kôda koristi se standardni HTML element <dialog> s pozivom dialog.showModal(), što osigurava nativnu pristupačnost, upravljanje tipkovnicom (Escape za zatvaranje) i blokiranje pomicanja pozadine.
  3. Pravilno centriranje na sredinu zaslona: Kako bi se izbjeglo fiksiranje dijaloga u gornji lijevi kut unutar flex kontejnera, primjenjuje se robusno centriranje:
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    margin: 0;
    width: min(90vw, 52rem);
    max-height: 85vh;
  4. Višejezična lokalizacija natpisa: Oznake na gumbima i u modalnom prozoru automatski se prilagođavaju aktivnom jeziku stranice (hr, sl, en).
  5. Otpornost na tranzicije stranica (View Transitions): Slušači događaja registrirani su na document.addEventListener('astro:page-load', ...), omogućujući besprijekoran rad gumba tijekom klijentske navigacije.

Kreirajte komponentu 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-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>

Korak 8 – Interaktivne početne stranice (index.mdx)

Odjeljak s naslovom “Korak 8 – Interaktivne početne stranice (index.mdx)”

Početne stranice koriste predložak splash i komponente LinkCard unutar CardGrid za izravan skok na poglavlja dokumentacije:

import { LinkCard, CardGrid } from '@astrojs/starlight/components';
## Istražite poglavlja vodiča
<CardGrid>
<LinkCard
title="1. Višeagentski timovi"
href="/hr/subagents/"
description="Sastavite specijalizirane timove podagenata i spriječite zasićenje kontekstnog prozora."
/>
</CardGrid>

Svako poglavlje pohranjeno je kao zasebna Markdown datoteka unutar direktorija src/content/docs/:

  • src/content/docs/<poglavlje>.md (slovenski - korijenski jezik)
  • src/content/docs/en/<poglavlje>.md (engleski jezik)
  • src/content/docs/hr/<poglavlje>.md (hrvatski jezik)

Zaglavlje svake datoteke definirano je standardnim YAML formatom:

---
title: Naslov poglavlja
description: Kratak sažetak sadržaja u jednoj rečenici.
sidebar:
order: 1
---

Terminal window
npm run dev

Pokretanje u pozadini (za automatizirana i agentska okruženja):

Odjeljak s naslovom “Pokretanje u pozadini (za automatizirana i agentska okruženja):”
Terminal window
npx astro dev --background
  • Provjera statusa: npx astro dev status
  • Pregled zapisa: npx astro dev logs
  • Zaustavljanje poslužitelja: npx astro dev stop

Prije objave uvijek pokrenite produkcijsku izgradnju:

Terminal window
npm run build

Provjerite jesu li sve statičke rute generirane, slike optimizirane te je li Pagefind indeks pretraživanja uspješno stvoren.


Korak 12 – Automatizirana implementacija na Vercel

Odjeljak s naslovom “Korak 12 – Automatizirana implementacija na Vercel”

Implementaciju na produkciju provodimo jednom jedinom naredbom putem Vercel CLI-ja:

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

Nakon završetka provjerite HTTP status objavljene stranice:

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

Očekivani izlaz: 200.


Korak 13 – Postavljanje vlastite domene i automatski SSL certifikat putem Vercel CLI-ja

Odjeljak s naslovom “Korak 13 – Postavljanje vlastite domene i automatski SSL certifikat putem Vercel CLI-ja”

Zadane Vercel URL adrese (poput https://antigravity-docs-teal.vercel.app) su dugačke i generičke. Za profesionalan dojam dokumentacije povezujemo vlastitu domenu ili poddomenu (npr. antigravity.clawdie.si).

Vercel upravlja globalnom Anycast Edge mrežom s automatskim izdavanjem Let’s Encrypt / ZeroSSL certifikata. Vlastitu domenu možete povezati i verificirati izravno putem terminala bez migracije postojećih imenskih poslužitelja (DNS nameservers).

Pomoću naredbe vercel domains add domenu dodajemo u svoj projekt:

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

Ovo stvara usmjerivački unos unutar Vercelove globalne CDN mreže koji usmjerava zahtjeve za antigravity.clawdie.si na aktivnu produkcijsku izgradnju.

Provjerite točne parametre koje je potrebno unijeti u DNS zapise:

Terminal window
npx vercel domains verify antigravity.clawdie.si

Vercel provjerava javne DNS zapise i prikazuje preporučene opcije:

  • Opcija A (A zapis – preporučeno): Usmjerite antigravity na Vercelov Anycast IP 76.76.21.21.
  • Opcija B (CNAME zapis): Usmjerite antigravity na cname.vercel-dns.com. (ili jedinstveni CNAME hash).

Prijavite se u kontrolnu ploču DNS pružatelja gdje upravljate domenom i unesite zapis:

Vrsta zapisa Ime / Poslužitelj (Host) Vrijednost / Cilj TTL
A antigravity 76.76.21.21 3600 (ili Automatski)
ili CNAME antigravity cname.vercel-dns.com. 3600 (ili Automatski)

13.4 – Automatska verifikacija i izdavanje SSL certifikata

Odjeljak s naslovom “13.4 – Automatska verifikacija i izdavanje SSL certifikata”

Nakon propagacije DNS zapisa (obično 1 do 5 minuta), ponovno pokrenite provjeru:

Terminal window
npx vercel domains verify antigravity.clawdie.si

Očekivani izlaz:

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

Vercel automatski pokreće ACME autorizaciju, izdaje besplatan SSL/TLS certifikat i forsira sigurnu HTTPS enkripciju uz HTTP/2 i HSTS.

Potvrdite ispravnost certifikata i odziv rubnog predmeđuspremnika pomoću curl:

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

Očekivana zaglavlja odgovora:

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

Korak 14 – Provjera performansi i optimizacije (Google PageSpeed Insights)

Odjeljak s naslovom “Korak 14 – Provjera performansi i optimizacije (Google PageSpeed Insights)”

Za neovisnu provjeru brzine, pristupačnosti i Core Web Vitals metrika testirali smo stranicu Googleovim službenim alatom PageSpeed Insights.

Testiranje na produkcijskoj domeni (https://antigravity.clawdie.si/) potvrdilo je besprijekornih 100 bodova u sve četiri kategorije:

Google PageSpeed Insights savršenih 100 na Antigravity Docs

  • Performanse (Performance): 100 — Gotovo trenutačan prikaz najvećeg sadržajnog elementa (LCP) i nula milisekundi blokiranja zahvaljujući Astrovom pristupu bez klijentskog JavaScripta prema zadanim postavkama.
  • Pristupačnost (Accessibility): 100 — Ugrađeni visoki kontrasti boja, standardni ARIA atributi i podrška za čitače zaslona.
  • Dobre prakse (Best Practices): 100 — Suvremena kompresija slika (Sharp .webp), forsirani HTTPS i nula zastarjelih API sučelja.
  • SEO optimizacija: 100 — Automatske metapodatkovne oznake, strukturirane mape web stranice i višejezične oznake hreflang.
  • Agentno pregledavanje (Agentic Browsing): 2/2 — Potpuno čitljiva semantička struktura, optimizirana za AI agente i automatizirane indeksere.

Uspješno ste postavili višejezični dokumentacijski portal Astro Starlight, prilagodili ga i objavili na platformi Vercel.

Sljedeći koraci i resursi: