HTML fragments
Return an HTML fragment when one request should replace only part of a page. This works well for search results, modal contents, and HTMX-style swaps. The server renders a component, while the browser keeps the rest of the document in place.
There is one important choice on the receiving page:
- If the page already loaded Citry's runtime, a normal DOM insertion is enough. Citry discovers the new fragment and activates it.
- If the page did not load Citry, the insertion method must execute the fragment's loader script. Assigning a string to
innerHTMLdoes not execute inserted<script>elements.
Render the fragment
Render a component, then serialize it with the "fragment" DepsStrategy:
html = Card(title="Welcome").render().serialize(
deps_strategy="fragment",
)
The calls produce three different values:
Card(...)creates aCitryElement..render()creates aCitryRender..serialize(...)returns the HTML string sent to the browser.
The fragment contains the component markup plus the manifests needed for its browser behavior and dependencies. If the component is entirely server-side, Citry can return plain HTML without those additions.
Make asset routes available
A client-active fragment refers to Citry's runtime and generated assets by URL. Mount one of Citry's web framework integrations so those URLs can be served.
Client-active output includes components that use normal Alpine attributes, $component, client props, browser or server event handlers, or Events state. If such a fragment has no mounted integration or recorded route prefix, serialization raises RuntimeError instead of returning broken URLs.
A worker that only renders fragments can record the prefix used by the serving process with Citry.set_mounted_prefix:
from citry import Citry, Component
app = Citry()
app.set_mounted_prefix("/citry")
class Notice(Component):
citry = app
def js_data(self, kwargs, slots):
return {"message": "Ready"}
template = """
<p class="notice">Loading...</p>
"""
js = """
$component(({ els, data }) => {
els[0].textContent = data.message;
});
"""
html = Notice().render().serialize(
deps_strategy="fragment",
)
Set the prefix before serialization. The generated URLs are fixed when Citry turns the render into a string.
Insert into a page that already uses Citry
Load Citry once in the host document, then insert the response:
<script src="/citry/citry.js"></script>
<div id="results"></div>
<script>
fetch('/search-fragment')
.then((response) => response.text())
.then((html) => {
document.getElementById('results').innerHTML = html;
});
</script>
The existing runtime notices the fragment manifest, fetches missing assets, and activates the complete fragment. It reuses dependencies that are still loaded in the page.
Render a fresh fragment for each insertion
If a second insertion loses its styling or browser behavior, check whether the endpoint is returning the same serialized HTML. A rendered fragment that uses Citry's browser runtime must be inserted only once per page. Render a fresh fragment for each later insertion, even when the content is unchanged.
Saving the serialized response and returning it on every request reuses the same component identities, so Citry rejects the second insertion:
# Wrong: every request returns the same rendered fragment.
saved_html = Card(title="Welcome").render().serialize(
deps_strategy="fragment",
)
def card_fragment():
return saved_html
Render inside the request handler so each response represents a new fragment:
def card_fragment():
# Each request creates a fragment that can be inserted once.
return Card(title="Welcome").render().serialize(
deps_strategy="fragment",
)
A static demo can load its pre-rendered fragment once, then reload the host document to let the reader try again.
Insert into a page without Citry
A fragment can include a small loader for Citry's runtime. The browser still has to execute that loader. Scripts inserted through innerHTML stay inert, so the innerHTML example above only works because Citry was already loaded.
For a runtime-free host, use a swap library that executes response scripts, or parse the response and recreate its <script> elements as live DOM nodes. The loader can then start Citry and adopt the manifests that arrived with the fragment.
Whichever insertion method you choose, insert the fragment as one transaction. Do not split its markup, manifests, and ownership markers into separate swaps.
Deliver component dependencies
Fragment serialization handles dependency declarations according to their form:
- URL dependencies remain URLs and are fetched by the browser.
- Local files are included as script or style descriptors by default.
- With
Dependencies.local_files = "serve", mounted applications turn local files into fingerprinted URLs instead. - Objects that only provide opaque pre-rendered HTML through
__html__cannot be decomposed into a fragment dependency. Serialization raisesTypeError; declare aScript,Style, or URL instead.
The deps_position option applies to document and simple serialization. A fragment always appends the information needed for adoption, so that option is ignored.
Run fragments across several workers
The request for a generated asset may reach a different worker from the one that rendered the fragment. Configure a shared cache backend so every worker can serve the generated values. See Cache backends for Redis, DiskCache, Django, and deployment generations.
Use the same mounted prefix and cache configuration in the rendering and serving processes.
Keep fragments intact in production
HTML optimizers and sanitizers must preserve Citry's ownership comments, manifest scripts, and client attributes. See Preserve client-active HTML for the exact list.
See also
- Component JavaScript and CSS for a component's own browser behavior and styles.
- Dependency files for URLs and local files.
- Client interactivity for browser scope and component lifecycles.
- Event actions for returning rendered updates from a Python handler.
- Rendering for render and serialization choices.