Scaffolding and the Site Template
So you're tired of running small examples, and ready to write a real site? Then you're at the right chapter. Even with the entire Yesod library at your fingertips, there are still a lot of steps you need to go through to get a production-quality site setup:
- Config file parsing
- Signal handling (*nix)
- More efficient static file serving
- A good file layout
The scaffolded site is a combination of many Yesoders' best practices combined together into a ready-to-use skeleton for your sites. It is highly recommended for all sites. This chapter will explain the overall structure of the scaffolding, how to use it, and some of its less-than-obvious features.
How to Scaffold
The yesod package installs both a library and an executable (conveniently named yesod as well). This executable provides a few commands (run yesod by itself to get a list). In order to generate a scaffolding, the command is yesod init. This will start a question-and-answer process where you get to provide basic details (your name, the project name, etc). After answering the questions, you will have a site template in a subfolder with the name of your project.
The most important of these questions is the database backend. You get four choices here: SQLite, PostgreSQL, MongoDB, and tiny. tiny is not a database backend; instead, it is specifying that you do not want to use any database. This option also turns off a few extra dependencies, giving you a leaner overall site. The remainder of this chapter will focus on the scaffoldings for one of the database backends. There will be minor differences for the tiny backend.
After creating your files, the scaffolder will print a message about getting started. It gives two sets of options for commands: one using cabal, and the other using cabal-dev. cabal-dev is basically a wrapper around cabal which causes all dependencies to be built in a sandbox. Using it is a good way to ensure that installing other packages will not break your site setup. It is strongly recommended. If you don't have cabal-dev, you can install it by running cabal install cabal-dev.
Note that you really do need to use the cabal install
(or cabal-dev install) command. Most likely, you do not yet have all
the dependencies in place needed by your site. For example, none of the database backends, nor
the Javascript minifier (hjsmin) are installed when installing the
yesod
package.
Finally, to launch your development site, you would use yesod devel (or yesod --dev devel). This site will automatically reload whenever you change your code.
File Structure
The scaffolded site is built as a fully cabalized Haskell package. In addition to source files, config files, templates, and static files are produced as well.
Cabal file
Whether directly using cabal, or indirectly using yesod devel, building your code will always go through the cabal file. If
you open the file, you'll see that there are both library and executable blocks. Only one of
these is built at a time, depending on the value of the library-only
flag. If library-only
is turned on, then the library is built, which
is how yesod devel calls your app. Otherwise, the executable is
built.
The library-only
flag should only be used by
yesod devel; you should never be explicitly passing it into
cabal. There is an additional flag, dev
, that
allows cabal to build an executable, but turns on some of the same features as
the library-only flag, i.e., no optimizations and reload versions of the Shakespearean
template functions.
In general, you will build as follows:
- When developing, use yesod devel exclusively.
- When building a production build, perform cabal clean
&& cabal configure && cabal build. This will produce an
optimized executable in your
dist
folder.
You'll also notice that we specify all language extensions in the cabal file. The extensions are specified twice: once for the executable, and once for the library. If you add any extensions to the list, add it to both places.
You might be surprised to see the NoImplicitPrelude
extension. We turn this
on since the site includes its own module, Import
, with a few changes to the
Prelude that make working with Yesod a little more convenient.
The last thing to note is the exported-modules list. If you add any modules to your application, you must update this list to get yesod devel to work correctly. Unfortunately, neither Cabal nor GHC will give you a warning if you forgot to make this update, and instead you'll get a very scary-looking error message from yesod devel.
Routes and entities
Multiple times in this book, you've seen a comment like "We're declaring our routes/entities with quasiquotes for convenience. In a production site, you should use an external file." The scaffolding uses such an external file.
Routes are defined in config/routes
, and entities in
config/models
. They have the exact same syntax as the quasiquoting
you've seen throughout the book, and yesod devel knows to automatically
recompile the appropriate modules when these files change.
The models
files is referenced by Model.hs
. You are free to declare whatever you like in this file, but here are some
guidelines:
- Any data types used in
entities
must be imported/declared inModel.hs
, above thepersistFile
call. - Helper utilities should either be declared in
Import.hs
or, if very model-centric, in a file within theModel
folder and imported intoImport.hs
.
Foundation and Application modules
The mkYesod
function which we have used throughout the book declares a few
things:
- Route type
- Route render function
- Dispatch function
The dispatch function refers to all of the handler functions, and therefore all of those must either be defined in the same file as the dispatch function, or be imported by the dispatch function.
Meanwhile, the handler functions will almost certainly refer to the route type. Therefore, they must be either in the same file where the route type is defined, or must import that file. If you follow the logic here, your entire application must essentially live in a single file!
Clearly this isn't what we want. So instead of using mkYesod
, the scaffolding
site uses a decomposed version of the function. Foundation
calls
mkYesodData
, which declares the route type and render function. Since it does
not declare the dispatch function, the handler functions need not be in scope.
Import.hs
imports Foundation.hs
, and all the handler modules
import Import.hs
.
In Application.hs
, we call mkYesodDispatch
, which creates our
dispatch function. For this to work, all handler functions must be in scope, so be sure to add an
import statement for any new handler modules you create.
Other than that, Application.hs
is pretty simple. It provides
two functions: withDevelAppPort
is used by yesod
devel to launch your app, and getApplication
is used by the
executable to launch.
Foundation.hs
is much more exciting. It:
- Declares your foundation datatype
- Declares a number of instances, such as
Yesod
,YesodAuth
, andYesodPersist
- Imports the messages files. If you look for the line starting with
mkMessage
, you will see that it specifies the folder containing the messages (messages
) and the default language (en, for English).
This is the right file for adding extra instances for your foundation, such as
YesodAuthEmail
or YesodBreadcrumbs
.
We'll be referring back to this file later, as we discussed some of the special
implementations of Yesod
typeclass methods.
Import
The Import
module was born out of a few commonly recurring
patterns.
- I want to define some helper functions (maybe the
<> = mappend
operator) to be used by all handlers. - I'm always adding the same five import statements (
Data.Text
,Control.Applicative
, etc) to every handler module. - I want to make sure I never use some evil function (
head
,readFile
, ...) fromPrelude
.
The solution is to turn on the NoImplicitPrelude
language extension,
re-export the parts of Prelude
we want, add in all the other stuff we want,
define our own functions as well, and then import this file in all handlers.
Handler modules
Handler modules should go inside the Handler
folder. The site
template includes one module: Handler/Root.hs
. How you split up your handler
functions into individual modules is your decision, but a good rule of thumb is:
- Different methods for the same route should go in the same file, e.g.
getBlogR
andpostBlogR
. - Related routes can also usually go in the same file, e.g.,
getPeopleR
andgetPersonR
.
Of course, it's entirely up to you. When you add a new handler file, make sure you do the following:
- Add it to version control (you are using version control, right?).
- Add it to the cabal file.
- Add it to the
Application.hs
file. - Put a module statement at the top, and an
import Import
line below it.
widgetFile
It's very common to want to include CSS and Javascript specific to a page. You don't want to
have to remember to include those Lucius and Julius files manually every time you refer to a
Hamlet file. For this, the site template provides the widgetFile
function.
If you have a handler function:
getRootR = defaultLayout $(widgetFile "homepage")
, Yesod will look for the following files:
templates/homepage.hamlet
templates/homepage.lucius
templates/homepage.cassius
templates/homepage.julius
If any of those files are present, they will be automatically included in the output.
defaultLayout
One of the first things you're going to want to customize is the look of your site. The layout is actually broken up into two files:
templates/default-layout-wrapper.hamlet
contains just the basic shell of a page. This file is interpreted as plain Hamlet, not as a Widget, and therefore cannot refer to other widgets, embed i18n strings, or add extra CSS/JS.templates/default-layout.hamlet
is where you would put the bulk of your page. You must remember to include thewidget
value in the page, as that contains the per-page contents. This file is interpreted as a Widget.
Also, since default-layout is included via the widgetFile
function,
any Lucius, Cassius, or Julius files named default-layout.*
will
automatically be included as well.
Static files
The scaffolded site automatically includes the static file subsite, optimized for serving files that will not change over the lifetime of the current build. What this means is that:
- When your static file identifiers are generated (e.g.,
static/mylogo.png
becomesmylogo_png
), a query-string parameter is added to it with a hash of the contents of the file. All of this happens at compile time. - When
yesod-static
serves your static files, it sets expiration headers far in the future, and incldues an etag based on a hash of your content. - Whenever you embed a link to
mylogo_png
, the rendering includes the query-string parameter. If you change the logo, recompile, and launch your new app, the query string will have changed, causing users to ignore the cached copy and download a new version.
Additionally, you can set a specific static root in your
Settings.hs
file to serve from a different domain name. This has the
advantage of not requiring transmission of cookies for static file requests, and also lets you
offload static file hosting to a CDN or a service like Amazon S3. See the comments in the file
for more details.
Another optimization is that CSS and Javascript included in your widgets will not be included inside your HTML. Instead, their contents will be written to an external file, and a link given. This file will be named based on a hash of the contents as well, meaning:
- Caching works properly.
- Yesod can avoid an expensive disk write of the CSS/Javascript file contents if a file with the same hash already exists.
Finally, all of your Javascript is automatically minified via hjsmin.
Conclusion
The purpose of this chapter was not to explain every line that exists in the scaffolded site, but instead to give a general overview to how it works. The best way to become more familiar with it is to jump right in and start writing a Yesod site with it.