├── jekyll-workbox-plugin.gemspec ├── LICENSE ├── lib └── jekyll-workbox-plugin.rb └── README.md /jekyll-workbox-plugin.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'jekyll-workbox-plugin' 3 | s.version = '0.0.2' 4 | s.date = '2019-02-01' 5 | s.summary = 'Workbox support for Jekyll.' 6 | s.description = 'This plugin provides workbox support for Jekyll. Generate a service worker and provides precache with Google Workbox.' 7 | s.authors = ['Benedikt Meurer'] 8 | s.email = 'benedikt.meurer@googlemail.com' 9 | s.files = Dir['lib/*.rb'] 10 | s.homepage = 'https://github.com/bmeurer/jekyll-workbox-plugin' 11 | s.license = 'MIT' 12 | end 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Benedikt Meurer 4 | Copyright (c) 2017 Lavas 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/jekyll-workbox-plugin.rb: -------------------------------------------------------------------------------- 1 | class WorkboxHelper 2 | WORKBOX_VERSION = '3.6.3' 3 | 4 | def initialize(site, config) 5 | @site = site 6 | @config = config 7 | @sw_filename = @config['sw_dest_filename'] || 'sw.js' 8 | @sw_src_filepath = @config['sw_src_filepath'] || 'sw.js' 9 | end 10 | 11 | def generate_workbox_precache() 12 | directory = @config['precache_glob_directory'] || '/' 13 | directory = @site.in_dest_dir(directory) 14 | patterns = @config['precache_glob_patterns'] || ['**/*.{html,js,css,eot,svg,ttf,woff}'] 15 | ignores = @config['precache_glob_ignores'] || [] 16 | recent_posts_num = @config['precache_recent_posts_num'] 17 | 18 | # according to workbox precache {url: 'main.js', revision: 'xxxx'} 19 | @precache_list = [] 20 | 21 | # find precache files with glob 22 | precache_files = [] 23 | patterns.each do |pattern| 24 | name_to_aliases = {} 25 | 26 | # this means there's no alias defined - just use file as only alias 27 | if pattern.is_a?(String) 28 | name_to_aliases = { pattern => pattern } 29 | elsif pattern.is_a?(Hash) 30 | name_to_aliases = pattern 31 | end 32 | 33 | # at this point, name_to_aliases is file -> alias or 34 | # file -> [alias1, alias2], etc. there will never be more 35 | # than one key per yaml conventions 36 | filename, aliases = name_to_aliases.first 37 | 38 | # glob the filename, and push all aliases as just values 39 | Dir.glob(File.join(directory, filename)) do |filepath| 40 | if aliases.is_a?(String) 41 | file_alias = File.join(directory, aliases) 42 | precache_files.push({ filepath => file_alias }) 43 | elsif aliases.is_a?(Array) 44 | aliases.each do |filepath_alias| 45 | file_alias = File.join(directory, filepath_alias) 46 | precache_files.push({ filepath => file_alias }) 47 | end 48 | end 49 | end 50 | end 51 | precache_files = precache_files.uniq 52 | 53 | # precache recent n posts 54 | posts_path_url_map = {} 55 | if recent_posts_num 56 | precache_files.concat( 57 | @site.posts.docs 58 | .reverse.take(recent_posts_num) 59 | .map do |post| 60 | posts_path_url_map[post.path] = post.url 61 | { post.path => post.path } 62 | end 63 | ) 64 | end 65 | 66 | # filter with ignores 67 | ignores.each do |pattern| 68 | Dir.glob(File.join(directory, pattern)) do |ignored_filepath| 69 | precache_files = precache_files.select { |file_map| 70 | file_map.first.first != ignored_filepath 71 | }.map{ |file_map| file_map } 72 | end 73 | end 74 | 75 | # generate md5 for each precache file 76 | md5 = Digest::MD5.new 77 | precache_files.each do |filepath_map| 78 | filepath, filepath_alias = filepath_map.first 79 | md5.reset 80 | md5 << File.read(filepath) 81 | if posts_path_url_map[filepath] 82 | url = posts_path_url_map[filepath] 83 | else 84 | url = filepath_alias.sub(@site.dest, '') 85 | end 86 | @precache_list.push({ 87 | url: @site.baseurl.to_s + url, 88 | revision: md5.hexdigest 89 | }) 90 | end 91 | end 92 | 93 | def write_sw() 94 | # read the sw.js source file 95 | sw_js_str = File.read(@site.in_source_dir(@sw_src_filepath)) 96 | 97 | # prepend the import scripts 98 | sw_js_str = "importScripts('https://storage.googleapis.com/workbox-cdn/releases/#{WorkboxHelper::WORKBOX_VERSION}/workbox-sw.js');\n#{sw_js_str}" 99 | 100 | # generate precache list and inject it into the sw.js 101 | precache_list_str = @precache_list.map do |precache_item| 102 | precache_item.to_json 103 | end.join(",") 104 | sw_js_str = sw_js_str.sub( 105 | "workbox.precaching.precacheAndRoute([])", 106 | "workbox.precaching.precacheAndRoute([#{precache_list_str}])") 107 | 108 | # write sw.js 109 | sw_dest_file = File.new(@site.in_dest_dir(@sw_filename), 'w') 110 | sw_dest_file.puts(sw_js_str) 111 | sw_dest_file.close 112 | end 113 | end 114 | 115 | module Jekyll 116 | Hooks.register :site, :post_write do |site| 117 | pwa_config = site.config['workbox'] || {} 118 | sw_helper = WorkboxHelper.new(site, pwa_config) 119 | 120 | sw_helper.generate_workbox_precache() 121 | sw_helper.write_sw() 122 | end 123 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jekyll Workbox Plugin [![Gem Version](https://badge.fury.io/rb/jekyll-workbox-plugin.png)](http://badge.fury.io/rb/jekyll-workbox-plugin) 2 | 3 | > Google Workbox integration for Jekyll 4 | 5 | This plugin provides integration with [Google Workbox](https://developers.google.com/web/tools/workbox/) for the [Jekyll](https://jekyllrb.com/) static site generator. It generates a service worker and provides precache integration with artifacts managed by Jekyll. 6 | 7 | This plugin provides [workbox-cli](https://developers.google.com/web/tools/workbox/modules/workbox-cli) like functionality for projects using Jekyll. It's based on the [Jekyll PWA Plugin](https://github.com/lavas-project/jekyll-pwa), but it tries to be less clever than that, and focuses purely on the Workbox integration. 8 | 9 | You see this plugin in action on [my website](https://benediktmeurer.de), which is built using Jekyll and comes with a service worker for offline capabilities. 10 | 11 | ## Installation 12 | 13 | This plugin is available as a [RubyGem][ruby-gem]. 14 | 15 | ### Option #1 16 | 17 | Add `gem 'jekyll-workbox-plugin'` to the `jekyll_plugin` group in your `Gemfile`: 18 | 19 | ```ruby 20 | source 'https://rubygems.org' 21 | 22 | gem 'jekyll' 23 | 24 | group :jekyll_plugins do 25 | gem 'jekyll-workbox-plugin' 26 | end 27 | ``` 28 | 29 | Then run `bundle` to install the gem. 30 | 31 | ### Option #2 32 | 33 | Alternatively, you can also manually install the gem using the following command: 34 | 35 | ``` 36 | $ gem install jekyll-workbox-plugin 37 | ``` 38 | 39 | After the plugin has been installed successfully, add the following lines to your `_config.yml` in order to tell Jekyll to use the plugin: 40 | 41 | ```yaml 42 | plugins: 43 | - jekyll-workbox-plugin 44 | ``` 45 | 46 | ## Getting Started 47 | 48 | ### Configuration 49 | 50 | Add the following configuration block to Jekyll's `_config.yml`: 51 | ```yaml 52 | workbox: 53 | sw_src_filepath: sw.js # Optional 54 | sw_dest_filename: sw.js # Optional 55 | precache_recent_posts_num: 5 # Optional 56 | precache_glob_directory: / # Optional 57 | precache_glob_patterns: # Optional 58 | - "{js,css,fonts}/**/*.{js,css,eot,svg,ttf,woff}" 59 | - index.html 60 | - "about.html": # This entry aliases about/ and contact/ to about.html 61 | - about/ 62 | - contact/ 63 | precache_glob_ignores: # Optional 64 | - "fonts/**/*" 65 | ``` 66 | 67 | Parameter | Description 68 | ---------- | ------------ 69 | sw_src_filepath | Filepath of the source service worker. Defaults to `sw.js` 70 | sw_dest_filename | Filename of the destination service worker. Defaults to `sw.js` 71 | precache_glob_directory | Directory of precache. [Workbox Config](https://developers.google.com/web/tools/workbox/get-started/webpack#optional-config) 72 | precache_glob_patterns | Patterns of precache. Accepts aliased names pointing to the same file to support pretty permalinks. [Workbox Config](https://developers.google.com/web/tools/workbox/get-started/webpack#optional-config) 73 | precache_glob_ignores | Ignores of precache. [Workbox Config](https://developers.google.com/web/tools/workbox/get-started/webpack#optional-config) 74 | precache_recent_posts_num | Number of recent posts to precache. 75 | 76 | ### Write your own Service Worker 77 | 78 | Create a file `sw.js` in the root path of your Jekyll project. You can change this source file's path with `sw_src_filepath` option if you don't like the default. 79 | 80 | Now you can write your own Service Worker with [Workbox APIs](https://developers.google.com/web/tools/workbox/reference-docs/latest/), including a line `workbox.precaching.precacheAndRoute([]);`, which will be re-written by this plugin according to the precache configuration specified in the `_config.yml` file. 81 | 82 | Here's what the `sw.js` like in my site. 83 | ```javascript 84 | // sw.js 85 | 86 | // set names for both precache & runtime cache 87 | workbox.core.setCacheNameDetails({ 88 | prefix: 'benediktmeurer.de', 89 | suffix: 'v1', 90 | precache: 'precache', 91 | runtime: 'runtime-cache' 92 | }); 93 | 94 | // let Service Worker take control of pages ASAP 95 | workbox.skipWaiting(); 96 | workbox.clientsClaim(); 97 | 98 | // default to `networkFirst` strategy 99 | workbox.routing.setDefaultHandler(workbox.strategies.networkFirst()); 100 | 101 | // let Workbox handle our precache list 102 | // NOTE: This will be populated by jekyll-workbox-plugin. 103 | workbox.precaching.precacheAndRoute([]); 104 | 105 | // use `Stale-while-revalidate` strategy for images and fonts. 106 | workbox.routing.registerRoute( 107 | /images/, 108 | workbox.strategies.staleWhileRevalidate() 109 | ); 110 | workbox.routing.registerRoute( 111 | /^https?:\/\/fonts\.googleapis\.com/, 112 | workbox.strategies.staleWhileRevalidate() 113 | ); 114 | ``` 115 | 116 | Make sure to follow the [Service Worker Checklist](https://developers.google.com/web/tools/workbox/guides/service-worker-checklist) from the Workbox documentation. Insert this snippet in your JavaScript code somewhere: 117 | 118 | ```js 119 | if ('serviceWorker' in navigator) { 120 | window.addEventListener('load', function() { 121 | navigator.serviceWorker.register('/sw.js'); 122 | }); 123 | } 124 | ``` 125 | 126 | i.e. put it inline near the closing `` tag of every page: 127 | 128 | ```html 129 | 136 | ``` 137 | 138 | ...or into your JavaScript bundle. And also make sure to set the `Cache-Control` HTTP header to `no-cache` for the `sw.js` file. For example when using [Netlify](https://www.netlify.com) just put this snippet into your `_headers` file: 139 | 140 | ``` 141 | # _headers 142 | /sw.js 143 | Cache-Control: no-cache 144 | ``` 145 | 146 | Or if you're using [Firebase](https://firebase.google.com), put something like this into your `firebase.json` file: 147 | 148 | ```json 149 | { 150 | "hosting": { 151 | "headers": [ 152 | { 153 | "source": "/sw.js", 154 | "headers": [{ 155 | "key": "Cache-Control", 156 | "value": "no-cache" 157 | }] 158 | } 159 | ] 160 | } 161 | } 162 | ``` 163 | 164 | # Contribute 165 | 166 | Just fork this repository, make changes and submit a pull request, or just file a bug report. 167 | 168 | # Copyright 169 | 170 | Copyright (c) 2018 Benedikt Meurer. 171 | 172 | Copyright (c) 2017 Pan Yuqi. 173 | 174 | License: MIT 175 | 176 | [ruby-gem]: https://rubygems.org/gems/jekyll-workbox-plugin 177 | --------------------------------------------------------------------------------