feat: recover portfolio pages from scratch workspaces

- Add base layouts (BaseLayout, BlogLayout, ProjectLayout)
- Add UI components (Header, Footer, Navigation, Card, Tag)
- Add About page with personal story
- Add Projects pages (index, detail)
- Add homepage content
- Add SEO files (robots.txt, webmanifest)

Work was done by agents in isolated workspaces.
Consolidated into main repo for proper git tracking.
This commit is contained in:
wh-leader
2026-05-11 07:41:22 +02:00
parent 85143c0b05
commit 05036766e4
16 changed files with 638 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
Placeholder for favicon - use favicon.io or other generator to create a comprehensive favicon set.
+1
View File
@@ -0,0 +1 @@
Placeholder for avatar - replace with real photo
+1
View File
@@ -0,0 +1 @@
Placeholder for OG image - replace with a 1200x630px image
+8
View File
@@ -0,0 +1,8 @@
User-agent: *
Allow: /
Sitemap: https://davidaragon.impresion3d.pro/sitemap-index.xml
# Disallow admin/private areas (if any)
Disallow: /admin/
Disallow: /api/
+21
View File
@@ -0,0 +1,21 @@
{
"name": "David Aragón - Indie Builder",
"short_name": "DA Portfolio",
"description": "Portfolio personal y blog de indie builder español",
"start_url": "/",
"display": "standalone",
"background_color": "#0a0e27",
"theme_color": "#60a5fa",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
+39
View File
@@ -0,0 +1,39 @@
---
const currentYear = new Date().getFullYear();
const socialLinks = [
{ href: 'https://twitter.com/davidaragon', label: 'X', icon: 'X' },
{ href: 'https://linkedin.com/in/davidaragon', label: 'LinkedIn', icon: 'in' },
{ href: 'https://github.com/davidaragon', label: 'GitHub', icon: 'GH' },
];
---
<footer class="mt-auto border-t border-text-tertiary/20 bg-surface">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="flex flex-col md:flex-row justify-between items-center gap-4">
<p class="text-sm text-text-secondary">
© {currentYear} David Aragón. Construyendo en público.
</p>
<div class="flex items-center gap-4">
{socialLinks.map(link => (
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
class="text-text-secondary hover:text-primary transition-colors text-sm font-medium"
aria-label={link.label}
>
{link.icon}
</a>
))}
</div>
<a
href="/rss.xml"
class="text-sm text-text-secondary hover:text-primary transition-colors"
>
RSS Feed
</a>
</div>
</div>
</footer>
+14
View File
@@ -0,0 +1,14 @@
---
import Navigation from './Navigation.astro';
---
<header class="sticky top-0 z-50 bg-background/80 backdrop-blur-md border-b border-text-tertiary/20">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<a href="/" class="text-xl font-bold text-primary hover:text-secondary transition-colors">
DA
</a>
<Navigation />
</div>
</div>
</header>
+24
View File
@@ -0,0 +1,24 @@
---
const navItems = [
{ href: '/about', label: 'Sobre mí' },
{ href: '/projects', label: 'Proyectos' },
{ href: '/blog', label: 'Blog' },
];
const currentPath = Astro.url.pathname;
---
<nav class="flex items-center gap-6">
{navItems.map(item => (
<a
href={item.href}
class={`text-sm font-medium transition-colors hover:text-primary ${
currentPath.startsWith(item.href)
? 'text-primary'
: 'text-text-secondary'
}`}
>
{item.label}
</a>
))}
</nav>
+17
View File
@@ -0,0 +1,17 @@
---
export interface Props {
href?: string;
class?: string;
}
const { href, class: className } = Astro.props;
const Component = href ? 'a' : 'div';
---
<Component
href={href}
class={`bg-surface rounded-lg border border-text-tertiary/20 p-6 hover:border-primary/40
transition-colors ${className || ''} ${href ? 'block' : ''}`}
>
<slot />
</Component>
+18
View File
@@ -0,0 +1,18 @@
---
export interface Props {
label: string;
variant?: 'primary' | 'secondary' | 'neutral';
}
const { label, variant = 'neutral' } = Astro.props;
const variants = {
primary: 'bg-primary/10 text-primary border-primary/20',
secondary: 'bg-secondary/10 text-secondary border-secondary/20',
neutral: 'bg-text-tertiary/10 text-text-secondary border-text-tertiary/20',
};
---
<span class={`inline-block px-3 py-1 rounded-full text-sm font-medium border ${variants[variant]}`}>
{label}
</span>
+49
View File
@@ -0,0 +1,49 @@
---
import '../styles/global.css';
export interface Props {
title: string;
description: string;
image?: string;
}
const { title, description, image } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const socialImage = image ? new URL(image, Astro.site) : new URL('/images/og-default.jpg', Astro.site);
---
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title} | David Aragón - Indie Builder</title>
<meta name="description" content={description}>
<link rel="canonical" href={canonicalURL} />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:url" content={canonicalURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={socialImage} />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content={canonicalURL} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={socialImage} />
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
</head>
<body class="min-h-screen flex flex-col">
<slot />
</body>
</html>
+34
View File
@@ -0,0 +1,34 @@
---
const blogPostJsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "{title}",
"description": "{description}",
"image": "{socialImage}",
"datePublished": "{publishDate}",
"author": {
"@type": "Person",
"name": "David Aragón",
"url": "https://davidaragon.impresion3d.pro"
},
"publisher": {
"@type": "Person",
"name": "David Aragón",
"url": "https://davidaragon.impresion3d.pro"
},
"keywords": "{keywords}",
"articleSection": "{category}",
"inLanguage": "es-ES"
};
---
<!DOCTYPE html>
<html lang="es">
<head>
<!-- JSON-LD Blog Structured Data -->
<script type="application/ld+json" set:html={JSON.stringify(blogPostJsonLd)} />
</head>
<body>
<!-- Blog content -->
</body>
</html>
+95
View File
@@ -0,0 +1,95 @@
---
import BaseLayout from './BaseLayout.astro';
import Header from '../components/layout/Header.astro';
import Footer from '../components/layout/Footer.astro';
import Tag from '../components/ui/Tag.astro';
import { formatDate } from '../utils/dateFormat';
export interface Props {
title: string;
description: string;
url: string;
github?: string;
status: string;
tags: string[];
startDate: Date;
image?: string;
}
const { title, description, url, github, status, tags, startDate, image } = Astro.props;
const statusColors = {
active: 'text-accent-green',
development: 'text-accent-yellow',
completed: 'text-text-tertiary',
};
---
<BaseLayout title={title} description={description} image={image}>
<Header />
<main class="flex-1">
<article class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<header class="mb-12">
<div class="flex items-center gap-3 mb-4">
<span class={`text-sm font-semibold uppercase tracking-wide ${statusColors[status] || 'text-text-tertiary'}`}>
{status}
</span>
<span class="text-text-tertiary">•</span>
<time datetime={startDate.toISOString()} class="text-sm text-text-tertiary">
Desde {formatDate(startDate)}
</time>
</div>
<h1 class="text-4xl md:text-5xl font-bold mb-4">{title}</h1>
<p class="text-xl text-text-secondary mb-6">{description}</p>
<div class="flex flex-wrap gap-3">
<a
href={url}
target="_blank"
rel="noopener noreferrer"
class="px-6 py-3 bg-primary hover:bg-primary/80 text-background rounded-lg
font-semibold transition-colors"
>
Ver Proyecto →
</a>
{github && (
<a
href={github}
target="_blank"
rel="noopener noreferrer"
class="px-6 py-3 bg-surface hover:bg-surface/80 text-text-primary rounded-lg
font-semibold border border-text-tertiary/20 transition-colors"
>
GitHub →
</a>
)}
</div>
</header>
{image && (
<div class="mb-12 rounded-xl overflow-hidden border border-text-tertiary/20">
<img
src={image}
alt={title}
class="w-full h-auto"
/>
</div>
)}
<div class="prose prose-invert prose-lg max-w-none">
<slot />
</div>
<footer class="mt-12 pt-8 border-t border-text-tertiary/20">
<h3 class="text-lg font-semibold mb-4">Tecnologías</h3>
<div class="flex flex-wrap gap-2">
{tags.map(tag => (
<Tag label={tag} variant="neutral" />
))}
</div>
</footer>
</article>
</main>
<Footer />
</BaseLayout>
+168
View File
@@ -0,0 +1,168 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import Header from '../components/layout/Header.astro';
import Footer from '../components/layout/Footer.astro';
---
<BaseLayout
title="Sobre mí"
description="Indie builder español construyendo productos SaaS. De desarrollador a emprendedor, compartiendo el viaje hacia €4M ARR."
>
<Header />
<main class="flex-1 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<article class="prose prose-invert prose-lg max-w-none">
<h1 class="text-4xl font-bold mb-8">Sobre mí</h1>
<div class="flex flex-col md:flex-row gap-8 mb-8 not-prose">
<div class="flex-shrink-0">
<img
src="/images/avatar.jpg"
alt="David Aragón"
class="w-48 h-48 rounded-full object-cover border-4 border-primary/20"
/>
</div>
<div class="flex-1">
<h2 class="text-2xl font-semibold mb-4 text-text-primary">Hola 👋</h2>
<p class="text-text-secondary text-lg leading-relaxed">
Soy David, desarrollador e indie builder español. Estoy construyendo
un portfolio de productos SaaS enfocados en el mercado español y europeo,
con el objetivo de alcanzar €4M ARR.
</p>
</div>
</div>
<section class="mb-12">
<h2 class="text-3xl font-semibold mb-4">Mi Historia</h2>
<p class="text-text-secondary leading-relaxed mb-4">
Después de años trabajando como desarrollador, decidí dar el salto al emprendimiento
tecnológico. Vi una oportunidad en el mercado español: muchas soluciones B2B están
dominadas por empresas extranjeras que no entienden las particularidades del mercado
local, especialmente en temas de cumplimiento normativo.
</p>
<p class="text-text-secondary leading-relaxed mb-4">
Mi enfoque es construir en público, compartiendo tanto los éxitos como los fracasos.
Creo en la transparencia y en la comunidad indie, y quiero contribuir al ecosistema
de emprendedores españoles compartiendo mi viaje.
</p>
</section>
<section class="mb-12">
<h2 class="text-3xl font-semibold mb-4">Filosofía de Construcción</h2>
<ul class="space-y-3">
<li class="flex items-start">
<span class="text-primary mr-2">→</span>
<span class="text-text-secondary">
<strong class="text-text-primary">Spanish-first:</strong> Productos diseñados
para el mercado español y europeo
</span>
</li>
<li class="flex items-start">
<span class="text-primary mr-2">→</span>
<span class="text-text-secondary">
<strong class="text-text-primary">Build-in-public:</strong> Transparencia
total en métricas, aprendizajes y decisiones
</span>
</li>
<li class="flex items-start">
<span class="text-primary mr-2">→</span>
<span class="text-text-secondary">
<strong class="text-text-primary">AI-assisted:</strong> Aprovechando herramientas
de IA para acelerar desarrollo (79% AI-assisted en WarrantyHub)
</span>
</li>
<li class="flex items-start">
<span class="text-primary mr-2">→</span>
<span class="text-text-secondary">
<strong class="text-text-primary">Compliance-focused:</strong> Ventaja competitiva
en regulaciones españolas y europeas
</span>
</li>
</ul>
</section>
<section class="mb-12">
<h2 class="text-3xl font-semibold mb-4">Stack Técnico</h2>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 not-prose">
<div class="bg-surface px-4 py-3 rounded-lg border border-text-tertiary/20">React</div>
<div class="bg-surface px-4 py-3 rounded-lg border border-text-tertiary/20">TypeScript</div>
<div class="bg-surface px-4 py-3 rounded-lg border border-text-tertiary/20">Node.js</div>
<div class="bg-surface px-4 py-3 rounded-lg border border-text-tertiary/20">Astro</div>
<div class="bg-surface px-4 py-3 rounded-lg border border-text-tertiary/20">Tailwind CSS</div>
<div class="bg-surface px-4 py-3 rounded-lg border border-text-tertiary/20">Docker</div>
<div class="bg-surface px-4 py-3 rounded-lg border border-text-tertiary/20">PostgreSQL</div>
<div class="bg-surface px-4 py-3 rounded-lg border border-text-tertiary/20">Python</div>
<div class="bg-surface px-4 py-3 rounded-lg border border-text-tertiary/20">FastAPI</div>
</div>
</section>
<section class="mb-12">
<h2 class="text-3xl font-semibold mb-4">En Qué Estoy Trabajando</h2>
<div class="bg-surface p-6 rounded-lg border border-primary/20">
<h3 class="text-xl font-semibold mb-2 text-primary">Portfolio SaaS €4M ARR</h3>
<p class="text-text-secondary mb-4">
Construyendo un portfolio de productos SaaS en 3 fases:
</p>
<ul class="space-y-2">
<li class="text-text-secondary">
<strong class="text-text-primary">Fase 1 (0-8 meses):</strong> WarrantyHub,
Verifactu compliance, análisis de contratos → €825K ARR
</li>
<li class="text-text-secondary">
<strong class="text-text-primary">Fase 2 (8-18 meses):</strong> Plataforma
de auditoría, integraciones → €1.8M ARR
</li>
<li class="text-text-secondary">
<strong class="text-text-primary">Fase 3 (18+ meses):</strong> Expansión
europea, liderazgo → €4M+ ARR
</li>
</ul>
</div>
</section>
<section>
<h2 class="text-3xl font-semibold mb-4">Conecta Conmigo</h2>
<p class="text-text-secondary mb-6">
Siempre estoy abierto a conversar con otros builders, inversores, o simplemente
personas interesadas en emprendimiento tecnológico español.
</p>
<div class="flex flex-wrap gap-4 not-prose">
<a
href="https://twitter.com/davidaragon"
target="_blank"
rel="noopener noreferrer"
class="bg-primary/10 hover:bg-primary/20 text-primary px-6 py-3 rounded-lg
font-medium transition-colors border border-primary/20"
>
X (Twitter)
</a>
<a
href="https://linkedin.com/in/davidaragon"
target="_blank"
rel="noopener noreferrer"
class="bg-primary/10 hover:bg-primary/20 text-primary px-6 py-3 rounded-lg
font-medium transition-colors border border-primary/20"
>
LinkedIn
</a>
<a
href="https://github.com/davidaragon"
target="_blank"
rel="noopener noreferrer"
class="bg-primary/10 hover:bg-primary/20 text-primary px-6 py-3 rounded-lg
font-medium transition-colors border border-primary/20"
>
GitHub
</a>
<a
href="mailto:david@impresion3d.pro"
class="bg-secondary/10 hover:bg-secondary/20 text-secondary px-6 py-3 rounded-lg
font-medium transition-colors border border-secondary/20"
>
Email
</a>
</div>
</section>
</article>
</main>
<Footer />
</BaseLayout>
+21
View File
@@ -0,0 +1,21 @@
---
import { getCollection } from 'astro:content';
import ProjectLayout from '../../layouts/ProjectLayout.astro';
export async function getStaticPaths() {
const projects = await getCollection('projects');
return projects
.filter(p => !p.data.draft)
.map(project => ({
params: { slug: project.slug },
props: { project },
}));
}
const { project } = Astro.props;
const { Content } = await project.render();
---
<ProjectLayout {...project.data}>
<Content />
</ProjectLayout>
+127
View File
@@ -0,0 +1,127 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/layout/Header.astro';
import Footer from '../../components/layout/Footer.astro';
import Card from '../../components/ui/Card.astro';
import Tag from '../../components/ui/Tag.astro';
import { getCollection } from 'astro:content';
const allProjects = (await getCollection('projects'))
.filter(p => !p.data.draft)
.sort((a, b) => b.data.startDate.getTime() - a.data.startDate.getTime());
const featured = allProjects.filter(p => p.data.featured);
const active = allProjects.filter(p => p.data.status === 'active' && !p.data.featured);
const others = allProjects.filter(p => p.data.status !== 'active' && !p.data.featured);
---
<BaseLayout
title="Proyectos"
description="Portfolio de productos SaaS: WarrantyHub, Garage61 API, y más proyectos en desarrollo"
>
<Header />
<main class="flex-1 max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<header class="mb-12">
<h1 class="text-4xl font-bold mb-4">Proyectos</h1>
<p class="text-xl text-text-secondary max-w-3xl">
Portfolio de productos SaaS construidos en público. Desde ideas hasta
productos escalables generando revenue recurrente.
</p>
</header>
{featured.length > 0 && (
<section class="mb-16">
<h2 class="text-2xl font-semibold mb-6 text-primary">🌟 Proyectos Destacados</h2>
<div class="grid md:grid-cols-2 gap-6">
{featured.map(project => (
<Card href={`/projects/${project.slug}`} class="flex flex-col">
{project.data.image && (
<img
src={project.data.image}
alt={project.data.title}
class="w-full h-48 object-cover rounded-t-lg -m-6 mb-4"
/>
)}
<h3 class="text-2xl font-semibold mb-2 text-text-primary">
{project.data.title}
</h3>
<p class="text-text-secondary mb-4 flex-1">
{project.data.description}
</p>
<div class="flex flex-wrap gap-2 mb-4">
{project.data.tags.slice(0, 4).map(tag => (
<Tag label={tag} variant="primary" />
))}
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-accent-green font-medium capitalize">
{project.data.status}
</span>
<span class="text-primary text-sm font-medium">
Ver proyecto →
</span>
</div>
</Card>
))}
</div>
</section>
)}
{active.length > 0 && (
<section class="mb-16">
<h2 class="text-2xl font-semibold mb-6">Proyectos Activos</h2>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{active.map(project => (
<Card href={`/projects/${project.slug}`}>
<h3 class="text-xl font-semibold mb-2 text-text-primary">
{project.data.title}
</h3>
<p class="text-text-secondary mb-4">
{project.data.description}
</p>
<div class="flex flex-wrap gap-2 mb-4">
{project.data.tags.slice(0, 3).map(tag => (
<Tag label={tag} variant="neutral" />
))}
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-accent-green font-medium">Activo</span>
<span class="text-primary text-sm font-medium">Ver →</span>
</div>
</Card>
))}
</div>
</section>
)}
{others.length > 0 && (
<section>
<h2 class="text-2xl font-semibold mb-6">Otros Proyectos</h2>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{others.map(project => (
<Card href={`/projects/${project.slug}`}>
<h3 class="text-xl font-semibold mb-2 text-text-primary">
{project.data.title}
</h3>
<p class="text-text-secondary mb-4">
{project.data.description}
</p>
<div class="flex flex-wrap gap-2 mb-4">
{project.data.tags.slice(0, 3).map(tag => (
<Tag label={tag} variant="neutral" />
))}
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-text-tertiary capitalize">
{project.data.status}
</span>
<span class="text-primary text-sm font-medium">Ver →</span>
</div>
</Card>
))}
</div>
</section>
)}
</main>
<Footer />
</BaseLayout>