feat: add homepage, content collections, and utils
- Homepage with hero, featured projects, latest posts sections - Content collections config (blog + projects schemas) - Date formatting and reading time utilities - Sample blog post and project for validation
This commit is contained in:
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: "Welcome to My Portfolio"
|
||||||
|
description: "First post to validate content collections setup"
|
||||||
|
publishDate: 2026-05-08
|
||||||
|
tags: ["meta", "announcement"]
|
||||||
|
category: "personal"
|
||||||
|
featured: true
|
||||||
|
draft: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Welcome
|
||||||
|
|
||||||
|
This is a sample blog post to validate that content collections are working correctly.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Markdown content with frontmatter validation
|
||||||
|
- Type-safe schema with Zod
|
||||||
|
- Automatic type generation for TypeScript
|
||||||
|
|
||||||
|
## Code Example
|
||||||
|
```typescript
|
||||||
|
const greeting = "Hello, World!";
|
||||||
|
console.log(greeting);
|
||||||
|
```
|
||||||
|
|
||||||
|
This post will be replaced with real content later.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
|
||||||
|
const blog = defineCollection({
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
publishDate: z.coerce.date(),
|
||||||
|
author: z.string().default('David Aragón'),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
category: z.enum(['technical', 'business', 'personal']),
|
||||||
|
featured: z.boolean().default(false),
|
||||||
|
draft: z.boolean().default(false),
|
||||||
|
image: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const projects = defineCollection({
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
url: z.string().url(),
|
||||||
|
github: z.string().url().optional(),
|
||||||
|
status: z.enum(['active', 'development', 'completed']),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
startDate: z.coerce.date(),
|
||||||
|
featured: z.boolean().default(false),
|
||||||
|
image: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { blog, projects };
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
title: "Sample Project"
|
||||||
|
description: "Test project for content collections validation"
|
||||||
|
url: "https://example.com"
|
||||||
|
status: "development"
|
||||||
|
tags: ["test"]
|
||||||
|
startDate: 2026-05-08
|
||||||
|
featured: false
|
||||||
|
draft: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Sample Project
|
||||||
|
|
||||||
|
This is a placeholder project to validate content collections.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- Astro
|
||||||
|
- TypeScript
|
||||||
|
- Tailwind CSS
|
||||||
|
|
||||||
|
This will be replaced with real project case studies.
|
||||||
+163
-14
@@ -1,17 +1,166 @@
|
|||||||
---
|
---
|
||||||
|
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';
|
||||||
|
import { formatDate } from '../utils/dateFormat';
|
||||||
|
|
||||||
|
// Fetch featured projects and recent blog posts
|
||||||
|
const projects = (await getCollection('projects'))
|
||||||
|
.filter(p => !p.data.draft && p.data.featured)
|
||||||
|
.slice(0, 3);
|
||||||
|
|
||||||
|
const blogPosts = (await getCollection('blog'))
|
||||||
|
.filter(p => !p.data.draft)
|
||||||
|
.sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime())
|
||||||
|
.slice(0, 3);
|
||||||
|
---
|
||||||
|
|
||||||
---
|
<BaseLayout
|
||||||
|
title="Inicio"
|
||||||
|
description="Portfolio personal de David Aragón - Indie builder construyendo €4M ARR en productos SaaS españoles"
|
||||||
|
>
|
||||||
|
<Header />
|
||||||
|
|
||||||
<html lang="en">
|
<main class="flex-1">
|
||||||
<head>
|
<!-- Hero Section -->
|
||||||
<meta charset="utf-8" />
|
<section class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-20 text-center">
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<h1 class="text-5xl md:text-6xl font-bold mb-6">
|
||||||
<link rel="icon" href="/favicon.ico" />
|
Hola, soy <span class="text-primary">David Aragón</span>
|
||||||
<meta name="viewport" content="width=device-width" />
|
</h1>
|
||||||
<meta name="generator" content={Astro.generator} />
|
<p class="text-xl md:text-2xl text-text-secondary max-w-3xl mx-auto mb-8 leading-relaxed">
|
||||||
<title>Astro</title>
|
Indie builder español construyendo en público. Trabajando hacia <strong class="text-primary">€4M ARR</strong>
|
||||||
</head>
|
con un portfolio de productos SaaS enfocados en compliance y automatización.
|
||||||
<body>
|
</p>
|
||||||
<h1>Astro</h1>
|
<div class="flex flex-wrap justify-center gap-4">
|
||||||
</body>
|
<a
|
||||||
</html>
|
href="/projects"
|
||||||
|
class="bg-primary hover:bg-primary/80 text-background px-8 py-3 rounded-lg
|
||||||
|
font-semibold transition-colors"
|
||||||
|
>
|
||||||
|
Ver Proyectos
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/blog"
|
||||||
|
class="bg-surface hover:bg-surface/80 text-text-primary px-8 py-3 rounded-lg
|
||||||
|
font-semibold border border-text-tertiary/20 transition-colors"
|
||||||
|
>
|
||||||
|
Leer Blog
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Featured Projects -->
|
||||||
|
<section class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
|
<h2 class="text-3xl font-bold">Proyectos Destacados</h2>
|
||||||
|
<a
|
||||||
|
href="/projects"
|
||||||
|
class="text-primary hover:text-secondary transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Ver todos →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{projects.length > 0 ? (
|
||||||
|
projects.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="primary" />
|
||||||
|
))}
|
||||||
|
</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 proyecto →</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div class="col-span-full text-center py-12">
|
||||||
|
<p class="text-text-secondary">
|
||||||
|
Proyectos destacados próximamente. Mientras tanto,
|
||||||
|
<a href="/blog" class="text-primary hover:underline">lee el blog</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Latest Blog Posts -->
|
||||||
|
<section class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
|
<h2 class="text-3xl font-bold">Últimas Publicaciones</h2>
|
||||||
|
<a
|
||||||
|
href="/blog"
|
||||||
|
class="text-primary hover:text-secondary transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Ver todas →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{blogPosts.length > 0 ? (
|
||||||
|
blogPosts.map(post => (
|
||||||
|
<Card href={`/blog/${post.slug}`}>
|
||||||
|
<div class="mb-3">
|
||||||
|
<Tag label={post.data.category} variant="secondary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold mb-2 text-text-primary">{post.data.title}</h3>
|
||||||
|
<p class="text-text-secondary mb-4">{post.data.description}</p>
|
||||||
|
<div class="flex items-center justify-between text-sm text-text-tertiary">
|
||||||
|
<time datetime={post.data.publishDate.toISOString()}>
|
||||||
|
{formatDate(post.data.publishDate)}
|
||||||
|
</time>
|
||||||
|
<span class="text-primary font-medium">Leer más →</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div class="col-span-full text-center py-12">
|
||||||
|
<p class="text-text-secondary">
|
||||||
|
Publicaciones próximamente. Suscríbete para recibir actualizaciones.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Newsletter CTA -->
|
||||||
|
<section class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||||
|
<div class="bg-gradient-to-r from-primary/10 to-secondary/10 rounded-2xl p-8 md:p-12 text-center border border-primary/20">
|
||||||
|
<h2 class="text-3xl font-bold mb-4">Construyendo en Público</h2>
|
||||||
|
<p class="text-text-secondary mb-6 max-w-2xl mx-auto">
|
||||||
|
Suscríbete para recibir actualizaciones sobre nuevos proyectos, aprendizajes,
|
||||||
|
y transparencia total en el viaje hacia €4M ARR.
|
||||||
|
</p>
|
||||||
|
<form class="flex flex-col sm:flex-row gap-3 max-w-md mx-auto">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="tu@email.com"
|
||||||
|
required
|
||||||
|
class="flex-1 px-4 py-3 rounded-lg bg-background border border-text-tertiary/20
|
||||||
|
text-text-primary placeholder-text-tertiary focus:outline-none focus:border-primary"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="bg-primary hover:bg-primary/80 text-background px-6 py-3 rounded-lg
|
||||||
|
font-semibold transition-colors whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Suscribirse
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<p class="text-xs text-text-tertiary mt-4">
|
||||||
|
Sin spam. Cancela cuando quieras.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</BaseLayout>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export function formatDate(date: Date): string {
|
||||||
|
return new Intl.DateTimeFormat('es-ES', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export function getReadingTime(content: string): string {
|
||||||
|
const wordsPerMinute = 200;
|
||||||
|
const words = content.trim().split(/\s+/).length;
|
||||||
|
const minutes = Math.ceil(words / wordsPerMinute);
|
||||||
|
return `${minutes} min lectura`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user