Skip to main content
ASoc
Comparison

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.

The ASoc Team10 min read

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

AxisHubSpotNext.js
What it isCRM first, CMS/landing pages secondA React framework; you build the site
Where a form submission landsA contact record, instantly, with lifecycle stageWherever you send it; nothing exists by default
Per-person attributionBuilt in — first touch, last touch, the whole timelineYou instrument events yourself; cookieless means aggregate only
Editing a pageIn-app editor, non-technical, live in secondsA commit and a deploy, or a CMS you add
Pages generated from your own catalogLimited; HubDB for simple tablesAny function you can write
Email and workflowsIncludedA provider plus code
HostingHubSpot's, included in the seat priceYours (static export, serverless, or Node)
Cost shapePer-seat subscription, rises with contactsEngineering time, then near-zero hosting
Lock-inContent and contacts live in HubSpotContent in git, contacts wherever you chose
Response headers, CSP, cachingPlatform-controlledYours 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 situationPick
Sales works inbound leads in a CRM dailyHubSpot
Marketing must ship and edit pages without engineeringHubSpot
You already pay for HubSpot CRM and need landing pagesHubSpot CMS
Pages are generated from your own product dataNext.js
The site needs logins, gated downloads, or a checkoutNext.js
You want content and page structure reviewed in pull requestsNext.js
Engineering-built site, HubSpot forms onlyNext.js + embeds, with the CSP work above

Mistakes and troubleshooting

SymptomCauseFix
HubSpot form renders nothing, console shows a CSP violationThe embed's script and iframe origins are not in your policyAdd them to script-src and frame-src, deliberately, with a comment saying why
HubSpot embed crashes the server renderThe snippet assumes a browser DOMLoad it from a client component after mount, not during SSR
Analytics shows blog conversions but not which postThe event has no per-post propertyCarry the article slug on the event, as blog_cta_clicked does
Migrated off HubSpot, lead volume dropsForms now have no deduplication or follow-upRebuild the sequence explicitly, or keep forms on HubSpot
Whole site 500s after a deploy, nothing in the diffRequest-path code depends on a missing env varGuard 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 propertyMeasure 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.

Keep reading

Comparison9 min read

Next.js vs. Nuxt: 24 of 92 Components Opt Into the Client

Not React versus Vue — the real split is where each framework lets you draw the server/client boundary. Next.js draws it through the import graph; Nuxt's default is universal.

Read more
Comparison11 min read

Next.js vs React + Vite for Admin Dashboards: How to Choose

Both ship excellent dashboards. The decision comes down to where your data lives, whether you need SEO, and who deploys it — not to raw performance.

Read more
Comparison9 min read

Next.js vs SvelteKit: Where a Write Is Allowed to Live

Bundle size is the wrong axis for a meta-framework choice. The real divide is routing defaults and whether a mutation can live anywhere or only on the route that owns it.

Read more