Monoidally construct web pages (fork of ertes's web-page)
root
Web-page
This package combines blaze-html, clay and jmacro into a framework-agnostic library to generate web pages dynamically from individual components. It is inspired by Yesod's widgets, but is more general, more powerful and can be used with all web frameworks.
Features
The Widget
type is very expressive. Features include:
Sane algebraic interface.
Works with your web framework of choice, as long as it supports blaze-html, clay and jmacro (or a subset of them).
Rendering to multiple resources (e.g. separate stylesheet and script).
Fully embedded stylesheet and script languages (jmacro and clay), so you can write your web pages entirely in Haskell.
Page-specific and external stylesheets and script.
Type-safe routing.
Monoidal piece-by-piece construction of pages.
Flexible polymorphic body type. You can compose Pandoc or plain text pages and later convert (map/traverse) them to HTML.
Multi-part bodies a.k.a. sections: You can construct pages composed of individual sections, and each part of your program can contribute to the various sections individually. (This is really just a consequence of the previous and the first point in this list.)
Hierarchial titles.
Optional lens and
MonadWriter
interfaces. You can also combine them and usescribe
andcensoring
to construct your pages from even richer or finer parts without writing your own combinators.Optional page-unique identifier generation, if you use a monad for constructing widgets.
Usage
This is a brief overview of the process to construct and render a web page using this library. The overall process consists of the following steps:
Widget: First construct a widget, which represents a web page component. In the sense of this library a full web page is a component, too.
Page: Render the widget to a page. At this point you reduce the rich structure of the widget to a set of strings and functions.
Resources: Realise the page by producing a set of resources (or files) that you can deliver to the client or load in a web browser.
The following sections explain each of these steps in detail. It is recommended to enable the OverloadedStrings
extension to get rid of repetitive string conversions. The remainder of this tutorial assumes that it is enabled.
Widgets
Your web page usually consists of multiple components that you have to combine. A composition of individual components is itself a component. This idea is implemented by the Widget
type:
Widget url h
Widget
is a product of the various parts of a web page. It contains an optional title, head markup, a body, a script, a stylesheet and links to external resources.
The first argument url
is the URL type. Typically you would want to use a custom type here that represents the valid routes of your web application (if you're writing one). However, type-safe routing is optional and not even handled directly by this library. Not to use it corresponds to setting url = Text
, which is what we will do here:
type MyWidget = Widget Text
The second and most important argument h
is the body type. The obvious choice is h = Html
, but most of the power of this library comes from leaving it polymorphic, which we will use very soon.
Construction
Widget
is a family of monoids. The easiest way to construct widgets is to construct them piece by piece using the Monoid
interface. This library defines many functions to make this very convenient. To construct a body widget simply use the pure
function:
pure :: h -> MyWidget h
Example:
import qualified Text.Blaze.Html5 as H
import Web.Page
widget1 :: MyWidget Html
widget1 =
pure (H.h1 "A title and" <>
H.p "a paragraph.")
As usual the type signatures are optional, but we will write them here for clarity.
While this library provides a powerful rendering mechanism, in some cases, especially when debugging or testing, you really just want to dump it on stdout using a quick and dirty one-liner:
hPutWidget
:: ([Text] -> Tl.Text)
-> Handle
-> Widget Text Html
-> IO ()
Most of this type signature is self-explanatory, except the first argument. It is the title renderer, which we will cover later. For now just use titleMinor " - "
:
import System.IO
main :: IO ()
main = hPutWidget (titleMinor " - ") stdout widget1
The output of this program should look very similar to the following (here multi-lined and indented for readability):
<!DOCTYPE HTML>
<html>
<head>
<title></title>
<meta charset="UTF-8">
</head>
<body>
<h1>A title and</h1>
<p>a paragraph.</p>
</body>
</html>
Congratulations. You have constructed and rendered your first widget.
Composing widgets
As noted earlier you would typically construct your widgets piece by piece. Beside the pure
function you will find the following functions handy for that:
caption :: (Monoid h) => Text -> MyWidget h
script :: (Monoid h) => JStat -> MyWidget h
style :: (Monoid h) => Css -> MyWidget h
The caption
function constructs a title widget -- a widget that contains only a title. Using Monoid
you can combine them to form a widget with a body and a title:
widget2 :: MyWidget Html
widget2 =
caption "Test page" <>
pure (H.h1 "Hello there") <>
pure (H.p "I'm a test page!")
Rendering this widget produces something like the following (most of the clutter removed for the sake of conciseness):
<head>
<title>Test page</title>
</head>
<body>
<h1>Hello there</h1>
<p>I'm a test page!</p>
</body>
Everything except the title can be composed, including the body, script and stylesheet. In particular pure
is a monoid morphism:
pure x <> pure y = pure (x <> y)
As noted the only exception is the title, which uses last-wins composition:
caption x <> caption y = caption y
This is necessary for hierarchial titles to behave the way you would expect.
A widget can contain more than what has been explained above. See the documentation of the Web.Page.Widget.Core
module for an overview.
Non-HTML bodies
The body of a widget does not have to be HTML. For example you can construct pure text widgets,
text1, text2 :: MyWidget Text
text1 = pure "just text"
text2 = pure "and some more text"
compose them (because Text
is a monoid),
text3 :: MyWidget Text
text3 = text1 <> pure " " <> text2
or even just use OverloadedStrings
,
text4 :: MyWidget Text
text4 =
"just text " <>
"and some more text"
which is actually a lot more general:
notQuiteText :: MyWidget Html
notQuiteText = "not quite" <> " text"
To render a widget the body must be of type Html
. Our text
widget is not, so we need to convert it first. As long as your conversion is just a function you can use the Functor
instance of Widget
(type simplified):
text :: MyWidget Text
fmap toHtml :: MyWidget Text -> MyWidget Html
fmap toHtml text :: MyWidget Html
If your conversion involves a functor you can use Traversable
instead:
traverse
:: (Applicative f)
=> (a -> f b)
-> MyWidget a -> f (MyWidget b)
Widget templates
(A.k.a. the Monad
instance that probably doesn't do what you thought it does.)
You can construct a widget with a hole that you can fill with markup elsewhere. The most common term for that concept is template. There are two ways to do it that differ in a subtlety. The first way is to write a widget function:
template :: Int -> String -> MyWidget Html
If the actual values come from widgets themselves,
intWidget :: MyWidget Int
strWidget :: MyWidget String
you would just use monadic composition:
do x <- intWidget
y <- strWidget
template x y
The template can derive anything from its arguments, including its title and its stylesheet. In some cases you might want to guarantee that a template derives only its body from its arguments, but still add a stylesheet that doesn't depend on the template arguments:
template2 :: MyWidget (Int -> String -> Html)
In this case you use applicative composition:
template2 <*> intWidget <*> strWidget
Widget composition is quite flexible. For example sometimes it's beneficial to separate the additional stylesheet from the actual body template:
template3 :: Int -> String -> Html
myStyle :: MyWidget ()
And here is how you apply it:
template3 <$> (myStyle *> intWidget)
<*> strWidget
Remember that ordering is relevant. If you want to add the styles of myStyle
after the styles of intWidget
and strWidget
, compose differently:
template3 <$> intWidget
<*> strWidget
<* myStyle
Multi-part widgets
A multi-part widget is really just a widget with a function as its body. Since functions with a monoid codomain form a monoid, we get most of the multi-part magic for free in Haskell.
This library does not specify what "part" means. A multi-part widget can denote anything from a page with multiple sections to a set of multiple pages. What it is depends on how you reduce such a widget to something that can be rendered.
Parts are indexed by a key, which can be anything with an Eq
instance. You can index parts by integers or strings, or you can make a more domain-specific key type:
data Section = Header | Menu | Content | Footer
deriving (Eq)
Now a widget can contribute to each section individually. You use the section
function for that purpose:
section :: (Eq k, Monoid h) => k -> h -> MyWidget (k -> h)
The result of this function is a widget that has the given content only in the given section and with all other sections empty. To construct a widget that contributes to multiple sections, just use composition:
mpWidget1 :: MyWidget (Section -> Html)
mpWidget1 =
section Menu (H.p "menu entries of widget1") <>
section Footer (H.p "footer of widget1")
mpWidget2 :: MyWidget (Section -> Html)
mpWidget2 =
section Menu (H.p "menu entries of widget2") <>
section Content (H.p "content of widget2")
When you compose these two widgets, their sections are appended separately:
mpWidget :: MyWidget (Section -> Html)
mpWidget = mpWidget1 <> mpWidget2
To render the widget its body type must be Html
, so we have to reduce it first. The way we reduce it gives concrete meaning to "multi-part". For multi-section pages you would use the flattenBody
function:
flattenBody
:: (Monoid h)
=> [k]
-> MyWidget (k -> h)
-> MyWidget h
This function takes a list of the sections you want to include in the resulting flattened widget, and the multi-section widget:
widget :: MyWidget Html
widget = flattenBody [Header, Menu, Content, Footer] mpWidget
Now you have a regular HTML widget that you can render. The body of the resulting document looks like this (comments added):
<!-- menu section -->
<p>menu entries of widget1</p>
<p>menu entries of widget2</p>
<!-- content section -->
<p>content of widget2</p>
<!-- footer section -->
<p>footer of widget1</p>
You can see that sections were composed separately.
Sometimes you have a lot of pages which are all pretty much the same, except for the body. You can represent such a set of pages as a multi-part widget:
manyPages :: MyWidget (PageIndex -> Html)
Now instead of flattening the widget you traverse it:
import Data.Traversable (sequenceA)
sequenceA :: MyWidget (k -> h) -> k -> MyWidget h
This pulls out the (k ->)
to the front and gives you an indexed widget that you can render:
sequenceA manyPages :: PageIndex -> MyWidget h
You probably have noticed the similarity between multi-part widgets and templates. In particular applicative templates are just widgets with function-typed bodies, too. That makes sense, because technically there is no difference between them. Both are widgets with indexed bodies, but typically the index type is finite for a multi-part widget. You probably don't construct a page with infinitely many sections, because delivering it to a client would take a long time. =)
Hierarchial titles
On most multi-page sites it is very convenient to construct the title piece by piece as well hierarchially. The following two functions do exactly that:
caption :: (Monoid h) => Text -> MyWidget h
title :: Text -> MyWidget h -> MyWidget h
We have already seen the caption
function that allows you to give your widget a title:
caption "Page title" <>
pure (H.h1 "Heading")
Given a widget with a title the title
function wraps it up in a higher-level title:
title "Site title" $
caption "Page title" <>
pure (H.h1 "Heading")
This is a widget with two title fragments in hierarchial order, the higher level title being "Site title" and the lower level title being "Page title". When composing multiple widgets that have a title, the last title wins. In particular in the following widget the title "subtitle1" is ignored:
title "title" $
caption "subtitle1" <>
title "subtitle2" (caption "subsubtitle")
In hierarchial order the resulting title will be:
"title" / "subtitle2" / "subsubtitle"
To understand why, decompose it first:
w1 = caption "subtitle1"
w2 = title "subtitle2" (caption "subsubtitle")
You can see that both w1
and w2
set a title. When you compose them (w1 <> w2
), the title of the latter wins.
Not all widgets set a title, and applying title
to a widget that doesn't set a title has no effect. Unless you manually apply the Widget
constructor a widget only sets a title, if it uses caption
or hasTitle
:
hasTitle :: (Monoid h) => MyWidget h
The hasTitle
function is useful in some hierarchial scenarios. It sets an empty title:
w = hasTitle <> pure (H.p "some body")
This widget does set a title, but an empty one (as opposed to a widget that sets no title at all). Now all title fragments are added via the title
function:
title "title" $
title "subtitle" $
title "subsubtitle" w
When rendering a widget you need to provide a title renderer, which is a function of the following type (the type alias is not actually defined by this library):
import qualified Data.Text.Lazy as Tl
type TitleRenderer = [Text] -> Tl.Text
Such a function receives the individual title fragments in order from highest level to lowest level. It then constructs a title string, for example by intercalating a separator character between the fragments. Since intercalating is so common, there are two predefined renderers that do exactly that:
titleMajor :: Text -> TitleRenderer
titleMinor :: Text -> TitleRenderer
Examples:
titleMajor " / " ["site", "department", "page"] =
"site / department / page"
titleMinor " - " ["site", "department", "page"] =
"page - department - site"
In most cases you would want to use the titleMinor
function, because it's usually more convenient for the user to have the page title in the leftmost position. Browser tab captions and title bars can quickly become too small to display the whole title, and the page title is usually much more informative than the site title.
Widget writers
Since widgets form a family of monoids you can also use a writer monad to construct them. This library defines a number of functions to make this very convenient. See the documentation of the Web.Page.Widget.Writer
module for a full overview. Using a writer monad also plays well with identifier generation, if you need it.
In most cases the correct type will be inferred, but we will specify it regardless:
myWidget :: (MonadWriter (Widget url Html) m) => m ()
If you are like me, you prefer to write type signatures for your top-level definitions. A constraint alias is provided for your convenience. The following type signature is equivalent to the above:
myWidget :: (MonadWidget url Html m) => m ()
If writer is the only effect you need, there is an even simpler alias that you can use, which is equivalent to the above as well:
myWidget :: WidgetWriter url Html ()
Now we can construct the widget piece by piece:
myWidget = do
setTitle "Hello"
addBody (H.h1 "Hello")
addBody (H.p "Hello world!")
addStyle $ html ? do
background white
color black
You can build the widget by reducing the writer:
w :: Widget url Html
w = execWriter myWidget
This widget can now be rendered to a page.
Rendering to pages
So far we have only seen the hPutWidget
function, which is very convenient for simple applications and static sites. However, it hides most of the power of the underlying renderer. This section explains how to use it.
To render a widget you can use the renderWidget
function:
import qualified Data.Text.Lazy as Tl
renderWidget
:: ([Text] -> Tl.Text)
-> Widget Text Html
-> Page
The first argument to this function is the title renderer as explained above. In most cases you would simply use the titleMinor
function:
page :: Page
page = renderWidget (titleMinor " - ") w
Pages are an intermediate step between rendering and delivery. They are necessary, because this library allows you to render to multiple resources, for example to a markup document, a stylesheet and a script. You can then use a clever hash-based routing mechanism to tell clients to cache stylesheets and scripts forever and reduce the required bandwidth to a minimum.
Realisation
To process of generating from a page an actual set of resources that you can deliver is referred to as realisation. To simply render to a single document with an inline stylesheet and an inline script you can use the realiseInline
function:
realiseInline :: Page -> Builder
All we need to do is to apply it to our page:
document :: Builder
document = realiseInline page
The Builder
type is the one from blaze-builder. Most web frameworks use it for efficient bytestring concatenation and provide a simple interface to deliver those strings to clients. For example WAI provides the responseBuilder
function. If you want to save the result to a file instead, just use toLazyByteString
or toByteStringIO
.