Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik21 min czytania

Astro

Astro to nowoczesny framework webowy z Island Architecture, który domyślnie wysyła zero JavaScript, zapewniając błyskawiczne ładowanie stron.

Astro - Kompletny Przewodnik po Island Architecture

Czym jest Astro?

Astro to nowoczesny framework webowy, który rewolucjonizuje sposób budowania stron internetowych dzięki Island Architecture. Główna filozofia Astro to "ship less JavaScript" - domyślnie strony Astro nie wysyłają żadnego JavaScriptu do przeglądarki, co przekłada się na błyskawiczne ładowanie i doskonałe wyniki Core Web Vitals.

Astro pozwala używać ulubionych frameworków UI (React, Vue, Svelte, Solid) jako izolowanych wysp interaktywności w morzu statycznego HTML. Dzięki temu możesz mieć interaktywny slider w React i formularz w Vue na tej samej stronie, ładując JavaScript tylko tam, gdzie jest naprawdę potrzebny.

Dlaczego Astro?

Kluczowe zalety

  1. Zero JavaScript domyślnie - Strony są statycznym HTML, JS ładowany na żądanie
  2. Island Architecture - Izolowane komponenty interaktywne
  3. Framework-agnostic - Używaj React, Vue, Svelte, Solid razem
  4. Content Collections - Type-safe zarządzanie treścią
  5. Doskonałe SEO - Statyczny HTML = idealne indeksowanie
  6. View Transitions - Płynne przejścia między stronami
  7. Server Islands - Dynamiczne wyspy renderowane na serwerze

Astro vs inne frameworki

CechaAstroNext.jsGatsbySvelteKit
Domyślny JS0 KB~80 KB+~50 KB+~15 KB
Multi-frameworkTakTylko ReactTylko ReactTylko Svelte
Content focusTakNieTakNie
Island ArchitectureNatywneNieNieNie
Learning curveNiskaŚredniaWysokaŚrednia
Build timeSzybkiŚredniWolnySzybki

Kiedy wybrać Astro?

  • Blogi i dokumentacja - Idealne dla stron content-first
  • Landing pages - Maksymalna wydajność i SEO
  • Portfolia - Szybkie, responsywne strony
  • E-commerce (strony produktowe) - Statyczne strony + dynamiczne koszyki
  • Marketing sites - Doskonałe Core Web Vitals
  • Hybrydowe aplikacje - Głównie statyczne z elementami interaktywnymi

Kiedy rozważyć alternatywy?

  • Wysoce interaktywne SPA - Next.js lub SvelteKit lepsze
  • Real-time aplikacje - Framework z pełnym hydration
  • Admin panele - React/Vue z pełnym client-side
  • Aplikacje typu dashboard - Gdzie wszystko jest interaktywne

Instalacja i konfiguracja

Tworzenie projektu

Code
Bash
# npm
npm create astro@latest

# pnpm
pnpm create astro@latest

# yarn
yarn create astro

# Przejdź do folderu
cd my-astro-project

# Uruchom dev server
npm run dev

Struktura projektu

Code
TEXT
my-astro-project/
├── astro.config.mjs     # Konfiguracja Astro
├── package.json
├── public/              # Statyczne assety (kopiowane bez zmian)
│   ├── favicon.ico
│   └── robots.txt
├── src/
│   ├── components/      # Komponenty Astro i innych frameworków
│   ├── content/         # Content Collections (MD/MDX)
│   │   ├── config.ts    # Schema collections
│   │   └── blog/        # Przykładowa kolekcja
│   ├── layouts/         # Layout components
│   ├── pages/           # Strony (routing oparty na plikach)
│   └── styles/          # Globalne style
└── tsconfig.json

Konfiguracja astro.config.mjs

astro.config.mjs
JavaScript
// astro.config.mjs
import { defineConfig } from 'astro/config'
import react from '@astrojs/react'
import vue from '@astrojs/vue'
import tailwind from '@astrojs/tailwind'
import mdx from '@astrojs/mdx'
import sitemap from '@astrojs/sitemap'

export default defineConfig({
  // Adres produkcyjny (dla sitemap i canonical URLs)
  site: 'https://example.com',

  // Integracje
  integrations: [
    react(),
    vue(),
    tailwind(),
    mdx(),
    sitemap(),
  ],

  // Output mode: 'static' (domyślne), 'server', 'hybrid'
  output: 'static',

  // Konfiguracja buildu
  build: {
    // Inline stylów poniżej tego rozmiaru
    inlineStylesheets: 'auto',
  },

  // Konfiguracja Vite
  vite: {
    // Opcje Vite
  },

  // Przekierowania
  redirects: {
    '/old-page': '/new-page',
    '/blog/[...slug]': '/articles/[...slug]',
  },

  // Prefetch links
  prefetch: {
    prefetchAll: true,
    defaultStrategy: 'hover',
  },
})

