All projects
Live · clientProject 06
Corporate siteClientSSG

PAX Elevators

One central message, “a part manufactured in 3 weeks instead of 3 months”, turned into a static site that loads on 3G, ranks on Google and ends in a WhatsApp message with context for the owner.

Nuxt 3 SSGTypeScriptTailwindNitroResendLeafletVercel

When the imported part takes 3 months, they fabricate it in 3 weeks. The website had one job: get that sentence to whoever has an elevator out of service.

Corporate site for a Venezuelan company that maintains and modernizes elevators and fabricates spare parts locally. Static, fast on poor networks, with a catalog, thermography diagnostics and a quoter that ends in WhatsApp.

Imported OEM part~3 months
Pulley, cover or bushing made in their workshop~3 weeks
Figures the client publishes on its site and flyers; the website turns them into the central message.
11real jobs documented with photos, 8 with their own page
232 KBof HTML for the home page, served from the CDN
4print pieces produced from the same HTML as the site
01 · How it works

From a stalled elevator to a WhatsApp message with context

Click through the steps or let them play. Each step lights up the piece it describes.

Search
"stalled elevator Caracas"
Page
prerendered HTML
Quoter
3 questions
Function
/api/cotizacion
Owner
email + WhatsApp
Step 1 of 5 · plays on its own
Someone searches for a problem, not a brand
A building manager with a stalled elevator does not search for "PAX Elevators": they search for the problem. Every service, every diagnostic and every catalog model has its own prerendered URL, with title, description and LocalBusiness structured data so the search engine can read it without executing anything.

The quoter, as it works on the site. Change the answers.

1 · Stops
2 · Equipment age
3 · Urgency
Recommended profile
Repair
current fault: diagnosis first, part fabricated if the OEM takes too long
Prefilled WhatsApp
"Hi PAX, I have an elevator with 5 – 9 stops, over 20 years old. I need: repair."
No price on screen. A replica of the site's logic with the same options; the message text is an example.
02 · Architecture

Everything static except one function

No CMS, no database, no server of its own. Content lives in a typed file, the build turns it into pages, and a single function receives forms.

Topology · Vercel
DNS
paxelevators.com
client domain at GoDaddy
primary apex
A and CNAME to Vercel · MX untouched at GoDaddy
Edge
CDN
about twenty static pages
x-vercel-cache: HIT
Nitro function
/api/cotizacion
the only dynamic piece
prerendered HTML · cache-control per route
Build
Nuxt 3 SSG
TypeScript · Tailwind · @nuxtjs/seo
sitemap · robots · schema.org
data/content.ts
jobs, services, models, sectors
1 file, no CMS
nuxt generate · crawlLinks discovers the routes
Media
Images
30 files · 2 MB
30 days
Videos
5 clips · 17.6 MB · poster
no autoplay
PDF catalogs
2 files · 51 MB
inline · 7 days
Output
Resend
email to the owner with the request
fetch, no SDK
WhatsApp
wa.me with the prefilled message
no backend
data/content.ts · one typed file
TRABAJOS8real jobs with photos, brand, sector and service
SERVICIOS4one page per service, with the jobs that prove it
DIAGNOSTICOS3thermography, insulation, instrumentation
MODELOS11catalog by family, each with its own page and a contact CTA
SECTORES3residential, commercial, corporate
TRUST_BRANDS6brands they service
Dynamic routes (/servicios/[slug], /catalogo/[slug]) are discovered at build time by following links and prerendered one by one.
Coverage by radius
The client does not publish an address. The map draws a 350 km circle around each base with Leaflet, loaded only on that page.
Structured data
LocalBusiness with two locations, phone and service area. Open Graph and Twitter cards with a static image.
From web to paper
Flyer, trifold, mini and premium are rendered with Chromium from HTML with the same fonts and palette.
03 · An end-to-end example

A Tuesday, a stalled Otis and a building manager

A made-up case, marked as an example, to see what each piece produces before getting into the challenges.

1
08:10 · mobile, 3G
She searches for the problem
Rosa manages a 9-storey building with an elevator out of service since Monday. She searches "Otis elevator repair Caracas" and lands on the repair service page. The HTML arrives whole; she does not wait for any application to load.
2
08:12
She sees jobs like hers
The page links the real jobs for that service: an Otis door operator motor in a residential tower, with photos. Not a promise: a job with a date and a brand.
3
08:14 · quoter
Three taps
5 to 9 stops, 10 to 20 years, urgent repair. The quoter proposes Repair and builds the message. Rosa taps WhatsApp: the chat opens with the text already written, she only adds the address.
4
08:15 · office
The owner gets context, not a "hello"
He knows how many stops, what age and what urgency before replying. Had Rosa used the form, the email arrives with the same fields in a table, and the date in Caracas time.
5
the following week
If the part does not exist, they make it
The diagnosis points to a pulley. Importing it takes months; the workshop makes it in weeks. That is the hero sentence, and that is why it is in the hero.
04 · The challenges

