Skip to content

feat(html): cache bust static files by adding hashes to file names #1368

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ clap_complete = "4.3.2"
once_cell = "1.17.1"
env_logger = "0.11.1"
handlebars = "6.0"
hex = "0.4.3"
log = "0.4.17"
memchr = "2.5.0"
opener = "0.7.0"
pulldown-cmark = { version = "0.10.0", default-features = false, features = ["html"] } # Do not update, part of the public api.
regex = "1.8.1"
serde = { version = "1.0.163", features = ["derive"] }
serde_json = "1.0.96"
sha2 = "0.10.8"
shlex = "1.3.0"
tempfile = "3.4.0"
toml = "0.5.11" # Do not update, see https://github.com/rust-lang/mdBook/issues/2037
Expand Down
1 change: 1 addition & 0 deletions guide/book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mathjax-support = true
site-url = "/mdBook/"
git-repository-url = "https://github.com/rust-lang/mdBook/tree/master/guide"
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
hash-files = true

[output.html.playground]
editable = true
Expand Down
6 changes: 6 additions & 0 deletions guide/src/format/configuration/renderers.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ The following configuration options are available:
This string will be written to a file named CNAME in the root of your site, as
required by GitHub Pages (see [*Managing a custom domain for your GitHub Pages
site*][custom domain]).
- **hash-files:** Include a cryptographic "fingerprint" of the files' contents in static asset filenames,
so that if the contents of the file are changed, the name of the file will also change.
For example, `css/chrome.css` may become `css/chrome-9b8f428e.css`.
Chapter HTML files are not renamed.
Static CSS and JS files can reference each other using `{{ resource "filename" }}` directives.
Defaults to `false` (in a future release, this may change to `true`).

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

Expand Down
10 changes: 10 additions & 0 deletions guide/src/format/theme/index-hbs.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,13 @@ Of course the inner html can be changed to your liking.

*If you would like other properties or helpers exposed, please [create a new
issue](https://github.com/rust-lang/mdBook/issues)*

### 3. resource

The path to a static file.
It implicitly includes `path_to_root`,
and accounts for files that are renamed with a hash in their filename.

```handlebars
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
```
4 changes: 4 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,9 @@ pub struct HtmlConfig {
/// The mapping from old pages to new pages/URLs to use when generating
/// redirects.
pub redirect: HashMap<String, String>,
/// If this option is turned on, "cache bust" static files by adding
/// hashes to their file names.
pub hash_files: bool,
}

impl Default for HtmlConfig {
Expand Down Expand Up @@ -616,6 +619,7 @@ impl Default for HtmlConfig {
cname: None,
live_reload_endpoint: None,
redirect: HashMap::new(),
hash_files: false,
}
}
}
Expand Down
246 changes: 53 additions & 193 deletions src/renderer/html_handlebars/hbs_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ use crate::book::{Book, BookItem};
use crate::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition};
use crate::errors::*;
use crate::renderer::html_handlebars::helpers;
use crate::renderer::html_handlebars::StaticFiles;
use crate::renderer::{RenderContext, Renderer};
use crate::theme::{self, playground_editor, Theme};
use crate::theme::{self, Theme};
use crate::utils;

use std::borrow::Cow;
Expand Down Expand Up @@ -222,134 +223,6 @@ impl HtmlHandlebars {
rendered
}

fn copy_static_files(
&self,
destination: &Path,
theme: &Theme,
html_config: &HtmlConfig,
) -> Result<()> {
use crate::utils::fs::write_file;

write_file(
destination,
".nojekyll",
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
)?;

if let Some(cname) = &html_config.cname {
write_file(destination, "CNAME", format!("{cname}\n").as_bytes())?;
}

write_file(destination, "book.js", &theme.js)?;
write_file(destination, "css/general.css", &theme.general_css)?;
write_file(destination, "css/chrome.css", &theme.chrome_css)?;
if html_config.print.enable {
write_file(destination, "css/print.css", &theme.print_css)?;
}
write_file(destination, "css/variables.css", &theme.variables_css)?;
if let Some(contents) = &theme.favicon_png {
write_file(destination, "favicon.png", contents)?;
}
if let Some(contents) = &theme.favicon_svg {
write_file(destination, "favicon.svg", contents)?;
}
write_file(destination, "highlight.css", &theme.highlight_css)?;
write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
write_file(destination, "highlight.js", &theme.highlight_js)?;
write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
write_file(
destination,
"FontAwesome/css/font-awesome.css",
theme::FONT_AWESOME,
)?;
write_file(
destination,
"FontAwesome/fonts/fontawesome-webfont.eot",
theme::FONT_AWESOME_EOT,
)?;
write_file(
destination,
"FontAwesome/fonts/fontawesome-webfont.svg",
theme::FONT_AWESOME_SVG,
)?;
write_file(
destination,
"FontAwesome/fonts/fontawesome-webfont.ttf",
theme::FONT_AWESOME_TTF,
)?;
write_file(
destination,
"FontAwesome/fonts/fontawesome-webfont.woff",
theme::FONT_AWESOME_WOFF,
)?;
write_file(
destination,
"FontAwesome/fonts/fontawesome-webfont.woff2",
theme::FONT_AWESOME_WOFF2,
)?;
write_file(
destination,
"FontAwesome/fonts/FontAwesome.ttf",
theme::FONT_AWESOME_TTF,
)?;
// Don't copy the stock fonts if the user has specified their own fonts to use.
if html_config.copy_fonts && theme.fonts_css.is_none() {
write_file(destination, "fonts/fonts.css", theme::fonts::CSS)?;
for (file_name, contents) in theme::fonts::LICENSES.iter() {
write_file(destination, file_name, contents)?;
}
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
write_file(destination, file_name, contents)?;
}
write_file(
destination,
theme::fonts::SOURCE_CODE_PRO.0,
theme::fonts::SOURCE_CODE_PRO.1,
)?;
}
if let Some(fonts_css) = &theme.fonts_css {
if !fonts_css.is_empty() {
write_file(destination, "fonts/fonts.css", fonts_css)?;
}
}
if !html_config.copy_fonts && theme.fonts_css.is_none() {
warn!(
"output.html.copy-fonts is deprecated.\n\
This book appears to have copy-fonts=false in book.toml without a fonts.css file.\n\
Add an empty `theme/fonts/fonts.css` file to squelch this warning."
);
}
for font_file in &theme.font_files {
let contents = fs::read(font_file)?;
let filename = font_file.file_name().unwrap();
let filename = Path::new("fonts").join(filename);
write_file(destination, filename, &contents)?;
}