Komponenty Astro

Podstawowy komponent

Code
ASTRO
---
// src/components/Card.astro
// Frontmatter - JavaScript wykonywany na serwerze

interface Props {
  title: string
  description: string
  href?: string
}

const { title, description, href = '#' } = Astro.props
---

<a href={href} class="card">
  <h2>{title}</h2>
  <p>{description}</p>
  <slot />
</a>

<style>
  /* Scoped styles - tylko dla tego komponentu */
  .card {
    display: block;
    padding: 1.5rem;
    border-radius: 8px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    text-decoration: none;
    transition: transform 0.2s;
  }

  .card:hover {
    transform: translateY(-4px);
  }

  h2 {
    margin: 0 0 0.5rem;
    font-size: 1.25rem;
  }

  p {
    margin: 0;
    opacity: 0.9;
  }
</style>

Sloty (named slots)

Code
ASTRO
---
// src/components/Layout.astro
interface Props {
  title: string
}

const { title } = Astro.props
---

<!DOCTYPE html>
<html lang="pl">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>{title}</title>
    <slot name="head" />
  </head>
  <body>
    <header>
      <slot name="header">
        <!-- Domyślna treść jeśli slot nie jest użyty -->
        <nav>Default Navigation</nav>
      </slot>
    </header>

    <main>
      <slot />
    </main>

    <footer>
      <slot name="footer" />
    </footer>
  </body>
</html>
Code
ASTRO
---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro'
---

<Layout title="Strona główna">
  <Fragment slot="head">
    <meta name="description" content="Opis strony" />
  </Fragment>

  <nav slot="header">
    <a href="/">Home</a>
    <a href="/about">About</a>
  </nav>

  <!-- Główna treść (domyślny slot) -->
  <h1>Witaj w Astro!</h1>
  <p>To jest główna treść strony.</p>

  <p slot="footer">© 2024 My Site</p>
</Layout>

Dynamiczne wyrażenia

Code
ASTRO
---
const items = ['Jabłko', 'Banan', 'Pomarańcza']
const isLoggedIn = true
const user = { name: 'Jan', role: 'admin' }
---

<!-- Wyrażenia JavaScript -->
<p>Suma: {1 + 1}</p>
<p>Nazwa: {user.name.toUpperCase()}</p>

<!-- Warunkowe renderowanie -->
{isLoggedIn ? (
  <p>Witaj, {user.name}!</p>
) : (
  <p>Zaloguj się</p>
)}

{isLoggedIn && <button>Wyloguj</button>}

<!-- Iteracja -->
<ul>
  {items.map((item) => (
    <li>{item}</li>
  ))}
</ul>

<!-- Dynamiczne atrybuty -->
<div class={`card ${user.role === 'admin' ? 'admin-card' : ''}`}>
  <a href={`/users/${user.name.toLowerCase()}`}>Profil</a>
</div>

<!-- Spread props -->
<Component {...user} />

Island Architecture

Dyrektywy klienta

Astro używa dyrektyw client:* do określenia, kiedy komponent powinien zostać zhydratowany:

Code
ASTRO
---
import ReactCounter from '../components/Counter.jsx'
import VueCarousel from '../components/Carousel.vue'
import SvelteForm from '../components/Form.svelte'
---

<!-- client:load - Hydratuj natychmiast po załadowaniu strony -->
<!-- Użyj dla: krytycznych interaktywnych elementów -->
<ReactCounter client:load />

<!-- client:idle - Hydratuj gdy przeglądarka jest bezczynna -->
<!-- Użyj dla: mniej ważnych komponentów -->
<VueCarousel client:idle />

<!-- client:visible - Hydratuj gdy komponent jest widoczny -->
<!-- Użyj dla: elementów poniżej fold, lazy loading -->
<SvelteForm client:visible />

<!-- client:visible z rootMargin -->
<HeavyComponent client:visible={{ rootMargin: '200px' }} />

<!-- client:media - Hydratuj na określonych media queries -->
<!-- Użyj dla: mobile-only interakcji -->
<MobileMenu client:media="(max-width: 768px)" />

