Skip to content

Commit 8e412d0

Browse files
feat: improve cache headers for Verso search page (#839)
This PR adds Netlify header rules that cause the content-addressed parts of the search index to be permanently cached, so clients don't need to do an HTTP round-trip after having downloaded them. A profile in Chrome showed a significant improvement.
1 parent 0f09d61 commit 8e412d0

5 files changed

Lines changed: 32 additions & 212 deletions

File tree

.github/workflows/ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,17 @@ jobs:
147147
run: |
148148
./generate-html.sh --mode preview --draft --output _out/draft-site
149149
150+
# Drop deploy/netlify-headers as _headers at each possible Netlify
151+
# publish-dir root so hashed search-index buckets can be cached
152+
# immutably. See deploy/netlify-headers for the rules.
153+
- name: Install Netlify cache headers
154+
run: |
155+
for dir in _out/site _out/site/reference _out/html-multi _out/draft-site _out/draft/html-multi; do
156+
if [ -d "$dir" ]; then
157+
cp deploy/netlify-headers "$dir/_headers"
158+
fi
159+
done
160+
150161
- name: Report status to Lean PR
151162
if: always() && github.repository == 'leanprover/reference-manual'
152163
shell: bash

Manual/Meta/Diagram.lean

Lines changed: 0 additions & 208 deletions
Original file line numberDiff line numberDiff line change
@@ -21,214 +21,6 @@ open Verso.Genre.Manual.InlineLean.Scopes (getScopes runWithOpenDecls runWithVar
2121

2222
namespace Manual
2323

24-
section BlockExtension
25-
26-
block_extension Block.diagram (svg : String) (width : String) (inline : Bool) where
27-
data := Json.arr #[.str svg, .str width, .bool inline]
28-
extraCss := [
29-
r#"
30-
.diagram svg text[font-family="text"] {
31-
font-family: var(--verso-text-font-family);
32-
}
33-
.diagram svg text[font-family="monospace"] {
34-
font-family: var(--verso-code-font-family);
35-
}
36-
"#
37-
]
38-
traverse _ _ _ := pure none
39-
toHtml :=
40-
open Verso.Output.Html in
41-
some <| fun _ _ _ data _ => do
42-
let .arr #[.str svgStr, .str w, .bool inl] := data
43-
| HtmlT.logError "Expected three-element JSON for diagram" *> pure .empty
44-
let style := "width: " ++ w ++ if inl then "; display: inline-block; vertical-align: middle" else ""
45-
pure {{
46-
<div class="diagram" style={{style}}>
47-
{{Html.text false svgStr}}
48-
</div>
49-
}}
50-
usePackages := ["svg"]
51-
toTeX :=
52-
some <| fun _ _ id data _ => do
53-
let .arr #[.str svgStr, .str w, .bool inl] := data
54-
| TeX.logError "Expected three-element JSON for diagram" *> pure .empty
55-
let filename := s!"diagram-{id}.svg"
56-
IO.FS.writeFile filename svgStr
57-
let widthOpt := if w == "100%" then "width=\\textwidth" else s!"width={w}"
58-
if inl then
59-
pure (.raw s!"\\includesvg[{widthOpt}]\{{filename}}")
60-
else
61-
pure (.raw s!"\\begin\{center}\\includesvg[{widthOpt}]\{{filename}}\\end\{center}\n")
62-
63-
block_extension Block.row (alignItems : String) where
64-
data := Json.str alignItems
65-
traverse _ _ _ := pure none
66-
toHtml :=
67-
open Verso.Output.Html in
68-
some <| fun _ blockHtml _ data content => do
69-
let .str ai := data | HtmlT.logError "Expected string JSON for row" *> pure .empty
70-
let style := s!"display: flex; flex-wrap: wrap; align-items: {ai}; gap: 1em;"
71-
pure {{
72-
<div class="diagram-row" style={{style}}>
73-
{{← content.mapM blockHtml}}
74-
</div>
75-
}}
76-
toTeX :=
77-
some <| fun _ go _ _ content => do
78-
pure <| .seq <| ← content.mapM go
79-
80-
end BlockExtension
81-
82-
83-
/--
84-
Parses a decimal float string (e.g. `"3.14"`, `"-0.5"`).
85-
Returns `none` for unrecognized formats.
86-
-/
87-
private def parseFloatStr (s : String) : Option Float :=
88-
let (neg, s) : Bool × String :=
89-
if s.startsWith "-" then (true, (s.drop 1).copy) else (false, s)
90-
let result : Option Float := match s.splitOn "." with
91-
| [intStr] =>
92-
intStr.toNat?.map fun n => Float.ofScientific n true 0
93-
| [intStr, fracStr] =>
94-
intStr.toNat?.bind fun n =>
95-
if fracStr.isEmpty then some (Float.ofScientific n true 0)
96-
else fracStr.toNat?.map fun frac =>
97-
Float.ofScientific n true 0 + Float.ofScientific frac true fracStr.length
98-
| _ => none
99-
result.map fun f => if neg then -f else f
100-
101-
/--
102-
A `ValDesc` for float arguments: accepts integer numerals (e.g. `1`) and
103-
string literals containing decimal numbers (e.g. `"0.08"`).
104-
-/
105-
private def floatValDesc [Monad m] [MonadError m] : ValDesc m Float where
106-
description := doc!"a number"
107-
signature := .Num ∪ .String
108-
get
109-
| .num n => pure (Float.ofScientific n.getNat true 0)
110-
| .str s =>
111-
match parseFloatStr s.getString with
112-
| some f => pure f
113-
| none => throwError "Expected a number, got {repr s.getString}"
114-
| .name n => throwError "Expected a number, got identifier {n.getId}"
115-
116-
section Config
117-
118-
variable [Monad m] [MonadInfoTree m] [MonadLiftT CoreM m] [MonadEnv m] [MonadError m]
119-
120-
structure DiagramConfig where
121-
width : Option String
122-
scale : Option Float
123-
inline : Bool
124-
125-
def DiagramConfig.parse : ArgParse m DiagramConfig :=
126-
DiagramConfig.mk <$> .named' `width true <*> .named `scale floatValDesc true <*> .flag `inline false
127-
128-
instance : FromArgs DiagramConfig m := ⟨DiagramConfig.parse⟩
129-
130-
structure RowConfig where
131-
align : String
132-
133-
def RowConfig.parse : ArgParse m RowConfig :=
134-
RowConfig.mk <$> .namedD' `align "top"
135-
136-
instance : FromArgs RowConfig m := ⟨RowConfig.parse⟩
137-
138-
end Config
139-
140-
open Illuminate in
141-
/--
142-
Returns the viewBox width (in diagram units) of an SVG diagram, using the same
143-
padding as `Illuminate.diagramToSvg` (padding = 5). Used to compute an em-based
144-
CSS width when the `scale` parameter is specified. Returns 0 for empty diagrams.
145-
-/
146-
def diagramViewBoxWidth (d : Diagram SVG) : Float :=
147-
match d.getEnvelope with
148-
| .empty => 0
149-
| .nonempty env => env Vec2.west + env Vec2.east + 10
150-
151-
open Lean.Widget Lean.Elab.Term Lean.Meta Illuminate in
152-
private meta unsafe def diagramExpanderUnsafe (config : DiagramConfig) (str : StrLit) :
153-
DocElabM Term := withoutAsync do
154-
if config.width.isSome && config.scale.isSome then
155-
throwErrorAt str "Cannot specify both 'width' and 'scale' in a diagram block"
156-
157-
let altStr ← parserInputString str
158-
159-
match Parser.runParserCategory (← getEnv) `term altStr (← getFileName) with
160-
| .error e => throwErrorAt str e
161-
| .ok stx =>
162-
let (svgStr, diagramWidth) ← runWithOpenDecls <| runWithVariables fun _vars => do
163-
let diaTy ← Meta.mkAppM ``Illuminate.Diagram #[.const ``Illuminate.SVG []]
164-
let e ← Elab.Term.elabTerm stx (some diaTy)
165-
Term.synthesizeSyntheticMVarsNoPostponing
166-
let e ← instantiateMVars e
167-
168-
-- Don't evaluate if elaboration produced errors
169-
if (← Core.getMessageLog).hasErrors then
170-
return ("", 0)
171-
172-
-- Evaluate to get SVG string
173-
let svgExpr := mkApp (mkConst ``Illuminate.diagramToSvg) e
174-
let svgStr ← evalExpr String (mkConst ``String) svgExpr
175-
176-
-- Compute diagram width from envelope if scale is needed
177-
let diagramWidth : Float ← if config.scale.isSome then do
178-
let widthExpr := mkApp (mkConst ``Manual.diagramViewBoxWidth) e
179-
evalExpr Float (mkConst ``Float) widthExpr
180-
else
181-
pure 0
182-
183-
-- Store diagram for widget RPC re-evaluation
184-
let env ← getEnv
185-
let opts ← getOptions
186-
let id ← Illuminate.nextDiagramId.modifyGet fun n => (n, n + 1)
187-
let sd : Illuminate.StoredDiagram := {
188-
env, opts, expr := e, gadgets := #[], regions := {}, returnsDwi := false
189-
}
190-
Illuminate.diagramStore.modify (·.push (id, sd))
191-
192-
-- Attach widget with CSS variable defaults for the infoview context
193-
let widgetSvg := "<div style=\"--verso-text-font-family: sans-serif; --verso-code-font-family: monospace;\">" ++
194-
"<style>svg text[font-family=\"text\"] { font-family: var(--verso-text-font-family); } " ++
195-
"svg text[font-family=\"monospace\"] { font-family: var(--verso-code-font-family); }</style>" ++
196-
svgStr ++ "</div>"
197-
let props : Json := .mkObj [
198-
("exprId", toJson id),
199-
("initialSvg", .str widgetSvg),
200-
("parameters", .arr #[])]
201-
savePanelWidgetInfo Illuminate.diagramWidget.javascriptHash.val (pure props) str
202-
203-
pure (svgStr, diagramWidth)
204-
205-
let cssWidth : String := match config.scale with
206-
| none => config.width.getD "100%"
207-
| some scale =>
208-
if diagramWidth > 0 then s!"{diagramWidth * scale}em" else "100%"
209-
210-
``(Block.other (Block.diagram $(quote svgStr) $(quote cssWidth) $(quote config.inline)) #[Block.code $(quote str.getString)])
211-
212-
open Lean.Widget Lean.Elab.Term Lean.Meta Illuminate in
213-
@[implemented_by diagramExpanderUnsafe]
214-
private opaque diagramExpanderImpl (config : DiagramConfig) (str : StrLit) : DocElabM Term
215-
216-
@[code_block]
217-
def diagram : CodeBlockExpanderOf DiagramConfig
218-
| config, str => diagramExpanderImpl config str
219-
220-
/-- Arranges the contained blocks in a horizontal row using flexbox. -/
221-
@[directive]
222-
def row : DirectiveExpanderOf RowConfig
223-
| config, stxs => do
224-
let alignItems ← match config.align with
225-
| "top" => pure "flex-start"
226-
| "middle" => pure "center"
227-
| "bottom" => pure "flex-end"
228-
| other => throwError "Expected 'top', 'middle', or 'bottom' for 'align', got {repr other}"
229-
let args ← stxs.mapM elabBlock
230-
``(Block.other (Block.row $(quote alignItems)) #[ $[ $args ],* ])
231-
23224
namespace Diagram
23325
open Illuminate
23426

Manual/RecursiveDefs/CoinductivePredicates.lean

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ languageEquivalent.coinduct {Q A Q' : Type}
377377

378378
It can be used to prove that these two DFAs have equivalent languages:
379379
:::row (align := "top")
380-
```diagram (scale := "0.1") +inline
380+
```diagram (cssScale := "0.1") +inline
381381
open Illuminate in
382382
let cfg : StateDiagramConfig := {}
383383
cfg.start 0 |>.atop
@@ -388,7 +388,7 @@ cfg.start 0 |>.atop
388388
(cfg.edge 0 1 "b")
389389
```
390390

391-
```diagram (scale := "0.1") +inline
391+
```diagram (cssScale := "0.1") +inline
392392
open Illuminate in
393393
let cfg : StateDiagramConfig := {}
394394
cfg.start 0 |>.atop

deploy/netlify-headers

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Netlify _headers rules for the reference manual.
2+
#
3+
# Hashed search-index bucket files are content-addressed: their
4+
# filenames include a per-build version string
5+
# (searchIndex_<bucket>.<version>.js), so every deploy produces fresh
6+
# filenames and old ones can be cached forever. The entry-point
7+
# searchIndex.js is deliberately *not* listed here, so it revalidates
8+
# normally and clients pick up the new version string on each deploy.
9+
10+
/-verso-search/searchIndex_*.*.js
11+
Cache-Control: public, max-age=31536000, immutable
12+
13+
/reference/-verso-search/searchIndex_*.*.js
14+
Cache-Control: public, max-age=31536000, immutable
15+
16+
/tutorials/-verso-search/searchIndex_*.*.js
17+
Cache-Control: public, max-age=31536000, immutable

lake-manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "git",
66
"subDir": null,
77
"scope": "",
8-
"rev": "06759f86fe54122ba3c081f40ed1cd24cffb8832",
8+
"rev": "8bcbd2b8723307c329c12997a0c16a6ac174d3e9",
99
"name": "verso",
1010
"manifestFile": "lake-manifest.json",
1111
"inputRev": "main",
@@ -35,7 +35,7 @@
3535
"type": "git",
3636
"subDir": null,
3737
"scope": "",
38-
"rev": "86210d4ad1b08b086d0bd638637a75246523dbb8",
38+
"rev": "a456461b368b71d2accd95234832cd9c174b5437",
3939
"name": "plausible",
4040
"manifestFile": "lake-manifest.json",
4141
"inputRev": "main",

0 commit comments

Comments
 (0)