Communitygithub.com

asolis87/vera-sesiom-skills

Convenciones de Astro para páginas de aterrizaje, sitios de contenido y proyectos SSG en Vera Sesiom. Sin JavaScript por defecto, Islas Vue para interactividad y arquitectura centrada en contenido.

¿Qué es vera-sesiom-skills?

Esta habilidad define las convenciones de Vera Sesiom para construir sitios web estáticos y rápidos utilizando Astro, ideal para páginas de aterrizaje, sitios de marketing, blogs y documentación. Su objetivo es maximizar el rendimiento y el SEO siguiendo una arquitectura de 'Islas', donde el JavaScript solo se carga cuando es necesario, usando Vue para componentes interactivos específicos. La habilidad enfatiza el uso de TypeScript estricto, Tailwind CSS para estilos, colecciones de contenido con esquemas Zod para contenido estructurado y la 'Arquitectura Screaming' donde la estructura del proyecto refleja el dominio, no el framework. También proporciona guías detalladas sobre patrones de componentes de Astro, manejo de imágenes con `astro:assets`, estrategias de SEO y un árbol de decisión para elegir entre Astro y Vue según el tipo de proyecto.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/asolis87/vera-sesiom-skills/tree/HEAD/skills/astro-landing

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Cuándo usar

  • Páginas de aterrizaje y sitios de marketing
  • Sitios web corporativos
  • Sitios con mucho contenido (blogs, documentación)
  • Cualquier proyecto donde el SEO y el rendimiento sean críticos
  • Sitios mayormente estáticos con mínima interactividad
  • Sitios que necesitan una carga inicial rápida y Core Web Vitals óptimos

NO para aplicaciones web completas — usa vue-frontend para aplicaciones interactivas (paneles, aplicaciones CRUD, aplicaciones en tiempo real).


Reglas críticas

  1. Sin JavaScript por defecto — Astro NO envía JavaScript a menos que añadas una directiva client:
  2. TypeScript SIEMPRE — igual que todos los proyectos de Vera Sesiom, modo estricto
  3. Usa Colecciones de Contenido para contenido estructurado — publicaciones de blog, documentación, casos de estudio
  4. Arquitectura de Islas — componentes Vue/React solo donde se necesita interactividad
  5. Salida estática (SSG) por defecto — SSR solo cuando el contenido dinámico lo requiera
  6. Tailwind CSS para estilos — igual que el frontend Vue
  7. Arquitectura Screaming — las carpetas reflejan el dominio, no el framework

Criterios de decisión: Astro vs Vue

Cuándo usar AstroCuándo usar Vue
Página de aterrizaje, sitio de marketingAplicación web (panel, administrador)
Mucho contenido (blog, documentos)UI altamente interactivo
El SEO es críticoSe necesitan características en tiempo real
Contenido mayormente estáticoGestión de estado compleja
Mínima interactividad (formularios, carruseles)Navegación SPA completa
Sin JavaScript es una característicaLos beneficios del ecosistema Vue importan

Regla: Si el sitio es contenido que la gente LEE → Astro. Si es una aplicación que la gente USA → Vue.


Estructura del proyecto (Arquitectura Screaming)

apps/landing/   (o apps/website/)
├── src/
│   ├── pages/                    # Enrutamiento basado en archivos (.astro)
│   │   ├── index.astro           # Página de inicio
│   │   ├── about.astro           # Página 'Acerca de'
│   │   ├── pricing.astro         # Página de precios
│   │   ├── blog/
│   │   │   ├── index.astro      # Lista del blog
│   │   │   └── [slug].astro      # Entrada del blog (dinámica)
│   │   └── contact.astro
│   ├── layouts/                  # Layouts de página
│   │   ├── BaseLayout.astro      # Estructura HTML base
│   │   ├── BlogLayout.astro      # Layout de entrada de blog
│   │   └── LandingLayout.astro   # Layout de página de aterrizaje
│   ├── components/               # Componentes Astro (estáticos por defecto)
│   │   ├── sections/             # Secciones de página
│   │   │   ├── Hero.astro
│   │   │   ├── Features.astro
│   │   │   ├── Pricing.astro
│   │   │   ├── Testimonials.astro
│   │   │   └── CTA.astro
│   │   ├── ui/                   # Átomos de UI reutilizables
│   │   │   ├── Button.astro
│   │   │   ├── Card.astro
│   │   │   ├── Badge.astro
│   │   │   └── Input.astro
│   │   └── interactive/          # Islas Vue/React
│   │       ├── ContactForm.vue   # Formulario con validación
│   │       ├── Carousel.vue      # Carrusel de imágenes
│   │       └── MobileMenu.vue    # Navegación móvil
│   ├── content/                  # Colecciones de contenido
│   │   ├── blog/                 # Publicaciones de blog en Markdown/MDX
│   │   │   ├── post-1.md
│   │   │   └── post-2.mdx
│   │   └── config.ts             # Esquema de la colección (Zod)
│   ├── styles/                   # Estilos globales
│   │   └── global.css            # Importaciones de Tailwind
│   ├── assets/                   # Imágenes optimizadas, fuentes
│   │   ├── images/
│   │   └── fonts/
│   └── utils/                    # Funciones auxiliares
│       ├── date-format.ts
│       └── seo.ts
├── public/                       # Archivos estáticos (sin procesar)
│   ├── favicon.ico
│   ├── robots.txt
│   └── fonts/
├── astro.config.mjs              # Configuración de Astro
├── tailwind.config.mjs           # Configuración de Tailwind
├── tsconfig.json
└── package.json

