Skip to content

Commit 98abb22

Browse files
authored
Merge pull request #1368 from notriddle/hash-files
feat(html): cache bust static files by adding hashes to file names
2 parents 4941acd + ab304e7 commit 98abb22

File tree

18 files changed

+557
-250
lines changed

18 files changed

+557
-250
lines changed

Cargo.lock

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ clap_complete = "4.3.2"
2727
once_cell = "1.17.1"
2828
env_logger = "0.11.1"
2929
handlebars = "6.0"
30+
hex = "0.4.3"
3031
log = "0.4.17"
3132
memchr = "2.5.0"
3233
opener = "0.7.0"
3334
pulldown-cmark = { version = "0.10.0", default-features = false, features = ["html"] } # Do not update, part of the public api.
3435
regex = "1.8.1"
3536
serde = { version = "1.0.163", features = ["derive"] }
3637
serde_json = "1.0.96"
38+
sha2 = "0.10.8"
3739
shlex = "1.3.0"
3840
tempfile = "3.4.0"
3941
toml = "0.5.11" # Do not update, see https://github.com/rust-lang/mdBook/issues/2037

guide/book.toml

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mathjax-support = true
1313
site-url = "/mdBook/"
1414
git-repository-url = "https://github.com/rust-lang/mdBook/tree/master/guide"
1515
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
16+
hash-files = true
1617

1718
[output.html.playground]
1819
editable = true

guide/src/format/configuration/renderers.md

+6
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ The following configuration options are available:
168168
This string will be written to a file named CNAME in the root of your site, as
169169
required by GitHub Pages (see [*Managing a custom domain for your GitHub Pages
170170
site*][custom domain]).
171+
- **hash-files:** Include a cryptographic "fingerprint" of the files' contents in static asset filenames,
172+
so that if the contents of the file are changed, the name of the file will also change.
173+
For example, `css/chrome.css` may become `css/chrome-9b8f428e.css`.
174+
Chapter HTML files are not renamed.
175+
Static CSS and JS files can reference each other using `{{ resource "filename" }}` directives.
176+
Defaults to `false` (in a future release, this may change to `true`).
171177

172178
[custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
173179

guide/src/format/theme/index-hbs.md

+10
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,13 @@ Of course the inner html can be changed to your liking.
9999

100100
*If you would like other properties or helpers exposed, please [create a new
101101
issue](https://github.com/rust-lang/mdBook/issues)*
102+
103+
### 3. resource
104+
105+
The path to a static file.
106+
It implicitly includes `path_to_root`,
107+
and accounts for files that are renamed with a hash in their filename.
108+
109+
```handlebars
110+
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
111+
```

src/config.rs

+4
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,9 @@ pub struct HtmlConfig {
587587
/// The mapping from old pages to new pages/URLs to use when generating
588588
/// redirects.
589589
pub redirect: HashMap<String, String>,
590+
/// If this option is turned on, "cache bust" static files by adding
591+
/// hashes to their file names.
592+
pub hash_files: bool,
590593
}
591594

592595
impl Default for HtmlConfig {
@@ -616,6 +619,7 @@ impl Default for HtmlConfig {
616619
cname: None,
617620
live_reload_endpoint: None,
618621
redirect: HashMap::new(),
622+
hash_files: false,
619623
}
620624
}
621625
}

src/renderer/html_handlebars/hbs_renderer.rs

+53-193
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ use crate::book::{Book, BookItem};
22
use crate::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition};
33
use crate::errors::*;
44
use crate::renderer::html_handlebars::helpers;
5+
use crate::renderer::html_handlebars::StaticFiles;
56
use crate::renderer::{RenderContext, Renderer};
6-
use crate::theme::{self, playground_editor, Theme};
7+
use crate::theme::{self, Theme};
78
use crate::utils;
89

910
use std::borrow::Cow;
@@ -222,134 +223,6 @@ impl HtmlHandlebars {
222223
rendered
223224
}
224225

