🎯 Рекомендуемые коллекции

Балансированные коллекции примеров кода из различных категорий, которые вы можете исследовать

Примеры Astro

Примеры фреймворка Astro - Современный генератор статических сайтов с архитектурой островов, частичной гидратацией и поддержкой мультифреймворков

💻 Astro Hello World typescript

🟢 simple

Базовые страницы Astro, компоненты, маршрутизация на основе файлов и основной синтаксис

⏱️ 15 min 🏷️ astro, static site, components, routing
Prerequisites: HTML/CSS basics, JavaScript fundamentals, Basic understanding of static site generators
// Astro Hello World Examples

// 1. Basic Astro page (src/pages/index.astro)
---
// Frontmatter - server-side JavaScript code
const title = "Welcome to Astro";
const message = "Modern web development simplified";
const features = [
  "Zero JS by default",
  "Islands Architecture",
  "Multi-framework support",
  "Built-in performance optimizations"
];

const currentDate = new Date().toLocaleDateString();
---

<!-- HTML template -->
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width" />
  <title>{title}</title>
  <style>
    :global(body) {
      font-family: system-ui, -apple-system, sans-serif;
      margin: 0;
      padding: 40px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      min-height: 100vh;
    }
    .container {
      max-width: 800px;
      margin: 0 auto;
      background: white;
      padding: 40px;
      border-radius: 12px;
      box-shadow: 0 20px 40px rgba(0,0,0,0.1);
    }
    .features {
      list-style: none;
      padding: 0;
    }
    .features li {
      padding: 12px 0;
      border-bottom: 1px solid #eee;
    }
    .features li:before {
      content: "✨ ";
      color: #667eea;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>{title}</h1>
    <p>{message}</p>
    <p>Generated on: {currentDate}</p>

    <h2>Key Features:</h2>
    <ul class="features">
      {features.map(feature => <li>{feature}</li>)}
    </ul>
  </div>
</body>
</html>

// 2. Astro component (src/components/Card.astro)
---
interface Props {
  title: string;
  description: string;
  imageUrl?: string;
  featured?: boolean;
}

const { title, description, imageUrl, featured = false } = Astro.props;
---

<div class={`card ${featured ? 'featured' : ''}`}>
  {imageUrl && <img src={imageUrl} alt={title} class="card-image" />}
  <div class="card-content">
    <h3>{title}</h3>
    <p>{description}</p>
    <slot />
  </div>
</div>

<style>
  .card {
    border: 1px solid #e2e8f0;
    border-radius: 8px;
    padding: 20px;
    margin-bottom: 20px;
    background: white;
    transition: transform 0.2s;
  }

  .card:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 16px rgba(0,0,0,0.1);
  }

  .card.featured {
    border-color: #667eea;
    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
  }

  .card-image {
    width: 100%;
    height: 200px;
    object-fit: cover;
    border-radius: 4px 4px 0 0;
    margin: -20px -20px 20px -20px;
  }

  .card-content h3 {
    margin-top: 0;
    color: #2d3748;
  }
</style>

// 3. Layout component (src/layouts/MainLayout.astro)
---
export interface Props {
  title: string;
}

const { title } = Astro.props;
const currentYear = new Date().getFullYear();
---

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width" />
  <title>{title} - My Astro Site</title>
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
  <style is:global>
    :root {
      --primary-color: #667eea;
      --text-color: #2d3748;
      --bg-color: #f7fafc;
      --border-color: #e2e8f0;
    }

    * {
      box-sizing: border-box;
    }

    body {
      margin: 0;
      font-family: system-ui, -apple-system, sans-serif;
      line-height: 1.6;
      color: var(--text-color);
      background: var(--bg-color);
    }

    .container {
      max-width: 1200px;
      margin: 0 auto;
      padding: 0 20px;
    }
  </style>
