Creating a Subsite

How many sites provide authentication systems? Or need to provide create-read-update-delete (CRUD) management of some objects? Or a blog? Or a wiki?

The theme here is that many websites include common components that can be reused throughout multiple sites. However, it is often quite difficult to get code to be modular enough to be truly plug-and-play: a component will require hooks into the routing system, usually for multiple routes, and will need some way of sharing styling information with the master site.

In Yesod, the solution is subsites. A subsite is a collection of routes and their handlers that can be easily inserted into a master site. By using type classes, it is easy to ensure that the master site provides certain capabilities, and to access the default site layout. And with type-safe URLs, it’s easy to link from the master site to subsites.

Hello World

Perhaps the trickiest part of writing subsites is getting started. Let’s dive in with a simple Hello World subsite. We need to create one module to contain our subsite’s data types, another for the subsite’s dispatch code, and then a final module for an application that uses the subsite.

-- @HelloSub/Data.hs
{-# LANGUAGE QuasiQuotes     #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies    #-}
module HelloSub.Data where

import           Yesod

-- Subsites have foundations just like master sites.
data HelloSub = HelloSub

-- We have a familiar analogue from mkYesod, with just one extra parameter.
-- We'll discuss that later.
mkYesodSubData "HelloSub" [parseRoutes|
/ SubHomeR GET
-- @HelloSub.hs
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE QuasiQuotes           #-}
{-# LANGUAGE TemplateHaskell       #-}
module HelloSub
    ( module HelloSub.Data
    , module HelloSub
    ) where

import           HelloSub.Data
import           Yesod

-- And we'll spell out the handler type signature.
getSubHomeR :: Yesod master => SubHandlerFor HelloSub master Html
getSubHomeR = liftHandler $ defaultLayout [whamlet|Welcome to the subsite!|]

instance Yesod master => YesodSubDispatch HelloSub master where
    yesodSubDispatch = $(mkYesodSubDispatch resourcesHelloSub)
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes       #-}
{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE TypeFamilies      #-}
import           HelloSub
import           Yesod

-- And let's create a master site that calls it.
data Master = Master
    { getHelloSub :: HelloSub

mkYesod "Master" [parseRoutes|
/ HomeR GET
/subsite SubsiteR HelloSub getHelloSub

instance Yesod Master

-- Spelling out type signature again.
getHomeR :: HandlerFor Master Html
getHomeR = defaultLayout
        <h1>Welcome to the homepage
            Feel free to visit the #
            <a href=@{SubsiteR SubHomeR}>subsite
            \ as well.

main = warp 3000 $ Master HelloSub

This simple example actually shows most of the complications involved in creating a subsite. Like a normal Yesod application, everything in a subsite is centered around a foundation datatype, HelloSub in our case. We then use mkYesodSubData to generate our subsite route data type and associated parse and render functions.

On the dispatch side, we start off by defining our handler function for the SubHomeR route. You should pay special attention to the type signature on this function:

getSubHomeR :: Yesod master
            => SubHandlerFor HelloSub master Html

This is the heart and soul of what a subsite is all about. All of our actions live in this layered monad, where we have our subsite wrapping around our main site. Given this monadic layering, it should come as no surprise that we end up calling liftHandler. In this case, our subsite is using the master site’s defaultLayout function to render a widget.

The defaultLayout function is part of the Yesod typeclass. Therefore, in order to call it, the master type argument must be an instance of Yesod. The advantage of this approach is that any modifications to the master site’s defaultLayout method will automatically be reflected in subsites.

When we embed a subsite in our master site route definition, we need to specify four pieces of information: the route to use as the base of the subsite (in this case, /subsite), the constructor for the subsite routes (SubsiteR), the subsite foundation data type (HelloSub) and a function that takes a master foundation value and returns a subsite foundation value (getHelloSub).

In the definition of getHomeR, we can see how the route constructor gets used. In a sense, SubsiteR promotes any subsite route to a master site route, making it possible to safely link to it from any master site template.