<!-- client:only - Renderuj TYLKO na kliencie (bez SSR) -->
<!-- Użyj dla: komponentów używających browser APIs -->
<ThreeScene client:only="react" />

<!-- Bez dyrektywy - statyczny HTML, zero JS -->
<StaticCard title="Nie interaktywne" />

Multi-framework components

Code
ASTRO
---
// Używaj React, Vue, Svelte na jednej stronie!
import ReactButton from '../components/Button.jsx'
import VueModal from '../components/Modal.vue'
import SvelteToast from '../components/Toast.svelte'
import SolidCounter from '../components/Counter.tsx' // Solid
---

<div class="app">
  <!-- React component -->
  <ReactButton client:load onClick={() => console.log('React!')}>
    Click me (React)
  </ReactButton>

  <!-- Vue component -->
  <VueModal client:idle title="Vue Modal">
    <p>Content from Astro</p>
  </VueModal>

  <!-- Svelte component -->
  <SvelteToast client:visible message="Hello from Svelte!" />

  <!-- Solid component -->
  <SolidCounter client:load initialCount={5} />
</div>

Instalacja integracji frameworków

Code
Bash
# React
npx astro add react

# Vue
npx astro add vue

# Svelte
npx astro add svelte

# Solid
npx astro add solid-js

# Preact (lżejsza alternatywa dla React)
npx astro add preact

# Lit (Web Components)
npx astro add lit

Przykład: React component

TSsrc/components/Counter.tsx
TypeScript
// src/components/Counter.tsx
import { useState } from 'react'

interface CounterProps {
  initialCount?: number
  label?: string
}

export default function Counter({ initialCount = 0, label = 'Count' }: CounterProps) {
  const [count, setCount] = useState(initialCount)

  return (
    <div className="counter">
      <p>{label}: {count}</p>
      <button onClick={() => setCount(c => c - 1)}>-</button>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  )
}

Przykład: Vue component

src/components/Accordion.vue
VUE
<!-- src/components/Accordion.vue -->
<template>
  <div class="accordion">
    <button @click="isOpen = !isOpen" class="accordion-header">
      {{ title }}
      <span>{{ isOpen ? '▲' : '▼' }}</span>
    </button>
    <div v-show="isOpen" class="accordion-content">
      <slot />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

defineProps<{
  title: string
}>()

const isOpen = ref(false)
</script>

<style scoped>
.accordion-header {
  width: 100%;
  padding: 1rem;
  background: #f0f0f0;
  border: none;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
}

.accordion-content {
  padding: 1rem;
  border: 1px solid #e0e0e0;
}
</style>

Content Collections

Definiowanie kolekcji

TSsrc/content/config.ts
TypeScript
// src/content/config.ts
import { defineCollection, z } from 'astro:content'

// Blog collection
const blogCollection = defineCollection({
  type: 'content', // MD/MDX files
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.date(),
    updatedDate: z.date().optional(),
    author: z.string().default('Admin'),
    image: z.object({
      url: z.string(),
      alt: z.string(),
    }).optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
  }),
})

// Authors collection
const authorsCollection = defineCollection({
  type: 'data', // JSON/YAML files
  schema: z.object({
    name: z.string(),
    email: z.string().email(),
    avatar: z.string().url(),
    bio: z.string(),
    social: z.object({
      twitter: z.string().optional(),
      github: z.string().optional(),
    }).optional(),
  }),
})

// Products collection z obrazami
const productsCollection = defineCollection({
  type: 'content',
  schema: ({ image }) => z.object({
    name: z.string(),
    price: z.number(),
    description: z.string(),
    // Optymalizowane obrazy przez Astro
    cover: image(),
    gallery: z.array(image()).optional(),
  }),
})

export const collections = {
  blog: blogCollection,
  authors: authorsCollection,
  products: productsCollection,
}

Tworzenie treści

Code
Markdown
---
# src/content/blog/pierwszy-post.md
title: "Mój pierwszy post w Astro"
description: "Wprowadzenie do Astro i Island Architecture"
pubDate: 2024-01-15
author: "Jan Kowalski"
image:
  url: "/images/astro-intro.jpg"
  alt: "Astro logo"
tags: ["astro", "webdev", "javascript"]
---

# Wprowadzenie

Astro to niesamowity framework...

## Dlaczego Astro?

Island Architecture pozwala na...

