re-frame.ssr.head¶
Build a page's <head> (title, meta tags, links, scripts, JSON-LD) from app state on the server, so crawlers and link unfurlers that do not run JavaScript see it in the first response. A head model is data derived from app-db, with the keys :title, :meta, :link, :script, :json-ld, :html-attrs and :body-attrs. The Ring handler in re-frame.ssr.ring resolves and emits the head for each page.
Ships in the day8/re-frame2-ssr artefact, alongside re-frame.ssr.
(:require [re-frame.core :as rf]
[re-frame.ssr.head :as head])
(rf/reg-head :head/article
(fn [db {:keys [params]}]
(let [{:keys [title summary]} (get-in db [:articles (:id params)])]
{:title (str title " — Example")
:meta [{:name "description" :content summary}]})))
;; A route selects it with {:head :head/article} in its metadata.
;; For a request frame on that route:
(head/head-model->html (head/head-model :app/request-17) {:wrap? true})
;; => "<head><title>Hello — Example</title>…</head>"
Register heads with rf/reg-head on the re-frame.core facade; its entry is on re-frame.ssr. head-model and head-model->html are also re-exported on re-frame.ssr as ssr/head-model and ssr/head-model->html, and each pair is the same function. There is no :rf/head subscription: :rf/head-model names a data shape, and head-model returns the model without recording it anywhere. Head metadata shows a complete head and route.
Every key of a head model is optional:
{:title "Hello — Example" ;; <title>; "" emits none
:meta [{:name "description" :content "…"} ;; one <meta> per map, in order
{:property "og:title" :content "Hello"}]
:link [{:rel "canonical" :href "https://example.com/a/1"}] ;; one <link> per map
:script [{:src "/js/widget.js"}] ;; one <script> per map, attributes only
:json-ld [{"@context" "https://schema.org" ;; one application/ld+json <script> per map
"@type" "Article"
"headline" "Hello"}]
:html-attrs {:lang "en"} ;; attributes for <html>, written by the host shell
:body-attrs {:class "article"}} ;; attributes for <body>, written by the host shell
:meta, :link, :script and :json-ld are vectors, even for one entry. A :script entry has no body, so inline script content cannot go through the head model. Text and attribute values are escaped for you, and every < in JSON-LD strings is written as \u003c, so a value cannot close its element.
Reading a head¶
head-model¶
- Kind: function
- Signature:
- Description: Returns
frame-id's head model. Pure and JVM-runnable. It resolves in one pass:- The effective route is
:routewhen the key is present (an explicit{:route nil}means no route), else the frame's active route slice fromruntime-dbat[:rf.runtime/routing :current]. - The head is a non-
nil:head-idwhen supplied, else the effective route's:headmetadata, elsedefault-head. - The head fn runs against that same effective route, so
{:route r}with no:head-idpreviewsrend to end. frame-idis required.- The frame selects the registrations as well as the data, so a head declared in one image cannot run against another image's
app-db. - The Ring handler calls it for every page and does not let the head fail the request. If resolution throws,
:rf.error/no-such-heador a throwing head fn alike, the page renders with an empty head and the handler emits the always-on:rf.error/ssr-head-resolution-failedrecord; the status stays as it was.
- The effective route is
- Errors:
:rf.error/no-such-head— the selected head id (:head-id, or the route's:head) is not registered. A route that declares no:headfalls back todefault-headwithout error.:rf.error/no-frame-context—frame-idisnil; the head never resolves against a default frame.
- Example:
Emitting a head¶
head-model->html¶
- Kind: function
- Signature:
- Description: Renders a head model to its inner-
<head>HTML fragment. The SSR pipeline calls it for you; call it directly when you emit your own HTML envelope.- Output order is fixed:
<title>, then<meta>in declaration order, then<link>, then<script>, then JSON-LD. :wrap?(defaultfalse) wraps the fragment in<head>…</head>.:html-attrsand:body-attrsare not emitted: they belong to<html>and<body>, which the host shell writes.
- Output order is fixed:
- Errors:
:rf.error/ssr-invalid-attribute-name— a:meta,:linkor:scriptattribute key outside the HTML5 attribute-name grammar.:rf.error/invalid-json-ld-number(JVM) — a non-finite number (##Inf,##-Inf,##NaN) in:json-ld, which JSON cannot represent.:rf.error/invalid-json-ld-key(JVM) — anilmap key in:json-ld.
- Example:
Framework integration¶
Not for application code — used by adapters, tools and the test harness.
default-head¶
- Kind: function
- Signature:
- Description: Returns the model
head-modelfalls back to when the effective route declares no:head, or there is no route. It carries:title(the frame's:doc, or""when it has none) and the viewport<meta>.- It carries no
<meta charset>: the host shell writes the charset, and a head model carrying one too would emit the tag twice. - A host reads it only when reimplementing the default flow.
- It carries no
- Example: