Communitygithub.com

qalimero/qalim-portfolio

Guia abrangente para construir e publicar sites estáticos com o framework Astro.

O que é qalim-portfolio?

Este skill oferece um guia completo para desenvolvedores que desejam construir e publicar sites estáticos usando o framework Astro, com foco em geração de sites estáticos (SSG). Ele cobre desde a configuração inicial do projeto até tópicos avançados como Content Collections, realce de sintaxe com Shiki e Expressive Code, integração de diagramas com Mermaid e PlantUML, busca do lado do cliente com Pagefind e Fuse.js, documentação versionada, internacionalização (i18n) e padrões comuns como paginação e RSS. O skill também fornece práticas recomendadas de desempenho, um checklist pré-publicação e guias de deploy para plataformas como Netlify, Vercel, GitHub Pages e GCP/Firebase. É ideal para cenários como "criar site Astro", "configurar coleções de conteúdo", "adicionar diagramas Mermaid ao Astro" ou "publicar blog estático", sendo compatível com as versões 4.x e 5.x do Astro.

Funciona com✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/qalimero/qalim-portfolio/tree/HEAD/.claude/skills/publishing-astro-websites

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Prévia

Do README da skill

qalim-portfolio preview

Documentação

Publicando Sites com Astro

Crie sites estáticos rápidos e focados em conteúdo com a abordagem SSG de runtime zero do Astro, hidratação parcial e suporte extensivo a Markdown.

Conteúdo

Início Rápido

# Crie um novo projeto (use o template Blog para sites Markdown)
npm create astro@latest

# Desenvolvimento
npm run dev          # Servidor local em http://localhost:4321
npm run build        # Gera arquivos estáticos em dist/
npm run preview      # Visualiza a build de produção

Quando Não Usar

Este skill foca em geração de sites estáticos (SSG). Considere outras abordagens para:

  • Aplicações com dados em tempo real - Use o modo SSR com conexões a banco de dados
  • Fluxos de autenticação de usuário - Requer manipulação de sessão no lado do servidor
  • E-commerce com inventário dinâmico - Use modo híbrido ou SSR completo
  • Aplicações de página única (SPAs) - Considere frameworks React/Vue diretamente

Para padrões híbridos SSG+SSR, veja a documentação de adaptadores do Astro.

Estrutura do Projeto

src/
  components/     # Componentes Astro, React, Vue, Svelte
  content/        # Coleções de Conteúdo (Markdown/MDX)
    config.ts     # Schemas das coleções
    docs/         # Exemplo de coleção
  layouts/        # Wrappers de página com slots
  pages/          # Roteamento baseado em arquivos
public/           # Ativos estáticos (imagens, fontes, favicons)
astro.config.mjs  # Configuração do framework

SSG vs SSR vs Híbrido

ModoQuando as Páginas RenderizamCaso de Uso
SSG (padrão)Tempo de buildBlogs, documentação, sites de marketing
SSRCada requisiçãoDados dinâmicos, personalização
HíbridoMistura de ambosPáginas estáticas + endpoints dinâmicos

Para sites puramente estáticos, use o padrão output: 'static' — nenhum adaptador é necessário.

Coleções de Conteúdo

Padrão Legado (Astro 4.x)

Defina schemas em src/content/config.ts:

import { defineCollection, z } from "astro:content";

export const collections = {
  docs: defineCollection({
    schema: z.object({
      title: z.string(),
      description: z.string().optional(),
      tags: z.array(z.string()).optional(),
      order: z.number().optional(),
      draft: z.boolean().default(false)
    })
  })
};

API Content Layer (Astro 5.0+)

Novo padrão com o carregador glob() — até 75% mais rápido em builds para sites grandes:

// src/content.config.ts (nota: nome de arquivo diferente)
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),
  schema: ({ image }) => z.object({
    title: z.string(),
    pubDate: z.coerce.date(),
    draft: z.boolean().default(false),
    cover: image(),  // Valida que a imagem existe
    author: reference('authors'),  // Referência entre coleções
  })
});

export const collections = { blog };

Padrões Avançados de Schema

