{"id":30285627,"url":"https://github.com/pupeno/prerenderer","last_synced_at":"2025-08-28T16:55:49.108Z","repository":{"id":62431770,"uuid":"42881535","full_name":"pupeno/prerenderer","owner":"pupeno","description":"Server pre-rendering for Single Page Applications using ClojureScript/JavaScript by use of NodeJS.","archived":false,"fork":false,"pushed_at":"2015-12-14T16:02:24.000Z","size":78,"stargazers_count":68,"open_issues_count":10,"forks_count":6,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-07-02T19:54:27.657Z","etag":null,"topics":["clojure","clojurescript","nodejs","spa"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pupeno.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-09-21T17:16:10.000Z","updated_at":"2025-06-05T18:08:57.000Z","dependencies_parsed_at":"2022-11-01T20:47:03.131Z","dependency_job_id":null,"html_url":"https://github.com/pupeno/prerenderer","commit_stats":null,"previous_names":["carouselapps/prerenderer"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/pupeno/prerenderer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pupeno%2Fprerenderer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pupeno%2Fprerenderer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pupeno%2Fprerenderer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pupeno%2Fprerenderer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pupeno","download_url":"https://codeload.github.com/pupeno/prerenderer/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pupeno%2Fprerenderer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270763461,"owners_count":24641026,"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","status":"online","status_checked_at":"2025-08-16T02:00:11.002Z","response_time":91,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["clojure","clojurescript","nodejs","spa"],"created_at":"2025-08-16T20:07:42.671Z","updated_at":"2025-08-16T20:07:43.298Z","avatar_url":"https://github.com/pupeno.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Prerenderer\n\n[![Code at GitHub](https://img.shields.io/badge/code-github-green.svg)](https://github.com/carouselapps/prerenderer)\n[![Clojars](https://img.shields.io/clojars/v/com.carouselapps/prerenderer.svg)](https://clojars.org/com.carouselapps/prerenderer)\n[![Join the chat at https://gitter.im/carouselapps/prerenderer](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/carouselapps/prerenderer?utm_source=badge\u0026utm_medium=badge\u0026utm_campaign=pr-badge\u0026utm_content=badge)\n\nThis library helps implementing server side prerendering of ClojureScript heavy applications such SPAs (Single Page\nApplications) by use of NodeJS.\n\nSPAs are the ones that ship a minimal HTML and a JavaScript application to the browser and then the\nJavaScript running in the browser renders the application. Some notable examples of this technique is Google Maps (the\none that started it all) and GMail. If you want to see a live demo of a SPA, check out our screencast\n[What is a Single Page Application?](https://carouselapps.com/2015/11/19/what-is-a-single-page-application/).\n\nThis method of developing applications lead to a much better user experience as the application is snappier so it feels\nsimilar to a native application and at the same time it can react to user events that the traditional mode of\ndevelopment tend to ignore. In the Clojure world libraries such as\n[Reagent](https://github.com/reagent-project/reagent), [re-frame](https://github.com/Day8/re-frame), [Om](https://github.com/omcljs/om),\netc. help with this task.\n\nThe problem with developing applications like this is that not all user agents execute JavaScript. Notably search engine\ncrawlers will fetch those pages and index them as no content, as the content would come later when JavaScript runs. Also\nsites such as Facebook, LinkedIn, Twitter, when you submit a link, they fetch content and embed a snippet. These would\nalso fail with an SPA. Over time more and more of these agents will execute JavaScript but I wouldn't count on this.\n\nThe solution is of course to prerender the application on the server and then send this prerendered version to the web\nagent, whether it is a browser or a crawler. When the application is implemented mostly in ClojureScript, to render it\non the server, we either need to re-implement it in Clojure or find a way to execute that ClojureScript. This library\nhelps you execute that ClojureScript on the server.\n\nThis technique is sometimes called isomorphic JavaScript, but a lot of people have a profound dislike for that name and\nhave proposed universal JavaScript instead.\n\n## Design\n\nNodeJS was not our first choice of technology for this task and indeed it came with its many complexities that this\nlibrary tries to abstract away. We first [attempted to use Nashorn](https://carouselapps.com/2015/09/11/isomorphic-clojurescriptjavascript-for-pre-rendering-single-page-applications-part-1/),\na JavaScript engine shipped with Java 8 but we found it tool limited. For example, it doesn't implement XMLHttpRequest\nwhich is very likely used in a SPA.\n\nWhen Nashorn was abandoned, we looked at the many possibilities, like PhantomJS. Ultimately we decided to\n[give NodeJS a try](https://carouselapps.com/2015/10/02/isomorphic-javascript-with-clojurescript-for-pre-rendering-single-page-applications-part-3/)\nbecause it felt like a first class in the ClojureScript world as the compiler can target it natively, but also\nbecause of the performance of the V8 engine and the wide availability of modules to implement the functionality we need\nsuch as XMLHttpRequest, file system access, web servers, etc.\n\n[Prerenderer](https://carouselapps.com/prerenderer) starts a NodeJS process in the background that loads your\napplication. It creates a web server that binds a random port and reports the port to a know file. The Clojure side of\nPrerenderer picks up that port and whenever you request to pre-render a page it'll send a request to that port. Back\ninside NodeJS, Prerender will call a function that you define that will do the actual pre-render and then return it back\nto Clojure.\n\nPrerenderer abstracts away as much as possible the details of running NodeJS, starting a secondary web process in it,\nsending requests to it, and sending back the results of pre-rendering. As a user of Prerenderer it looks almost as if\nyou are calling ClojureScript from Clojure. For the NodeJS web server, it uses the popular [express](http://expressjs.com/)\nmicro-framework.\n\nFor supporting AJAX, Prerenderer uses the [XMLHttpRequest NodeJS module](https://github.com/driverdan/node-XMLHttpRequest)\nbut unfortunately we found [a bug in it](https://github.com/driverdan/node-XMLHttpRequest/pull/115) and we also needed a\nfeature: [to set up default destination for AJAX calls that use relative paths](https://github.com/driverdan/node-XMLHttpRequest/pull/116),\na very common technique in JavaScript applications. Pull requests have been submitted and until then, Prerenderer uses\nour [own release of node-XMLHttpRequest: @pupeno/node-XMLHttpRequest](https://www.npmjs.com/package/@pupeno/xmlhttprequest).\n\nThe elephant in the room of course is that SPAs are never *done*. Imagine a SPA that has a timer and every second sends\na request to the server, and the server replies with the current time, which then the application displays. When is it\ndone rendering? Never. But Prerenderer needs to, at some point, decide that the page is *done enough* and ship it to\nthe browser.\n\nIf you are using plain Reagent it's up to you to decide when the application is done enough. If you are using Re-frame,\nPrerenderer ships with a simple helper to help you deal with it. For other libraries/frameworks, such as Om, Om Next,\nPetrol, you'll also have to find the appropriate solution and pull requests are welcome.\n\n### re-frame\n\nDoing prerendering with re-frame requires version 0.6.0 or later due to the new callback mechanism and queue systems.\nThe way it works is that Prerender will watch for events and once nothing happened for a period of time (300ms by\ndefault) it'll consider the application done and if a certain amount of time went by (3s by default) even if the\napplication is still active, it'll stop and send it to the browser.\n\nThe current default times are completely arbitrary so don't give them too much credit. It's likely that each application\nwill require their own tuning for maximum performance and results. Please, do [let us know](https://carouselapps.com/contact-us/)\nyour finding here and we'll use that information for providing better defaults in future releases.\n\nThis solution is far from perfect. Particularly it means that all pages have an extra 300ms load time. There are two\npossible solutions for that:\n- [Check whether there's a pending AJAX call](https://github.com/carouselapps/prerenderer/issues/5)\n- [Have a way for the app to signal that it's done rendering](https://github.com/carouselapps/prerenderer/issues/6)\n\n### Om\n\nWe don't use Om. Pull requests are welcome.\n\n### Om Next\n\nWe don't use Om Next. Pull requests are welcome.\n\n### Others\n\nPull requests are welcome.\n\n## Usage\n\nInclude the library on your `project.clj`:\n\n[![Clojars Project](http://clojars.org/com.carouselapps/prerenderer/latest-version.svg)](http://clojars.org/com.carouselapps/prerenderer)\n\n### Clojure\n\nOn the Clojure side first, as your application is starting, you need to start the JavaScript engine:\n\n```clojure\n(prerenderer/start! {:path \"target/js/server-side.js\"}))\n```\n\nMost likely you want to keep the JavaScript engine in an atom:\n\n```clojure\n(def js-engine (atom nil))\n\n(reset! js-engine (prerenderer/start! {:path \"target/js/server-side.js\"})))\n```\n\nWhen you run Prerenderer like that, if `target/js/server-side.js` is not present, it'll raise an exception. You can tell\nit to wait for it to appear, useful in development mode, by passing the attribute `:wait`:\n\n```clojure\n(reset! js-engine (prerenderer/start! {:path \"target/js/server-side.js\"\n                                       :wait true})))\n```\n\nIf your JavaScript app runs AJAX requests with relative paths (very common) such as `GET /users`, the app will make the\nrequest to `localhost:3000`. You can define both of this by passing `:default-ajax-host` and `:default-ajax-port`:\n\n```clojure\n(reset! js-engine (prerenderer/start! {:path              \"target/js/server-side.js\"\n                                       :default-ajax-host \"192.168.1.1\"\n                                       :default-ajax-port 12345})))\n```\n\nFor an actual example of this, look at [Ninja Tool's core.clj, around line 23](https://github.com/carouselapps/ninjatools/blob/master/src/clj/ninjatools/core.clj#L23).\nYou want them to point to where the Clojure server is running. In many cases for example, the port will be random.\n\nAlso, you may want to specify the working directory for the Node.js process like this:\n\n```clojure\n(reset! js-engine (prerenderer/start! {:path              \"js/server-side.js\"\n                                       :working-directory \"target\"})))\n```\n\nAfter that, prerendering happens by simply doing:\n\n```clojure\n(prerenderer/render @js-engine url headers)\n```\n\nwhere `url` is the URL you are prerendering and `headers` is map of the headers you want the ClojureScript to see\n(important for cookies for example, [altough currently not properly supported](https://github.com/carouselapps/prerenderer/issues/14)).\nIf you are using Ring, you can do something such as:\n\n```clojure\n(prerenderer/render @js-engine (ring.util.request/request-url request) (:headers request))\n```\n\nwhere `request` is the Ring request.\n\n### ClojureScript\n\nThe ClojureScript side of Prerenderer is a bit more involved. Prerenderer uses NodeJS and a few JavaScript libraries.\nTo install these libraries it uses [npm](https://www.npmjs.com/) so you need the [lein-npm plug in](https://github.com/RyanMcG/lein-npm)\nin your project. Something like:\n\n```clojure\n:plugins [; other plugins\n          [lein-npm \"0.6.1\"]]\n```\n\nRunning `lein deps` will install the necessary modules to your project's node_modules directory which I recommend adding\nto your list of ignored files for your source control system (`.gitignore`, `.hgignore`, etc.).\n\nYou need to compile the application for running in NodeJS and you'll also need to include some extra code that is NodeJS\nspecific and you don't want to ship with your application. If you have your ClojureScript in `src/cljs`, I'd recommend\n`src/node`; and if you have it on `src-cljs`, I'd go for `src-node`. It's up to you. Let's say your cljsbuild\nconfiguration looks like this:\n\n```clojure\n:cljsbuild {:builds {:app {:source-paths [\"src/cljs\"]\n                           :compiler     {:output-dir \"resources/public/js/app\"\n                                          :output-to  \"resources/public/js/app.js\"}}}}\n```\n\nYou'll want to add a second build so it'll look like this:\n\n```clojure\n:cljsbuild {:builds {:app         {:source-paths [\"src/cljs\"]\n                                   :compiler     {:output-dir \"resources/public/js/app\"\n                                                  :output-to  \"resources/public/js/app.js\"}}\n                     :server-side {:source-paths [\"src/cljs\" \"src/node\"]\n                                   :compiler     {:output-dir \"target/js/server-side\"\n                                                  :output-to  \"target/js/server-side.js\"\n                                                  :main       \"projectx.node\"\n                                                  :target     :nodejs}}}}\n```\n\nImportant parts are:\n- inclusion of `src/node`\n- defining main as `projectx.node`\n- targeting NodeJS\n\nRemember to also add it to your dev and uberjar profiles as needed but I'd refrain from any sort of optimizations. I\nfound them from problematic to just-not-working-on-NodeJS due to the NodeJS modules being out of scope for the compiler\nand they are not really needed.\n\nAs a reference, this is what I would use for a dev profile:\n\n```clojure\n:server-side {:compiler {:optimizations :none\n                         :source-map    true\n                         :pretty-print  true\n                         :verbose       true}}\n```\n\nand this for an uberjar:\n\n```clojure\n:server-side {:compiler {:optimizations :none\n                         :source-map    true\n                         :pretty-print  true}}\n```\n\nYes, pretty print, why not? And I included source maps in case NodeJS could pick it up and give me better stack traces\nbut I didn't look into it yet.\n\n`projectx.node` will implement your NodeJS specific part of the application, which will look something like this:\n\n```clojure\n(ns projectx.node\n  (:require [prerenderer.core :as prerenderer]))\n\n(defn render-and-send [page-path send-to-browser]\n  (send-to-browser (render page-path)))\n\n(set! *main-cli-fn* (prerenderer/create render-and-send \"ProjectX\"))\n```\n\n`prerenderer.core/create` creates the prerenderer and takes two arguments: the rendering function and the name of the\napplication. The name of your application is only used for logging and reporting purposes and it's just a simple string,\nwhatever you want.\n\n`render-and-send` receives two attributes, `page-path` and `send-to-browser`. `page-path` is the path that is being\nrequested, the one you have to render, while `send-to-browser` is a function that will send the data back to the\nbrowser, that is, triggering a NodeJS Express response.\n\n### re-frame\n\nIt's common in re-frame to start with a minimalistic HTML, trigger and event that then builds the page optionally\ntriggering many other events. We need to render the page to a string only after all events have been handled. Currently\nwe ship a simple heuristic: wait for 300ms of no events being triggered or 3s total, whichever happens first. When it\nlooks like the page is rendered, a thunk is called back that should return the desired string to be sent to the client.\n\nSince all of this is asyncronous and JavaScript is single-threaded, we cannot just wait for it to finish and then call\n`send-to-browser`. That's why Prerenderer ships with a helper function, called `render-by-timeout`, which implements the\nheuristics previously described and this is how you use it:\n\n```clojure\n(defn render-and-send [page-path send-to-browser]\n  (re-frame/dispatch-sync [:initialize-db])\n  (re-frame/dispatch-sync [:whatever-is-needed-to-render page-path])\n  (re-frame-prerenderer/render-by-timeout [views/main-panel] send-to-browser)))\n\n(set! *main-cli-fn* (prerenderer/create render-and-send \"Project X\"))\n```\n\nThe first argument, is the actual component to be render. In this case, Prerenderer will run~\n\n```clojure\n(reagent/render-to-string [views/main-panel])\n```\n\nand use ```send-to-browser``` to dispatch the result back to the browser. You can specify your own timeouts if you want:\n\n```clojure\n(re-frame-prerenderer/render-by-timeout [views/main-panel] send-to-browser) 400 4000)\n```\n\nFor a real life example of its usage, please, check [Ninja Tool's node.cljs](https://github.com/carouselapps/ninjatools/blob/master/src/node/ninjatools/node.cljs).\n\n### Heroku\n\nIf you are deploying to Heroku, you'll quickly find out that NodeJS is not installed on your Clojure dynos. Adding the\nNodeJS buildpack won't help because it'll try to detect whether your application is a NodeJS one and it'll fail. Making\nit look like a NodeJS application and adding an empty `package.json` will make lein-npm not work.\n\nIf you want to stay up to date on this matter, I'd recommend following this issue [heroku-buildpack-clojure/issues/44](https://github.com/heroku/heroku-buildpack-clojure/issues/44).\nIn the meantime, you need to use the nodejs branch of the Clojure buildpack:\n\n```bash\n$ heroku buildpacks:set https://github.com/heroku/heroku-buildpack-clojure#nodejs\n```\n\nYou also need Heroku to run `lein deps` to fetch your dependencies when it's building your uberjar. That can be achieved\nby adding:\n\n```clojure\n:prep-tasks  [\"deps\" \"javac\" \"compile\"]\n```\n\nto your uberjar profile. You can get some background about this issue in\n[lein-npm/issues/28](https://github.com/RyanMcG/lein-npm/issues/28).\n\n## Changelog\n\n### v0.2.0 - 2015-12-13\n- Changed the ClojureScript API to hide NodeJS details.\n- New re-frame implementation that depends on re-frame 0.6.0 but not on a fork.\n- [Added an option to specify the working directory for the JavaScript engine. Courtesy of Andrey Subbotin.](https://github.com/carouselapps/prerenderer/pull/12)\n- Added Function to stop JavaScript engine.\n- Renamed run to start! to match stop!\n- Added option :noop-when-stopped that will make prerenderer just issue a warning when the JavaScript engine is not\nrunning.\n\n### v0.1.0 - 2015-09-23\n- Initial version. For more information, check out https://carouselapps.com/2015/10/02/isomorphic-javascript-with-clojurescript-for-pre-rendering-single-page-applications-part-3/\n\n## License\n\nThis library has been extracted from the project [Ninja Tools](http://tools.screensaver.ninja).\n\nCopyright © 2015 Carousel Apps, Ltd. All rights reserved.\n\nDistributed under the Eclipse Public License either version 1.0 or (at your option) any later version.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpupeno%2Fprerenderer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpupeno%2Fprerenderer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpupeno%2Fprerenderer/lists"}