Zelphir Kaltstahl
Zelphir Kaltstahl

Reputation: 6189

Racket servlet serve static files

I want to try and code some blog or personal website or local web service in Racket. I've done the tutorial here and then I found this mailing list entry. In addition to that, I used the information from the documenation. URL based dispatch seems way easier than the tutorial, so I went for that and got the following code:

#lang racket

(provide/contract
  (start (-> request? response?)))

(require
  web-server/templates
  web-server/servlet-env
  web-server/servlet
  web-server/dispatch)

;; =====
;; STATE
;; =====
(define (get-vocabulary-for-topic topic)
  ;; for now always returns the same
  (list
    (list "sich fuer eine Person entscheiden" "xuǎnzé" "32" "选择")
    (list "teilnehmen" "cānyù" "14" "参与")
    (list "die Wahl" "dàxuǎn" "43" "大选")))

;; ======================
;; APPS HANDLING REQUESTS
;; ======================

(define (vocabulary-app request topic)
  (response/full
    200 #"OK"
    (current-seconds) TEXT/HTML-MIME-TYPE
    empty
    (list (string->bytes/utf-8 (render-vocabulary-page topic)))))

(define (vocabulary-overview-app request)
  (response/xexpr
    `(html
       (head (title "Vocabulary Overview")
             (link ((rel "stylesheet") (href "css/general.css") (type "text/css"))))
       (body (p "This is an overview of vocabulary pages.")))))

(define (overview-app request)
  (response/full
    200 #"OK"
    (current-seconds) TEXT/HTML-MIME-TYPE
    empty
    (list (string->bytes/utf-8 (render-overview-page)))))

;; ===============
;; RENDERING STUFF
;; ===============

(define (render-vocabulary-page topic)
  (let
    ([vocabulary (get-vocabulary-for-topic topic)])
    (let
      ([content (render-vocabulary-table vocabulary)]
        [page-title "Vocabulary"]
        [special-css-imports
          (render-css-include "css/vocabulary-table.css")]
        [special-js-imports ""]
        [header ""]
        [footer ""]
        [navigation ""])
      (include-template
        "templates/base.html"))))

(define (render-vocabulary-table vocabulary)
  (let
    ([table-headers (list "German" "Pinyin" "Tones" "Chinese")]
      [word-list vocabulary])
    (include-template "templates/vocabulary-table.html")))

(define (render-overview-page)
  (let
    ([content
       (let
         ([subpage-titles (list "vocabulary")])
         (include-template "templates/overview.html"))]
      [page-title "Overview"]
      [special-css-imports ""]
      [special-js-imports ""]
      [header ""]
      [footer ""]
      [navigation ""])
    (include-template
      "templates/base.html")))

(define (render-css-include path)
  (let
    ([path path])
    (include-template "templates/css-link.html")))


;; ====================
;; ROUTES MANAGING CODE
;; ====================
(define (start request)
  ;; for now only calling the dispatch
  ;; we could put some action here, which shall happen before dispatch
  (blog-dispatch request))

(define-values (blog-dispatch blug-url)
  (dispatch-rules
    [("index") overview-app]
    [("vocabulary") vocabulary-overview-app]
    [("vocabulary" (string-arg)) vocabulary-app]))

(define (respond-unknown-file req)
  (let
    ([content (include-template "templates/unknown-file.html")]
      [page-title "unknown file"]
      [special-css-imports ""]
      [special-js-imports ""]
      [header ""]
      [footer ""]
      [navigation ""])
    (response/full
      404 #"ERROR"
      (current-seconds) TEXT/HTML-MIME-TYPE
      empty
      (list
        (string->bytes/utf-8
          (include-template "templates/base.html"))))))

;; ===========================
;; ADDED FOR RUNNING A SERVLET
;; ===========================
(serve/servlet
  start
  #:servlet-path "/index"  ; default URL
  #:extra-files-paths (list (build-path (current-directory) "static"))  ; directory for static files
  #:port 8000 ; the port on which the servlet is running
  #:servlet-regexp #rx""
  #:launch-browser? true  ; should racket show the servlet running in a browser upon startup?
  ;; #:quit? false  ; ???
  #:listen-ip false  ; the server will listen on ALL available IP addresses, not only on one specified
  #:server-root-path (current-directory)
  #:file-not-found-responder respond-unknown-file)

This works fine so far, except that any file in my static directory, actually in subdirectories of the static directory, namely css cannot be found, when I navigate to any "subpages".

What I mean by that is, that a "main page" would be something like localhost:8000/index or localhost:8000/vocabulary and a subpage would be a page like localhost:8000/vocabulary/something.

It seems that the template rendering gets it wrong, does not always access the static directory from the root of the application directory, but instead only looks at localhost:8000/vocabulary/css/general.css, while it should look at localhost:8000/css/general.css, which it does, when I go to any "main page".

Here is a screenshot of this on a main page:

enter image description here

And here it is on a subpage:

enter image description here

So the static directory seems to change depending on what URL is visited. At first I thought I had finally understood how to serve static files, but it seems I have not and I don't know how to fix the problem in the best way.

Here is my directory structure:

/home/xiaolong/development/Racket/blog2
  - static/
    - css/
      general.css
      vocabulary-table.css
    + img/
    + js/
  - templates/
    base.html
    css-link.html
    js-link.html
    overview.html
    unknown-file.html
    vocabulary-table.html
  blog-demo.rkt
  blog.rkt

How can I fix the paths for static files?

I want to be able to simply type css/something.css in the code and no matter which app or route, it should serve the files and I don't want to change the include path depending on what route I am treating in the code.

The command I use for running the server is simply:

racket blog.rkt

from the root directory of the project.

Upvotes: 3

Views: 769

Answers (2)

Alexander McLin
Alexander McLin

Reputation: 41

The problem here is not understanding the concept of absolute paths versus relative paths, the concept of the base URL, and how they work together to form the full qualified URL for a given resource.

RFC 3986 is the reference for how URLs are constructed and processed. A note, it actually uses the term Uniform Resource Identifier, URI instead of URL. It supersedes previous documents which used the older and deprecated URL terminology.

But I'll continue to use URL term for sake of consistency with the original question.

It's important to understand the concept of a relative reference in RFC 3986 Section 4.1 and that it can be written as a relative path or absolute path.

Here, the agent that is interpreting the URLs is your browser, it's just processing the URLs your server has sent back to it using RFC 3986 as its basis of how URLs are supposed to work.

A fully qualified URL includes the protocol and host domain as well port number, if required, and the path giving the location of the document or resource relative to the protocol and host domain.

So in your case, the correct fully qualified URL of your CSS file would be http://localhost:8000/css/general.css

From RFC 3986 Section 3 The pieces http and localhost:8000 are called the protocol scheme and authority components respectively, and forms the main identifier for your website, http://localhost:8000 Anything coming after that identifier is called the path component and it precedes the query or URL fragment components which starts with "?" or "#" respectively.

The path component is designed to emulate UNIX file path conventions and constructs a tree hierarchy allowing you to express the location of some given resource relative to your website's identifier. There are some rules you need to take into account.

Relative references resolution is given in Section 5 of RFC 3986. The key concept is the base URL which is used with relative references to construct the fully qualified URL.

When your browser accesses the page located at http://localhost:8000/vocabulary/something and it sees css/general.css which is a relative reference written as a relative path. Following the resolution algorithm, the base URL is taken to be the URL originally used to access the page which is http://localhost:8000/vocabulary/something

Since there is no trailing slash at the end, the path subcomponent to the right of the right-most slash is dropped, giving you http://localhost:8000/vocabulary/. The URL relative reference css/general.css is then appended at the end of the rewritten base URL path, giving you http://localhost:8000/vocabulary/css/general.css, resulting in a 404 error when the browser attempts to retrieve the non-existent resource.

If instead, the browser sees /css/general.css which is a relative reference written as an absolute path. The base URL again is http://localhost:8000/vocabulary/something. Since the relative reference is an absolute path, the entire path to the right of your website identifier is dropped giving you http://localhost:8000. The relative reference is then appended to the rewritten base URL, giving you http://localhost:8000/css/general.css which is the correct fully qualified URL.

A note, if you happened to access a page with the following URL http://localhost:8000/vocabulary/something/ which has a trailing slash. Then any relative-path references such as css/general.css will be merged to construct the full URL http://localhost:8000/vocabulary/something/css/general.css because following the algorithm, the browser sees there is a trailing slash at the end of the original base URL and does not drop the last path subcomponent (or rather drops the empty subcomponent to the right of the trailing slash)

Upvotes: 3

John Clements
John Clements

Reputation: 17203

It sure looks to me like this is an HTML problem, not a racket problem. Specifically, it looks like you need to specify these as absolute paths, not as relative ones. So, in your code, you might want to change

(define (vocabulary-overview-app request)
  (response/xexpr
    `(html
       (head (title "Vocabulary Overview")
             (link ((rel "stylesheet") (href "css/general.css") (type "text/css"))))
       (body (p "This is an overview of vocabulary pages.")))))

to

(define (vocabulary-overview-app request)
  (response/xexpr
    `(html
       (head (title "Vocabulary Overview")
             (link ((rel "stylesheet") (href "/css/general.css") (type "text/css"))))
       (body (p "This is an overview of vocabulary pages.")))))

(note the leading slash in the path to the css file)

Does this answer your question?

Upvotes: 2

Related Questions