Applied
Engineering
Results
samueltorres.dev
samueltorres.dev es el portafolio de Samuel Torres Pichardo. Es un sitio estático y bilingüe (español / inglés) construido sobre la plantilla HTML Ashley. No usa framework ni proceso de compilación para las páginas: cada página es un archivo .html escrito a mano que el servidor entrega tal cual.
Las únicas piezas que se mueven son:
css/style.css (con Prepros).mail.php (formulario de contacto) y, opcionalmente, para la analítica propia.| Dato | Valor |
|---|---|
| Dominio | https://samueltorres.dev (sin www; el www redirige aquí) |
| Home en español | / (el archivo index.html) |
| Home en inglés | /home-1.html |
| Hosting | cPanel compartido, Apache + PHP 8.3, certificado AutoSSL gratuito |
| Carpeta pública | public_html/ |
| Cómo se suben los archivos | A mano, con el Administrador de archivos (File Manager) de cPanel |
noindex, nofollow, está fuera de sitemap.xml y ningún menú la enlaza, así que ni los buscadores ni los visitantes normales llegan a ella.
Todo vive en una sola carpeta. Las páginas HTML están en la raíz y los recursos y el código del servidor en subcarpetas. La columna ¿Se sube? indica si el elemento va al servidor.
| Ruta | Qué contiene | ¿Se sube? |
|---|---|---|
*.html (raíz) | Todas las páginas del sitio, en inglés y en español | Sí |
css/ | style.css (compilado) y las hojas de los plugins en css/plugins/ | Sí |
js/ | Los scripts del sitio y las librerías en js/plugins/ | Sí |
img/ | Fotos, capturas de proyectos, iconos y la portada para redes (og-cover.jpg) | Sí |
docs/certificates/ | PDF e imágenes de certificados que se abren desde la página de certificaciones | Sí (solo esta subcarpeta de docs/) |
mail.php | Backend del formulario de contacto | Sí |
.htaccess | Reglas de Apache: HTTPS, redirecciones, cabeceras, caché | Sí, con cuidado |
robots.txt, sitemap.xml | Instrucciones y lista de páginas para los buscadores | Sí |
favicon*, apple-touch-icon.png, site.webmanifest | Iconos de la pestaña del navegador y de la pantalla de inicio | Sí |
api/, admin/, cron/ | Analítica propia (ver su sección) | Solo si se activa la analítica |
config/ | Credenciales de la base de datos (db.php) | Solo con analítica, nunca a git |
scss/ | Código fuente de los estilos | No |
sql/, tools/ | Esquema de la base de datos, script para crear el administrador, generador del sitemap | No |
*.bak, *.bak2, *.zip | Respaldos locales | Nunca (cualquiera podría descargarlos por URL) |
*.md, contexto-completo.txt, DEPLOY-PENDIENTE.txt | Notas internas | No |
Cada página existe dos veces, una por idioma, como dos archivos separados. No hay motor de traducción: cada archivo ya trae su texto definitivo.
contact.html, cv.html, case-study-bea.html…-es: contact-es.html, cv-es.html, case-study-bea-es.html…index.html (se sirve en /) y su gemelo en inglés es home-1.html. El viejo home-1-es.html está retirado y redirige a /.404.html es la única página sin gemela.| Grupo | Archivos (nombre en inglés) |
|---|---|
| Home | index.html (ES) / home-1.html (EN) |
| Proyectos | portfolio-1.html, project-*.html (6), case-study-*.html (13) |
| Servicios | services.html, service.html, service-ai, -cloud, -linux, -security |
| Perfil | cv.html, certifications.html |
| Contacto y legal | contact.html, privacy.html |
| Internas | documentation.html (esta página), 404.html |
initLangSwitch(), en js/main.js, agrega el botón ES · EN a la barra superior de todas las páginas. Deduce la página gemela a partir del nombre del archivo (con la convención y la excepción del home de arriba) y simplemente enlaza a ella. Guarda la elección en localStorage, pero nunca redirige por su cuenta: quien comparte un enlace en español siempre recibe la página en español. En 404.html el botón aparece deshabilitado.
nombre.html y nombre-es.html), pon <html lang="en"> / lang="es", agrega a ambos el canonical y las tres etiquetas hreflang (en, es, x-default) y regenera el sitemap (ver SEO). El selector de idioma encontrará la gemela solo.
Todas las páginas siguen el mismo esqueleto. Si sabes qué hace cada bloque, puedes editar cualquier página sin romper la plantilla:
<!DOCTYPE html>
<html lang="es"> <!-- o "en" en la gemela en inglés -->
<head>
<!-- meta, descripción, canonical, hreflang, favicons, CSS -->
</head>
<body>
<!-- wrapper -->
<div class="mil-wrapper" id="top">
<!-- cursor -->
<div class="mil-ball"> ... </div>
<!-- preloader -->
<div class="mil-preloader"> ... </div>
<!-- barra de progreso -->
<div class="mil-progress-track"> ... </div>
<!-- menú -->
<div class="mil-menu-frame"> ... </div>
<!-- cortina -->
<div class="mil-curtain"></div>
<!-- marco -->
<div class="mil-frame"> ... </div>
<!-- contenido -->
<div class="mil-content">
<div id="swupMain" class="mil-main-transition">
<!-- banner --> ...
<!-- secciones de la página --> ...
<!-- footer --> ...
<!-- elementos ocultos --> ...
</div>
</div>
</div>
<!-- fin del wrapper -->
<!-- scripts: plugins, luego js/main.js, luego los de la página -->
</body>
</html>
| Bloque | Qué hace |
|---|---|
.mil-ball | El cursor circular propio que sigue al mouse (solo en escritorio). Sus etiquetas "More" / "Choose" aparecen sobre enlaces y sliders. |
.mil-preloader | Animación de entrada mientras carga la página (ver Preloader). |
.mil-progress-track | La barra delgada que indica cuánto has bajado en la página. |
.mil-menu-frame | El menú de pantalla completa que se abre con el botón de hamburguesa. Los enlaces están en #swupMenu. |
.mil-curtain | La capa que cubre la pantalla durante la transición entre páginas. |
.mil-frame | Barra superior fija (logo, selector de idioma, botón de menú) e inferior (página actual, "Subir"). |
#swupMain | La única parte que cambia entre páginas. Swup reemplaza solo este contenedor, así que todo lo de fuera se queda en su lugar. |
.mil-hidden-elements | SVG decorativos (flechas, líneas, dodecaedro) que main.js copia a la página donde hacen falta. |
mil-up: el elemento aparece y sube al entrar en pantalla (GSAP ScrollTrigger).mil-mb-30, mil-mb-60, mil-p-120-90…: ayudas de espaciado (margen inferior, relleno de sección) en píxeles.mil-dark-bg, mil-muted, mil-light-soft, mil-thin: tonos de fondo y de texto.Las páginas cargan cuatro hojas de estilo de plugins y la hoja propia del sitio, siempre en este orden:
<!-- bootstrap grid css -->
<link rel="stylesheet" href="/css/plugins/bootstrap-grid.css">
<!-- font awesome css -->
<link rel="stylesheet" href="/css/plugins/font-awesome.min.css">
<!-- swiper css -->
<link rel="stylesheet" href="/css/plugins/swiper.min.css">
<!-- fancybox css -->
<link rel="stylesheet" href="/css/plugins/fancybox.min.css">
<!-- ashley scss -->
<link rel="stylesheet" href="/css/style.css">
container, row, col-lg-*), no el resto de Bootstrap.css/style.css es un archivo generado: no lo edites a mano. Edita los archivos de scss/ y compila con Prepros (el archivo prepros.config de la raíz ya trae la configuración). scss/style.scss es la entrada que importa a los demás:
| Archivo | Qué contiene |
|---|---|
_variables.scss | Colores, fuentes (aquí se importa Outfit) y valores compartidos. Si cambias un color aquí, cambia en todo el sitio. |
_common.scss | Base: tipografía, enlaces, botones, formularios, migas de pan, fondos, espaciados. |
_components.scss | Piezas de la plantilla: preloader, cursor, menú, banner, servicios, footer, portafolio, acordeón, 404, transiciones, selector de idioma… |
_recognition.scss | El bloque de reconocimientos del home. |
_certifications.scss | La página de certificaciones. |
_project-preview.scss | La rejilla de proyectos y sus vistas previas. |
_documentation.scss | Esta página: índice lateral, bloques de código, botón de copiar. |
Todas las páginas cargan las mismas librerías y después js/main.js. Al final van solo los scripts que esa página necesita.
<!-- jQuery js -->
<script src="/js/plugins/jquery.min.js"></script>
<!-- swup js -->
<script src="/js/plugins/swup.min.js"></script>
<!-- swiper js -->
<script src="/js/plugins/swiper.min.js"></script>
<!-- fancybox js -->
<script src="/js/plugins/fancybox.min.js"></script>
<!-- gsap js -->
<script src="/js/plugins/gsap.min.js"></script>
<!-- scroll smoother -->
<script src="/js/plugins/smooth-scroll.js"></script>
<!-- scroll trigger js -->
<script src="/js/plugins/ScrollTrigger.min.js"></script>
<!-- scroll to js -->
<script src="/js/plugins/ScrollTo.min.js"></script>
<!-- ashley js -->
<script src="/js/main.js"></script>
<!-- scripts de la página (solo donde hacen falta) -->
<script src="/js/contact-form.js"></script>
| Librería | Para qué se usa |
|---|---|
| jQuery | Base de main.js y de Fancybox. |
| Swup | Transiciones suaves entre páginas sin recargar todo. |
| Swiper | Sliders (proyectos, testimonios, galerías). |
| Fancybox | Visor ampliado de imágenes y certificados. |
| GSAP + ScrollTrigger + ScrollTo | Animaciones: preloader, aparición al hacer scroll, barra de progreso, saltos suaves a anclas. |
| Smooth scroll | Desplazamiento más suave con la rueda del mouse. |
| Archivo | Se carga en | Qué hace |
|---|---|---|
main.js | Todas las páginas | Swup, selector de idioma, preloader, cursor, menú, barra de progreso, animaciones de scroll, sliders y Fancybox. |
contact-form.js | Contacto | Envía el formulario a mail.php sin recargar y muestra el mensaje de resultado. |
certifications.js | Home, certificaciones | Configura el visor Fancybox para los certificados. |
recognition.js | Home | El bloque interactivo de reconocimientos. |
documentation.js | Esta página | Resalta la sección actual en el índice y hace funcionar los botones "Copiar". |
track.js | Ninguna todavía | Analítica propia, lista pero sin activar (ver su sección). |
i18n.js, translations.js | Ninguna | Motor de traducción antiguo, ya sin uso. Se conservan en disco solo como referencia. |
#swupMain. Por eso un script propio de una página tiene que volver a arrancar en el evento swup:contentReplaced y limpiar lo suyo (listeners, temporizadores, ScrollTriggers) en swup:willReplaceContent. La sección "REINIT" al final de main.js y los archivos recognition.js y documentation.js muestran cómo.
Cada página arranca con una animación breve sobre fondo negro, en dos etapas, que GSAP ejecuta desde la sección "preloader" de js/main.js:
.mil-animation-1) aparecen y se desvanecen una tras otra..mil-animation-2): un bloque de color lo barre y revela samueltorres.dev.mil-hidden y la página queda lista para usarse.<div class="mil-preloader">
<div class="mil-preloader-animation">
<div class="mil-pos-abs mil-animation-1">
<p class="mil-h3 mil-muted mil-thin">Ingeniería</p>
<p class="mil-h3 mil-muted">Aplicada</p>
<p class="mil-h3 mil-muted mil-thin">Resultados</p>
</div>
<div class="mil-pos-abs mil-animation-2">
<div class="mil-reveal-frame">
<p class="mil-reveal-box"></p>
<p class="mil-h3 mil-muted mil-thin">samueltorres.dev</p>
</div>
</div>
</div>
</div>
.html (un buscar y reemplazar en la carpeta basta) y volver a subirlos todos. Las páginas en inglés usan "Applied / Engineering / Results".
El formulario de contact.html / contact-es.html envía un correo a la bandeja de Samuel. Participan tres piezas:
<form id="mil-contact-form" action="/mail.php" method="POST"> con los campos name, email, message y un campo oculto antibots, website. Los atributos data-msg-* guardan los mensajes que ve el visitante en cada idioma.js/contact-form.js: revisa que nada esté vacío, desactiva el botón ("Enviando…"), manda los datos con fetch más la cabecera X-Requested-With: XMLHttpRequest y luego muestra el mensaje de éxito o de error.mail.php: vuelve a validar todo en el servidor y envía el correo con la función mail() de PHP.| # | Revisión | Si falla |
|---|---|---|
| 1 | La petición es POST | 405 method_not_allowed |
| 2 | Trae la cabecera X-Requested-With | 403 forbidden |
| 3 | Viene de samueltorres.dev o www.samueltorres.dev (Origin/Referer, lista en ALLOWED_ORIGINS) | 403 forbidden |
| 4 | Esa IP no lleva 3 envíos en los últimos 15 minutos | 429 rate_limited |
| 5 | El campo oculto website está vacío (un bot lo llenaría) | Responde "éxito" pero no envía nada |
| 6 | Nombre, correo y mensaje están llenos | 400 missing_fields |
| 7 | El correo tiene un formato válido | 400 invalid_email |
| 8 | El servidor logra enviar el correo | 500 mail_failed |
Si todo pasa, la respuesta es {"success": true}. El correo sale como [email protected] con la dirección del visitante en Reply-To, así que al pulsar "Responder" en Gmail le contestas directo al visitante. El destinatario está escrito en el código del servidor y nunca viene del navegador, por lo que nadie puede usar el formulario para mandar correos a otra persona.
samueltorres.dev aparezcan como válidos. Sin ellos, Gmail suele rechazar o esconder los correos de no-reply@.error_log de public_html.El sitio trae su propio contador de visitas, sin Google Analytics ni ningún otro tercero. Está construido pero no activado: todavía ninguna página carga track.js. Tiene cuatro partes:
| Parte | Qué hace |
|---|---|
js/track.js | Manda una sola petición por página vista (página, título, de dónde viene, idioma) a /api/collect.php. Sin cookies ni localStorage, y no hace nada si el navegador tiene activado "No rastrear". |
api/collect.php | Solo acepta peticiones del propio dominio y guarda la visita en MySQL (tablas sessions y visits). |
admin/ | Panel con contraseña: resumen, visitas, páginas más vistas y sesiones. |
cron/purge.php | Borra los datos de más de 90 días. Se ejecuta desde el cron del hosting, nunca desde el navegador. |
Privacidad: la dirección IP nunca se guarda. El servidor solo conserva una huella de un solo sentido (SHA-256 de IP + navegador + la fecha del día) que cambia a diario: sirve para agrupar las páginas de una misma visita, pero no permite identificar a nadie ni seguirlo de un día a otro. Los pasos completos para activarla (base de datos, config/db.php, primer administrador, cron) están en README-analytics.md.
x-default. Así Google sabe qué versión mostrarle a cada visitante.img/og-cover.jpg) que se usan al compartir un enlace en LinkedIn, WhatsApp y apps parecidas. Hoy está en los home, el portafolio, los casos de estudio y certificaciones.sitemap.xml: la lista de las 60 URL públicas con sus pares de idioma. No lo edites a mano. Regenéralo con php tools/build-sitemap.php desde la raíz del proyecto. El script omite 404.html, documentation*.html y el retirado home-1-es.html.robots.txt: permite todo el sitio, bloquea /admin/, /api/, /config/, /sql/, /docs/, /cron/ y /tools/, y apunta al sitemap.El archivo .htaccess de la raíz le dice a Apache cómo servir el sitio:
| Regla | Efecto |
|---|---|
| HTTPS | Toda petición http:// se redirige (301) a https://. |
| Sin www | www.samueltorres.dev se redirige (301) a samueltorres.dev. |
| Home antiguo | /home-1-es.html se redirige (301) a /. |
| Página de error | Cualquier URL que no exista muestra 404.html. |
| Sin listado de carpetas | Options -Indexes: nadie puede ver el contenido de una carpeta. |
| Cabeceras de seguridad | nosniff, X-Frame-Options: SAMEORIGIN (el sitio no se puede incrustar en otro) y Referrer-Policy. |
| Caché | El HTML nunca se guarda en caché (los cambios se ven al momento). Imágenes, CSS y JS se guardan 1 año. |
| Compresión | gzip / Brotli si el servidor los soporta. |
css/style.css o un .js y lo subes, los visitantes que ya tenían el anterior pueden seguir viéndolo. Para forzar la actualización, cambia la referencia en el HTML (por ejemplo style.css?v=2). Para revisar tus propios cambios, recarga con Ctrl+F5.
public_html de la cuenta de hosting también contiene otros sitios en subcarpetas, y estos heredan este .htaccess. Por eso cada regla va dentro de <IfModule> y apunta a rutas concretas. Después de cambiarlo, prueba todo el sitio y ten a la mano la copia anterior por si hay que revertir.
Los cambios se publican a mano: se suben los archivos con el Administrador de archivos de cPanel a public_html, respetando la misma estructura de carpetas del proyecto.
css/style.css.sitemap.xml.DEPLOY-PENDIENTE.txt (una lista local con el formato archivo → subir a public_html/carpeta/; no se sube al servidor ni a git).public_html, reemplazando el anterior.mail.php, manda una prueba desde el formulario.*.bak, *.bak2, *.zip, config/ (salvo con analítica), sql/, tools/, scss/, *.md, contexto-completo.txt ni nada de docs/ que no sea docs/certificates/. El servidor los entrega como texto o como descarga a cualquiera que adivine la URL. Si alguno ya está en el servidor, bórralo.
scss/_variables.scss y se usa en todo el sitio.<i class="fab fa-linkedin"></i> (marcas) o <i class="fas fa-chevron-down"></i> (sólidos)..mil-hidden-elements).favicon.ico, favicon-16x16.png, favicon-32x32.png, apple-touch-icon.png e img/icon-192.png / icon-512.png (declarados en site.webmanifest).