Search code examples
clojurecompojurering

Serve static resources using Clojure's Ring


I am learning how to create web apps using Clojure's Ring. I'm trying to serve a static .html file which contains a reference to a .css file through a <link> tag in its head part. The .css file is in the same directory as the index.html file I'm trying to serve, however, the .css file is not being loaded (I get an error with a 500 status code with a reason phrase of:

Reponse map is nil

This is my code below:

(defroutes approutes
  (GET "/" reqmap
    (resource-response "index.html")))


(def server (run-jetty #'approutes {:join? false, :port 3000}))

What am I missing here? and how can I serve an html file that has references to other files (.css, .js, .jpeg, etc)? I've had some luck (although I can't completly explain why) using Ring's wrap-resource middleware in the ring.middleware.resource namespace although that function is to be used only when a request map matches a static resource (and as you can see, the route "/" doesn't match a resource per-se).

Thank you.


Solution

  • You need to add one bit of middleware that will take care of serving static files from a folder you can select, something like the following:

    ;; Add to your (ns :requires at the top of the file)
    (:require [ring.middleware.resource :refer wrap-resource])
    
    ;; more of your existing code here...
    
    (def app
      (wrap-resource approutes "public")) ;; serve static files from "resources/public" in your project
    
    (def server (run-jetty #'app {:join? false, :port 3000}))
    

    This should be enough to get you going, so if you start your server, you should be able to open files in addresses such as http://localhost:3000/style.css which should be found in public/resources/style.css in your project. Any other static files should work. There's a guide in the Ring wiki in Github that explains two similar functions (middlewares) that you can use.

    Next, in your index.html file you should be able to reference other files such as CSS files, like the following:

    <html>
      <head>
        <link rel="stylesheet" href="css/style.css">
      </head>
      <!-- and son on... -->
    

    Here's a sample project I wrote a while ago that does show the same ideas: https://github.com/dfuenzalida/antizer-demo


    UPDATE

    I've done a quick run from scratch, this should help you find what the problem is.

    Created a new project with:

    lein new app hello-ring
    

    Actually, the name is a bit misleading because we'll be using both Ring and Compojure.

    We'll update the file project.clj with the following:

    (defproject hello-ring "0.1.0-SNAPSHOT"
      :description "FIXME: write description"
      :url "http://example.com/FIXME"
      :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
                :url "https://www.eclipse.org/legal/epl-2.0/"}
      :dependencies [[org.clojure/clojure "1.10.0"]
                     [compojure "1.6.1"]
                     [ring/ring-core "1.6.3"]
                     [ring/ring-jetty-adapter "1.6.3"]]
      :main ^:skip-aot hello-ring.core
      :target-path "target/%s"
      :profiles {:uberjar {:aot :all}})
    

    Now let's edit the file src/hello_ring/core.clj, its contents should be like the following:

    (ns hello-ring.core
      (:require [ring.adapter.jetty :refer [run-jetty]]
                [ring.middleware.resource :refer [wrap-resource]]
                [ring.util.response :refer [resource-response]]
                [compojure.core :refer [defroutes GET]])
      (:gen-class))
    
    (defroutes approutes
      (GET "/" []
           (resource-response "public/index.html")))
    
    (def app
      (-> approutes
          (wrap-resource "public"))) ;; files from resources/public are served
    
    (defn server []
      (run-jetty app {:join? false, :port 3000}))
    
    (defn -main [& args]
      (server))
    

    Finally let's create a couple of static resources. Create the folder structure resources/public/css and in the file resources/public/css/style.css enter the following:

    body {
        font-face: sans-serif;
        padding-left: 20px;
    }
    

    ... and a basic HTML file in resources/public/index.html with the following:

    <html>
      <head>
        <title>It works!</title>
        <link rel="stylesheet" href="css/style.css" />
      </head>
      <body>
        <h1>it works!</h1>
      </body>
    </html>
    

    ... that's it. The HTML file will attempt to load the CSS file. Save everything and check if it matches the following folder structure:

    .
    ├── CHANGELOG.md
    ├── doc
    │   └── intro.md
    ├── LICENSE
    ├── project.clj
    ├── README.md
    ├── resources
    │   └── public
    │       ├── css
    │       │   └── style.css
    │       └── index.html
    ├── src
    │   └── hello_ring
    │       └── core.clj
    └── test
        └── hello_ring
            └── core_test.clj
    

    Now you should be able to start the service from the command line with:

    lein run
    

    The output will look like the following:

    $ lein run
    2019-08-05 23:46:14.919:INFO::main: Logging initialized @1221ms
    2019-08-05 23:46:16.281:INFO:oejs.Server:main: jetty-9.2.21.v20170120
    2019-08-05 23:46:16.303:INFO:oejs.ServerConnector:main: Started ServerConnector@2c846d55{HTTP/1.1}{0.0.0.0:3000}
    2019-08-05 23:46:16.303:INFO:oejs.Server:main: Started @2606ms
    

    Connect to the server in http://0.0.0.0:3000/ ... You should see your page in the browser with the It works! message and a basic CSS reset. In the console you're most likely to see some exceptions because the browser attempts to load the file /favicon.ico from your server, which does not exist (you can create it as an empty file for now).

    I hope this helps.