```javascript
// Przykład kodu
const greeting = 'Hello, Astro!'
console.log(greeting)

Podsumowanie

To był krótki wstęp do Astro.

Code
TEXT
### Pobieranie treści

```astro
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content'
import Layout from '../../layouts/Layout.astro'
import BlogCard from '../../components/BlogCard.astro'

// Pobierz wszystkie posty (nie drafty)
const posts = await getCollection('blog', ({ data }) => {
  return data.draft !== true
})

// Sortuj po dacie
const sortedPosts = posts.sort(
  (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
)
---

<Layout title="Blog">
  <h1>Blog</h1>

  <div class="posts-grid">
    {sortedPosts.map((post) => (
      <BlogCard
        title={post.data.title}
        description={post.data.description}
        pubDate={post.data.pubDate}
        href={`/blog/${post.slug}`}
        image={post.data.image}
      />
    ))}
  </div>
</Layout>

Dynamiczne strony dla kolekcji

Code
ASTRO
---
// src/pages/blog/[...slug].astro
import { getCollection, type CollectionEntry } from 'astro:content'
import Layout from '../../layouts/Layout.astro'

// Generuj statyczne ścieżki dla wszystkich postów
export async function getStaticPaths() {
  const posts = await getCollection('blog')

  return posts.map((post) => ({
    params: { slug: post.slug },
    props: { post },
  }))
}

interface Props {
  post: CollectionEntry<'blog'>
}

const { post } = Astro.props

// Renderuj content do HTML
const { Content, headings, remarkPluginFrontmatter } = await post.render()
---

<Layout title={post.data.title}>
  <article>
    <header>
      <h1>{post.data.title}</h1>
      <p>Opublikowano: {post.data.pubDate.toLocaleDateString('pl-PL')}</p>
      {post.data.author && <p>Autor: {post.data.author}</p>}
    </header>

    <!-- Table of Contents -->
    <nav class="toc">
      <h2>Spis treści</h2>
      <ul>
        {headings.map((heading) => (
          <li style={`margin-left: ${(heading.depth - 2) * 20}px`}>
            <a href={`#${heading.slug}`}>{heading.text}</a>
          </li>
        ))}
      </ul>
    </nav>

    <!-- Wyrenderowana treść MD/MDX -->
    <Content />

    <!-- Tagi -->
    <footer>
      <div class="tags">
        {post.data.tags.map((tag) => (
          <a href={`/tags/${tag}`} class="tag">#{tag}</a>
        ))}
      </div>
    </footer>
  </article>
</Layout>

MDX Integration

Instalacja

Code
Bash
npx astro add mdx

MDX z komponentami

Code
MDX
---
// src/content/blog/interaktywny-post.mdx
title: "Interaktywny post z MDX"
description: "Post z osadzonymi komponentami React"
pubDate: 2024-02-01
---

import Counter from '../../components/Counter'
import { Alert } from '../../components/Alert'
import CodeBlock from '../../components/CodeBlock'

# Interaktywny post

Ten post zawiera interaktywne komponenty!

## Licznik

Oto interaktywny licznik (React):

<Counter client:visible initialCount={10} />

## Alerty

<Alert type="info">
  To jest informacja dla czytelnika.
</Alert>

<Alert type="warning">
  Uwaga! To ważne ostrzeżenie.
</Alert>

## Kod z podświetlaniem

<CodeBlock lang="typescript" filename="example.ts">
{`interface User {
  name: string
  email: string
}

const user: User = {
  name: 'Jan',
  email: 'jan@example.com'
}`}
</CodeBlock>

## Normalna treść Markdown

Możesz mieszać **Markdown** z komponentami:

- Lista punktów
- Działa normalnie
- Z *formatowaniem*

> Cytaty też działają!

Konfiguracja MDX

astro.config.mjs
JavaScript
// astro.config.mjs
import { defineConfig } from 'astro/config'
import mdx from '@astrojs/mdx'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
import rehypePrismPlus from 'rehype-prism-plus'

export default defineConfig({
  integrations: [
    mdx({
      // Remark plugins (markdown processing)
      remarkPlugins: [
        remarkGfm,     // GitHub Flavored Markdown
        remarkMath,    // Math expressions
      ],
      // Rehype plugins (HTML processing)
      rehypePlugins: [
        rehypeKatex,       // Math rendering
        rehypePrismPlus,   // Code syntax highlighting
      ],
      // Shiki syntax highlighting (built-in)
      shikiConfig: {
        theme: 'dracula',
        wrap: true,
      },
    }),
  ],
})

Routing

File-based routing

Code
TEXT
src/pages/
├── index.astro          → /
├── about.astro          → /about
├── contact.astro        → /contact
├── blog/
│   ├── index.astro      → /blog
│   └── [slug].astro     → /blog/:slug
├── products/
│   ├── index.astro      → /products
│   └── [...path].astro  → /products/* (catch-all)
└── api/
    └── users.ts         → /api/users (API endpoint)

Dynamiczne routing

Code
ASTRO
---
// src/pages/blog/[slug].astro
export function getStaticPaths() {
  return [
    { params: { slug: 'post-1' }, props: { title: 'Post 1' } },
    { params: { slug: 'post-2' }, props: { title: 'Post 2' } },
    { params: { slug: 'post-3' }, props: { title: 'Post 3' } },
  ]
}

const { slug } = Astro.params
const { title } = Astro.props
---

<h1>{title}</h1>
<p>Slug: {slug}</p>

Rest parameters (catch-all)

Code
ASTRO
---
// src/pages/docs/[...path].astro
export function getStaticPaths() {
  return [
    { params: { path: undefined } },        // /docs
    { params: { path: 'getting-started' } }, // /docs/getting-started
    { params: { path: 'api/reference' } },   // /docs/api/reference
  ]
}

const { path } = Astro.params
// path może być: undefined, 'getting-started', 'api/reference'
---

Pagination

Code
ASTRO
---
// src/pages/blog/[page].astro
import type { GetStaticPaths, Page } from 'astro'
import { getCollection } from 'astro:content'

export const getStaticPaths = (async ({ paginate }) => {
  const posts = await getCollection('blog')
  const sortedPosts = posts.sort(
    (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
  )

  // 10 postów na stronę
  return paginate(sortedPosts, { pageSize: 10 })
}) satisfies GetStaticPaths

interface Props {
  page: Page
}

const { page } = Astro.props
---

<h1>Blog - Strona {page.currentPage}</h1>

{page.data.map((post) => (
  <article>
    <h2><a href={`/blog/${post.slug}`}>{post.data.title}</a></h2>
  </article>
))}

<!-- Nawigacja paginacji -->
<nav>
  {page.url.prev && <a href={page.url.prev}>← Poprzednia</a>}
  <span>Strona {page.currentPage} z {page.lastPage}</span>
  {page.url.next && <a href={page.url.next}>Następna →</a>}
</nav>

Server-Side Rendering (SSR)

Włączenie SSR

astro.config.mjs
JavaScript
// astro.config.mjs
import { defineConfig } from 'astro/config'
import node from '@astrojs/node'

export default defineConfig({
  output: 'server', // lub 'hybrid'
  adapter: node({
    mode: 'standalone', // lub 'middleware'
  }),
})

Tryb hybrydowy

astro.config.mjs
JavaScript
// astro.config.mjs
export default defineConfig({
  output: 'hybrid', // Domyślnie statyczne, opt-in do SSR
  adapter: node({ mode: 'standalone' }),
})
Code
ASTRO
---
// src/pages/static-page.astro
// Ta strona będzie statyczna (domyślnie w hybrid)
---

<h1>Statyczna strona</h1>
Code
ASTRO
---
// src/pages/dynamic-page.astro
// Wymusz SSR dla tej strony
export const prerender = false

// Teraz możesz używać dynamicznych danych
const response = await fetch('https://api.example.com/data')
const data = await response.json()
---

<h1>Dynamiczna strona</h1>
<p>Dane: {JSON.stringify(data)}</p>

Adaptery dla różnych platform

JSNode.js
JavaScript
# Node.js
npx astro add node

# Vercel
npx astro add vercel

# Netlify
npx astro add netlify

# Cloudflare Workers
npx astro add cloudflare

# Deno
npx astro add deno

API Endpoints

TSsrc/pages/api/users.ts
TypeScript
// src/pages/api/users.ts
import type { APIRoute } from 'astro'

export const GET: APIRoute = async ({ params, request }) => {
  const users = [
    { id: 1, name: 'Jan' },
    { id: 2, name: 'Anna' },
  ]

  return new Response(JSON.stringify(users), {
    status: 200,
    headers: {
      'Content-Type': 'application/json',
    },
  })
}

export const POST: APIRoute = async ({ request }) => {
  const body = await request.json()

  // Walidacja, zapis do bazy...

  return new Response(JSON.stringify({ success: true, data: body }), {
    status: 201,
    headers: {
      'Content-Type': 'application/json',
    },
  })
}
TSsrc/pages/api/users/[id].ts
TypeScript
// src/pages/api/users/[id].ts
import type { APIRoute } from 'astro'

export const GET: APIRoute = async ({ params }) => {
  const { id } = params

  // Pobierz usera z bazy...
  const user = { id, name: `User ${id}` }

  return new Response(JSON.stringify(user), {
    status: 200,
    headers: { 'Content-Type': 'application/json' },
  })
}

export const DELETE: APIRoute = async ({ params }) => {
  const { id } = params

  // Usuń usera...

  return new Response(null, { status: 204 })
}

View Transitions

Podstawowe View Transitions

Code
ASTRO
---
// src/layouts/Layout.astro
import { ViewTransitions } from 'astro:transitions'
---

<!DOCTYPE html>
<html lang="pl">
  <head>
    <meta charset="UTF-8" />
    <title>{title}</title>
    <!-- Włącz View Transitions -->
    <ViewTransitions />
  </head>
  <body>
    <slot />
  </body>
</html>

Nazywanie elementów dla transitions

Code
ASTRO
---
// Element z transition name
---

<header transition:name="header">
  Stały header
</header>

<main transition:name="main-content">
  <!-- Treść która się zmienia -->
  <slot />
</main>

<!-- Obrazek z płynną animacją -->
<img
  src={image.url}
  alt={image.alt}
  transition:name={`image-${id}`}
/>

Animacje transitions

Code
ASTRO
---
import { fade, slide } from 'astro:transitions'
---

<!-- Fade in/out -->
<div transition:animate={fade({ duration: '0.3s' })}>
  Fade content
</div>

<!-- Slide -->
<aside transition:animate={slide({ duration: '0.5s' })}>
  Sliding sidebar
</aside>

<!-- Custom animation -->
<article transition:animate={{
  old: {
    name: 'fadeOut',
    duration: '0.2s',
    easing: 'ease-out',
  },
  new: {
    name: 'fadeIn',
    duration: '0.3s',
    delay: '0.1s',
    easing: 'ease-in',
  },
}}>
  Custom animated content
</article>

Persist elements

Code
ASTRO
<!-- Element który NIE będzie re-renderowany przy nawigacji -->
<audio controls transition:persist>
  <source src="/music.mp3" type="audio/mpeg" />
</audio>

<!-- Video player który gra dalej -->
<video transition:persist="video-player" autoplay>
  <source src="/video.mp4" type="video/mp4" />
</video>

Stylowanie

Scoped styles (domyślne)

Code
ASTRO
<style>
  /* Te style dotyczą TYLKO tego komponentu */
  h1 {
    color: blue;
  }

  .card {
    padding: 1rem;
  }
</style>

Global styles

Code
ASTRO
<style is:global>
  /* Te style są globalne */
  body {
    margin: 0;
    font-family: system-ui;
  }
</style>

Import stylów

Code
ASTRO
---
// Import CSS file
import '../styles/global.css'

// Import SCSS (wymaga integracji)
import '../styles/main.scss'
---

Tailwind CSS

Code
Bash
npx astro add tailwind
Code
ASTRO
---
// Używaj Tailwind classes
---

<div class="flex items-center gap-4 p-6 bg-white dark:bg-gray-800 rounded-lg shadow-lg">
  <h1 class="text-2xl font-bold text-gray-900 dark:text-white">
    Hello Tailwind!
  </h1>
  <button class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded">
    Click me
  </button>
</div>

CSS Modules

Code
ASTRO
---
import styles from './Component.module.css'
---

<div class={styles.container}>
  <h1 class={styles.title}>CSS Modules</h1>
</div>

Optymalizacja obrazów

Wbudowany Image component

Code
ASTRO
---
import { Image } from 'astro:assets'
import heroImage from '../assets/hero.jpg'
---

<!-- Lokalne obrazy - automatyczna optymalizacja -->
<Image
  src={heroImage}
  alt="Hero image"
  width={800}
  height={600}
  quality={80}
  format="webp"
/>

<!-- Remote images -->
<Image
  src="https://example.com/image.jpg"
  alt="Remote image"
  width={400}
  height={300}
  inferSize
/>

<!-- Responsive images -->
<Image
  src={heroImage}
  alt="Responsive"
  widths={[320, 640, 1280]}
  sizes="(max-width: 640px) 100vw, 50vw"
/>

Picture component

Code
ASTRO
---
import { Picture } from 'astro:assets'
import myImage from '../assets/photo.jpg'
---

<Picture
  src={myImage}
  alt="Photo"
  formats={['avif', 'webp', 'jpg']}
  widths={[400, 800, 1200]}
  sizes="(max-width: 800px) 100vw, 800px"
/>

Konfiguracja obrazów

astro.config.mjs
JavaScript
// astro.config.mjs
import { defineConfig } from 'astro/config'

export default defineConfig({
  image: {
    // Dozwolone domeny dla remote images
    domains: ['example.com', 'cdn.example.com'],

    // Dozwolone ścieżki
    remotePatterns: [{
      protocol: 'https',
      hostname: '**.cloudinary.com',
    }],

    // Serwis optymalizacji
    service: {
      entrypoint: 'astro/assets/services/sharp', // domyślne
    },
  },
})

Integracje

Popularne integracje

Code
Bash
# UI Frameworks
npx astro add react
npx astro add vue
npx astro add svelte
npx astro add solid-js
npx astro add preact

# Styling
npx astro add tailwind

# Content
npx astro add mdx

# SEO & Analytics
npx astro add sitemap
npx astro add partytown  # Third-party scripts

# Adapters (SSR)
npx astro add vercel
npx astro add netlify
npx astro add cloudflare
npx astro add node

# Inne
npx astro add db         # Astro DB (libSQL)
npx astro add alpinejs   # Alpine.js

Własna integracja

TSmy-integration.ts
TypeScript
// my-integration.ts
import type { AstroIntegration } from 'astro'

export default function myIntegration(): AstroIntegration {
  return {
    name: 'my-integration',
    hooks: {
      'astro:config:setup': ({ addWatchFile, config, updateConfig }) => {
        // Modyfikuj konfigurację Astro
      },
      'astro:build:start': () => {
        // Przed buildem
      },
      'astro:build:done': ({ dir }) => {
        // Po buildzie
      },
    },
  }
}

Astro DB

Setup

Code
Bash
npx astro add db
TSdb/config.ts
TypeScript
// db/config.ts
import { defineDb, defineTable, column } from 'astro:db'

const User = defineTable({
  columns: {
    id: column.number({ primaryKey: true }),
    name: column.text(),
    email: column.text({ unique: true }),
    createdAt: column.date({ default: new Date() }),
  },
})

const Post = defineTable({
  columns: {
    id: column.number({ primaryKey: true }),
    title: column.text(),
    content: column.text(),
    authorId: column.number({ references: () => User.columns.id }),
    published: column.boolean({ default: false }),
  },
})

export default defineDb({
  tables: { User, Post },
})

Seeding

TSdb/seed.ts
TypeScript
// db/seed.ts
import { db, User, Post } from 'astro:db'

export default async function seed() {
  await db.insert(User).values([
    { id: 1, name: 'Jan', email: 'jan@example.com' },
    { id: 2, name: 'Anna', email: 'anna@example.com' },
  ])

  await db.insert(Post).values([
    { id: 1, title: 'Hello Astro DB', content: '...', authorId: 1, published: true },
  ])
}

Queries

Code
ASTRO
---
import { db, User, Post, eq } from 'astro:db'

// Select all
const users = await db.select().from(User)

// Select with where
const publishedPosts = await db
  .select()
  .from(Post)
  .where(eq(Post.published, true))

// Join
const postsWithAuthors = await db
  .select({
    title: Post.title,
    authorName: User.name,
  })
  .from(Post)
  .innerJoin(User, eq(Post.authorId, User.id))
---

<ul>
  {users.map((user) => (
    <li>{user.name} - {user.email}</li>
  ))}
</ul>

SEO i Performance

SEO Component

Code
ASTRO
---
// src/components/SEO.astro
interface Props {
  title: string
  description: string
  image?: string
  canonicalUrl?: string
  type?: 'website' | 'article'
}

const {
  title,
  description,
  image = '/og-default.jpg',
  canonicalUrl = Astro.url.href,
  type = 'website'
} = Astro.props

const siteUrl = Astro.site?.href || 'https://example.com'
const imageUrl = new URL(image, siteUrl).href
---

<!-- Basic Meta -->
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalUrl} />

