Server-rendered HTML
The HTML the server sends for an interactive page already holds the page's content. Search engines, link previews, and readers without JavaScript see it, and you do not need to turn anything on. When Vue starts in the browser, it either adopts that HTML or builds the page again.
This page explains what happens to a field the user is typing in, what the page shows before Vue starts, and what your deployment must leave untouched.
What the server sends
Citry puts the body's content in one element that Vue starts in, called the Vue host. When Vue starts, it either adopts the server's HTML or replaces it:
- Vue adopts the HTML. The server writes the page exactly as Vue's first render in the browser would, and Vue reuses those elements instead of creating them again. This is called hydration.
- Vue replaces the HTML. When the server cannot write the page the way Vue would, the host carries the same HTML a static page gets. Vue clears it and builds the page when it starts, in one step, so the browser never shows an empty page in between.
Most pages are adopted. The When Vue rebuilds HTML section lists when Vue replaces a page or builds part of it in the browser.
Vue starts after the browser has read the whole page, so the content can appear first. Your own defer and module scripts placed before Citry's scripts run before Vue starts. When your script needs the finished page, listen for the citry:ready event.
Citry writes this HTML from the compiled templates and the data the browser receives. It does not run Vue on the server, and it does not offer general-purpose Vue server-side rendering.
Keep typed input
A text input, <textarea>, or <select> keeps the user's edit when its component renders again, as long as the value the template gives it has not changed. That holds for a fixed value and for a value Python computes:
<input name="role" value="Owner" />
<input name="email" c-value="row['email']" />
Say the server sent ada@example.com and the user typed draft@example.com:
- A browser-only update, or a server render that sends
ada@example.comagain, leavesdraft@example.comin the field. - A server render that sends a different value, such as
new@example.com, replaces the draft. Once the server's value changes, the field shows it. - A
<select>the user has not changed always shows the value the template gives it, even when a server render changes its options.
The same rule applies to a value that c-bind spreads onto the field.
A value bound to browser state with Vue's :value, including a js_data() value, behaves as in Vue: every render writes it again. So does a value inside an object spread with Vue's v-bind="...". To keep a draft through those renders, store it in Vue state with v-model.
Checkboxes and radio buttons are not affected. Whether they are checked follows v-model or c-checked as usual.
When a server render reorders keyed rows, each row keeps its elements, and the field the user is typing in keeps its focus and caret.
Before Vue starts
Until the browser has loaded and started Vue, the page is plain HTML:
- A part hidden with
v-showor:hiddenbased on browser state is visible, and anaria-expandedbound to browser state is missing. - Submitting a form does an ordinary browser submission, because handlers such as
@submit.preventare not attached yet. - Text typed into an input before Vue starts is replaced by the rendered value when Vue starts.
- An element filled only by
v-textshows its text when the value comes fromjs_data()and is a string, a whole number,TrueorFalse(shown astrueandfalse), orNone(shown as nothing). Other values, such as a fraction, a dict, a list, or browser state, leave the element empty until Vue sets the text. Content written next tov-textshows until Vue replaces it. - A part that Vue builds in the browser (see Browser-built parts) shows the HTML the server wrote for it. Both branches of a
v-ifandv-elsethat depend on browser state can be visible, and anidon both branches appears twice. Focus, a text selection, or text typed inside that part is lost when Vue builds it; the rest of the page keeps them. - Component styles apply from the first paint. A page served through Citry's mounted routes links its component stylesheets in
<head>, and a standalone page includes them there.
Wrap browser parts
Some content depends on values that exist only in the browser, such as text from data() or a v-if that reads it. The server cannot write it for Vue to adopt, so Vue builds the contents of the nearest surrounding element in the browser. Everything inside that element is created again.
Wrap such a part in its own element, such as a <div> around the v-if, so that its siblings are still adopted:
<section>
<h2>{{ title }}</h2>
<div>
<p v-if="open">Ships within two working days.</p>
</div>
</section>
If no element surrounds the part, Vue replaces the whole page instead.
Skip server content
Two settings send an empty Vue host and let Vue build the page in the browser. Use them only for pages that search engines and readers without JavaScript do not need, such as a signed-in dashboard.
Citry(ssr=False)sends every page without its content.serialize(ssr=False)does it for one serialization.ssr_element_thresholdsends a page without its content when the server would write that many elements or fewer. A small page can start slightly faster this way, because the browser does not parse the HTML and then hydrate it. The default,0, keeps the content of every page with at least one element. The setting applies only to pages Vue can adopt; a page Vue replaces always carries its server HTML.
from citry import Citry, Component
app = Citry(ssr_element_threshold=5_000)
class Page(Component):
citry = app
template = """
<main>Ready</main>
"""
js = """
$component({});
"""
page = Page()
html = page.render().serialize(ssr=False)
ssr_element_threshold must be a non-negative int. Creating Citry with another type raises TypeError, and with a negative value raises ValueError.
Preserve page HTML
For an interactive page, Citry keeps your page's <html>, <head>, and <body>, and moves the body's content into the generated Vue host. It adds a configuration script and the component and extension assets that start Vue. An interactive HTML fragment instead carries a small description of its components for the runtime already on the page.
HTML minifiers, CDN optimizers, sanitizers, and streaming transforms must keep:
- the Vue host element and everything inside it, byte for byte;
- Citry's configuration scripts;
- a fragment's description block.
Do not change elements inside the Vue host with code outside Vue. A minifier that collapses whitespace in the host's text makes the browser console report "Hydration completed but contains mismatches", and Vue then rewrites the changed text.
Citry requests its own scripts and stylesheets with crossorigin="anonymous" and answers with Access-Control-Allow-Origin: *. When those requests cross origins, as in a sandboxed iframe or behind a CDN that rewrites asset URLs, a proxy that drops the header leaves the page showing its HTML while its components never start. See Keep the CORS header.
When Vue rebuilds HTML
The sections below list the exact cases in which Vue does not adopt the server's HTML. Read them when a page loses focus or typed text as Vue starts, or when the browser console reports a mismatch.
Replaced pages
Vue replaces the whole page's server HTML when the page:
- uses a Content Security Policy,
security_javascript="warn", script integrity, or a configured i18n extension; - has a component with its own
on_dependencies()method, or an extension that defineson_dependencies(),on_serialize(),on_js_loaded(), or a browser hook; - places assets with a
deps_positionother than the default; - has a browser-only part with no element around it, such as a
v-ifat the top level of the page component's template; - contains HTML that the browser's parser would rearrange.
A page uses a Content Security Policy when you pass csp_nonce, or when security_csp is "warn" or "strict" on Citry or on serialize(). See Add a CSP nonce.
Before Vue starts, such a page shows its static HTML: both branches of a v-if and v-else can be visible, v-show, :class, and other Vue bindings have no effect yet, and attributes such as @click stay in the HTML, where the browser ignores them. When Vue builds the page, focus, a text selection, and text typed earlier are lost, and an <iframe> or <video> loads again.
Some of these pages are sent with an empty Vue host instead:
- a page with
security_javascript="warn", because the warning would list every Vue attribute left in the HTML; - a page under a Content Security Policy that would reject its HTML, such as an
onclickattribute or ajavascript:link; - a page whose body contains a
<script>, for example inside<c-raw>, because the browser would run it while reading the page, and a page Vue builds never runs it.
HTML fragments are always built in the browser.
A strict policy can also block style attributes in the served HTML. See Inline style attributes.
Browser-built parts
Vue sets event listeners, bound attributes such as :aria-expanded or :hidden, an input's value, and v-show itself while it hydrates, so these can depend on browser state without any rebuild. The server leaves out a value that exists only in the browser.
Anything else that depends on browser state, such as text or a v-if condition, makes Vue build the contents of the nearest surrounding element in the browser (see Wrap browser parts). That part runs in the same app, with the same state, events, and provide and inject, as the rest of the page. Until Vue starts, it shows HTML written from the values the server has, and it keeps the classes and styles the server knows: a class="card" beside a :class that reads data() still styles the card.
The same happens for:
- a component called with extra attributes, listeners,
v-model,v-show, or a custom directive, and a component whose template is only text; - a scoped slot, or slots created conditionally or in a loop;
- HTML inserted as one finished block, such as
<c-raw>contents or aMarkupvalue with tags, when the browser's parser would rearrange it where it sits (a<p>inside a<p>), when it holds a<script>, or when it starts with an HTML comment; - markup the browser's parser would rearrange, such as a
<div>inside a<p>or a table part outside its table; - inline
<svg>or<math>,<template>,<iframe>,<noscript>, and<script>or<style>inside a component; - text containing a carriage return or NUL, text that displays a non-integer number or an object, and
<textarea>or<pre>text that starts with a newline; - a
:stylevalue the server cannot write the way the browser reads it: a fractional number, an empty value,trueorfalse, a value with a semicolon, quote, backslash, brace, comment, or unbalanced parenthesis, a vendor-prefixed name such asWebkitTransition, or a list of fallback values; - custom directives on elements.
Such an element is sent empty when its contents would hold something the browser would run or load twice, such as a <script>, an <iframe>, <object> or <embed>, a custom element, an on* attribute such as onerror, or an autofocus or autoplay attribute. A <textarea> or <title> whose text depends on browser state is sent empty too.
Hydration differences
After hydration, the page is the same as a page Vue built in the browser, except for details a script might notice:
- An element whose contents Vue built in the browser keeps a
data-allow-mismatch="children"attribute. - A
styleattribute keeps the text the server wrote, where the browser would otherwise rewrite it in its own form. - Lists, slots, and HTML inserted as one block keep the server's
<!--[-->and<!--]-->comments around them, where Vue would create empty text nodes. - The Vue host has no
data-v-appattribute. Listen forcitry:readyto know the page is ready.
See also
- Security and CSP for nonces and pages without JavaScript.
- Limits and errors when components do not start.
- HTML fragments for interactive fragment loading.
- Component JS and CSS for component-owned assets.