- Astro static site with Tailwind CSS v4 - 6 sections: Hero (gradient mesh), About, Capabilities, Differentiator, CTA, Footer - Sticky navbar with mobile menu - Scroll reveal animations - SEO meta tags and structured data - Docker + nginx config for Coolify deployment Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
30 KiB
FIT Labs Landing Page — Implementation Plan
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build and deploy a single-page landing for fitlabs.dev — FactorIT's AI innovation incubator.
Architecture: Astro static site with Tailwind CSS. 6 sections in a single index.astro page composed from Astro components. No client-side framework — vanilla JS only for navbar scroll, scroll-reveal animations, and gradient mesh. Deployed via Docker (nginx) on Coolify.
Tech Stack: Astro 5.x, Tailwind CSS 4.x, Lucide Icons (SVG inline), Docker + nginx
Spec: docs/superpowers/specs/2026-03-14-fitlabs-landing-design.md
File Structure
fitlabs.dev/
├── public/
│ ├── LogoFIT.png # Logo (already exists at project root)
│ └── favicon.svg # Favicon
├── src/
│ ├── layouts/
│ │ └── Layout.astro # Base HTML: <head> meta/SEO/fonts, <body> slot
│ ├── components/
│ │ ├── Navbar.astro # Sticky navbar with scroll behavior
│ │ ├── Hero.astro # Gradient mesh hero
│ │ ├── About.astro # "Qué es FIT Labs" section
│ │ ├── Capabilities.astro # 5 capabilities list
│ │ ├── Differentiator.astro # Two-column differentiator
│ │ ├── Contact.astro # CTA section
│ │ └── Footer.astro # Footer
│ ├── pages/
│ │ └── index.astro # Composes all sections
│ └── styles/
│ └── global.css # Tailwind imports, custom animations, gradient mesh
├── nginx/
│ └── nginx.conf # Nginx config for static serving
├── Dockerfile # Multi-stage: node build → nginx serve
├── astro.config.mjs # Astro config (static output)
├── package.json
├── tsconfig.json
└── .gitignore
Chunk 1: Project Setup & Base Layout
Task 1: Scaffold Astro Project
Files:
-
Create:
package.json,astro.config.mjs,tsconfig.json,.gitignore -
Step 1: Initialize Astro project
cd /Users/felipearentsen/Claude/code/fitlabs.dev
npm create astro@latest . -- --template minimal --no-install --typescript strict
- Step 2: Add Tailwind CSS integration
npx astro add tailwind -y
- Step 3: Configure astro.config.mjs for static output
// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwindcss from '@astrojs/tailwind';
export default defineConfig({
output: 'static',
integrations: [tailwindcss()],
site: 'https://fitlabs.dev',
});
- Step 4: Update .gitignore
Add to .gitignore:
node_modules/
dist/
.astro/
.superpowers/
- Step 5: Move logo to public/
cp /Users/felipearentsen/Claude/code/fitlabs.dev/LogoFIT.png /Users/felipearentsen/Claude/code/fitlabs.dev/public/LogoFIT.png
- Step 6: Install dependencies and verify build
npm install
npm run build
Expected: Build succeeds, dist/ directory created.
- Step 7: Initialize git and commit
git init
git add -A
git commit -m "chore: scaffold Astro project with Tailwind CSS"
Task 2: Global Styles & Tailwind Config
Files:
-
Create:
src/styles/global.css -
Modify:
tailwind.config.mjs -
Step 1: Configure Tailwind with custom colors and fonts
// tailwind.config.mjs
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {
colors: {
dark: {
bg: '#0a0a1a',
surface: '#0f172a',
footer: '#06080f',
},
light: {
bg: '#f0f9ff',
surface: '#e0e7ff',
base: '#f8fafc',
},
brand: {
blue: '#0369a1',
violet: '#7c3aed',
cyan: '#06b6d4',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
},
},
plugins: [],
};
- Step 2: Create global.css with Tailwind imports and custom animations
/* src/styles/global.css */
@import 'tailwindcss';
@theme {
--font-sans: 'Inter', system-ui, sans-serif;
}
/* Gradient mesh animation for hero */
@keyframes mesh-drift-1 {
0%, 100% { transform: translate(0, 0); }
33% { transform: translate(30px, -20px); }
66% { transform: translate(-20px, 15px); }
}
@keyframes mesh-drift-2 {
0%, 100% { transform: translate(0, 0); }
33% { transform: translate(-25px, 20px); }
66% { transform: translate(15px, -30px); }
}
@keyframes mesh-drift-3 {
0%, 100% { transform: translate(0, 0); }
33% { transform: translate(20px, 25px); }
66% { transform: translate(-30px, -10px); }
}
/* Scroll reveal */
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
/* Capability list slide-in */
.slide-in {
opacity: 0;
transform: translateX(-20px);
transition: opacity 0.5s ease, transform 0.5s ease;
}
.slide-in.visible {
opacity: 1;
transform: translateX(0);
}
/* Gradient text utility */
.gradient-text {
background: linear-gradient(135deg, #0369a1, #7c3aed);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
- Step 3: Verify build
npm run build
Expected: Build succeeds.
- Step 4: Commit
git add tailwind.config.mjs src/styles/global.css
git commit -m "feat: add Tailwind config with brand colors and global animations"
Task 3: Base Layout with SEO
Files:
-
Create:
src/layouts/Layout.astro -
Step 1: Create Layout.astro with meta tags and Open Graph
---
// src/layouts/Layout.astro
interface Props {
title?: string;
description?: string;
}
const {
title = 'FIT Labs — Innovación con IA by FactorIT',
description = 'Soluciones de inteligencia artificial enterprise que escalan. La incubadora de innovación de FactorIT.',
} = Astro.props;
---
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://fitlabs.dev" />
<meta property="og:site_name" content="FIT Labs" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<!-- 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;600;700;800&display=swap"
rel="stylesheet"
/>
<!-- Structured Data -->
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "Organization",
"name": "FIT Labs",
"description": description,
"url": "https://fitlabs.dev",
"parentOrganization": {
"@type": "Organization",
"name": "FactorIT",
"url": "https://www.factorit.com"
}
})} />
<title>{title}</title>
</head>
<body class="font-sans antialiased">
<slot />
</body>
</html>
- Step 2: Create a minimal favicon.svg
<!-- public/favicon.svg -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#0a0a1a"/>
<text x="50%" y="55%" dominant-baseline="middle" text-anchor="middle"
font-family="Inter, sans-serif" font-weight="800" font-size="14" fill="#06b6d4">
FIT
</text>
</svg>
- Step 3: Create index.astro shell
---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro';
import '../styles/global.css';
---
<Layout>
<main>
<p class="text-white bg-dark-bg p-8">FIT Labs — Coming soon</p>
</main>
</Layout>
- Step 4: Verify dev server and build
npm run dev -- --port 4321 &
sleep 3
curl -s http://localhost:4321 | head -20
kill %1
npm run build
Expected: HTML returned with correct meta tags. Build succeeds.
- Step 5: Commit
git add src/layouts/Layout.astro src/pages/index.astro public/favicon.svg
git commit -m "feat: add base layout with SEO meta tags and structured data"
Chunk 2: Page Sections
Task 4: Navbar Component
Files:
-
Create:
src/components/Navbar.astro -
Step 1: Create Navbar.astro
---
// src/components/Navbar.astro
---
<nav id="navbar" class="fixed top-0 left-0 right-0 z-50 transition-all duration-300">
<div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<!-- Logo -->
<a href="https://www.factorit.com" target="_blank" rel="noopener noreferrer">
<img src="/LogoFIT.png" alt="FIT Labs" class="h-8" />
</a>
<!-- Desktop Nav -->
<div class="hidden md:flex items-center gap-8">
<a href="#capacidades" class="text-sm text-slate-300 hover:text-white transition-colors">Capacidades</a>
<a href="#nosotros" class="text-sm text-slate-300 hover:text-white transition-colors">Nosotros</a>
<a href="#contacto" class="px-4 py-2 text-sm font-semibold text-white rounded-lg bg-gradient-to-br from-brand-blue to-brand-violet hover:scale-105 transition-transform">
Contacto
</a>
</div>
<!-- Mobile Hamburger -->
<button id="menu-toggle" class="md:hidden text-white" aria-label="Abrir menú">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path id="menu-icon" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
<!-- Mobile Overlay -->
<div id="mobile-menu" class="fixed inset-0 bg-dark-bg/95 backdrop-blur-sm flex flex-col items-center justify-center gap-8 hidden z-40">
<button id="menu-close" class="absolute top-6 right-6 text-white" aria-label="Cerrar menú">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<a href="#capacidades" class="text-2xl text-white hover:text-brand-cyan transition-colors mobile-link">Capacidades</a>
<a href="#nosotros" class="text-2xl text-white hover:text-brand-cyan transition-colors mobile-link">Nosotros</a>
<a href="#contacto" class="text-2xl text-white hover:text-brand-cyan transition-colors mobile-link">Contacto</a>
</div>
</nav>
<script>
// Navbar scroll: transparent → solid
const navbar = document.getElementById('navbar');
window.addEventListener('scroll', () => {
if (window.scrollY > 50) {
navbar?.classList.add('bg-dark-bg/90', 'backdrop-blur-md', 'shadow-lg');
} else {
navbar?.classList.remove('bg-dark-bg/90', 'backdrop-blur-md', 'shadow-lg');
}
});
// Mobile menu toggle
const toggle = document.getElementById('menu-toggle');
const menu = document.getElementById('mobile-menu');
const close = document.getElementById('menu-close');
const links = document.querySelectorAll('.mobile-link');
function openMenu() {
menu?.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
function closeMenu() {
menu?.classList.add('hidden');
document.body.style.overflow = '';
}
toggle?.addEventListener('click', openMenu);
close?.addEventListener('click', closeMenu);
links.forEach(link => link.addEventListener('click', closeMenu));
menu?.addEventListener('click', (e) => {
if (e.target === menu) closeMenu();
});
</script>
- Step 2: Add Navbar to index.astro
---
import Layout from '../layouts/Layout.astro';
import Navbar from '../components/Navbar.astro';
import '../styles/global.css';
---
<Layout>
<Navbar />
<main>
<div class="h-screen bg-dark-bg flex items-center justify-center">
<p class="text-white text-2xl">Hero placeholder</p>
</div>
<div class="h-screen bg-light-bg" id="nosotros"></div>
<div class="h-screen bg-dark-bg" id="capacidades"></div>
<div class="h-screen bg-light-bg"></div>
<div class="h-screen bg-dark-bg" id="contacto"></div>
</main>
</Layout>
- Step 3: Verify — dev server, scroll nav behavior, mobile menu
npm run dev -- --port 4321
Open http://localhost:4321. Verify: navbar transparent at top, solid on scroll, mobile menu opens/closes.
- Step 4: Commit
git add src/components/Navbar.astro src/pages/index.astro
git commit -m "feat: add sticky navbar with scroll behavior and mobile menu"
Task 5: Hero Section
Files:
-
Create:
src/components/Hero.astro -
Step 1: Create Hero.astro with gradient mesh
---
// src/components/Hero.astro
---
<section id="hero" class="relative min-h-screen flex items-center justify-center bg-dark-bg overflow-hidden">
<!-- Gradient Mesh Blobs -->
<div class="absolute inset-0 overflow-hidden">
<div
class="absolute top-[-10%] right-[-5%] w-[500px] h-[500px] rounded-full opacity-60"
style="background: radial-gradient(circle, rgba(124,58,237,0.3) 0%, transparent 70%); filter: blur(80px); animation: mesh-drift-1 20s ease-in-out infinite;"
></div>
<div
class="absolute bottom-[-15%] left-[-10%] w-[600px] h-[600px] rounded-full opacity-50"
style="background: radial-gradient(circle, rgba(6,182,212,0.25) 0%, transparent 70%); filter: blur(80px); animation: mesh-drift-2 20s ease-in-out infinite;"
></div>
<div
class="absolute top-[40%] left-[40%] -translate-x-1/2 -translate-y-1/2 w-[550px] h-[550px] rounded-full opacity-40"
style="background: radial-gradient(circle, rgba(3,105,161,0.2) 0%, transparent 70%); filter: blur(80px); animation: mesh-drift-3 20s ease-in-out infinite;"
></div>
</div>
<!-- Content -->
<div class="relative z-10 text-center px-6 max-w-2xl mx-auto">
<p class="text-brand-cyan text-xs tracking-[0.2em] uppercase mb-4 font-semibold">
FactorIT Innovation Lab
</p>
<h1 class="text-5xl md:text-7xl font-extrabold text-white mb-6">
FIT Labs
</h1>
<p class="text-lg md:text-xl text-slate-400 mb-10 leading-relaxed">
Donde la inteligencia artificial se convierte en soluciones enterprise que escalan
</p>
<a
href="#capacidades"
class="inline-block px-8 py-3 text-base font-semibold text-white rounded-lg bg-gradient-to-br from-brand-blue to-brand-violet hover:scale-105 hover:shadow-[0_0_30px_rgba(124,58,237,0.3)] transition-all duration-300"
>
Explorar capacidades ↓
</a>
</div>
</section>
- Step 2: Add Hero to index.astro (replace placeholder)
Update index.astro to import and use <Hero /> instead of the hero placeholder div.
- Step 3: Verify — animated gradient blobs drifting, content centered, CTA visible
npm run dev -- --port 4321
Open http://localhost:4321. Verify: blobs animate, text is readable, button links to #capacidades.
- Step 4: Commit
git add src/components/Hero.astro src/pages/index.astro
git commit -m "feat: add hero section with gradient mesh animation"
Task 6: About Section
Files:
-
Create:
src/components/About.astro -
Step 1: Create About.astro
---
// src/components/About.astro
---
<section id="nosotros" class="bg-light-bg py-24 px-6">
<div class="max-w-3xl mx-auto text-center reveal">
<p class="text-brand-blue text-xs tracking-[0.2em] uppercase font-semibold mb-3">
Sobre nosotros
</p>
<h2 class="text-3xl md:text-4xl font-bold text-slate-900 mb-6">
La incubadora de innovación de FactorIT
</h2>
<p class="text-slate-500 text-lg leading-relaxed">
FIT Labs es donde exploramos, prototipamos y llevamos a producción soluciones
de inteligencia artificial para empresas. Nacimos del ADN tecnológico de
FactorIT para resolver los desafíos más complejos con IA — no entregamos
demos, entregamos sistemas que funcionan en el mundo real.
</p>
</div>
</section>
- Step 2: Add About to index.astro
Import and replace the #nosotros placeholder.
- Step 3: Verify and commit
npm run build
git add src/components/About.astro src/pages/index.astro
git commit -m "feat: add about section"
Task 7: Capabilities Section
Files:
-
Create:
src/components/Capabilities.astro -
Step 1: Create Capabilities.astro
---
// src/components/Capabilities.astro
const capabilities = [
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>`,
title: 'Documentos Inteligentes',
description: 'OCR, extracción y clasificación automática',
borderColor: 'border-brand-cyan',
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>`,
title: 'Agentes de IA',
description: 'Conversacionales y autónomos',
borderColor: 'border-brand-violet',
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg>`,
title: 'Automatización con IA',
description: 'Procesos end-to-end inteligentes',
borderColor: 'border-brand-blue',
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="m19 9-5 5-4-4-3 3"/></svg>`,
title: 'Análisis de Datos',
description: 'Insights e inteligencia de negocio',
borderColor: 'border-brand-cyan',
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8l-7 5V8l-7 5V4a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"/></svg>`,
title: 'Soluciones Industriales',
description: 'IA aplicada a tu vertical',
borderColor: 'border-brand-violet',
},
];
---
<section id="capacidades" class="bg-dark-bg py-24 px-6">
<div class="max-w-3xl mx-auto">
<div class="text-center mb-12 reveal">
<p class="text-brand-cyan text-xs tracking-[0.2em] uppercase font-semibold mb-3">
Capacidades
</p>
<h2 class="text-3xl md:text-4xl font-bold text-white">
Lo que construimos
</h2>
</div>
<div class="flex flex-col gap-4">
{capabilities.map((cap, i) => (
<div
class={`flex items-center gap-5 bg-dark-surface border-l-[3px] ${cap.borderColor} rounded-r-lg px-6 py-5 slide-in hover:bg-dark-surface/80 hover:shadow-[0_0_20px_rgba(6,182,212,0.05)] transition-all duration-300`}
style={`transition-delay: ${i * 100}ms;`}
>
<div class="text-brand-cyan shrink-0" set:html={cap.icon} />
<div>
<h3 class="text-white font-semibold">{cap.title}</h3>
<p class="text-slate-400 text-sm mt-1">{cap.description}</p>
</div>
</div>
))}
</div>
</div>
</section>
- Step 2: Add Capabilities to index.astro
Import and replace the #capacidades placeholder.
- Step 3: Verify — 5 items with colored borders, icons visible
npm run dev -- --port 4321
- Step 4: Commit
git add src/components/Capabilities.astro src/pages/index.astro
git commit -m "feat: add capabilities section with icon list"
Task 8: Differentiator Section
Files:
-
Create:
src/components/Differentiator.astro -
Step 1: Create Differentiator.astro
---
// src/components/Differentiator.astro
const values = [
{ text: 'Arquitectura para escalar', borderColor: 'border-brand-blue' },
{ text: 'Performance en producción', borderColor: 'border-brand-violet' },
{ text: 'Integración con tu ecosistema', borderColor: 'border-brand-cyan' },
];
---
<section class="bg-light-bg py-24 px-6">
<div class="max-w-4xl mx-auto reveal">
<p class="text-brand-blue text-xs tracking-[0.2em] uppercase font-semibold mb-8">
Nuestro enfoque
</p>
<div class="flex flex-col md:flex-row gap-12 md:gap-16">
<!-- Narrative -->
<div class="flex-1">
<h2 class="text-2xl md:text-3xl font-bold text-slate-900 mb-4">
Innovación con los pies en la tierra
</h2>
<p class="text-slate-500 leading-relaxed">
Nos apasiona la IA, pero nos obsesiona que funcione. Cada proyecto que
sale de FIT Labs está pensado para integrarse en operaciones reales,
soportar carga real, y crecer cuando tu negocio crece.
</p>
</div>
<!-- Values list -->
<div class="flex-1 flex flex-col gap-3">
{values.map((val) => (
<div class={`bg-white border-l-[3px] ${val.borderColor} rounded-lg px-5 py-4 shadow-sm`}>
<p class="text-slate-900 font-semibold text-sm">{val.text}</p>
</div>
))}
</div>
</div>
</div>
</section>
- Step 2: Add to index.astro
Import and replace the light-bg placeholder.
- Step 3: Verify and commit
npm run build
git add src/components/Differentiator.astro src/pages/index.astro
git commit -m "feat: add differentiator section with values"
Task 9: Contact (CTA) Section
Files:
-
Create:
src/components/Contact.astro -
Step 1: Create Contact.astro
---
// src/components/Contact.astro
---
<section id="contacto" class="relative bg-dark-bg py-24 px-6 overflow-hidden">
<!-- Subtle glow -->
<div
class="absolute top-[-30%] left-1/2 -translate-x-1/2 w-[500px] h-[300px] rounded-full opacity-40"
style="background: radial-gradient(circle, rgba(124,58,237,0.15) 0%, transparent 70%); filter: blur(60px);"
></div>
<div class="relative z-10 max-w-2xl mx-auto text-center reveal">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
¿Tienes un desafío en mente?
</h2>
<p class="text-slate-400 text-lg mb-10 leading-relaxed">
Cuéntanos qué problema quieres resolver. Nosotros vemos cómo la IA puede ayudarte.
</p>
<a
href="mailto:contacto@fitlabs.dev"
class="inline-block px-10 py-4 text-base font-semibold text-white rounded-lg bg-gradient-to-br from-brand-blue to-brand-violet hover:scale-105 hover:shadow-[0_0_30px_rgba(124,58,237,0.3)] transition-all duration-300"
>
Conversemos →
</a>
<p class="mt-5 text-sm text-slate-500">
<a href="mailto:contacto@fitlabs.dev" class="hover:text-brand-cyan transition-colors">
contacto@fitlabs.dev
</a>
</p>
</div>
</section>
- Step 2: Add to index.astro
Import and replace the #contacto placeholder.
- Step 3: Verify and commit
npm run build
git add src/components/Contact.astro src/pages/index.astro
git commit -m "feat: add CTA contact section"
Task 10: Footer
Files:
-
Create:
src/components/Footer.astro -
Step 1: Create Footer.astro
---
// src/components/Footer.astro
---
<footer class="bg-dark-footer py-8 px-6 border-t border-white/5">
<div class="max-w-6xl mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
<!-- Left: branding -->
<div>
<a href="https://www.factorit.com" target="_blank" rel="noopener noreferrer" class="text-white font-bold text-sm hover:text-brand-cyan transition-colors">
FIT <span class="text-brand-cyan">Labs</span>
</a>
<p class="text-slate-500 text-xs mt-1">by FactorIT</p>
</div>
<!-- Right: links -->
<div class="flex items-center gap-6 text-sm text-slate-500">
<a href="https://www.linkedin.com/company/factorit" target="_blank" rel="noopener noreferrer" class="hover:text-white transition-colors">LinkedIn</a>
<a href="https://github.com/factorit" target="_blank" rel="noopener noreferrer" class="hover:text-white transition-colors">GitHub</a>
<a href="https://www.factorit.com" target="_blank" rel="noopener noreferrer" class="hover:text-white transition-colors">factorit.com</a>
</div>
</div>
</footer>
- Step 2: Add to index.astro
Import Footer and place after </main>.
- Step 3: Verify and commit
npm run build
git add src/components/Footer.astro src/pages/index.astro
git commit -m "feat: add footer with FactorIT branding"
Chunk 3: Animations, Polish & Deployment
Task 11: Scroll Reveal Animations
Files:
-
Modify:
src/layouts/Layout.astro(add script before</body>) -
Step 1: Add IntersectionObserver script to Layout.astro
Add before </body>:
<script>
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
},
{ threshold: 0.1 }
);
document.querySelectorAll('.reveal, .slide-in').forEach((el) => {
observer.observe(el);
});
</script>
- Step 2: Verify — sections and capabilities animate on scroll
npm run dev -- --port 4321
Open http://localhost:4321. Scroll down. Verify: sections fade in, capability items slide in from left with staggered delay.
- Step 3: Commit
git add src/layouts/Layout.astro
git commit -m "feat: add scroll reveal animations"
Task 12: Final index.astro Assembly
Files:
-
Modify:
src/pages/index.astro -
Step 1: Ensure index.astro imports all components in order
---
import Layout from '../layouts/Layout.astro';
import Navbar from '../components/Navbar.astro';
import Hero from '../components/Hero.astro';
import About from '../components/About.astro';
import Capabilities from '../components/Capabilities.astro';
import Differentiator from '../components/Differentiator.astro';
import Contact from '../components/Contact.astro';
import Footer from '../components/Footer.astro';
import '../styles/global.css';
---
<Layout>
<Navbar />
<main>
<Hero />
<About />
<Capabilities />
<Differentiator />
<Contact />
</main>
<Footer />
</Layout>
- Step 2: Full visual check on dev server
npm run dev -- --port 4321
Check all 6 sections render, navbar works, animations fire, mobile menu works, all links work.
- Step 3: Build and verify output
npm run build
ls -la dist/
cat dist/index.html | head -30
Expected: dist/index.html exists with all content.
- Step 4: Commit
git add src/pages/index.astro
git commit -m "feat: assemble complete landing page"
Task 13: Docker + Nginx for Coolify
Files:
-
Create:
Dockerfile -
Create:
nginx/nginx.conf -
Step 1: Create nginx.conf
# nginx/nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
include /etc/nginx/mime.types;
gzip on;
gzip_min_length 1000;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# SPA fallback
location / {
try_files $uri $uri/index.html /index.html;
}
error_page 404 /404.html;
location = /404.html {
root /usr/share/nginx/html;
internal;
}
}
}
- Step 2: Create Dockerfile
# Dockerfile
FROM node:lts AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine AS runtime
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 8080
- Step 3: Test Docker build locally
docker build -t fitlabs-dev .
docker run --rm -p 8080:8080 fitlabs-dev &
sleep 2
curl -s http://localhost:8080 | head -20
docker stop $(docker ps -q --filter ancestor=fitlabs-dev)
Expected: HTML content returned, site accessible at http://localhost:8080.
- Step 4: Commit
git add Dockerfile nginx/nginx.conf
git commit -m "feat: add Dockerfile and nginx config for Coolify deployment"
Task 14: Final Build Verification
- Step 1: Clean build
rm -rf dist/
npm run build
Expected: Build succeeds with no warnings.
- Step 2: Check output size
du -sh dist/
find dist/ -type f | wc -l
Expected: Small output (~100KB–500KB), few files.
- Step 3: Verify HTML includes all sections
grep -c 'id="hero"\|id="nosotros"\|id="capacidades"\|id="contacto"' dist/index.html
Expected: 4 matches.
- Step 4: Final commit
git add -A
git commit -m "chore: final build verification — ready for Coolify deploy"
Deployment Notes
To deploy on Coolify:
- Push repo to a Git remote (GitHub, GitLab, etc.)
- In Coolify, create a new resource → Docker-based
- Point to the repo, Coolify will detect the Dockerfile
- Set port to
8080 - Configure domain:
fitlabs.dev - Deploy