Compare commits
11 Commits
fbf4f31d92
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b82f6d05b | ||
|
|
c22e3fe918 | ||
|
|
401000af97 | ||
|
|
915c8928c8 | ||
|
|
f923dd2f56 | ||
|
|
890975edf7 | ||
|
|
5a754059d8 | ||
|
|
226ac46c2b | ||
|
|
f3c2027e44 | ||
|
|
d604a658f9 | ||
|
|
35b8775531 |
173
README.md
173
README.md
@@ -1,3 +1,172 @@
|
||||
# landing-2pi
|
||||
# 2piDev Landing Page
|
||||
|
||||
Repositorio para el código de la landing page de 2piDev
|
||||
Landing institucional de 2piDev construida con Eleventy y Tailwind CSS. El sitio es completamente estático: se genera en `_site/` y se puede servir desde Nginx, Dokploy o cualquier hosting de archivos estáticos.
|
||||
|
||||
## Stack
|
||||
|
||||
- [Eleventy](https://www.11ty.dev/) para generar HTML estático.
|
||||
- [Tailwind CSS](https://tailwindcss.com/) para estilos utilitarios y componentes.
|
||||
- [Bun](https://bun.sh/) como runtime/package manager.
|
||||
- Nginx en producción vía Docker.
|
||||
|
||||
## Estructura
|
||||
|
||||
```txt
|
||||
.
|
||||
├── src/
|
||||
│ ├── index.njk # Landing principal
|
||||
│ ├── css/
|
||||
│ │ ├── tailwind.css # CSS fuente con directivas Tailwind
|
||||
│ │ └── styles.css # CSS compilado que Eleventy copia al build
|
||||
│ └── images/ # Logos, screenshots, retratos y favicon
|
||||
├── _site/ # Build local generado, ignorado por Git
|
||||
├── .eleventy.js # Configuración de Eleventy
|
||||
├── tailwind.config.js # Configuración de Tailwind
|
||||
├── postcss.config.js # PostCSS + Tailwind + Autoprefixer
|
||||
├── Dockerfile # Build estático + runtime Nginx
|
||||
└── package.json # Scripts y dependencias
|
||||
```
|
||||
|
||||
## Desarrollo Local
|
||||
|
||||
Instalar dependencias:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
Generar el sitio:
|
||||
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
Levantar servidor de desarrollo:
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Por defecto, Eleventy sirve el sitio en `http://localhost:8080/`.
|
||||
|
||||
## Scripts
|
||||
|
||||
```json
|
||||
{
|
||||
"build:css": "tailwindcss -i ./src/css/tailwind.css -o ./src/css/styles.css --minify",
|
||||
"build": "bun run build:css && eleventy",
|
||||
"dev": "bun run build:css && eleventy --serve --watch"
|
||||
}
|
||||
```
|
||||
|
||||
Notas:
|
||||
|
||||
- `src/css/tailwind.css` es el archivo que se edita.
|
||||
- `src/css/styles.css` es generado por Tailwind y se mantiene versionado para que Eleventy pueda copiarlo como asset estático.
|
||||
- `_site/` no se versiona.
|
||||
|
||||
## Deploy con Dokploy
|
||||
|
||||
La forma recomendada para Dokploy es usar el `Dockerfile` incluido. El contenedor:
|
||||
|
||||
1. Instala dependencias con Bun.
|
||||
2. Ejecuta `bun run build`.
|
||||
3. Copia `_site/` a Nginx.
|
||||
4. Sirve la landing desde el puerto `80`.
|
||||
|
||||
En Dokploy:
|
||||
|
||||
- Tipo de deploy: Dockerfile.
|
||||
- Puerto interno: `80`.
|
||||
- No requiere variables de entorno.
|
||||
- No requiere base de datos ni servicios externos.
|
||||
|
||||
Build manual de la imagen:
|
||||
|
||||
```bash
|
||||
docker build -t 2pidev-landing-page .
|
||||
```
|
||||
|
||||
Ejecución local con Docker:
|
||||
|
||||
```bash
|
||||
docker run --rm -p 8080:80 2pidev-landing-page
|
||||
```
|
||||
|
||||
Luego abrir `http://localhost:8080/`.
|
||||
|
||||
## Assets
|
||||
|
||||
Los assets viven en `src/images/` y se copian automáticamente a `_site/images/`.
|
||||
|
||||
Incluye:
|
||||
|
||||
- Logo principal y favicon.
|
||||
- Imágenes generadas/originales para hero, resultados y casos de éxito.
|
||||
- Retratos ficticios generados para testimonios.
|
||||
- Logos SVG de tecnologías: `.NET`, TypeScript, React, PostgreSQL y AWS.
|
||||
|
||||
Para reemplazar una imagen, conservar el mismo nombre de archivo evita cambios en el HTML. Si se cambia el nombre, actualizar la referencia en `src/index.njk`.
|
||||
|
||||
## Contenido Principal
|
||||
|
||||
Toda la landing está en `src/index.njk`. Las secciones actuales son:
|
||||
|
||||
- Header con navegación y CTA.
|
||||
- Hero principal.
|
||||
- Problemas frecuentes de empresas en crecimiento.
|
||||
- Soluciones digitales.
|
||||
- Resultados y métricas.
|
||||
- Método de trabajo.
|
||||
- Stack tecnológico.
|
||||
- Casos de éxito.
|
||||
- Testimonios.
|
||||
- Formulario de contacto.
|
||||
- Footer.
|
||||
|
||||
Algunas listas, como servicios, pasos, tecnologías, casos y testimonios, están definidas como arrays dentro del template para facilitar edición rápida.
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Mantener textos visibles con tildes y ortografía en español.
|
||||
- Mantener assets locales siempre que sea posible para evitar dependencias externas.
|
||||
- Usar SVG para logos e íconos de marca cuando exista una fuente confiable.
|
||||
- Antes de commitear, correr:
|
||||
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
## Git
|
||||
|
||||
Repositorio remoto:
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
```
|
||||
|
||||
Debe apuntar a:
|
||||
|
||||
```txt
|
||||
https://git.2pidev.com/jselesan/landing-2pi.git
|
||||
```
|
||||
|
||||
Flujo sugerido:
|
||||
|
||||
```bash
|
||||
git status
|
||||
bun run build
|
||||
git add .
|
||||
git commit -m "Describe el cambio"
|
||||
git push
|
||||
```
|
||||
|
||||
## Mantenimiento
|
||||
|
||||
Si se agregan nuevas tecnologías, sumar el SVG a `src/images/` y actualizar el array `techs` en `src/index.njk`.
|
||||
|
||||
Si se agregan nuevas secciones o componentes visuales, preferir mantenerlos en el mismo template mientras la landing siga siendo simple. Crear layouts/includes de Eleventy solo cuando haya repetición real o más páginas.
|
||||
|
||||
## Licencias y Uso de Imágenes
|
||||
|
||||
Los retratos de testimonios son ficticios y generados para este proyecto. Los logos de tecnologías pertenecen a sus respectivas marcas. Las imágenes de producto/interfaz usadas como visuales de la landing deben mantenerse como assets locales del proyecto para que el deploy sea reproducible.
|
||||
|
||||
1550
pnpm-lock.yaml
generated
Normal file
1550
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
4
src/_data/site.js
Normal file
4
src/_data/site.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
posthogKey: process.env.POSTHOG_API_KEY || null,
|
||||
posthogHost: process.env.POSTHOG_API_HOST || 'https://us.i.posthog.com',
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
30
src/gracias.njk
Normal file
30
src/gracias.njk
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
layout: false
|
||||
title: Gracias | 2piDev
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ title }}</title>
|
||||
<link rel="icon" href="/images/favicon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
<body class="min-h-screen bg-gradient-to-br from-orange-50 via-white to-blue-50">
|
||||
<div class="flex min-h-screen items-center justify-center px-4">
|
||||
<div class="max-w-md text-center">
|
||||
<div class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-green-100">
|
||||
<svg class="h-10 w-10 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="font-display text-4xl font-bold text-slate-900">¡Gracias!</h1>
|
||||
<p class="mt-4 text-lg text-slate-600">Hemos recibido tu mensaje y te contactaremos pronto.</p>
|
||||
<a href="/" class="mt-8 inline-flex items-center gap-2 rounded-lg bg-orange-600 px-6 py-3 text-sm font-bold text-white hover:bg-orange-700 transition-colors">
|
||||
← Volver al inicio
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
src/images/logo-64x64.png
Normal file
BIN
src/images/logo-64x64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
186
src/index.njk
186
src/index.njk
@@ -12,12 +12,50 @@ description: Soluciones digitales de élite para empresas que necesitan operar,
|
||||
<title>{{ title }}</title>
|
||||
<link rel="icon" href="/images/favicon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
{% if site.posthogKey %}
|
||||
<script>
|
||||
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host+"/static/array.full.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getSurveys getActiveMatchingSurveys captureException".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
||||
posthog.init('{{ site.posthogKey }}', {
|
||||
api_host: '{{ site.posthogHost }}',
|
||||
capture_performance: true,
|
||||
disable_session_recording: false,
|
||||
})
|
||||
posthog.capture('$pageview');
|
||||
</script>
|
||||
{% endif %}
|
||||
<!-- OpenReplay Tracking Code -->
|
||||
<script>
|
||||
var initOpts = {
|
||||
projectKey: "5o5HLGZhn4TlGBbOFkVu",
|
||||
// 0 - plain, 1 - obscured, 2 - ignored
|
||||
defaultInputMode: 2,
|
||||
obscureTextNumbers: false,
|
||||
obscureTextEmails: true,
|
||||
};
|
||||
var startOpts = { userID: "" };
|
||||
(function(A,s,a,y,e,r){
|
||||
r=window.OpenReplay=[e,r,y,[s-1, e]];
|
||||
s=document.createElement('script');s.src=A;s.async=!a;
|
||||
document.getElementsByTagName('head')[0].appendChild(s);
|
||||
r.start=function(v){r.push([0])};
|
||||
r.stop=function(v){r.push([1])};
|
||||
r.setUserID=function(id){r.push([2,id])};
|
||||
r.setUserAnonymousID=function(id){r.push([3,id])};
|
||||
r.setMetadata=function(k,v){r.push([4,k,v])};
|
||||
r.event=function(k,p,i){r.push([5,k,p,i])};
|
||||
r.issue=function(k,p){r.push([6,k,p])};
|
||||
r.isActive=function(){return false};
|
||||
r.getSessionToken=function(){};
|
||||
})("//static.openreplay.com/latest/openreplay.js",1,0,initOpts,startOpts);
|
||||
</script>
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="sticky top-0 z-50 border-b border-line/70 bg-white/92 backdrop-blur">
|
||||
<nav class="section-shell flex h-16 items-center justify-between gap-5">
|
||||
<a href="#inicio" class="flex items-center gap-3" aria-label="2piDev inicio">
|
||||
<img src="/images/logo.svg" alt="" class="h-10 w-10">
|
||||
<img src="/images/logo-64x64.png" alt="" class="h-10 w-10">
|
||||
<span class="font-display text-xl font-bold text-ink">2piDev</span>
|
||||
</a>
|
||||
<div class="hidden items-center gap-8 md:flex">
|
||||
@@ -284,20 +322,25 @@ description: Soluciones digitales de élite para empresas que necesitan operar,
|
||||
<p class="flex items-center gap-3"><span class="h-2.5 w-2.5 rounded-full bg-white"></span>Sede Central, Hub de Innovación Digital</p>
|
||||
</div>
|
||||
</div>
|
||||
<form class="rounded-xl border border-white/20 bg-white p-6 text-ink shadow-panel">
|
||||
<form action="https://formsubmit.co/hola@2pidev.com" method="POST" class="rounded-xl border border-white/20 bg-white p-6 text-ink shadow-panel">
|
||||
<input type="hidden" name="_next" value="https://www.2pidev.com/gracias">
|
||||
<input type="hidden" name="_captcha" value="false">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="text-sm font-bold">Nombre
|
||||
<input class="mt-2 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" type="text" name="nombre" placeholder="Juan Pérez">
|
||||
<input id="nombre" class="field mt-2 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" type="text" name="nombre" placeholder="Juan Pérez">
|
||||
<p class="error-message hidden mt-1 text-xs text-red-500">Por favor completá tu nombre</p>
|
||||
</label>
|
||||
<label class="text-sm font-bold">Empresa
|
||||
<input class="mt-2 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" type="text" name="empresa" placeholder="Nombre de tu empresa">
|
||||
<input id="empresa" class="field mt-2 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" type="text" name="empresa" placeholder="Nombre de tu empresa">
|
||||
</label>
|
||||
</div>
|
||||
<label class="mt-4 block text-sm font-bold">Email corporativo
|
||||
<input class="mt-2 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" type="email" name="email" placeholder="juan@empresa.com">
|
||||
<input id="email" class="field mt-2 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" type="text" name="email" placeholder="juan@empresa.com">
|
||||
<p class="error-message hidden mt-1 text-xs text-red-500">Ingresá un email válido</p>
|
||||
</label>
|
||||
<label class="mt-4 block text-sm font-bold">Mensaje
|
||||
<textarea class="mt-2 min-h-32 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" name="mensaje" placeholder="Cuéntanos un poco sobre tu proyecto..."></textarea>
|
||||
<textarea id="mensaje" class="field mt-2 min-h-32 w-full rounded-md border border-line px-4 py-3 font-normal outline-none focus:border-azure focus:ring-2 focus:ring-blue-100" name="mensaje" placeholder="Cuéntanos un poco sobre tu proyecto..."></textarea>
|
||||
<p class="error-message hidden mt-1 text-xs text-red-500">Por favor completá tu mensaje</p>
|
||||
</label>
|
||||
<button class="btn-primary mt-5 w-full" type="submit">Enviar mensaje</button>
|
||||
</form>
|
||||
@@ -306,7 +349,7 @@ description: Soluciones digitales de élite para empresas que necesitan operar,
|
||||
</main>
|
||||
|
||||
<footer class="bg-white py-12">
|
||||
<div class="section-shell grid gap-8 md:grid-cols-4">
|
||||
<div class="section-shell grid gap-8 md:grid-cols-[auto_1fr]">
|
||||
<div>
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="/images/logo.svg" alt="" class="h-10 w-10">
|
||||
@@ -314,23 +357,7 @@ description: Soluciones digitales de élite para empresas que necesitan operar,
|
||||
</div>
|
||||
<p class="mt-4 text-sm leading-6 text-muted">Engineering elegance. Software de alto rendimiento diseñado para el futuro corporativo.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-black uppercase tracking-wide text-ink">Compañía</h3>
|
||||
<div class="mt-4 space-y-2 text-sm text-muted">
|
||||
<p>Sobre nosotros</p>
|
||||
<p>Carreras</p>
|
||||
<p>Blog tech</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-black uppercase tracking-wide text-ink">Legal</h3>
|
||||
<div class="mt-4 space-y-2 text-sm text-muted">
|
||||
<p>Privacidad</p>
|
||||
<p>Términos</p>
|
||||
<p>Seguridad</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="md:text-right">
|
||||
<h3 class="text-sm font-black uppercase tracking-wide text-ink">Contacto</h3>
|
||||
<div class="mt-4 space-y-2 text-sm text-muted">
|
||||
<p>Hablemos de tu proyecto</p>
|
||||
@@ -342,5 +369,116 @@ description: Soluciones digitales de élite para empresas que necesitan operar,
|
||||
<p>(c) 2026 2piDev. Engineered Elegance.</p>
|
||||
</div>
|
||||
</footer>
|
||||
{% if site.posthogKey %}
|
||||
<script>
|
||||
(function() {
|
||||
document.addEventListener('click', function(e) {
|
||||
var link = e.target.closest('a');
|
||||
if (!link) return;
|
||||
if (link.matches('.btn-primary, .btn-secondary, .nav-link')) {
|
||||
posthog.capture('cta_click', {
|
||||
label: link.textContent.trim(),
|
||||
href: link.getAttribute('href')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var form = document.querySelector('form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function() {
|
||||
var data = new FormData(form);
|
||||
posthog.capture('contact_form_submitted', {
|
||||
nombre: data.get('nombre') || '',
|
||||
empresa: data.get('empresa') || '',
|
||||
email: data.get('email') || ''
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var sections = ['servicios', 'soluciones', 'tecnologias', 'contacto', 'casos'];
|
||||
var observer = new IntersectionObserver(function(entries) {
|
||||
entries.forEach(function(entry) {
|
||||
if (entry.isIntersecting) {
|
||||
posthog.capture('section_view', { section: entry.target.id });
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.3 });
|
||||
sections.forEach(function(id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) observer.observe(el);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
(function() {
|
||||
var form = document.querySelector('form');
|
||||
if (!form) return;
|
||||
|
||||
var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
var fields = {
|
||||
nombre: { el: document.getElementById('nombre'), msg: 'Por favor completá tu nombre' },
|
||||
email: { el: document.getElementById('email'), msg: 'Ingresá un email válido' },
|
||||
mensaje: { el: document.getElementById('mensaje'), msg: 'Por favor completá tu mensaje' }
|
||||
};
|
||||
|
||||
function showError(field, message) {
|
||||
field.el.classList.add('border-red-500');
|
||||
field.el.classList.remove('border-line');
|
||||
var errorEl = field.el.parentElement.querySelector('.error-message');
|
||||
if (errorEl) {
|
||||
errorEl.textContent = message || field.msg;
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function clearError(field) {
|
||||
field.el.classList.remove('border-red-500');
|
||||
field.el.classList.add('border-line');
|
||||
var errorEl = field.el.parentElement.querySelector('.error-message');
|
||||
if (errorEl) errorEl.classList.add('hidden');
|
||||
}
|
||||
|
||||
function validate() {
|
||||
var valid = true;
|
||||
|
||||
if (!fields.nombre.el.value.trim()) {
|
||||
showError(fields.nombre);
|
||||
valid = false;
|
||||
} else {
|
||||
clearError(fields.nombre);
|
||||
}
|
||||
|
||||
if (!fields.email.el.value.trim() || !emailRegex.test(fields.email.el.value.trim())) {
|
||||
showError(fields.email);
|
||||
valid = false;
|
||||
} else {
|
||||
clearError(fields.email);
|
||||
}
|
||||
|
||||
if (!fields.mensaje.el.value.trim()) {
|
||||
showError(fields.mensaje);
|
||||
valid = false;
|
||||
} else {
|
||||
clearError(fields.mensaje);
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (!validate()) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(fields).forEach(function(key) {
|
||||
fields[key].el.addEventListener('input', function() {
|
||||
clearError(fields[key]);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user