Five times the obvious solution was wrong

Every challenge follows the same pattern: what anyone would do first, why it does not work, what was done, and the number that proves it. The technical detail stays folded.

01

Content without a CMS

The obvious approach
WordPress, or a headless CMS with a panel so the client can edit.
Why it fails
The client has no technical team and does not want another account with another invoice. The site changes a few times a year and every change goes through whoever built it. A CMS here is an attack surface, plugin updates and a login nobody will use.
data/content.ts · one typed file
TRABAJOS8real jobs with photos, brand, sector and service
SERVICIOS4one page per service, with the jobs that prove it
DIAGNOSTICOS3thermography, insulation, instrumentation
MODELOS11catalog by family, each with its own page and a contact CTA
SECTORES3residential, commercial, corporate
TRUST_BRANDS6brands they service
Dynamic routes (/servicios/[slug], /catalogo/[slug]) are discovered at build time by following links and prerendered one by one.
What was done
All content lives in a typed TypeScript file: jobs, services, diagnostics, models, sectors, process, brands. Dynamic pages read from it and the build prerenders them by following links. Adding a model means adding an object; the type warns if a field is missing.
0databases, panels and third-party accounts for content. A new model is an object and a build.
Technical detail

The eight jobs with their own page cross-reference the services: getTrabajosByService makes each service page show the jobs that prove it, without duplicating content. Sectors and brands are lists in the same file.

The cost is that the client cannot edit on their own. For a business that changes its site twice a year that is fine; for one publishing weekly news, it is not.
02

A quoter that does not invent prices

The obvious approach
A calculator: enter stops and years, get an estimated price.
Why it fails
There were no real ranges validated with the client. An invented price on the website is the worst possible first contact: it either scares people off or commits to a number nobody gave.
1 · Stops
2 · Equipment age
3 · Urgency
Recommended profile
Repair
current fault: diagnosis first, part fabricated if the OEM takes too long
Prefilled WhatsApp
"Hi PAX, I have an elevator with 5 – 9 stops, over 20 years old. I need: repair."
No price on screen. A replica of the site's logic with the same options; the message text is an example.
What was done
Three questions anyone can answer without knowing elevators: stops, age, urgency. The output is not a number, it is a service profile and a message already written for WhatsApp or the form, with the catalog model if they came from there.
3 → 4three questions, four service profiles. Zero prices on screen.
Technical detail

Options carry a hint underneath ("Low-rise residential · duplex / villa") so people can place themselves without technical vocabulary. The recommendation maps to a service value the form already understands, so the owner receives the same field whichever way it comes in.

03

The form that used to be a PHP script

The obvious approach
A send-email.php on the shared hosting, which is what that hosting knows how to run.
Why it fails
The email provider credential lived inside the script, and once the site moved to static hosting that file gets served as plain text. There was no validation either: any bot could fill the owner's inbox.
What was done
A Nitro function in server/api/cotizacion.post.ts: it trims and cleans every field, requires name, phone and city, drops bots with a honeypot field only a bot fills in, and calls Resend with fetch. The credential is an environment variable; without it, the endpoint answers 503 with a useful message.
503"Sending unavailable. Use WhatsApp for now." A failure that says what to do, instead of a form that looks like it sent.
Technical detail

The email to the owner is a table with the fields, the free-text message in a separate block and the date in America/Caracas. Everything is escaped before it enters the HTML. The PHP copy stayed in legacy/ without the credential, for traceability.

04

Switching hosting without taking down the client's email

The obvious approach
Point the domain's nameservers at Vercel and done.
Why it fails
The client's email lives at GoDaddy and there is a second MX for transactional sends. Changing nameservers takes the MX records down with it. And GoDaddy had an active forwarding rule that blocked editing the A records: it would not save until the rule was removed.
ForwardingGoDaddy redirectremoved
A @76.76.21.21changed
CNAME wwwcname.vercel-dns.comchanged
MXsecureserver.net · client emailuntouched
MX sendAmazon SES · transactional sendsuntouched
What was done
Only two records: A on the apex and CNAME on www, in the client's account. The MX records were not touched. And the apex stayed as the primary domain, because the site's canonical and sitemap were already without www: Vercel shipped with the redirect the other way round.
2 / 0two records changed, zero MX touched. The previous Apache hosting stayed alive until confirming nothing depended on it.
Technical detail

The cache and redirect configuration that lived in netlify.toml moved to Nuxt routeRules, so it lives with the code and not with the provider. Nitro detects the Vercel preset on its own: prerendered pages plus one function for the form.

05

70 MB of media on a site built for 3G

