How This Site Is Built
This website was built by Claude Code using Astro, under the total supervision of the site administrator. Every file, component, and line of content was generated through an AI-assisted workflow — but every decision, review, and approval was made by a human before anything was committed or deployed. The result is a partnership: the AI handles the mechanics, the administrator owns the direction.
Astro is a modern static site generator designed for content-driven websites. If you want to build your own site with Astro, the official Build your first Astro Blog tutorial is the best place to start, and the full documentation lives at docs.astro.build.
Minimum Requirements to Build a Site with Astro
- Node.js — version
18.17.1,20.3.0, or higher (check withnode -v) - A text editor — VS Code is recommended, with the official Astro VS Code extension
- A terminal — Astro is controlled through its command-line interface (CLI)
- A package manager —
npm(bundled with Node.js),pnpm, oryarn
To scaffold a new project:
npm create astro@latest
That’s it. Astro has zero mandatory dependencies beyond Node.js. No database, no backend runtime, no framework lock-in. You get a dist/ folder of static HTML, CSS, and (optionally) JS that any web server can host.
Below is a detailed walkthrough of what happens when you run npm run build (or npm run dev) and how every .astro and .md file in this project participates in generating the final static HTML.
1. What Astro Is (in 30 Seconds)
Astro is a static site generator (SSG). It takes source files — .astro components, .md Markdown, CSS, images — and compiles them at build time into plain HTML, CSS, and (optionally) JS files. The output is a dist/ folder of static assets that any web server can serve directly. There is no server-side runtime, no Node process running in production.
The key mental model: everything runs once, on your machine, then becomes frozen HTML.
2. The Build Pipeline — Flow Diagram
┌─────────────────────────────────────────────────────────────────────┐
│ npm run build │
│ (runs `astro build`) │
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 1 — ROUTE DISCOVERY │
│ │
│ Astro scans src/pages/ for route files. │
│ │
│ src/pages/index.astro → generates /index.html (the homepage) │
│ │
│ Each .astro file in pages/ = one URL route. │
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 2 — EXECUTE THE FRONTMATTER (the --- fenced block) │
│ │
│ For each page, Astro runs the JavaScript/TypeScript code between │
│ the --- fences at the top. This is server-side code that runs │
│ ONCE at build time, never in the browser. │
│ │
│ In index.astro, the frontmatter: │
│ │
│ 1. Imports the layout: Base.astro │
│ 2. Imports components: Beaker, CodeRain, GitHubRepos, │
│ LanguageToggle │
│ 3. Imports markdown: hero.md, hero_es.md, about.md, │
│ about_es.md, learning.md, │
│ learning_es.md │
│ 4. Imports JSON data: src/data/experience.json │
│ (powers ExperienceTimeline) │
│ 5. Imports styles: global.css │
│ │
│ Component frontmatter also runs now: │
│ - GitHubRepos.astro: fetches the GitHub API (await fetch(...)) │
│ - CodeRain.astro: generates pseudo-random column data │
│ - Beaker.astro: no logic (empty frontmatter) │
│ - LanguageToggle.astro: no logic (empty frontmatter) │
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 3 — MARKDOWN → HTML CONVERSION │
│ │
│ Each .md file imported as { Content as X } is compiled from │
│ Markdown into an Astro component that renders HTML. │
│ │
│ Example: │
│ hero.md contains: │
│ # Miguel Jackson │
│ Chemical engineer with a deep curiosity... │
│ │
│ Becomes a <Content /> component that renders: │
│ <h1>Miguel Jackson</h1> │
│ <p>Chemical engineer with a deep curiosity...</p> │
│ │
│ Astro's built-in Markdown processor handles headings, bold, │
│ links, lists — all standard Markdown syntax. │
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 4 — TEMPLATE RENDERING (component tree assembly) │
│ │
│ Astro evaluates the HTML template (everything below the ---) │
│ and recursively renders each component. This is where the │
│ component tree is assembled: │
│ │
│ index.astro template │
│ └─ <Base title="..."> ← layout wrapper │
│ └─ <slot /> ← filled by index.astro's body │
│ ├─ <LanguageToggle /> ← EN/ES toggle buttons + script │
│ ├─ <CodeRain /> ← falling-code background │
│ ├─ <nav> ← sidebar navigation │
│ ├─ <main> │
│ │ ├─ <header class="hero"> │
│ │ │ ├─ <Hero /> ← hero.md rendered HTML │
│ │ │ ├─ <HeroEs /> ← hero_es.md rendered HTML │
│ │ │ ├─ nav links (GitHub, LinkedIn) │
│ │ │ └─ <Beaker /> ← SVG beaker animation │
│ │ ├─ <section id="about"> │
│ │ │ ├─ <About /> ← about.md │
│ │ │ └─ <AboutEs /> ← about_es.md │
│ │ ├─ <section id="experience"> │
│ │ │ └─ <ExperienceTimeline /> │
│ │ │ ↑ src/data/experience.json (EN+ES inline) │
│ │ ├─ <section id="repos"> │
│ │ │ └─ <GitHubRepos /> ← GitHub API data → HTML │
│ │ ├─ <section id="learning"> │
│ │ │ ├─ <Learning /> ← learning.md │
│ │ │ └─ <LearningEs /> ← learning_es.md │
│ │ └─ <footer> │
│ └─ (end of slot content) │
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 5 — CSS PROCESSING │
│ │
│ Astro collects all CSS: │
│ │
│ A) global.css (imported in index.astro frontmatter) │
│ → applied site-wide, unscoped │
│ │
│ B) <style> blocks inside components (Beaker, CodeRain, │
│ GitHubRepos, LanguageToggle) │
│ → automatically SCOPED by Astro. Each selector gets a unique │
│ data-astro-cid-XXXX attribute so styles don't leak between │
│ components. │
│ │
│ All CSS is bundled, minified, and written to dist/_assets/. │
│ A <link> tag is injected into the HTML <head> automatically. │
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 6 — CLIENT-SIDE JAVASCRIPT │
│ │
│ Astro's philosophy: ship ZERO JS by default. │
│ │
│ The only JS in the output comes from the <script> tag inside │
│ LanguageToggle.astro. Astro bundles this script and includes it │
│ in the final HTML. It handles: │
│ - Reading language preference from localStorage │
│ - Toggling visibility of data-lang-content="en" / "es" divs │
│ - Detecting browser language as fallback │
│ │
│ All other components (Beaker, CodeRain, GitHubRepos) produce │
│ static HTML + CSS only. Their animations are pure CSS. │
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 7 — OUTPUT TO dist/ │
│ │
│ Astro writes the final output: │
│ │
│ dist/ │
│ ├── index.html ← the homepage, fully rendered │
│ ├── _assets/ │
│ │ ├── *.css ← bundled + minified CSS │
│ │ └── *.js ← language toggle script (only JS) │
│ └── favicon.svg ← static asset (copied from public/) │
│ │
│ This folder is the entire website — ready to deploy to any │
│ static file server. No Node, no build tools needed at runtime. │
└─────────────────────────────────────────────────────────────────────┘
3. Every File and Its Role
3.1 Configuration
| File | Purpose |
|---|---|
astro.config.mjs | Tells Astro the site URL and that built assets go under _assets/. This is the minimum config — no SSR, no integrations, pure static output. |
package.json | Defines scripts (dev, build, preview) and the single dependency: astro. |
3.2 Pages — src/pages/
| File | Role | When It Runs |
|---|---|---|
index.astro | The main page. This is the entry point that Astro discovers during route scanning. It imports everything else and defines the full page structure. One file in pages/ = one HTML file in dist/. | Build time — the frontmatter (imports) runs first, then the template is rendered. |
Why it exists: Astro uses file-based routing. Any .astro file in src/pages/ becomes a URL. index.astro → /index.html → the root URL /. If you added src/pages/blog.astro, Astro would generate /blog/index.html.
3.3 Layouts — src/layouts/
| File | Role | When It Runs |
|---|---|---|
Base.astro | Wraps the page in a full HTML document: <!doctype html>, <head> with meta tags / title / favicon, and <body>. Contains a <slot /> where the page content is injected. | Build time — called by index.astro via <Base title="...">. |
Why it exists: Layouts separate the HTML boilerplate (<html>, <head>, <meta>) from the page content. If you added more pages, they’d all use <Base> to get consistent head tags without duplicating the <head> block.
How <slot /> works: When index.astro writes:
<Base title="Miguel Jackson — migueljackson.dev">
<LanguageToggle />
<CodeRain />
...everything else...
</Base>
Everything between the opening and closing <Base> tags is inserted where <slot /> appears in Base.astro. Think of it like a “content hole” that the parent fills.
3.4 Components — src/components/
| File | What It Produces | Frontmatter Logic | Runs At |
|---|---|---|---|
Beaker.astro | An SVG beaker with CSS-animated bubbles and a sloshing liquid surface. Pure visual decoration. | None (empty --- block) | Build time — outputs static SVG + <style> |
CodeRain.astro | Falling “code tokens” background (const, =>, await, etc.) using CSS animation. | Generates 14 columns of pseudo-random tokens using a seeded PRNG (so builds are reproducible). | Build time — the JS in the frontmatter computes the columns, then the template renders them as static HTML divs. |
GitHubRepos.astro | A grid of public GitHub repositories with name, language, stars, and description. | Fetches the GitHub API (await fetch(...)) to get up to 30 repos, filters out forks/archived, keeps top 9. | Build time — the API call happens during astro build. The response is baked into the HTML. No runtime API calls. |
LanguageToggle.astro | An EN/ES toggle button pair fixed to the top-right corner. | None (empty --- block) | Build time for the HTML/CSS. But it also contains a <script> tag that ships to the browser — the only client-side JS on the entire site. |
Why separate components? Each .astro component encapsulates:
- Its own logic (frontmatter)
- Its own markup (template)
- Its own styles (
<style>block, automatically scoped)
This means Beaker.astro’s CSS class .bubble won’t accidentally style something in GitHubRepos.astro. Astro adds unique data-astro-cid-XXXX attributes to enforce this isolation.
3.5 Content — src/content/
| File | Section | Language |
|---|---|---|
hero.md | Hero header (name + tagline) | English |
hero_es.md | Hero header | Spanish |
about.md | About section | English |
about_es.md | About section | Spanish |
learning.md | Currently learning | English |
learning_es.md | Currently learning | Spanish |
how-this-site-is-built.md | This article | English |
how-this-site-is-built_es.md | This article | Spanish |
The experience timeline lives outside src/content/ because it isn’t a prose section — it’s structured data driving an interactive component. It sits at src/data/experience.json, with both English and Spanish text inline per entry (see UPDATING.md for the schema).
How Astro understands Markdown: When index.astro imports:
import { Content as Hero } from '../content/hero.md';
Astro’s built-in Markdown processor:
- Reads the
.mdfile - Parses the Markdown syntax (headings, bold, links, lists)
- Converts it to HTML
- Wraps it in a renderable Astro component called
Content - When
<Hero />appears in the template, the compiled HTML is inserted at that position
Why Markdown instead of writing HTML directly? Markdown is easier to edit for content updates. You change text in .md files without touching any .astro component logic. The content is separated from the presentation.
3.6 Styles — src/styles/
| File | Role |
|---|---|
global.css | Site-wide styles: CSS custom properties (colors, fonts), layout rules, typography, hero section, sidebar nav, footer, responsive breakpoints. Imported in index.astro’s frontmatter. |
Two kinds of CSS in this project:
- Global (
global.css) — imported as a module in the frontmatter. Astro bundles it without scoping, so rules likeh1,a,bodyapply everywhere. - Scoped (
<style>in components) — Astro rewrites selectors to only match elements within that component. For example,.repos ulinGitHubRepos.astrobecomes.repos ul[data-astro-cid-abc123].
4. The Bilingual System — How EN/ES Works
Both English and Spanish content are rendered into the HTML at build time. The page contains both languages simultaneously, hidden/shown via CSS and a small client-side script.
BUILD TIME (all content baked in) BROWSER (toggle visibility)
───────────────────────────────── ─────────────────────────────
hero.md → <div data-lang-content="en">...</div> ┐
hero_es.md → <div data-lang-content="es">...</div> ├─ LanguageToggle.astro's
about.md → <div data-lang-content="en">...</div> │ <script> sets
about_es.md→ <div data-lang-content="es">...</div> │ display:none on the
...etc for all sections... │ inactive language
┘
User clicks EN/ES button
→ script queries all [data-lang-content] elements
→ hides the non-selected language (display: none)
→ saves preference to localStorage
→ on next visit, reads localStorage (or falls back to browser language)
This approach means:
- No server-side language routing needed
- No duplicate HTML pages
- Instant switching with no page reload
- Works offline after initial load
5. Build Time vs. Browser Time — What Runs Where
| What | When | Where | Example |
|---|---|---|---|
Frontmatter code (between ---) | Build | Your machine | GitHub API fetch, CodeRain column generation |
| Markdown → HTML | Build | Your machine | hero.md → <h1>Miguel Jackson</h1> |
| Template rendering | Build | Your machine | <Beaker /> → SVG markup |
| CSS bundling + scoping | Build | Your machine | Component styles get data-astro-cid |
<script> tags | Browser | User’s browser | Language toggle logic |
| CSS animations | Browser | User’s browser | Beaker bubbles, code rain falling |
The critical insight: After astro build finishes, everything in dist/ is plain HTML/CSS/JS. There is no Astro runtime, no Node process, no build tool involved in serving the site. Any static file server can host the output as-is.
6. Component Lifecycle — The .astro File Anatomy
Every .astro file has the same three-part structure:
┌─────────────────────────────────────┐
│ --- │ FRONTMATTER (optional)
│ // TypeScript / JavaScript │ - Runs at build time only
│ // Import other components │ - Can use await, fetch, fs, etc.
│ // Define variables, fetch data │ - Has access to Astro.props
│ // This code NEVER ships to the │ - Output: variables available
│ // browser │ to the template below
│ --- │
├─────────────────────────────────────┤
│ │ TEMPLATE (required)
│ <div>{variable}</div> │ - HTML-like syntax with {} for
│ <OtherComponent /> │ JS expressions
│ {items.map(i => <li>{i}</li>)} │ - Can include child components
│ │ - Rendered at build time into
│ │ static HTML
├─────────────────────────────────────┤
│ <style> │ STYLE (optional)
│ .class { color: blue; } │ - Scoped to this component
│ </style> │ by default
│ │ - Astro adds data attributes
│ <script> │ for isolation
│ // Client-side JS │
│ </script> │ SCRIPT (optional)
│ │ - Ships to the browser
│ │ - Only way to get interactivity
└─────────────────────────────────────┘
7. Why Each File Type Matters — Summary
astro.config.mjs → Tells Astro HOW to build (site URL, asset path)
src/pages/*.astro → Tells Astro WHAT pages to generate (file-based routing)
src/layouts/*.astro → Provides the HTML shell (head, meta, body wrapper)
src/components/*.astro → Reusable UI pieces (each with logic + markup + styles)
src/content/*.md → The actual text content, easy to edit without code knowledge
src/styles/global.css → Site-wide visual design (not scoped to any component)
The dependency chain for this site:
astro.config.mjs
│
▼
src/pages/index.astro (the main route)
│
├── imports ──→ src/layouts/Base.astro
│ └── provides <html>, <head>, <body>, <slot/>
│
├── imports ──→ src/styles/global.css
│ └── site-wide colors, typography, layout
│
├── imports ──→ src/components/Beaker.astro
│ └── SVG + CSS animation (self-contained)
│
├── imports ──→ src/components/CodeRain.astro
│ └── frontmatter generates data → template renders divs
│
├── imports ──→ src/components/GitHubRepos.astro
│ └── frontmatter fetches GitHub API → template renders grid
│
├── imports ──→ src/components/LanguageToggle.astro
│ └── HTML buttons + <script> for runtime toggle
│
└── imports ──→ src/content/*.md (markdown files)
└── Markdown → HTML → rendered as <Content /> components
8. Case Study — Building the Animated Beaker Icon
The beaker in the hero section is a good example of how to create animated icons in Astro with no JavaScript framework, no image file, and no external library — just an .astro component combining inline SVG for the shape and CSS animations for the motion.
The three ingredients
- Inline SVG drawn directly in the component template (the shape)
- A
<clipPath>and<linearGradient>to give the liquid its contained, gradient fill - CSS
@keyframesin the component’s<style>block (the motion)
All of this lives in one file: src/components/Beaker.astro.
Step 1 — Draw the shape with SVG paths
The beaker outline is a single <path> with a d attribute describing every corner, line, and curve of the silhouette:
<path d="M 35 22 L 35 72 L 15 148 Q 15 158 25 158 L 95 158 Q 105 158 105 148 L 85 72 L 85 22"
fill="none" stroke="#4aa3ff" stroke-width="2.5"/>
The M is “move to”, L is “line to”, and Q is a quadratic Bezier curve (used for the rounded bottom corners). The exact same path is reused as a <clipPath id="beaker-body">. Anything drawn inside a group that references this clip path (<g clip-path="url(#beaker-body)">) gets cut to the beaker’s interior shape — this is the trick that keeps the liquid from spilling out of the glass.
Step 2 — Layer the liquid
Three elements are stacked inside the clipped group:
- A
<rect>filled with a<linearGradient>— the static liquid body, darker at the bottom, lighter at the top. - A
<path class="surface">— a wavy line drawn with SVG’sQ(quadratic Bezier) commands, representing the liquid surface. - Six
<circle class="bubble">elements, each starting near the bottom, for the rising bubbles.
Step 3 — Animate with CSS only
The <style> block at the bottom of the component defines two keyframe animations:
@keyframes rise {
0% { transform: translateY(0); opacity: 0; }
15% { opacity: 0.95; }
85% { opacity: 0.6; }
100% { transform: translateY(-55px); opacity: 0; }
}
@keyframes slosh {
0%, 100% { transform: translateX(-2px); }
50% { transform: translateX(2px); }
}
These are applied with simple CSS rules:
.surfacegetsanimation: slosh 4s ease-in-out infinite;— side-to-side sway..bubblegetsanimation: rise 3.2s infinite ease-in;— vertical rise plus fade in/out.
To make the bubbles rise out of sync (so they look natural instead of marching in formation), each bubble gets a different animation-delay:
.b1 { animation-delay: 0s; }
.b2 { animation-delay: 0.5s; }
.b3 { animation-delay: 1.1s; }
/* ...etc for .b4, .b5, .b6 */
Step 4 — Respect accessibility preferences
A final media query turns animations off for users who have told their OS to reduce motion:
@media (prefers-reduced-motion: reduce) {
.bubble, .surface { animation: none; }
.bubble { opacity: 0.7; }
}
Why this approach fits Astro perfectly
- Zero JavaScript. The entire animation is declarative CSS — nothing ships to the browser beyond the static SVG markup and a few CSS rules.
- Self-contained. The SVG path definitions, the clip path, the gradient, and the animations all live in a single
.astrofile. Dropping<Beaker />anywhere in the site brings the whole icon along. - Scoped styles. Because Astro scopes the
<style>block automatically, the.bubbleclass used here will never collide with a.bubbleclass in another component. - No asset pipeline. No PNG to optimize, no GIF to transcode, no Lottie file to ship. The SVG scales perfectly at any size and the CSS animations are GPU-accelerated.
General recipe for any animated icon in Astro
- Create a new
.astrofile insrc/components/(e.g.,MyIcon.astro). - Paste or hand-write an SVG in the template.
- Give the parts you want to animate distinct
classnames. - Add a
<style>block with@keyframesand apply them viaanimation:on those classes. - Stagger timing with
animation-delayso elements don’t move in unison. - Add a
@media (prefers-reduced-motion: reduce)rule to disable or soften motion. - Import and use it as
<MyIcon />on any page.
That’s the whole pattern — no framework, no runtime, no build configuration. The icon compiles to a handful of static SVG and CSS bytes.
9. From dist/ to Production — The Deployment Pipeline
A static dist/ folder doesn’t help anyone if it’s stuck on a developer’s laptop. The site uses a CI/CD pipeline to push every commit to redundant production hosts, fronted by a single edge tunnel, so visitors see one URL and never know which backend served the bytes.
The flow at a glance
public DNS (the two domains)
│
▼
edge tunnel (HA, two connectors)
╱ ╲
primary node failover node
(home network) (cloud VPS)
Caddy serves dist/ Caddy serves dist/
▲ ▲
│ │
└─── rsync over ──┐ │
private mesh │ │
▼ ▼
self-hosted build runner
(GitHub Actions, dedicated host
on the same private mesh)
▲
│
push to main
│
GitHub repo
Step by step
- Push to
main. A commit lands on the default branch. - GitHub Actions wakes the self-hosted runner. The build job runs on a dedicated Linux host owned by this project — not GitHub’s shared runners — so deploy credentials never leave that machine’s filesystem.
- The runner builds the site. Same command as local:
npm ci && npm run build. Output: a freshdist/. dist/is rsync’d to both production nodes over a private mesh network (no public SSH ports anywhere). Each target receives only the static output — no source code, nonode_modules, no secrets.rsync --deletekeeps each node byte-identical to the latest build.- Both nodes are connectors on the same edge tunnel. Public DNS points at the tunnel, not at either node. The edge picks a healthy connector for each request and shifts traffic away from a connector that disappears. From the visitor’s perspective there is one origin.
- Both nodes serve the same
dist/. The static-file server on each node rewrites the response on the fly so the same build can answer bothmigueljackson.devandmigueljackson.ai— internal links rendered as.devget rewritten to.aiwhen the request arrived on the.aihostname, and vice versa. One build, two domains, no per-domain branching in the source.
Why two backends instead of one?
A single host is a single point of failure: a power outage, ISP blip, or kernel panic and the site goes dark until someone notices. With two connectors:
- The edge tunnel load-balances and fails over automatically at the connection level — no DNS TTL to wait for, no health-check script to maintain.
- Either node can be rebooted, upgraded, or rebuilt without dropping visitor traffic, as long as the other is up.
- The nodes intentionally live on different networks (home + cloud) so a single ISP, power utility, or data-centre incident can’t take both down at once.
Why a self-hosted runner instead of a GitHub-hosted one?
Two reasons:
- Deploy keys stay put. The SSH key that lets the runner write into the production nodes is generated on the runner and never leaves it. There are no SSH private keys in GitHub Secrets, which eliminates that whole class of leak risk.
- Network reach. The runner is on the same private mesh as the production nodes, so it can reach them without anyone publishing an SSH port to the public internet. The production nodes don’t need an open SSH port at all.
What the deploy job doesn’t do
- It doesn’t run on the production nodes. They serve static files only — no Node.js, no build tools, no long-lived processes beyond the static-file server and the tunnel connector.
- It doesn’t bake in any secrets. The build is deterministic — identical input produces identical output. No per-environment variables, no runtime config baked into the bundle.
- It doesn’t reload anything. The static-file server picks up new files on the next request; no service restart, no cache flush, no warm-up step.
The local script still works
deploy.sh at the repo root remains usable for one-off pushes from a laptop, kept around as an emergency path if CI is down. Day-to-day, the workflow is: commit, push, wait ~30 seconds, refresh the browser.
That’s the final mile. The build pipeline (sections 1–7) turns source files into a dist/ folder; the deployment pipeline (this section) turns that folder into a redundant, edge-fronted website with zero manual steps after git push.
Cómo se construyó este sitio
Este sitio web fue construido por Claude Code usando Astro, bajo la supervisión total del administrador del sitio. Cada archivo, componente y línea de contenido fue generado a través de un flujo de trabajo asistido por IA — pero cada decisión, revisión y aprobación la tomó un humano antes de que algo se commiteara o desplegara. El resultado es una colaboración: la IA se encarga de la mecánica, el administrador es el dueño de la dirección.
Astro es un generador de sitios estáticos moderno, diseñado para sitios web enfocados en contenido. Si quieres construir tu propio sitio con Astro, el tutorial oficial Build your first Astro Blog es el mejor punto de partida, y la documentación completa vive en docs.astro.build.
Requisitos mínimos para construir un sitio con Astro
- Node.js — versión
18.17.1,20.3.0o superior (verifica connode -v) - Un editor de texto — se recomienda VS Code, con la extensión oficial Astro VS Code
- Una terminal — Astro se controla a través de su interfaz de línea de comandos (CLI)
- Un gestor de paquetes —
npm(viene con Node.js),pnpmoyarn
Para crear un proyecto nuevo desde cero:
npm create astro@latest
Eso es todo. Astro no tiene dependencias obligatorias más allá de Node.js. Ni base de datos, ni runtime de backend, ni framework que te amarre. Obtienes una carpeta dist/ con HTML, CSS y (opcionalmente) JS estáticos que cualquier servidor web puede alojar.
A continuación hay un recorrido detallado de lo que pasa cuando corres npm run build (o npm run dev) y cómo cada archivo .astro y .md en este proyecto participa en generar el HTML estático final.
1. Qué es Astro (en 30 segundos)
Astro es un generador de sitios estáticos (SSG). Toma archivos fuente — componentes .astro, Markdown .md, CSS, imágenes — y los compila en tiempo de build en archivos HTML, CSS y (opcionalmente) JS planos. La salida es una carpeta dist/ de assets estáticos que cualquier servidor web puede servir directamente. No hay runtime del lado del servidor, ni proceso Node corriendo en producción.
El modelo mental clave: todo corre una sola vez, en tu máquina, y luego queda congelado como HTML.
2. El pipeline de build — Diagrama de flujo
┌─────────────────────────────────────────────────────────────────────┐
│ npm run build │
│ (corre `astro build`) │
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PASO 1 — DESCUBRIMIENTO DE RUTAS │
│ │
│ Astro escanea src/pages/ buscando archivos de ruta. │
│ │
│ src/pages/index.astro → genera /index.html (la página inicial) │
│ │
│ Cada archivo .astro en pages/ = una ruta URL. │
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PASO 2 — EJECUTAR EL FRONTMATTER (bloque entre ---) │
│ │
│ Para cada página, Astro corre el código JavaScript/TypeScript │
│ entre las vallas ---. Este código es del lado del servidor y corre │
│ UNA SOLA VEZ en tiempo de build, nunca en el navegador. │
│ │
│ En index.astro, el frontmatter: │
│ │
│ 1. Importa el layout: Base.astro │
│ 2. Importa componentes: Beaker, CodeRain, GitHubRepos, │
│ LanguageToggle │
│ 3. Importa markdown: hero.md, hero_es.md, about.md, │
│ about_es.md, learning.md, │
│ learning_es.md │
│ 4. Importa data JSON: src/data/experience.json │
│ (alimenta ExperienceTimeline) │
│ 5. Importa estilos: global.css │
│ │
│ El frontmatter de los componentes también corre aquí: │
│ - GitHubRepos.astro: consulta la API de GitHub (await fetch) │
│ - CodeRain.astro: genera datos de columnas pseudo-aleatorios │
│ - Beaker.astro: sin lógica (frontmatter vacío) │
│ - LanguageToggle.astro: sin lógica (frontmatter vacío) │
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PASO 3 — MARKDOWN → HTML │
│ │
│ Cada archivo .md importado como { Content as X } se compila de │
│ Markdown a un componente Astro que renderiza HTML. │
│ │
│ Ejemplo: │
│ hero.md contiene: │
│ # Miguel Jackson │
│ Ingeniero químico con una curiosidad profunda... │
│ │
│ Se vuelve un componente <Content /> que renderiza: │
│ <h1>Miguel Jackson</h1> │
│ <p>Ingeniero químico con una curiosidad profunda...</p> │
│ │
│ El procesador de Markdown incluido en Astro maneja encabezados, │
│ negritas, enlaces y listas — toda la sintaxis estándar de Markdown.│
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PASO 4 — RENDERIZADO DE TEMPLATES (ensamblaje del árbol) │
│ │
│ Astro evalúa el template HTML (todo lo que está debajo del ---) │
│ y renderiza cada componente recursivamente. Aquí es donde se │
│ ensambla el árbol de componentes: │
│ │
│ template de index.astro │
│ └─ <Base title="..."> ← envoltorio del layout │
│ └─ <slot /> ← lo llena el cuerpo de index │
│ ├─ <LanguageToggle /> ← botones EN/ES + script │
│ ├─ <CodeRain /> ← fondo de código cayendo │
│ ├─ <nav> ← navegación lateral │
│ ├─ <main> │
│ │ ├─ <header class="hero"> │
│ │ │ ├─ <Hero /> ← hero.md como HTML │
│ │ │ ├─ <HeroEs /> ← hero_es.md como HTML │
│ │ │ ├─ enlaces (GitHub, LinkedIn) │
│ │ │ └─ <Beaker /> ← animación SVG del beaker │
│ │ ├─ <section id="about"> │
│ │ │ ├─ <About /> ← about.md │
│ │ │ └─ <AboutEs /> ← about_es.md │
│ │ ├─ <section id="experience"> │
│ │ │ └─ <ExperienceTimeline /> │
│ │ │ ↑ src/data/experience.json (EN+ES inline) │
│ │ ├─ <section id="repos"> │
│ │ │ └─ <GitHubRepos /> ← datos de GitHub → HTML │
│ │ ├─ <section id="learning"> │
│ │ │ ├─ <Learning /> ← learning.md │
│ │ │ └─ <LearningEs /> ← learning_es.md │
│ │ └─ <footer> │
│ └─ (fin del slot) │
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PASO 5 — PROCESAMIENTO DE CSS │
│ │
│ Astro recolecta todo el CSS: │
│ │
│ A) global.css (importado en el frontmatter de index.astro) │
│ → aplicado a todo el sitio, sin scope │
│ │
│ B) Bloques <style> dentro de los componentes (Beaker, CodeRain, │
│ GitHubRepos, LanguageToggle) │
│ → automáticamente con SCOPE por Astro. Cada selector recibe un │
│ atributo único data-astro-cid-XXXX para que los estilos no │
│ se filtren entre componentes. │
│ │
│ Todo el CSS se empaqueta, minifica y escribe en dist/_assets/. │
│ Se inyecta automáticamente un tag <link> en el <head> del HTML. │
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PASO 6 — JAVASCRIPT DEL LADO DEL CLIENTE │
│ │
│ La filosofía de Astro: enviar CERO JS por defecto. │
│ │
│ El único JS en la salida viene del tag <script> dentro de │
│ LanguageToggle.astro. Astro lo empaqueta y lo incluye en el HTML │
│ final. Este script: │
│ - Lee la preferencia de idioma desde localStorage │
│ - Alterna la visibilidad de divs data-lang-content="en" / "es" │
│ - Detecta el idioma del navegador como fallback │
│ │
│ Todos los demás componentes (Beaker, CodeRain, GitHubRepos) │
│ producen solo HTML + CSS estático. Sus animaciones son puro CSS. │
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PASO 7 — SALIDA EN dist/ │
│ │
│ Astro escribe la salida final: │
│ │
│ dist/ │
│ ├── index.html ← la página principal, renderizada │
│ ├── _assets/ │
│ │ ├── *.css ← CSS empaquetado + minificado │
│ │ └── *.js ← script del toggle de idioma (único JS) │
│ └── favicon.svg ← asset estático (copiado de public/) │
│ │
│ Esta carpeta es el sitio web entero — lista para desplegar en │
│ cualquier servidor de archivos estáticos. No se necesita Node ni │
│ herramientas de build en tiempo de ejecución. │
└─────────────────────────────────────────────────────────────────────┘
3. Cada archivo y su rol
3.1 Configuración
| Archivo | Propósito |
|---|---|
astro.config.mjs | Le dice a Astro la URL del sitio y que los assets compilados van bajo _assets/. Es la configuración mínima — sin SSR, sin integraciones, puro output estático. |
package.json | Define los scripts (dev, build, preview) y la única dependencia: astro. |
3.2 Páginas — src/pages/
| Archivo | Rol | Cuándo corre |
|---|---|---|
index.astro | La página principal. Es el punto de entrada que Astro descubre al escanear rutas. Importa todo lo demás y define la estructura completa de la página. Un archivo en pages/ = un archivo HTML en dist/. | En tiempo de build — el frontmatter (imports) corre primero, luego se renderiza el template. |
Por qué existe: Astro usa ruteo basado en archivos. Cualquier archivo .astro en src/pages/ se vuelve una URL. index.astro → /index.html → la URL raíz /. Si agregaras src/pages/blog.astro, Astro generaría /blog/index.html.
3.3 Layouts — src/layouts/
| Archivo | Rol | Cuándo corre |
|---|---|---|
Base.astro | Envuelve la página en un documento HTML completo: <!doctype html>, <head> con meta tags / title / favicon, y <body>. Contiene un <slot /> donde se inyecta el contenido de la página. | En tiempo de build — llamado por index.astro vía <Base title="...">. |
Por qué existe: Los layouts separan el boilerplate del HTML (<html>, <head>, <meta>) del contenido de la página. Si agregaras más páginas, todas usarían <Base> para tener los mismos tags del head sin duplicar el bloque <head>.
Cómo funciona <slot />: Cuando index.astro escribe:
<Base title="Miguel Jackson — migueljackson.dev">
<LanguageToggle />
<CodeRain />
...todo lo demás...
</Base>
Todo lo que está entre el tag de apertura y cierre de <Base> se inserta donde aparece <slot /> en Base.astro. Piénsalo como un “hueco de contenido” que el componente padre llena.
3.4 Componentes — src/components/
| Archivo | Lo que produce | Lógica del frontmatter | Corre en |
|---|---|---|---|
Beaker.astro | Un beaker SVG con burbujas animadas por CSS y una superficie de líquido que se balancea. Pura decoración visual. | Ninguna (bloque --- vacío) | Build — produce SVG estático + <style> |
CodeRain.astro | Fondo de “tokens de código” cayendo (const, =>, await, etc.) usando animación CSS. | Genera 14 columnas de tokens pseudo-aleatorios usando un PRNG con semilla (para que los builds sean reproducibles). | Build — el JS del frontmatter calcula las columnas, luego el template las renderiza como divs estáticos. |
GitHubRepos.astro | Una grilla con los repositorios públicos de GitHub con nombre, lenguaje, stars y descripción. | Consulta la API de GitHub (await fetch(...)) para traer hasta 30 repos, filtra forks y archivados, se queda con los primeros 9. | Build — la llamada a la API pasa durante astro build. La respuesta queda horneada en el HTML. Cero llamadas en runtime. |
LanguageToggle.astro | Un par de botones EN/ES fijos en la esquina superior derecha. | Ninguna (bloque --- vacío) | Build para el HTML/CSS. Pero además contiene un tag <script> que sí se envía al navegador — el único JS del lado del cliente en todo el sitio. |
Por qué separar en componentes? Cada componente .astro encapsula:
- Su propia lógica (frontmatter)
- Su propio markup (template)
- Sus propios estilos (bloque
<style>, con scope automático)
Esto significa que la clase CSS .bubble de Beaker.astro no va a estilizar accidentalmente algo en GitHubRepos.astro. Astro agrega atributos únicos data-astro-cid-XXXX para hacer cumplir ese aislamiento.
3.5 Contenido — src/content/
| Archivo | Sección | Idioma |
|---|---|---|
hero.md | Encabezado hero (nombre + tagline) | Inglés |
hero_es.md | Encabezado hero | Español |
about.md | Sección “About” | Inglés |
about_es.md | Sección “About” | Español |
learning.md | Aprendiendo actualmente | Inglés |
learning_es.md | Aprendiendo actualmente | Español |
how-this-site-is-built.md | Este artículo | Inglés |
how-this-site-is-built_es.md | Este artículo | Español |
La línea de tiempo de experiencia vive fuera de src/content/ porque no es una sección de prosa — es data estructurada que alimenta un componente interactivo. Está en src/data/experience.json, con el texto en inglés y español en línea por cada entrada (ver UPDATING.md para el esquema).
Cómo Astro entiende Markdown: Cuando index.astro importa:
import { Content as Hero } from '../content/hero.md';
El procesador de Markdown incluido en Astro:
- Lee el archivo
.md - Parsea la sintaxis de Markdown (encabezados, negritas, enlaces, listas)
- La convierte en HTML
- La envuelve en un componente Astro renderizable llamado
Content - Cuando
<Hero />aparece en el template, el HTML compilado se inserta en esa posición
Por qué Markdown en lugar de escribir HTML directo? Markdown es mucho más fácil de editar para actualizar contenido. Cambias texto en archivos .md sin tocar la lógica de ningún componente .astro. El contenido queda separado de la presentación.
3.6 Estilos — src/styles/
| Archivo | Rol |
|---|---|
global.css | Estilos del sitio entero: propiedades custom de CSS (colores, fuentes), reglas de layout, tipografía, sección hero, navegación lateral, footer, breakpoints responsivos. Se importa en el frontmatter de index.astro. |
Dos tipos de CSS en este proyecto:
- Global (
global.css) — importado como módulo en el frontmatter. Astro lo empaqueta sin scope, así que reglas comoh1,a,bodyaplican en todos lados. - Con scope (
<style>en componentes) — Astro reescribe los selectores para que solo apliquen a elementos dentro de ese componente. Por ejemplo,.repos ulenGitHubRepos.astrose vuelve.repos ul[data-astro-cid-abc123].
4. El sistema bilingüe — Cómo funciona EN/ES
Tanto el contenido en inglés como en español se renderizan en el HTML en tiempo de build. La página contiene los dos idiomas al mismo tiempo, ocultándose/mostrándose vía CSS y un pequeño script del lado del cliente.
BUILD (todo el contenido horneado) NAVEGADOR (alternar visibilidad)
────────────────────────────────── ───────────────────────────────
hero.md → <div data-lang-content="en">...</div> ┐
hero_es.md → <div data-lang-content="es">...</div> ├─ El <script> de
about.md → <div data-lang-content="en">...</div> │ LanguageToggle.astro
about_es.md→ <div data-lang-content="es">...</div> │ pone display:none al
...y así con todas las secciones... │ idioma inactivo
┘
El usuario clickea el botón EN/ES
→ el script busca todos los elementos [data-lang-content]
→ oculta el idioma que no está seleccionado (display: none)
→ guarda la preferencia en localStorage
→ en la próxima visita, lee de localStorage (o usa el idioma del navegador)
Este enfoque significa:
- No hace falta ruteo de idioma del lado del servidor
- No hay páginas HTML duplicadas
- Cambio instantáneo sin recargar la página
- Funciona offline después de la carga inicial
5. Build vs. Navegador — Qué corre dónde
| Qué | Cuándo | Dónde | Ejemplo |
|---|---|---|---|
Código del frontmatter (entre ---) | Build | Tu máquina | Consulta a la API de GitHub, generación de columnas del CodeRain |
| Markdown → HTML | Build | Tu máquina | hero.md → <h1>Miguel Jackson</h1> |
| Renderizado del template | Build | Tu máquina | <Beaker /> → markup SVG |
| Empaquetado + scope de CSS | Build | Tu máquina | Los estilos de componentes reciben data-astro-cid |
Tags <script> | Navegador | Navegador del usuario | Lógica del toggle de idioma |
| Animaciones CSS | Navegador | Navegador del usuario | Burbujas del beaker, código cayendo |
La idea clave: Después de que astro build termina, todo lo que está en dist/ es HTML/CSS/JS plano. No hay runtime de Astro, ni proceso Node, ni herramienta de build involucrada en servir el sitio. Cualquier servidor de archivos estáticos puede hospedar la salida tal cual.
6. Ciclo de vida del componente — Anatomía del archivo .astro
Cada archivo .astro tiene la misma estructura de tres partes:
┌─────────────────────────────────────┐
│ --- │ FRONTMATTER (opcional)
│ // TypeScript / JavaScript │ - Corre solo en build
│ // Importa otros componentes │ - Puede usar await, fetch, fs, etc.
│ // Define variables, trae datos │ - Tiene acceso a Astro.props
│ // Este código NUNCA llega al │ - Salida: variables disponibles
│ // navegador │ para el template de abajo
│ --- │
├─────────────────────────────────────┤
│ │ TEMPLATE (obligatorio)
│ <div>{variable}</div> │ - Sintaxis tipo HTML con {} para
│ <OtroComponente /> │ expresiones JS
│ {items.map(i => <li>{i}</li>)} │ - Puede incluir componentes hijos
│ │ - Se renderiza en build a HTML
│ │ estático
├─────────────────────────────────────┤
│ <style> │ STYLE (opcional)
│ .clase { color: blue; } │ - Con scope al componente
│ </style> │ por defecto
│ │ - Astro agrega atributos data
│ <script> │ para aislarlo
│ // JS del lado del cliente │
│ </script> │ SCRIPT (opcional)
│ │ - Se envía al navegador
│ │ - Es la única manera de tener
│ │ interactividad
└─────────────────────────────────────┘
7. Por qué importa cada tipo de archivo — Resumen
astro.config.mjs → Le dice a Astro CÓMO construir (URL del sitio, ruta de assets)
src/pages/*.astro → Le dice a Astro QUÉ páginas generar (ruteo basado en archivos)
src/layouts/*.astro → Provee el shell del HTML (head, meta, envoltorio del body)
src/components/*.astro → Piezas de UI reutilizables (cada una con lógica + markup + estilos)
src/content/*.md → El contenido de texto real, fácil de editar sin saber código
src/styles/global.css → El diseño visual del sitio entero (sin scope a ningún componente)
La cadena de dependencias de este sitio:
astro.config.mjs
│
▼
src/pages/index.astro (la ruta principal)
│
├── importa ──→ src/layouts/Base.astro
│ └── provee <html>, <head>, <body>, <slot/>
│
├── importa ──→ src/styles/global.css
│ └── colores, tipografía y layout del sitio
│
├── importa ──→ src/components/Beaker.astro
│ └── SVG + animación CSS (autocontenido)
│
├── importa ──→ src/components/CodeRain.astro
│ └── el frontmatter genera datos → el template renderiza divs
│
├── importa ──→ src/components/GitHubRepos.astro
│ └── el frontmatter consulta la API de GitHub → grilla HTML
│
├── importa ──→ src/components/LanguageToggle.astro
│ └── botones HTML + <script> para el toggle en runtime
│
└── importa ──→ src/content/*.md (archivos markdown)
└── Markdown → HTML → renderizado como componentes <Content />
8. Caso práctico — Construyendo el ícono animado del beaker
El beaker que aparece en la sección hero es un buen ejemplo de cómo crear íconos animados en Astro sin framework de JavaScript, sin archivo de imagen y sin librería externa — solo un componente .astro que combina SVG en línea para la forma y animaciones CSS para el movimiento.
Los tres ingredientes
- SVG en línea dibujado directo en el template del componente (la forma)
- Un
<clipPath>y un<linearGradient>para que el líquido quede contenido y tenga un relleno en gradiente @keyframesde CSS en el bloque<style>del componente (el movimiento)
Todo esto vive en un solo archivo: src/components/Beaker.astro.
Paso 1 — Dibujar la forma con paths de SVG
El contorno del beaker es un solo <path> con un atributo d que describe cada esquina, línea y curva de la silueta:
<path d="M 35 22 L 35 72 L 15 148 Q 15 158 25 158 L 95 158 Q 105 158 105 148 L 85 72 L 85 22"
fill="none" stroke="#4aa3ff" stroke-width="2.5"/>
M es “mover a”, L es “línea hasta”, y Q es una curva Bezier cuadrática (la usamos para las esquinas redondeadas de abajo). El mismo path se reusa como <clipPath id="beaker-body">. Cualquier cosa que se dibuje dentro de un grupo que referencia ese clip path (<g clip-path="url(#beaker-body)">) queda recortada a la forma interior del beaker — ese es el truco para que el líquido no se salga del vaso.
Paso 2 — Capas del líquido
Dentro del grupo recortado se apilan tres elementos:
- Un
<rect>con relleno de<linearGradient>— el cuerpo estático del líquido, más oscuro abajo y más claro arriba. - Un
<path class="surface">— una línea ondulada dibujada con los comandosQ(Bezier cuadrática) de SVG, representando la superficie del líquido. - Seis
<circle class="bubble">, cada uno empezando cerca del fondo, para las burbujas que suben.
Paso 3 — Animar con puro CSS
El bloque <style> al final del componente define dos animaciones de keyframes:
@keyframes rise {
0% { transform: translateY(0); opacity: 0; }
15% { opacity: 0.95; }
85% { opacity: 0.6; }
100% { transform: translateY(-55px); opacity: 0; }
}
@keyframes slosh {
0%, 100% { transform: translateX(-2px); }
50% { transform: translateX(2px); }
}
Se aplican con reglas CSS simples:
.surfacerecibeanimation: slosh 4s ease-in-out infinite;— vaivén de lado a lado..bubblerecibeanimation: rise 3.2s infinite ease-in;— subida vertical con fade in/out.
Para que las burbujas no suban todas juntas (así se ve natural, no marcial), cada burbuja recibe un animation-delay distinto:
.b1 { animation-delay: 0s; }
.b2 { animation-delay: 0.5s; }
.b3 { animation-delay: 1.1s; }
/* ...así hasta .b4, .b5, .b6 */
Paso 4 — Respetar las preferencias de accesibilidad
Una media query final apaga las animaciones para los usuarios que le pidieron a su sistema operativo reducir el movimiento:
@media (prefers-reduced-motion: reduce) {
.bubble, .surface { animation: none; }
.bubble { opacity: 0.7; }
}
Por qué este enfoque le calza perfecto a Astro
- Cero JavaScript. Toda la animación es CSS declarativo — al navegador no llega nada más allá del markup SVG estático y un par de reglas de CSS.
- Autocontenido. Las definiciones de los paths SVG, el clip path, el gradiente y las animaciones viven en un solo archivo
.astro. Poner<Beaker />en cualquier lado del sitio arrastra el ícono completo. - Estilos con scope. Como Astro le pone scope automático al bloque
<style>, la clase.bubbleusada aquí nunca va a chocar con una clase.bubbleen otro componente. - Sin pipeline de assets. No hay PNG que optimizar, ni GIF que transcodificar, ni archivo Lottie que enviar. El SVG escala perfecto a cualquier tamaño y las animaciones CSS corren por GPU.
Receta general para cualquier ícono animado en Astro
- Crea un nuevo archivo
.astroensrc/components/(por ejemplo,MyIcon.astro). - Pega o escribe a mano un SVG en el template.
- Dale nombres de
classdistintos a las partes que quieras animar. - Agrega un bloque
<style>con@keyframesy aplícalos conanimation:a esas clases. - Escalona los tiempos con
animation-delaypara que los elementos no se muevan al unísono. - Agrega una regla
@media (prefers-reduced-motion: reduce)para desactivar o suavizar el movimiento. - Impórtalo y úsalo como
<MyIcon />en cualquier página.
Ese es el patrón completo — sin framework, sin runtime, sin configuración de build. El ícono se compila a un puñado de bytes estáticos de SVG y CSS.