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
- Zero JavaScript domyślnie - Strony są statycznym HTML, JS ładowany na żądanie
- Island Architecture - Izolowane komponenty interaktywne
- Framework-agnostic - Używaj React, Vue, Svelte, Solid razem
- Content Collections - Type-safe zarządzanie treścią
- Doskonałe SEO - Statyczny HTML = idealne indeksowanie
- View Transitions - Płynne przejścia między stronami
- Server Islands - Dynamiczne wyspy renderowane na serwerze
Astro vs inne frameworki
| Cecha | Astro | Next.js | Gatsby | SvelteKit |
|---|---|---|---|---|
| Domyślny JS | 0 KB | ~80 KB+ | ~50 KB+ | ~15 KB |
| Multi-framework | Tak | Tylko React | Tylko React | Tylko Svelte |
| Content focus | Tak | Nie | Tak | Nie |
| Island Architecture | Natywne | Nie | Nie | Nie |
| Learning curve | Niska | Średnia | Wysoka | Średnia |
| Build time | Szybki | Średni | Wolny | Szybki |
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
# 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 devStruktura projektu
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.jsonKonfiguracja astro.config.mjs
// 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
---
// 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)
---
// 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>---
// 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
---
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:
---
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
---
// 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
# 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 litPrzykład: React component
// 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 -->
<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
// 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
---
# 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.
### 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
---
// 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
npx astro add mdxMDX z komponentami
---
// 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
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
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
---
// 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)
---
// 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
---
// 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
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
export default defineConfig({
output: 'hybrid', // Domyślnie statyczne, opt-in do SSR
adapter: node({ mode: 'standalone' }),
})---
// src/pages/static-page.astro
// Ta strona będzie statyczna (domyślnie w hybrid)
---
<h1>Statyczna strona</h1>---
// 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
# 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 denoAPI Endpoints
// 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',
},
})
}// 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
---
// 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
---
// 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
---
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
<!-- 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)
<style>
/* Te style dotyczą TYLKO tego komponentu */
h1 {
color: blue;
}
.card {
padding: 1rem;
}
</style>Global styles
<style is:global>
/* Te style są globalne */
body {
margin: 0;
font-family: system-ui;
}
</style>Import stylów
---
// Import CSS file
import '../styles/global.css'
// Import SCSS (wymaga integracji)
import '../styles/main.scss'
---Tailwind CSS
npx astro add tailwind---
// 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
---
import styles from './Component.module.css'
---
<div class={styles.container}>
<h1 class={styles.title}>CSS Modules</h1>
</div>Optymalizacja obrazów
Wbudowany Image component
---
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
---
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
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
# 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.jsWłasna integracja
// 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
npx astro add db// 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
// 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
---
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
---
// 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
---
// 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
export default defineConfig({
prefetch: {
prefetchAll: true, // Prefetch wszystkie linki
defaultStrategy: 'hover', // 'tap', 'hover', 'viewport', 'load'
},
})Deployment
Statyczny hosting
# Build
npm run build
# Output w dist/
# Upload dist/ do dowolnego statycznego hostinguVercel
npx astro add vercel// astro.config.mjs
import vercel from '@astrojs/vercel/serverless'
export default defineConfig({
output: 'server',
adapter: vercel({
webAnalytics: { enabled: true },
imageService: true,
}),
})Netlify
npx astro add netlify// astro.config.mjs
import netlify from '@astrojs/netlify'
export default defineConfig({
output: 'server',
adapter: netlify({
edgeMiddleware: true,
}),
})Cloudflare Pages
npx astro add cloudflare// astro.config.mjs
import cloudflare from '@astrojs/cloudflare'
export default defineConfig({
output: 'server',
adapter: cloudflare({
mode: 'directory',
}),
})Best Practices
1. Minimalizuj JavaScript
---
// 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
// Type-safe content
const posts = await getCollection('blog', ({ data }) => {
return import.meta.env.PROD ? !data.draft : true
})3. Optymalizuj obrazy
---
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
---
import { ViewTransitions } from 'astro:transitions'
---
<head>
<ViewTransitions />
</head>5. SSR tylko gdy potrzebne
// 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?
- Stwórz projekt Astro
- Przenieś treści do Content Collections
- Przekonwertuj komponenty (JSX → Astro lub zachowaj z client:)
- 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.