Skip to content

Commit

Permalink
Make app installable as PWA, cache static assets in service worker (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
ethanjli authored Sep 23, 2022
1 parent 3350ed6 commit cddb5f1
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 2 deletions.
3 changes: 3 additions & 0 deletions internal/app/fluitans/routes/assets/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func NewTemplated(r godest.TemplateRenderer) *TemplatedHandlers {

func (h *TemplatedHandlers) Register(er godest.EchoRouter) {
er.GET(AppURLPrefix+"app.webmanifest", h.getWebmanifest())
er.GET(AppURLPrefix+"offline", h.getOffline())
}

func RegisterStatic(er godest.EchoRouter, em godest.Embeds) {
Expand All @@ -35,6 +36,8 @@ func RegisterStatic(er godest.EchoRouter, em godest.Embeds) {
year = 365 * day
)

// TODO: serve sw.js with an ETag!
er.GET("/sw.js", echo.WrapHandler(godest.HandleFS("/", em.AppFS, week)))
er.GET("/favicon.ico", echo.WrapHandler(godest.HandleFS("/", em.StaticFS, week)))
er.GET(FontsURLPrefix+"*", echo.WrapHandler(godest.HandleFS(FontsURLPrefix, em.FontsFS, year)))
er.GET(
Expand Down
15 changes: 15 additions & 0 deletions internal/app/fluitans/routes/assets/templated.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package assets
import (
"github.com/labstack/echo/v4"
"github.com/sargassum-world/godest"

"github.com/sargassum-world/fluitans/internal/app/fluitans/auth"
)

func (h *TemplatedHandlers) getWebmanifest() echo.HandlerFunc {
Expand All @@ -19,3 +21,16 @@ func (h *TemplatedHandlers) getWebmanifest() echo.HandlerFunc {
)
}
}

func (h *TemplatedHandlers) getOffline() echo.HandlerFunc {
t := "app/offline.page.tmpl"
h.r.MustHave(t)
return func(c echo.Context) error {
const cacheMaxAge = 86400 // 1 day
// Produce output
return h.r.CacheablePage(
c.Response(), c.Request(), t, struct{}{}, auth.Auth{},
godest.WithRevalidateWhenStale(cacheMaxAge),
)
}
}
7 changes: 6 additions & 1 deletion web/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"devDependencies": {
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-node-resolve": "^14.1.0",
"@rollup/plugin-replace": "^4.0.0",
"@sargassum-world/styles": "^0.2.0",
"bulma": "^0.9.4",
"eslint": "^8.23.1",
Expand All @@ -37,6 +38,10 @@
"@fontsource/oxygen-mono": "^4.5.9",
"@hotwired/stimulus": "3.1.0",
"@hotwired/turbo": "^7.0.1",
"@sargassum-world/stimulated": "^0.4.3"
"@sargassum-world/stimulated": "^0.4.3",
"workbox-expiration": "^6.5.4",
"workbox-recipes": "^6.5.4",
"workbox-routing": "^6.5.4",
"workbox-strategies": "^6.5.4"
}
}
36 changes: 36 additions & 0 deletions web/app/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import scss from 'rollup-plugin-scss';
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import purify from 'purify-css';
import copy from 'rollup-plugin-copy';
import replace from '@rollup/plugin-replace';

const production = !process.env.ROLLUP_WATCH;
const buildDir = 'public/build';
Expand Down Expand Up @@ -121,10 +122,45 @@ function bundleGenerator(type, appName, context) {
};
}

function swGenerator() {
return {
input: `src/sw.js`,
output: {
sourcemap: !production,
format: 'iife',
name: 'serviceWorker',
file: `${buildDir}/sw.js`
},
plugins: [
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
}),
commonjs(),
replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify(production ? 'production' : 'development'),
}),

// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: false
}
};
}

export default [
themeGenerator('eager', undefined),
themeGenerator('light'),
themeGenerator('dark'),
bundleGenerator('eager', 'appEager'),
bundleGenerator('deferred', 'app', 'window'),
swGenerator(),
];
4 changes: 4 additions & 0 deletions web/app/src/main-deferred.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,8 @@ Stimulus.register('navigation-menu', NavigationMenuController);
Stimulus.register('theme', ThemeController);
Stimulus.register('turbo-cache', TurboCacheController);

if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}

export {};
62 changes: 62 additions & 0 deletions web/app/src/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
offlineFallback,
staticResourceCache,
warmStrategyCache,
} from 'workbox-recipes';
import { registerRoute, setDefaultHandler } from 'workbox-routing';
import { CacheFirst, NetworkOnly } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';

const cacheFirst = new CacheFirst();

// Static assets (served with file-rev, so cache forever)

staticResourceCache();
registerRoute(/static\/.*/, cacheFirst);
registerRoute(/app\/.*/, cacheFirst);

// Favicon (served with max-age cache-control header)