<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={imageUrl} />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:type" content={type} />

<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={imageUrl} />

<!-- JSON-LD -->
<script type="application/ld+json" set:html={JSON.stringify({
  '@context': 'https://schema.org',
  '@type': type === 'article' ? 'BlogPosting' : 'WebSite',
  name: title,
  description: description,
  url: canonicalUrl,
  image: imageUrl,
})} />

Performance tips

Code
ASTRO
---
// 1. Prefetch links
---

<!-- Prefetch przy hover -->
<a href="/about" data-astro-prefetch>About</a>

<!-- Prefetch przy wejściu do viewport -->
<a href="/products" data-astro-prefetch="viewport">Products</a>

<!-- Wyłącz prefetch -->
<a href="/external" data-astro-prefetch="false">External</a>
astro.config.mjs
JavaScript
// astro.config.mjs
export default defineConfig({
  prefetch: {
    prefetchAll: true,  // Prefetch wszystkie linki
    defaultStrategy: 'hover', // 'tap', 'hover', 'viewport', 'load'
  },
})

Deployment

Statyczny hosting

Code
Bash
# Build
npm run build

# Output w dist/
# Upload dist/ do dowolnego statycznego hostingu

Vercel

Code
Bash
npx astro add vercel
astro.config.mjs
JavaScript
// astro.config.mjs
import vercel from '@astrojs/vercel/serverless'