</head>
<body>
  <header>
    <nav class="container">
      <div class="nav-brand">
        <a href="/">Astro Site</a>
      </div>
      <ul class="nav-links">
        <li><a href="/">Home</a></li>
        <li><a href="/about">About</a></li>
        <li><a href="/blog">Blog</a></li>
        <li><a href="/contact">Contact</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <slot />
  </main>

  <footer>
    <div class="container">
      <p>&copy; {currentYear} My Astro Site. All rights reserved.</p>
    </div>
  </footer>

  <style>
    header {
      background: white;
      border-bottom: 1px solid var(--border-color);
      padding: 1rem 0;
      position: sticky;
      top: 0;
      z-index: 100;
    }

    nav {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .nav-brand a {
      font-size: 1.5rem;
      font-weight: bold;
      color: var(--primary-color);
      text-decoration: none;
    }

    .nav-links {
      display: flex;
      list-style: none;
      margin: 0;
      padding: 0;
      gap: 2rem;
    }

    .nav-links a {
      color: var(--text-color);
      text-decoration: none;
      font-weight: 500;
      transition: color 0.2s;
    }

    .nav-links a:hover {
      color: var(--primary-color);
    }

    main {
      min-height: calc(100vh - 120px);
      padding: 2rem 0;
    }

    footer {
      background: white;
      border-top: 1px solid var(--border-color);
      padding: 2rem 0;
      text-align: center;
      color: #718096;
    }

    @media (max-width: 768px) {
      nav {
        flex-direction: column;
        gap: 1rem;
      }

      .nav-links {
        gap: 1rem;
      }
    }
  </style>
</body>
</html>

// 4. Blog listing page (src/pages/blog/index.astro)
---
import Card from '../../components/Card.astro';

// Mock blog data
const posts = [
  {
    title: "Getting Started with Astro",
    description: "Learn the basics of Astro and how to build fast websites.",
    date: "2024-01-15",
    slug: "getting-started",
    featured: true,
    image: "/blog-astro.jpg"
  },
  {
    title: "Islands Architecture Explained",
    description: "Understanding Astro's islands architecture for partial hydration.",
    date: "2024-01-10",
    slug: "islands-architecture",
    featured: false,
    image: "/blog-islands.jpg"
  },
  {
    title: "Performance Optimization in Astro",
    description: "Tips and tricks for making your Astro site lightning fast.",
    date: "2024-01-05",
    slug: "performance-optimization",
    featured: false,
    image: "/blog-performance.jpg"
  }
];

const pageTitle = "Blog - My Astro Site";
---

<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width" />
  <title>{pageTitle}</title>
  <link rel="stylesheet" href="/styles/global.css" />
</head>
<body>
  <div class="container">
    <h1>Latest Blog Posts</h1>

    <div class="posts-grid">
      {posts.map(post => (
        <Card
          title={post.title}
          description={post.description}
          imageUrl={post.image}
          featured={post.featured}
        >
          <div class="post-meta">
            <span class="date">{post.date}</span>
            <a href={`/blog/${post.slug}`} class="read-more">Read More →</a>
          </div>
        </Card>
      ))}
    </div>
  </div>
</body>
</html>

<style>
  .container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 40px 20px;
  }

  h1 {
    text-align: center;
    margin-bottom: 3rem;
    color: #2d3748;
  }

  .posts-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
    gap: 2rem;
  }

  .post-meta {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid #e2e8f0;
  }

  .date {
    color: #718096;
    font-size: 0.9rem;
  }

  .read-more {
    color: #667eea;
    text-decoration: none;
    font-weight: 500;
    transition: color 0.2s;
  }

  .read-more:hover {
    color: #5a67d8;
  }
</style>

