diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2016 Tony Morris
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions, the following disclaimer and the following SHA-512 checksum.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions, the following disclaimer and the following SHA-512
+checksum in the documentation and/or other materials provided with the
+distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+THIS SOFTWARE IS PROVIDED WITH A SHA-512 CHECKSUM.
+ecdfdf543fd166b13b76b11e0b010e00d693028771880ffd8f9252ae0b8ac42bddf8eb7a23c21c86a3b50039d642ad9eeb7e8a0e82f6a345f60feb4a54808362
diff --git a/Setup.lhs b/Setup.lhs
new file mode 100644
--- /dev/null
+++ b/Setup.lhs
@@ -0,0 +1,44 @@
+#!/usr/bin/env runhaskell
+\begin{code}
+{-# OPTIONS_GHC -Wall #-}
+module Main (main) where
+
+import Data.List ( nub )
+import Data.Version ( showVersion )
+import Distribution.Package ( PackageName(PackageName), PackageId, InstalledPackageId, packageVersion, packageName )
+import Distribution.PackageDescription ( PackageDescription(), TestSuite(..) )
+import Distribution.Simple ( defaultMainWithHooks, UserHooks(..), simpleUserHooks )
+import Distribution.Simple.Utils ( rewriteFile, createDirectoryIfMissingVerbose )
+import Distribution.Simple.BuildPaths ( autogenModulesDir )
+import Distribution.Simple.Setup ( BuildFlags(buildVerbosity), fromFlag )
+import Distribution.Simple.LocalBuildInfo ( withLibLBI, withTestLBI, LocalBuildInfo(), ComponentLocalBuildInfo(componentPackageDeps) )
+import Distribution.Verbosity ( Verbosity )
+import System.FilePath ( (</>) )
+
+main :: IO ()
+main = defaultMainWithHooks simpleUserHooks
+  { buildHook = \pkg lbi hooks flags -> do
+     generateBuildModule (fromFlag (buildVerbosity flags)) pkg lbi
+     buildHook simpleUserHooks pkg lbi hooks flags
+  }
+
+generateBuildModule :: Verbosity -> PackageDescription -> LocalBuildInfo -> IO ()
+generateBuildModule verbosity pkg lbi = do
+  let dir = autogenModulesDir lbi
+  createDirectoryIfMissingVerbose verbosity True dir
+  withLibLBI pkg lbi $ \_ libcfg -> do
+    withTestLBI pkg lbi $ \suite suitecfg -> do
+      rewriteFile (dir </> "Build_" ++ testName suite ++ ".hs") $ unlines
+        [ "module Build_" ++ testName suite ++ " where"
+        , "deps :: [String]"
+        , "deps = " ++ (show $ formatdeps (testDeps libcfg suitecfg))
+        ]
+  where
+    formatdeps = map (formatone . snd)
+    formatone p = case packageName p of
+      PackageName n -> n ++ "-" ++ showVersion (packageVersion p)
+
+testDeps :: ComponentLocalBuildInfo -> ComponentLocalBuildInfo -> [(InstalledPackageId, PackageId)]
+testDeps xs ys = nub $ componentPackageDeps xs ++ componentPackageDeps ys
+
+\end{code}
diff --git a/casr-logbook-html.cabal b/casr-logbook-html.cabal
new file mode 100644
--- /dev/null
+++ b/casr-logbook-html.cabal
@@ -0,0 +1,78 @@
+name:               casr-logbook-html
+version:            0.0.1
+license:            OtherLicense
+license-file:       LICENSE
+author:             Tony Morris <ʇǝu˙sıɹɹoɯʇ@ןןǝʞsɐɥ>
+maintainer:         Tony Morris
+copyright:          Copyright (C) 2016 Tony Morris
+synopsis:           CASR 61.345 Pilot Personal Logbook HTML output
+category:           Aviation
+description:        
+  <<https://i.imgur.com/p6LT40r.png>>
+  .
+  CASR 61.345 Pilot Personal Logbook HTML output
+  .
+  <<https://i.imgur.com/Lfhcmtg.png>>
+
+homepage:           https://github.com/tonymorris/casr-logbook-html
+bug-reports:        https://github.com/tonymorris/casr-logbook-html/issues
+cabal-version:      >= 1.10
+build-type:         Custom
+extra-source-files: changelog
+
+source-repository   head
+  type:             git
+  location:         git@github.com:tonymorris/casr-logbook-html.git
+
+flag                small_base
+  description:      Choose the new, split-up base package.
+
+library
+  default-language:
+                    Haskell2010
+
+  build-depends:
+                    base < 5 && >= 4.8
+                    , casr-logbook >= 0.1.3 && < 0.2
+                    , lucid >= 2.9 && < 3
+                    , lens >= 4.1 && < 5
+                    , digit >= 0.2.8 && < 3
+                    , time >= 1.5 && < 1.6
+                    , text >= 1.2 && < 1.3
+
+  ghc-options:
+                    -Wall
+
+  default-extensions:
+                    NoImplicitPrelude
+
+  hs-source-dirs:
+                    src
+
+  exposed-modules:
+                    Data.Aviation.Casr.Logbook.Html.Html
+
+test-suite doctests
+  type:
+                    exitcode-stdio-1.0
+
+  main-is:
+                    doctests.hs
+
+  default-language:
+                    Haskell2010
+
+  build-depends:
+                    base < 5 && >= 3
+                    , doctest >= 0.9.7
+                    , filepath >= 1.3
+                    , directory >= 1.1
+                    , QuickCheck >= 2.0
+                    , template-haskell >= 2.8
+
+  ghc-options:
+                    -Wall
+                    -threaded
+
+  hs-source-dirs:
+                    test
diff --git a/changelog b/changelog
new file mode 100644
--- /dev/null
+++ b/changelog
@@ -0,0 +1,4 @@
+0.0.1
+
+* Initial release
+
diff --git a/src/Data/Aviation/Casr/Logbook/Html/Html.hs b/src/Data/Aviation/Casr/Logbook/Html/Html.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Aviation/Casr/Logbook/Html/Html.hs
@@ -0,0 +1,691 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Data.Aviation.Casr.Logbook.Html.Html(
+  htmlTimeAmount
+, strTimeAmount
+, strEngine
+, htmlAircraft
+, htmlRatingDay
+, htmlRating
+, htmlRatingShort
+, htmlRatings
+, htmlRatingsShort
+, htmlAviatorName
+, htmlAviatorARN
+, htmlAviatorDob
+, htmlAviatorRatings
+, htmlAviator
+, htmlAviatorShort
+, htmlFlightPoint
+, htmlFlightPath
+, htmlCommand
+, htmlTimeAmountZero
+, htmlTimeAmountZeroWith 
+, htmlAviators
+, htmlAircraftFlightName
+, htmlAircraftFlight
+, htmlTimeOfDayTime
+, htmlTime
+, htmlFlightPathTime
+, htmlSimulatorFlightName
+, htmlSimulatorFlight
+, htmlLocation
+, htmlExamResult
+, htmlExamName
+, htmlExam
+, htmlBriefingName
+, htmlBriefing
+, space2dot
+, htmlEntryTag
+, htmlEntry 
+, htmlEntries 
+, htmlLogbook
+, htmlTitleAviator
+, htmlLogbookDocument
+, htmlLogbookHeader
+) where
+
+import Control.Applicative((*>))
+import Control.Category((.), id)
+import Control.Lens((^.))
+import Control.Monad(when, (=<<), (>>=))
+import Data.Aviation.Casr.Logbook(
+    arn
+  , firstname
+  , surname
+  , logbookaviator
+  , daytime
+  , briefingTime
+  , briefingName
+  , examName
+  , examTime
+  , simulatorflightname
+  , simulatortype
+  , point
+  , flightStart
+  , flightEnd
+  , flightpath
+  , aircraftRegistration
+  , aircraftflightname
+  , flightaircraft
+  , landingTime
+  , zerotimeamount
+  , flightPathList
+  , Command(ICUS, Dual, InCommand)
+  , Rating(Rating)
+  , Aircraft(Aircraft)
+  , Engine(Single, Multi)
+  , FlightPath
+  , FlightPoint(FlightPoint)
+  , Time(Time)
+  , TimeAmount(TimeAmount)
+  , DayNight(DayNight)
+  , Briefing(Briefing)
+  , Exam(Exam)
+  , Location(Location)
+  , Aviator(Aviator)
+  , Entries(Entries)
+  , Logbook(Logbook)
+  , AircraftFlight(AircraftFlight)
+  , SimulatorFlight(SimulatorFlight)
+  , Briefing
+  , Exam
+  , Entry(BriefingEntry, ExamEntry, SimulatorFlightEntry, AircraftFlightEntry)
+  )
+import Data.Bool(not)
+import Data.Char(toUpper)
+import Data.Digit(Digit)
+import Data.Eq((==))
+import Data.Foldable(fold, sequence_, mapM_, null)
+import Data.Function(($))
+import Data.Functor((<$>))
+import Data.Int(Int)
+import Data.List(intersperse, concat)
+import Data.Maybe(Maybe, maybe)
+import Data.Monoid(Monoid, (<>), mempty)
+import Data.String(String, fromString)
+import Data.Time(Day, TimeOfDay)
+import Data.Text(Text)
+import qualified Data.Text as Text(pack)
+import Lucid(
+    id_
+  , class_
+  , h1_
+  , h2_
+  , h3_
+  , span_
+  , a_
+  , div_
+  , href_
+  , src_
+  , type_
+  , script_
+  , body_
+  , title_
+  , rel_
+  , link_
+  , title_
+  , html_
+  , doctype_
+  , head_
+  , lang_
+  , hr_
+  , ul_
+  , li_
+  , Html
+  , toHtmlRaw
+  )
+import Text.Printf(printf)
+
+import Prelude(show, fromIntegral, (/), (*), Double)
+
+htmlTimeAmount ::
+  TimeAmount
+  -> Html ()
+htmlTimeAmount t =
+  span_ [] $
+    do  fromString (strTimeAmount t)
+        "hrs"
+
+strTimeAmount ::
+  TimeAmount
+  -> String
+strTimeAmount (TimeAmount h x) =
+  show h <> "." <> show x
+
+strEngine ::
+  Engine
+  -> String
+strEngine Single =
+  "single-engine"
+strEngine Multi =
+  "multi-engine"
+
+htmlAircraft ::
+  AircraftFlight
+  -> Aircraft
+  -> Html ()
+htmlAircraft _ (Aircraft t r e) =
+  span_ [class_ "aircraft"] $
+    do  span_ [class_ "aircrafttype"] (fromString t)
+        " "
+        span_ [class_ "aircraftregistration"] (fromString r)
+        " "
+        span_ [class_ "aircraftengine"] (fromString (strEngine e))
+
+htmlRatingDay ::
+  Maybe Day
+  -> Html ()
+htmlRatingDay =
+  maybe mempty (\q -> 
+    do  " "
+        span_ [] $
+          fromString (show q))
+
+htmlRating ::
+  Rating
+  -> Html ()
+htmlRating (Rating n d) =
+  span_ [] $
+    do  span_ [] (fromString n)
+        htmlRatingDay d
+
+htmlRatingShort ::
+  Rating
+  -> Html ()
+htmlRatingShort (Rating n _) =
+  span_ [] (fromString n)
+
+htmlRatings ::
+  [Rating]
+  -> Html ()
+htmlRatings =
+  sequence_ . intersperse ", " . (htmlRating <$>)
+
+htmlRatingsShort ::
+  [Rating]
+  -> Html ()
+htmlRatingsShort =
+  sequence_ . intersperse ", " . (htmlRatingShort <$>)
+
+htmlAviatorName ::
+  String
+  -> String
+  -> Html ()
+htmlAviatorName s f =
+  do  li_ [id_ "aviatorname"] $
+        do  span_ [class_ "key"] "Name: "
+            span_ [class_ "value"] $
+              do  fromString (toUpper <$> s)
+                  ", "
+                  fromString f
+
+htmlAviatorARN ::
+  [Digit]
+  -> Html ()
+htmlAviatorARN a =
+  when (not . null $ a) $
+    do  li_ [id_ "aviatorarn"] $ 
+          do  span_ [class_ "key"] "ARN: "
+              span_ [class_ "value"] $
+                fromString (a >>= show)
+
+htmlAviatorDob ::
+  Maybe Day
+  -> Html ()
+htmlAviatorDob =
+  maybe mempty (\q ->
+    do  li_ [id_ "aviatordob"] $
+          do  span_ [class_ "key"] "Date of Birth: "
+              span_ [class_ "value"] .
+                fromString . show $ q)
+
+htmlAviatorRatings ::
+  [Rating]
+  -> Html ()
+htmlAviatorRatings r =
+  when (not . null $ r) $
+    do  li_ [id_ "aviatorratings"] $ 
+          do  span_ [class_ "key"] "Ratings: "
+              span_ [class_ "value"] .
+                htmlRatings $ r
+
+htmlAviator ::
+  Aviator
+  -> Html ()
+htmlAviator (Aviator s f a d r) =
+  div_ [id_ "aviator", class_ "aviator"] .
+    ul_ [] $
+      do  htmlAviatorName s f
+          htmlAviatorARN a
+          htmlAviatorDob d
+          htmlAviatorRatings r
+            
+htmlAviatorShort ::
+  Aviator
+  -> Html ()    
+htmlAviatorShort (Aviator s f a _ r) =
+  do  fromString f
+      " "
+      fromString s
+      when (not . null $ a) $ " "
+      fromString (a >>= show)
+      when (not . null $ r) $ " "
+      htmlRatingsShort r
+
+htmlFlightPoint ::
+  AircraftFlight
+  -> FlightPoint
+  -> Html ()
+htmlFlightPoint _ (FlightPoint p _ _) =
+  span_ [class_ "flightpoint"] $
+    fromString p
+
+htmlFlightPath ::
+  AircraftFlight
+  -> FlightPath
+  -> Html ()
+htmlFlightPath fl p =
+  span_ [class_ "flightpath"] $
+    fold (intersperse (toHtmlRaw (" &mdash; " :: Text)) (htmlFlightPoint fl <$> flightPathList p))
+    
+htmlCommand ::
+  AircraftFlight
+  -> Command
+  -> Html ()
+htmlCommand _ InCommand =
+  span_ [class_ "command incommand"] "In-Command"
+htmlCommand _ (ICUS a) =
+  do  span_ [class_ "command incommandunderinstruction"] "In-Command Under-Instruction"
+      span_ [class_ "commandphrase"] $ " by "
+      span_ [class_ "commandaviator"] $ htmlAviatorShort a
+htmlCommand _ (Dual a) =
+  do  span_ [class_ "command dualunderinstruction"] "Dual Under-Instruction"
+      span_ [class_ "commandphrase"] $ " by "
+      span_ [class_ "commandaviator"] $ htmlAviatorShort a
+
+htmlTimeAmountZero ::
+  TimeAmount
+  -> Html ()
+htmlTimeAmountZero =
+  htmlTimeAmountZeroWith id
+
+htmlTimeAmountZeroWith :: 
+  Monoid a =>
+  (Html () -> a)
+  -> TimeAmount
+  -> a
+htmlTimeAmountZeroWith f z =
+  if z == zerotimeamount
+    then
+      mempty
+    else
+      f (htmlTimeAmount z)
+
+htmlAviators ::
+  [Aviator]
+  -> Html ()
+htmlAviators =
+  ul_ [] .
+    mapM_ (li_ [] . htmlAviatorShort)
+
+htmlAircraftFlightName ::
+  String
+  -> Html ()
+htmlAircraftFlightName n =
+  h3_ [class_ "aircraftflightname"] $
+          fromString n
+
+htmlAircraftFlight ::
+  AircraftFlight
+  -> Html ()
+htmlAircraftFlight fl@(AircraftFlight n a c (DayNight d m) p o i) =
+  div_ [class_ "aircraftflight"] $
+    do  htmlAircraftFlightName n
+        ul_ [] $
+          do  li_ [] $
+                do  span_ [class_ "key"] "Time: "
+                    span_ [class_ "value"] .
+                     htmlFlightPathTime $ p
+              li_ [] $
+                do  span_ [class_ "key"] "Aircraft: "
+                    span_ [class_ "value"] .
+                     htmlAircraft fl $ a
+              li_ [] $
+                do  span_ [class_ "key"] "Command: "
+                    span_ [class_ "value"] .
+                     htmlCommand fl $ c
+              htmlTimeAmountZeroWith (\t ->
+                li_ [] $
+                do  span_ [class_ "key"] "Amount (day): "
+                    span_ [class_ "value"] t) d
+              htmlTimeAmountZeroWith (\t ->
+                li_ [] $
+                do  span_ [class_ "key"] "Amount (night): "
+                    span_ [class_ "value"] t) m
+              htmlTimeAmountZeroWith (\t ->
+                li_ [] $
+                do  span_ [class_ "key"] "Amount (instrument): "
+                    span_ [class_ "value"] t) i
+              li_ [] $
+                do  span_ [class_ "key"] "Flight Path: "
+                    span_ [class_ "value"] .
+                      htmlFlightPath fl $ p
+              when (not . null $ o) . li_ [] $
+                do  span_ [class_ "key"] "Other Crew: "
+                    span_ [class_ "value"] .
+                      htmlAviators $ o
+
+htmlTimeOfDayTime ::
+  Maybe TimeOfDay
+  -> Html ()
+htmlTimeOfDayTime =
+  maybe mempty (\e -> do  " "
+                          fromString (show e))
+
+htmlTime ::
+  Time
+  -> Html ()
+htmlTime (Time t d) =
+  span_ [class_ "time"] $ 
+    do  fromString (show t)
+        htmlTimeOfDayTime d
+
+htmlFlightPathTime ::
+  FlightPath
+  -> Html()
+htmlFlightPathTime p =
+  let s = p ^. flightStart . landingTime
+      e = p ^. flightEnd . landingTime
+  in  if s == e
+        then
+          htmlTime s
+        else
+          do  htmlTime s
+              toHtmlRaw (" &mdash; " :: Text)
+              htmlTime e
+
+htmlSimulatorFlightName ::
+  String
+  -> Html ()
+htmlSimulatorFlightName n =
+  h3_ [class_ "simulatorflightname"] $
+          fromString n
+
+htmlSimulatorFlight ::
+  SimulatorFlight
+  -> Html ()
+htmlSimulatorFlight (SimulatorFlight n t y o a i) =
+  div_ [class_ "simulatorflight"] $
+    do  htmlSimulatorFlightName n
+        ul_ [] $
+          do  li_ [] $
+                do  span_ [class_ "key"] "Time: "
+                    span_ [class_ "value"] .
+                     htmlTime $ t
+              li_ [] $
+                do  span_ [class_ "key"] "Type: "
+                    span_ [class_ "value"] (fromString y)
+              when (not . null $ o) . li_ [] $
+                do  span_ [class_ "key"] "Other Crew: "
+                    div_ [class_ "value"] .
+                      htmlAviators $ o
+              li_ [] $
+                do  span_ [class_ "key"] "Amount: "
+                    span_ [class_ "value"] .
+                     htmlTimeAmount $ a
+              li_ [] $
+                do  span_ [class_ "key"] "Instrument: "
+                    span_ [class_ "value"] .
+                     htmlTimeAmount $ i
+  
+htmlLocation ::
+  Location
+  -> Html ()
+htmlLocation (Location n t o) =  
+  span_ [class_ "location"] $ 
+    do  fromString n
+        " "
+        let t' = fromString (show t)
+            o' = fromString (show o)
+        span_ [class_ "locationopenstreetmap"] $
+          a_ [href_ ("http://www.openstreetmap.org/?mlat=" <> t' <> "&mlon=" <> o' <> "#map=16/" <> t' <> "/" <> o')] 
+          "osm"
+        " "
+        span_ [class_ "locationgooglemaps"] $
+          a_ [href_ ("https://www.google.com/maps/?q=" <> t' <> "," <> o')]
+          "gmap"
+        
+htmlExamResult ::
+  Int
+  -> Int
+  -> Html ()
+htmlExamResult x y =
+  do  fromString (show x)
+      "/"
+      fromString (show y)
+
+htmlExamName ::
+  String
+  -> Html ()
+htmlExamName n =
+  h3_ [class_ "examname"] $ 
+    fromString n
+
+htmlExam ::
+  Exam
+  -> Html ()
+htmlExam (Exam n l t a r m) =
+  let r' = do  span_ [class_ "examresult"] . fromString . show $ r
+               span_ [class_ "examresultoutof"] "/"
+               span_ [class_ "examresultmaximum"] . fromString . show $ m
+               " ("
+               span_ [class_ "examresultpercentage"] . fromString . printf "%.2f" $ (100 * fromIntegral r / fromIntegral m :: Double)
+               span_ [class_ "examresultpercentsign"] "%"
+               ")"
+  in  div_ [class_ "exam"] $
+        do  htmlExamName n
+            ul_ [] $
+              do  li_ [] $
+                    do  span_ [class_ "key"] "Time: "
+                        span_ [class_ "value"] .
+                          htmlTime $ t
+                  li_ [] $
+                    do  span_ [class_ "key"] "Location: "
+                        span_ [class_ "value"] .
+                          htmlLocation $ l
+                  li_ [] $
+                    do  span_ [class_ "key"] "Delegate: "
+                        span_ [class_ "value"] .
+                          htmlAviatorShort $ a
+                  li_ [] $
+                    do  span_ [class_ "key"] "Result: "
+                        span_ [class_ "value"] r'
+
+htmlBriefingName ::
+  String
+  -> Html ()
+htmlBriefingName n =
+  h3_ [class_ "briefingname"] $ 
+    fromString n
+
+htmlBriefing ::
+  Briefing
+  -> Html ()
+htmlBriefing (Briefing n l t a m) =
+  div_ [class_ "briefing"] $
+    do  htmlBriefingName n
+        ul_ [] $
+          do  li_ [] $
+                do  span_ [class_ "key"] "Time: "
+                    span_ [class_ "value"] .
+                      htmlTime $ t
+              li_ [] $
+                do  span_ [class_ "key"] "Location: "
+                    span_ [class_ "value"] .
+                      htmlLocation $ l
+              li_ [] $
+                do  span_ [class_ "key"] "Amount: "
+                    span_ [class_ "value"] .
+                      htmlTimeAmountZero $ m
+              li_ [] $
+                do  span_ [class_ "key"] "Briefer: "
+                    span_ [class_ "value"] .
+                      htmlAviatorShort $ a
+
+space2dot ::
+  String
+  -> String
+space2dot =
+  (<$>) $ \c -> case c of 
+                ' ' -> '.'
+                _   -> c
+
+htmlEntryTag ::
+  Entry a b c d
+  -> Html ()
+htmlEntryTag (AircraftFlightEntry e _) =
+  let lk = space2dot . concat $
+                          [
+                            "FLT_"
+                          , e ^. aircraftflightname
+                          , "_"
+                          , e ^. flightaircraft . aircraftRegistration
+                          , "_"
+                          , e ^. flightpath . flightStart . point
+                          , "-"
+                          , e ^. flightpath . flightEnd . point
+                          ]
+  in  do  a_ [id_ (Text.pack ('#' : lk))] ""
+          a_ [href_ (Text.pack ('#' : lk))] . span_ [class_ "entrytag"] $ "FLT"
+
+htmlEntryTag (SimulatorFlightEntry e _) =
+  let lk = space2dot . concat $
+                          [
+                            "SIM_"
+                          , e ^. simulatorflightname
+                          , "_"
+                          , e ^. simulatortype
+                          ]
+  in  do  a_ [id_ (Text.pack ('#' : lk))] ""
+          a_ [href_ (Text.pack ('#' : lk))] . span_ [class_ "entrytag"] $ "SIM"
+htmlEntryTag (ExamEntry e _) =
+  let lk = space2dot . concat $
+                          [
+                            "EXM_"
+                          , e ^. examName
+                          , "_"
+                          , show (e ^. examTime . daytime)
+                          ]
+  in  do  a_ [id_ (Text.pack ('#' : lk))] ""
+          a_ [href_ (Text.pack ('#' : lk))] . span_ [class_ "entrytag"] $ "EXM"
+htmlEntryTag (BriefingEntry e _) =
+  let lk = space2dot . concat $
+                          [
+                            "BRF_"
+                          , e ^. briefingName
+                          , "_"
+                          , show (e ^. briefingTime . daytime)
+                          ]
+  in  do  a_ [id_ (Text.pack ('#' : lk))] ""
+          a_ [href_ (Text.pack ('#' : lk))] . span_ [class_ "entrytag"] $ "BRF"        
+
+htmlEntry :: 
+  (AircraftFlight -> a -> Html x)
+  -> (SimulatorFlight -> b -> Html x)
+  -> (Exam -> c -> Html x)
+  -> (Briefing -> d -> Html x)
+  -> Entry a b c d
+  -> Html x
+htmlEntry aircraftFlightMeta' simulatorFlightMeta' examMeta' briefingMeta' x =
+  do  htmlEntryTag x
+      case x of
+        AircraftFlightEntry e ae ->
+          do  div_ [] $
+                do  htmlAircraftFlight e
+                    aircraftFlightMeta' e ae
+        SimulatorFlightEntry e ae ->
+          do  div_ [] $
+                do  htmlSimulatorFlight e
+                    simulatorFlightMeta' e ae
+        ExamEntry e ae ->
+          do  div_ [] $
+                do  htmlExam e
+                    examMeta' e ae
+        BriefingEntry e ae ->
+          do  div_ [] $
+                do  htmlBriefing e
+                    briefingMeta' e ae
+  
+htmlEntries :: 
+  (AircraftFlight -> a -> Html x)
+  -> (SimulatorFlight -> b -> Html x)
+  -> (Exam -> c -> Html x)
+  -> (Briefing -> d -> Html x)
+  -> Entries a b c d
+  -> Html ()
+htmlEntries aircraftFlightMeta' simulatorFlightMeta' examMeta' briefingMeta' (Entries es) =
+  mapM_ (\e -> hr_ [] *> htmlEntry aircraftFlightMeta' simulatorFlightMeta' examMeta' briefingMeta' e) es
+
+htmlLogbook ::
+  (AircraftFlight -> a -> Html x)
+  -> (SimulatorFlight -> b -> Html x)
+  -> (Exam -> c -> Html x)
+  -> (Briefing -> d -> Html x)
+  -> Html ()
+  -> Logbook a b c d
+  -> Html ()
+htmlLogbook aircraftFlightMeta' simulatorFlightMeta' examMeta' briefingMeta' reports' (Logbook a es) =
+  do  htmlAviator a
+      hr_ []
+      reports'
+      htmlEntries aircraftFlightMeta' simulatorFlightMeta' examMeta' briefingMeta' es
+
+htmlTitleAviator ::
+  Aviator
+  -> Html ()
+htmlTitleAviator a =
+  fromString (concat
+                [
+                  a ^. firstname
+                , " "
+                , a ^. surname
+                , " ("
+                , show =<< (a ^. arn)
+                , ")"
+                ])
+
+htmlLogbookDocument ::
+  (AircraftFlight -> a -> Html x)
+  -> (SimulatorFlight -> b -> Html x)
+  -> (Exam -> c -> Html x)
+  -> (Briefing -> d -> Html x)
+  -> Html ()
+  -> Logbook a b c d
+  -> Html ()
+htmlLogbookDocument aircraftFlightMeta' simulatorFlightMeta' examMeta' briefingMeta' reports' b =
+  do  doctype_
+      html_ [lang_ "en"] $
+        do  head_ $ 
+              do  title_ ("Pilot Personal Logbook " <> toHtmlRaw (" &mdash; " :: Text) <> htmlTitleAviator (b ^. logbookaviator))
+                  link_ [href_ "https://fonts.googleapis.com/css?family=Inconsolata:400,700", rel_ "stylesheet", type_ "text/css"]
+                  link_ [href_ "casr-logbook.css", rel_ "stylesheet", type_ "text/css"]
+                  link_ [href_ "/atom.xml", rel_ "alternate", type_ "application/atom+xml", title_ "Atom feed"]
+                  script_ [type_ "text/javascript", src_ "http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"] ("" :: Text)
+                  script_ [type_ "text/javascript", src_ "https://raw.github.com/Mathapedia/LaTeX2HTML5/master/latex2html5.min.js"] ("" :: Text)                  
+            body_ [class_ "casr-logbook"] $ 
+              do  htmlLogbookHeader b
+                  htmlLogbook aircraftFlightMeta' simulatorFlightMeta' examMeta' briefingMeta' reports' b
+
+htmlLogbookHeader ::
+  Logbook a b c d
+  -> Html ()
+htmlLogbookHeader _ =
+  do  div_ [id_ "header", class_ "header"] $
+        h1_ "Pilot Personal Log Book"
+      div_ [id_ "subheader", class_ "subheader"] $
+        h2_ $
+          do  "Civil Aviation Safety Regulation 1998 (61.345)"
+              " "
+              span_ [class_ "austlii"] $
+                a_ [href_ "http://www.austlii.edu.au/au/legis/cth/consol_reg/casr1998333/s61.345.html"] "austlii.edu.au"
diff --git a/test/doctests.hs b/test/doctests.hs
new file mode 100644
--- /dev/null
+++ b/test/doctests.hs
@@ -0,0 +1,32 @@
+module Main where
+
+import Build_doctests (deps)
+import Control.Applicative
+import Control.Monad
+import Data.List
+import System.Directory
+import System.FilePath
+import Test.DocTest
+
+main ::
+  IO ()
+main =
+  getSources >>= \sources -> doctest $
+      "-isrc"
+    : "-idist/build/autogen"
+    : "-optP-include"
+    : "-optPdist/build/autogen/cabal_macros.h"
+    : "-hide-all-packages"
+    : map ("-package="++) deps ++ sources
+
+getSources :: IO [FilePath]
+getSources = filter (isSuffixOf ".hs") <$> go "src"
+  where
+    go dir = do
+      (dirs, files) <- getFilesAndDirectories dir
+      (files ++) . concat <$> mapM go dirs
+
+getFilesAndDirectories :: FilePath -> IO ([FilePath], [FilePath])
+getFilesAndDirectories dir = do
+  c <- map (dir </>) . filter (`notElem` ["..", "."]) <$> getDirectoryContents dir
+  (,) <$> filterM doesDirectoryExist c <*> filterM doesFileExist c
