Skip to content
SoyGarfield
Research #05

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
Two-stage diagram. Built: a React and Canvas customizer that measures text and flags printable-area overflow, now orphaned code. Today, in production: an iframe to an external legacy PHP tool.
From a custom customizer to an embedded external tool. A deliberate simplification, documented in the git history itself. Original diagram.

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);
}
Real code (still in the repository, though no longer used) from 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);
Real code recovered from git history (a commit before d1b954f), from the customizer's validation logic.

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);
});
Real code from 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');
}
Real code from 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).

Real metrics measured on October 18, 2026.
MetricResultEnvironment / evidence
Lighthouse Performance (mobile, lab)79 / 100Lighthouse 13.5.0, simulated throttling
LCP (lab)5.1 sSame Lighthouse run, not field (CrUX) data
Local build time8.16 svite build + SSR + prerender script
Export size10 MB across 171 files/dist folder generated by that build
Prerendered routes22 of 22Prerender script log (every landing page and the blog)
Development commits26, from July to September 2026The repository's git log

Simplifying custom code that already works is as valid a decision as building it, as long as it stays documented.

Pietro Fiorillo

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.