// 5. API endpoint (src/pages/api/posts.json.ts)
---
export async function GET() {
  const posts = [
    {
      id: 1,
      title: "Getting Started with Astro",
      content: "Learn the basics of Astro and how to build fast websites...",
      excerpt: "Learn the basics of Astro and how to build fast websites.",
      author: "John Doe",
      date: "2024-01-15",
      tags: ["astro", "tutorial", "beginner"]
    },
    {
      id: 2,
      title: "Islands Architecture Explained",
      content: "Understanding Astro's islands architecture for partial hydration...",
      excerpt: "Understanding Astro's islands architecture for partial hydration.",
      author: "Jane Smith",
      date: "2024-01-10",
      tags: ["astro", "islands", "performance"]
    }
  ];

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

// 6. Dynamic route page (src/pages/blog/[slug].astro)
---
export async function getStaticPaths() {
  const posts = [
    { slug: "getting-started", title: "Getting Started with Astro" },
    { slug: "islands-architecture", title: "Islands Architecture Explained" },
    { slug: "performance-optimization", title: "Performance Optimization in Astro" }
  ];

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

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

<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width" />
  <title>{title}</title>
  <link rel="stylesheet" href="/styles/global.css" />
</head>
<body>
  <article class="container">
    <header>
      <h1>{title}</h1>
      <nav>
        <a href="/blog">← Back to Blog</a>
      </nav>
    </header>

    <main>
      <p>This is the {title} article.</p>
      <p>Article slug: {slug}</p>
      <!-- In a real application, you would fetch and display the full article content here -->
    </main>

    <footer>
      <p>Published on January {slug.includes('performance') ? '5' : slug.includes('islands') ? '10' : '15'}, 2024</p>
    </footer>
  </article>
</body>
</html>

<style>
  .container {
    max-width: 800px;
    margin: 0 auto;
    padding: 40px 20px;
  }

  header {
    margin-bottom: 2rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid #e2e8f0;
  }

  header h1 {
    margin: 0 0 1rem 0;
    color: #2d3748;
  }

  header nav a {
    color: #667eea;
    text-decoration: none;
    font-weight: 500;
  }

  header nav a:hover {
    text-decoration: underline;
  }

  main {
    line-height: 1.8;
    font-size: 1.1rem;
  }

  main p {
    margin-bottom: 1.5rem;
  }

  footer {
    margin-top: 3rem;
    padding-top: 1rem;
    border-top: 1px solid #e2e8f0;
    color: #718096;
    font-size: 0.9rem;
  }
</style>

💻 Острова Astro и интеграция фреймворков typescript

🟡 intermediate ⭐⭐⭐⭐

Архитектура островов с компонентами React, Vue, Svelte и стратегиями частичной гидратации

⏱️ 30 min 🏷️ astro, islands, react, vue, svelte, hydration
Prerequisites: Astro basics, React/Vue/Svelte fundamentals, Understanding of hydration
// Astro Islands and Framework Integration Examples

// 1. React island component (src/components/ReactCounter.tsx)
import React, { useState, useEffect } from 'react';

export default function ReactCounter({ initialCount = 0 }) {
  const [count, setCount] = useState(initialCount);
  const [mounted, setMounted] = useState(false);

  // Track when component is mounted on client
  useEffect(() => {
    setMounted(true);
  }, []);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(initialCount);

  return (
    <div className="react-counter">
      <h3>React Counter Island</h3>
      {!mounted ? (
        <p>Loading React component...</p>
      ) : (
        <>
          <p>Count: {count}</p>
          <div className="counter-controls">
            <button onClick={decrement}>-</button>
            <button onClick={reset}>Reset</button>
            <button onClick={increment}>+</button>
          </div>
          <p className="hydration-status">
            Status: <span className="success">Hydrated ✅</span>
          </p>
        </>
      )}
    </div>
  );
}

<style jsx>{`
  .react-counter {
    padding: 20px;
    border: 2px solid #61dafb;
    border-radius: 8px;
    background: #f0f9ff;
    margin: 20px 0;
    text-align: center;
  }

  .counter-controls {
    display: flex;
    gap: 10px;
    justify-content: center;
    margin: 15px 0;
  }

  .counter-controls button {
    padding: 8px 16px;
    border: none;
    border-radius: 4px;
    background: #61dafb;
    color: white;
    cursor: pointer;
    font-weight: bold;
  }

  .counter-controls button:hover {
    background: #4fa8c5;
  }

  .hydration-status {
    font-size: 0.9rem;
    color: #059669;
    margin-top: 10px;
  }

  .success {
    color: #059669;
    font-weight: bold;
  }
`}</style>

// 2. Vue island component (src/components/VueTodo.vue)
<template>
  <div class="vue-todo">
    <h3>Vue Todo Island</h3>

    <div class="todo-input">
      <input
        v-model="newTodo"
        @keyup.enter="addTodo"
        placeholder="Add new todo..."
      />
      <button @click="addTodo">Add</button>
    </div>

    <ul class="todo-list">
      <li
        v-for="todo in todos"
        :key="todo.id"
        :class="{ completed: todo.completed }"
      >
        <input
          type="checkbox"
          v-model="todo.completed"
          @change="updateTodo(todo.id)"
        />
        <span>{{ todo.text }}</span>
        <button @click="removeTodo(todo.id)" class="remove">×</button>
      </li>
    </ul>

    <div class="todo-stats">
      <p>Total: {{ todos.length }}</p>
      <p>Completed: {{ completedCount }}</p>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

const todos = ref<Todo[]>([]);
const newTodo = ref('');

const completedCount = computed(() =>
  todos.value.filter(todo => todo.completed).length
);

const addTodo = () => {
  if (newTodo.value.trim()) {
    todos.value.push({
      id: Date.now(),
      text: newTodo.value.trim(),
      completed: false
    });
    newTodo.value = '';
  }
};

const updateTodo = (id: number) => {
  // Vue's reactivity handles this automatically
  console.log('Todo updated:', id);
};

const removeTodo = (id: number) => {
  todos.value = todos.value.filter(todo => todo.id !== id);
};

onMounted(() => {
  console.log('Vue Todo component mounted');
});
</script>

<style scoped>
.vue-todo {
  padding: 20px;
  border: 2px solid #42b883;
  border-radius: 8px;
  background: #f0fdf4;
  margin: 20px 0;
  max-width: 400px;
}

.todo-input {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.todo-input input {
  flex: 1;
  padding: 8px 12px;
  border: 1px solid #d1d5db;
  border-radius: 4px;
}

.todo-input button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  background: #42b883;
  color: white;
  cursor: pointer;
}

.todo-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.todo-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid #d1d5db;
}

.todo-list li.completed span {
  text-decoration: line-through;
  color: #6b7280;
}

.todo-list .remove {
  margin-left: auto;
  background: #ef4444;
  color: white;
  border: none;
  border-radius: 50%;
  width: 24px;
  height: 24px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

.todo-stats {
  display: flex;
  justify-content: space-between;
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid #d1d5db;
  font-size: 0.9rem;
  color: #6b7280;
}
</style>

// 3. Svelte island component (src/components/SvelteTimer.svelte)
<script lang="ts">
  let time = 0;
  let isRunning = false;
  let interval: NodeJS.Timeout;

  function start() {
    if (!isRunning) {
      isRunning = true;
      interval = setInterval(() => {
        time += 1;
      }, 1000);
    }
  }

  function pause() {
    if (isRunning) {
      isRunning = false;
      clearInterval(interval);
    }
  }

  function reset() {
    isRunning = false;
    clearInterval(interval);
    time = 0;
  }

  function formatTime(seconds: number): string {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  }

  // Cleanup on component destruction
  onDestroy(() => {
    clearInterval(interval);
  });
</script>

<div class="svelte-timer">
  <h3>Svelte Timer Island</h3>

  <div class="timer-display">
    {formatTime(time)}
  </div>

  <div class="timer-controls">
    <button on:click={start} disabled={isRunning}>
      Start
    </button>
    <button on:click={pause} disabled={!isRunning}>
      Pause
    </button>
    <button on:click={reset}>
      Reset
    </button>
  </div>

  <div class="timer-status">
    Status: {isRunning ? 'Running 🏃' : 'Stopped ⏸️'}
  </div>
</div>

<style>
  .svelte-timer {
    padding: 20px;
    border: 2px solid #ff3e00;
    border-radius: 8px;
    background: #fff7ed;
    margin: 20px 0;
    text-align: center;
    max-width: 300px;
  }

  .timer-display {
    font-size: 3rem;
    font-weight: bold;
    color: #ff3e00;
    margin: 20px 0;
    font-family: monospace;
  }

  .timer-controls {
    display: flex;
    gap: 10px;
    justify-content: center;
    margin: 20px 0;
  }

  .timer-controls button {
    padding: 10px 20px;
    border: none;
    border-radius: 4px;
    background: #ff3e00;
    color: white;
    cursor: pointer;
    font-weight: bold;
    transition: opacity 0.2s;
  }

  .timer-controls button:hover:not(:disabled) {
    opacity: 0.8;
  }

  .timer-controls button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  .timer-status {
    margin-top: 15px;
    color: #ea580c;
    font-weight: 500;
  }
</style>

// 4. Astro page with islands (src/pages/islands.astro)
---
import ReactCounter from '../components/ReactCounter.tsx';
import VueTodo from '../components/VueTodo.vue';
import SvelteTimer from '../components/SvelteTimer.svelte';

const pageTitle = "Astro Islands Architecture";
const description = "Experience multi-framework components with partial hydration";
---

<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width" />
  <title>{pageTitle}</title>
  <style>
    :global(body) {
      font-family: system-ui, -apple-system, sans-serif;
      line-height: 1.6;
      margin: 0;
      padding: 40px 20px;
      background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
      min-height: 100vh;
    }

    .container {
      max-width: 1200px;
      margin: 0 auto;
    }

    .header {
      text-align: center;
      margin-bottom: 3rem;
    }

    .header h1 {
      color: #2d3748;
      margin-bottom: 1rem;
    }

    .header p {
      color: #718096;
      font-size: 1.1rem;
      max-width: 600px;
      margin: 0 auto;
    }

    .islands-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
      gap: 2rem;
      margin-bottom: 3rem;
    }

    .island-info {
      background: white;
      padding: 30px;
      border-radius: 12px;
      box-shadow: 0 10px 25px rgba(0,0,0,0.1);
    }

    .island-info h2 {
      color: #2d3748;
      margin-top: 0;
    }

    .client-js-info {
      background: #fef3c7;
      border: 1px solid #f59e0b;
      border-radius: 8px;
      padding: 20px;
      margin-bottom: 2rem;
      text-align: center;
    }

    .client-js-info h3 {
      margin-top: 0;
      color: #92400e;
    }

    .performance-stats {
      background: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.1);
      margin-top: 2rem;
    }

    .stats-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 1rem;
      margin-top: 1rem;
    }

    .stat-item {
      text-align: center;
      padding: 1rem;
      background: #f8fafc;
      border-radius: 6px;
    }

    .stat-value {
      font-size: 1.5rem;
      font-weight: bold;
      color: #667eea;
    }

    .stat-label {
      font-size: 0.9rem;
      color: #718096;
      margin-top: 0.5rem;
    }

    @media (max-width: 768px) {
      .islands-grid {
        grid-template-columns: 1fr;
      }

      .stats-grid {
        grid-template-columns: 1fr;
      }
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <h1>{pageTitle}</h1>
      <p>{description}</p>
    </div>

    <div class="client-js-info">
      <h3>🌴 Islands Architecture in Action</h3>
      <p>Only the interactive components below are shipped to the browser. Everything else is pre-rendered HTML!</p>
    </div>

    <div class="islands-grid">
      <div class="island-info">
        <h2>React Counter Island</h2>
        <p>A React component that's hydrated client-side. Only this component's JavaScript is loaded.</p>
        <ReactCounter client:load initialCount={10} />
      </div>

      <div class="island-info">
        <h2>Vue Todo Island</h2>
        <p>A Vue.js component with local state management. Hydrated independently from other components.</p>
        <VueTodo client:visible />
      </div>

      <div class="island-info">
        <h2>Svelte Timer Island</h2>
        <p>A Svelte component with reactive state. Compiled to minimal JavaScript for maximum performance.</p>
        <SvelteTimer client:idle />
      </div>
    </div>

    <div class="performance-stats">
      <h2>Performance Benefits</h2>
      <div class="stats-grid">
        <div class="stat-item">
          <div class="stat-value">0 KB</div>
          <div class="stat-label">JS by Default</div>
        </div>
        <div class="stat-item">
          <div class="stat-value">Partial</div>
          <div class="stat-label">Hydration</div>
        </div>
        <div class="stat-item">
          <div class="stat-value">Multi</div>
          <div class="stat-label">Framework Support</div>
        </div>
        <div class="stat-item">
          <div class="stat-value">Fast</div>
          <div class="stat-label">Page Load</div>
        </div>
      </div>
    </div>
  </div>
</body>
</html>

// 5. Advanced island with data fetching (src/components/ReactUserList.tsx)
import React, { useState, useEffect } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
  company: string;
}