schema: ({ image }) => z.object({
  cover: image(),                    // Valida imagem em src/
  category: z.enum(['tech', 'news']),
  author: reference('authors'),      // Ref entre coleções
  relatedPosts: z.array(reference('blog')).optional(),
})

Carregadores Personalizados (Conteúdo Remoto)

Busque conteúdo de APIs externas (lançamentos do GitHub, CMS, etc.):

// src/loaders/github-releases.ts
import type { Loader } from 'astro/loaders';

export function githubReleasesLoader(repo: string): Loader {
  return {
    name: 'github-releases',
    load: async ({ store, logger }) => {
      logger.info(`Buscando lançamentos para ${repo}`);
      const response = await fetch(`https://api.github.com/repos/${repo}/releases`);
      const releases = await response.json();

      for (const release of releases) {
        store.set({
          id: release.tag_name,
          data: {
            version: release.tag_name,
            published_at: release.published_at,
            body: release.body  // Notas de lançamento em Markdown
          }
        });
      }
    }
  };
}

Registre em content.config.ts:

import { githubReleasesLoader } from './loaders/github-releases';

const releases = defineCollection({
  loader: githubReleasesLoader('owner/repo'),
  schema: z.object({
    version: z.string(),
    published_at: z.string(),
    body: z.string(),
  })
});

Consulte e renderize coleções:

---
import { getCollection } from "astro:content";

export async function getStaticPaths() {
  const docs = await getCollection("docs");
  return docs.map(doc => ({
    params: { slug: doc.slug },
    props: { doc }
  }));
}

const { doc } = Astro.props;
const { Content } = await doc.render();
---

<article>
  <h1>{doc.data.title}</h1>
  <Content />
</article>

Realce de Sintaxe

Configuração Básica do Shiki

import { defineConfig } from "astro/config";

export default defineConfig({
  markdown: {
    shikiConfig: {
      theme: "github-dark",
      wrap: true
    }
  }
});

Tema Duplo Claro/Escuro

shikiConfig: {
  themes: {
    light: 'github-light',
    dark: 'github-dark',
  },
}

Adicione CSS para alternar temas:

@media (prefers-color-scheme: dark) {
  .astro-code, .astro-code span {
    color: var(--shiki-dark) !important;
    background-color: var(--shiki-dark-bg) !important;
  }
}

Realce de Linhas e Transformadores

```typescript {2,4}
const a = 1;
const b = 2;  // destacado
const c = 3;
console.log(a + b + c);  // destacado
```
~~

**Transformadores Shiki (Astro 4.14+):**

```javascript
import { transformerNotationFocus, transformerNotationDiff } from '@shikijs/transformers';

shikiConfig: {
  transformers: [transformerNotationFocus(), transformerNotationDiff()],
}
```

Use comentários de notação no código:
- `// [!code focus]` - Foca esta linha
- `// [!code ++]` - Marca como adição (verde)
- `// [!code --]` - Marca como remoção (vermelho)

### Expressive Code (Recomendado para Documentação)

Blocos de código ricos com botão de copiar, nomes de arquivo, realce de diferenças:

```bash
npm install astro-expressive-code
```

```javascript
import expressiveCode from 'astro-expressive-code';

export default defineConfig({
  integrations: [expressiveCode()],
});
```

Funcionalidades: Botão de copiar, abas de arquivo, marcadores de linha, frames de terminal, marcadores de texto.

## Integração de Diagramas

### Mermaid (Recomendado)

Instale a integração do Astro:

```bash
npm install astro-mermaid mermaid
```

```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mermaid from 'astro-mermaid';

export default defineConfig({
  integrations: [mermaid({ theme: 'default' })]
});
```

Use em Markdown:

~~~markdown
```mermaid
graph TD;
    A-->B;
    B-->C;
```
~~

Funcionalidades: Renderização no lado do cliente, alternância automática de tema, funcionamento offline, sem necessidade de Playwright.

**Alternativa (SVG estático em tempo de build):** Use `rehype-mermaid` com Playwright para diagramas pré-renderizados (requer `npx playwright install --with-deps`).

**Estratégias de Tema para Modo Escuro:**

