Hashets (a portmanteau of 'hash' and 'assets') is a utility for handling cache busting of static assets. It works by adding the hash of the file's contents to the file name.
- ⚡ Three options:
- Either generate files with hashed names before compiling,
- use
hashets.HashToDir
orhashets.HashToTempDir
at runtime, - or create a
hashets.FSWrapper
which translates requests for hashed file names to their original names.
- 🧒 Easy integration into templates by using a map of file names to hashed file names
- 📦 Support for
fs.FS
- 🏖 Hassle-free versioning, that only causes refetching of files when their contents change (vs.
?v=1.2.3
)
First impressions matter, so here are some examples of how to use hashets.
This method is for you, if:
- 🧒 You want the easiest solution of all
- 🤏 Have small assets, or you don't mind if your application takes a few milliseconds longer to start
- 🕚 You know your assets at runtime
- 🕵 You need cache busting during development and not just in production
hashets.WrapFS
simply wraps an fs.FS
, calculates the hashes of all its files,
and translates requests for hashed file names to their original names:
Add a static.go
to your static
directory:
static
├── file_to_hash.ext
└── static.go
package static
import (
"embed"
"github.com/mavolin/hashets/hashets"
)
//go:embed file_to_hash.ext
var assets embed.FS
var (
FS *hashets.FSWrapper
FileNames hashets.Map
)
func init() {
var err error
FS, FileNames, err = hashets.WrapFS(assets, hashets.Options{})
if err != nil {
panic(err)
}
}
FS
now translates requests for FS.Open("file_to_hash_generateHash.ext")
to assets.Open("file_to_hash.ext")
.
Additionally, FileNames
maps all original file names to their hashed equivalents:
var FileNames = hashets.Map{
"file_to_hash.ext": "file_to_hash_generateHash.ext",
}
Use this map in your templates to generate links to your assets:
<link rel="stylesheet" href="/static/{{ .FileNames.Get "file_to_hash.ext" }}">
Then simply serve FS
under /static
:
http.Handle("/static/", http.FileServer(http.FS(FS)))
Of course, instead of an embed.FS
, you can also use any other fs.FS
implementation, such as os.DirFS
, etc.
This method is for you, if:
- 📏 You have larger assets and need lightning fast startup times
- 🕑 You know your assets at compile time
- 🕵 You need cache busting during development and not just in production
Add a static.go
to your static
directory:
static
├── orig
│ └── file_to_hash.ext
└── static.go
package static
import "static/hashed"
//go:generate hashets -o hashed orig
Now run go generate
.
Your file structure should now look like this:
static
├── hashed
│ ├── file_to_hash_generateHash.ext
│ └── hashets_map.go
├── orig
│ └── file_to_hash.ext
└── static.go
Besides the hashed files, hashets
also generated a hashets_map.go
file,
that contains the FileNames
hashets.Map
, that maps the original file names
to their hashed equivalents:
package hashed
import "github.com/mavolin/hashets/hashets"
var FileNames = hashets.Map{
"file_to_hash.ext": "file_to_hash_generateHash.ext",
}
This method is for you, if:
- 📏 You have larger assets and need lightning fast startup times
- 🕑 You know your assets at compile time
- 🤷 You don't need cache busting during development
The go generate
solution has one big drawback:
If you generate static assets in the same go generate
run and hashets
is
executed before the files are generated, the hashes will be wrong.
Luckily, there is another handy solution:
Add a static.go
and a hashets_map.go
to your static
directory:
static
├── file_to_hash.ext
├── hashets_map.go
└── static.go
static.go
package static
import "embed"
// It is important that you use wildcards for your files, as otherwise the
// hashed files generated by your CI won't be included in the embed.FS.
//go:embed file_to_hash*.txt
var FS embed.FS
hashets_map.go
package static
import "github.com/mavolin/hashets"
// FileNames maps the original file names to their hashed equivalents.
// Unless you run hashets, this map will be nil, which causes [hashets.Map.Get]
// to behave specially:
// Instead of returning the hashed file name, it will return the path that it
// is given as-is.
//
// That means, unless you run hashets, you will simply use your unhashed assets.
var FileNames hashets.Map
Now, in your CI, run hashets
before compiling:
hashets -replace -ignore static.go static
This will replace all of your assets with their hashed equivalents, i.e.
replace file_to_hash.ext
with file_to_hash_generateHash.ext
.
Additionally, it will overwrite hashets_map.go
with a FileNames
map that
contains the correct mappings.
This method is for you, if:
- 🛠 You need maximum customizability
- 🤏 You have smaller assets or don't mind if your application takes a few milliseconds longer to start
- 🕚 You know your assets at compile or runtime
- 🕵 You need cache busting during development and not just in production
If all of the above don't do the trick for you, you can also create hashes
using hashets.HashToDir
and hashets.HashToTempDir
, which will generate
hashed files and write them to an arbitrary or a temporary directory.
Head over to pkg.go.dev to read more.
Built with ❤ by Maximilian von Lindern. Available under the MIT License.