export default defineConfig({
  output: 'server',
  adapter: vercel({
    webAnalytics: { enabled: true },
    imageService: true,
  }),
})

Netlify

Code
Bash
npx astro add netlify
astro.config.mjs
JavaScript
// astro.config.mjs
import netlify from '@astrojs/netlify'

export default defineConfig({
  output: 'server',
  adapter: netlify({
    edgeMiddleware: true,
  }),
})

Cloudflare Pages

Code
Bash
npx astro add cloudflare
astro.config.mjs
JavaScript
// astro.config.mjs
import cloudflare from '@astrojs/cloudflare'

export default defineConfig({
  output: 'server',
  adapter: cloudflare({
    mode: 'directory',
  }),
})

Best Practices

1. Minimalizuj JavaScript

Code
ASTRO
---
// Komponenty bez client: są statyczne
import StaticCard from './StaticCard.astro'

// Tylko te które POTRZEBUJĄ interaktywności
import InteractiveWidget from './Widget.jsx'
---

<!-- Zero JS -->
<StaticCard title="Static" />

<!-- JS tylko gdy widoczny -->
<InteractiveWidget client:visible />

2. Używaj Content Collections

Code
TypeScript
// Type-safe content
const posts = await getCollection('blog', ({ data }) => {
  return import.meta.env.PROD ? !data.draft : true
})