export default function ReactUserList() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchUsers();
  }, []);

  const fetchUsers = async () => {
    try {
      setLoading(true);
      const response = await fetch('/api/users');
      if (!response.ok) {
        throw new Error('Failed to fetch users');
      }
      const data = await response.json();
      setUsers(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  if (loading) {
    return (
      <div className="user-list loading">
        <h3>React User List Island</h3>
        <div className="spinner">Loading users...</div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="user-list error">
        <h3>React User List Island</h3>
        <div className="error-message">Error: {error}</div>
        <button onClick={fetchUsers}>Retry</button>
      </div>
    );
  }

  return (
    <div className="user-list">
      <h3>React User List Island</h3>
      <p>Users fetched from API endpoint:</p>
      <ul className="users">
        {users.map(user => (
          <li key={user.id} className="user-item">
            <strong>{user.name}</strong>
            <span className="email">{user.email}</span>
            <span className="company">{user.company}</span>
          </li>
        ))}
      </ul>
      <button onClick={fetchUsers} className="refresh-btn">
        Refresh Users
      </button>
    </div>
  );
}

<style jsx>{`
  .user-list {
    padding: 20px;
    border: 2px solid #61dafb;
    border-radius: 8px;
    background: #f0f9ff;
    margin: 20px 0;
    max-width: 500px;
  }

  .loading, .error {
    text-align: center;
  }

  .spinner {
    padding: 20px;
    color: #3b82f6;
  }

  .error-message {
    color: #ef4444;
    margin: 10px 0;
  }

  .users {
    list-style: none;
    padding: 0;
    margin: 20px 0;
  }

  .user-item {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 12px;
    background: white;
    border-radius: 6px;
    margin-bottom: 8px;
  }

  .email {
    color: #6b7280;
    font-size: 0.9rem;
  }

  .company {
    color: #3b82f6;
    font-size: 0.85rem;
    font-weight: 500;
  }

  .refresh-btn {
    background: #61dafb;
    color: white;
    border: none;
    padding: 8px 16px;
    border-radius: 4px;
    cursor: pointer;
    font-weight: 500;
  }

  .refresh-btn:hover {
    background: #4fa8c5;
  }

  .error button {
    background: #ef4444;
    color: white;
    border: none;
    padding: 8px 16px;
    border-radius: 4px;
    cursor: pointer;
    margin-top: 10px;
  }
`}</style>

// 6. API endpoint for users (src/pages/api/users.json.ts)
---
export async function GET() {
  const users = [
    {
      id: 1,
      name: "John Doe",
      email: "[email protected]",
      company: "Tech Corp"
    },
    {
      id: 2,
      name: "Jane Smith",
      email: "[email protected]",
      company: "Design Studio"
    },
    {
      id: 3,
      name: "Bob Johnson",
      email: "[email protected]",
      company: "Startup Inc"
    }
  ];

  // Simulate network delay
  await new Promise(resolve => setTimeout(resolve, 1000));

  return new Response(JSON.stringify(users), {
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'no-cache'
    }
  });
}
---

💻 Коллекции контента Astro и MDX typescript

🔴 complex ⭐⭐⭐⭐

Управление контентом с коллекциями, интеграцией MDX и динамической генерацией контента

⏱️ 35 min 🏷️ astro, content, mdx, cms, typescript
Prerequisites: Astro fundamentals, Markdown/MDX knowledge, TypeScript basics, CMS concepts
// Astro Content Collections and MDX Examples

// 1. Content collection configuration (src/content/config.ts)
import { defineCollection, z } from 'astro:content';

// Define schema for blog posts
const blog = defineCollection({
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    heroImage: z.string().optional(),
    tags: z.array(z.string()).default([]),
    author: z.object({
      name: z.string(),
      email: z.string().email(),
      bio: z.string().optional(),
    }),
    draft: z.boolean().default(false),
    featured: z.boolean().default(false),
    readingTime: z.number().optional(),
  }),
});

