{"id":27195669,"url":"https://github.com/spacegangster/page-renderer","last_synced_at":"2025-07-14T08:34:17.457Z","repository":{"id":62434088,"uuid":"61961537","full_name":"spacegangster/page-renderer","owner":"spacegangster","description":"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.","archived":false,"fork":false,"pushed_at":"2024-08-24T19:45:39.000Z","size":91,"stargazers_count":114,"open_issues_count":1,"forks_count":0,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-06-23T13:20:54.597Z","etag":null,"topics":["cache-busting","clojure","clojure-pwa","clojure-pwa-generator","metadata","opengraph","pwa","pwa-generator","service-workers","twitter-cards"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/spacegangster.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-06-25T21:04:22.000Z","updated_at":"2025-05-08T10:06:53.000Z","dependencies_parsed_at":"2025-04-09T19:47:35.101Z","dependency_job_id":"9786725b-b621-4778-b6ae-093d8efa9c56","html_url":"https://github.com/spacegangster/page-renderer","commit_stats":{"total_commits":61,"total_committers":2,"mean_commits":30.5,"dds":"0.14754098360655743","last_synced_commit":"4ddfcd14970eefded9648870fd6592ad4b0e767c"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/spacegangster/page-renderer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spacegangster%2Fpage-renderer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spacegangster%2Fpage-renderer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spacegangster%2Fpage-renderer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spacegangster%2Fpage-renderer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/spacegangster","download_url":"https://codeload.github.com/spacegangster/page-renderer/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spacegangster%2Fpage-renderer/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265262774,"owners_count":23736465,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cache-busting","clojure","clojure-pwa","clojure-pwa-generator","metadata","opengraph","pwa","pwa-generator","service-workers","twitter-cards"],"created_at":"2025-04-09T19:47:31.170Z","updated_at":"2025-07-14T08:34:17.394Z","avatar_url":"https://github.com/spacegangster.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# page-renderer\n\nHolistic approach for making simple PWAs and complex HTML5 pages from Clojure. \nBindings for hiccup and garden (css).\n\n\n[![Clojars Project](https://img.shields.io/clojars/v/page-renderer.svg)](https://clojars.org/page-renderer)\n![Tests](https://github.com/spacegangster/page-renderer/actions/workflows/github-test.yml/badge.svg)\n\nSo it's like a layer of knowledge about the real world on top of Hiccup.\n\n## Features\nOut of the box:\n- Precaching Service Worker generation based on [Workbox](https://developers.google.com/web/tools/workbox/)\n- Cache-busting for local assets\n- That `name=\"viewport\"` meta tag that you need for responsive pages and language tag\n- Meta for SEO, Twitter, Facebook (Open Graph), link sharing\n- Clojure stylesheets with `garden`\n- Clojure markup rendered with `hiccup`\n- Async stylesheets loading\n\n\n## Requirements\nJava 8 or later.\n\n## Usage\n\n### 1. Define a page\n```clojure\n(ns pages.home)\n\n(defn page [req]\n   ;; essentials\n  {:title \"Lightpad\"\n   :body [:body.page [:h1 \"Ah, a Page!\"]]\n   :head-tags [[:meta {:name \"custom\" :property \"stuff\"}]]\n   :stylesheet-async \"large-stuff.css\" ; injects an async renderer(s)\n   :script \"/app.js\" ; async by default\n   :garden-css [:h1 {:font-size :20px}] ; critical path css (or just inline the whole thing)\n   :garden-css-cache? true ; uses simple-dimple memoize cache, so only lives in the lifecycle\n\n   ;; seo and meta\n   :description \"Like a notepad but cyberpunk\"\n   :twitter-site \"@lightpad_ai\"\n\n   ;; images\n   :favicon \"https://lightpad.ai/favicon.png\"\n   :og-image \"https://lightpad.ai/og-image.png\"\n\n   ;; PWA stuff\n   :manifest    true\n   :lang        \"en\"\n   :theme-color \"hsl(0, 0%, 96%)\"\n   :service-worker \"/service-worker.js\" ; will inject also a service worker lifecycle script\n   :sw-default-url \"/app\"\n   :sw-add-assets [\"/icons/fonts/icomoon.woff\", \"/lightning-150.png\"]})\n```\n\n### 2. Wire it up to your routes (e.g. Compojure)\n```clojure\n(ns server\n (:require [page-renderer.api :as pr]\n           [compojure.core :refer [defroutes GET]] \n           [pages.home :as p]))\n\n(defroutes\n  (GET \"/\" req\n   {:status 200\n    :headers {\"Content-Type\" \"text/html\"}\n    :body (pr/render-page (p/page req)})\n\n  (GET \"/service-worker.js\" req\n   {:status 200\n    :headers {\"Content-Type\" \"text/javascript\"}\n    ; will generate a simple Workbox-based service worker on the fly with cache-busting\n    :body (pr/generate-service-worker (p/page req))})\n\n  (GET \"/quicker-way\" req (pr/respond-page (p/page req))))\n```\n\n\n### 3. Celebrate\n\n##### Page output\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n    \u003cmeta charset=\"utf-8\"\u003e\n    \u003clink href=\"/favicon.png\" rel=\"icon\" type=\"image/png\"\u003e\n    \u003cmeta content=\"width=device-width, initial-scale=1, maximum-scale=1\" name=\"viewport\"\u003e\n    \u003ctitle\u003ePage\u003c/title\u003e\n    \u003cmeta content=\"Some bird stuff\" name=\"description\"\u003e\n    \u003cmeta content=\"summary\" name=\"twitter:card\"\u003e\n    \u003cmeta content=\"birds.org\" name=\"twitter:site\"\u003e\n    \u003cmeta content=\"Some bird stuff\" name=\"twitter:description\"\u003e\n    \u003cmeta content=\"https://birds.org/great-tit.png?mtime=1560280129605\" name=\"twitter:image\"\u003e\n    \u003cmeta content=\"Page\" property=\"og:title\"\u003e\n    \u003cmeta content=\"Some bird stuff\" property=\"og:description\"\u003e\n    \u003cmeta content=\"https://birds.org/great-tit.png?mtime=1560280129605\" property=\"og:image\"\u003e\n    \u003cstyle id=\"inline-css--garden\"\u003e\n    h1 {\n    font-size: 20px;\n    }\n    \u003c/style\u003e\n    \n    \u003c!-- Service Worker Lifecycle Snippet --\u003e\n    \u003cscript\u003e\n    import { Workbox } from 'https://storage.googleapis.com/workbox-cdn/releases/4.1.0/workbox-window.prod.mjs';\n    \n    const promptStr = 'New version of the application is downloaded, do you want to update? May take two reloads.';\n    function createUIPrompt(opts) {\n      if (confirm(promptStr)) {\n         opts.onAccept()\n      }\n    }\n    \n    if ('serviceWorker' in navigator) {\n      const wb = new Workbox('/service-worker.js');\n      wb.addEventListener('waiting', (event) =\u003e {\n        const prompt = createUIPrompt({\n          onAccept: async () =\u003e {\n            wb.addEventListener('activated', (event) =\u003e {\n              console.log('sw-init: activated')\n              window.location.reload();\n            })\n            wb.addEventListener('controlling', (event) =\u003e {\n              console.log('sw-init: controlling')\n            });\n            wb.messageSW({type: 'SKIP_WAITING'});\n          }\n        })\n      });\n      wb.register();\n    }\n    \u003c/script\u003e\n\u003c/head\u003e\n\u003cbody class=\"page\"\u003e\n    \u003ch1\u003eAh, a Page!\u003c/h1\u003e\n    \u003cscript\u003e\n    (function(){\n    var link = document.createElement('link');\n    link.rel='stylesheet';\n    link.href='large-stuff.css';\n    link.type='text/css';\n    document.head.appendChild(link);\n    })()\n    \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n##### Service Worker\n```js\nimportScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js')\n\nworkbox.precaching.precacheAndRoute([\n    { url: '/heavy-stuff.css', revision: 'file-hash' },\n    { url: '/fonts/icomoon.woff', revision: 'file-hash' },\n    { url: '/lightpad/compiled/app.js', revision: 'file-hash' },\n    { url: '/favicon.png', revision: 'file-hash' },\n    { url: '/app', revision: 'file-hash' }\n], { ignoreURLParametersMatching: [/hash/] })\n\nworkbox.routing.registerNavigationRoute(\n    workbox.precaching.getCacheKeyForURL('/app'), {\n        whitelist: [ /^\\/app/ ],\n        blacklist: [ /^\\/app\\/service-worker.js/ ]\n    }\n)\n\nworkbox.routing.setCatchHandler(({event}) =\u003e {\n    console.log('swm: event ', event)\n})\n\naddEventListener('message', (event) =\u003e {\n    if (event.data \u0026\u0026 event.data.type === 'SKIP_WAITING') {\n        console.log('swm: skipping waiting')\n        skipWaiting()\n    }\n})\n\nself.addEventListener('activate', () =\u003e {\n    console.log('swm: activated')\n})\n\nself.addEventListener('install', () =\u003e {\n    console.log('swm: installed')\n})\n```\n\n\n## API\n\nUse `page-renderer.api` namespace.\n\n```\n(defn ^String render-page [renderable])\n```\nProduces an html string.\n\n```\n(defn ^String generate-service-worker [renderable])\n```\nProduces a JavaScript ServiceWorker script text. Service worker will additionally\nload [Workbox](https://developers.google.com/web/tools/workbox/) script.\n\n\n```\n(defn ^Map respond-page [renderable])\n```\nProduces Ring compatible response map with status 200.\n\n```\n(defn ^Map respond-service-worker [^Map renderable])\n```\nProduces Ring compatible response map with status 200.\n\n`renderable` – is a map that may have the following fields\n\n##### Mains\n-  `^Vector :body`       - a vector for Hiccup to render into HTML of the document's body\n-  `^String :title`      - content for title tag\n-  `^String :favicon`    - favicon's url\n-  `^String :og-image`   - Open Graph image URL\n-  `^String :script`     - script name, will be loaded asynchronously\n-  `^String :stylesheet` - stylesheet filename, will be plugged into the head, will cause\n                         browser waiting for download.\n\n##### Assets\n-  `^String  :stylesheet-async` - stylesheet filename, will be loaded asynchronously by script.\n-  `^String  :garden-css`       - data structure for Garden CSS\n-  `^String  :garden-css-caching?` - enable memoize Garden CSS (default: false)\n-  `^String  :script-sync`      - script name, will be loaded synchronously\n-  `^String  :js-module`        - entry point for JS modules. If you prefer your scripts to be served as modules\n-  `^Boolean :skip-cachebusting?`    - will skip automatic cachebusting if set. Defaults to false.\n-  `^String  :on-dom-interactive-js` - a js snippet to run once DOM is interactive or ready.\n-  `^String/Collection\u003cString\u003e :stylesheet-inline` - stylesheet filename, will be inlined into the head.\n\n##### PWA related\n-  `^String  :link-image-src`           - url to image-src\n-  `^String  :link-apple-icon`          - url to image used for apple-touch-icon link\n-  `^String  :link-apple-startup-image` - url to image used for apple-touch-startup-image link\n-  `^String  :theme-color`              - theme color for PWA (defaults to white)\n-  `^String/Boolean :manifest`          - truthy value will add a manifest link\n      If a string is passed – it'll be treated as a manifest url. Otherwise '/manifest.json'\n      will be specified.\n\n-  `^String/Boolean :service-worker` - service worker url, defaults to /service-worker.js\n-  `^String         :sw-default-url` – application default url.\n      Must be an absolute path like '/app'. Defaults to '/'. Will be used in a regexp.\n-  `^List\u003cString\u003e   :sw-add-assets` - a collection of additional\n      assets you want to precache, like [\"/fonts/icon-font.woff\" \"/logo.png\"]\n\n##### More meta\n-  `^String :lang`             - when provided will render a meta tag and a document attribute for page language.\n-  `^String :meta-title`       - content for the title tag (preferred)\n-  `^String :meta-keywords`    - content for the keywords tag\n-  `^String :meta-description` - meta description\n-  `^Map    :meta-props`       – meta which must be rendered as props {'fb:app_id' 123}\n-  `^String :head-tags`        - data structure to render into HTML of the document's head\n\n##### Open Graph meta (@link https://ogp.me)\n-  `^String :og-title`       - OpenGraph title\n-  `^String :og-description` - OpenGraph description\n-  `^String :og-image`       - absolute url to image for OpenGraph\n-  `^String :og-type`        - OpenGraph object type\n-  `^String :og-url`         - OpenGraph page permalink\n\n##### Twitter meta (@link https://developer.twitter.com/en/docs/tweets/optimize-with-cards/guides/getting-started)\n-  `^String  :twitter-site`        - Twitter @username. Required for all Twitter meta to render\n-  `^String  :twitter-creator`     - Twitter @username.\n-  `^Keyword :twitter-card-type`   - Twitter card type one of #{:summary  :summary_large_image :app :player}\n-  `^String  :twitter-description` - Twitter card description\n-  `^String  :twitter-image`       - Twitter image link. Twitter images are useu\n-  `^String  :twitter-image-alt`   - Twitter image alt\n\n\n## Service Worker generation\n`page-renderer` allows you to produce a full-blown offline-ready\n [PWA](https://developers.google.com/web/progressive-web-apps/) fast.\nYour users will be able to \"install\" it as a PWA app on mobile platforms or as Chrome\napp on desktop platforms. All you need to do is just add another route to your scheme.\n\n### How it works\nIf you use `service-worker` field then `page-renderer` will generate\na precaching service worker. The worker utilizes\n [Workbox (by Google)](https://developers.google.com/web/tools/workbox/)\nand will precache all the assets that you've defined in `renderable`, and will be able\nto serve them offline. It also does proper cache-busting with hashes.\n`page-renderer` will also inject a service worker lifecycle management script into\nyour page so that your users will be prompted to download a newer version of your\nwebsite when it's ready.\n\n## How cache-busting works here\n`page-renderer` provides very basic, but bulletproof cache-busting by providing\na url param with content-hash (or last modification timestamp), like `/file?hash=abec112221122`.\nFor every stylesheet, script and image on resource paths – it will generate\na content hash. If the file can't be found on the classpath\nor inside a local `resources/public` directory it will receive the library load time,\nroughly equaling the application start time.\n\n\n## Where to see in action:\n\n- [Liverm0r/PWA-clojure](https://github.com/Liverm0r/PWA-clojure) – PWA example by Artur Dumchev\n- [Plus Minus game](https://plusminus.me/app) by Artur Dumchev. [Repo](https://github.com/liverm0r/plusminus.me)\n\nPersonally I use it for all my website projects including:\n- [Lightpad.ai](https://lightpad.ai) – includes generated service worker, installable PWA\n- [Spacegangster.io](https://spacegangster.io) – my website\n\n\n\n## Troubleshooting\n\nIf you are using a frontend proxy server like Nginx – don't forget to prevent it from\nserving service-worker as a static asset. My js assets block looks like this\n```nginx\n        location ~ ^(?!/service-worker).*\\.(?:js|css|svg)$ {\n                etag off;\n                expires 1y;\n                gzip_vary on;\n                add_header Cache-Control \"public\";\n                access_log off;\n        }\n```\nAlso note the switched off etag. If you use page-renderer you can turn off etag and\nuse expires header only for more aggressive caching and preventing avoidable requests.\nSee details [in this thread on StackOverflow](https://stackoverflow.com/questions/499966/etag-vs-header-expires).\n\n\n\n## License\n\nCopyright © 2019 Ivan Fedorov\n\nDistributed under the MIT License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspacegangster%2Fpage-renderer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fspacegangster%2Fpage-renderer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspacegangster%2Fpage-renderer/lists"}