glacier-web-cli 0.5.0

Command-line tool for creating and running Glacier applications.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
---
title: Views and Templates
description: How Glacier's compiled HTML templates work, from props, expressions and escaping to layouts, partials, form helpers, PDFs and static files.
category: Views and Controllers
order: 3
---

Glacier's views are HTML templates in `src/views/`. Each one declares the
values it needs, and is compiled into a Rust struct when the app builds, so
a template that uses a field the model doesn't have, or a route that
doesn't exist, stops the build instead of failing in front of a visitor.

After reading this guide, you will know:

- Where templates live and how they become structs.
- How to write expressions, conditions and loops, and how output is escaped.
- How layouts and partials fit together.
- Which helpers templates can use, including the form helpers.
- What `html!` and `#[derive(Template)]` are for.
- How files in `public/` are served.

The examples come from the `notes` app introduced in
[Routing](routing.html).

## Templates

### Where Templates Live

Templates are `.html` files under `src/views/`, one folder per controller,
named after the action that renders them. `render` in `Notes.index` uses
`src/views/notes/index.html` (see
[Controllers](controllers.html#rendering-a-template)). The `notes` app has:

```text
$ find src/views -name '*.html' | sort
src/views/admin/dashboard.html
src/views/docs/index.html
src/views/docs/show.html
src/views/layouts/admin.html
src/views/layouts/app.html
src/views/notes/_form.html
src/views/notes/edit.html
src/views/notes/index.html
src/views/notes/new.html
src/views/notes/preview.html
src/views/notes/show.html
src/views/pages/home.html
src/views/settings/show.html
src/views/shared/_flash.html
```

### Templates Are Structs

The app turns the folder into Rust as it compiles, with no file of yours:
it reads `src/views/` and makes one
struct per file, in a module per folder. `notes/index.html` becomes
`views::notes::Index`, `layouts/app.html` becomes `views::layouts::App`,
and `notes/_form.html` becomes `views::notes::Form` (a leading `_` is
dropped). Folder names must be valid Rust identifiers, and file names
snake_case words.

Templates see every model, every path helper, what every folder under
`src/` holds (a `Price` from `src/clients/`, say), the app's own
functions and Glacier's prelude, and the helpers described
[below](#helpers). An app that wants to choose for itself writes
`src/views.rs`: `pub use` lines for what templates see, then
`glacier::views!();`.

### Props

A template's first line declares its props: the values it's rendered
with, and their Rust types:

```html
{% props notes: Array<Note>, pinned: Option<Note>, page: glacier::pagination::Page, total: i64 %}
<h1>Notes</h1>
{%- if let Some(note) = pinned %}
<p class="pinned">Pinned: {{ link_to(&note.title, note) }}</p>
{%- end %}

{% if notes.is_empty() -%}
<p>No notes yet.</p>
{%- else -%}
<.table headers={vec!["Title".to_owned(), "Body".to_owned(), String::new()]}>
  {%- for note in notes %}
    <tr>
      <td>{{ note.title }}</td>
      <td>{{ excerpt(&note.body) }}</td>
      <td>
        {{ link_to("Show", note) }}
        {{ link_to("Edit", edit_note_path(note.id)) }}
      </td>
    </tr>
  {%- end %}
</.table>
{{ paginate(&page, total as u64) }}
{%- end %}

<p><a href="{{ new_note_path() }}">New note</a></p>
```

`{% props notes: Array<Note>, pinned: Option<Note>, page: glacier::pagination::Page, total: i64 %}`
makes `Index` a struct with those four fields, which
`render notes, pinned: pinned, page: page, total: total` fills in. The props line must come before anything else in the file, at most
once, and leaves no blank line in the output. A template with no props,
such as `pages/home.html`, needs no props line.

Templates own their data, so props are owned values like `Note` and
`Array<Note>`, or `'static` references such as `&'static str`, never
borrows.

## Template Syntax

| Syntax | Does |
| --- | --- |
| `{{ expr }}` | Outputs a Rust expression, HTML-escaped |
| `{% if cond %} … {% else if cond %} … {% else %} … {% end %}` | Conditions; `if let` works too |
| `{% for pattern in expr %} … {% end %}` | Loops |
| `{% let pattern = expr %}` | A local variable, until the end of the enclosing block |
| `{# comment #}` | A comment, left out of the output |
| `{%-`, `-%}`, `{{-`, `-}}` | Trims whitespace before or after the tag |

`{% endif %}` and `{% endfor %}` work as well as `{% end %}`. Any other tag,
such as `{% while %}`, is a compile error.

### Expressions (updated)

Anything between `{{` and `}}` is a Rust expression. Props are in scope by
name, and so are the helpers and everything templates see
([above](#templates-are-structs)):

```html
{% props note: Note %}
<h1>Note</h1>

<p>Written on {{ note.inserted_at.format("%-d %B %Y").to_string() }} ({{ note.inserted_at.time_ago() }})</p>

<.card title={note.title.clone()}>
  <:header><small>Note #{{ note.id }}</small></:header>
  <div class="body">{{ sanitize(&note.body) }}</div>
</.card>

<p>
  <a href="{{ edit_note_path(note.id) }}">Edit</a>
  <a href="{{ notes_path(after: note.id) }}">Back</a>
</p>
<button type="button" commandfor="delete-note" command="show-modal">Delete</button>
<.modal id="delete-note" title="Delete this note?">
  <p>It can't be brought back.</p>
  {{ form_tag(note.to_path(), "delete") }}<.button kind="danger">Delete</.button></form>
</.modal>
```

The template can call methods and macros, as with
`note.inserted_at.format("%-d %B %Y").to_string()` above, and the
text, number and time helpers, as `note.inserted_at.time_ago()` (see
[Text and Number Helpers](glacier_language.html#text-and-number-helpers)):

```elixir
test "formats the date it was written" do
  note = insert(Note)
  res = get note
  assert res.body =~ format!("Written on {}", note.inserted_at.format("%-d %B %Y"))
  assert res.body =~ "(just now)"
  travel(3.days())
  assert get(note).body =~ "(3 days ago)"
  # A path with a query: `notes_path(after: note.id)`.
  assert get(note).body =~ format!("href=\"/notes?after={}\"", note.id)
end
```

`{{ }}` outputs strings, `char`s, numbers and `bool`s, nested templates,
and the trusted HTML that `raw()`, `link_to` and the form helpers return.
Your own types can be output too, by implementing `glacier::view::Render`
for them. `Some(value)` outputs the value and `None` outputs nothing.
Anything else, such as a timestamp, is a compile error until it's turned
into one of those, for example with `.to_string()`.

A path helper takes query parameters as keywords, as it does in
`.glacier` code: `notes_path(after: note.id)` above is
`/notes?after=7`, each value escaped.

`self` isn't available in templates; use the props by name.

### Escaping

Text output with `{{ }}` is HTML-escaped: `&`, `<`, `>`, `"` and `'` become
entities. A note titled `<script>alert(1)</script>` is shown as text,
not run:

```elixir
test "escapes what it prints" do
  note = insert(Note, title: "<script>alert(1)</script>")
  res = get note
  assert res.body =~ "&lt;script&gt;alert(1)&lt;/script&gt;"
  refute res.body =~ "<script>alert"
end
```

To output HTML as it is, wrap it in `raw(…)`:

```html
{% props body: &'static str %}
{{ raw(body) }}
<p>{{ link_to("All docs", docs_path()) }}</p>
```

WARNING: `raw` outputs exactly what it's given. Only pass it HTML your app
wrote, never anything a visitor sent.

For HTML a visitor did send, such as a note's body, `sanitize(…)` keeps
only safe markup, as Rails' `sanitize` does. Paragraphs, emphasis, lists,
headings, links and images stay. Scripts, styles, event handlers such as
`onerror`, and links to `javascript:` go. `notes/show.html` prints the
body that way:

```elixir
test "keeps a body's safe HTML only" do
  note = insert(Note, body: "<em>Soon</em>, <a href=\"javascript:alert(1)\">see</a><script>alert(1)</script>")
  res = get note
  assert res.body =~ "<em>Soon</em>, <a>see</a>"
  refute res.body =~ "alert(1)"
end
```

`strip_tags(…)` goes further and keeps only the text:
`strip_tags("<b>Bold</b> move")` is `Bold move`.

### Conditions and Loops

`{% if %}` takes any Rust condition, including `if let`, and
`{% else if %}` and `{% else %}` add branches. `{% for %}` loops over
anything iterable. Looping over a prop or a variable borrows it, so
`{% for note in notes %}` gives `&Note`s and leaves `notes` usable
afterwards. Both appear in `notes/index.html` above.

### Whitespace Control

A `-` just inside a tag removes all the whitespace on that side of it,
including newlines. `notes/index.html` uses it so the empty list renders
cleanly:

```elixir
test "says when there are no notes" do
  res = get "/notes"
  assert res.body =~ "<h1>Notes</h1>\n\n<p>No notes yet.</p>"
end
```

The `-%}` after `{% if notes.is_empty() %}` removed the newline before
`<p>`, and `{%-` before `{% else %}` the one after it.

## Layouts

`render` puts the page inside a layout, `src/views/layouts/app.html` by
default:

```html
{% props title: String, content: Rendered %}
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>{{ title }} · notes</title>
    <link rel="stylesheet" href="/app.css">
  </head>
  <body>
    {{ shared::Flash {}.render() }}
    {{ content }}
  </body>
</html>
```

A layout is a template whose props include `content: Rendered`, which is
the rendered page. `title` is the prop that `render … title: "Notes"`
sets. Layout structs also implement `Default`, so any layout prop an
action doesn't set is left at its type's default value.

```elixir
test "renders the page inside the layout" do
  res = get "/notes"
  assert res.body =~ "<!doctype html>"
  assert res.body =~ "<title>Notes · notes</title>"
  assert res.body =~ "<h1>Notes</h1>"
  assert res.body =~ "<meta name=\"csrf-token\" content=\""
end
```

Another file in `src/views/layouts/` is another layout, which an action
chooses with `layout: :admin`, and `layout: :none` renders a page on its
own (see [Controllers](controllers.html#rendering-a-template)). The
`notes` app's admin pages use `layouts/admin.html`:

```html
{% props title: String, notes_count: i64, content: Rendered %}
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>{{ title }} · notes admin</title>
    <link rel="stylesheet" href="/app.css">
  </head>
  <body class="admin">
    <header>Admin area · {{ notes_count }} notes · {{ form_tag(admin_session_path(), "delete") }}<button>Lock</button></form></header>
    {{ shared::Flash {}.render() }}
    {{ content }}
  </body>
</html>
```

A layout has no named blocks for a template to fill; it gets values only
through its props.

### Values Every Page Shows (new)

Some of what a layout shows comes from the database on every request: a
count in the header, an account's custom styles. Rather than every action
passing it, middleware loads it once and `assign`s it to the layout's
prop of that name, as Phoenix's `assign` does. The admin pipeline in
`notes` runs this middleware:

```glacier
# What every admin page's header shows, loaded once per request: the
# admin layout's `notes_count` prop.
module AdminCounts do
  implements Middleware

  def call(ctx, next) do
    assign("notes_count", Note.count()!)
    next(ctx)
  end
end
```

and the admin layout above has a `notes_count: i64` prop, which shows it:

```glacier
test "the admin layout shows what the pipeline assigned" do
  insert(Note)
  insert(Note)
  post "/admin/session", passcode: "let me in"
  assert get("/admin").body =~ "Admin area · 2 notes"
end
```

A prop an action sets wins over the assigned value; a prop neither sets
is its type's default. The value must have the prop's type, so
`assign("notes_count", 2)` (an `i32`) leaves an `i64` prop at `0`, and
logs a warning saying so.

## Partials

A partial is a template whose file name starts with `_`. It's a struct like
any other, so a template renders it by building the struct and calling
`.render()` inside `{{ }}`. `notes/new.html` and `notes/edit.html` share
the form in `notes/_form.html`:

```html
{% props note: Note %}
<h1>Edit note</h1>
{%- let path = note.to_path() %}

{{ Form { note }.render() }}

<p>
  <a href="{{ path }}">Show</a>
  <a href="{{ notes_path() }}">Back</a>
</p>
```

Inside the same folder, the struct's name is enough: `Form { note }`. From
anywhere else, name the folder, as both layouts do with the flash
messages in `shared/_flash.html`:

```html
{%- if let Some(notice) = flash("notice") %}
<p class="flash">{{ notice }}</p>
{%- end %}
{%- if let Some(alert) = flash("alert") %}
<p class="flash alert">{{ alert }}</p>
{%- end %}
```

```elixir
test "renders the flash partial in either layout" do
  post "/admin/session", passcode: "let me in"
  res = get "/admin"
  assert res.body =~ "<p class=\"flash\">Admin area unlocked.</p>"
end
```

A partial's props are checked like any struct's fields, so leaving one out
is a compile error.

## Components (new)

A component is a piece of page an app uses again and again, such as a
button or a card, called from any template with a tag, as Phoenix's HEEx
writes them: `<.button kind="danger">Delete</.button>`. Each one is a
template in `src/components/`, beside `src/views/`, named for its tag
(`button.html` for `<.button>`), whose props are its attributes:

```html
{% props kind: String = "primary", inner_block: Slot %}
<button class="button button-{{ kind }}">{{ inner_block }}</button>
```

A prop with a default (`kind: String = "primary"`) may be left out of
the call. A prop of type `Slot` takes content rather than a value:
`inner_block` is what the caller writes between the tags, and any other
slot, such as `header` here, is written inside the call as
`<:header>…</:header>`. `{{ inner_block }}` renders a slot. A slot with a
default (`Slot::default()`, nothing) is optional:

```html
{% props title: String, header: Slot = Slot::default(), inner_block: Slot %}
<section class="card">
  <header><h2>{{ title }}</h2>{{ header }}</header>
  {{ inner_block }}
</section>
```

An attribute's value is text in quotes (`kind="danger"`) or an expression
in braces (`title={note.title.clone()}`), and one with neither
(`disabled`) is `true`. `notes/show.html` puts the note in a card and its
delete button in a button:

```html
{% props note: Note %}
<h1>Note</h1>

<p>Written on {{ note.inserted_at.format("%-d %B %Y").to_string() }} ({{ note.inserted_at.time_ago() }})</p>

<.card title={note.title.clone()}>
  <:header><small>Note #{{ note.id }}</small></:header>
  <div class="body">{{ sanitize(&note.body) }}</div>
</.card>

<p>
  <a href="{{ edit_note_path(note.id) }}">Edit</a>
  <a href="{{ notes_path(after: note.id) }}">Back</a>
</p>
<button type="button" commandfor="delete-note" command="show-modal">Delete</button>
<.modal id="delete-note" title="Delete this note?">
  <p>It can't be brought back.</p>
  {{ form_tag(note.to_path(), "delete") }}<.button kind="danger">Delete</.button></form>
</.modal>
```

```elixir
test "shows a note in a card, with a button" do
  note = insert(Note, title: "Shopping", body: "Eggs")
  body = get(note).body
  assert body =~ "<section class=\"card\">"
  assert body =~ "<h2>Shopping</h2><small>Note ##{note.id}</small>"
  assert body =~ "<div class=\"body\">Eggs</div>"
  assert body =~ "<button class=\"button button-danger\">Delete</button>"
end
```

Glacier reads a component's props when it builds the pages that use it,
so a call that leaves out a required attribute, names one the component
doesn't take, or gives it the wrong type, is a build error that says so:

```text
error: notes/show.html:6: `<.card>` needs `title`
error: notes/show.html:18: `<.button>` takes no `size`; it takes `kind`, and the slots `inner_block`
```

A component is a template like any other, so it can use helpers, other
components and its own `if` and `for`.

### Previewing Components (new)

`/dev/components`, in development, shows every component's examples on
one page, in the app's own layout and stylesheet, for working on them
side by side. A component's examples are a template beside it,
`<name>.preview.html`, which uses its tag with the values to show; it
takes no props:

```html
<.card title="Shopping">
  <:header><small>Note #7</small></:header>
  <p>Eggs, flour, milk.</p>
</.card>
```

```glacier
test "each component's preview renders, for /dev/components" do
  previews = notes::views::component_previews()
  names = previews.iter().map(fn (name, _) -> *name end).collect::<Array<_>>()
  assert names == ["button", "card", "modal", "select", "table"]
  page = glacier::view::gallery_html(previews)
  assert page =~ "<a href=\"#button\">button</a>"
  assert page =~ "Delete this note?"
end
```

`views::component_previews()` gives each one rendered, by name.

### The Component Library (new)

`glacier new` writes a whole component library into `src/components/`,
the app's own to change, as Phoenix's `core_components.ex` and shadcn's
components are: about 140 components, from buttons, fields, tables and
dialogs to navigation, layouts, charts drawn on the server, and the
GOV.UK patterns for long forms (error summaries, summary lists, task
lists, date inputs). Each is plain HTML styled by a `ui-*` class, with
its variant and size as attributes, and works without JavaScript:

```html
{% props variant: String = "", size: String = "", r#type: String = "submit", inner_block: Slot %}
<button class="ui-btn"{% if !variant.is_empty() %} data-variant="{{ variant }}"{% end %}{% if !size.is_empty() %} data-size="{{ size }}"{% end %} type="{{ r#type }}">{{ inner_block }}</button>
```

Its stylesheet is `public/ui/components.css`, and everything it looks
like comes from the tokens in `public/ui/tokens.css`: colours (light, and
dark when the system or `data-theme="dark"` asks), type, spacing, radii
and shadows. Change a token and every component follows; with Tailwind,
`bg-primary` and `text-muted-foreground` follow too. The Geist fonts it
uses are in `public/fonts/`.

Form controls take a field and do the wiring: the label, the
`id`/`for`, the value to show, `aria-invalid`, and the errors under the
control. `Field::named("settings[theme]", &theme)` is a field for a form
without a record, and `options([...])` makes a select's options:

```html
<.native_select field={Field::named("settings[theme]", "dark")} options={options([("light", "Light"), ("dark", "Dark"), ("system", "Match the system")])} />
```

A dialog is the browser's own `<dialog>`: a button with
`commandfor="its-id" command="show-modal"` opens it and a `<form
method="dialog">` button closes it, without JavaScript. Browsers older
than Chrome 135, Safari 26 and Firefox 144 don't know `commandfor` yet;
`public/ui/ui.js`, which the layout loads, does it for them:

```html
<button class="ui-btn" data-variant="destructive" type="button" commandfor="delete-project" command="show-modal">Delete project</button>
<.dialog id="delete-project" title="Delete ‘Acme Inc.’?" description="This removes 12 projects. It can't be undone.">
  <div class="ui-dialog-footer">
    <form method="dialog"><.button variant="outline">Cancel</.button></form>
    <.button variant="destructive">Delete project</.button>
  </div>
</.dialog>
```

Every component has a preview beside it, so `/dev/components` shows
the whole library in the app's own layout:

```glacier
test "every component's preview renders" do
  res = get "/dev/components"
  assert res.status == :ok
  assert_select(res, "section h2 code", count: 141)
  assert_select(res, "section#button button.ui-btn[data-variant=destructive]", text: "Delete project")
  assert_select(res, "section#dialog dialog.ui-dialog h2", text: "Delete ‘Acme Inc.’?")
end
```

## Helpers (updated)

These functions are available in every template:

| Helper | Gives |
| --- | --- |
| `link_to(text, target)` | `<a href="…">text</a>`, to a path or a record; the text is escaped |
| `form_for(&record)` | An opening `<form>` tag for a record, with the CSRF token |
| `form_tag(action, method)` | An opening `<form>` tag for any path and method, with the CSRF token |
| `csrf_field()` | The hidden CSRF token field, for a form written by hand |
| `csrf_token()` | The CSRF token itself, for a `<meta name="csrf-token">` tag that scripts read |
| `flash("notice")` | The flash message of that kind, as an `Option<String>` |
| `raw(html)` | HTML output as it is, without escaping |
| `sanitize(html)` | HTML with only safe tags and attributes kept (see [Escaping]#escaping) |
| `strip_tags(html)` | The text of some HTML, every tag removed |
| `markdown(text)` | Markdown as HTML, cleaned as `sanitize` cleans (see [Markdown]#markdown) |
| `paginate(&page, total)` | Links to a list's other pages (see [Pagination]#pagination) |

The path helpers from the route table, such as `notes_path()` and
`edit_note_path(note.id)`, are available too, and so are records' own
paths, `note.to_path()`. Through the prelude, templates also have `t`,
`t_count`, `t_with` and `locale` for translations, and the text and
number helpers (`body.truncated(140)`, `price.to_currency("GBP")`).

### Markdown (new)

`markdown(&note.body)` turns Markdown into HTML: CommonMark, with
GitHub's tables, strikethrough (`~~gone~~`), task lists (`- [x] done`)
and footnotes. Then it's cleaned as `sanitize` cleans it, so HTML written
into the Markdown, a `<script>` or a `javascript:` link, is dropped:

```html
<div class="body">{{ markdown(&note.body) }}</div>
```

```glacier
test "previews a note's body as Markdown, safely" do
  note = insert(Note, title: "List", body: "Buy *milk* and ~~eggs~~.\n\n- [x] bread\n\n<script>alert(1)</script>")
  res = get preview_note_path(note.id)
  assert res.body =~ "<em>milk</em>"
  assert res.body =~ "<del>eggs</del>"
  assert_select(res, "li input[type=checkbox][checked]")
  refute res.body =~ "<script"
end
```

### Your Own Helpers

An app's own helpers go in `src/helpers/`, one module per file, as Rails'
`app/helpers/` does: `CommentsHelper` in
`src/helpers/comments_helper.glacier`. Templates call their functions by
name, as they do `link_to`. The `notes` app has one that cuts a long
note short for its list:

```elixir
# Helpers for the notes pages. Templates call them by name, as they do
# `link_to`: `{{ excerpt(&note.body) }}`.
module NotesHelper do
  # The start of a long text, cut at a word, for lists.
  def excerpt(text: &str) -> String do
    if text.chars().count() <= 40 do
      return text.to_owned()
    end
    cut: String = text.chars().take(40).collect()
    start = cut.rsplit_once(' ').map(fn (start, _) -> start end).unwrap_or(&cut)
    "#{start}…"
  end
end
```

`src/views/notes/index.html` shows each note's body with
`{{ excerpt(&note.body) }}`:

```elixir
test "calls the app's own helpers" do
  insert(Note, body: "A long note, about many things, that goes on and on")
  res = get "/notes"
  assert res.body =~ "<td>A long note, about many things, that…</td>"
end
```

Every helper is available in every template, whichever file it's in. An
app with only a few can keep them in one module, `Helpers` in
`src/helpers.glacier`, in place of the folder. Controllers and other code
call them through their module: `NotesHelper.excerpt(&note.body)`.

An app helper named like one of Glacier's, its own `markdown` say, is the
one templates call.

### Pagination

`paginate(&page, total)` links to the other pages of a list split with
`glacier::pagination::Page`, as Kaminari's `paginate` does. The links
include previous and next, the first and last pages, and two either side
of the current one, with `…` for the gaps. They keep the request's other
query params. A list that fits on one page gets no links.
`notes/index.html` ends with it, below its table, and the action takes the
page from `?page=`:

```elixir
test "lists twenty notes a page" do
  for i in 1..=25 do
    insert(Note, title: format!("Note {i}"))
  end
  first = get "/notes"
  assert first.body =~ "Note 25"
  refute first.body =~ "Note 5<"
  assert first.body =~ "<a href=\"/notes?page=2\" rel=\"next\">Next ›</a>"
  second = get "/notes?page=2"
  assert second.body =~ "Note 5<"
  assert second.body =~ "<span class=\"current\" aria-current=\"page\">2</span>"
end
```

The links come in a `<nav class="pagination">`. The current page is a
`span.current`, a gap is a `span.gap`, and "Previous" or "Next" with no
page to go to is a `span.disabled`, for styling.

### Links

`link_to("Show", note)` links to the record's page, and
`link_to("Edit", edit_note_path(note.id))` to any path, as in
`notes/index.html` above. The docs index links to each page:

```html
{% props slugs: Array<&'static str> %}
<h1>Docs</h1>
<ul>
  {%- for slug in slugs %}
  <li>{{ link_to(slug, doc_path(slug)) }}</li>
  {%- end %}
</ul>
```

```elixir
test "links with link_to" do
  res = get "/docs"
  assert res.body =~ "<li><a href=\"/docs/routing\">routing</a></li>"
end
```

For a link with attributes, such as a class, write the `<a>` tag yourself
with a path helper in its `href`, as `notes/show.html` does.

### Forms (updated)

`form_for(&note)` opens the right form for a record: a `POST` to `/notes` for a new note, and a `PATCH`
to `/notes/7` for a saved one. `form_tag(path, method)` does the same for
any path. Both add the CSRF token (except to a `get` form), and, for
`patch`, `put` and `delete`, a hidden `_method` field, since forms can only send `GET` and `POST` (see
[Routing](routing.html#method-override)). Both write only the opening
tag, so close the form with `</form>`:

```html
{% props note: Note %}
{{ form_for(&note) }}
  <.input field={note.field("title")} />
  <.input field={note.field("body")} type="textarea" />
  <.button>Save</.button>
</form>
```

```elixir
test "opens the right form for a new note and a saved one" do
  res = get "/notes/new"
  assert res.body =~ "<form method=\"post\" action=\"/notes\"><input type=\"hidden\" name=\"_csrf_token\""
  note = insert(Note)
  res = get edit_note_path(note.id)
  assert res.body =~ format!("<form method=\"post\" action=\"/notes/{}\">", note.id)
  assert res.body =~ "<input type=\"hidden\" name=\"_method\" value=\"patch\">"
end
```

The fields are `<.input>` components. `note.field("title")` gives what
an input needs, as Phoenix's `form[:title]` does: the param's name, an id
for the label, the value to show (what was typed, if the note failed to
save) and the field's errors. The `input` component draws a label, the
input (`type="textarea"` for a text area, any other `type` for an
`<input>`) and the errors under it:

```html
{% props field: Field, r#type: String = "text", label: String = "", inner_block: Slot = Slot::default() %}
<p class="field">
  <label for="{{ field.id }}">{% if label.is_empty() %}{{ field.label }}{% else %}{{ label }}{% end %}</label>
  {%- if r#type == "textarea" %}
  <textarea id="{{ field.id }}" name="{{ field.name }}">{{ field.value }}</textarea>
  {%- else %}
  <input type="{{ r#type }}" id="{{ field.id }}" name="{{ field.name }}" value="{{ field.value }}">
  {%- end %}
  {{ inner_block }}
  {%- for message in field.errors.iter() %}
  <small class="error">{{ field.label }} {{ message }}</small>
  {%- end %}
</p>
```

```elixir
test "draws each field with its label and value" do
  note = insert(Note, title: "Shopping", body: "Eggs")
  body = get(edit_note_path(note.id)).body
  assert body =~ "<label for=\"title\">Title</label>"
  assert body =~ "<input type=\"text\" id=\"title\" name=\"title\" value=\"Shopping\">"
  assert body =~ "<textarea id=\"body\" name=\"body\">Eggs</textarea>"
end
```

A changeset has `field` too, for a form re-shown from one.
`note.text("title")` and `note.errors_on("title")`, the parts `field`
gathers, are covered in [Models](models.html), for a field written by
hand.

```elixir
test "shows a form's errors" do
  res = post "/notes", params_for(Note), title: ""
  assert res.body =~ "<small class=\"error\">Title can&#39;t be blank</small>"
end
```

`form_tag` builds the delete button on `notes/show.html` and the settings
form, whose field names nest under `settings` for `require` (see
[Controllers](controllers.html#nested-params-with-require)):

```html
{% props theme: String %}
<h1>Settings</h1>

{{ form_tag(settings_path(), "patch") }}
  <.select field={Field::named("settings[theme]", &theme)} options={options([("light", "Light"), ("dark", "Dark")])} />
  <button>Save</button>
</form>
```

Neither helper sets an `enctype`, which a form with a file field needs;
[File Uploads](file_uploads.html) covers those forms.

NOTE: There's no `select` component or helpers for formatting numbers,
dates or text yet. Write the HTML, and use Rust's formatting where you
need it.

## Translations (new)

An app in more than one language keeps its text in
`config/locales/<locale>.yml`, as Rails does, compiled into the app. The
shop sells in English and French:

```yaml
en:
  shop:
    title: Shop
    products:
      zero: Nothing on sale yet
      one: "%{count} product on sale"
      other: "%{count} products on sale"
    receipt_subject: "Your receipt for %{product}"
```

```yaml
fr:
  shop:
    title: Boutique
    products:
      one: "%{count} produit en vente"
      other: "%{count} produits en vente"
    receipt_subject: "Votre reçu pour %{product}"
```

`t("shop.title")` is the text for the request's locale, and
`t_count("shop.products", n)` picks the plural form for `n`, filling in
`%{count}`:

```html
<h1>{{ t("shop.title") }}</h1>
<p>{{ t_count("shop.products", products.len() as i64) }}</p>
```

The form comes from the locale's rules: `one` and `other` in English and
French (where 0 is `one` too), `one`, `few` and `many` in Russian or
Polish, a single `other` in Japanese, and so on. A `zero` form, when
there is one, is used for 0 in any language. In `.glacier`, `t` takes the
count and any other `%{name}` by name: `t("inbox", count: n, name:
user.name)`. A missing translation reads `Translation missing:
fr.shop.title`.

### The Request's Locale

The `Locales` middleware picks each request's locale from those the app
has, the first being the default:

```glacier
# English unless the browser (or `?locale=fr`) asks for French.
middleware Locales.new(["en", "fr"])
```

It takes, in order, a `locale` param (`?locale=fr`, which it remembers
in a `locale` cookie, so a language switcher is a link), that cookie,
and the best match in the browser's `Accept-Language` (`fr-CA` matches
`fr`). An unknown `?locale=` is a `400 Bad Request`. Where a signed-in
user has a language of their own, `set_locale(&user.locale)` in a
middleware or action sets it for the rest of the request. `locale()`
reads it anywhere, to scope a query, say.

```glacier
test "speaks the browser's language" do
  assert get("/").body =~ "Nothing on sale yet"
  insert(Product, name: "Field Guide")
  assert get("/").body =~ "1 product on sale"

  glacier::testing::header("accept-language", "fr-FR,fr;q=0.9,en;q=0.5")
  res = get "/"
  assert res.body =~ "Boutique"
  assert res.body =~ "1 produit en vente"
end

test "keeps a locale asked for" do
  insert_list(Product, 2)
  assert get("/?locale=fr").body =~ "2 produits en vente"
  # Remembered in a cookie, for the pages after.
  assert get("/").body =~ "2 produits en vente"
end
```

## Inline Templates and Rust

### `html!`

`html!("…")` compiles a template written inline in Rust or `.glacier`. It
uses the same syntax, sees the local variables where it's written, and
gives a `Rendered`, which `.into_response()` turns into an HTML response:

```elixir
def show(ctx) do
  path = ctx.param("path").unwrap_or_default()
  html!("<p>You asked for {{ path }}</p>").into_response()
end
```

It escapes like any template:

```elixir
test "escapes values in html!" do
  res = get "/attachments/%3Cb%3E"
  assert res.body == "<p>You asked for &lt;b&gt;</p>"
end
```

### `#[derive(Template)]`

The structs `views!()` makes are ordinary Rust structs with
`#[derive(glacier::view::Template)]` and
`#[template(path = "notes/index.html")]`. You can write such a struct
yourself in Rust, with `path = …` for a file under `src/views/` or
`source = "…"` for a template in a string. Its fields are the template's
values, and `.render()` renders it.

## Checked When the App Compiles

Templates are compiled with the app, so mistakes are build errors. A
template that can't be parsed names the file and line:

```text
error: src/views/notes/show.html:2: unclosed `{% if %}`; add `{% end %}`
  --> src/lib.rs:3:1
   |
3 | glacier::app!();
  | ^^^^^^^^^^^^^^^^
```

A mistake in a Rust expression, such as a field the model doesn't have or
a path helper that doesn't exist, is reported by the Rust compiler. Its
message names the missing field or function, but points at the
`glacier::app!()` line in `src/lib.rs` rather than at the template;
[Routing](routing.html#path-helpers) shows one.

## Changing Templates Under `glacier server` (new)

`glacier server` rebuilds and restarts the app when a file under `src/`
or `config/` changes. Most template edits don't need that. A template is
compiled into its code (expressions, `if`s, loops, components and their
attributes) and the text around it. When only the text changes (markup,
classes, wording), the running app reads the new text from the file at
its next render, so there's no build:

```text
[glacier] reloaded src/views/pages/home.html (no rebuild needed)
```

Change an expression, a condition or a component's attributes and it
rebuilds as before, keeping the previous version running until the new
one is up:

```text
[glacier] changed: src/views/pages/home.html
[glacier] restarted in 2.1s
```

Either way the page in the browser reloads itself. Under `glacier
server`, HTML pages get a small script that waits on
`/__glacier/reload` and reloads the page when a template's text or a
file in `public/` changes, and when the server comes back after a
restart. When the app uses Tailwind, a template edit also rebuilds the
stylesheet, so a new class works straight away.

This applies only to debug builds run by `glacier server`. Tests and
release builds use the compiled text, and pages get no script.

## PDFs

`glacier::pdf::render_template` prints a template to a PDF with headless
Chrome, as Rails apps do with Grover or a Puppeteer service.
`Response::download` sends it to be saved, like Rails' `send_data`. The
`services` app prints a paid order's receipt:

```elixir
# GET /orders/:id/receipt.pdf: a paid order's receipt, printed from the
# orders/receipt.html template by headless Chrome.
def receipt do
  order = Order.find(params[:id])!
  if not order.status.is_paid() do
    return head :not_found
  end
  filename = "receipt-#{order.id}.pdf"
  options = glacier::pdf::PdfOptions.a4().footer(r#"<div style="font-size:8px; margin: 0 10mm">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>"#)
  pdf = glacier::pdf::render_template(Receipt { order: order }, &options)!
  Response.download(pdf, &filename)
end
```

The template is printed on its own, without the app's layout, so
`orders/receipt.html` is a whole page with its own `<style>`.
`PdfOptions` sets the rest:

- `a4()` and `letter()` set the paper, with backgrounds printed and 10 mm
  margins. `.landscape()` turns the page and `.margins(top, right, bottom,
  left)` takes millimetres.
- `.header(html)` and `.footer(html)` repeat on every page. Chrome fills
  elements with the classes `pageNumber`, `totalPages`, `date` and
  `title`. Leave room for them with the margins, and give them a font
  size, as Chrome's default is tiny.
- `.allow(prefix)` lets the page load from URLs starting with `prefix`.

Each PDF starts its own Chrome process (the one at `CHROME_PATH`, or
where Chrome or Chromium is usually installed), so expect a second or two
each. Print in a [background job](background_jobs.html) when that's too
long for a request. The page runs with JavaScript off, and every request
it makes fails unless it's for a `data:` URL or an allowed prefix, so HTML
built from user input can't reach the network or the machine's own
services. Put images and fonts inline with
`glacier::pdf::data_url("image/png", &bytes)`; printing waits for the
page to load, so they're drawn.

In tests nothing is printed. Each render answers with a small placeholder
PDF, and `rendered_pdfs()` returns the HTML and options it was given:

```elixir
test "a paid order's receipt is a PDF" do
  order = insert(Order, price_id: "price_basic")
  assert get(format!("/orders/{}/receipt.pdf", order.id)).status == :not_found

  order.status = OrderStatus::Paid
  order.save()!
  res = get format!("/orders/{}/receipt.pdf", order.id)
  assert res.status == :ok
  assert res.header("content-type") == Some("application/pdf")
  assert res.header("content-disposition").unwrap() =~ format!("attachment; filename=\"receipt-{}.pdf\"", order.id)
  # Nothing is printed in a test: the HTML is kept to check instead.
  printed = rendered_pdfs()
  assert printed.len() == 1
  assert printed[0].html =~ "paid by ada@example.com"
  assert printed[0].options.footer.as_deref().unwrap() =~ "totalPages"
end
```

`Response::inline(bytes, name)` sends a file for the browser to show,
such as a PDF in its viewer, instead of saving it.

## Styling with Tailwind (new)

A new app is styled with [Tailwind CSS](https://tailwindcss.com) as
well as the component library: templates use Tailwind's classes
(`class="flex gap-2"`) for their own layout, and the build turns
`assets/app.css` into `public/assets/app.css`, with only the classes the
app's templates use. The `ui` app's is what `glacier new` writes:

```css
/* The app's stylesheet: Tailwind CSS (https://tailwindcss.com), built
   into public/assets/app.css by every build. The colours, fonts and radii
   are the component library's, from public/ui/tokens.css: change them
   there, and `bg-primary`, `text-muted-foreground` and the rest follow. */
@import "tailwindcss" source("../src");

/* Tailwind's `font-sans` and `rounded-md` read the same names, which
   tokens.css sets after this file. */
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-card-foreground: var(--card-foreground);
  --color-popover: var(--popover);
  --color-popover-foreground: var(--popover-foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-secondary: var(--secondary);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-accent: var(--accent);
  --color-accent-foreground: var(--accent-foreground);
  --color-destructive: var(--destructive);
  --color-success: var(--success);
  --color-warning: var(--warning);
  --color-info: var(--info);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
}

/* Plain HTML looks right too: some generated pages (sign-in, the
   scaffold's simple forms) use bare elements. */
@layer base {
  body {
    @apply mx-auto my-16 max-w-2xl bg-background px-4 font-sans text-foreground;
  }
  h1 {
    @apply mb-4 text-3xl font-extrabold tracking-tight;
  }
  h2 {
    @apply mt-8 mb-3 text-xl font-semibold tracking-tight;
  }
  p,
  form,
  ul,
  table {
    @apply my-3;
  }
  a {
    @apply text-primary underline underline-offset-4;
  }
  label {
    @apply text-sm font-medium;
  }
  input:not([type="checkbox"], [type="radio"], [type="hidden"]):not([class]),
  textarea:not([class]),
  select:not([class]) {
    @apply mt-1 block h-9 w-full rounded-md border border-input bg-transparent px-3 text-sm;
  }
  button:not([class]) {
    @apply h-9 cursor-pointer rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground;
  }
  code {
    @apply rounded-sm bg-muted px-1 font-mono text-sm;
  }
  .error {
    @apply mt-1 block text-sm text-destructive;
  }
}
```

`@import "tailwindcss" source("../src")` brings in Tailwind and has it
look for classes in `src/`, in templates, components and `.glacier`
files alike. `@theme inline` maps Tailwind's colours to the library's
tokens, so `bg-primary`, `text-muted-foreground` and `border-border`
follow `public/ui/tokens.css`, dark mode included. The `@layer base`
rules style plain HTML, so the generated sign-in pages and simple forms,
which use bare `<input>`s and `<button>`s, look right without classes.

There's no Node and nothing to install. The build uses Tailwind's
standalone program, a pinned release that Glacier downloads the first
time into `~/.cache/glacier/` and checks against its known SHA-256
before running it. Every build builds the stylesheet, `cargo build`,
`cargo test` and `glacier server` alike, so it follows the templates as
you change them, and release builds minify it and compile it into the
binary with the rest of `public/`. `public/assets/` is in `.gitignore`:
it's built, never edited.

To use a Tailwind of your own (offline, or another version), set
`GLACIER_TAILWIND_PATH` to its program. An app without `assets/app.css`
doesn't use Tailwind: `glacier new --no-tailwind` makes one with a plain
`public/app.css` (the component library is the same), and apps made
before 0.5 keep theirs. To move one over, add `assets/app.css`, link
`/assets/app.css` in the layout, and add `/public/assets/` to
`.gitignore`.

## JavaScript with Vite (new)

An app whose JavaScript needs npm packages or bundling can use
[Vite](https://vite.dev). Node is needed for this, and only for this.
`glacier new --vite` sets it up: a `package.json`, a `vite.config.js`,
`assets/js/app.js` as the entry point, and one line in the layout's
`<head>`:

```html
{{ vite("assets/js/app.js") }}
```

Run `npm install` once, then `glacier server` starts Vite's dev server
beside the app (on port 5173, or `GLACIER_VITE_PORT`). The `vite` helper
then links the entry from it, with Vite's client, so changes to scripts
and styles it bundles apply in place, without a reload:

```html
<script type="module" src="http://localhost:5173/@vite/client"></script>
<script type="module" src="http://localhost:5173/assets/js/app.js"></script>
```

A release build (`cargo build --release`, or `glacier build --release`)
runs `vite build` first. It writes hashed files and a manifest to
`public/build/`, and they're compiled into the binary with the rest of
`public/`. `vite(…)` then reads the manifest and links the built files,
with the entry's CSS and its imported modules preloaded, as Rails' and
Laravel's Vite helpers do:

```html
<link rel="stylesheet" href="/build/assets/app-5UjPuW-k.css">
<link rel="modulepreload" href="/build/assets/shared-B7PI925R.js">
<script type="module" src="/build/assets/app-4ed993c7.js"></script>
```

`vite(["assets/js/app.js", "assets/css/print.css"])` links several
entries; each must be in `vite.config.js`'s `input`. An entry missing
from the manifest, or a missing manifest, is logged and leaves an HTML
comment in its place, so the page still renders. `node_modules/` and
`public/build/` are in the generated `.gitignore`.

Vite is optional. Tailwind needs no Node, and without Vite an app's own
scripts go in `public/` and are linked directly, as the component
library's `/ui/ui.js` is. Modern browsers load
`<script type="module">` and its `import`s without a bundler.

## Static Files

Files in `public/` are served at the root of the site, so
`public/app.css` is `/app.css` (the notes app's plain stylesheet) and
`public/assets/app.css` is `/assets/app.css`. An app with a `public/` folder serves it
without a line of its own, after the middleware in its settings
(`src/app.glacier`):

```glacier
# The app. Its config, database, migrations, seeds, jobs, static files and
# routes are added for you; these are the settings of its own.
app do
  alias glacier.middleware.MethodOverride
  alias glacier.middleware.Logger

  # Before the logger, so logs show the overridden method.
  middleware MethodOverride
  middleware Logger
end
```

```elixir
test "serves files from public/" do
  res = get "/app.css"
  assert res.status == :ok
  assert res.headers["content-type"] == "text/css; charset=utf-8"
end
```

`glacier::static_files!("public")` serves the folder from disk in debug
builds. In release builds it compiles every file in it into the binary
(skipping names that start with a dot), so a deploy is one file. Either
way, it answers only `GET` and `HEAD`, sets an `ETag` so browsers can
revalidate with `304 Not Modified`, and passes any request that doesn't
name a file on to the router.