Now.js Framework Documentation

Now.js Framework Documentation

PageNavigator

EN 06 Nov 2026 02:55

PageNavigator

Overview

PageNavigator loads server-rendered pages (for example a CMS front end) into the main element (Now.config.mainSelector, normally #main) without a full reload, and updates the address bar as usual.

  • A click on an internal link fetches the same URL with the header X-Partial-Page: 1. The server answers with JSON holding only what changes. The navigator swaps the content of #main, updates the title and the page-specific meta tags, and calls pushState with the URL.
  • Opening a URL directly, as a first visit or a search engine crawler does, still gets the full page from the server. SEO does not change.
  • Anything unexpected makes the navigator load the full page instead. That covers a network error, an answer that is not JSON, a missing response header, reload: true and a leave guard saying no. A link therefore never does nothing.

Use it when: pages are built on the server (PHP) and you want SPA-like speed.
Do not use it when: the app renders client-side templates. Use RouterManager instead. Never enable both, because they intercept the same links.

Enabling

await Now.init({
  router: {enabled: false},
  navigator: {
    enabled: true,
    // Paths (relative to Now.basePath) loaded normally: a string prefix or a RegExp
    exclude: ['admin/', 'api/']
  }
});
Option Default Meaning
enabled false Intercept links
header 'X-Partial-Page' Header sent to ask for a partial page; the response must carry it with 1
exclude [] Paths not intercepted (string prefix or RegExp)
scrollToTop true A new page starts at the top. If the URL has #id, the page scrolls to that element instead
focusContent true Moves focus to #main after loading, so screen readers continue from the new content
progress true Progress bar along the top of the window (see Progress bar)
transition false Cross-fades between pages with the View Transitions API. It only works in browsers that support the API and is skipped when the user prefers reduced motion

Server contract

The request carries X-Partial-Page: 1. The server must answer with JSON and the header X-Partial-Page: 1. It should also send Vary: X-Partial-Page and Cache-Control: no-store, because the same URL can answer in two forms.

{
  "content": "<div>…inner HTML of #main…</div>",
  "title": "Page title",
  "head": "<meta name=\"description\" content=\"…\" data-page-meta><link rel=\"canonical\" href=\"…\" data-page-meta>",
  "styles": ["https://fonts.googleapis.com/css2?family=Kanit"],
  "reload": false
}
Key Meaning
content HTML that replaces the inner HTML of #main (required)
title document.title
head Page-specific <head> tags, each marked data-page-meta. The current [data-page-meta] tags are removed and these are inserted. Full pages must mark the same tags with data-page-meta
styles Stylesheets the page needs. Each is added once and never removed
reload true means the server cannot answer partially (maintenance page, language switch …), so the page loads in full
anything else Passed through in the page:loaded event (detail.data), for example the current menu

If the server redirects (for example to a login page), fetch follows the redirect and the address bar shows the final URL.

Forms

A method="get" form, such as a search box, loads its result page the same way as a link. Following the rules of GET, the query of action is replaced by the form's fields, including the button that was pressed.

These forms are not touched:

  • POST forms
  • Forms handled by FormManager (data-form)
  • Forms with a target
  • Forms under data-navigate="false"

Progress bar

While a page loads, a thin bar runs along the top edge of the window in two steps:

  1. When loading starts, it runs quickly to 80% and waits there.
  2. When the new page is in, it fills from wherever it got to up to 100% and fades out.

Most pages arrive in a moment, so the bar runs briefly and fills straight away. Tune it with these CSS variables in the theme's CSS:

:root {
  --page-progress-color: var(--color-primary); /* colour */
  --page-progress-height: 3px;                 /* thickness */
  --page-progress-at: 0.8;                     /* where the first step waits (0-1) */
  --page-progress-start: 0.5s;                 /* duration of the first step */
  --page-progress-end: 0.2s;                   /* fill time, then fade time */
}
  • If the bar's colour would vanish into whatever is at the top of the window (for example a header in the same primary colour, with contrast below 2), the bar turns white or dark, whichever stands out.

The element is <div class="page-progress">. It has the class is-loading during the first step and is-done during the second.

  • When a link turns into a normal page load, the bar stays at 80% until the page goes.
  • If the page never goes (the link was a download), the loading state ends on its own within 3 seconds.
  • Turn the bar off with progress: false.
  • The link or an ancestor has data-navigate="false"
  • The link has target (other than _self), download or rel="external"
  • Ctrl, Cmd, Shift or Alt is held, or the click is not the main mouse button
  • The URL is on another origin, outside Now.basePath, or is mailto:/tel:
  • The URL is a file: any extension other than .php, .html or .htm (for example .pdf, .zip)
  • The URL is a #id link on the same page
  • The path matches exclude
PageNavigator.isNavigable('/news/123');      // true
PageNavigator.isNavigable('/files/a.pdf');   // false

Scripts in the new content

innerHTML never executes <script>, so PageNavigator runs the new scripts in order after the swap:

  • Inline scripts run every time.
  • A script with src that is already on the page is not loaded again.
  • DOMContentLoaded and load listeners that the new scripts add are called once all of them have run. Those events never fire again on this document. Older templates that use document.addEventListener('DOMContentLoaded', …) therefore keep working unchanged.

Components, elements and forms inside #main are destroyed and created through the same steps as RouterManager.render().

The site config that AppConfigManager bound to the page on the full load (data-text="data.web_title", data-attr="data-username:data.google_client_id" on social login buttons, data-if …) is bound to the new #main content too, through AppConfigManager.applyBindings(). Markup outside #main is left as it is.

Events

Event Source Data
route:changed EventManager {path, params: {}, query, hash, route: null, previous, isNotFoundPage, scroll: false}. This is the same event the router emits, so components that refresh on it (MenuManager, EventCalendar …) work unchanged
page:loaded EventManager and document (CustomEvent) {url, previous, status, data}, where data is the whole JSON payload
content:rendered EventManager Emitted by RouterManager.render()
// A plain script that does not use EventManager
document.addEventListener('page:loaded', e => {
  console.log('now showing', e.detail.url);
});

Leave guard

const off = PageNavigator.beforeLeave(url => {
  // false = do not swap; load the page in full instead, so the page's beforeunload runs
  return !editor.hasUnsavedChanges();
});
off(); // unregister

The guard applies to link clicks and to Back/Forward.

Back / Forward and scroll position

  • The scroll position is kept in history.state.scrollY and restored on Back/Forward.
  • history.scrollRestoration is set to manual, because the content arrives after a fetch. A page reload also restores the saved position.
  • PageNavigator positions the page itself. The route:changed it emits carries scroll: false, so ScrollManager does not scroll to #main again.
  • While a page loads, body has the class loading and #main has aria-busy="true". The theme decides how that looks.

API

Method Description
init(options) Starts the navigator. Now.init calls it with the navigator config
navigate(url, {fromHistory, scrollY}) Loads a page partially and returns true when the content was swapped
isNavigable(href) Whether the URL can be loaded partially
beforeLeave(guard) Registers a leave guard and returns an unregister function