- 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:
- Should the app live at
multibodysystemsdynamics.com/app/or atapp.multibodysystemsdynamics.com? - 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 gets | App | Tool page on the main site |
|---|---|---|
| Text in the HTML | A title and a loading shell | Hundreds of words: equations, explanations, a worked example |
| Time to useful content | ~7–8 s of Pyodide boot, ~30 MB | Immediate, static HTML |
| Distinct URLs per topic | One page with query parameters | One 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:
- 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).
- An instant preview: about 50 lines of closed-form JavaScript and an SVG. It reacts immediately and needs no Pyodide.
- 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:
| Concern | app. subdomain | /app/ subfolder |
|---|---|---|
| Never load Pyodide on marketing pages | Natural: different origin | Possible, but one mistake in a shared layout ships 30 MB |
| Service worker scope | The whole origin is the app; caching rules are simple | Must be scoped to /app/, and the site’s own caching must stay out of its way |
| Caching headers | Immutable vendor paths without touching the site’s rules | One _headers file serving two very different products |
| Deploy cadence | App deploys on library releases, site deploys on content | Every deploy ships both |
| Tech stack | Vite + Pyodide | Astro |
| Rollback | Per product | Together |
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, androbots.txtallows 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.