// Define schema for projects
const projects = defineCollection({
  schema: z.object({
    title: z.string(),
    description: z.string(),
    publishDate: z.coerce.date(),
    tech: z.array(z.string()),
    liveUrl: z.string().url().optional(),
    githubUrl: z.string().url().optional(),
    featured: z.boolean().default(false),
    category: z.enum(['web', 'mobile', 'desktop', 'other']),
  }),
});

export const collections = {
  blog,
  projects,
};

// 2. Blog post with frontmatter (src/content/blog/getting-started.md)
---
title: "Getting Started with Astro Content Collections"
description: "Learn how to use Astro's Content Collections for type-safe content management."
pubDate: 2024-01-15
heroImage: "/images/blog-content-collections.jpg"
tags: ["astro", "content-collections", "tutorial", "cms"]
author:
  name: "Jane Smith"
  email: "[email protected]"
  bio: "Technical writer and Astro enthusiast"
draft: false
featured: true
readingTime: 5
---

import { Code } from 'astro:components';

# Getting Started with Astro Content Collections

Astro Content Collections provide a **type-safe** way to manage your content files. Let's explore how they work!

## What are Content Collections?

Content Collections are directories of content files that Astro:

- ✅ Validates your frontmatter against a schema
- ✅ Provides TypeScript types for your content
- ✅ Enables performance optimizations
- ✅ Supports various file formats (MD, MDX, JSON)