const faviconCacheFirst = new CacheFirst({
cacheName: 'favicon-cache',
plugins: [
new ExpirationPlugin({
maxAgeSeconds: 1 * 24 * 60 * 60,
}),
],
});
warmStrategyCache({
urls: ['/favicon.ico'],
strategy: faviconCacheFirst,
});
registerRoute(/favicon\.ico/, faviconCacheFirst);

// Fonts (served with max-age cache-control header)

const fontCacheFirst = new CacheFirst({
cacheName: 'font-cache',
plugins: [
new ExpirationPlugin({
maxAgeSeconds: 90 * 24 * 60 * 60,
}),
],
});
warmStrategyCache({
urls: [
'/fonts/atkinson-hyperlegible-latin-400-normal.woff2',
'/fonts/atkinson-hyperlegible-latin-700-normal.woff2',
],
strategy: fontCacheFirst,
});
registerRoute(/fonts\/.*/, fontCacheFirst);

// Pages (no cache)

setDefaultHandler(new NetworkOnly());
warmStrategyCache({
urls: ['/app/offline'],
strategy: cacheFirst,
});
offlineFallback({
pageFallback: '/app/offline',
});
68 changes: 68 additions & 0 deletions web/app/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@
is-module "^1.0.0"
resolve "^1.19.0"

"@rollup/plugin-replace@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-4.0.0.tgz#e34c457d6a285f0213359740b43f39d969b38a67"
integrity sha512-+rumQFiaNac9y64OHtkHGmdjm7us9bo1PlbgQfdihQtuNxzjpaB064HbRnewUOggLQxVCCyINfStkgmBeQpv1g==
dependencies:
"@rollup/pluginutils" "^3.1.0"
magic-string "^0.25.7"

"@rollup/pluginutils@4":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.0.tgz#a14bbd058fdbba0a5647143b16ed0d86fb60bd08"
Expand Down Expand Up @@ -1092,6 +1100,11 @@ hosted-git-info@^2.1.4:
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==

idb@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.0.tgz#2cc886be57738419e57f9aab58f647e5e2160270"
integrity sha512-Wsk07aAxDsntgYJY4h0knZJuTxM73eQ4reRAO+Z1liOh8eMCJ/MoDS8fCui1vGT9mnjtl1sOu3I2i/W1swPYZg==

ignore@^5.1.1, ignore@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
Expand Down Expand Up @@ -2309,6 +2322,61 @@ word-wrap@^1.2.3:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==

[email protected]:
version "6.5.4"
resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.5.4.tgz#a5c6ec0c6e2b6f037379198d4ef07d098f7cf137"
integrity sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==
dependencies:
workbox-core "6.5.4"

[email protected]:
version "6.5.4"
resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.5.4.tgz#df48bf44cd58bb1d1726c49b883fb1dffa24c9ba"
integrity sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==

[email protected], workbox-expiration@^6.5.4:
version "6.5.4"
resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.5.4.tgz#501056f81e87e1d296c76570bb483ce5e29b4539"
integrity sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==
dependencies:
idb "^7.0.1"
workbox-core "6.5.4"

[email protected]:
version "6.5.4"
resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.5.4.tgz#740e3561df92c6726ab5f7471e6aac89582cab72"
integrity sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==
dependencies:
workbox-core "6.5.4"
workbox-routing "6.5.4"
workbox-strategies "6.5.4"

workbox-recipes@^6.5.4:
version "6.5.4"
resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.5.4.tgz#cca809ee63b98b158b2702dcfb741b5cc3e24acb"
integrity sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==
dependencies:
workbox-cacheable-response "6.5.4"
workbox-core "6.5.4"
workbox-expiration "6.5.4"
workbox-precaching "6.5.4"
workbox-routing "6.5.4"
workbox-strategies "6.5.4"

[email protected], workbox-routing@^6.5.4:
version "6.5.4"
resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.5.4.tgz#6a7fbbd23f4ac801038d9a0298bc907ee26fe3da"
integrity sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==
dependencies:
workbox-core "6.5.4"

[email protected], workbox-strategies@^6.5.4:
version "6.5.4"
resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.5.4.tgz#4edda035b3c010fc7f6152918370699334cd204d"
integrity sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==
dependencies:
workbox-core "6.5.4"

wrap-ansi@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
Expand Down
1 change: 0 additions & 1 deletion web/templates/app/app.webmanifest.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"name": "Fluitans",
"description": "An application for managing Sargassum networks, domain names, and organizations.",
"categories": ["utilities"],
"short_name": "Fluitans",
"lang": "en-US",
"display": "standalone",
"scope": "/",
Expand Down
16 changes: 16 additions & 0 deletions web/templates/app/offline.page.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{{template "shared/base.layout.tmpl" .}}

{{define "title"}}You're not connected{{end}}
{{define "description"}}This page can only be viewed with an internet connection.{{end}}

{{define "content"}}
<main class="main-container">
<section class="section content">
<h1>Try again later</h1>
<p>
It looks like either you don't have an internet connection right now, or the server is
temporarily down.
</p>
</section>
</main>
{{end}}

0 comments on commit cddb5f1

Please sign in to comment.