Serving XHTML

Practical web programming with PLT Scheme

Part one of two. Next part ยป

Lisp/Scheme is a wonderful language for web development. Just a simple thing like representing XML fragments with s-expressions gives it a tremendous advantage over some other languages, where you would manually make sure your tags are properly closed, and no attributes are left unterminated. I come from a background in PHP, so I can safely say I've "been there, done that".

However, when I first sat down with PLT scheme to try it out, I was surprised by the complete lack of real-world practical examples of web development. The built in web server seemed like a neat thing, but virtually all tutorials I could find focused on the continuation based aspects while ignoring the basic needs for standards compliancy, SEO and design.

Serving content

OK, let's get started. We need to use at least version 4 of PLT Scheme. If you use Ubuntu, make sure to download the latest release manually, since the PLT package available through the repositories is a bit behind.

First of all we need to present an empty page. PLT Scheme comes with a built in web server we can extend and use as we like. Most web centric tutorials show you how to create a "servlet". I would like some more control over how URL:s are handled, so I will show you how to extend the web server functionality with a custom "dispatcher". A dispatcher routes each URL to a suitable procedure. It's actually not that hard:

#lang scheme

(define (my-response not-used-request)
  '(html (p "Hello World")))

(require web-server/dispatchers/dispatch-pathprocedure)

(define some-dispatcher
  (make "/" my-response))

(require web-server/web-server)

(define stop-server
  (serve #:dispatch some-dispatcher
         #:port 8080))

Just paste this into your definitions window and run it, then point your browser to http://localhost:8080. Ta-Da!

In just two lines of code, we defined our own custom dispatcher. The next part of this tutorial will show you in-depth how to create a more complex dispatcher. For now, we simply return a basic Hello World page at the root of the web server. We also also start the web server itself, and in the same time define a function to stop it. Simple, no?

(I'm running the server at port 8080, since the standard port 80 is usually not available for user-level servers in Unix systems. If you are using Windows, you could just serve everything from port 80 and save some yourself some URL typing.)

This means you are running your own web server inside DrScheme, and you have full power over how it will handle URL:s.

Serving XHTML

If you check the page source of the example above, it should look like this:

<html><p>Hello World</p></html>

Pretty spartan. I want to serve valid XHTML so this is just not good enough. It turns out it's not possible to output proper XHTML if you just return an xexpr from your response function. To output valid XHTML, including the doctype and everything, you need to return a full response object. It's basically an object representing the complete HTTP response sent from the server, including all headers like mime type, redirects and attachments. This needs a lot of code but is not very interesting.

; To send a response to the client, we'll need the response struct.
(require web-server/http/response-structs)

; We need xml to define the pages themselves.
(require xml)

; This function builds a complete HTTP response with proper XHTML page from a language, head and body, and wraps
; it in the proper doctype and opening XML-tag. I also need conditional comments, which xexpr->string happily ignores.
; [language] is a string with the language of the content, for example "en" for english or "sv" for swedish.
; [head] is an xexpr with the complete head-tag. (hint: Use xhtml-head to generate this.)
; [body] is an xexpr with the complete body-tag.
(define (xhtml-response language head body)
  ; We need to make sure the empty tag shorthand will only be used where apropriate.
  (empty-tag-shorthand html-empty-tags)
  ; We need to return a full response struct, since an xexpr can't represent everything we need.
  (make-response/full 200 "Okay" (current-seconds) TEXT/HTML-MIME-TYPE empty
                      ; The actual page as a list of strings.
                      (list
                       ; The opening XML-tag. You do use UTF-8, right? RIGHT!?
                       "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
                       ; We need a string port to write to.
                       (let ((port (open-output-string)))
                         ; display-xml is nice for development, since it outputs readable XHTML.
                         ; However, the result is not always what you want. Take a look at the bunny-link
                         ; in the example below. FF3 will link even the space after the link-tag. Ugly.
                         ; In production, you would probably want to use the unreadable write-xml instead.
                         
                         ; Render the XML structure of the page to a string.
                         (write-xml
                          ; We'll need a document struct...
                          (make-document
                           ; ...which takes a "prolog"...
                           (make-prolog
                            empty
                            ; ..which takes a document type (the "!DOCTYPE")...
                            (make-document-type
                             'html
                             ; ...which takes a DTD.
                             (make-external-dtd/public
                              "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
                              "-//W3C//DTD XHTML 1.0 Strict//EN")
                             #f))
                           ; Now, the only thing missing is the actual document content.
                           ; We'll get it by converting the document xexpr to an XML struct.
                           (xexpr->xml
                            ; The xexpr we get by wrapping the supplied head and body tags
                            ; with a root HTML-tag. Oh, and we need that xmlns attribute!
                            `(html ((xmlns "http://www.w3.org/1999/xhtml")
                                    (xml:lang "en")
                                    (lang ,language))
                                   ,head
                                   ,body))
                           empty)
                          ; We'll write to the string port we opened earlier.
                          port)
                         ; And return the string of the complete document.
                         (get-output-string port)))))

Don't be afraid. It's not complicated, just a bit verbose. Copy-paste all this into your definitions window and modify your response function to take advantage of it:

(define (my-response not-used-request)
  (xhtml-response "en" '(head) '(body (p "Hello World"))))

Push the run button in DrScheme and update your browser window. The HTML source should now look like this:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"><head></head><body><p>Hello World</p></body></html>

That's better!

Conditional comments

Internet Explorer (ie) 6 is the browser that cause me the most grief, insisting to render some things differently than everyone else. Fortunately, all ie browsers supports conditional comments. This is a completely valid (X)HTML comment with a special syntax inside. All versions of ie will recognize the condition syntax and parse the content if it matches the version number. Any other browser will just ignore it.

; This little function will build a conditional comment for ie browsers. Mighty useful.
; [condition] is a string in the ie condition format, for example "if lte IE 6".
; [content] is an xexpr representing the content.
; The return value is a comment struct, that can be used in another xexpr.
; NOTE!!! This can't be rendered with xexpr->string, since it will ignore all comments.
; Use write-xml or display-xml instead.
(define (conditionalize condition content)
  (make-comment (string-append "[" condition "]>" (xexpr->string content) "<![endif]")))

Beware that the conditional comment will be ignored if you try to pass them through the function xexpr->string, which is the function used by the standard servlet code. Instead, the xexpr needs to be converted to an XML object with the function xexpr->xml. The XML object can then be written to a port with the comments intact. See the function xhtml-response above for details.

Curing the head-ache

With the conditional comments, I have everything I need to start writing a decent head tag with conditionally included CSS files. But writing head tags gets incredibly tedious, so lets define a function that does it for us.

; This function simplifies building a proper head-tag.
; [title] is a string of the page title.
; [description] is a string with a short summary of the content.
; [keywords] is a list of keyword strings.
; [javascripts] is a list of strings, each one a path to a javascript. 
; [stylesheets] is a list of strings, each one a path to a stylesheet. 
; [ie-stylesheets] is a list of dotted pairs of an ie-formatted condition and a path to a stylesheet.
; You should probably only call this function from some
; template, and pick the SEO stuff from your page-struct.
(define (xhtml-head
         title
         description
         keywords
         javascripts
         stylesheets
         ie-stylesheets)
  ; An abstraction of the link-tag, which is used both for normal and conditional stylesheets.  
  (define (build-stylesheet-link stylesheet)
    `(link ((rel "stylesheet")
            (title "standard")
            (media "screen")
            (href ,stylesheet))))
  `(head (title ,title)
         (meta ((name "description")
                (content ,description)))
         (meta ((name "keywords")
                (content ,(string-join keywords ", "))))
         ,@(map (lambda (javascript)
                  `(script ((type "text/javascript")
                            (src ,javascript))))
                javascripts)
         ,@(map build-stylesheet-link
                stylesheets)
         ,@(map (lambda (ie-stylesheet)
                  (conditionalize (car ie-stylesheet) (build-stylesheet-link (cdr ie-stylesheet))))
                ie-stylesheets)))

Again, copy-paste the new definitions into DrScheme and modify the response function to use the head-constuctor:

(define (my-response not-used-request)
  (xhtml-response "en"
                  (xhtml-head "Hello World with XHTML"
                              "A simple Hello World example page build with the PLT Scheme webserver."
                              '("Hello World" "XHTML" "PLT Scheme")
                              '("behaviour/jquery.js"
                                "behaviour/carousell.js"
                                "behaviour/lightbox.js")
                              '("design/reset.css"
                                "design/layout.css"
                                "design/typography.css")
                              '(("if IE 6" . "design/ie6.css")
                                ("if IE 7" . "design/ie7.css")))
                  '(body (p "Hello World"))))

Oopsie! Out Hello World code suddenly grew quite large. Well, it needed to, since this is supposed to reflect a realistic scenario, where we need to include a few external stylesheets and javascripts. The HTML source should now look like this (sans linebreaks and indention):

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
	<head>
		<title>Hello World with XHTML</title>
		<meta name="description" content="A simple Hello World example page build with the PLT Scheme webserver." />
		<meta name="keywords" content="Hello World, XHTML, PLT Scheme" />
		<script type="text/javascript" src="behaviour/jquery.js"></script>
		<script type="text/javascript" src="behaviour/carousell.js"></script>
		<script type="text/javascript" src="behaviour/lightbox.js"></script>
		<link rel="stylesheet" title="standard" media="screen" href="design/reset.css" />
		<link rel="stylesheet" title="standard" media="screen" href="design/layout.css" />
		<link rel="stylesheet" title="standard" media="screen" href="design/typography.css" />
		<!--[if IE 6]><link rel="stylesheet" title="standard" media="screen" href="design/ie6.css" /><![endif]-->
		<!--[if IE 7]><link rel="stylesheet" title="standard" media="screen" href="design/ie7.css" /><![endif]-->
	</head>
	<body><p>Hello World</p></body>
</html>

Now is a good time to sit back and consider what we have achieved. We have defined three very helpful functions to deal with the nitty-gritty details of outputting well formed XHTML. Let's put them in their own file.

Now you need to know a little bit about the PLT module system. Each file should begin with the line:

#lang scheme

...followed by a list of the defined functions that should be available to code outside of the module. It should look something like this:

(provide conditionalize)
(provide xhtml-head)
(provide xhtml-response)

Because I'm such a nice person, I've already done this for you: xhtml.ss

Now you can "require" this file from your other code. Modify the code in your definitions window to look like this:

#lang scheme

(require "xhtml.ss")

(define (my-response not-used-request)
  (xhtml-response "en"
                  (xhtml-head "Hello World with XHTML"
                              "A simple Hello World example page build with the PLT Scheme webserver."
                              '("Hello World" "XHTML" "PLT Scheme")
                              '("behaviour/jquery.js"
                                "behaviour/carousell.js"
                                "behaviour/lightbox.js")
                              '("design/reset.css"
                                "design/layout.css"
                                "design/typography.css")
                              '(("if IE 6" . "design/ie6.css")
                                ("if IE 7" . "design/ie7.css")))
                  '(body (p "Hello World"))))

(require web-server/dispatchers/dispatch-pathprocedure)

(define some-dispatcher
  (make "/" my-response))

(require web-server/web-server)

(define stop-server
  (serve #:dispatch some-dispatcher
         #:port 8080))

Save the definitions window and the file xhtml.ss in the same directory, and it should all work.

This concludes this part of the tutorial. Next, we will extend the web server with our own custom dispatcher.