How I built a live customizer and then simplified it into an iframe: the Ciudad del Láser case
When does it make sense to simplify an already-built custom feature instead of continuing to maintain it?
By Pietro Fiorillo ·
In this article
The problem: quoting with no counter, for a shop that lives on WhatsApp
Ciudad del Láser has no counter service: every quote goes through WhatsApp or email. The previous site didn't explain how to prepare a cutting file, and the client couldn't update content without depending on a developer.
What I built first: a live customizer with real validation
For custom mate cups and plaques, I built a customizer in React that used the browser's Canvas to measure the text the client typed, instead of depending on an external library.
let measureCanvas;
function measureTextWidth(text, family, sizePx) {
if (typeof document === 'undefined') return 0;
if (!measureCanvas) measureCanvas = document.createElement('canvas');
const ctx = measureCanvas.getContext('2d');
ctx.font = `${sizePx}px ${family}`;
return (text || '').split('\n')
.reduce((max, line) => Math.max(max, ctx.measureText(line).width), 0);
}src/hooks/useTextMeasurement.js.With that measurement, the customizer compared the text against a fixed printable area and flagged when the design went past the margins, on top of detecting characters unsafe for laser engraving.
const { width: textWidth, lineCount } = useTextMeasurement(
d.text, d.font.family, d.fontSize
);
const textHeight = lineCount * d.fontSize * 1.2;
const overflowsPrintableArea =
textWidth > PRINTABLE_BOX.width || textHeight > PRINTABLE_BOX.height;
const unsafeChars = findUnsafeCharacters(d.text);What's in production today: an iframe to a tool that already existed
At some point, that custom customizer was replaced with an <iframe> embedding an already-existing external PHP tool (ember.com.ar/virolas). The change is documented in commit d1b954f, whose message literally says it simplifies the Personalizador by removing the mate and plaque customizers.
I won't invent a reason that isn't written in the commit. What I can say is that, in engineering terms, it's a reasonable call: an external tool that already solved the problem avoids maintaining an equivalent piece of custom software, at the cost of fine control over the experience.
The 5 mm margin rule and the line colors (cutting, marking, engraving) still exist, but as content in a «How to prepare your file» guide, not as validation running in the client's browser.
How the sitemap stays current without depending on a build
Content (services, blog) lives in Sanity. Instead of regenerating the sitemap only on each build, a Cloud Function assembles it on the fly, querying Sanity with GROQ every time someone requests it.
export const sitemap = onRequest({ cors: false }, async (req, res) => {
const [services, posts] = await Promise.all([
sanityClient.fetch(`*[_type == "service"]{ "slug": slug.current }`),
sanityClient.fetch(`*[_type == "blogPost"]{ "slug": slug.current }`),
]);
const entries = [
...STATIC_URLS.map((s) => urlEntry(s.loc, s.priority)),
...services.map((s) => urlEntry(`/${s.slug}`, '0.9')),
...posts.map((p) => urlEntry(`/blog/${p.slug}`, '0.5')),
];
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${entries.join('\n')}
</urlset>`;
res.set('Content-Type', 'application/xml');
res.set('Cache-Control', 'public, max-age=300, s-maxage=3600');
res.status(200).send(xml);
});functions/index.js.This means that if a new service is published in Sanity, it shows up in the sitemap without waiting for the site's next build, even though the HTML pages still need that build to exist.
How the form builds the WhatsApp message
function handleSubmit(e) {
e.preventDefault();
const message = [
`Hola! Quiero cotizar: ${serviceName}.`,
`Nombre: ${form.nombre}`,
form.telefono && `Teléfono: ${form.telefono}`,
form.detalle && `Detalle: ${form.detalle}`,
].filter(Boolean).join('\n');
window.open(buildWhatsappLink(message), '_blank', 'noreferrer');
}src/components/QuoteForm.jsx.How I validated this architecture
I ran the repository's real build process (vite build plus the SSR step and the prerender script) with no secret environment variables, since Sanity is consumed in public mode (useCdn: true).
| Metric | Result | Environment / evidence |
|---|---|---|
| Lighthouse Performance (mobile, lab) | 79 / 100 | Lighthouse 13.5.0, simulated throttling |
| LCP (lab) | 5.1 s | Same Lighthouse run, not field (CrUX) data |
| Local build time | 8.16 s | vite build + SSR + prerender script |
| Export size | 10 MB across 171 files | /dist folder generated by that build |
| Prerendered routes | 22 of 22 | Prerender script log (every landing page and the blog) |
| Development commits | 26, from July to September 2026 | The repository's git log |
Simplifying custom code that already works is as valid a decision as building it, as long as it stays documented.
My part in this project
My part in Ciudad del Láser included frontend development, integrating Sanity as the CMS, the sitemap Cloud Function and technical SEO, as part of my work at MANYA Digital.
What I'd do differently: orphaned code with no explicit decision
The original customizer wasn't deleted, it was just stopped being used. Today it lives in the repository alongside the rest of the code, with no note saying whether it's a future reference or just something nobody got around to cleaning up. That ambiguity is the real problem, not the code itself.
What I'd test next
I'd explicitly decide the fate of the customizer's orphaned code: document it as a reference for a future custom version, or remove it outright. I'd also evaluate whether it's worth bringing the live customizer back now that the tested validation logic already exists, instead of depending on an external tool outside my control.
Sources
Frequently asked questions
Why remove the custom customizer if it was already built?
I don't know for certain, since I wasn't part of that specific decision as documented in the commit; what the git history shows is that it was simplified to an already-existing external iframe. It's a reasonable maintenance-cost call: a tool that already worked, even from another provider, solved the same problem with less custom code to maintain.
Does the React and Canvas customizer code still exist?
Yes, several components (DesignPreview, TextControls, LogoControls, PreviewToolbar) are still in the repository, but no file imports them. It's orphaned code: it doesn't break anything, but it doesn't do anything either.
Does the 5 mm margin rule still apply today?
It still exists, but as written guidance for the client to prepare their file, not as automatic validation in a live customizer. The shift from a software-enforced control to an instruction-based one is real, and worth being precise about.
How does the site generate the sitemap if the content lives in Sanity?
A Cloud Function queries Sanity in real time (services and blog posts) every time the sitemap is requested, and assembles it by combining those routes with the site's fixed ones. It doesn't depend on anyone running a build for the sitemap to stay current.
Why React and Vite instead of Next.js, like other projects?
Because this site didn't need hybrid rendering or server routes: an SPA with its own prerender step (a script that generates HTML per route after the build) covers the use case with a lighter base.
How much does it cost to maintain a site like this?
Hosting cost is low because the final site is static. The real cost is keeping the content in Sanity current (services, blog, testimonials) and the external customization tool staying up, since it isn't under the project's direct control.