Compare commits
37 Commits
b93018b4cd
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ab6633cc2 | |||
| 904684e6b8 | |||
| 54cad6684c | |||
| 3f7e54c72e | |||
| 0b7021f827 | |||
| 21a63b6947 | |||
| c514b3bce1 | |||
| 58f0a00d4c | |||
| 83845f4894 | |||
| 7e90812373 | |||
| 1037f6a4ed | |||
| 8176b0b09b | |||
| 5937425872 | |||
| 42873ad9cc | |||
| 35f3ba8767 | |||
| ce8415c26f | |||
| 8cd0b2fa24 | |||
| 8a30951a64 | |||
| f93ae22b77 | |||
| 65f9defcdd | |||
| 15c946c0a1 | |||
| ada08a31be | |||
| d11c9a6a1e | |||
| 259fef1f95 | |||
| 47bf985a79 | |||
| 8d85589f0c | |||
| c31692bae0 | |||
| 695dad2770 | |||
| 01ce85a140 | |||
| e9be93f24d | |||
| cc7043148a | |||
| 600e9ac3b4 | |||
| 05036766e4 | |||
| 85143c0b05 | |||
| 8c11722608 | |||
| f252241f9e | |||
| e7fd109a7a |
@@ -0,0 +1,11 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
dist
|
||||||
|
.astro
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: CI/CD Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Build & Deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
run: |
|
||||||
|
docker login gitlab.impresion3d.pro -u "${{ secrets.DOCKER_USERNAME }}" -p "${{ secrets.DOCKER_PASSWORD }}"
|
||||||
|
docker build -t gitlab.impresion3d.pro/root/davidaragon-portfolio:latest .
|
||||||
|
docker push gitlab.impresion3d.pro/root/davidaragon-portfolio:latest
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# jetbrains setting folder
|
||||||
|
.idea/
|
||||||
+527
@@ -0,0 +1,527 @@
|
|||||||
|
# Personal Portfolio Architecture - davidaragon.impresion3d.pro
|
||||||
|
|
||||||
|
**Project**: Personal Tech Portfolio & Dev Blog
|
||||||
|
**Tech Stack**: Astro + Tailwind CSS
|
||||||
|
**Deploy**: Docker + Portainer on 192.168.1.30
|
||||||
|
**Investment**: €8,100 (4-week sprint)
|
||||||
|
**Strategic Value**: €45-95K 3-year value (477-1,118% ROI)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Technology Stack
|
||||||
|
|
||||||
|
### Core Framework
|
||||||
|
- **Astro 4.x**: Static site generator optimized for content-first sites
|
||||||
|
- Ultra-fast static HTML generation
|
||||||
|
- Markdown-first content workflow
|
||||||
|
- Minimal JavaScript (island architecture)
|
||||||
|
- Built-in SEO optimization
|
||||||
|
- Zero-config TypeScript support
|
||||||
|
|
||||||
|
### Styling & UI
|
||||||
|
- **Tailwind CSS 3.x**: Utility-first CSS framework
|
||||||
|
- Dark mode optimized
|
||||||
|
- Responsive design system
|
||||||
|
- Custom color palette
|
||||||
|
- Typography plugin for blog content
|
||||||
|
|
||||||
|
### Content Management
|
||||||
|
- **Astro Content Collections**: Type-safe content management
|
||||||
|
- Blog posts (Markdown + frontmatter)
|
||||||
|
- Project case studies
|
||||||
|
- Schema validation with Zod
|
||||||
|
- Automatic type generation
|
||||||
|
|
||||||
|
### Build & Deploy
|
||||||
|
- **Docker**: Containerized deployment
|
||||||
|
- Nginx as static file server
|
||||||
|
- Multi-stage build (node build + nginx serve)
|
||||||
|
- Optimized image size (<50MB)
|
||||||
|
- **Portainer**: Container orchestration on 192.168.1.30
|
||||||
|
- Stack deployment via docker-compose
|
||||||
|
- Environment variable management
|
||||||
|
- Persistent volume for logs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
davidaragon-portfolio/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/ # Reusable UI components
|
||||||
|
│ │ ├── layout/
|
||||||
|
│ │ │ ├── Header.astro
|
||||||
|
│ │ │ ├── Footer.astro
|
||||||
|
│ │ │ └── Navigation.astro
|
||||||
|
│ │ ├── blog/
|
||||||
|
│ │ │ ├── PostCard.astro
|
||||||
|
│ │ │ ├── PostList.astro
|
||||||
|
│ │ │ └── PostLayout.astro
|
||||||
|
│ │ ├── projects/
|
||||||
|
│ │ │ ├── ProjectCard.astro
|
||||||
|
│ │ │ └── ProjectGrid.astro
|
||||||
|
│ │ └── ui/
|
||||||
|
│ │ ├── Button.astro
|
||||||
|
│ │ ├── Card.astro
|
||||||
|
│ │ └── Tag.astro
|
||||||
|
│ ├── content/ # Content collections
|
||||||
|
│ │ ├── config.ts # Collection schemas
|
||||||
|
│ │ ├── blog/ # Blog posts (Markdown)
|
||||||
|
│ │ │ ├── building-in-public.md
|
||||||
|
│ │ │ ├── spanish-saas.md
|
||||||
|
│ │ │ └── warrantyhub-pwa.md
|
||||||
|
│ │ └── projects/ # Project case studies
|
||||||
|
│ │ ├── warrantyhub.md
|
||||||
|
│ │ └── garage61.md
|
||||||
|
│ ├── layouts/ # Page layouts
|
||||||
|
│ │ ├── BaseLayout.astro
|
||||||
|
│ │ ├── BlogLayout.astro
|
||||||
|
│ │ └── ProjectLayout.astro
|
||||||
|
│ ├── pages/ # Routes (file-based routing)
|
||||||
|
│ │ ├── index.astro # Homepage
|
||||||
|
│ │ ├── about.astro # About page
|
||||||
|
│ │ ├── projects/
|
||||||
|
│ │ │ ├── index.astro # Projects listing
|
||||||
|
│ │ │ └── [slug].astro # Individual project
|
||||||
|
│ │ ├── blog/
|
||||||
|
│ │ │ ├── index.astro # Blog listing
|
||||||
|
│ │ │ └── [slug].astro # Individual post
|
||||||
|
│ │ └── rss.xml.ts # RSS feed generator
|
||||||
|
│ ├── styles/ # Global styles
|
||||||
|
│ │ ├── global.css # Base styles + Tailwind
|
||||||
|
│ │ └── syntax.css # Code syntax highlighting
|
||||||
|
│ └── utils/ # Helper functions
|
||||||
|
│ ├── dateFormat.ts
|
||||||
|
│ └── readingTime.ts
|
||||||
|
├── public/ # Static assets
|
||||||
|
│ ├── images/
|
||||||
|
│ │ ├── avatar.jpg
|
||||||
|
│ │ └── projects/
|
||||||
|
│ ├── favicon.ico
|
||||||
|
│ └── robots.txt
|
||||||
|
├── .docker/
|
||||||
|
│ ├── Dockerfile # Multi-stage build
|
||||||
|
│ ├── nginx.conf # Nginx configuration
|
||||||
|
│ └── docker-compose.yml # Portainer stack
|
||||||
|
├── astro.config.mjs # Astro configuration
|
||||||
|
├── tailwind.config.cjs # Tailwind configuration
|
||||||
|
├── tsconfig.json # TypeScript configuration
|
||||||
|
└── package.json # Dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Content Collections Schema
|
||||||
|
|
||||||
|
### Blog Posts
|
||||||
|
```typescript
|
||||||
|
// src/content/config.ts
|
||||||
|
const blog = defineCollection({
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
publishDate: z.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(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Projects
|
||||||
|
```typescript
|
||||||
|
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.date(),
|
||||||
|
featured: z.boolean().default(false),
|
||||||
|
image: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Design System
|
||||||
|
|
||||||
|
### Color Palette (Dark Mode Optimized)
|
||||||
|
```css
|
||||||
|
/* Tailwind config */
|
||||||
|
colors: {
|
||||||
|
background: '#0a0e27', // Deep navy
|
||||||
|
surface: '#131729', // Card backgrounds
|
||||||
|
primary: '#60a5fa', // Blue accent
|
||||||
|
secondary: '#a78bfa', // Purple accent
|
||||||
|
text: {
|
||||||
|
primary: '#f1f5f9', // Light text
|
||||||
|
secondary: '#94a3b8', // Muted text
|
||||||
|
tertiary: '#64748b', // Subtle text
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
green: '#34d399', // Success
|
||||||
|
red: '#f87171', // Error
|
||||||
|
yellow: '#fbbf24', // Warning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
- **Headings**: Inter (font-weight: 600-800)
|
||||||
|
- **Body**: Inter (font-weight: 400-500)
|
||||||
|
- **Code**: JetBrains Mono (font-weight: 400)
|
||||||
|
- **Scale**:
|
||||||
|
- H1: 3rem (48px)
|
||||||
|
- H2: 2.25rem (36px)
|
||||||
|
- H3: 1.875rem (30px)
|
||||||
|
- Body: 1rem (16px)
|
||||||
|
- Small: 0.875rem (14px)
|
||||||
|
|
||||||
|
### Component System
|
||||||
|
- **Cards**: Glassmorphism effect with subtle backdrop blur
|
||||||
|
- **Buttons**: Hover transitions, focus states
|
||||||
|
- **Navigation**: Sticky header with scroll-based styling
|
||||||
|
- **Links**: Underline on hover, smooth transitions
|
||||||
|
- **Code blocks**: Syntax highlighting with Shiki (VS Code Dark+ theme)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. SEO Strategy
|
||||||
|
|
||||||
|
### Meta Tags
|
||||||
|
- **Title template**: `{page} | David Aragón - Indie Builder`
|
||||||
|
- **Description**: Dynamic per page
|
||||||
|
- **Open Graph**: Images for all blog posts and projects
|
||||||
|
- **Twitter Cards**: Summary with large image
|
||||||
|
|
||||||
|
### Sitemap
|
||||||
|
- Auto-generated via Astro integration
|
||||||
|
- Includes all pages, blog posts, projects
|
||||||
|
- Priority: Homepage (1.0), Blog/Projects (0.8), Posts (0.6)
|
||||||
|
|
||||||
|
### RSS Feed
|
||||||
|
- Full-text feed at `/rss.xml`
|
||||||
|
- Includes title, description, content, publish date
|
||||||
|
- Auto-updated on build
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **Target Lighthouse score**: 95+ across all metrics
|
||||||
|
- **Core Web Vitals**:
|
||||||
|
- LCP: <2.5s
|
||||||
|
- FID: <100ms
|
||||||
|
- CLS: <0.1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Docker Deployment Architecture
|
||||||
|
|
||||||
|
### Multi-Stage Dockerfile
|
||||||
|
```dockerfile
|
||||||
|
# Stage 1: Build
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 2: Serve
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
COPY .docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nginx Configuration
|
||||||
|
- Serve static files from `/usr/share/nginx/html`
|
||||||
|
- Gzip compression for text assets
|
||||||
|
- Cache-Control headers (1 year for assets, 1 hour for HTML)
|
||||||
|
- 404 fallback to custom 404.html
|
||||||
|
- Security headers (CSP, X-Frame-Options, etc.)
|
||||||
|
|
||||||
|
### Docker Compose (Portainer Stack)
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
portfolio:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: .docker/Dockerfile
|
||||||
|
container_name: davidaragon-portfolio
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3010:80"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.portfolio.rule=Host(`davidaragon.impresion3d.pro`)"
|
||||||
|
- "traefik.http.services.portfolio.loadbalancer.server.port=80"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment on 192.168.1.30
|
||||||
|
1. **Portainer stack deployment**:
|
||||||
|
- Create new stack "davidaragon-portfolio"
|
||||||
|
- Upload docker-compose.yml
|
||||||
|
- Configure environment variables
|
||||||
|
- Deploy stack
|
||||||
|
|
||||||
|
2. **Domain configuration**:
|
||||||
|
- DNS A record: `davidaragon.impresion3d.pro` → 192.168.1.30
|
||||||
|
- Traefik reverse proxy for HTTPS
|
||||||
|
- Let's Encrypt SSL certificate
|
||||||
|
|
||||||
|
3. **CI/CD (future)**:
|
||||||
|
- Gitea Actions on push to main branch
|
||||||
|
- Build Docker image
|
||||||
|
- Push to local registry or rebuild on server
|
||||||
|
- Portainer webhook to redeploy stack
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Content Strategy
|
||||||
|
|
||||||
|
### Launch Content (5 Blog Posts)
|
||||||
|
1. **"Building in Public: My €4M ARR Portfolio Plan"**
|
||||||
|
- Category: Business
|
||||||
|
- Tags: indie hacker, SaaS portfolio, build-in-public
|
||||||
|
- Length: 1,500-2,000 words
|
||||||
|
|
||||||
|
2. **"Why I'm Building Spanish-First SaaS Products"**
|
||||||
|
- Category: Business
|
||||||
|
- Tags: Spanish SaaS, European market, compliance
|
||||||
|
- Length: 1,200-1,500 words
|
||||||
|
|
||||||
|
3. **"WarrantyHub: Building a PWA with 79% AI-Assisted Development"**
|
||||||
|
- Category: Technical
|
||||||
|
- Tags: PWA, AI-assisted coding, React, IndexedDB
|
||||||
|
- Length: 2,000-2,500 words
|
||||||
|
|
||||||
|
4. **"Validating Business Ideas: My €4M Research Framework"**
|
||||||
|
- Category: Business
|
||||||
|
- Tags: business validation, market research
|
||||||
|
- Length: 1,800-2,200 words
|
||||||
|
|
||||||
|
5. **"From Concept to €502K ARR: WarrantyHub Financial Model"**
|
||||||
|
- Category: Business
|
||||||
|
- Tags: SaaS financial model, ARR projections
|
||||||
|
- Length: 1,500-2,000 words
|
||||||
|
|
||||||
|
### Project Case Studies (2)
|
||||||
|
1. **WarrantyHub**: Problem, solution, architecture, status
|
||||||
|
2. **Garage61 API**: Platform strategy, Python library, future plans
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Pages Architecture
|
||||||
|
|
||||||
|
### Homepage (`/`)
|
||||||
|
- **Hero section**: Personal tagline + CTA
|
||||||
|
- **Featured projects**: 3-4 project cards
|
||||||
|
- **Latest blog posts**: 3 most recent posts
|
||||||
|
- **Newsletter signup**: Email collection form
|
||||||
|
- **Social links**: X, LinkedIn, GitHub
|
||||||
|
|
||||||
|
### About Page (`/about`)
|
||||||
|
- **Personal story**: 400-600 word narrative
|
||||||
|
- **Professional photo**: Authentic builder vibe
|
||||||
|
- **Technical skills**: Tech stack showcase
|
||||||
|
- **Current focus**: Portfolio goals and timeline
|
||||||
|
- **Contact**: Email, social media
|
||||||
|
|
||||||
|
### Projects Page (`/projects`)
|
||||||
|
- **Project grid**: All projects with filters (status, tech)
|
||||||
|
- **Project cards**: Title, description, tech stack, links
|
||||||
|
- **Featured projects**: Highlighted at top
|
||||||
|
- **Case study links**: Deep-dive for each project
|
||||||
|
|
||||||
|
### Blog Page (`/blog`)
|
||||||
|
- **Post listing**: All posts with pagination (10 per page)
|
||||||
|
- **Category filters**: Technical, Business, Personal
|
||||||
|
- **Tag cloud**: Popular tags
|
||||||
|
- **Search**: Client-side search with Fuse.js
|
||||||
|
- **RSS link**: Subscribe to feed
|
||||||
|
|
||||||
|
### Individual Post (`/blog/[slug]`)
|
||||||
|
- **Post metadata**: Publish date, reading time, tags
|
||||||
|
- **Table of contents**: Auto-generated from headings
|
||||||
|
- **Syntax highlighting**: Code blocks with copy button
|
||||||
|
- **Share buttons**: X, LinkedIn
|
||||||
|
- **Related posts**: 3 similar posts by tags
|
||||||
|
|
||||||
|
### Individual Project (`/projects/[slug]`)
|
||||||
|
- **Project header**: Title, status, links (live, GitHub)
|
||||||
|
- **Hero image**: Screenshot or diagram
|
||||||
|
- **Overview**: Problem statement and solution
|
||||||
|
- **Tech stack**: Technologies used
|
||||||
|
- **Architecture**: Technical deep-dive
|
||||||
|
- **Learnings**: Challenges and insights
|
||||||
|
- **Next steps**: Future roadmap
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Performance Optimization
|
||||||
|
|
||||||
|
### Build-Time Optimizations
|
||||||
|
- **Image optimization**: Astro Image component with automatic format conversion (WebP/AVIF)
|
||||||
|
- **Code splitting**: Per-route JavaScript bundles
|
||||||
|
- **CSS purging**: Unused Tailwind classes removed
|
||||||
|
- **HTML minification**: Whitespace removal
|
||||||
|
- **Asset hashing**: Cache-busting for updated files
|
||||||
|
|
||||||
|
### Runtime Optimizations
|
||||||
|
- **Lazy loading**: Images load on scroll
|
||||||
|
- **Preload critical fonts**: Inter and JetBrains Mono
|
||||||
|
- **DNS prefetch**: External resources (analytics, CDN)
|
||||||
|
- **Service worker** (future): Offline support and caching
|
||||||
|
|
||||||
|
### Bundle Size Targets
|
||||||
|
- **Total JS**: <50KB (gzipped)
|
||||||
|
- **Total CSS**: <30KB (gzipped)
|
||||||
|
- **First load**: <200KB total
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Analytics & Monitoring
|
||||||
|
|
||||||
|
### Analytics
|
||||||
|
- **Plausible Analytics**: Privacy-friendly, GDPR-compliant
|
||||||
|
- Deployed on 192.168.1.30 (Docker container)
|
||||||
|
- Custom events: Newsletter signups, project clicks
|
||||||
|
- Traffic sources, top pages, referrers
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
- **Uptime monitoring**: Portainer health checks
|
||||||
|
- **Error tracking**: Browser console errors (future: Sentry)
|
||||||
|
- **Performance monitoring**: Lighthouse CI on builds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Security Considerations
|
||||||
|
|
||||||
|
### Headers
|
||||||
|
- **Content-Security-Policy**: Restrict script sources
|
||||||
|
- **X-Frame-Options**: Prevent clickjacking
|
||||||
|
- **X-Content-Type-Options**: Prevent MIME sniffing
|
||||||
|
- **Referrer-Policy**: Control referrer information
|
||||||
|
|
||||||
|
### HTTPS
|
||||||
|
- **Let's Encrypt certificate**: Auto-renewal via Traefik
|
||||||
|
- **HSTS**: Force HTTPS after first visit
|
||||||
|
- **Redirect HTTP → HTTPS**: Automatic
|
||||||
|
|
||||||
|
### Input Validation
|
||||||
|
- **Newsletter form**: Email validation, CSRF protection
|
||||||
|
- **Contact form** (future): Rate limiting, spam prevention
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Acceptance Criteria
|
||||||
|
|
||||||
|
### Foundation Complete
|
||||||
|
- ✅ Astro project initialized with TypeScript
|
||||||
|
- ✅ Tailwind CSS configured with custom design system
|
||||||
|
- ✅ Content collections set up for blog and projects
|
||||||
|
- ✅ All core pages implemented (Home, About, Projects, Blog)
|
||||||
|
- ✅ Responsive design (mobile, tablet, desktop)
|
||||||
|
- ✅ Dark mode fully implemented
|
||||||
|
|
||||||
|
### Content Complete
|
||||||
|
- ✅ 5 blog posts written and published
|
||||||
|
- ✅ 2 project case studies complete
|
||||||
|
- ✅ Visual assets created (images, diagrams, photos)
|
||||||
|
- ✅ RSS feed functional
|
||||||
|
- ✅ Sitemap generated
|
||||||
|
|
||||||
|
### Deployment Complete
|
||||||
|
- ✅ Docker image built (<50MB)
|
||||||
|
- ✅ Nginx configured with security headers
|
||||||
|
- ✅ Portainer stack deployed on 192.168.1.30
|
||||||
|
- ✅ Domain davidaragon.impresion3d.pro resolves correctly
|
||||||
|
- ✅ HTTPS working with valid certificate
|
||||||
|
- ✅ Analytics tracking functional
|
||||||
|
|
||||||
|
### Performance & SEO
|
||||||
|
- ✅ Lighthouse score 95+ (all categories)
|
||||||
|
- ✅ Core Web Vitals pass
|
||||||
|
- ✅ All meta tags present
|
||||||
|
- ✅ Open Graph images for all posts
|
||||||
|
- ✅ Sitemap accessible at /sitemap.xml
|
||||||
|
- ✅ RSS feed accessible at /rss.xml
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Future Enhancements (Post-Launch)
|
||||||
|
|
||||||
|
### Phase 2 (Month 2-3)
|
||||||
|
- Newsletter automation (email drip campaigns)
|
||||||
|
- Search functionality (Algolia or Fuse.js)
|
||||||
|
- Comment system (giscus or custom)
|
||||||
|
- Related posts algorithm improvement
|
||||||
|
|
||||||
|
### Phase 3 (Month 4-6)
|
||||||
|
- Interactive demos for projects (embedded CodeSandbox)
|
||||||
|
- Video content (embedded YouTube)
|
||||||
|
- Spanish/English language toggle
|
||||||
|
- Progressive Web App features (offline support)
|
||||||
|
|
||||||
|
### Phase 4 (Month 7+)
|
||||||
|
- Guest blog posts from community
|
||||||
|
- Podcast interviews (embedded player)
|
||||||
|
- Community forum or discussion board
|
||||||
|
- Advanced analytics dashboard
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Maintenance Plan
|
||||||
|
|
||||||
|
### Weekly (6-9 hours)
|
||||||
|
- **Content**: 1 blog post (4-6 hours)
|
||||||
|
- **Engagement**: Social media, comments (2-3 hours)
|
||||||
|
|
||||||
|
### Monthly
|
||||||
|
- **Review analytics**: Traffic, popular content
|
||||||
|
- **Update projects**: Status, screenshots
|
||||||
|
- **Dependency updates**: npm audit, security patches
|
||||||
|
|
||||||
|
### Quarterly
|
||||||
|
- **Content audit**: Update outdated posts
|
||||||
|
- **SEO review**: Keyword performance, backlinks
|
||||||
|
- **Performance audit**: Lighthouse scores, Core Web Vitals
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Success Metrics
|
||||||
|
|
||||||
|
### Launch (Month 1)
|
||||||
|
- 500+ unique visitors
|
||||||
|
- 50+ email subscribers
|
||||||
|
- 20+ social shares
|
||||||
|
- 5+ inbound inquiries
|
||||||
|
|
||||||
|
### Growth (Month 3)
|
||||||
|
- 2,000+ unique visitors
|
||||||
|
- 100+ email subscribers
|
||||||
|
- 5+ qualified leads
|
||||||
|
|
||||||
|
### Long-term (Year 1)
|
||||||
|
- 5,000+ monthly visitors
|
||||||
|
- 300+ email subscribers
|
||||||
|
- 50+ customers attributed to content
|
||||||
|
- €15K+ customer acquisition value saved
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Architecture designed by**: wh-leader
|
||||||
|
**Date**: May 8, 2026
|
||||||
|
**Status**: Ready for implementation
|
||||||
|
**Next**: Break into atomic implementer tasks
|
||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
# CI/CD Setup Instructions
|
||||||
|
|
||||||
|
## ✅ What's Done
|
||||||
|
|
||||||
|
1. **Gitea Actions workflow** created (`.gitea/workflows/ci-cd.yaml`)
|
||||||
|
- Build job: Verify Astro build works
|
||||||
|
- Deploy job: Build Docker image + push to registry + trigger Portainer
|
||||||
|
|
||||||
|
2. **Docker files** created:
|
||||||
|
- `Dockerfile` - Multi-stage build (Node + nginx)
|
||||||
|
- `docker-compose.yml` - Local testing
|
||||||
|
- `nginx.conf` - SPA routing + cache headers
|
||||||
|
- `.dockerignore` - Optimize build
|
||||||
|
|
||||||
|
3. **Pushed to git** - CI will run on next push to main branch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Manual Steps Required
|
||||||
|
|
||||||
|
### 1. Configure Gitea Secrets
|
||||||
|
|
||||||
|
Go to: https://gitlab.impresion3d.pro/root/davidaragon-portfolio/settings/secrets
|
||||||
|
|
||||||
|
Add these secrets:
|
||||||
|
|
||||||
|
#### `DOCKER_USERNAME`
|
||||||
|
```
|
||||||
|
root
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `DOCKER_PASSWORD`
|
||||||
|
```
|
||||||
|
<your Gitea personal access token with registry permissions>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `PORTAINER_WEBHOOK_URL`
|
||||||
|
```
|
||||||
|
<webhook URL from Portainer stack - see step 2>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create Portainer Stack
|
||||||
|
|
||||||
|
1. Go to Portainer: http://192.168.1.30:9000
|
||||||
|
2. Navigate to: **Stacks** → **Add stack**
|
||||||
|
3. Name: `davidaragon-portfolio`
|
||||||
|
4. Build method: **Git Repository** or **Web editor**
|
||||||
|
|
||||||
|
#### Stack compose file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
portfolio:
|
||||||
|
image: gitlab.impresion3d.pro/root/davidaragon-portfolio:latest
|
||||||
|
container_name: davidaragon-portfolio
|
||||||
|
ports:
|
||||||
|
- "8081:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Deploy the stack
|
||||||
|
6. Go to stack **Webhooks** tab
|
||||||
|
7. Create webhook for service `portfolio`
|
||||||
|
8. Copy webhook URL (format: `http://192.168.1.30:9000/api/stacks/webhooks/...`)
|
||||||
|
9. Add this URL as `PORTAINER_WEBHOOK_URL` secret in Gitea (step 1)
|
||||||
|
|
||||||
|
### 3. Optional: Configure Gitea Runner
|
||||||
|
|
||||||
|
If no runners registered yet:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On server 192.168.1.30 (port 2222)
|
||||||
|
docker run -d \
|
||||||
|
--name gitea-runner-portfolio \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-e GITEA_INSTANCE_URL=https://gitlab.impresion3d.pro \
|
||||||
|
-e GITEA_RUNNER_REGISTRATION_TOKEN=<your_token> \
|
||||||
|
-e GITEA_RUNNER_NAME=ubuntu-latest \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
gitea/act_runner:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Get registration token from: https://gitlab.impresion3d.pro/admin/actions/runners
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 How It Works
|
||||||
|
|
||||||
|
### On Every Push to Main:
|
||||||
|
|
||||||
|
1. **Build job** runs:
|
||||||
|
- Install Node.js 20
|
||||||
|
- Install dependencies (`npm ci`)
|
||||||
|
- Build Astro site (`npm run build`)
|
||||||
|
- Upload `dist/` artifact
|
||||||
|
|
||||||
|
2. **Deploy job** runs (only if build succeeds):
|
||||||
|
- Download `dist/` artifact
|
||||||
|
- Build Docker image (multi-stage)
|
||||||
|
- Push to Gitea registry: `gitlab.impresion3d.pro/root/davidaragon-portfolio:latest`
|
||||||
|
- Trigger Portainer webhook → Portainer pulls new image and restarts container
|
||||||
|
|
||||||
|
### Result:
|
||||||
|
- Site auto-deployed to http://192.168.1.30:8081
|
||||||
|
- No manual intervention needed after initial setup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Test Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/code_ubuntu/davidaragon-portfolio
|
||||||
|
|
||||||
|
# Build and run with Docker Compose
|
||||||
|
docker-compose up --build
|
||||||
|
|
||||||
|
# Visit: http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Next Steps
|
||||||
|
|
||||||
|
1. Configure secrets in Gitea (step 1)
|
||||||
|
2. Create Portainer stack (step 2)
|
||||||
|
3. Add webhook URL to Gitea secrets
|
||||||
|
4. Push any commit to main → CI/CD runs automatically!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Monitor CI/CD
|
||||||
|
|
||||||
|
- **Actions**: https://gitlab.impresion3d.pro/root/davidaragon-portfolio/actions
|
||||||
|
- **Registry**: https://gitlab.impresion3d.pro/root/-/packages/container/davidaragon-portfolio
|
||||||
|
- **Portainer**: http://192.168.1.30:9000/#!/stacks
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
# Multi-stage build for Astro static site
|
||||||
|
|
||||||
|
# Stage 1: Build
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install ALL dependencies (build needs devDependencies)
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build static site
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 2: Production - nginx
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy built site to nginx html directory
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy nginx config (if needed for SPA routing)
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
# Personal Portfolio Implementation Roadmap
|
||||||
|
|
||||||
|
**Project**: davidaragon.impresion3d.pro
|
||||||
|
**Goal**: Professional portfolio + dev blog for indie builder
|
||||||
|
**Timeline**: 4 weeks (3 weeks implementation + 1 week content/polish)
|
||||||
|
**Methodology**: Atomic task decomposition → Parallel implementation → Sequential deployment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task Dependency Graph
|
||||||
|
|
||||||
|
```
|
||||||
|
Foundation (Week 1)
|
||||||
|
====================
|
||||||
|
t_28db7e16: [#29-impl-1] Initialize Astro + Tailwind + TypeScript
|
||||||
|
└── t_0078b4b3: [#29-impl-2] Content collections (blog + projects schemas)
|
||||||
|
├── t_0ec36bde: [#29-impl-3] Base layout (Header, Footer, Nav)
|
||||||
|
│ ├── t_6e85e022: [#29-impl-4] About page ║
|
||||||
|
│ ├── t_7aa42f76: [#29-impl-5] Homepage (hero + featured) ║
|
||||||
|
│ ├── t_b7d940a1: [#29-impl-6] Blog pages + RSS ║
|
||||||
|
│ └── t_26ed02cc: [#29-impl-7] Projects pages ║
|
||||||
|
└── (All above) ──→ t_66684b8f: [#29-impl-8] Docker deployment
|
||||||
|
|
||||||
|
Content & Polish (Week 3-4)
|
||||||
|
============================
|
||||||
|
t_c4e23cc8: [#29-impl-9] Blog content (5 posts) ║
|
||||||
|
t_637198bd: [#29-impl-10] Project case studies (3 projects) ║
|
||||||
|
t_8f00b91a: [#29-impl-11] SEO optimization (structured data, OG, analytics)
|
||||||
|
|
||||||
|
Legend:
|
||||||
|
║ = Can run in parallel
|
||||||
|
└── = Must wait for parent
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase Breakdown
|
||||||
|
|
||||||
|
### Phase 1: Foundation (Week 1)
|
||||||
|
|
||||||
|
**Priority: P1** (all tasks blocking)
|
||||||
|
|
||||||
|
| Task ID | Title | Assignee | Est. Time | Blocks |
|
||||||
|
|---------|-------|----------|-----------|--------|
|
||||||
|
| t_28db7e16 | Initialize Astro + Tailwind | wh-implementer | 2-3h | ALL |
|
||||||
|
| t_0078b4b3 | Content collections | wh-implementer | 3-4h | Layouts + Pages |
|
||||||
|
| t_0ec36bde | Base layout | wh-implementer | 3-4h | All pages |
|
||||||
|
| t_6e85e022 | About page | wh-implementer | 2-3h | - |
|
||||||
|
| t_7aa42f76 | Homepage hero | wh-implementer | 3-4h | - |
|
||||||
|
|
||||||
|
**Parallelization opportunity**: After #29-impl-3 completes, tasks #29-impl-4, #29-impl-5, #29-impl-6, #29-impl-7 can ALL run simultaneously (4 parallel tracks).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Pages & Features (Week 2)
|
||||||
|
|
||||||
|
**Priority: P1-P2**
|
||||||
|
|
||||||
|
| Task ID | Title | Assignee | Est. Time | Parallel? |
|
||||||
|
|---------|-------|----------|-----------|-----------|
|
||||||
|
| t_b7d940a1 | Blog pages + RSS | wh-implementer | 4-5h | YES ║ |
|
||||||
|
| t_26ed02cc | Projects pages | wh-implementer | 3-4h | YES ║ |
|
||||||
|
|
||||||
|
**Can run in parallel** with #29-impl-4 and #29-impl-5.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Deployment (Week 2-3)
|
||||||
|
|
||||||
|
**Priority: P1** (blocks launch)
|
||||||
|
|
||||||
|
| Task ID | Title | Assignee | Est. Time | Depends on |
|
||||||
|
|---------|-------|----------|-----------|------------|
|
||||||
|
| t_66684b8f | Docker + Nginx + Portainer | wh-implementer | 4-5h | ALL pages (impl-1 through impl-7) |
|
||||||
|
|
||||||
|
**Must wait** for all page implementation tasks to complete (can't deploy empty site).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Content Creation (Week 3-4)
|
||||||
|
|
||||||
|
**Priority: P2** (nice-to-have for MVP, critical for launch)
|
||||||
|
|
||||||
|
| Task ID | Title | Assignee | Est. Time | Parallel? |
|
||||||
|
|---------|-------|----------|-----------|-----------|
|
||||||
|
| t_c4e23cc8 | Blog posts (5 articles) | wh-writer | 6-8h | YES ║ |
|
||||||
|
| t_637198bd | Project case studies (3 projects) | wh-writer | 4-5h | YES ║ |
|
||||||
|
|
||||||
|
**Can run in parallel** (different content types, no dependencies).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 5: SEO & Polish (Week 4)
|
||||||
|
|
||||||
|
**Priority: P3** (launch enhancement)
|
||||||
|
|
||||||
|
| Task ID | Title | Assignee | Est. Time | Depends on |
|
||||||
|
|---------|-------|----------|-----------|------------|
|
||||||
|
| t_8f00b91a | SEO optimization | wh-implementer | 2-3h | ALL pages |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Path Analysis
|
||||||
|
|
||||||
|
**Longest sequential path** (critical path):
|
||||||
|
|
||||||
|
```
|
||||||
|
impl-1 (3h) → impl-2 (4h) → impl-3 (4h) → impl-8 (5h) = 16 hours
|
||||||
|
```
|
||||||
|
|
||||||
|
**With parallelization**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Week 1: impl-1 (3h) → impl-2 (4h) → impl-3 (4h) = 11h
|
||||||
|
Week 2: impl-4, impl-5, impl-6, impl-7 in parallel (max 5h)
|
||||||
|
Week 2: impl-8 (5h)
|
||||||
|
Week 3-4: impl-9, impl-10 in parallel (max 8h)
|
||||||
|
Week 4: impl-11 (3h)
|
||||||
|
|
||||||
|
Total calendar time: 3-4 weeks
|
||||||
|
Total sequential hours: 16h (foundation) + 5h (pages) + 5h (deploy) + 8h (content) + 3h (SEO) = 37 hours
|
||||||
|
With parallelization: ~27 hours of actual work
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Assignee Profiles
|
||||||
|
|
||||||
|
| Profile | Responsibilities | Skills | Tasks |
|
||||||
|
|---------|------------------|--------|-------|
|
||||||
|
| **wh-implementer** | Code implementation | Astro, React, Docker, FastAPI | impl-1 through impl-8, impl-11 |
|
||||||
|
| **wh-writer** | Content creation | Spanish writing, technical writing, SEO | impl-9, impl-10 |
|
||||||
|
| **wh-leader** | Orchestration | Planning, Gitea issues, monitoring | This task (t_22a3d7db) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Kanban Board States
|
||||||
|
|
||||||
|
All tasks created in **`todo`** state.
|
||||||
|
|
||||||
|
Transition rules:
|
||||||
|
- `todo` → `ready`: When all parent tasks in `done`
|
||||||
|
- `ready` → `running`: Dispatcher assigns to available worker
|
||||||
|
- `running` → `done`: Worker calls `kanban_complete(...)`
|
||||||
|
- `running` → `blocked`: Worker needs human input
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Gates
|
||||||
|
|
||||||
|
### Gate 1: Foundation Complete (Week 1)
|
||||||
|
- [ ] Tasks impl-1, impl-2, impl-3 in `done`
|
||||||
|
- [ ] `npm run dev` starts without errors
|
||||||
|
- [ ] Tailwind CSS working (test with colored elements)
|
||||||
|
- [ ] Content collections validated (no schema errors)
|
||||||
|
- [ ] Header, Footer, Navigation visible
|
||||||
|
|
||||||
|
### Gate 2: Pages Complete (Week 2)
|
||||||
|
- [ ] Tasks impl-4, impl-5, impl-6, impl-7 in `done`
|
||||||
|
- [ ] All pages accessible (`/`, `/about`, `/blog`, `/projects`)
|
||||||
|
- [ ] No TypeScript errors
|
||||||
|
- [ ] No broken links
|
||||||
|
- [ ] Responsive design works (mobile, tablet, desktop)
|
||||||
|
|
||||||
|
### Gate 3: Deployment Ready (Week 2-3)
|
||||||
|
- [ ] Task impl-8 in `done`
|
||||||
|
- [ ] Docker image builds successfully
|
||||||
|
- [ ] Final image size <50MB
|
||||||
|
- [ ] Health check passes
|
||||||
|
- [ ] Site accessible at http://localhost:3010
|
||||||
|
- [ ] Nginx serves static files with gzip compression
|
||||||
|
|
||||||
|
### Gate 4: Content Published (Week 3-4)
|
||||||
|
- [ ] Tasks impl-9, impl-10 in `done`
|
||||||
|
- [ ] At least 3 blog posts published
|
||||||
|
- [ ] At least 2 project case studies published
|
||||||
|
- [ ] All content validates against schemas
|
||||||
|
- [ ] No typos or grammar errors
|
||||||
|
|
||||||
|
### Gate 5: SEO Optimized (Week 4)
|
||||||
|
- [ ] Task impl-11 in `done`
|
||||||
|
- [ ] Sitemap generated and accessible
|
||||||
|
- [ ] robots.txt present
|
||||||
|
- [ ] Structured data validates (Google Rich Results Test)
|
||||||
|
- [ ] OG preview looks correct (opengraph.xyz)
|
||||||
|
- [ ] Analytics configured (if using)
|
||||||
|
|
||||||
|
### Gate 6: Production Launch
|
||||||
|
- [ ] All 11 implementation tasks in `done`
|
||||||
|
- [ ] All 6 acceptance gates passed
|
||||||
|
- [ ] Portainer stack deployed to 192.168.1.30:3010
|
||||||
|
- [ ] HTTPS working via Traefik (davidaragon.impresion3d.pro)
|
||||||
|
- [ ] DNS pointing to correct server
|
||||||
|
- [ ] Health check passing
|
||||||
|
- [ ] Site accessible publicly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gitea Issue Mapping
|
||||||
|
|
||||||
|
All tasks map to **Gitea Issue #29**: "Build personal portfolio with blog and projects showcase"
|
||||||
|
|
||||||
|
Task naming convention: `[#29-impl-N] Description`
|
||||||
|
|
||||||
|
This allows tracking all work back to the single strategic Gitea issue while maintaining atomic Kanban tasks for execution.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known Blockers & Mitigations
|
||||||
|
|
||||||
|
### Blocker 1: Git Push Authentication
|
||||||
|
**Status**: BLOCKED
|
||||||
|
**Impact**: Cannot push commits to GitLab
|
||||||
|
**Mitigation**:
|
||||||
|
- Commits created locally (all changes staged)
|
||||||
|
- Push manually after SSH key configured
|
||||||
|
- Or: use personal access token with HTTPS
|
||||||
|
|
||||||
|
**Resolution needed**: SSH key or GitLab personal access token
|
||||||
|
|
||||||
|
### Blocker 2: Real Images (OG, Favicon, Avatar)
|
||||||
|
**Status**: LOW PRIORITY
|
||||||
|
**Impact**: Site uses placeholders for images
|
||||||
|
**Mitigation**:
|
||||||
|
- Create placeholders in implementation tasks
|
||||||
|
- Replace with real images before launch
|
||||||
|
- Can launch with placeholders if needed (not blocking)
|
||||||
|
|
||||||
|
**Resolution**: Create professional OG image (1200x630), favicon set, avatar photo
|
||||||
|
|
||||||
|
### Blocker 3: Analytics Configuration
|
||||||
|
**Status**: OPTIONAL
|
||||||
|
**Impact**: No traffic analytics initially
|
||||||
|
**Mitigation**:
|
||||||
|
- Launch without analytics (privacy-first)
|
||||||
|
- Or: set up self-hosted Plausible later
|
||||||
|
- Not blocking for MVP
|
||||||
|
|
||||||
|
**Resolution**: Optional - configure Plausible after launch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
### Development Metrics
|
||||||
|
- [ ] 11 tasks completed in 4 weeks
|
||||||
|
- [ ] Zero P1 bugs at launch
|
||||||
|
- [ ] <50MB Docker image size
|
||||||
|
- [ ] >80% test coverage (if tests added)
|
||||||
|
- [ ] 100% TypeScript strict compliance
|
||||||
|
|
||||||
|
### Product Metrics (post-launch)
|
||||||
|
- Lighthouse Score: >90 (all categories)
|
||||||
|
- Core Web Vitals: Pass (LCP <2.5s, FID <100ms, CLS <0.1)
|
||||||
|
- Uptime: >99% (monitor with UptimeRobot)
|
||||||
|
- Page load: <2s (first contentful paint)
|
||||||
|
|
||||||
|
### Content Metrics (Month 1)
|
||||||
|
- 3-5 blog posts published
|
||||||
|
- 2-3 project case studies published
|
||||||
|
- RSS subscribers: goal 50+
|
||||||
|
- Organic traffic: goal 100+ visitors/month
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Launch Roadmap
|
||||||
|
|
||||||
|
### Month 1-2: Content Cadence
|
||||||
|
- Publish 2 blog posts per week
|
||||||
|
- Update project case studies with metrics
|
||||||
|
- Add 1-2 more projects (Garage61 API, etc.)
|
||||||
|
|
||||||
|
### Month 3-4: Features
|
||||||
|
- Newsletter signup backend (ConvertKit/Mailchimp)
|
||||||
|
- Comments system (Giscus/Utterances)
|
||||||
|
- Search functionality
|
||||||
|
- Dark/light mode toggle
|
||||||
|
|
||||||
|
### Month 5-6: Growth
|
||||||
|
- Guest posts on Spanish indie blogs
|
||||||
|
- X (Twitter) thread automation from blog posts
|
||||||
|
- SEO optimization (internal linking, backlinks)
|
||||||
|
- Analytics dashboard (public metrics page)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contact & Support
|
||||||
|
|
||||||
|
**Project Owner**: David Aragón (caleidos)
|
||||||
|
**Primary Agent**: wh-leader (orchestrator)
|
||||||
|
**Repository**: https://gitlab.impresion3d.pro/root/davidaragon-portfolio
|
||||||
|
**Deployment**: 192.168.1.30:3010 → davidaragon.impresion3d.pro
|
||||||
|
|
||||||
|
**Blockers?** Comment on Kanban task or create Gitea issue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Generated**: 2026-05-08
|
||||||
|
**Last Updated**: 2026-05-08
|
||||||
|
**Status**: ✅ Ready for execution
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
# Portfolio Project Status - Orchestration Summary
|
||||||
|
|
||||||
|
**Date**: 2026-05-08
|
||||||
|
**Orchestrator**: wh-leader
|
||||||
|
**Task**: t_22a3d7db
|
||||||
|
**Gitea Issue**: #29
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Orchestration Complete
|
||||||
|
|
||||||
|
Successfully decomposed personal portfolio project into **11 atomic implementation tasks** with clear dependencies, assignees, and acceptance criteria.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Tasks Created
|
||||||
|
|
||||||
|
| ID | Title | Assignee | Phase | Priority | Status |
|
||||||
|
|----|-------|----------|-------|----------|--------|
|
||||||
|
| t_28db7e16 | [#29-impl-1] Initialize Astro + Tailwind + TypeScript | wh-implementer | Foundation | P1 | todo |
|
||||||
|
| t_0078b4b3 | [#29-impl-2] Content collections (blog + projects) | wh-implementer | Foundation | P1 | todo |
|
||||||
|
| t_0ec36bde | [#29-impl-3] Base layout (Header, Footer, Nav) | wh-implementer | Foundation | P1 | todo |
|
||||||
|
| t_6e85e022 | [#29-impl-4] About page | wh-implementer | Foundation | P2 | todo |
|
||||||
|
| t_7aa42f76 | [#29-impl-5] Homepage (hero + featured) | wh-implementer | Foundation | P1 | todo |
|
||||||
|
| t_b7d940a1 | [#29-impl-6] Blog pages + RSS feed | wh-implementer | Pages | P2 | todo |
|
||||||
|
| t_26ed02cc | [#29-impl-7] Projects pages | wh-implementer | Pages | P2 | todo |
|
||||||
|
| t_66684b8f | [#29-impl-8] Docker deployment | wh-implementer | Deployment | P1 | todo |
|
||||||
|
| t_c4e23cc8 | [#29-impl-9] Blog content (5 posts) | wh-writer | Content | P2 | todo |
|
||||||
|
| t_637198bd | [#29-impl-10] Project case studies (3 projects) | wh-writer | Content | P2 | todo |
|
||||||
|
| t_8f00b91a | [#29-impl-11] SEO optimization | wh-implementer | Polish | P3 | todo |
|
||||||
|
|
||||||
|
**Total**: 11 tasks
|
||||||
|
**Estimated effort**: 37 hours sequential, ~27 hours with parallelization
|
||||||
|
**Timeline**: 4 weeks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Dependency Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Foundation (Sequential)
|
||||||
|
=======================
|
||||||
|
impl-1 → impl-2 → impl-3
|
||||||
|
↓
|
||||||
|
Pages (Parallel after impl-3)
|
||||||
|
==============================
|
||||||
|
impl-4 ║ impl-5 ║ impl-6 ║ impl-7
|
||||||
|
↓
|
||||||
|
Deployment (Sequential, waits for all pages)
|
||||||
|
============================================
|
||||||
|
impl-8
|
||||||
|
|
||||||
|
Content (Parallel, independent)
|
||||||
|
================================
|
||||||
|
impl-9 ║ impl-10
|
||||||
|
|
||||||
|
Polish (Sequential, waits for all pages)
|
||||||
|
=========================================
|
||||||
|
impl-11
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical Path**: impl-1 → impl-2 → impl-3 → impl-8 (16 hours)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Documentation Created
|
||||||
|
|
||||||
|
1. **ARCHITECTURE.md** (15.7KB)
|
||||||
|
- Complete technical architecture
|
||||||
|
- Tech stack breakdown (Astro, Tailwind, Docker, Nginx)
|
||||||
|
- Directory structure
|
||||||
|
- Deployment strategy
|
||||||
|
- Content types and schemas
|
||||||
|
- SEO and performance requirements
|
||||||
|
|
||||||
|
2. **IMPLEMENTATION-ROADMAP.md** (9.4KB)
|
||||||
|
- Task dependency graph
|
||||||
|
- Phase breakdown with timing
|
||||||
|
- Critical path analysis
|
||||||
|
- Assignee profiles
|
||||||
|
- Acceptance gates (6 gates)
|
||||||
|
- Known blockers and mitigations
|
||||||
|
- Success metrics
|
||||||
|
- Post-launch roadmap
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Key Decisions
|
||||||
|
|
||||||
|
### Tech Stack
|
||||||
|
- **Framework**: Astro (static site generation, fast)
|
||||||
|
- **Styling**: Tailwind CSS (design system, dark mode)
|
||||||
|
- **Language**: TypeScript strict mode (type safety)
|
||||||
|
- **Content**: Astro Content Collections (type-safe markdown)
|
||||||
|
- **Deployment**: Docker + Nginx (production-ready)
|
||||||
|
- **Orchestration**: Portainer (container management)
|
||||||
|
- **Proxy**: Traefik (automatic HTTPS)
|
||||||
|
|
||||||
|
### Architecture Patterns
|
||||||
|
- **Spanish-first**: All UI text in Spanish (es-ES locale)
|
||||||
|
- **Dark mode only**: No light mode toggle (simplicity)
|
||||||
|
- **Static generation**: All pages pre-rendered (performance)
|
||||||
|
- **Content types**: Blog posts + project case studies
|
||||||
|
- **SEO-first**: Sitemap, structured data, OG tags
|
||||||
|
- **Privacy-focused**: No cookies, optional analytics (Plausible)
|
||||||
|
|
||||||
|
### Deployment Strategy
|
||||||
|
- **Host**: 192.168.1.30:3010
|
||||||
|
- **Domain**: davidaragon.impresion3d.pro
|
||||||
|
- **HTTPS**: Via Traefik with Let's Encrypt
|
||||||
|
- **Image**: Multi-stage Docker build (<50MB)
|
||||||
|
- **Server**: Nginx with gzip, caching, security headers
|
||||||
|
- **Health checks**: Built-in for monitoring
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Known Blockers
|
||||||
|
|
||||||
|
### 1. Git Push Authentication
|
||||||
|
**Status**: BLOCKED
|
||||||
|
**Impact**: Cannot push commits to GitLab (2 commits staged locally)
|
||||||
|
**Mitigation**: Commits created locally, will push when SSH key configured
|
||||||
|
**Action needed**: Configure SSH key or personal access token
|
||||||
|
|
||||||
|
### 2. Real Images
|
||||||
|
**Status**: OPTIONAL
|
||||||
|
**Impact**: Placeholders for OG image, favicon, avatar
|
||||||
|
**Mitigation**: Launch with placeholders, replace later
|
||||||
|
**Action needed**: Create 1200x630 OG image, favicon set, avatar photo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Success Criteria
|
||||||
|
|
||||||
|
### Development (Gates 1-3)
|
||||||
|
- [ ] All foundation tasks complete (impl-1, impl-2, impl-3)
|
||||||
|
- [ ] All page tasks complete (impl-4 through impl-7)
|
||||||
|
- [ ] Docker deployment working (impl-8)
|
||||||
|
- [ ] Site accessible locally at http://localhost:3010
|
||||||
|
- [ ] No TypeScript errors, no broken links
|
||||||
|
|
||||||
|
### Content (Gate 4)
|
||||||
|
- [ ] 3-5 blog posts published (impl-9)
|
||||||
|
- [ ] 2-3 project case studies (impl-10)
|
||||||
|
- [ ] All content validates against schemas
|
||||||
|
|
||||||
|
### SEO (Gate 5)
|
||||||
|
- [ ] Structured data validates (Google Rich Results Test)
|
||||||
|
- [ ] OG preview looks correct (opengraph.xyz)
|
||||||
|
- [ ] Sitemap generated and accessible
|
||||||
|
|
||||||
|
### Launch (Gate 6)
|
||||||
|
- [ ] HTTPS working via Traefik
|
||||||
|
- [ ] DNS pointing to davidaragon.impresion3d.pro
|
||||||
|
- [ ] Health check passing
|
||||||
|
- [ ] Uptime monitoring configured
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Next Steps
|
||||||
|
|
||||||
|
### Immediate (Dispatcher)
|
||||||
|
1. Dispatcher picks up tasks in `todo` state
|
||||||
|
2. Promotes impl-1 to `ready` (no dependencies)
|
||||||
|
3. Assigns to wh-implementer profile
|
||||||
|
4. Worker executes impl-1 → impl-2 → impl-3 sequentially
|
||||||
|
|
||||||
|
### Week 1 (Foundation)
|
||||||
|
- wh-implementer completes impl-1, impl-2, impl-3
|
||||||
|
- Gate 1 acceptance: Foundation complete
|
||||||
|
|
||||||
|
### Week 2 (Pages - Parallel)
|
||||||
|
- wh-implementer can work on impl-4, impl-5, impl-6, impl-7 in parallel
|
||||||
|
- All 4 tasks can be dispatched simultaneously
|
||||||
|
- Gate 2 acceptance: Pages complete
|
||||||
|
|
||||||
|
### Week 2-3 (Deployment)
|
||||||
|
- wh-implementer completes impl-8
|
||||||
|
- Test locally before Portainer deployment
|
||||||
|
- Gate 3 acceptance: Deployment ready
|
||||||
|
|
||||||
|
### Week 3-4 (Content)
|
||||||
|
- wh-writer completes impl-9 and impl-10 in parallel
|
||||||
|
- Gate 4 acceptance: Content published
|
||||||
|
|
||||||
|
### Week 4 (Polish + Launch)
|
||||||
|
- wh-implementer completes impl-11 (SEO)
|
||||||
|
- Gate 5 acceptance: SEO optimized
|
||||||
|
- Manual Portainer deployment to 192.168.1.30
|
||||||
|
- Gate 6 acceptance: Production launch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Lessons & Patterns
|
||||||
|
|
||||||
|
### Atomic Task Design
|
||||||
|
- Each task touches 1-3 files maximum
|
||||||
|
- Clear acceptance criteria (checkboxes)
|
||||||
|
- One implementer can complete without coordination
|
||||||
|
- Verifiable in single session (<5 hours)
|
||||||
|
|
||||||
|
### Dependency Management
|
||||||
|
- Sequential where necessary (foundation)
|
||||||
|
- Parallel where possible (pages, content)
|
||||||
|
- Clear parent-child relationships in Kanban
|
||||||
|
|
||||||
|
### Documentation First
|
||||||
|
- ARCHITECTURE.md before any code
|
||||||
|
- IMPLEMENTATION-ROADMAP.md before any tasks
|
||||||
|
- Both docs committed to repo (source of truth)
|
||||||
|
|
||||||
|
### Assignee Specialization
|
||||||
|
- wh-implementer: Code, infra, deployment (9 tasks)
|
||||||
|
- wh-writer: Content, Spanish writing (2 tasks)
|
||||||
|
- wh-leader: Orchestration, planning (1 task)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 References
|
||||||
|
|
||||||
|
- **Repository**: https://gitlab.impresion3d.pro/root/davidaragon-portfolio
|
||||||
|
- **Gitea Issue**: #29 (Build personal portfolio)
|
||||||
|
- **Architecture**: /home/caleidos/code_ubuntu/davidaragon-portfolio/ARCHITECTURE.md
|
||||||
|
- **Roadmap**: /home/caleidos/code_ubuntu/davidaragon-portfolio/IMPLEMENTATION-ROADMAP.md
|
||||||
|
- **Strategic Plan**: /home/caleidos/code_ubuntu/planificacion_empresarial/ideas/019-personal-portfolio-blog/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Orchestrator**: wh-leader
|
||||||
|
**Completed**: 2026-05-08
|
||||||
|
**Status**: ✅ Ready for dispatcher to assign workers
|
||||||
@@ -1,3 +1,43 @@
|
|||||||
# davidaragon-portfolio
|
# Astro Starter Kit: Minimal
|
||||||
|
|
||||||
Personal tech portfolio and dev blog - davidaragon.impresion3d.pro
|
```sh
|
||||||
|
npm create astro@latest -- --template minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
|
|
||||||
|
## 🚀 Project Structure
|
||||||
|
|
||||||
|
Inside of your Astro project, you'll see the following folders and files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/
|
||||||
|
├── public/
|
||||||
|
├── src/
|
||||||
|
│ └── pages/
|
||||||
|
│ └── index.astro
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||||
|
|
||||||
|
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||||
|
|
||||||
|
Any static assets, like images, can be placed in the `public/` directory.
|
||||||
|
|
||||||
|
## 🧞 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :------------------------ | :----------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||||
|
|
||||||
|
## 👀 Want to learn more?
|
||||||
|
|
||||||
|
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import tailwind from '@astrojs/tailwind';
|
||||||
|
import sitemap from '@astrojs/sitemap';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
site: 'https://davidaragon.impresion3d.pro',
|
||||||
|
integrations: [
|
||||||
|
tailwind(),
|
||||||
|
sitemap(),
|
||||||
|
],
|
||||||
|
markdown: {
|
||||||
|
shikiConfig: {
|
||||||
|
theme: 'dark-plus',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
portfolio:
|
||||||
|
image: gitlab.impresion3d.pro/root/davidaragon-portfolio:latest
|
||||||
|
container_name: davidaragon-portfolio
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3001:80"
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
portfolio:
|
||||||
|
build: .
|
||||||
|
container_name: davidaragon-portfolio
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
start_period: 5s
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Serve static files with cache headers
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# SPA fallback - serve index.html for all routes
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+6841
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "davidaragon-portfolio",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.14.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "^5.0.0",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/rss": "^4.0.18",
|
||||||
|
"@astrojs/sitemap": "^3.7.2",
|
||||||
|
"@astrojs/tailwind": "^6.0.2",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"@tailwindcss/typography": "^0.5.15"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 655 B |
@@ -0,0 +1 @@
|
|||||||
|
Placeholder for favicon - use favicon.io or other generator to create a comprehensive favicon set.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 749 B |
@@ -0,0 +1 @@
|
|||||||
|
Placeholder for avatar - replace with real photo
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Placeholder for OG image - replace with a 1200x630px image
|
||||||
@@ -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/
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
title: "From Goldsmith to Builder: Why I'm Building in Public"
|
||||||
|
description: "I didn't start as a developer. I started working metal with my hands. Today I build software. This is my story."
|
||||||
|
publishDate: 2026-05-11
|
||||||
|
author: "David Aragón"
|
||||||
|
tags: ["build-in-public", "introduction", "background"]
|
||||||
|
category: "personal"
|
||||||
|
featured: true
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# From Goldsmith to Builder: Why I'm Building in Public
|
||||||
|
|
||||||
|
I didn't start in tech. I started working metal with my hands.
|
||||||
|
|
||||||
|
I was a goldsmith by trade. Then a professional photographer for 13 years. Later a trainer for LinkedIn Learning, teaching about NAS servers and storage. Then a Python developer at NTT Data. And now I work at KEO Connectivity with IoT protocols for energy systems.
|
||||||
|
|
||||||
|
Strange paths. But all connected by the same thing: **obsession with understanding how things work, and then making them work better**.
|
||||||
|
|
||||||
|
## The Pattern
|
||||||
|
|
||||||
|
When I was a photographer, I couldn't afford the equipment I needed. So I improvised. Built supports, modified accessories, made what I had work.
|
||||||
|
|
||||||
|
That same impulse led me to build my own 3D printer from scratch. To tinker with Arduino. To learn Python to automate workflows at Qloudea while managing social media and providing technical support.
|
||||||
|
|
||||||
|
**The pattern is always the same: identify a problem, obsess over solving it, learn whatever it takes to fix it.**
|
||||||
|
|
||||||
|
## Why Now
|
||||||
|
|
||||||
|
After years solving problems for others, I decided to solve problems for myself. Build my own products.
|
||||||
|
|
||||||
|
I don't have an MBA. I don't have funding. I don't have a team. I just have experience building things, teaching what I learn, and an abnormally high tolerance for not knowing what I'm doing.
|
||||||
|
|
||||||
|
And I decided to do it in public because:
|
||||||
|
|
||||||
|
1. **Accountability**: It's harder to quit when you share progress publicly
|
||||||
|
2. **Teaching**: I've always been a trainer. Sharing what I learn is part of who I am
|
||||||
|
3. **Community**: I want to connect with other Spanish builders on the same journey
|
||||||
|
|
||||||
|
## What I'm Working On
|
||||||
|
|
||||||
|
Currently focused on **WarrantyHub**, a digital warranty management platform for home users.
|
||||||
|
|
||||||
|
Why warranties? Because I saw the problem in real life: chaotic management with papers, Excel, lost emails. And because there's room for a Spanish solution that understands the local market.
|
||||||
|
|
||||||
|
I'm using AI aggressively to accelerate development. Not because it's trendy. Because when you're sole developer, sole marketer, sole product manager, sole support, you need every advantage you can get.
|
||||||
|
|
||||||
|
## What to Expect
|
||||||
|
|
||||||
|
I'll share launches, technical learnings, product decisions, and failures (because they teach more than successes).
|
||||||
|
|
||||||
|
Don't expect generic motivational posts or recycled X threads. This is documentation of a real journey.
|
||||||
|
|
||||||
|
If you're a builder, this is also for you.
|
||||||
|
|
||||||
|
See you in the next post. 🚀
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Follow me on [X](https://twitter.com/davidaragon) for more frequent updates._
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
title: "Modbus TCP vs. EEBUS SPINE: A Technical Comparison for Device Architects"
|
||||||
|
description: "Every device architect I talk to already knows Modbus. That's precisely where the comparison gets interesting."
|
||||||
|
publishDate: 2026-05-11
|
||||||
|
author: "David Aragón"
|
||||||
|
tags: ["EEBUS", "Modbus", "IoT", "protocols", "energy-management"]
|
||||||
|
category: "technical"
|
||||||
|
featured: true
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
*Originally published on [LinkedIn](https://www.linkedin.com/pulse/modbus-tcp-vs-eebus-spine-technical-comparison-device-arag%C3%B3n-galiana-qagje/)*
|
||||||
|
|
||||||
|
Every device architect I talk to already knows Modbus. That's precisely where the comparison gets interesting.
|
||||||
|
|
||||||
|
## Protocol vs. Data Model
|
||||||
|
|
||||||
|
Modbus TCP is a communication protocol. SPINE is a data model with communication semantics. Mixing those two categories is where most evaluations go wrong.
|
||||||
|
|
||||||
|
In Modbus, **meaning is external**. Register 40031 might hold the current setpoint temperature on one manufacturer's heat pump. On another manufacturer's product, that same value lives in a different register entirely. The register map lives in a vendor PDF, and every integration is a one-off mapping exercise.
|
||||||
|
|
||||||
|
In SPINE, **meaning is in the protocol**. A `HeatPumpAppliance` entity exposes features with defined data types and semantics. The EMS doesn't need a vendor manual to understand what `operationMode` means: it's specified, discoverable, and identical across every EEBUS-compatible manufacturer.
|
||||||
|
|
||||||
|
## Communication Model: Polling vs. Subscription
|
||||||
|
|
||||||
|
The communication model is also inverted. Modbus is **polling-based**: your EMS asks, the device answers, on a schedule you define. SPINE is **subscription-based**: you subscribe to the features you care about, and the device notifies you on change. For real-time energy coordination, the difference in latency and network load is noticeable.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
Security is another axis. Modbus TCP has none natively. SHIP, the transport layer EEBUS runs on, includes TLS and device pairing as defined components of the spec, not optional additions.
|
||||||
|
|
||||||
|
## The Complexity Trade-off
|
||||||
|
|
||||||
|
The tradeoff is real: SPINE is more complex upfront. The data model takes time to internalize, and the SHIP pairing flow alone has caught more than one integration team off guard.
|
||||||
|
|
||||||
|
But the comparison isn't Modbus complexity versus EEBUS complexity for one device. It's **Modbus complexity per device, per manufacturer, per firmware version**, versus **EEBUS complexity once**.
|
||||||
|
|
||||||
|
## Why "Once" Matters
|
||||||
|
|
||||||
|
That "once" is where the stack choice matters. We built the KEO stack with both SHIP and SPINE fully abstracted: no pairing flow to implement, no data model to interpret, no spec ambiguity to chase down. The integration starts at the use case level.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If you're stuck on a SHIP pairing issue, a SPINE feature that's misbehaving, or sizing up the build-vs-buy decision for an EEBUS stack, feel free to reach out. Always up for comparing notes.
|
||||||
@@ -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,48 @@
|
|||||||
|
---
|
||||||
|
title: "Python Project Template"
|
||||||
|
description: "Opinionated Python project template with modern tooling: uv, ruff, mypy, pytest, Docker, and CI/CD ready"
|
||||||
|
url: "https://gitlab.impresion3d.pro/root/python-project-template"
|
||||||
|
status: "active"
|
||||||
|
tags: ["Python", "Docker", "CI/CD", "DevOps"]
|
||||||
|
startDate: 2026-05-01
|
||||||
|
featured: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python Project Template
|
||||||
|
|
||||||
|
An opinionated Python project template designed for rapid development with modern tooling and infrastructure-as-code practices.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Modern Python tooling**: uv for fast dependency management, ruff for linting, mypy for type checking
|
||||||
|
- **Testing**: pytest with coverage reports
|
||||||
|
- **Docker ready**: Multi-stage Dockerfile optimized for Python projects
|
||||||
|
- **CI/CD pipeline**: Gitea Actions workflows for automated testing and deployment
|
||||||
|
- **Portainer integration**: Deploy to self-hosted infrastructure with one push
|
||||||
|
- **Development environment**: nox for task automation
|
||||||
|
|
||||||
|
## Why I Built This
|
||||||
|
|
||||||
|
After setting up multiple Python projects from scratch, I noticed I was repeating the same patterns: uv for dependencies, ruff for code quality, Docker for deployment, Gitea Actions for CI/CD. Each time I'd spend hours configuring these tools.
|
||||||
|
|
||||||
|
This template captures those decisions so new projects start with production-ready infrastructure on day one.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Python 3.12+** with uv package manager
|
||||||
|
- **Quality tools**: ruff (linting), mypy (type checking), pytest (testing)
|
||||||
|
- **Containerization**: Docker with multi-stage builds
|
||||||
|
- **CI/CD**: Gitea Actions with automated testing and deployment
|
||||||
|
- **Orchestration**: Portainer stack deployment
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
Perfect for:
|
||||||
|
- Web APIs (FastAPI, Flask)
|
||||||
|
- CLI tools
|
||||||
|
- Background workers
|
||||||
|
- Self-hosted applications
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Active and maintained. I use this template for all new Python 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.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
title: "WarrantyHub"
|
||||||
|
description: "Plataforma digital para gestionar todas las garantías de tus productos del hogar desde un solo lugar"
|
||||||
|
url: "https://warrantyhub.impresion3d.pro"
|
||||||
|
status: "development"
|
||||||
|
tags: ["React", "FastAPI", "PostgreSQL", "SaaS"]
|
||||||
|
startDate: 2026-04-15
|
||||||
|
featured: true
|
||||||
|
image: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
# WarrantyHub
|
||||||
|
|
||||||
|
**Gestión digital de garantías para el hogar**
|
||||||
|
|
||||||
|
## El Problema
|
||||||
|
|
||||||
|
Todos tenemos el mismo problema: compramos electrodomésticos, gadgets, muebles... y guardamos los tickets y garantías en algún cajón. Cuando algo se rompe y necesitas reclamar:
|
||||||
|
|
||||||
|
- ❌ No encuentras el ticket
|
||||||
|
- ❌ No sabes si todavía está en garantía
|
||||||
|
- ❌ No tienes los datos del establecimiento
|
||||||
|
- ❌ El proceso de reclamación es un caos
|
||||||
|
|
||||||
|
**WarrantyHub resuelve esto.**
|
||||||
|
|
||||||
|
## La Solución
|
||||||
|
|
||||||
|
Una plataforma donde guardas todas tus garantías digitalmente:
|
||||||
|
|
||||||
|
✅ **Digitalización rápida**: Foto del ticket y listo
|
||||||
|
✅ **Recordatorios**: Te avisamos antes de que expire la garantía
|
||||||
|
✅ **Organización**: Todos tus productos en un solo lugar
|
||||||
|
✅ **Búsqueda fácil**: Encuentra cualquier garantía en segundos
|
||||||
|
✅ **Proceso guiado**: Te ayudamos con el proceso de reclamación
|
||||||
|
|
||||||
|
## Stack Técnico
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- React + TypeScript
|
||||||
|
- Tailwind CSS
|
||||||
|
- Vite
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- FastAPI (Python)
|
||||||
|
- PostgreSQL
|
||||||
|
- Docker + Portainer
|
||||||
|
|
||||||
|
**Features actuales:**
|
||||||
|
- Autenticación de usuarios
|
||||||
|
- Subida y digitalización de tickets (OCR)
|
||||||
|
- Gestión de productos y garantías
|
||||||
|
- Notificaciones de expiración
|
||||||
|
|
||||||
|
## Estado Actual
|
||||||
|
|
||||||
|
🚧 **En desarrollo activo**
|
||||||
|
|
||||||
|
Trabajando en:
|
||||||
|
- Sistema de recordatorios automáticos
|
||||||
|
- Mejora de OCR para extracción de datos
|
||||||
|
- Integración con tiendas para importar garantías automáticamente
|
||||||
|
|
||||||
|
## Por Qué Este Proyecto
|
||||||
|
|
||||||
|
Es un problema que yo mismo tenía. Y después de hablar con amigos y familia, descubrí que **todos** tienen el mismo problema.
|
||||||
|
|
||||||
|
No es sexy. No es AI generativa. Pero resuelve un problema real que la gente tiene cada día.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status:** Pronto en beta privada. [Suscríbete](/blog) para recibir invitación.
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
import Header from '../components/layout/Header.astro';
|
||||||
|
import Footer from '../components/layout/Footer.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title="About"
|
||||||
|
description="Spanish builder. From goldsmith to photographer to developer. Building products and sharing the journey."
|
||||||
|
>
|
||||||
|
<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">About Me</h1>
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row gap-8 mb-8 not-prose">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<img
|
||||||
|
src="/david-aragon.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">Hi 👋</h2>
|
||||||
|
<p class="text-text-secondary text-lg leading-relaxed">
|
||||||
|
I'm David, a Spanish builder. I didn't start in tech — I was a goldsmith, then a professional
|
||||||
|
photographer for 13 years, instructor for LinkedIn Learning, and developer. Now I build products
|
||||||
|
that solve real problems.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-3xl font-semibold mb-4">The Path</h2>
|
||||||
|
<p class="text-text-secondary leading-relaxed mb-4">
|
||||||
|
I started working with metal as a goldsmith. One day I got interested in photography,
|
||||||
|
and that obsession led me to build my own brand for 13 years. When I couldn't
|
||||||
|
afford the equipment I needed, I improvised. I built supports, modified
|
||||||
|
accessories, made what I had work.
|
||||||
|
</p>
|
||||||
|
<p class="text-text-secondary leading-relaxed mb-4">
|
||||||
|
That same drive led me to learn Python, build my own 3D printer from scratch,
|
||||||
|
work at Qloudea developing internal tools while managing social media
|
||||||
|
and providing technical support. Then Python developer at NTT Data, and now I work at KEO
|
||||||
|
Connectivity with IoT protocols for energy systems (EEBUS).
|
||||||
|
</p>
|
||||||
|
<p class="text-text-secondary leading-relaxed mb-4">
|
||||||
|
<strong class="text-text-primary">The pattern is always the same:</strong> identify a
|
||||||
|
problem, obsess over solving it, learn whatever it takes to fix it.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-3xl font-semibold mb-4">What I'm Doing Now</h2>
|
||||||
|
<p class="text-text-secondary leading-relaxed mb-4">
|
||||||
|
I work full-time at KEO as Field Application Engineer, where I handle technical
|
||||||
|
documentation, workflow automation, internal tools, and customer-facing support.
|
||||||
|
</p>
|
||||||
|
<p class="text-text-secondary leading-relaxed mb-4">
|
||||||
|
In parallel, I'm building <strong class="text-primary">WarrantyHub</strong> — a platform
|
||||||
|
for end users to manage warranties for their home products from one place.
|
||||||
|
It's a problem I had myself, and I discovered all my friends and family have the same problem.
|
||||||
|
</p>
|
||||||
|
<p class="text-text-secondary leading-relaxed mb-4">
|
||||||
|
I also work on other experimental projects: productivity CLIs, automation tools,
|
||||||
|
Discord bots, and templates to accelerate Python development.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-3xl font-semibold mb-4">Current Projects</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="bg-surface p-5 rounded-lg border border-text-tertiary/20">
|
||||||
|
<h3 class="text-lg font-semibold text-primary mb-2">WarrantyHub</h3>
|
||||||
|
<p class="text-text-secondary text-sm">
|
||||||
|
Digital home warranty manager. Self-hosted PWA with freemium model.
|
||||||
|
Stack: React + FastAPI + PostgreSQL.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-surface p-5 rounded-lg border border-text-tertiary/20">
|
||||||
|
<h3 class="text-lg font-semibold text-primary mb-2">Python Project Template</h3>
|
||||||
|
<p class="text-text-secondary text-sm">
|
||||||
|
Opinionated template for Python projects: uv, ruff, mypy, pytest, Docker, Portainer.
|
||||||
|
CI/CD with Gitea Actions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-surface p-5 rounded-lg border border-text-tertiary/20">
|
||||||
|
<h3 class="text-lg font-semibold text-primary mb-2">Hermes Stack</h3>
|
||||||
|
<p class="text-text-secondary text-sm">
|
||||||
|
Hermes Agent gateway stack for QNAP/Portainer. Automation with AI agents
|
||||||
|
for infrastructure management.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-surface p-5 rounded-lg border border-text-tertiary/20">
|
||||||
|
<h3 class="text-lg font-semibold text-primary mb-2">Other Experiments</h3>
|
||||||
|
<p class="text-text-secondary text-sm">
|
||||||
|
Discord bots (iRacing), automation tools (Portainer backups),
|
||||||
|
utility scripts (FLAC tagging, cookie extraction), and more.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-3xl font-semibold mb-4">Self-Hosted Infrastructure</h2>
|
||||||
|
<p class="text-text-secondary leading-relaxed mb-4">
|
||||||
|
All my projects run on local self-hosted infrastructure. I prefer to have complete
|
||||||
|
control over the stack and keep costs low while experimenting.
|
||||||
|
</p>
|
||||||
|
<div class="bg-surface p-6 rounded-lg border border-primary/20">
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-primary">Current Stack</h3>
|
||||||
|
<ul class="space-y-2 text-text-secondary">
|
||||||
|
<li class="flex items-start">
|
||||||
|
<span class="text-primary mr-2">→</span>
|
||||||
|
<span><strong class="text-text-primary">Local physical server</strong> -
|
||||||
|
Main Docker host</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start">
|
||||||
|
<span class="text-primary mr-2">→</span>
|
||||||
|
<span><strong class="text-text-primary">Portainer</strong> - Stack and container
|
||||||
|
orchestration</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start">
|
||||||
|
<span class="text-primary mr-2">→</span>
|
||||||
|
<span><strong class="text-text-primary">Gitea</strong> -
|
||||||
|
Git hosting + Docker registry + CI/CD</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start">
|
||||||
|
<span class="text-primary mr-2">→</span>
|
||||||
|
<span><strong class="text-text-primary">MinIO</strong> - Object storage (S3-compatible)</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start">
|
||||||
|
<span class="text-primary mr-2">→</span>
|
||||||
|
<span><strong class="text-text-primary">Nginx Proxy Manager</strong> - Reverse proxy
|
||||||
|
with automatic SSL (Let's Encrypt)</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start">
|
||||||
|
<span class="text-primary mr-2">→</span>
|
||||||
|
<span><strong class="text-text-primary">PostgreSQL</strong> - Main database
|
||||||
|
for applications</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-text-secondary text-sm mt-4">
|
||||||
|
Fully automated CI/CD: push to main → build image → push to registry →
|
||||||
|
deploy to Portainer. Everything on local infrastructure.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-3xl font-semibold mb-4">Why Build in Public</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">Accountability:</strong> It's harder to quit
|
||||||
|
when you share progress publicly
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start">
|
||||||
|
<span class="text-primary mr-2">→</span>
|
||||||
|
<span class="text-text-secondary">
|
||||||
|
<strong class="text-text-primary">Teaching:</strong> I've always been an instructor
|
||||||
|
(LinkedIn Learning, Podcast CulturaNAS 2000+ subs). Sharing what I learn is part of who I am
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start">
|
||||||
|
<span class="text-primary mr-2">→</span>
|
||||||
|
<span class="text-text-secondary">
|
||||||
|
<strong class="text-text-primary">Community:</strong> I want to connect with other
|
||||||
|
Spanish builders on the same journey
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-3xl font-semibold mb-4">Tech Stack</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">Python</div>
|
||||||
|
<div class="bg-surface px-4 py-3 rounded-lg border border-text-tertiary/20">FastAPI</div>
|
||||||
|
<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">PostgreSQL</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">Tailwind CSS</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">n8n</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-3xl font-semibold mb-4">Experience</h2>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="bg-surface p-6 rounded-lg border border-text-tertiary/20">
|
||||||
|
<div class="flex items-start justify-between mb-2">
|
||||||
|
<h3 class="text-xl font-semibold text-primary">Field Application Engineer</h3>
|
||||||
|
<span class="text-sm text-text-tertiary whitespace-nowrap ml-4">2023 - Present</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-text-secondary mb-2">KEO Connectivity (Germany)</p>
|
||||||
|
<p class="text-text-secondary text-sm">
|
||||||
|
EEBUS protocol for IoT energy systems (e-mobility, smart homes,
|
||||||
|
heat pumps). Technical documentation, automation with n8n, internal tools,
|
||||||
|
technical support.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-surface p-6 rounded-lg border border-text-tertiary/20">
|
||||||
|
<div class="flex items-start justify-between mb-2">
|
||||||
|
<h3 class="text-xl font-semibold text-primary">Python Developer</h3>
|
||||||
|
<span class="text-sm text-text-tertiary whitespace-nowrap ml-4">2022 - 2023</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-text-secondary mb-2">NTT Data</p>
|
||||||
|
<p class="text-text-secondary text-sm">
|
||||||
|
Python development, Google Cloud, Kubernetes, BigQuery. Data analysis and BigData.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-surface p-6 rounded-lg border border-text-tertiary/20">
|
||||||
|
<div class="flex items-start justify-between mb-2">
|
||||||
|
<h3 class="text-xl font-semibold text-primary">ICT Instructor + Developer</h3>
|
||||||
|
<span class="text-sm text-text-tertiary whitespace-nowrap ml-4">2013 - 2022</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-text-secondary mb-2">Qloudea + LinkedIn Learning</p>
|
||||||
|
<p class="text-text-secondary text-sm">
|
||||||
|
Development of internal tools (HTML5, CSS, JavaScript, Python, Django, Flutter).
|
||||||
|
Official instructor on NAS servers for professionals. Social media management, YouTube,
|
||||||
|
product photography.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-surface p-6 rounded-lg border border-text-tertiary/20">
|
||||||
|
<div class="flex items-start justify-between mb-2">
|
||||||
|
<h3 class="text-xl font-semibold text-primary">Professional Photographer</h3>
|
||||||
|
<span class="text-sm text-text-tertiary whitespace-nowrap ml-4">2003 - 2016</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-text-secondary mb-2">Own Brand</p>
|
||||||
|
<p class="text-text-secondary text-sm">
|
||||||
|
13 years managing my own photography brand. Creator of CulturaNAS Podcast
|
||||||
|
(2000+ subscribers).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 class="text-3xl font-semibold mb-4">Connect With Me</h2>
|
||||||
|
<p class="text-text-secondary mb-6">
|
||||||
|
Always open to talking with other builders, people interested in EEBUS/IoT,
|
||||||
|
or just curious people about building products.
|
||||||
|
</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/david-aragon-galiana"
|
||||||
|
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>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
|
import Header from '../../components/layout/Header.astro';
|
||||||
|
import Footer from '../../components/layout/Footer.astro';
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const blogEntries = await getCollection('blog', ({ data }) => !data.draft);
|
||||||
|
return blogEntries.map(entry => ({
|
||||||
|
params: { slug: entry.slug },
|
||||||
|
props: { entry },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props;
|
||||||
|
const { Content } = await entry.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title={entry.data.title}
|
||||||
|
description={entry.data.description}
|
||||||
|
>
|
||||||
|
<Header />
|
||||||
|
<main class="flex-1 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<article>
|
||||||
|
<header class="mb-8">
|
||||||
|
<time class="text-sm text-text-tertiary block mb-2">
|
||||||
|
{entry.data.publishDate.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</time>
|
||||||
|
|
||||||
|
<h1 class="text-4xl font-bold mb-4">{entry.data.title}</h1>
|
||||||
|
|
||||||
|
<p class="text-xl text-text-secondary mb-4">
|
||||||
|
{entry.data.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{entry.data.tags.map((tag) => (
|
||||||
|
<span class="inline-block px-3 py-1 rounded-full text-sm font-medium border bg-text-tertiary/10 text-text-secondary border-text-tertiary/20">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="prose prose-invert prose-lg max-w-none">
|
||||||
|
<Content />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="mt-12 pt-8 border-t border-text-tertiary/20">
|
||||||
|
<a
|
||||||
|
href="/blog"
|
||||||
|
class="text-primary hover:text-secondary transition-colors font-medium"
|
||||||
|
>
|
||||||
|
← Back to all posts
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</BaseLayout>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
|
import Header from '../../components/layout/Header.astro';
|
||||||
|
import Footer from '../../components/layout/Footer.astro';
|
||||||
|
|
||||||
|
const allPosts = await getCollection('blog', ({ data }) => !data.draft);
|
||||||
|
const sortedPosts = allPosts.sort(
|
||||||
|
(a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()
|
||||||
|
);
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title="Blog"
|
||||||
|
description="Thoughts on building products, technical learnings, and the indie builder journey"
|
||||||
|
>
|
||||||
|
<Header />
|
||||||
|
<main class="flex-1 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<h1 class="text-4xl font-bold mb-8">Blog</h1>
|
||||||
|
|
||||||
|
<div class="space-y-8">
|
||||||
|
{sortedPosts.map((post) => (
|
||||||
|
<article class="bg-surface rounded-lg border border-text-tertiary/20 p-6 hover:border-primary/40 transition-colors">
|
||||||
|
<a href={`/blog/${post.slug}`} class="block">
|
||||||
|
<time class="text-sm text-text-tertiary block mb-2">
|
||||||
|
{post.data.publishDate.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</time>
|
||||||
|
|
||||||
|
<h2 class="text-2xl font-semibold mb-3 text-text-primary hover:text-primary transition-colors">
|
||||||
|
{post.data.title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-text-secondary mb-4">
|
||||||
|
{post.data.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
|
{post.data.tags.map((tag) => (
|
||||||
|
<span class="inline-block px-3 py-1 rounded-full text-sm font-medium border bg-text-tertiary/10 text-text-secondary border-text-tertiary/20">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="text-primary font-medium">Read more →</span>
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sortedPosts.length === 0 && (
|
||||||
|
<p class="text-text-secondary text-center py-12">
|
||||||
|
No posts yet. Check back soon!
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</BaseLayout>
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
---
|
||||||
|
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="Home"
|
||||||
|
description="David Aragón's portfolio - Builder creating products and sharing the journey"
|
||||||
|
>
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<main class="flex-1">
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||||
|
<div class="flex flex-col md:flex-row items-center gap-12">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<img
|
||||||
|
src="/david-aragon.jpg"
|
||||||
|
alt="David Aragón"
|
||||||
|
class="w-48 h-48 md:w-64 md:h-64 rounded-full object-cover border-4 border-primary/20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 text-center md:text-left">
|
||||||
|
<h1 class="text-4xl md:text-5xl font-bold mb-6">
|
||||||
|
Hi, I'm <span class="text-primary">David Aragón</span>
|
||||||
|
</h1>
|
||||||
|
<p class="text-xl text-text-secondary mb-6 leading-relaxed">
|
||||||
|
Builder creating products that solve real problems.
|
||||||
|
From goldsmith to photographer to developer. Now building in public.
|
||||||
|
</p>
|
||||||
|
<p class="text-lg text-text-secondary mb-8">
|
||||||
|
Currently working on <strong class="text-primary">WarrantyHub</strong> -
|
||||||
|
digital warranty management for home users.
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-wrap justify-center md:justify-start gap-4">
|
||||||
|
<a
|
||||||
|
href="/blog"
|
||||||
|
class="bg-primary hover:bg-primary/80 text-background px-8 py-3 rounded-lg
|
||||||
|
font-semibold transition-colors"
|
||||||
|
>
|
||||||
|
Read Blog
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/about"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
About Me
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</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">Latest Posts</h2>
|
||||||
|
<a
|
||||||
|
href="/blog"
|
||||||
|
class="text-primary hover:text-secondary transition-colors font-medium"
|
||||||
|
>
|
||||||
|
View all →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{blogPosts.length > 0 ? (
|
||||||
|
blogPosts.map((post) => (
|
||||||
|
<Card href={`/blog/${post.slug}`}>
|
||||||
|
<div class="mb-4">
|
||||||
|
<time class="text-sm text-text-tertiary">
|
||||||
|
{formatDate(post.data.publishDate)}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold mb-3 text-text-primary group-hover:text-primary transition-colors">
|
||||||
|
{post.data.title}
|
||||||
|
</h3>
|
||||||
|
<p class="text-text-secondary mb-4 line-clamp-3">
|
||||||
|
{post.data.description}
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
|
{post.data.tags?.slice(0, 3).map((tag: string) => (
|
||||||
|
<Tag>{tag}</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
<span class="text-primary font-medium">Read more →</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div class="col-span-full text-center py-12">
|
||||||
|
<p class="text-text-secondary">
|
||||||
|
Posts coming soon. Subscribe to receive updates.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Featured Projects (if any) -->
|
||||||
|
{projects.length > 0 && (
|
||||||
|
<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">Projects</h2>
|
||||||
|
<a
|
||||||
|
href="/projects"
|
||||||
|
class="text-primary hover:text-secondary transition-colors font-medium"
|
||||||
|
>
|
||||||
|
View all →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{projects.map((project) => (
|
||||||
|
<Card href={`/projects/${project.slug}`}>
|
||||||
|
<div class="mb-4">
|
||||||
|
<span class="text-sm text-text-tertiary capitalize">
|
||||||
|
{project.data.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold mb-3 text-text-primary group-hover:text-primary transition-colors">
|
||||||
|
{project.data.title}
|
||||||
|
</h3>
|
||||||
|
<p class="text-text-secondary mb-4">
|
||||||
|
{project.data.description}
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{project.data.tags?.slice(0, 4).map((tag: string) => (
|
||||||
|
<Tag>{tag}</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</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 border border-primary/20
|
||||||
|
rounded-2xl p-8 md:p-12 text-center">
|
||||||
|
<h2 class="text-3xl font-bold mb-4">Stay Updated</h2>
|
||||||
|
<p class="text-text-secondary text-lg mb-8 max-w-2xl mx-auto">
|
||||||
|
Get occasional updates about launches, learnings, and technical decisions.
|
||||||
|
No spam, no BS - just real builder content.
|
||||||
|
</p>
|
||||||
|
<form
|
||||||
|
class="max-w-md mx-auto flex flex-col sm:flex-row gap-4"
|
||||||
|
method="POST"
|
||||||
|
action="/api/subscribe"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
placeholder="your@email.com"
|
||||||
|
required
|
||||||
|
class="flex-1 px-6 py-3 rounded-lg bg-surface border border-text-tertiary/20
|
||||||
|
text-text-primary placeholder-text-tertiary focus:outline-none
|
||||||
|
focus:border-primary transition-colors"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="bg-primary hover:bg-primary/80 text-background px-8 py-3 rounded-lg
|
||||||
|
font-semibold transition-colors whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Subscribe
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</BaseLayout>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
@apply bg-background text-text-primary font-sans;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
module.exports = {
|
||||||
|
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,ts,tsx}'],
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
background: '#0a0e27',
|
||||||
|
surface: '#131729',
|
||||||
|
primary: '#60a5fa',
|
||||||
|
secondary: '#a78bfa',
|
||||||
|
text: {
|
||||||
|
primary: '#f1f5f9',
|
||||||
|
secondary: '#94a3b8',
|
||||||
|
tertiary: '#64748b',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
green: '#34d399',
|
||||||
|
red: '#f87171',
|
||||||
|
yellow: '#fbbf24',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'sans-serif'],
|
||||||
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require('@tailwindcss/typography')],
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
|
"exclude": ["dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user