Next.js vs HubSpot: The Line Is the Lead Record, Not PageSpeed
HubSpot is a CRM with a CMS attached. What a Next.js storefront had to write instead: a Server Action, a CSP that blocks form embeds, and attribution with no person in it.
HubSpot is a CRM with a CMS attached; Next.js is a React framework with neither. The real question is not which renders faster — it is where the lead record lives. If every form submission must land on a contact timeline a sales team works, HubSpot wins. If the site's pages are derived from your own data, Next.js wins.
Most comparisons of these two argue about PageSpeed. That is the least interesting axis, because a competent build wins it on either platform and loses it on either platform a year later. This post comes from the Next.js side: a marketing storefront with 111 products, 364 blog posts and eight measured page types, where every piece of HubSpot's lead plumbing had to be written by hand. Here is what that actually cost.
The comparison that matters
| Axis | HubSpot | Next.js |
|---|---|---|
| What it is | CRM first, CMS/landing pages second | A React framework; you build the site |
| Where a form submission lands | A contact record, instantly, with lifecycle stage | Wherever you send it; nothing exists by default |
| Per-person attribution | Built in — first touch, last touch, the whole timeline | You instrument events yourself; cookieless means aggregate only |
| Editing a page | In-app editor, non-technical, live in seconds | A commit and a deploy, or a CMS you add |
| Pages generated from your own catalog | Limited; HubDB for simple tables | Any function you can write |
| Email and workflows | Included | A provider plus code |
| Hosting | HubSpot's, included in the seat price | Yours (static export, serverless, or Node) |
| Cost shape | Per-seat subscription, rises with contacts | Engineering time, then near-zero hosting |
| Lock-in | Content and contacts live in HubSpot | Content in git, contacts wherever you chose |
| Response headers, CSP, caching | Platform-controlled | Yours to set, and yours to get wrong |
Check HubSpot's current pricing and tier limits before deciding — they change per tier and per contact count, and a blog post is the wrong place to freeze them.
Where HubSpot genuinely wins
A form on a HubSpot page is a contact record. That one sentence is the entire product. The submission is deduplicated against an existing contact, stamped with the page it came from, attached to a lifecycle stage, and visible to whoever picks up the phone. Nothing in Next.js does any part of that.
Here is the equivalent on this site — the waitlist subscribe, which is as simple as lead capture gets:
// src/lib/actions/newsletter.ts — a Server Action, abridged
export async function subscribeToWaitlist(
_prev: FormState,
formData: FormData,
): Promise<FormState> {
// Honeypot: real users never fill this hidden field.
if (formData.get("company"))
return { ok: true, message: "You're on the list!" };
const email = String(formData.get("email") ?? "")
.trim()
.toLowerCase();
if (!EMAIL_RE.test(email)) {
return { ok: false, message: "Please enter a valid email address." };
}
const apiKey = process.env.RESEND_API_KEY;
const audienceId = process.env.RESEND_AUDIENCE_ID;
if (!apiKey || !audienceId) {
console.error("waitlist: RESEND_API_KEY / RESEND_AUDIENCE_ID not configured");
return { ok: false, message: "Something went wrong — please try again later." };
}
// ...POST https://api.resend.com/contacts, then three failure branches
}
That is one field, going to one audience, and it still needs an email regex, a honeypot, two missing-env guards, a non-2xx branch and a network-error branch. The matching client is src/components/molecules/NewsletterForm.tsx, where the honeypot input sits in a wrapper that is both display: none and aria-hidden, and carries tabIndex={-1} and autoComplete="off" itself — so a bot filling every field trips it, while a keyboard or screen-reader user never reaches it.
None of that produces a contact timeline. It produces a row in an email audience. Everything HubSpot would layer on top — deduplication, lifecycle stage, the owner assignment, the follow-up sequence — is still unwritten. If that layer is the point, buy it rather than building it.
The embed trap nobody mentions
The common advice is "keep HubSpot for forms and build the rest in Next.js." That works, but it collides with two things on a hardened Next.js site, and only one of them is the documented SSR problem.
The first is rendering: HubSpot's embed snippet expects a DOM to attach to, so the naive copy-paste into a React component fails during server rendering.
The second is the one that bites after you fix the first. This site sends a Content-Security-Policy on every route from next.config.ts:
// next.config.ts — the real policy, abridged to the two relevant directives
const contentSecurityPolicy = [
"default-src 'self'",
`script-src 'self' 'unsafe-inline'${scriptSrcEval} https://app.lemonsqueezy.com https://assets.lemonsqueezy.com https://va.vercel-scripts.com`,
"frame-src 'self' https://*.lemonsqueezy.com https://*.asoctemplates.com https://*.vercel.app",
// ...
].join("; ");
Every external origin in that file was added because a specific line of code loads it, and the file says so in a comment. A HubSpot form embed loads a script from a HubSpot origin and renders into an iframe from another — neither is on either list, so the embed is blocked twice over, silently, in the browser console. The fix is two more entries in the allowlist, which is fine. The point is that a hosted form on a self-hosted site is a security-policy decision, not a paste, and the comparison posts that recommend the hybrid never mention it. If you take the hybrid route, see Next.js Content Security Policy for how this policy is structured and what 'unsafe-inline' is doing in it.
Where Next.js wins: pages derived from your own data
HubSpot's model assumes a human authors each page. This storefront's pages mostly have no author. One typed catalog entry generates a product page, its Open Graph image, its JSON-LD, its sitemap row, its cards across the site, and its membership in the category hubs. 111 products do not mean 111 authored pages; they mean one renderer and one data file.
The blog works the same way: posts are MDX in the repository, and a post's product references are catalog slugs a test resolves, so an article cannot outlive the product it recommends. HubSpot's blog is a capable publishing tool and this is not a knock on it — but if the publication itself is the product, the comparison you actually want is Next.js vs Ghost, not this one.
The same applies to the category hub pages. Their backing products are derived — a category plus a ready edition of the right framework — rather than hand-listed, because the hand-listed version silently stopped covering new products and left 24 of them on no hub at all. That is a bug class HubDB can reproduce and a page editor cannot.
The attribution you give up, measured
This is the honest cost of leaving HubSpot, and it is not performance. The site runs cookieless analytics with a deliberately tiny event union:
// src/lib/analytics.ts
type ConversionEvent =
| "preview_opened"
| "buy_clicked"
| "waitlist_joined"
| "blog_cta_clicked"
| "wishlist_added";
blog_cta_clicked carries { post, target } — the article slug and the catalog slug — which is what makes the content programme attributable per post rather than per session. It is genuinely useful: it answers "which article sells which template".
What it cannot answer is "who". There is no person, no company, no timeline, and by design no cookie, so a visitor who reads three posts across two weeks and then buys is three anonymous sessions and one order. HubSpot would have shown you one contact and the path they took. If your sales motion needs the path, that is a reason to stay, and no amount of event instrumentation substitutes for it.
What the measurements actually show
Desktop Lighthouse on this site is 100 for performance, accessibility and SEO across all eight main page types; mobile is 89–99 for performance with accessibility and SEO at 100 (docs/superpowers/LIGHTHOUSE.md). Best practices reads 96 locally for one dull reason: the analytics beacon 404s when the build runs off its hosting platform.
Two caveats worth more than the scores. First, these are local lab runs, not field data. Second, and more importantly: this number is a property of the build, not of Next.js. A badly built Next.js marketing site scores worse than a default HubSpot page, and the agency claims of a fixed 15-to-30-point advantage are not something anyone can honour in advance. Treat performance as a tiebreak, not a reason.
What you own that HubSpot would have owned
Not everything you take back is a win. This repository's request path runs src/proxy.ts on every request to refresh the auth session, and it fails site-wide if two environment variables are missing from the hosting project. That is a whole-site outage from a config omission — a failure mode a hosted CMS structurally cannot have, because nobody hands you the request path. Response headers, the CSP above, redirects, cache behaviour and the request middleware are all yours now, which means they are all yours to get wrong at 2am.
Which one to pick
| Your situation | Pick |
|---|---|
| Sales works inbound leads in a CRM daily | HubSpot |
| Marketing must ship and edit pages without engineering | HubSpot |
| You already pay for HubSpot CRM and need landing pages | HubSpot CMS |
| Pages are generated from your own product data | Next.js |
| The site needs logins, gated downloads, or a checkout | Next.js |
| You want content and page structure reviewed in pull requests | Next.js |
| Engineering-built site, HubSpot forms only | Next.js + embeds, with the CSP work above |
Mistakes and troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| HubSpot form renders nothing, console shows a CSP violation | The embed's script and iframe origins are not in your policy | Add them to script-src and frame-src, deliberately, with a comment saying why |
| HubSpot embed crashes the server render | The snippet assumes a browser DOM | Load it from a client component after mount, not during SSR |
| Analytics shows blog conversions but not which post | The event has no per-post property | Carry the article slug on the event, as blog_cta_clicked does |
| Migrated off HubSpot, lead volume drops | Forms now have no deduplication or follow-up | Rebuild the sequence explicitly, or keep forms on HubSpot |
| Whole site 500s after a deploy, nothing in the diff | Request-path code depends on a missing env var | Guard the proxy path and fail open, not closed |
| "Our Next.js rebuild was supposed to be faster" | Scores were a build property, not a platform property | Measure your own build; do not inherit a vendor benchmark |
FAQ
Is Next.js a replacement for HubSpot? No. Next.js replaces HubSpot's CMS at most. It does not replace the CRM, the contact timeline, the workflows or the email sending, and those are what HubSpot is actually sold for.
Can I use HubSpot forms on a Next.js site? Yes, and it is a reasonable hybrid. Load the embed from a client component rather than during server rendering, and add its origins to your Content-Security-Policy — on a site with a real CSP it is blocked by default.
Will moving from HubSpot CMS to Next.js improve SEO? Not by itself. It gives you control of the sitemap, canonicals, JSON-LD and internal linking as code you can test, which is worth having. Rankings change because the pages got better, not because the renderer changed.
Which is cheaper? HubSpot costs a predictable subscription that rises with seats and contacts. Next.js costs engineering time up front and very little to host. Below roughly one engineer's attention, HubSpot is usually cheaper in total.
Templates in this post
If the Next.js route is the one, a template removes the boilerplate — layout, metadata, sitemap, dark mode — so the work left is the part HubSpot could not do anyway. ASoc Beacon, ASoc Beaker and ASoc Blueprint are starting points in code you own.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For the adjacent decisions, see Next.js vs Framer, WordPress vs Next.js and website builder vs developer.