1. **Variáveis CSS** - Deixe o navegador resolver as cores em tempo de execução:
```javascript
// config mermaid
mermaid.initialize({
  theme: 'base',
  themeVariables: {
    primaryColor: 'var(--diagram-primary)',
    lineColor: 'var(--diagram-line)'
  }
});
```

2. **Elemento Picture** - Gere ambos os temas, alterne com media query:
```html
<picture>
  <source srcset="/diagrams/flow-dark.svg" media="(prefers-color-scheme: dark)">
  <img src="/diagrams/flow-light.svg" alt="Diagrama de fluxo">
</picture>
```

3. **SVG Inline** - Segmente classes SVG com CSS (risco: conflitos de estilo) - adicione critérios de acessibilidade com possibilidade de ter role='image' ou role="presentation":
```css
.dark .mermaid-svg .node rect {
  fill: var(--bg-dark);
}
```

### PlantUML

```bash
npx astro add plantuml
```

Use em Markdown:

~~~markdown
```plantuml
@startuml
Alice -> Bob: Olá
Bob --> Alice: Oi!
@enduml
```
~~

## Busca do Lado do Cliente

### Pagefind (Recomendado para Sites Grandes)

Busca estática sem configuração que indexa no tempo de build:

```bash
npm install pagefind
```

Adicione ao `package.json`:

```json
{
  "scripts": {
    "build": "astro build && npx pagefind --site dist",
    "postbuild": "pagefind --site dist"
  }
}
```

Use em componentes:

```astro
<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script src="/pagefind/pagefind-ui.js" type="text/javascript"></script>

<div id="search"></div>
<script>
  window.addEventListener('DOMContentLoaded', () => {
    new PagefindUI({ element: '#search', showSubResults: true });
  });
</script>
```

Funcionalidades: Sem serviço externo, funciona offline, indexação automática, bundle pequeno (~10KB).

**Controle Granular de Indexação:**

```html
<!-- Indexe apenas o conteúdo principal, não cabeçalhos/barras laterais -->
<main data-pagefind-body>
  <h1 data-pagefind-meta="title">Título da Página</h1>
  <p data-pagefind-weight="10">Texto introdutório importante</p>

  <!-- Exclua de trechos de busca -->
  <nav data-pagefind-ignore>
    <a href="/related">Posts Relacionados</a>
  </nav>
</main>
```

| Atributo | Propósito |
|-----------|-----------|
| `data-pagefind-body` | Limita a indexação a este elemento apenas |
| `data-pagefind-ignore` | Exclui elemento do índice |
| `data-pagefind-meta="chave"` | Define campo de metadados |
| `data-pagefind-weight="10"` | Aumenta relevância (padrão: 1) |

### Pagefind vs Fuse.js

| Funcionalidade | Pagefind | Fuse.js |
|----------------|----------|---------|
| Arquitetura | Blocos binários pré-construídos | Em memória em tempo de execução |
| Largura de banda | Baixa (carrega apenas blocos necessários) | Alta (baixa índice completo) |
| Escalabilidade | 10.000+ páginas | < 500 páginas |
| Multilíngue | Stemming nativo | Configuração manual |
| Caso de Uso | Busca global no site | Filtragem de listas pequenas |

### Fuse.js (Alternativa Leve)

Para sites menores com necessidades de UI personalizada:

```astro
---
import { getCollection } from "astro:content";
const posts = await getCollection('blog');
const searchIndex = JSON.stringify(posts.map(post => ({
  title: post.data.title,
  slug: post.slug,
  body: post.body.slice(0, 500)
})));
---

<input type="search" id="search" placeholder="Buscar..." />
<ul id="results"></ul>

<script define:vars={{ searchIndex }}>
  import Fuse from 'fuse.js';

  const fuse = new Fuse(JSON.parse(searchIndex), {
    keys: ['title', 'body'],
    threshold: 0.3
  });

  document.getElementById('search').addEventListener('input', (e) => {
    const results = fuse.search(e.target.value);
    const resultsEl = document.getElementById('results');

    // Limpa resultados anteriores com segurança
    resultsEl.replaceChildren();

    // Constrói resultados usando métodos DOM seguros
    results.forEach(r => {
      const li = document.createElement('li');
      const link = document.createElement('a');
      link.href = `/blog/${r.item.slug}`;
      link.textContent = r.item.title;
      li.appendChild(link);
      resultsEl.appendChild(li);
    });
  });
</script>
```