let playground_config = &html_config.playground;

// Ace is a very large dependency, so only load it when requested
if playground_config.editable && playground_config.copy_js {
// Load the editor
write_file(destination, "editor.js", playground_editor::JS)?;
write_file(destination, "ace.js", playground_editor::ACE_JS)?;
write_file(destination, "mode-rust.js", playground_editor::MODE_RUST_JS)?;
write_file(
destination,
"theme-dawn.js",
playground_editor::THEME_DAWN_JS,
)?;
write_file(
destination,
"theme-tomorrow_night.js",
playground_editor::THEME_TOMORROW_NIGHT_JS,
)?;
}

Ok(())
}

/// Update the context with data for this file
fn configure_print_version(
&self,
Expand Down Expand Up @@ -381,43 +254,6 @@ impl HtmlHandlebars {
handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
}

/// Copy across any additional CSS and JavaScript files which the book
/// has been configured to use.
fn copy_additional_css_and_js(
&self,
html: &HtmlConfig,
root: &Path,
destination: &Path,
) -> Result<()> {
let custom_files = html.additional_css.iter().chain(html.additional_js.iter());

debug!("Copying additional CSS and JS");

for custom_file in custom_files {
let input_location = root.join(custom_file);
let output_location = destination.join(custom_file);
if let Some(parent) = output_location.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Unable to create {}", parent.display()))?;
}
debug!(
"Copying {} -> {}",
input_location.display(),
output_location.display()
);

fs::copy(&input_location, &output_location).with_context(|| {
format!(
"Unable to copy {} to {}",
input_location.display(),
output_location.display()
)
})?;
}

Ok(())
}

fn emit_redirects(
&self,
root: &Path,
Expand Down Expand Up @@ -544,6 +380,57 @@ impl Renderer for HtmlHandlebars {
fs::create_dir_all(destination)
.with_context(|| "Unexpected error when constructing destination path")?;

let mut static_files = StaticFiles::new(&theme, &html_config, &ctx.root)?;

// Render search index
#[cfg(feature = "search")]
{
let default = crate::config::Search::default();
let search = html_config.search.as_ref().unwrap_or(&default);
if search.enable {
super::search::create_files(&search, &mut static_files, &book)?;
}
}

debug!("Render toc js");
{
let rendered_toc = handlebars.render("toc_js", &data)?;
static_files.add_builtin("toc.js", rendered_toc.as_bytes());
debug!("Creating toc.js ✓");
}

if html_config.hash_files {
static_files.hash_files()?;
}

debug!("Copy static files");
let resource_helper = static_files
.write_files(&destination)
.with_context(|| "Unable to copy across static files")?;

handlebars.register_helper("resource", Box::new(resource_helper));

debug!("Render toc html");
{
data.insert("is_toc_html".to_owned(), json!(true));
data.insert("path".to_owned(), json!("toc.html"));
let rendered_toc = handlebars.render("toc_html", &data)?;
utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?;
debug!("Creating toc.html ✓");
data.remove("path");
data.remove("is_toc_html");
}

utils::fs::write_file(
destination,
".nojekyll",
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
)?;

if let Some(cname) = &html_config.cname {
utils::fs::write_file(destination, "CNAME", format!("{cname}\n").as_bytes())?;
}

let mut is_index = true;
for item in book.iter() {
let ctx = RenderItemContext {
Expand Down Expand Up @@ -588,33 +475,6 @@ impl Renderer for HtmlHandlebars {
debug!("Creating print.html ✓");
}

debug!("Render toc");
{
let rendered_toc = handlebars.render("toc_js", &data)?;
utils::fs::write_file(destination, "toc.js", rendered_toc.as_bytes())?;
debug!("Creating toc.js ✓");
data.insert("is_toc_html".to_owned(), json!(true));
let rendered_toc = handlebars.render("toc_html", &data)?;
utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?;
debug!("Creating toc.html ✓");
data.remove("is_toc_html");
}

debug!("Copy static files");
self.copy_static_files(destination, &theme, &html_config)
.with_context(|| "Unable to copy across static files")?;
self.copy_additional_css_and_js(&html_config, &ctx.root, destination)
.with_context(|| "Unable to copy across additional CSS and JS")?;

// Render search index
#[cfg(feature = "search")]
{
let search = html_config.search.unwrap_or_default();
if search.enable {
super::search::create_files(&search, destination, book)?;
}
}

self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)
.context("Unable to emit redirects")?;

Expand Down
1 change: 1 addition & 0 deletions src/renderer/html_handlebars/helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod navigation;
pub mod resources;
pub mod theme;
pub mod toc;
Loading