Skip to content

Latest commit

 

History

History
202 lines (154 loc) · 6.64 KB

File metadata and controls

202 lines (154 loc) · 6.64 KB

SSG & Pre-Rendering

No.JS works seamlessly with static site generators and server-side pre-rendering. Because the framework initializes on top of existing HTML — rather than replacing it — you can deliver fully pre-rendered pages that No.JS then enriches with reactivity, without any build step or special server support.


The core idea

No.JS reads its initial state from the state= attribute on any element. If you pre-populate that attribute with your data before the browser renders the page, the content is immediately available — no fetch required, no flicker, 100% indexable by Googlebot.

<!-- HTML generated by your SSG / server with inline data -->
<div id="app" state='{"product": {"name": "Sneaker X", "price": 299, "slug": "sneaker-x"}}'>
  <h1 bind="product.name"></h1>          <!-- "Sneaker X" — in initial HTML -->
  <span bind="'$' + product.price"></span> <!-- "$299" — in initial HTML -->
</div>

When No.JS initializes, it reads the state= value, creates a reactive context, and binds normally — but since the content is already rendered, the user sees it instantly and search engines index it without needing to execute JavaScript.


Patterns

Static pages (SSG)

For pages where data changes infrequently (blog posts, product listings, landing pages), generate the HTML with data inline at build time using any SSG or templating tool.

Eleventy example (product.njk):

---
layout: base.html
---
<div id="app" state='{{ product | dump }}'>
  <h1 bind="product.name"></h1>
  <p bind="product.description"></p>
  <button post="/api/cart/add" body='{"id":"{{ product.id }}"}'>
    Add to cart
  </button>
</div>

{{ product | dump }} is a Nunjucks filter that serializes the product object to a JSON string. Equivalent filters exist in other template engines: {{ product | to_json }} in Jinja2, {{ product | json }} in Liquid.

The state= value is set at build time from the template data. No.JS boots, reads it, and makes the "Add to cart" button interactive — but <h1> and <p> were already in the HTML.

Server-side rendering (SSR)

For dynamic data (user-specific pages, real-time content), your server generates the state= attribute at request time.

Node.js / Express example:

app.get('/products/:id', async (req, res) => {
  const product = await db.products.findById(req.params.id);
  res.render('product', {
    state: JSON.stringify({ product })
  });
});
<!-- product.html template -->
<div id="app" state="<%= state %>">
  <h1 bind="product.name"></h1>
</div>

Hybrid: static structure, dynamic interaction

You can combine static content with dynamic updates. Render the initial state server-side for SEO, and let No.JS refresh it client-side on user interaction:

<!-- Pre-rendered initial state for Googlebot -->
<div id="app" state='{"product": {"name": "Sneaker X", "stock": 12}}'>
  <h1 bind="product.name"></h1>
  <span bind="product.stock + ' in stock'"></span>

  <!-- Dynamic: only runs after user interaction, not needed for SEO -->
  <button post="/api/cart/add" body='{"id":"sneaker-x"}' then="cart.count++">
    Add to cart
  </button>
</div>

What to put in state= vs get=

Content type Approach Why
SEO-critical (title, description, main content) state= pre-rendered Indexed immediately, no fetch required
User-specific (cart, preferences, session) get= with JS Changes per user — can't be pre-rendered
Frequently updated (stock levels, prices) get= with refresh= Needs to be live — pre-render for fallback only
Static references (nav menu, categories) state= pre-rendered Rarely changes, should be in initial HTML

Rule of thumb

Content a search engine should index → put it in state= (pre-rendered)
Content that changes per user/session → use get= (fetched client-side)

SEO-critical content placed inside a get= directive will only appear after the fetch completes. While Googlebot does execute JavaScript, there is typically a delay of several days before dynamically loaded content is indexed. For product names, page titles, descriptions, and canonical URLs, pre-rendering is strongly recommended.


No.JS + Eleventy quick-start

npm install @11ty/eleventy
_site/           ← built HTML output (deploy this)
_includes/
  base.html      ← base layout
pages/
  product.njk    ← product page template
_data/
  products.json  ← your data
.eleventy.js

.eleventy.js:

module.exports = function(eleventyConfig) {
  eleventyConfig.addPassthroughCopy("assets");
  return { dir: { output: "_site" } };
};

pages/product.njk:

---
layout: base.html
pagination:
  data: products
  size: 1
  alias: product
permalink: /products/{{ product.slug }}/
---
<div id="app" state='{{ product | dump }}'>
  <h1 bind="product.name"></h1>
  <p bind="product.description"></p>
</div>

_includes/base.html:

<!DOCTYPE html>
<html>
<head>
  <title>{{ product.name }} | My Store</title>
  <meta name="description" content="{{ product.description }}">
  <script src="/assets/no.js" defer></script>
</head>
<body>{{ content | safe }}</body>
</html>

Run npx eleventy and your _site/ directory contains fully pre-rendered HTML pages that No.JS enriches with interactivity.


Security note

When embedding data in state=, always HTML-escape the JSON value to prevent XSS:

// Node.js — safe embedding
const stateJson = JSON.stringify(product).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
// → use stateJson as the state= attribute value

Most template engines (Jinja2, Nunjucks, Twig, Liquid) escape attribute values by default. Verify that your SSG does not disable escaping for state= values.


Managing <head> in SSG pages

When your SSG renders each page with its own <title> and <meta name="description"> server-side, those tags are already in the initial HTML — no client-side directive is needed for static content.

For SPA routing (single index.html with a No.JS router), the server renders only one HTML file and the router handles navigation. Use the page-title, page-description, page-canonical, and page-jsonld attributes directly on <template route> elements to update <head> on each navigation — see Route Head Attributes →.

For non-routing pages (product pages, landing pages without a router), the page-title and page-description directives from Head Management → provide the same capability as a body directive with full reactive support.


Next: Data Fetching →