Patrón de componente Astro

Cada archivo .astro tiene dos partes: frontmatter (lado del servidor) y plantilla (HTML).

---
// src/components/ui/Button.astro
// Frontmatter: se ejecuta en el servidor, SIN JavaScript en el navegador

export interface Props {
  variant?: 'primary' | 'secondary' | 'outline'
  size?: 'sm' | 'md' | 'lg'
  href?: string
}

const {
  variant = 'primary',
  size = 'md',
  href,
  ...rest
} = Astro.props

const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors'
const variantClasses = {
  primary: 'bg-blue-600 text-white hover:bg-blue-700',
  secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
  outline: 'border-2 border-blue-600 text-blue-600 hover:bg-blue-50'
}
const sizeClasses = {
  sm: 'px-3 py-1.5 text-sm',
  md: 'px-4 py-2 text-base',
  lg: 'px-6 py-3 text-lg'
}

const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`
---

{href ? (
  <a href={href} class={classes} {...rest}>
    <slot />
  </a>
) : (
  <button class={classes} {...rest}>
    <slot />
  </button>
)}

Puntos clave:

  • El frontmatter se ejecuta en tiempo de compilación (o en tiempo de solicitud para SSR)
  • No se necesita export default — la plantilla ES la exportación
  • <slot /> para contenido hijo
  • Las props se tipan con interfaces de TypeScript

Patrón de Layout

---
// src/layouts/BaseLayout.astro
import Nav from '../components/Nav.astro'
import Footer from '../components/Footer.astro'

export interface Props {
  title: string
  description?: string
  image?: string
}

const { title, description = '', image = '/og-default.png' } = Astro.props
const canonicalURL = new URL(Astro.url.pathname, Astro.site)
---

<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="canonical" href={canonicalURL} />
    <title>{title}</title>
    <meta name="description" content={description} />
    
    <!-- Open Graph -->
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:image" content={image} />
    <meta property="og:url" content={canonicalURL} />
    <meta property="og:type" content="website" />
    
    <!-- Twitter -->
    <meta name="twitter:card" content="summary_large_image" />
    
    <slot name="head" />
  </head>
  <body class="bg-white text-gray-900 antialiased">
    <Nav />
    <main>
      <slot />
    </main>
    <Footer />
  </body>
</html>

Uso:

---
// src/pages/index.astro
import BaseLayout from '../layouts/BaseLayout.astro'
import Hero from '../components/sections/Hero.astro'
---

<BaseLayout title="Vera Sesiom - Desarrollo de Software" description="Construimos software que importa.">
  <Hero />
</BaseLayout>

Patrón de Colecciones de Contenido

Define contenido estructurado con validación de esquema Zod.

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

const blogCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    author: z.string().default('Vera Sesiom'),
    image: z.object({
      url: z.string(),
      alt: z.string()
    }).optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false)
  })
})

export const collections = {
  blog: blogCollection
}
---
title: "Por qué elegimos Astro para páginas de aterrizaje"
description: "El rendimiento y la experiencia de desarrollo importan. Aquí está por qué Astro es nuestra opción para sitios de contenido."
pubDate: 2024-01-15
author: "Líder Técnico"
image:
  url: "/images/blog/astro-landing.jpg"
  alt: "Logo de Astro en un sitio web moderno"
tags: ["astro", "rendimiento", "arquitectura"]
---

El contenido va aquí...
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content'
import BaseLayout from '../../layouts/BaseLayout.astro'

const posts = (await getCollection('blog'))
  .filter(post => !post.data.draft)
  .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
---

<BaseLayout title="Blog" description="Últimos artículos de Vera Sesiom">
  <h1>Blog</h1>
  <ul>
    {posts.map(post => (
      <li>
        <a href={`/blog/${post.slug}`}>
          <h2>{post.data.title}</h2>
          <p>{post.data.description}</p>
          <time>{post.data.pubDate.toLocaleDateString('es')}</time>
        </a>
      </li>
    ))}
  </ul>
</BaseLayout>
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content'
import BaseLayout from '../../layouts/BaseLayout.astro'

export async function getStaticPaths() {
  const posts = await getCollection('blog')
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post }
  }))
}

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

<BaseLayout title={post.data.title} description={post.data.description}>
  <article>
    <header>
      <h1>{post.data.title}</h1>
      <time>{post.data.pubDate.toLocaleDateString('es')}</time>
      <p>Por {post.data.author}</p>
    </header>
    <Content />
  </article>
</BaseLayout>

Arquitectura de Islas (Componentes Vue)

Los componentes interactivos envían JavaScript solo cuando es necesario. Usa directivas client: para controlar la hidratación.

<!-- src/components/interactive/ContactForm.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'

const form = ref({
  name: '',
  email: '',
  message: ''
})

const errors = ref<Record<string, string>>({})

const isValid = computed(() => {
  return form.value.name && 
         form.value.email.includes('@') && 
         form.value.message.length > 10
})

async function submitForm() {
  if (!isValid.value) return
  
  // Enviar a la API
  const response = await fetch('/api/contact', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(form.value)
  })
  
  if (response.ok) {
    // Manejar éxito
    form.value = { name: '', email: '', message: '' }
  }
}
</script>

<template>
  <form @submit.prevent="submitForm" class="space-y-4">
    <div>
      <label for="name">Nombre</label>
      <input v-model="form.name" type="text" id="name" required />
    </div>
    <div>
      <label for="email">Correo electrónico</label>
      <input v-model="form.email" type="email" id="email" required />
    </div>
    <div>
      <label for="message">Mensaje</label>
      <textarea v-model="form.message" id="message" rows="4" required />
    </div>
    <button type="submit" :disabled="!isValid">
      Enviar mensaje
    </button>
  </form>
</template>

Usando en Astro:

---
// src/pages/contact.astro
import BaseLayout from '../layouts/BaseLayout.astro'
import ContactForm from '../components/interactive/ContactForm.vue'
---

<BaseLayout title="Contáctanos">
  <h1>Ponte en contacto</h1>
  <p>Nos encantaría saber de ti.</p>
  
  <!-- client:visible = hidratar cuando se desplaza a la vista (MEJOR PRÁCTICA) -->
  <ContactForm client:visible />
</BaseLayout>

Árbol de decisión de directivas client:

DirectivaCuándo usarlaJS se envía
Sin directivaContenido estático, sin interactividadSin JavaScript
client:visibleInteractividad debajo del pliegue (formularios, carruseles) — PREDETERMINADOCuando se desplaza a la vista
client:loadInteractividad crítica arriba del pliegueInmediatamente
client:idleMejoras no críticas (analítica, chat)Después de que la página esté inactiva
client:mediaCondicional por punto de interrupción (menú móvil)Cuando la media query coincide
client:onlyComponentes tipo SPA (omitir SSR)Inmediatamente, sin SSR

Mejores prácticas:

  • Por defecto sin directiva — puramente estático
  • Por defecto client:visible cuando se necesita interactividad
  • Usa client:load con moderación — solo para elementos críticos arriba del pliegue
  • Usa client:idle para mejoras no bloqueantes
---
<!-- Buenos ejemplos -->
import Hero from './Hero.astro'                    <!-- Estático, sin JS -->
import Carousel from './Carousel.vue'             
import MobileMenu from './MobileMenu.vue'         
import Chat from './Chat.vue'                     
import Stats from './Stats.astro'                  <!-- Contador estático = CSS -->

<Hero />                                          <!-- Estático, sin hidratación -->
<Carousel client:visible />                       <!-- Hidratar cuando es visible -->
<MobileMenu client:media="(max-width: 768px)" />  <!-- Solo móvil -->
<Chat client:idle />                               <!-- Después de que la página cargue -->
<Stats />                                          <!-- Animaciones CSS puras, sin JS -->

Optimización de imágenes

Usa astro:assets para optimización automática.

---
// src/pages/index.astro
import { Image } from 'astro:assets'
import heroImage from '../assets/images/hero.png'
---

<Image 
  src={heroImage} 
  alt="Imagen principal" 
  width={800} 
  height={600}
  loading="lazy"
  format="webp"
/>

Para imágenes de CMS o fuentes externas:

---
import { Image } from 'astro:assets'
---

<Image 
  src="https://example.com/image.jpg" 
  alt="Imagen externa" 
  width={800}
  height={600}
/>

Reglas:

  • Siempre especifica width y height para prevenir CLS
  • Usa loading="lazy" para imágenes debajo del pliegue
  • Prefiere formatos webp o avif
  • Mantén las imágenes principales optimizadas y con el tamaño adecuado

Patrón SEO

---
// src/components/SEO.astro
export interface Props {
  title: string
  description: string
  image?: string
  article?: boolean
  publishedTime?: Date
  author?: string
  tags?: string[]
}

const { 
  title, 
  description, 
  image = '/og-default.png',
  article = false,
  publishedTime,
  author,
  tags = []
} = Astro.props

const canonicalURL = new URL(Astro.url.pathname, Astro.site)
const imageURL = new URL(image, Astro.site)
---

<!-- Meta Tags primarias -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />

<!-- Open Graph -->
<meta property="og:type" content={article ? 'article' : 'website'} />
<meta property="og:url" content={canonicalURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={imageURL} />

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={canonicalURL} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={imageURL} />

<!-- Article specific -->
{article && publishedTime && (
  <meta property="article:published_time" content={publishedTime.toISOString()} />
)}
{article && author && (
  <meta property="article:author" content={author} />
)}
{article && tags.map(tag => (
  <meta property="article:tag" content={tag} />
))}

Skills relacionados