Skip to main content

Your JavaScript Page Loads. What Did the Server Actually Send?

Updated 8 min readMarkdown
Part of seriesHTML Delivery

A practical way to debug missing content, misleading status codes, and indexing problems in Node.js applications.

Open a documentation page in your browser. The heading appears, the navigation works, and the code examples look right.

Now request the same URL from a terminal:

curl -sS https://docs.example.com/docs/installation

The response contains a root element, a loading message, and a JavaScript bundle. The installation instructions are missing.

That difference is where the investigation starts.

It does not prove that a search engine cannot index the page. It does show that reading the instructions requires more than fetching its HTML.

The browser has done work you haven’t inspected

Consider this simplified React component:

import { useEffect, useState } from 'react';

export default function InstallationPage() {
  const [doc, setDoc] = useState(null);
  const [error, setError] = useState(false);

  useEffect(() => {
    let cancelled = false;

    async function load() {
      try {
        const response = await fetch('/api/docs/installation');

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }

        const data = await response.json();

        if (!cancelled) setDoc(data);
      } catch {
        if (!cancelled) setError(true);
      }
    }

    load();
    return () => { cancelled = true; };
  }, []);

  if (error) return <p>Could not load this document.</p>;
  if (!doc) return <p>Loading documentation…</p>;

  return (
    <article>
      <h1>{doc.title}</h1>
      <p>{doc.text}</p>
    </article>
  );
}

The component can display a perfectly readable page. Its main content still depends on a successful browser-side request.

That request might work because you are logged in. It might fail without a cookie, behind a firewall rule, or after an API deployment changes its response. A screenshot cannot tell you which conditions were necessary.

Google documents separate crawling, rendering, and indexing stages. Google Search can execute JavaScript, but content in an application shell must first become available during rendering. Google also notes that other bots may not execute JavaScript. See its JavaScript documentation.

For this example, the useful question is narrower than “Can Google read React?”

Can the main document be obtained reliably from its public URL?

Compare three views of the same page

Start with the network response:

curl -sS \
  -D response-headers.txt \
  -o response-body.html \
  https://docs.example.com/docs/installation

This makes a GET request and saves its headers separately from its body. Inspect both files.

Then open the page in a browser and inspect the live DOM. The Elements panel shows what exists after scripts have had an opportunity to change the document.

Finally, disable JavaScript in the browser’s developer tools and reload the URL directly. Do not navigate there through an already-running application.

These checks answer different questions:

Check What it reveals
Raw response What this HTTP request received
Live DOM What this browser produced after execution
Reload without JavaScript Whether the document depends on browser scripts for its main content

Disabling JavaScript is a dependency check. It does not reproduce Google’s renderer.

Look for one specific sentence from the article in each view. Searching for the main heading alone can miss a page that delivers its title but fetches everything useful later.

Fix the content boundary

For a small documentation site, I would put the article in the HTML response and add interactive features afterward.

That does not require abandoning JavaScript. Node.js can send an empty application shell or a complete document. The runtime is not the deciding factor.

Here is a complete example using Node.js 24’s built-in HTTP server. There are no package dependencies.

Save this as server.mjs:

import { createServer } from 'node:http';

const docs = new Map([
  [
    'installation',
    {
      title: 'Installation',
      text: 'Install the package with npm install example-client.'
    }
  ]
]);

const escapeHTML = value =>
  String(value).replace(/[&<>"']/g, char => ({
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;'
  }[char]));

function handler(req, res) {
  const path = new URL(req.url, 'http://localhost').pathname;
  const slug = path.startsWith('/docs/') ? path.slice(6) : '';
  const doc = docs.get(slug);

  res.setHeader('Content-Type', 'text/html; charset=utf-8');

  if (!doc) {
    res.writeHead(404);
    res.end(
      '<!doctype html><title>Not found</title><h1>Not found</h1>'
    );
    return;
  }

  const canonical = new URL(
    '/docs/' + slug,
    'https://docs.example.com'
  ).href;

  res.writeHead(200);
  res.end(`<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>${escapeHTML(doc.title)}</title>
  <link rel="canonical" href="${escapeHTML(canonical)}">
</head>
<body>
  <nav>
    <a href="/docs/installation">Installation</a>
  </nav>
  <main>
    <article>
      <h1>${escapeHTML(doc.title)}</h1>
      <p>${escapeHTML(doc.text)}</p>
    </article>
  </main>
</body>
</html>`);
}

