Applied
Engineering
Results
samueltorres.dev
samueltorres.dev is Samuel Torres Pichardo's portfolio. It is a static, bilingual (Spanish / English) site built on top of the Ashley HTML template. There is no framework and no build step for the pages themselves: every page is a hand-written .html file that the server delivers as-is.
The only moving parts are:
css/style.css (with Prepros).mail.php (contact form) and, optionally, by the in-house analytics.| Item | Value |
|---|---|
| Domain | https://samueltorres.dev (without www; www redirects here) |
| Spanish home | / (the index.html file) |
| English home | /home-1.html |
| Hosting | Shared cPanel hosting, Apache + PHP 8.3, free AutoSSL certificate |
| Web root | public_html/ |
| How files get there | Manual upload through the cPanel File Manager |
noindex, nofollow, is left out of sitemap.xml and is not linked from any menu, so search engines and regular visitors never land on it.
Everything lives in one folder. The HTML pages sit at the root; assets and server code live in subfolders. The Uploaded? column says whether the item belongs on the server.
| Path | What it contains | Uploaded? |
|---|---|---|
*.html (root) | All the pages of the site, in English and Spanish | Yes |
css/ | style.css (compiled) and the plugin stylesheets in css/plugins/ | Yes |
js/ | The site scripts and the libraries in js/plugins/ | Yes |
img/ | Photos, project screenshots, icons, the social-share cover (og-cover.jpg) | Yes |
docs/certificates/ | Certificate PDFs/images opened from the certifications page | Yes (only this subfolder of docs/) |
mail.php | Contact-form backend | Yes |
.htaccess | Apache rules: HTTPS, redirects, headers, caching | Yes, carefully |
robots.txt, sitemap.xml | Instructions and page list for search engines | Yes |
favicon*, apple-touch-icon.png, site.webmanifest | Browser-tab and home-screen icons | Yes |
api/, admin/, cron/ | In-house analytics (see its section) | Only if analytics is turned on |
config/ | Database credentials (db.php) | Only with analytics, never to git |
scss/ | Source of the stylesheet | No |
sql/, tools/ | Database schema, admin creation script, sitemap generator | No |
*.bak, *.bak2, *.zip | Local backups | Never (anyone could download them by URL) |
*.md, contexto-completo.txt, DEPLOY-PENDIENTE.txt | Internal notes | No |
Every page exists twice, once per language, as two separate files. There is no translation engine: each file already contains its final text.
contact.html, cv.html, case-study-bea.html…-es: contact-es.html, cv-es.html, case-study-bea-es.html…index.html (served at /) and its English twin is home-1.html. The old home-1-es.html is retired and redirects to /.404.html is the only page without a twin.| Group | Files (English name) |
|---|---|
| Home | index.html (ES) / home-1.html (EN) |
| Projects | portfolio-1.html, project-*.html (6), case-study-*.html (13) |
| Services | services.html, service.html, service-ai, -cloud, -linux, -security |
| Profile | cv.html, certifications.html |
| Contact and legal | contact.html, privacy.html |
| Internal | documentation.html (this page), 404.html |
initLangSwitch() in js/main.js adds the ES · EN button to the top bar of every page. It works out the twin page from the file name (applying the naming convention and the home exception above) and simply links to it. It remembers the choice in localStorage but never redirects on its own, so whoever shares a Spanish link always gets the Spanish page. On 404.html the button is disabled.
name.html and name-es.html), set <html lang="en"> / lang="es", add the canonical and the three hreflang tags (en, es, x-default) to both, and regenerate the sitemap (see SEO). The language switch will find the twin automatically.
Every page follows the same skeleton. Knowing what each block does makes it easy to edit any page without breaking the template:
<!DOCTYPE html>
<html lang="en"> <!-- or "es" on the Spanish twin -->
<head>
<!-- meta, description, 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>
<!-- scrollbar progress -->
<div class="mil-progress-track"> ... </div>
<!-- menu -->
<div class="mil-menu-frame"> ... </div>
<!-- curtain -->
<div class="mil-curtain"></div>
<!-- frame -->
<div class="mil-frame"> ... </div>
<!-- content -->
<div class="mil-content">
<div id="swupMain" class="mil-main-transition">
<!-- banner --> ...
<!-- page sections --> ...
<!-- footer --> ...
<!-- hidden elements --> ...
</div>
</div>
</div>
<!-- wrapper end -->
<!-- scripts: plugins, then js/main.js, then page scripts -->
</body>
</html>
| Block | What it does |
|---|---|
.mil-ball | The custom circular cursor that follows the mouse (desktop only). Its "More" / "Choose" labels appear over links and sliders. |
.mil-preloader | Intro animation shown while the page loads (see Preloader). |
.mil-progress-track | The thin bar that shows how far down the page you have scrolled. |
.mil-menu-frame | The full-screen menu that opens from the hamburger button. The links live in #swupMenu. |
.mil-curtain | The layer that covers the screen during page transitions. |
.mil-frame | Fixed top bar (logo, language switch, menu button) and bottom bar (current page, "Back to top"). |
#swupMain | The only part that changes between pages. Swup swaps just this container, so everything outside it stays in place. |
.mil-hidden-elements | Decorative SVGs (arrows, lines, dodecahedron) that main.js copies into the page where they are needed. |
mil-up: the element fades in and rises as it enters the screen (GSAP ScrollTrigger).mil-mb-30, mil-mb-60, mil-p-120-90…: spacing helpers (bottom margin, section padding) in pixels.mil-dark-bg, mil-muted, mil-light-soft, mil-thin: background and text tones.Pages load four plugin stylesheets plus the site's own stylesheet, always in this order:
<!-- 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-*), not the rest of Bootstrap.css/style.css is generated; do not edit it by hand. Edit the files in scss/ and compile with Prepros (the prepros.config file in the root already has the configuration). scss/style.scss is the entry point that imports the rest:
| File | What it contains |
|---|---|
_variables.scss | Colours, fonts (it imports Outfit) and shared values. Change a colour here and it changes everywhere. |
_common.scss | Base: typography, links, buttons, forms, breadcrumbs, backgrounds, spacing. |
_components.scss | Template pieces: preloader, cursor, menu, banner, services, footer, portfolio, accordion, 404, transitions, the language switch… |
_recognition.scss | The recognition block on the home page. |
_certifications.scss | The certifications page. |
_project-preview.scss | The project grid and its previews. |
_documentation.scss | This page: side index, code blocks, copy button. |
Every page loads the same libraries and then js/main.js. After that come only the scripts that page needs.
<!-- 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>
<!-- page scripts (only where needed) -->
<script src="/js/contact-form.js"></script>
| Library | What it is used for |
|---|---|
| jQuery | Base for main.js and Fancybox. |
| Swup | Smooth transitions between pages without a full reload. |
| Swiper | Sliders (projects, testimonials, galleries). |
| Fancybox | Lightbox for images and certificates. |
| GSAP + ScrollTrigger + ScrollTo | Animations: preloader, reveal on scroll, progress bar, smooth jumps to anchors. |
| Smooth scroll | Softer mouse-wheel scrolling. |
| File | Loaded on | What it does |
|---|---|---|
main.js | Every page | Swup, language switch, preloader, cursor, menu, progress bar, scroll animations, sliders and Fancybox. |
contact-form.js | Contact | Sends the form to mail.php without reloading and shows the result message. |
certifications.js | Home, certifications | Configures the Fancybox lightbox for certificates. |
recognition.js | Home | The interactive recognition block. |
documentation.js | This page | Highlights the current section in the index and powers the "Copy" buttons. |
track.js | None yet | In-house analytics, ready but not switched on (see its section). |
i18n.js, translations.js | None | Old translation engine, no longer used. Kept on disk for reference only. |
#swupMain is swapped. A page-specific script therefore has to start again on the swup:contentReplaced event and clean up (listeners, timers, ScrollTriggers) on swup:willReplaceContent. The "REINIT" section at the end of main.js and the files recognition.js and documentation.js show how.
Every page starts with a short black-screen animation in two stages, run by GSAP from the "preloader" section of js/main.js:
.mil-animation-1) appear and fade one after another..mil-animation-2): a colour block sweeps across and reveals samueltorres.dev.mil-hidden class and the page becomes usable.<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">Applied</p>
<p class="mil-h3 mil-muted">Engineering</p>
<p class="mil-h3 mil-muted mil-thin">Results</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 file (a search-and-replace across the folder does it) and every one of them has to be uploaded again.
The form on contact.html / contact-es.html sends an email to Samuel's inbox. It involves three pieces:
<form id="mil-contact-form" action="/mail.php" method="POST"> with the fields name, email, message and a hidden anti-bot field, website. The data-msg-* attributes hold the messages shown to the visitor in each language.js/contact-form.js: checks that nothing is empty, disables the button ("Sending…"), sends the data with fetch plus the X-Requested-With: XMLHttpRequest header, and then shows the success or error message.mail.php: validates everything again on the server and sends the email with PHP's mail() function.| # | Check | If it fails |
|---|---|---|
| 1 | The request is a POST | 405 method_not_allowed |
| 2 | It carries the X-Requested-With header | 403 forbidden |
| 3 | It comes from samueltorres.dev or www.samueltorres.dev (Origin/Referer, list in ALLOWED_ORIGINS) | 403 forbidden |
| 4 | That IP has not sent 3 requests in the last 15 minutes | 429 rate_limited |
| 5 | The hidden website field is empty (a bot would fill it in) | Replies "success" but sends nothing |
| 6 | Name, email and message are filled in | 400 missing_fields |
| 7 | The email address is valid | 400 invalid_email |
| 8 | The server manages to send the email | 500 mail_failed |
When everything passes, the answer is {"success": true}. The email goes out as [email protected] with the visitor's address in Reply-To, so clicking "Reply" in Gmail answers the visitor directly. The recipient is written into the server code and never comes from the browser, which means nobody can use the form to send email to anyone else.
samueltorres.dev show as valid. Without them Gmail usually rejects or hides mail from no-reply@.error_log file in public_html.The site includes its own visit counter, with no Google Analytics or any other third party. It is built but not switched on: no page loads track.js yet. It has four parts:
| Part | What it does |
|---|---|
js/track.js | Sends a single request per page view (page, title, referrer, language) to /api/collect.php. No cookies or localStorage, and it does nothing if the browser has "Do Not Track" on. |
api/collect.php | Only accepts requests from the site's own domain and saves the visit in MySQL (sessions and visits tables). |
admin/ | Password-protected panel: overview, visits, most-viewed pages and sessions. |
cron/purge.php | Deletes data older than 90 days. Runs from the hosting's cron, never from the browser. |
Privacy: the IP address is never stored. The server keeps only a one-way fingerprint (SHA-256 of IP + browser + the day's date) that changes every day, so it groups the pages seen in one visit but cannot identify anyone or follow them from one day to the next. Full steps to switch it on (database, config/db.php, first admin, cron) are in README-analytics.md.
x-default. This tells Google which version to show to each visitor.img/og-cover.jpg) used when a link is shared on LinkedIn, WhatsApp and similar apps. Currently present on the home pages, the portfolio, the case studies and the certifications page.sitemap.xml: the list of the 60 public URLs with their language pairs. Do not edit it by hand. Regenerate it with php tools/build-sitemap.php from the project root. The script skips 404.html, documentation*.html and the retired home-1-es.html.robots.txt: allows the whole site, blocks /admin/, /api/, /config/, /sql/, /docs/, /cron/ and /tools/, and points to the sitemap.The .htaccess file in the root tells Apache how to serve the site:
| Rule | Effect |
|---|---|
| HTTPS | Every http:// request is redirected (301) to https://. |
| No www | www.samueltorres.dev is redirected (301) to samueltorres.dev. |
| Old home | /home-1-es.html is redirected (301) to /. |
| Error page | Any missing URL shows 404.html. |
| No folder listing | Options -Indexes: nobody can browse a folder's contents. |
| Security headers | nosniff, X-Frame-Options: SAMEORIGIN (the site cannot be embedded on another site) and Referrer-Policy. |
| Caching | HTML is never cached (changes show up immediately). Images, CSS and JS are cached for 1 year. |
| Compression | gzip / Brotli when the server supports it. |
css/style.css or a .js file and upload it, visitors who already had the old one may keep it. To force the update, change the reference in the HTML (for example style.css?v=2). To check your own changes, reload with Ctrl+F5.
public_html also contains other sites in subfolders, and they inherit this .htaccess. That is why every rule is wrapped in <IfModule> and targets specific paths. After changing it, test the whole site and keep the previous copy to hand in case you need to roll back.
Changes are published by hand, uploading files through the cPanel File Manager into public_html, keeping the same folder structure as the project.
css/style.css gets updated.sitemap.xml.DEPLOY-PENDIENTE.txt (a local list in the form file → upload to public_html/folder/, never uploaded or committed to git).public_html, replacing the old one.mail.php, send a test from the form.*.bak, *.bak2, *.zip, config/ (except with analytics), sql/, tools/, scss/, *.md, contexto-completo.txt or anything in docs/ other than docs/certificates/. The server delivers them as plain text or as downloads to anyone who guesses the URL. If one is already on the server, delete it.
scss/_variables.scss and used across the whole site.<i class="fab fa-linkedin"></i> (brands) or <i class="fas fa-chevron-down"></i> (solid)..mil-hidden-elements).favicon.ico, favicon-16x16.png, favicon-32x32.png, apple-touch-icon.png and img/icon-192.png / icon-512.png (declared in site.webmanifest).