225-
fn copy_static_files(
226-
&self,
227-
destination: &Path,
228-
theme: &Theme,
229-
html_config: &HtmlConfig,
230-
) -> Result<()> {
231-
use crate::utils::fs::write_file;
232-
233-
write_file(
234-
destination,
235-
".nojekyll",
236-
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
237-
)?;
238-
239-
if let Some(cname) = &html_config.cname {
240-
write_file(destination, "CNAME", format!("{cname}\n").as_bytes())?;
241-
}
242-
243-
write_file(destination, "book.js", &theme.js)?;
244-
write_file(destination, "css/general.css", &theme.general_css)?;
245-
write_file(destination, "css/chrome.css", &theme.chrome_css)?;
246-
if html_config.print.enable {
247-
write_file(destination, "css/print.css", &theme.print_css)?;
248-
}
249-
write_file(destination, "css/variables.css", &theme.variables_css)?;
250-
if let Some(contents) = &theme.favicon_png {
251-
write_file(destination, "favicon.png", contents)?;
252-
}
253-
if let Some(contents) = &theme.favicon_svg {
254-
write_file(destination, "favicon.svg", contents)?;
255-
}
256-
write_file(destination, "highlight.css", &theme.highlight_css)?;
257-
write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
258-
write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
259-
write_file(destination, "highlight.js", &theme.highlight_js)?;
260-
write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
261-
write_file(
262-
destination,
263-
"FontAwesome/css/font-awesome.css",
264-
theme::FONT_AWESOME,
265-
)?;
266-
write_file(
267-
destination,
268-
"FontAwesome/fonts/fontawesome-webfont.eot",
269-
theme::FONT_AWESOME_EOT,
270-
)?;
271-
write_file(
272-
destination,
273-
"FontAwesome/fonts/fontawesome-webfont.svg",
274-
theme::FONT_AWESOME_SVG,
275-
)?;
276-
write_file(
277-
destination,
278-
"FontAwesome/fonts/fontawesome-webfont.ttf",
279-
theme::FONT_AWESOME_TTF,
280-
)?;
281-
write_file(
282-
destination,
283-
"FontAwesome/fonts/fontawesome-webfont.woff",
284-
theme::FONT_AWESOME_WOFF,
285-
)?;
286-
write_file(
287-
destination,
288-
"FontAwesome/fonts/fontawesome-webfont.woff2",
289-
theme::FONT_AWESOME_WOFF2,
290-
)?;
291-
write_file(
292-
destination,
293-
"FontAwesome/fonts/FontAwesome.ttf",
294-
theme::FONT_AWESOME_TTF,
295-
)?;
296-
// Don't copy the stock fonts if the user has specified their own fonts to use.
297-
if html_config.copy_fonts && theme.fonts_css.is_none() {
298-
write_file(destination, "fonts/fonts.css", theme::fonts::CSS)?;
299-
for (file_name, contents) in theme::fonts::LICENSES.iter() {
300-
write_file(destination, file_name, contents)?;
301-
}
302-
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
303-
write_file(destination, file_name, contents)?;
304-
}
305-
write_file(
306-
destination,
307-
theme::fonts::SOURCE_CODE_PRO.0,
308-
theme::fonts::SOURCE_CODE_PRO.1,
309-
)?;
310-
}
311-
if let Some(fonts_css) = &theme.fonts_css {
312-
if !fonts_css.is_empty() {
313-
write_file(destination, "fonts/fonts.css", fonts_css)?;
314-
}
315-
}
316-
if !html_config.copy_fonts && theme.fonts_css.is_none() {
317-
warn!(
318-
"output.html.copy-fonts is deprecated.\n\
319-
This book appears to have copy-fonts=false in book.toml without a fonts.css file.\n\
320-
Add an empty `theme/fonts/fonts.css` file to squelch this warning."
321-
);
322-
}
323-
for font_file in &theme.font_files {
324-
let contents = fs::read(font_file)?;
325-
let filename = font_file.file_name().unwrap();
326-
let filename = Path::new("fonts").join(filename);
327-
write_file(destination, filename, &contents)?;
328-
}
329-
330-
let playground_config = &html_config.playground;
331-
332-
// Ace is a very large dependency, so only load it when requested
333-
if playground_config.editable && playground_config.copy_js {
334-
// Load the editor
335-
write_file(destination, "editor.js", playground_editor::JS)?;
336-
write_file(destination, "ace.js", playground_editor::ACE_JS)?;
337-
write_file(destination, "mode-rust.js", playground_editor::MODE_RUST_JS)?;
338-
write_file(
339-
destination,
340-
"theme-dawn.js",
341-
playground_editor::THEME_DAWN_JS,
342-
)?;
343-
write_file(
344-
destination,
345-
"theme-tomorrow_night.js",
346-
playground_editor::THEME_TOMORROW_NIGHT_JS,
347-
)?;
348-
}
349-
350-
Ok(())
351-
}
352-
353226
/// Update the context with data for this file
354227
fn configure_print_version(
355228
&self,
@@ -381,43 +254,6 @@ impl HtmlHandlebars {
381254
handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
382255
}
383256

384-
/// Copy across any additional CSS and JavaScript files which the book
385-
/// has been configured to use.
386-
fn copy_additional_css_and_js(
387-
&self,
388-
html: &HtmlConfig,
389-
root: &Path,
390-
destination: &Path,
391-
) -> Result<()> {
392-
let custom_files = html.additional_css.iter().chain(html.additional_js.iter());
393-
394-
debug!("Copying additional CSS and JS");
395-
396-
for custom_file in custom_files {
397-
let input_location = root.join(custom_file);
398-
let output_location = destination.join(custom_file);
399-
if let Some(parent) = output_location.parent() {
400-
fs::create_dir_all(parent)
401-
.with_context(|| format!("Unable to create {}", parent.display()))?;
402-
}
403-
debug!(
404-
"Copying {} -> {}",
405-
input_location.display(),
406-
output_location.display()
407-
);
408-
409-
fs::copy(&input_location, &output_location).with_context(|| {
410-
format!(
411-
"Unable to copy {} to {}",
412-
input_location.display(),
413-
output_location.display()
414-
)
415-
})?;
416-
}
417-
418-
Ok(())
419-
}
420-
421257
fn emit_redirects(
422258
&self,
423259
root: &Path,
@@ -544,6 +380,57 @@ impl Renderer for HtmlHandlebars {
544380
fs::create_dir_all(destination)
545381
.with_context(|| "Unexpected error when constructing destination path")?;
546382

383+
let mut static_files = StaticFiles::new(&theme, &html_config, &ctx.root)?;
384+
385+
// Render search index
386+
#[cfg(feature = "search")]
387+
{
388+
let default = crate::config::Search::default();
389+
let search = html_config.search.as_ref().unwrap_or(&default);
390+
if search.enable {
391+
super::search::create_files(&search, &mut static_files, &book)?;
392+
}
393+
}
394+
395+
debug!("Render toc js");
396+
{
397+
let rendered_toc = handlebars.render("toc_js", &data)?;
398+
static_files.add_builtin("toc.js", rendered_toc.as_bytes());
399+
debug!("Creating toc.js ✓");
400+
}
401+
402+
if html_config.hash_files {
403+
static_files.hash_files()?;
404+
}
405+
406+
debug!("Copy static files");
407+
let resource_helper = static_files
408+
.write_files(&destination)
409+
.with_context(|| "Unable to copy across static files")?;
410+
411+
handlebars.register_helper("resource", Box::new(resource_helper));
412+
413+
debug!("Render toc html");
414+
{
415+
data.insert("is_toc_html".to_owned(), json!(true));
416+
data.insert("path".to_owned(), json!("toc.html"));
417+
let rendered_toc = handlebars.render("toc_html", &data)?;
418+
utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?;
419+
debug!("Creating toc.html ✓");
420+
data.remove("path");
421+
data.remove("is_toc_html");
422+
}
423+
424+
utils::fs::write_file(
425+
destination,
426+
".nojekyll",
427+
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
428+
)?;
429+
430+
if let Some(cname) = &html_config.cname {
431+
utils::fs::write_file(destination, "CNAME", format!("{cname}\n").as_bytes())?;
432+
}
433+
547434
let mut is_index = true;
548435
for item in book.iter() {
549436
let ctx = RenderItemContext {
@@ -588,33 +475,6 @@ impl Renderer for HtmlHandlebars {
588475
debug!("Creating print.html ✓");
589476
}
590477

591-
debug!("Render toc");
592-
{
593-
let rendered_toc = handlebars.render("toc_js", &data)?;
594-
utils::fs::write_file(destination, "toc.js", rendered_toc.as_bytes())?;
595-
debug!("Creating toc.js ✓");
596-
data.insert("is_toc_html".to_owned(), json!(true));
597-
let rendered_toc = handlebars.render("toc_html", &data)?;
598-
utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?;
599-
debug!("Creating toc.html ✓");
600-
data.remove("is_toc_html");
601-
}
602-
603-
debug!("Copy static files");
604-
self.copy_static_files(destination, &theme, &html_config)
605-
.with_context(|| "Unable to copy across static files")?;
606-
self.copy_additional_css_and_js(&html_config, &ctx.root, destination)
607-
.with_context(|| "Unable to copy across additional CSS and JS")?;
608-
609-
// Render search index
610-
#[cfg(feature = "search")]
611-
{
612-
let search = html_config.search.unwrap_or_default();
613-
if search.enable {
614-
super::search::create_files(&search, destination, book)?;
615-
}
616-
}
617-
618478
self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)
619479
.context("Unable to emit redirects")?;
620480

Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod navigation;
2+
pub mod resources;
23
pub mod theme;
34
pub mod toc;

0 commit comments

Comments
 (0)