The obvious approach
Upload the catalogs, the videos and the photos and let the browser cope.
Why it fails
The two PDF catalogs weigh 51 MB and the operation videos 17.6 MB. If that enters the home page flow, the site does not load on the phone of the person who needs it. And the coverage map drags in an entire library that only one page uses.
/_nuxt/** /_fonts/**1 year · immutablehashed names: they never change
/images/**30 days30 images · 2 MB
/catalogos/**7 days · inline2 PDFs · 51 MB, open in the browser
/videoson demand5 clips · 17.6 MB, with poster, no autoplay
What was done
Per-route cache rules in Nuxt itself: one year and immutable for everything hashed, 30 days for images, 7 days for catalogs with content-disposition: inline so they open in the browser. Videos carry a poster and do not autoplay. Leaflet is imported dynamically only on /cobertura.
2 MBof images for the whole site, optimized at 1200 and 1920 px. The remaining 68 MB only travel if someone asks for them.
Technical detail

Fonts (Space Grotesk and Inter) are downloaded at build time and served from the site's own domain with a one-year cache: no request to Google Fonts, no layout shift on load.

The catalogs are still factory PDFs. The inventory to turn them into per-family pages exists, and eleven models already have their page; the rest waits for the client to mark which families to show.
"The website exists to produce a WhatsApp message with context."
Everything else, catalog, thermography, videos, map, jobs with photos, is what convinces before the button is pressed. Every decision was measured against that: if it does not bring the visitor closer to that message, it is surplus.
05 · Decisions

One filter: does this add an invoice or a chore for the client?

If the answer was yes, another way was found. The client pays for the domain; everything else runs on free tiers.

WHY
Why Nuxt SSG and not WordPress?
Because the site has to be found and has to load on poor networks, and the client is not going to manage a panel. Prerendered HTML, zero plugins to update, zero database to back up. The same pattern as Venekambio, applied to a physical business.
Go deeper

The @nuxtjs/seo module generates the sitemap, robots and structured data from configuration. The Open Graph image generated at build time was disabled because of a module incompatibility; a static image is served instead, which is more predictable for WhatsApp.

WHY
Why is the central message fabrication and not years of experience?
Because it is what nobody else offers. In Venezuela, importing an OEM part is slow and expensive; fabricating pulleys, covers and bronze bushings in their own workshop solves the client's problem, not the company's ego. The years go in a paragraph; fabrication goes in the headline, the flyer and the trifold.
WHY
Why coverage by radius and not an address?
The client serves the whole country from two bases and does not publish a physical address. The map draws a 350 km circle around each base with Leaflet, which loads only on that page. The structured data declares two locations and the service area: Venezuela.
WHY
The print pieces come from the same HTML
Flyer, trifold, mini and premium were laid out in HTML with the site's fonts and palette and rendered with Chromium: PDF with 3 mm bleed for the print shop, PDF without bleed and PNG at 288 dpi for WhatsApp. One design system for web and paper.
Go deeper
Flyer
half letter · double-sided
"3 months vs 3 weeks" + QR
Trifold
landscape letter · C-fold
services, warranty, jobs, coverage
Mini
quarter letter · 4 per sheet
"Elevator out of service? We fix it."
Premium
half letter · 4 variants
the web hero, on paper

The trifold is C-folded into equal thirds; the preview PNGs show the fold lines and the PDFs do not. The mini has a four-per-letter-sheet version with cut lines for printing at the office.

The only dynamic piece, summarized.

// server/api/cotizacion.post.ts
if (clean(body.website)) return { ok: true } // honeypot: the bot thinks it sent
if (!nombre || !telefono || !ciudad) throw createError({ statusCode: 400 })
if (!resendApiKey) throw createError({ statusCode: 503, statusMessage: 'Usa WhatsApp por ahora.' })
06 · What I would do differently

Four decisions I would make the other way today

I did
Starting on the client's Apache hosting with a PHP script for the form
Today I would
A serverless function with the credential in an environment variable from day one. The PHP forced a migration and a key rotation.
I did
Shipping the factory catalogs as 51 MB PDFs
Today I would
Per-family pages from the start, with the PDF as a secondary download. Eleven models already have a page; the rest waits on the client.
I did
Coverage map left as a placeholder for months
Today I would
Leaflet with deferred loading from the first version. It was an afternoon of work and the page stayed half done for longer than reasonable.
I did
Deliveries as zip files (v1, v2, tar for Netlify)
Today I would
One repository and deployments from it. Versions in folders do not show what changed between one delivery and the next.
07 · If you take three things away

Recap

1
Truly static: the content is a typed file, the build turns it into pages and the CDN serves them. A single function for the form.
2
The site does not sell a company, it sells a solution: a part fabricated in weeks instead of months. That message is in the hero, in the quoter and in four print pieces from the same HTML.
3
The hosting migration touched two DNS records and no MX, and the form went from a PHP script with the key inside to a function with a honeypot and a 503 that says what to do.
Links