+1 (478) 654-9062

Search Visibility for a Meteor App: Rendering, Meta Tags, and the Crawler Problem

A question that shows up a year or two after a Meteor app launches: marketing pages stopped ranking, or a shared link shows a blank preview. The cause is usually structural. A classic Meteor client app serves one HTML document with a script bundle, and everything the user sees is built in the browser after the bundle loads and DDP connects. That is fine for a logged-in tool. It is a problem for any page that needs to be found.

This note is about measuring the problem first, then picking the smallest fix.

Step 1: See what a crawler sees

Do not guess. Fetch the page without running JavaScript:

curl -s https://app.example.com/pricing | head -n 60

If the <title> is generic, the <body> holds only a loading indicator, and the text content is absent, then any consumer that does not execute JavaScript sees nothing. Search engines do render JavaScript for many pages, but rendering is a deferred second pass and is not guaranteed. Link-preview scrapers (chat apps, social networks) generally do not run your scripts at all, so they rely on Open Graph tags in the initial HTML.

Write down three things per public route: the title in raw HTML, the meta description in raw HTML, and whether the main content text appears in raw HTML. That table is your scope.

Step 2: Separate the pages that matter from the app

Most Meteor apps have a small number of public routes (home, pricing, docs, public profiles, shared items) and a large authenticated surface that search engines never need. Do not solve rendering for the whole app. Solve it for the handful of routes in your table.

Also check the basics that are independent of rendering: a real robots.txt, an XML sitemap, canonical URLs, and correct HTTP status codes. Client-routed apps often return 200 for every unknown path, which search engines read as soft-404 pages. A server-side route that returns a real 404 for unknown paths is a cheap fix.

Step 3: Pick the smallest fix that covers the table

Option A: inject head tags on the server. If the problem is only titles and Open Graph tags, you can modify the initial HTML in a WebApp connect handler or use a head-injection package, filling in tags from the database per route. Body content is still client-rendered, but link previews work and titles are correct. Before adopting any package, check its last release and whether it supports your Meteor version; this is the same abandoned-package question as everywhere else in the app.

Option B: prerendering for crawlers. Detect crawler user agents at the proxy or in a WebApp handler and serve a pre-rendered HTML snapshot of the public routes. It leaves the app untouched, but you now own a snapshot cache and a user-agent list, and serving different content to crawlers than to users needs to stay faithful to what users see.

Option C: server-side rendering. Meteor has server-rendering support for React (the server-render package), and community SSR packages exist for Blaze with varying maintenance. SSR gives the most complete result and the most work: components must not touch window at import time, data must be available on the server before render, and the client has to hydrate to the same markup. On a Blaze app this is rarely the economical choice.

Option D: move the public pages out. Serve marketing and content pages from a separate static site or a small server-rendered service on the same domain, and leave the Meteor app for the logged-in product. This is often the best answer, and it matches the strangler pattern used in a migration off Meteor: the public routes are the first and lowest-risk slice to move, because they are read-mostly and carry no latency compensation or subscription logic.

What to avoid

  • Rendering the whole app server-side to fix five pages.
  • Adding a prerender layer and never verifying it again. Re-run the Step 1 table after every deploy that touches routing.
  • Treating this as an SEO-tooling project. It is a routing and HTML-delivery problem, and the evidence is in the raw response.

How we would decide

If the public surface is small and changes rarely, Option A plus a sitemap is usually enough, and the app can stay as it is. If the public surface is large or is the product itself (user-generated pages, a catalog), Option C or D is worth the effort, and the choice should follow the larger keep, upgrade, or migrate decision rather than precede it. Doing SSR work on a Blaze app you plan to leave within a year is money spent on code you will delete.

In every case, measure before and after with the same curl check, and let the raw HTML decide whether the fix worked.