Para necessidades empresariais, considere Algolia (API de busca hospedada).

## Documentação Versionada

### Padrão de Documentação com Múltiplas Versões

Use estrutura baseada em pastas:

```
src/content/docs/
  v1/
    getting-started.md
    api-reference.md
  v2/
    getting-started.md
    api-reference.md
    new-feature.md
```

## Internacionalização (i18n)

Configure em `astro.config.mjs`:

```javascript
export default defineConfig({
  i18n: {
    defaultLocale: "en",
    locales: ["en", "fr", "es"],
    routing: {
      prefixDefaultLocale: false
    }
  }
});
```

Estruture o conteúdo por localidade:

```
src/content/docs/
  en/
    getting-started.md
  fr/
    getting-started.md
```

Detecte a localidade em componentes:

```astro
---
const locale = Astro.currentLocale || 'en';
---
```

### Fallback para Traduções Ausentes

Mostre conteúdo em inglês com um banner quando as traduções não existirem:

```astro
---
// src/pages/[lang]/[...slug].astro
import { getCollection, getEntry } from "astro:content";

const languages = ['en', 'es', 'fr'];
const defaultLang = 'en';

export async function getStaticPaths() {
  const englishDocs = await getCollection('docs', ({ id }) => id.startsWith('en/'));
  const paths = [];

  for (const doc of englishDocs) {
    const slug = doc.id.replace(/^en\//, '');

    for (const lang of languages) {
      const localizedId = `${lang}/${slug}`;
      const localizedDoc = await getEntry('docs', localizedId);

      paths.push({
        params: { lang, slug },
        props: {
          entry: localizedDoc || doc,  // Fallback para inglês
          isFallback: !localizedDoc
        }
      });
    }
  }
  return paths;
}

const { entry, isFallback } = Astro.props;
const { Content } = await entry.render();
---

{isFallback && (
  <div class="translation-notice">
    Esta página ainda não está disponível no seu idioma.
  </div>
)}
<Content />
```

## Padrões Comuns

### Listagens Paginadas

Para sites com mais de 50 posts, divida em páginas:

```astro
---
// src/pages/blog/page/[page].astro
import { getCollection } from "astro:content";

const POSTS_PER_PAGE = 10;

export async function getStaticPaths() {
  const allPosts = await getCollection("blog");
  const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE);
  
  const paths = [];
  for (let page = 1; page <= totalPages; page++) {
    paths.push({
      params: { page: String(page) },
      props: { page, totalPages }
    });
  }
  return paths;
}

const { page, totalPages } = Astro.props;
const start = (page - 1) * POSTS_PER_PAGE;
const end = start + POSTS_PER_PAGE;
const posts = (await getCollection("blog"))
  .filter(post => !post.data.draft)
  .slice(start, end);
---

<h1>Blog - Página {page}</h1>
<ul>
  {posts.map(post => (
    <li>
      <a href={`/blog/${post.slug}`}>{post.data.title}</a>
      <span>{post.data.pubDate.toLocaleDateString()}</span>
    </li>
  ))}
</ul>

<nav>
  {page > 1 && <a href={`/blog/page/${page - 1}`}>Anterior</a>}
  {page < totalPages && <a href={`/blog/page/${page + 1}`}>Próxima</a>}
</nav>
```

### Tags e Categorias

Gere páginas de arquivo por tag:

```astro
---
// src/pages/tags/[tag].astro
import { getCollection } from "astro:content";

export async function getStaticPaths() {
  const posts = await getCollection("blog");
  const tags = [...new Set(posts.flatMap(p => p.data.tags || []))];
  
  return tags.map(tag => ({
    params: { tag },
    props: {
      posts: posts.filter(p => p.data.tags?.includes(tag))
    }
  }));
}

const { tag } = Astro.params;
const { posts } = Astro.props;
---

<h1>Tag: {tag}</h1>
<ul>
  {posts.map(post => (
    <li><a href={`/blog/${post.slug}`}>{post.data.title}</a></li>
  ))}
</ul>
```

