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 />
|
||||
|
||||
<main class="flex-1">
|
||||
<!-- Hero Section -->
|
||||
<section class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-20 text-center">
|
||||
<h1 class="text-5xl md:text-6xl font-bold mb-6">
|
||||
Hola, soy <span class="text-primary">David Aragón</span>
|
||||
</h1>
|
||||
<p class="text-xl md:text-2xl text-text-secondary max-w-3xl mx-auto mb-8 leading-relaxed">
|
||||
Indie builder español construyendo en público. Trabajando hacia <strong class="text-primary">€4M ARR</strong>
|
||||
con un portfolio de productos SaaS enfocados en compliance y automatización.
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
<a
|
||||
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>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Astro</h1>
|
||||
</body>
|
||||
</html>
|
||||
<!-- 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