## Example Frontmatter

The frontmatter of this file is automatically validated:

<Code code={`---
title: "Getting Started with Astro Content Collections"
description: "Learn how to use Astro's Content Collections"
pubDate: 2024-01-15
tags: ["astro", "content-collections"]
---`} />

## Working with Collections

You can query collections in your Astro pages:

\```astro
---
import { getCollection } from 'astro:content';

const posts = await getCollection('blog', ({ data }) => {
  return !data.draft && data.pubDate <= new Date();
});

const featuredPosts = posts.filter(post => post.data.featured);
---

<h1>Featured Posts</h1>
{featuredPosts.map(post => <article>{post.data.title}</article>)}
\```

## Benefits

1. **Type Safety**: Catch errors at build time
2. **Performance**: Optimized content loading
3. **Developer Experience**: Autocomplete and validation
4. **Flexibility**: Support for multiple content types

---

*Happy coding with Astro! 🚀*

// 3. Project documentation (src/content/projects/astro-portfolio.md)
---
title: "Astro Portfolio Website"
description: "A blazing-fast portfolio site built with Astro and Tailwind CSS."
publishDate: 2024-01-10
tech: ["Astro", "Tailwind CSS", "TypeScript", "MDX"]
liveUrl: "https://portfolio.example.com"
githubUrl: "https://github.com/user/astro-portfolio"
featured: true
category: "web"
---

# Astro Portfolio Website

A modern, lightning-fast portfolio website showcasing the power of Astro's island architecture.

## Features

- 🚀 **Blazing fast** page loads with 100+ Lighthouse score
- 🎨 Beautiful UI with Tailwind CSS
- 📝 Blog powered by MDX
- 🌴 Islands for interactive components
- 🔍 SEO optimized

## Tech Stack

This project leverages:

\```bash
astro          # The main framework
tailwindcss    # For styling
@astrojs/mdx   # MDX support
@astrojs/react # React integration
typescript     # Type safety
\```

## Performance

- First Contentful Paint: **0.8s**
- Largest Contentful Paint: **1.2s**
- Cumulative Layout Shift: **0.02**
- Total Blocking Time: **0ms**

## Live Demo

👉 [View Live Site](https://portfolio.example.com)

## Source Code

👉 [GitHub Repository](https://github.com/user/astro-portfolio)

---

*Built with ❤️ using Astro*

// 4. Dynamic blog listing page (src/pages/blog/index.astro)
---
import { getCollection } from 'astro:content';
import BlogCard from '../../components/BlogCard.astro';

// Get all blog posts, filtering out drafts
const posts = await getCollection('blog', ({ data }) => {
  return !data.draft;
});

// Sort by publication date (newest first)
posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());

// Get unique tags
const allTags = [...new Set(posts.flatMap(post => post.data.tags))];

// Featured posts
const featuredPosts = posts.filter(post => post.data.featured);

const pageTitle = "Blog - My Astro Site";
const pageDescription = "Latest articles about web development, Astro, and modern JavaScript.";
---

<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width" />
  <title>{pageTitle}</title>
  <meta name="description" content={pageDescription} />
  <link rel="stylesheet" href="/styles/global.css" />
</head>
<body>
  <main class="container">
    <header class="page-header">
      <h1>Blog</h1>
      <p>{pageDescription}</p>

      <!-- Featured Posts -->
      {featuredPosts.length > 0 && (
        <section class="featured-section">
          <h2>Featured Posts</h2>
          <div class="featured-grid">
            {featuredPosts.map(post => (
              <BlogCard
                post={post}
                featured={true}
              />
            ))}
          </div>
        </section>
      )}
    </header>

    <!-- Filter Tags -->
    <section class="tags-section">
      <h3>Filter by Tags</h3>
      <div class="tags-cloud">
        <a href="/blog" class="tag active">All</a>
        {allTags.map(tag => (
          <a href={`/blog/tag/${tag}`} class="tag">{tag}</a>
        ))}
      </div>
    </section>

    <!-- All Posts -->
    <section class="posts-section">
      <h2>All Posts</h2>
      <div class="posts-grid">
        {posts.map(post => (
          <BlogCard post={post} />
        ))}
      </div>
    </section>
  </main>
</body>
</html>

<style>
  .container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 40px 20px;
  }

  .page-header {
    text-align: center;
    margin-bottom: 3rem;
  }

  .page-header h1 {
    font-size: 3rem;
    margin-bottom: 1rem;
    color: #2d3748;
  }

  .page-header p {
    font-size: 1.2rem;
    color: #718096;
    max-width: 600px;
    margin: 0 auto 2rem;
  }

  .featured-section {
    margin-bottom: 3rem;
  }

  .featured-section h2 {
    text-align: center;
    margin-bottom: 2rem;
    color: #2d3748;
  }

  .featured-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
    gap: 2rem;
  }

  .tags-section {
    margin-bottom: 3rem;
    text-align: center;
  }

  .tags-section h3 {
    margin-bottom: 1rem;
    color: #2d3748;
  }

  .tags-cloud {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    justify-content: center;
  }

  .tag {
    padding: 0.5rem 1rem;
    background: #f7fafc;
    border: 1px solid #e2e8f0;
    border-radius: 9999px;
    color: #4a5568;
    text-decoration: none;
    transition: all 0.2s;
    font-size: 0.9rem;
  }

  .tag:hover,
  .tag.active {
    background: #667eea;
    color: white;
    border-color: #667eea;
  }

  .posts-section h2 {
    text-align: center;
    margin-bottom: 2rem;
    color: #2d3748;
  }

  .posts-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
    gap: 2rem;
  }
</style>

// 5. Blog card component (src/components/BlogCard.astro)
---
export interface Props {
  post: import('astro:content').CollectionEntry<'blog'>;
  featured?: boolean;
}

const { post, featured = false } = Astro.props;

// Format reading time if available
const readingTime = post.data.readingTime
  ? `${post.data.readingTime} min read`
  : '';

// Format publication date
const formatDate = (date: Date) => {
  return new Intl.DateTimeFormat('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }).format(date);
};
---

<article class={`blog-card ${featured ? 'featured' : ''}`}>
  {post.data.heroImage && (
    <div class="card-image">
      <img
        src={post.data.heroImage}
        alt={post.data.title}
        loading="lazy"
      />
      {featured && <span class="featured-badge">Featured</span>}
    </div>
  )}

  <div class="card-content">
    <div class="card-meta">
      <time datetime={post.data.pubDate.toISOString()}>
        {formatDate(post.data.pubDate)}
      </time>
      {readingTime && (
        <span class="reading-time">• {readingTime}</span>
      )}
    </div>

    <h2 class="card-title">
      <a href={`/blog/${post.slug}`}>{post.data.title}</a>
    </h2>

    <p class="card-description">{post.data.description}</p>

    {post.data.tags.length > 0 && (
      <div class="card-tags">
        {post.data.tags.map(tag => (
          <span class="tag">{tag}</span>
        ))}
      </div>
    )}

    <div class="card-footer">
      <div class="author">
        <div class="author-info">
          <strong>{post.data.author.name}</strong>
        </div>
      </div>

      <a href={`/blog/${post.slug}`} class="read-more">
        Read More →
      </a>
    </div>
  </div>
</article>

<style>
  .blog-card {
    background: white;
    border-radius: 12px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    overflow: hidden;
    transition: transform 0.2s, box-shadow 0.2s;
    border: 2px solid transparent;
  }

  .blog-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 24px rgba(0,0,0,0.15);
  }

  .blog-card.featured {
    border-color: #667eea;
    box-shadow: 0 6px 20px rgba(102, 126, 234, 0.2);
  }

  .card-image {
    position: relative;
    height: 200px;
    overflow: hidden;
  }

  .card-image img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .featured-badge {
    position: absolute;
    top: 1rem;
    right: 1rem;
    background: #667eea;
    color: white;
    padding: 0.25rem 0.75rem;
    border-radius: 9999px;
    font-size: 0.8rem;
    font-weight: 600;
  }

  .card-content {
    padding: 1.5rem;
  }

  .card-meta {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 1rem;
    color: #718096;
    font-size: 0.9rem;
  }

  .card-title {
    margin: 0 0 1rem 0;
    line-height: 1.3;
  }

  .card-title a {
    color: #2d3748;
    text-decoration: none;
    font-size: 1.25rem;
    font-weight: 600;
  }

  .card-title a:hover {
    color: #667eea;
  }

  .card-description {
    color: #718096;
    line-height: 1.6;
    margin-bottom: 1rem;
  }

  .card-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 1.5rem;
  }

  .tag {
    background: #f7fafc;
    color: #4a5568;
    padding: 0.25rem 0.75rem;
    border-radius: 9999px;
    font-size: 0.8rem;
    font-weight: 500;
  }

  .card-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-top: 1rem;
    border-top: 1px solid #e2e8f0;
  }

  .author-info {
    font-size: 0.9rem;
    color: #4a5568;
  }

  .read-more {
    color: #667eea;
    text-decoration: none;
    font-weight: 500;
    font-size: 0.9rem;
    transition: color 0.2s;
  }

  .read-more:hover {
    color: #5a67d8;
  }
</style>

// 6. Dynamic blog post page (src/pages/blog/[slug].astro)
---
import { getCollection, getEntry } from 'astro:content';
import { type CollectionEntry } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('blog', ({ data }) => {
    return !data.draft;
  });

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

const { post } = Astro.props;

// Get related posts (same tags, not current post)
const relatedPosts = await getCollection('blog', ({ data }) => {
  return !data.draft &&
         data.tags.some(tag => post.data.tags.includes(tag));
});

const filteredRelated = relatedPosts
  .filter(relatedPost => relatedPost.slug !== post.slug)
  .slice(0, 3);

const pageTitle = post.data.title;
const pageDescription = post.data.description;
---

<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width" />
  <title>{pageTitle}</title>
  <meta name="description" content={pageDescription} />

  {/* Open Graph tags */}
  <meta property="og:title" content={pageTitle} />
  <meta property="og:description" content={pageDescription} />
  <meta property="og:type" content="article" />

  {/* Twitter Card tags */}
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:title" content={pageTitle} />
  <meta name="twitter:description" content={pageDescription} />

  <link rel="stylesheet" href="/styles/global.css" />
  <style>
    .container {
      max-width: 800px;
      margin: 0 auto;
      padding: 40px 20px;
    }

    .post-header {
      text-align: center;
      margin-bottom: 3rem;
    }

    .post-header h1 {
      font-size: 2.5rem;
      line-height: 1.2;
      margin-bottom: 1rem;
      color: #2d3748;
    }

    .post-meta {
      display: flex;
      justify-content: center;
      align-items: center;
      gap: 1rem;
      margin-bottom: 1.5rem;
      color: #718096;
      font-size: 0.95rem;
    }

    .author-info {
      display: flex;
      align-items: center;
      gap: 0.5rem;
    }

    .post-hero {
      margin-bottom: 3rem;
    }

    .post-hero img {
      width: 100%;
      height: auto;
      border-radius: 12px;
      box-shadow: 0 10px 30px rgba(0,0,0,0.1);
    }

    .post-tags {
      display: flex;
      justify-content: center;
      flex-wrap: wrap;
      gap: 0.5rem;
      margin-bottom: 2rem;
    }

    .tag {
      background: #f7fafc;
      color: #4a5568;
      padding: 0.5rem 1rem;
      border-radius: 9999px;
      font-size: 0.9rem;
      font-weight: 500;
    }

    .post-content {
      line-height: 1.8;
      font-size: 1.1rem;
      color: #2d3748;
    }

    .post-content h2,
    .post-content h3 {
      margin-top: 2.5rem;
      margin-bottom: 1rem;
      color: #1a202c;
    }

    .post-content p {
      margin-bottom: 1.5rem;
    }

    .post-content pre {
      background: #f7fafc;
      border: 1px solid #e2e8f0;
      border-radius: 8px;
      padding: 1.5rem;
      overflow-x: auto;
      margin: 1.5rem 0;
    }

    .post-content code {
      background: #f7fafc;
      padding: 0.2rem 0.4rem;
      border-radius: 4px;
      font-size: 0.9em;
    }

    .related-posts {
      margin-top: 4rem;
      padding-top: 2rem;
      border-top: 1px solid #e2e8f0;
    }

    .related-posts h2 {
      margin-bottom: 1.5rem;
      color: #2d3748;
    }

    .related-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
      gap: 1.5rem;
    }

    .related-card {
      background: white;
      padding: 1.5rem;
      border-radius: 8px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.1);
      border: 1px solid #e2e8f0;
    }

    .related-card h3 {
      margin: 0 0 0.5rem 0;
      font-size: 1.1rem;
    }

    .related-card h3 a {
      color: #2d3748;
      text-decoration: none;
    }

    .related-card h3 a:hover {
      color: #667eea;
    }

    .related-card p {
      color: #718096;
      font-size: 0.9rem;
      margin: 0;
    }

    .navigation {
      margin-top: 3rem;
      text-align: center;
    }

    .navigation a {
      color: #667eea;
      text-decoration: none;
      font-weight: 500;
      margin: 0 1rem;
    }

    .navigation a:hover {
      text-decoration: underline;
    }

    @media (max-width: 768px) {
      .post-header h1 {
        font-size: 2rem;
      }

      .post-meta {
        flex-direction: column;
        gap: 0.5rem;
      }

      .related-grid {
        grid-template-columns: 1fr;
      }
    }
  </style>
</head>
<body>
  <div class="container">
    <article>
      <header class="post-header">
        <h1>{post.data.title}</h1>

        <div class="post-meta">
          <div class="author-info">
            <span>By {post.data.author.name}</span>
          </div>
          <span>•</span>
          <time datetime={post.data.pubDate.toISOString()}>
            {post.data.pubDate.toLocaleDateString('en-US', {
              year: 'numeric',
              month: 'long',
              day: 'numeric'
            })}
          </time>
          {post.data.readingTime && (
            <>
              <span>•</span>
              <span>{post.data.readingTime} min read</span>
            </>
          )}
        </div>

        {post.data.tags.length > 0 && (
          <div class="post-tags">
            {post.data.tags.map(tag => (
              <span class="tag">{tag}</span>
            ))}
          </div>
        )}
      </header>

      {post.data.heroImage && (
        <div class="post-hero">
          <img src={post.data.heroImage} alt={post.data.title} />
        </div>
      )}

      <div class="post-content">
        <post.Content />
      </div>

      {filteredRelated.length > 0 && (
        <section class="related-posts">
          <h2>Related Posts</h2>
          <div class="related-grid">
            {filteredRelated.map(relatedPost => (
              <div class="related-card">
                <h3>
                  <a href={`/blog/${relatedPost.slug}`}>
                    {relatedPost.data.title}
                  </a>
                </h3>
                <p>{relatedPost.data.description}</p>
              </div>
            ))}
          </div>
        </section>
      )}

      <nav class="navigation">
        <a href="/blog">← Back to Blog</a>
      </nav>
    </article>
  </div>
</body>
</html>