### Feed RSS

Gere um feed RSS usando a integração `@astrojs/rss`:

```bash
npm install @astrojs/rss
```

```astro
---
// src/pages/rss.xml.js
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';

export async function GET(context) {
  const posts = await getCollection('blog');
  
  return rss({
    title: 'Meu Blog',
    description: 'Um blog sobre construção de sites com Astro',
    site: context.site,
    items: posts.map(post => ({
      title: post.data.title,
      pubDate: post.data.pubDate,
      description: post.data.description,
      link: `/blog/${post.slug}/`
    }))
  });
}
```

### Formulários

Para gerenciamento de formulários, considere soluções sem servidor como Formspree, Netlify Forms, ou um endpoint personalizado em um adaptador SSR.

## Melhores Práticas de Desempenho

### Pré-busca (Prefetching)

Habilite a pré-busca integrada para navegação instantânea:

```javascript
// astro.config.mjs
export default defineConfig({
  prefetch: true
});
```

Use o componente `ClientRouter` para pré-buscar links quando eles ficarem visíveis.

### CSS Crítico

Inclua inline o CSS crítico e carregue o restante de forma assíncrona para melhorar a First Contentful Paint (FCP).

## Publicação (Deploy)

### Netlify

Instale o adaptador:

```bash
npm install @astrojs/netlify
```

```javascript
import netlify from '@astrojs/netlify';

export default defineConfig({
  output: 'static',  // ou 'server' para SSR
  adapter: netlify()
});
```

Crie um arquivo `netlify.toml`:

```toml
[build]
  command = "npm run build"
  publish = "dist"

[[redirects]]
  from = "/api/*"
  to = "/.netlify/functions/:splat"
  status = 200
```

### Vercel

```bash
npm install @astrojs/vercel
```

```javascript
import vercel from '@astrojs/vercel/serverless';

export default defineConfig({
  output: 'server',
  adapter: vercel()
});
```

### GitHub Pages

```bash
npm install @astrojs/github-pages
```

Configure o caminho base se o site estiver em um subdiretório:

```javascript
export default defineConfig({
  site: 'https://usuario.github.io/repositorio',
  base: '/repositorio',
  adapter: githubPages()
});
```

### GCP/Firebase

Use o adaptador Firebase:

```bash
npm install @astrojs/firebase
```

## Checklist Pré-Publicação

- [ ] Testar todos os links internos e externos
- [ ] Verificar SEO: meta tags, Open Graph, descrições
- [ ] Configurar redirecionamentos 301 se necessário
- [ ] Otimizar imagens (formato, tamanho, lazy loading)
- [ ] Verificar sitemap.xml e robots.txt
- [ ] Configurar análises (Google Analytics, Plausible, etc.)
- [ ] Testar em dispositivos móveis e diferentes navegadores

## Testes e Qualidade

### Vitest (Testes Unitários)

```bash
npm install vitest
```

```javascript
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node'
  }
});
```

### Playwright (Testes End-to-End)

```bash
npm install @playwright/test
npx playwright install
```

```javascript
// e2e/home.spec.js
import { test, expect } from '@playwright/test';

test('home page has title', async ({ page }) => {
  await page.goto('http://localhost:4321');
  await expect(page).toHaveTitle(/Meu Blog/);
});
```

### Verificação de Links

Use ferramentas como `linkinator` ou `broken-link-checker` para verificar links quebrados:

```bash
npx linkinator http://localhost:4321
```

## Solução de Problemas

- **Erro `Module not found`**: Verifique se todos os pacotes estão instalados (`npm install`).
- **Página não atualiza**: Limpe o cache do Astro com `rm -rf .astro`.
- **Imagens não aparecem**: Confirme que estão em `public/` ou use o formato de import do Astro.

---

*Este guia cobre as práticas essenciais para construir e publicar sites Astro. Para mais detalhes, consulte a [documentação oficial do Astro](https://docs.astro.build).*

Habilidades Relacionadas