https://github.com/spacegangster/page-renderer
Write HTML-pages as Clojure maps, with all that meta. Bindings for garden and hiccup. Helps with PWA generation too. Offline-ready web apps with service workers, social meta and async stylesheets.
https://github.com/spacegangster/page-renderer
cache-busting clojure clojure-pwa clojure-pwa-generator metadata opengraph pwa pwa-generator service-workers twitter-cards
Last synced: about 1 month ago
JSON representation
Write HTML-pages as Clojure maps, with all that meta. Bindings for garden and hiccup. Helps with PWA generation too. Offline-ready web apps with service workers, social meta and async stylesheets.
- Host: GitHub
- URL: https://github.com/spacegangster/page-renderer
- Owner: spacegangster
- License: mit
- Created: 2016-06-25T21:04:22.000Z (almost 9 years ago)
- Default Branch: master
- Last Pushed: 2024-08-24T19:45:39.000Z (9 months ago)
- Last Synced: 2025-03-10T20:07:50.025Z (2 months ago)
- Topics: cache-busting, clojure, clojure-pwa, clojure-pwa-generator, metadata, opengraph, pwa, pwa-generator, service-workers, twitter-cards
- Language: Clojure
- Homepage:
- Size: 88.9 KB
- Stars: 113
- Watchers: 5
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# page-renderer
Holistic approach for making simple PWAs and complex HTML5 pages from Clojure.
Bindings for hiccup and garden (css).[](https://clojars.org/page-renderer)
So it's like a layer of knowledge about the real world on top of Hiccup.
## Features
Out of the box:
- Precaching Service Worker generation based on [Workbox](https://developers.google.com/web/tools/workbox/)
- Cache-busting for local assets
- That `name="viewport"` meta tag that you need for responsive pages and language tag
- Meta for SEO, Twitter, Facebook (Open Graph), link sharing
- Clojure stylesheets with `garden`
- Clojure markup rendered with `hiccup`
- Async stylesheets loading## Requirements
Java 8 or later.## Usage
### 1. Define a page
```clojure
(ns pages.home)(defn page [req]
;; essentials
{:title "Lightpad"
:body [:body.page [:h1 "Ah, a Page!"]]
:head-tags [[:meta {:name "custom" :property "stuff"}]]
:stylesheet-async "large-stuff.css" ; injects an async renderer(s)
:script "/app.js" ; async by default
:garden-css [:h1 {:font-size :20px}] ; critical path css (or just inline the whole thing)
:garden-css-cache? true ; uses simple-dimple memoize cache, so only lives in the lifecycle;; seo and meta
:description "Like a notepad but cyberpunk"
:twitter-site "@lightpad_ai";; images
:favicon "https://lightpad.ai/favicon.png"
:og-image "https://lightpad.ai/og-image.png";; PWA stuff
:manifest true
:lang "en"
:theme-color "hsl(0, 0%, 96%)"
:service-worker "/service-worker.js" ; will inject also a service worker lifecycle script
:sw-default-url "/app"
:sw-add-assets ["/icons/fonts/icomoon.woff", "/lightning-150.png"]})
```### 2. Wire it up to your routes (e.g. Compojure)
```clojure
(ns server
(:require [page-renderer.api :as pr]
[compojure.core :refer [defroutes GET]]
[pages.home :as p]))(defroutes
(GET "/" req
{:status 200
:headers {"Content-Type" "text/html"}
:body (pr/render-page (p/page req)})(GET "/service-worker.js" req
{:status 200
:headers {"Content-Type" "text/javascript"}
; will generate a simple Workbox-based service worker on the fly with cache-busting
:body (pr/generate-service-worker (p/page req))})(GET "/quicker-way" req (pr/respond-page (p/page req))))
```### 3. Celebrate
##### Page output
```html
Page
h1 {
font-size: 20px;
}
import { Workbox } from 'https://storage.googleapis.com/workbox-cdn/releases/4.1.0/workbox-window.prod.mjs';
const promptStr = 'New version of the application is downloaded, do you want to update? May take two reloads.';
function createUIPrompt(opts) {
if (confirm(promptStr)) {
opts.onAccept()
}
}
if ('serviceWorker' in navigator) {
const wb = new Workbox('/service-worker.js');
wb.addEventListener('waiting', (event) => {
const prompt = createUIPrompt({
onAccept: async () => {
wb.addEventListener('activated', (event) => {
console.log('sw-init: activated')
window.location.reload();
})
wb.addEventListener('controlling', (event) => {
console.log('sw-init: controlling')
});
wb.messageSW({type: 'SKIP_WAITING'});
}
})
});
wb.register();
}
Ah, a Page!
(function(){
var link = document.createElement('link');
link.rel='stylesheet';
link.href='large-stuff.css';
link.type='text/css';
document.head.appendChild(link);
})()
```
##### Service Worker
```js
importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js')workbox.precaching.precacheAndRoute([
{ url: '/heavy-stuff.css', revision: 'file-hash' },
{ url: '/fonts/icomoon.woff', revision: 'file-hash' },
{ url: '/lightpad/compiled/app.js', revision: 'file-hash' },
{ url: '/favicon.png', revision: 'file-hash' },
{ url: '/app', revision: 'file-hash' }
], { ignoreURLParametersMatching: [/hash/] })workbox.routing.registerNavigationRoute(
workbox.precaching.getCacheKeyForURL('/app'), {
whitelist: [ /^\/app/ ],
blacklist: [ /^\/app\/service-worker.js/ ]
}
)workbox.routing.setCatchHandler(({event}) => {
console.log('swm: event ', event)
})addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
console.log('swm: skipping waiting')
skipWaiting()
}
})self.addEventListener('activate', () => {
console.log('swm: activated')
})self.addEventListener('install', () => {
console.log('swm: installed')
})
```## API
Use `page-renderer.api` namespace.
```
(defn ^String render-page [renderable])
```
Produces an html string.```
(defn ^String generate-service-worker [renderable])
```
Produces a JavaScript ServiceWorker script text. Service worker will additionally
load [Workbox](https://developers.google.com/web/tools/workbox/) script.```
(defn ^Map respond-page [renderable])
```
Produces Ring compatible response map with status 200.```
(defn ^Map respond-service-worker [^Map renderable])
```
Produces Ring compatible response map with status 200.`renderable` – is a map that may have the following fields
##### Mains
- `^Vector :body` - a vector for Hiccup to render into HTML of the document's body
- `^String :title` - content for title tag
- `^String :favicon` - favicon's url
- `^String :og-image` - Open Graph image URL
- `^String :script` - script name, will be loaded asynchronously
- `^String :stylesheet` - stylesheet filename, will be plugged into the head, will cause
browser waiting for download.##### Assets
- `^String :stylesheet-async` - stylesheet filename, will be loaded asynchronously by script.
- `^String :garden-css` - data structure for Garden CSS
- `^String :garden-css-caching?` - enable memoize Garden CSS (default: false)
- `^String :script-sync` - script name, will be loaded synchronously
- `^String :js-module` - entry point for JS modules. If you prefer your scripts to be served as modules
- `^Boolean :skip-cachebusting?` - will skip automatic cachebusting if set. Defaults to false.
- `^String :on-dom-interactive-js` - a js snippet to run once DOM is interactive or ready.
- `^String/Collection :stylesheet-inline` - stylesheet filename, will be inlined into the head.##### PWA related
- `^String :link-image-src` - url to image-src
- `^String :link-apple-icon` - url to image used for apple-touch-icon link
- `^String :link-apple-startup-image` - url to image used for apple-touch-startup-image link
- `^String :theme-color` - theme color for PWA (defaults to white)
- `^String/Boolean :manifest` - truthy value will add a manifest link
If a string is passed – it'll be treated as a manifest url. Otherwise '/manifest.json'
will be specified.- `^String/Boolean :service-worker` - service worker url, defaults to /service-worker.js
- `^String :sw-default-url` – application default url.
Must be an absolute path like '/app'. Defaults to '/'. Will be used in a regexp.
- `^List :sw-add-assets` - a collection of additional
assets you want to precache, like ["/fonts/icon-font.woff" "/logo.png"]##### More meta
- `^String :lang` - when provided will render a meta tag and a document attribute for page language.
- `^String :meta-title` - content for the title tag (preferred)
- `^String :meta-keywords` - content for the keywords tag
- `^String :meta-description` - meta description
- `^Map :meta-props` – meta which must be rendered as props {'fb:app_id' 123}
- `^String :head-tags` - data structure to render into HTML of the document's head##### Open Graph meta (@link https://ogp.me)
- `^String :og-title` - OpenGraph title
- `^String :og-description` - OpenGraph description
- `^String :og-image` - absolute url to image for OpenGraph
- `^String :og-type` - OpenGraph object type
- `^String :og-url` - OpenGraph page permalink##### Twitter meta (@link https://developer.twitter.com/en/docs/tweets/optimize-with-cards/guides/getting-started)
- `^String :twitter-site` - Twitter @username. Required for all Twitter meta to render
- `^String :twitter-creator` - Twitter @username.
- `^Keyword :twitter-card-type` - Twitter card type one of #{:summary :summary_large_image :app :player}
- `^String :twitter-description` - Twitter card description
- `^String :twitter-image` - Twitter image link. Twitter images are useu
- `^String :twitter-image-alt` - Twitter image alt## Service Worker generation
`page-renderer` allows you to produce a full-blown offline-ready
[PWA](https://developers.google.com/web/progressive-web-apps/) fast.
Your users will be able to "install" it as a PWA app on mobile platforms or as Chrome
app on desktop platforms. All you need to do is just add another route to your scheme.### How it works
If you use `service-worker` field then `page-renderer` will generate
a precaching service worker. The worker utilizes
[Workbox (by Google)](https://developers.google.com/web/tools/workbox/)
and will precache all the assets that you've defined in `renderable`, and will be able
to serve them offline. It also does proper cache-busting with hashes.
`page-renderer` will also inject a service worker lifecycle management script into
your page so that your users will be prompted to download a newer version of your
website when it's ready.## How cache-busting works here
`page-renderer` provides very basic, but bulletproof cache-busting by providing
a url param with content-hash (or last modification timestamp), like `/file?hash=abec112221122`.
For every stylesheet, script and image on resource paths – it will generate
a content hash. If the file can't be found on the classpath
or inside a local `resources/public` directory it will receive the library load time,
roughly equaling the application start time.## Where to see in action:
- [Liverm0r/PWA-clojure](https://github.com/Liverm0r/PWA-clojure) – PWA example by Artur Dumchev
- [Plus Minus game](https://plusminus.me/app) by Artur Dumchev. [Repo](https://github.com/liverm0r/plusminus.me)Personally I use it for all my website projects including:
- [Lightpad.ai](https://lightpad.ai) – includes generated service worker, installable PWA
- [Spacegangster.io](https://spacegangster.io) – my website## Troubleshooting
If you are using a frontend proxy server like Nginx – don't forget to prevent it from
serving service-worker as a static asset. My js assets block looks like this
```nginx
location ~ ^(?!/service-worker).*\.(?:js|css|svg)$ {
etag off;
expires 1y;
gzip_vary on;
add_header Cache-Control "public";
access_log off;
}
```
Also note the switched off etag. If you use page-renderer you can turn off etag and
use expires header only for more aggressive caching and preventing avoidable requests.
See details [in this thread on StackOverflow](https://stackoverflow.com/questions/499966/etag-vs-header-expires).## License
Copyright © 2019 Ivan Fedorov
Distributed under the MIT License.