• Aug 2, 2026
  • 6 min read

app.domain or domain/app? Where to Put a Lead-Magnet PWA (and What It Means for SEO)

I built a free browser app that runs a Python mechanism solver (how it’s built). It is a lead magnet: the reason an engineer would remember the brand and, one day, ask for consulting. Before launching it I had to answer two questions that look like one:

  1. Should the app live at multibodysystemsdynamics.com/app/ or at app.multibodysystemsdynamics.com?
  2. Will it bring search traffic?

The second answer decides the first. The app will not rank, so the subdomain costs nothing and buys a lot of separation.

Why the app can’t rank

Look at the app the way a crawler does:

What a crawler getsAppTool page on the main site
Text in the HTMLA title and a loading shellHundreds of words: equations, explanations, a worked example
Time to useful content~7–8 s of Pyodide boot, ~30 MBImmediate, static HTML
Distinct URLs per topicOne page with query parametersOne URL per mechanism
What it answers”Let me play with a solver""How do I compute slider-crank kinematics?”

A JavaScript canvas with a 30 MB runtime has no text to index. Even crawlers that execute JavaScript are not going to wait for SciPy to load in order to find a slider. And the old version of the marketing site had the same problem in a milder form: all content was injected with innerHTML, so the built index.html was an empty <div id="app">. Bing, social previews and LLM crawlers saw nothing.

So the rule became: content ranks, the app converts.

The funnel I settled on

search → tool page (static, ranks) → instant preview → "Open in full solver" → app → enquiry

Each tool page on the main site, like a slider-crank kinematics page or a four-bar linkage calculator, is static Astro HTML with three layers:

  1. Long-form content: the equations, the Grashof condition, a runnable code sample (every Python block on the site is executed against the real library in CI, because the old site published one that called a method that doesn’t exist).
  2. An instant preview: about 50 lines of closed-form JavaScript and an SVG. It reacts immediately and needs no Pyodide.
  3. A deep link into the full solver with the current parameters:
https://app.multibodysystemsdynamics.com/?demo=four-bar&a=1&b=3&c=2.5&d=3.2&utm_source=site

The app reads ?demo= and the parameter keys (which are the same keys as the controls and the Python function arguments), clamps them to the control ranges, and keeps the URL updated while you edit. “Copy link” therefore shares the exact setup, which is also how people send a mechanism to a colleague.

The heavy runtime loads only when someone has shown intent by clicking.

Subdomain vs subfolder

The usual SEO advice is that subfolders consolidate ranking signals better than subdomains. That’s true, and it doesn’t matter here, because the app is not supposed to rank. What does matter:

Concernapp. subdomain/app/ subfolder
Never load Pyodide on marketing pagesNatural: different originPossible, but one mistake in a shared layout ships 30 MB
Service worker scopeThe whole origin is the app; caching rules are simpleMust be scoped to /app/, and the site’s own caching must stay out of its way
Caching headersImmutable vendor paths without touching the site’s rulesOne _headers file serving two very different products
Deploy cadenceApp deploys on library releases, site deploys on contentEvery deploy ships both
Tech stackVite + PyodideAstro
RollbackPer productTogether

On Cloudflare Pages each product is its own project: the site and the app deploy independently, and each can be rolled back with one click without touching the other.

The speed win is simple to state: marketing pages never pay for the runtime. The site is about 25 static pages, roughly 456 KB in total with about 11 KB of JavaScript. The app is 30 MB on first load and proud of it.

What the app does for SEO anyway

Not ranking is not the same as being invisible:

  • The app home has a proper <title>, description and a canonical URL, and robots.txt allows it. If someone searches for the brand plus “solver”, it can appear.
  • It earns links. Engineers share solver setups; every shared deep link points at the brand.
  • It feeds the site’s content. Every new solver template gets its own tool page with a preview and a deep link, which is new indexable content.

Lead-magnet rules I’m keeping

  • No signup for the “wow” moment. Solving a mechanism must work without an email address. A gate before value kills the funnel.
  • Gate the thing worth paying attention to: a downloadable validation report, saved projects, larger exports. Those are worth an email.
  • The code is visible in the browser anyway. The moat is templates, reports and the brand, not hiding JavaScript.
  • Put a consulting call to action where the app fails: solver errors and unsupported models are exactly when someone needs a human.
  • Don’t make “Install” a call to action. The PWA’s value is the cached runtime on repeat visits, not the home-screen icon.

Migrating without losing what already ranked

The site moved from a single-page app to Astro at the same time. Two rules kept it safe: every one of the 17 old URLs exists in the new site (no redirects, no lost history), and I ported the writing rather than the positioning. The old copy, about 8,800 words, was the real asset.

Takeaways

  • Decide whether something should rank before deciding where it lives. A heavy app should not; content should.
  • Static pages with an instant preview give search engines text and give visitors immediate interaction.
  • Deep links carry the context from content into the app, and make every shared setup a backlink.
  • A subdomain keeps the runtime, the cache and the deploys of the app separate from the site. The ranking cost of subdomains doesn’t apply when the app isn’t meant to rank.

Once visitors arrive, some of them write in. Next: how those messages are stored and how I get alerted, in contact forms with Cloudflare Workers KV and self-hosted ntfy alerts.