We reviewed the source code your developer shared, along with the live site. Installing GTM and the domain verification is a ten minute job, and he is right that this is a custom-coded site rather than a website builder. But we found something that changes the order of the work: the contact form never changes the URL. Set up the standard way, by destination page, the account would record zero leads while appearing fully installed. Separately, the site takes 8.2 seconds to render on mobile. All three items are already fixed, compiled and verified in a real browser.
The site is a single landing page built with Next.js 16 and React 19, bilingual English
and Portuguese through a language toggle, hosted on Namecheap with LiteSpeed. The contact form
sends email through Resend. The actual application code is eight files: the rest of the 377 MB
archive was node_modules, .next and the .git folder.
| Measurement layer | Status | Consequence |
|---|---|---|
| Google Tag Manager | Not installed | No event leaves the site |
| Google Analytics 4 | Not installed | No behavioural baseline |
| Meta Pixel | Not installed | Campaigns optimise without signal |
| Meta domain verification | Not installed | No control over the domain's ad rules |
| Thank-you page | Does not exist | Covered in section 02 |
| Meta description | Missing | Google writes the search snippet on its own |
The account starts from zero. That has an upside: the structure can be built correctly from day one, with no misconfiguration inherited from anyone.
Without this resolved first, installing GTM produces the worst possible outcome: everything looks configured, the tag shows as active, and the report still shows zero leads.
The form uses a Next.js Server Action. It sends the data in the background, receives
confirmation and only swaps the button text to a success state. No reload, no
/thank-you, the URL stays identical.
A conversion configured by destination URL, which is the default approach, never fires. Paid campaigns run without a conversion signal, and the algorithm optimises blind, spending to reach people who are never recognised as leads.
Until a real conversion event exists, no channel can be compared to another and no budget decision is based on evidence. This is why it had to be fixed before any tag went live, rather than after.
The event now fires at the exact point where the server confirms the submission, inside
handleSubmit, instead of relying on a URL change.
setBtnStatus(btnStatuses.success);
// Conversion: fires only after the server action confirms the email was sent.
track('generate_lead', {
form_id: 'contact_main',
form_language: lang,
user_data: { email: data.email, phone: data.phone },
});
The user_data object is included deliberately. It is what allows Enhanced
Conversions in Google and Advanced Matching in Meta to be switched on later, without
needing a second round of development work.
The same gap applied to the quote buttons. All six "request a quote" CTAs only scroll the page
down to the form, so none of them produced any signal either. They now fire
quote_button_click, carrying the button's own text and its position on the page,
which makes it possible to see which CTA actually brings the lead.
The WhatsApp link in the footer now fires its own event (whatsapp_click), since
it is the third contact path on the site.
Measured with Google PageSpeed Insights on 7 August, mobile profile, slow 4G network.
| Metric | Value | Reading |
|---|---|---|
| Largest Contentful Paint | 8.2 s | More than three times the 2.5 s threshold |
| Speed Index | 8.7 s | Content takes too long to appear |
| First Contentful Paint | 2.6 s | Acceptable |
| Total Blocking Time | 0 ms | No JavaScript blocking |
| Cumulative Layout Shift | 0 | Stable layout |
JavaScript is not the problem: blocking time is zero. The weight is in the images.
On first load the site downloads six images in parallel, roughly 2 MB in total, all at the highest priority. Five of them sit below the fold and a visitor would only reach them after scrolling. On a slow 4G connection, which is the test scenario, that alone accounts for the 8.2 seconds. On top of that, the project photos are stored as PNG, a format designed for graphics rather than photography, and three of them exceed 400 KB each.
| Initial mobile page load | Before | After |
|---|---|---|
| Images downloaded | 1,962 KB | 126 KB |
| Requests | 11 | 6 |
| Images at highest priority | 6 | 1 |
| Total weight of image files | 3,488 KB | 1,097 KB |
Files under /public are served with no Cache-Control header, so
returning visitors re-download every image on every visit. The Next.js build files are
already correct, only the public folder was left out. This is three lines in the LiteSpeed
.htaccess, and the exact snippet is included in the package. Since the hosting
runs cPanel, it can be edited directly in the panel's File Manager.
PageSpeed also flags a few accessibility items that were left untouched because they affect the visual design and are your call: buttons without an accessible name, insufficient colour contrast in some text, and heading levels out of sequence. They are recorded here for a second round.
The archive contained my-app/.env.local with an active
RESEND_API_KEY, along with the .git folder and
node_modules. That key can send email on behalf of
info@vironportugal.com, which means anyone holding it can send mail that
appears to come from your domain.
The project's own .gitignore already excludes .env files, so this
was not intended to leave the development machine. The key should be rotated in the
Resend dashboard. Nothing else needs to change: your sending setup itself is correct,
with SPF, DKIM and DMARC properly configured, so email from the form is being delivered.
The node_modules and .git folders are also why the archive arrived at
377 MB for a project whose real code fits in eight files.
Rather than returning the full 377 MB, the package contains only what changed: three replaced files, ten new images, a line-by-line diff so he can review before applying, and a README in English.
app/layout.tsx, app/page.tsx,
app/Components/Gallery.tsx and the ten .webp files into
public/.
npm run build, then deploy exactly as usual. No new
dependency was added: GTM is loaded through next/script, which already
ships with Next.js, so there is no need to run npm install.
Three lines in .htaccess. The exact snippet is in the
README and can be applied through cPanel.
The code was compiled from your own source and opened in a real headless browser at a mobile viewport. Each item below was confirmed on screen, not assumed.
gtm.js, gtm.dom and gtm.loadgenerate_lead event fires on form success and GTM processes itquote_button_click event fires on all five rendered quote CTAs, each with its own distinct location labelwhatsapp_click event fires on clicknpm run build passes, including the TypeScript checkThe form test was run with email sending disabled, so that no false enquiry would land in your inbox. The file was then restored to its original state and checked byte for byte.
Once the site is published, the remaining work is on our side and no longer depends on your developer.
generate_lead as the conversionuser_data already carried by the eventMeta accepts a DNS TXT record as an alternative to the meta tag. Your domain, DNS and hosting are all with Namecheap, so the record can be added under Domain List, Manage, Advanced DNS. It takes a couple of minutes and requires no deploy. The meta tag is included in the package either way: both routes work and do not conflict.
The package is ready, tested and verified against your own source. Once it is deployed, Viron's measurement setup starts correct from the very first euro invested, and every campaign decision after that is based on a number you can trust.
Message Juliana →