3. Optymalizuj obrazy

Code
ASTRO
---
import { Image } from 'astro:assets'
import hero from '../assets/hero.jpg'
---

<!-- Zawsze używaj Image/Picture -->
<Image src={hero} alt="Hero" width={800} height={600} />

4. Wykorzystuj View Transitions

Code
ASTRO
---
import { ViewTransitions } from 'astro:transitions'
---

<head>
  <ViewTransitions />
</head>

5. SSR tylko gdy potrzebne

Code
JavaScript
// Preferuj 'static' lub 'hybrid'
export default defineConfig({
  output: 'hybrid', // Domyślnie statyczne
})

FAQ - Najczęściej zadawane pytania

Czy Astro jest szybszy od Next.js?

Dla stron content-first - tak. Astro wysyła zero JS domyślnie, Next.js minimum ~80KB. Dla wysoce interaktywnych aplikacji różnica jest mniejsza.

Czy mogę używać React w Astro?

Tak! Użyj npx astro add react i importuj komponenty z dyrektywą client:*.

Jak migrować z Gatsby/Next.js?

  1. Stwórz projekt Astro
  2. Przenieś treści do Content Collections
  3. Przekonwertuj komponenty (JSX → Astro lub zachowaj z client:)
  4. Dostosuj routing (file-based)

Czy Astro obsługuje TypeScript?

Tak, out-of-box. Po prostu używaj .ts i .astro z typami.

Jak dodać CMS?

Astro działa z każdym headless CMS: Contentful, Sanity, Strapi, Directus, etc. Pobieraj dane w frontmatter i renderuj.

Podsumowanie

Astro to rewolucyjny framework dla stron content-first, oferujący:

  • Zero JavaScript domyślnie - Maksymalna wydajność
  • Island Architecture - JS tylko tam gdzie potrzebny
  • Multi-framework - React, Vue, Svelte razem
  • Content Collections - Type-safe zarządzanie treścią
  • View Transitions - Płynne przejścia stron
  • Doskonałe SEO - Statyczny HTML = idealne indeksowanie

Astro jest idealny dla blogów, dokumentacji, landing pages i stron marketingowych, gdzie wydajność i SEO są kluczowe.