createServer(handler).listen(3000, '127.0.0.1', () => {
  console.log('Open http://127.0.0.1:3000/docs/installation');
});

Run it:

node server.mjs

Then inspect the document and a missing route:

curl -i http://127.0.0.1:3000/docs/installation
curl -i http://127.0.0.1:3000/docs/missing

The first response contains the article. The second returns a 404.

Replace the example canonical origin before using this code on a real site. The fixed origin is deliberate: the canonical URL should come from trusted site configuration.

This example supports exact paths without trailing slashes. A production application should also define its preferred trailing-slash behavior and redirect the alternative consistently.

The code escapes plain text before inserting it into HTML. It is not a Markdown renderer or an HTML sanitizer. Those are separate requirements if your content includes rich markup.

The relevant server APIs are documented in the Node.js 24 HTTP reference.

Apply the same decision inside your framework

In Express, this boundary usually sits inside the route handler and template rendering. Express provides explicit response status and HTML response methods in its 5.x response API.

In a React framework, inspect whether the route delivers the main article through server rendering or build-time generation, or waits for a browser-side effect.

Choose based on the content’s lifecycle:

  • Documentation released with your code can often be generated during the build.
  • Public information that changes frequently may need request-time rendering or a defined revalidation strategy.
  • Personal account data belongs behind authentication and may reasonably depend on client-side requests.

Server rendering introduces its own operational questions. What happens when the content database is slow? What gets cached? How does a content update invalidate that cache?

Moving a request to the server is useful only if its failure behavior is understood.

A missing document needs a missing-document response

A common deployment rule sends the application shell for every path.

That makes direct visits to client-side routes work. It can also make /docs/this-page-never-existed return 200 OK.

In the example above, the server looks up the document before sending its response. Unknown documents receive 404, rather than a successful response containing an error message.

Google’s JavaScript guidance specifically discusses meaningful status codes and soft 404 problems. This is worth checking on the deployed URL, where proxy and hosting rules may differ from local behavior.

Also test navigation. Use real links:

<a href="/docs/installation">Installation</a>

An element that changes routes only through a click handler gives you another dependency to investigate.

Check directives before rewriting the application

A rendering fix will not resolve an accidental indexing restriction.

Check for both an HTML robots directive and an HTTP header:

<meta name="robots" content="noindex">
X-Robots-Tag: noindex

If the public page should be indexable, find where the restriction originates. It may come from the application, a preview-environment setting, or infrastructure configuration.

Google must be able to crawl a page to read its noindex instruction. Blocking the URL in robots.txt can prevent that instruction from being seen. See Google’s noindex documentation.

Keep canonical URLs consistent with your preferred public URLs. Canonical annotations are signals about duplicate versions, not commands that guarantee indexing. Google explains the distinction in its canonicalization guide.

A sitemap helps with URL discovery. It cannot supply content missing from the page response or guarantee that submitted URLs will be indexed. See the sitemap overview.

Use evidence to choose the next fix

Symptom Possible cause Verification Likely fix
Article absent from raw HTML Client-only fetching Compare response with live DOM Deliver main content from server or build
Works only when signed in API depends on session Repeat without cookies Provide an intentional public content path
Missing URL returns 200 Catch-all application fallback Request a nonexistent path Resolve document before choosing status
Public page has noindex Environment setting leaked Inspect HTML and headers Correct the source of the directive
Local version works, production fails Edge or deployment behavior Inspect deployed response and logs Fix the responsible infrastructure rule

After deployment, use Search Console’s URL Inspection tool to examine Google’s reported state and available rendered output. Keep the live test separate from the indexed-page record: checking the current implementation does not establish that the index has already changed.

Server logs can help establish whether requests reached your infrastructure and what responses were returned. They do not show that the content was selected for indexing.

A useful release check is small: request one real document and one nonexistent document, then inspect their status, headers, and body. Repeat it after changing hosting rules.

That catches a surprisingly consequential class of mistakes before anyone needs to debate whether the framework is “SEO friendly.”

Sources

About the author

  • We are passionate about a couple of things. At first, - we want to make the internet better. Better by being compliant, better UX, Better UI, Better Content, yes - and this results in better understanding how Search Engines work in the World of AI.

No comments yet. Be the first to comment.