Compare commits

...

31 commits

Author SHA1 Message Date
9f647cdf16
Add sslOnlyMiddleware to the Foundation 2017-10-19 12:21:39 +11:00
1e07c6f9c3
1.6.4 2017-10-16 23:07:29 +11:00
3c072d174a
Actually, uh, write the styles correctly for the previous commit's change -_- 2017-10-16 23:07:10 +11:00
e7653e3184
1.6.3 2017-10-16 23:02:33 +11:00
eb23583876
Remove the bottom margin from h-feeds so they line up with the h-card properly 2017-10-16 23:02:27 +11:00
c96f81855b
1.6.2 2017-10-16 22:40:56 +11:00
db32b02811
Properly set p-name on h-feeds, rather than trying to infer it and getting awful names 2017-10-16 22:40:35 +11:00
46c393552a
Take out the Google Analytics stuff, since I'm not using it and I'm probably not gonna use it 2017-10-16 22:23:50 +11:00
bd1fe8fbd8
Make the navbar expand on medium screen widths too 2017-10-16 22:22:18 +11:00
a3b3986b76
1.6.1 2017-10-16 21:25:27 +11:00
99049a88ef
Give code a nice dark colour scheme like the stuff around it 2017-10-16 21:25:17 +11:00
50f2cde09c
1.6.0 2017-10-16 21:08:12 +11:00
1611ec2ef1
Take out the Hamlet newline configuration since it actually doesn't affect the output. Like, at all. 2017-10-16 21:06:18 +11:00
2d1cbbf105
Remove a redundant import 2017-10-16 20:58:44 +11:00
5a83121431
Move the schema.org/BreadcrumbList stuff out of Foundation 2017-10-16 20:57:36 +11:00
a880e66c31
Put category listings back into the sitemap.xml 2017-10-16 14:56:01 +11:00
1364a2e36b
Store categories directly in entry_category 2017-10-16 13:47:49 +11:00
6f18f1b5d8
Fix sticky author card so it works in Chrome (it may also have been broken in FF oops) 2017-10-16 13:27:32 +11:00
7f7f64c8b5
Mark up h-entries with schema.org/BlogPosting as well. It's so gross compared to h-entry oh my god. 2017-10-16 09:07:08 +11:00
bf970db801
Add an optional photo field to entries, which becomes a card-topping u-photo when displayed 2017-10-16 08:29:21 +11:00
cdda4e2eee
Put the schema.org/Person stuff back. I decided to include friggin' Facebook's metadata approach, so this isn't that unreasonable 2017-10-15 22:42:33 +11:00
a0004946d6
Avoid rendering Markdown when pushing entry content into the og:description, and also, add the standard meta description and meta author 2017-10-15 22:34:05 +11:00
3b00565bda
Set the meta description from the big h-card to match the og:description 2017-10-15 22:33:13 +11:00
62920930b5
Use an <article> for the big h-card rather than a <div> - it provides a teeny bit more info and is still semantically correct 2017-10-15 22:08:32 +11:00
656da87e19
Keep the .author class on the <aside> containing the h-card, rather than trying to use .p-author directly 2017-10-15 03:29:49 +11:00
db16b0d2a7
Use <aside> for the big h-card on the homepage rather than <div>, and mark it up as a p-author 2017-10-15 03:13:12 +11:00
068ce7a41c
Slightly more efficient implementations of Model.Entry.Shorten 2017-10-15 02:37:37 +11:00
940b62b6fc
Remove unused category ID binding to avoid compiler warnings 2017-10-15 01:41:46 +11:00
e24e7db641
Render entryContent as Markdown too :3 2017-10-15 01:12:18 +11:00
805e422eba
Render the p-note as Markdown - this is mostly useful because it means other h-card fields can be defined inside it 2017-10-15 01:00:12 +11:00
012c62dd09
Fix inconsistent formatting of package version constraints 2017-10-15 00:10:35 +11:00
30 changed files with 211 additions and 124 deletions

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
dist* dist*
static/tmp/ static/tmp/
static/combined/ static/combined/
static/uploads/
config/client_session_key.aes config/client_session_key.aes
*.hi *.hi
*.o *.o

View file

@ -4,7 +4,7 @@ User
fullName Text maxlen=500 fullName Text maxlen=500
email Text maxlen=190 email Text maxlen=190
avatar Text maxlen=190 avatar Text maxlen=190
note Text sqltype=mediumtext note Markdown sqltype=mediumtext
UniqueUser username UniqueUser username
UniqueEmail email UniqueEmail email
deriving Typeable deriving Typeable
@ -28,7 +28,8 @@ Profile
Entry Entry
kind EntryKind maxlen=255 kind EntryKind maxlen=255
name Text Maybe maxlen=255 name Text Maybe maxlen=255
content Text sqltype=longtext content Markdown sqltype=longtext
photo Text Maybe maxlen=190
published UTCTime published UTCTime
updated UTCTime updated UTCTime
authorId UserId authorId UserId
@ -38,11 +39,7 @@ Syndication
profileId ProfileId profileId ProfileId
url Text sqltype=varchar(255) url Text sqltype=varchar(255)
Category
tag Slug sqltype=varchar(190)
UniqueTag tag
EntryCategory EntryCategory
entryId EntryId entryId EntryId
categoryId CategoryId category Category sqltype=varchar(190)
UniqueEntryCategory entryId categoryId UniqueEntryCategory entryId category

View file

@ -8,7 +8,7 @@
/ HomeR GET / HomeR GET
/avatars/#UserId AvatarR GET /avatars/#UserId AvatarR GET
/categories/#Slug CategoryR GET /categories/#Category CategoryR GET
/feed FeedR GET /feed FeedR GET
!/#EntryKind/feed FeedKindR GET !/#EntryKind/feed FeedKindR GET

View file

@ -35,5 +35,4 @@ title: 00dani.me
app-name: lebd app-name: lebd
username: dani username: dani
repository: https://gitlab.com/00dani/lebd repository: https://gitlab.com/00dani/lebd
#analytics: UA-YOURCODE
fb-app-id: "_env:FB_APP_ID:142105433189339" fb-app-id: "_env:FB_APP_ID:142105433189339"

2
package-lock.json generated
View file

@ -1,6 +1,6 @@
{ {
"name": "lebd", "name": "lebd",
"version": "1.5.0", "version": "1.6.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View file

@ -1,6 +1,6 @@
{ {
"name": "lebd", "name": "lebd",
"version": "1.5.0", "version": "1.6.4",
"description": "the codebase backing 00dani.me, an indieweb.org site", "description": "the codebase backing 00dani.me, an indieweb.org site",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -1,5 +1,5 @@
name: lebd name: lebd
version: "1.5.0" version: "1.6.4"
dependencies: dependencies:
@ -48,10 +48,11 @@ dependencies:
- wai - wai
- blaze-markup >=0.8 && <0.9 - blaze-markup >=0.8 && <0.9
- conduit-combinators >= 1.1 && <1.2 - conduit-combinators >=1.1 && <1.2
- esqueleto >=2.5 && <2.6 - esqueleto >=2.5 && <2.6
- friendly-time >=0.4 && <0.5 - friendly-time >=0.4 && <0.5
- foreign-store >=0.2 && <0.3 - foreign-store >=0.2 && <0.3
- markdown >=0.1 && <0.2
- mustache >=2.2 && <2.3 - mustache >=2.2 && <2.3
- parsec >=3.1 && <3.2 - parsec >=3.1 && <3.2
- slug >=0.1 && <0.2 - slug >=0.1 && <0.2

View file

@ -22,10 +22,12 @@ import qualified Yesod.Core.Unsafe as Unsafe
import Package import Package
import Model.Cache ( getCached ) import Model.Cache ( getCached )
import Model.Category ( Category )
import Model.Entry ( entryTitle ) import Model.Entry ( entryTitle )
import Model.Entry.Kind ( EntryKind, allEntryKinds, pluralise ) import Model.Entry.Kind ( EntryKind, allEntryKinds, pluralise )
import SchemaOrg.BreadcrumbList ( breadcrumbList )
import Data.Aeson ( encode, object ) import Data.Aeson ( encode )
import qualified Text.Blaze.Internal as B import qualified Text.Blaze.Internal as B
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Lazy.Encoding as E import qualified Data.Text.Lazy.Encoding as E
@ -65,6 +67,9 @@ mkYesodData "App" $(parseRoutesFile "config/routes")
-- | A convenient synonym for creating forms. -- | A convenient synonym for creating forms.
type Form x = Html -> MForm (HandlerT App IO) (FormResult x, Widget) type Form x = Html -> MForm (HandlerT App IO) (FormResult x, Widget)
sessionLifetime :: Int
sessionLifetime = 120 -- minutes
-- Please see the documentation for the Yesod typeclass. There are a number -- Please see the documentation for the Yesod typeclass. There are a number
-- of settings which can be configured by overriding methods here. -- of settings which can be configured by overriding methods here.
instance Yesod App where instance Yesod App where
@ -75,7 +80,7 @@ instance Yesod App where
-- Store session data on the client in encrypted cookies, -- Store session data on the client in encrypted cookies,
-- default session idle timeout is 120 minutes -- default session idle timeout is 120 minutes
makeSessionBackend _ = sslOnlySessions . strictSameSiteSessions $ Just <$> defaultClientSessionBackend makeSessionBackend _ = sslOnlySessions . strictSameSiteSessions $ Just <$> defaultClientSessionBackend
120 -- timeout in minutes sessionLifetime
"config/client_session_key.aes" "config/client_session_key.aes"
-- Redirect static requests to a subdomain - this is recommended for best -- Redirect static requests to a subdomain - this is recommended for best
@ -95,7 +100,7 @@ instance Yesod App where
-- b) Validates that incoming write requests include that token in either a header or POST parameter. -- b) Validates that incoming write requests include that token in either a header or POST parameter.
-- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware -- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware
-- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package. -- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package.
yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware . sslOnlyMiddleware sessionLifetime
defaultLayout widget = do defaultLayout widget = do
master <- getYesod master <- getYesod
@ -107,7 +112,7 @@ instance Yesod App where
-- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance. -- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance.
(title, crumbs) <- breadcrumbs (title, crumbs) <- breadcrumbs
let allCrumbs = maybe crumbs (\route -> crumbs ++ [(route, title)]) mcurrentRoute let allCrumbs = maybe crumbs (\route -> crumbs ++ [(route, title)]) mcurrentRoute
jsonCrumbs <- fmap (E.decodeUtf8 . encode) . withUrlRenderer $ jsonLdBreadcrumbList allCrumbs jsonCrumbs <- fmap (E.decodeUtf8 . encode) . withUrlRenderer $ breadcrumbList allCrumbs
let navbars = [leftMenuItems, rightMenuItems] <*> [muser] let navbars = [leftMenuItems, rightMenuItems] <*> [muser]
@ -186,22 +191,6 @@ instance YesodBreadcrumbs App where
breadcrumb (EntryWithSlugR kind entryId _) = breadcrumb $ EntryR kind entryId breadcrumb (EntryWithSlugR kind entryId _) = breadcrumb $ EntryR kind entryId
breadcrumb _ = return ("home", Nothing) breadcrumb _ = return ("home", Nothing)
jsonLdBreadcrumbList :: [(Route App, Text)] -> (Route App -> [(Text, Text)] -> Text) -> Value
jsonLdBreadcrumbList crumbs url = object
[ ("@context", "http://schema.org")
, ("@type", "BreadcrumbList")
, "itemListElement" .= zipWith (jsonLdListItem url) [1 :: Int ..] crumbs
]
jsonLdListItem :: (Route App -> [(Text, Text)] -> Text) -> Int -> (Route App, Text) -> Value
jsonLdListItem url i (r, t) = object
[ ("@type", "ListItem")
, "position" .= i
, "item" .= object
[ "@id" .= url r []
, "name" .= t
]
]
-- How to run database actions. -- How to run database actions.
instance YesodPersist App where instance YesodPersist App where
type YesodPersistBackend App = SqlBackend type YesodPersistBackend App = SqlBackend

View file

@ -5,18 +5,18 @@ module Handler.Categories where
import Import hiding ( on, (==.) ) import Import hiding ( on, (==.) )
import Database.Esqueleto import Database.Esqueleto
import Web.Slug ( Slug, unSlug )
import Widget.Feed ( hFeed ) import Widget.Feed ( hFeed )
import Model.Category ( Category, asTag )
import qualified Data.Text as T import qualified Data.Text as T
getCategoryR :: Slug -> Handler Html getCategoryR :: Category -> Handler Html
getCategoryR tag = do getCategoryR tag = do
entries <- runDB . select . from $ \(category `InnerJoin` ec `InnerJoin` entry) -> do title <- asks $ siteTitle . appSettings
on $ entry ^. EntryId ==. ec ^. EntryCategoryEntryId entries <- runDB . select . from $ \(entry `InnerJoin` category) -> do
on $ category ^. CategoryId ==. ec ^. EntryCategoryCategoryId on $ entry ^. EntryId ==. category ^. EntryCategoryEntryId
where_ $ category ^. CategoryTag ==. val tag where_ $ category ^. EntryCategoryCategory ==. val tag
return entry return entry
defaultLayout $ do defaultLayout $ do
setTitle . toHtml . T.cons '#' . unSlug $ tag setTitle . toHtml . asTag $ tag
hFeed entries T.concat [asTag tag, " ~ ", title] `hFeed` entries

View file

@ -7,11 +7,13 @@
module Handler.Common where module Handler.Common where
import Data.FileEmbed (embedFile) import Data.FileEmbed (embedFile)
import Database.Esqueleto ( (^.) )
import qualified Database.Esqueleto as E
import Yesod.Sitemap import Yesod.Sitemap
import Import import Import
import Model.Entry.Kind ( EntryKind, allEntryKinds ) import Model.Entry.Kind ( allEntryKinds )
import Widget.Entry ( entryR ) import Widget.Entry ( entryR )
-- These handlers embed files in the executable at compile time to avoid a -- These handlers embed files in the executable at compile time to avoid a
@ -31,7 +33,9 @@ getRobotsR = robots SitemapR
getSitemapR :: Handler TypedContent getSitemapR :: Handler TypedContent
getSitemapR = do getSitemapR = do
categories <- runDB $ selectList [] [Asc CategoryTag] categories <- runDB . E.select . E.distinct . E.from $ \ec -> do
E.orderBy [E.asc $ ec ^. EntryCategoryCategory]
return $ ec ^. EntryCategoryCategory
entries <- runDB $ selectList [] [Desc EntryPublished] entries <- runDB $ selectList [] [Desc EntryPublished]
sitemap $ do sitemap $ do
yield SitemapUrl yield SitemapUrl
@ -40,29 +44,19 @@ getSitemapR = do
, sitemapChangeFreq = Just Daily , sitemapChangeFreq = Just Daily
, sitemapPriority = Nothing , sitemapPriority = Nothing
} }
yieldMany $ categoryToSitemapUrl <$> categories yieldMany $ sitemapUrl . CategoryR . E.unValue <$> categories
yieldMany $ kindToSitemapUrl <$> allEntryKinds yieldMany $ sitemapUrl . EntriesR <$> allEntryKinds
yieldMany $ entryToSitemapUrl <$> entries yieldMany $ entryToSitemapUrl <$> entries
categoryToSitemapUrl :: Entity Category -> SitemapUrl (Route App) sitemapUrl :: a -> SitemapUrl a
categoryToSitemapUrl (Entity catId cat) = SitemapUrl sitemapUrl loc = SitemapUrl
{ sitemapLoc = CategoryR $ categoryTag cat { sitemapLoc = loc
, sitemapLastMod = Nothing , sitemapLastMod = Nothing
, sitemapChangeFreq = Nothing , sitemapChangeFreq = Nothing
, sitemapPriority = Nothing , sitemapPriority = Nothing
} }
kindToSitemapUrl :: EntryKind -> SitemapUrl (Route App)
kindToSitemapUrl kind = SitemapUrl
{ sitemapLoc = EntriesR kind
, sitemapLastMod = Nothing
, sitemapChangeFreq = Nothing
, sitemapPriority = Nothing
}
entryToSitemapUrl :: Entity Entry -> SitemapUrl (Route App) entryToSitemapUrl :: Entity Entry -> SitemapUrl (Route App)
entryToSitemapUrl entry = SitemapUrl entryToSitemapUrl entry = (sitemapUrl $ entryR entry)
{ sitemapLoc = entryR entry { sitemapLastMod = Just . entryUpdated . entityVal $ entry
, sitemapLastMod = Just . entryUpdated . entityVal $ entry
, sitemapChangeFreq = Nothing
, sitemapPriority = Nothing
} }

View file

@ -9,6 +9,7 @@ import Yesod.AtomFeed ( atomLink )
import Model.Cache ( getCached ) import Model.Cache ( getCached )
import Model.Entry ( entryTitle ) import Model.Entry ( entryTitle )
import Model.Markdown ( unMarkdown )
import Widget.Entry ( entryR, hEntry ) import Widget.Entry ( entryR, hEntry )
import Widget.Feed ( hFeed ) import Widget.Feed ( hFeed )
@ -19,10 +20,11 @@ getEntriesR :: K.EntryKind -> Handler Html
getEntriesR kind = do getEntriesR kind = do
entries <- runDB $ selectList [EntryKind ==. kind] [Desc EntryPublished] entries <- runDB $ selectList [EntryKind ==. kind] [Desc EntryPublished]
title <- asks $ siteTitle . appSettings title <- asks $ siteTitle . appSettings
let myTitle = T.concat [K.pluralise kind, " ~ ", title]
defaultLayout $ do defaultLayout $ do
setTitle . toHtml . K.pluralise $ kind setTitle . toHtml . K.pluralise $ kind
atomLink (FeedKindR kind) $ T.concat [K.pluralise kind, " ~ ", title] FeedKindR kind `atomLink` myTitle
hFeed entries hFeed myTitle entries
getEntryR :: a -> EntryId -> Handler Html getEntryR :: a -> EntryId -> Handler Html
getEntryR _ = renderEntry <=< getCached getEntryR _ = renderEntry <=< getCached
@ -40,9 +42,12 @@ renderEntry entry = do
defaultLayout $ do defaultLayout $ do
setTitle . toHtml . entryTitle . entityVal $ entry setTitle . toHtml . entryTitle . entityVal $ entry
toWidgetHead [hamlet| toWidgetHead [hamlet|
<meta name="author" content=#{userFullName $ entityVal author}>
<link rel="author" href=@{userProfile $ entityVal author}>
<meta name="description" content=#{unMarkdown $ entryContent $ entityVal entry}>
<meta property="og:title" content=#{entryTitle $ entityVal entry}> <meta property="og:title" content=#{entryTitle $ entityVal entry}>
<meta property="og:type" content="article"> <meta property="og:type" content="article">
<meta property="og:description" content=#{entryContent $ entityVal entry}> <meta property="og:description" content=#{unMarkdown $ entryContent $ entityVal entry}>
<meta property="article:author" content=@{userProfile $ entityVal author}> <meta property="article:author" content=@{userProfile $ entityVal author}>
<meta property="article:section" content=#{K.pluralise $ entryKind $ entityVal entry}> <meta property="article:section" content=#{K.pluralise $ entryKind $ entityVal entry}>
|] |]

View file

@ -11,12 +11,13 @@ module Model where
import ClassyPrelude.Yesod import ClassyPrelude.Yesod
import Database.Persist.Quasi import Database.Persist.Quasi
import Web.Slug ( Slug )
import Yesod.Auth.HashDB ( HashDBUser(..) ) import Yesod.Auth.HashDB ( HashDBUser(..) )
import Text.Mustache ( (~>) ) import Text.Mustache ( (~>) )
import qualified Text.Mustache as M import qualified Text.Mustache as M
import Model.Category ( Category )
import Model.Entry.Kind ( EntryKind ) import Model.Entry.Kind ( EntryKind )
import Model.Markdown ( Markdown )
-- You can define all of your database entities in the entities file. -- You can define all of your database entities in the entities file.
-- You can find more information on persistent and how to declare entities -- You can find more information on persistent and how to declare entities

14
src/Model/Category.hs Normal file
View file

@ -0,0 +1,14 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Model.Category where
import Database.Persist ( PersistField )
import Web.Slug ( Slug, unSlug )
import Yesod ( PathPiece )
import qualified Data.Text as T
newtype Category = Category { unCategory :: Slug }
deriving (Eq, Read, Show, PathPiece, PersistField)
asTag :: Category -> T.Text
asTag = T.cons '#' . unSlug . unCategory

View file

@ -2,14 +2,29 @@
module Model.Entry where module Model.Entry where
import Model ( Entry, entryName, entryContent ) import Model ( Entry, entryName, entryContent )
import Model.Markdown ( Markdown(Markdown), unMarkdown )
import Data.Maybe ( fromMaybe ) import Data.Maybe ( fromMaybe )
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
entryTitle :: Entry -> T.Text entryTitle :: Entry -> T.Text
entryTitle = fromMaybe <$> shorten 30 . entryContent <*> entryName entryTitle = fromMaybe <$> TL.toStrict . unMarkdown . shorten 30 . entryContent <*> entryName
shorten :: Int -> T.Text -> T.Text class Shorten a where
shorten n t shorten :: Int -> a -> a
| T.length t > n = flip T.append "..." . T.take (n - 1) $ t
instance Shorten T.Text where
shorten i t
| T.compareLength t n == GT = flip T.append "..." . T.take (n - 1) $ t
| otherwise = t | otherwise = t
where n = fromIntegral i
instance Shorten TL.Text where
shorten i t
| TL.compareLength t n == GT = flip TL.append "..." . TL.take (n - 1) $ t
| otherwise = t
where n = fromIntegral i
instance Shorten Markdown where
shorten n (Markdown t) = Markdown $ shorten n t

38
src/Model/Markdown.hs Normal file
View file

@ -0,0 +1,38 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
module Model.Markdown ( Markdown(..) ) where
import Data.Aeson ( FromJSON(..), ToJSON(..), Value(Object), object, (.=), (.:) )
import Data.Default ( def )
import Database.Persist ( PersistField(..), PersistValue(PersistText) )
import Database.Persist.Sql ( PersistFieldSql(..), SqlType(SqlString) )
import Data.String ( IsString )
import Text.Blaze ( ToMarkup(..) )
import Text.Markdown ( markdown )
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
newtype Markdown = Markdown { unMarkdown :: TL.Text }
deriving (Eq, Ord, Monoid, IsString, Show)
instance ToMarkup Markdown where
toMarkup (Markdown t) = markdown def t
instance PersistField Markdown where
toPersistValue (Markdown t) = PersistText $ TL.toStrict t
fromPersistValue (PersistText t) = Right . Markdown $ TL.fromStrict t
fromPersistValue wrongValue = Left $ T.concat
[ "Model.Markdown: When attempting to create Markdown from a PersistValue, received "
, T.pack $ show wrongValue
, " when a value of type PersistText was expected."
]
instance PersistFieldSql Markdown where
sqlType _ = SqlString
instance ToJSON Markdown where
toJSON (Markdown text) = object ["markdown" .= text]
instance FromJSON Markdown where
parseJSON (Object v) = Markdown <$> v .: "markdown"
parseJSON _ = mempty

View file

@ -0,0 +1,21 @@
{-# LANGUAGE OverloadedStrings #-}
module SchemaOrg.BreadcrumbList ( breadcrumbList ) where
import Data.Aeson
import qualified Data.Text as T
breadcrumbList :: [(a, T.Text)] -> (a -> [(T.Text, T.Text)] -> T.Text) -> Value
breadcrumbList crumbs url = object
[ ("@context", "http://schema.org")
, ("@type", "BreadcrumbList")
, "itemListElement" .= zipWith (listItem url) [1 :: Int ..] crumbs
]
listItem :: (a -> [(T.Text, T.Text)] -> T.Text) -> Int -> (a, T.Text) -> Value
listItem url i (r, t) = object
[ ("@type", "ListItem")
, "position" .= i
, "item" .= object
[ "@id" .= url r []
, "name" .= t
]
]

View file

@ -19,9 +19,8 @@ import Data.Yaml (decodeEither')
import Database.Persist.MySQL (MySQLConf (..)) import Database.Persist.MySQL (MySQLConf (..))
import Language.Haskell.TH.Syntax (Exp, Name, Q) import Language.Haskell.TH.Syntax (Exp, Name, Q)
import Network.Wai.Handler.Warp (HostPreference) import Network.Wai.Handler.Warp (HostPreference)
import Text.Hamlet (HamletSettings(hamletNewlines), NewlineStyle(AlwaysNewlines), defaultHamletSettings)
import Yesod.Default.Config2 (applyEnvValue, configSettingsYml) import Yesod.Default.Config2 (applyEnvValue, configSettingsYml)
import Yesod.Default.Util (WidgetFileSettings(wfsHamletSettings), widgetFileNoReload, import Yesod.Default.Util (WidgetFileSettings, widgetFileNoReload,
widgetFileReload) widgetFileReload)
import qualified Database.MySQL.Base as MySQL import qualified Database.MySQL.Base as MySQL
@ -59,11 +58,8 @@ data AppSettings = AppSettings
, appSkipCombining :: Bool , appSkipCombining :: Bool
-- ^ Perform no stylesheet/script combining -- ^ Perform no stylesheet/script combining
-- Example app-specific configuration values.
, appAnalytics :: Maybe Text
-- ^ Google Analytics code
, appFacebookId :: Maybe Int , appFacebookId :: Maybe Int
-- ^ Facebook app ID, also used for analytics. -- ^ Facebook app ID.
, siteTitle :: Text , siteTitle :: Text
-- ^ Site-wide title. -- ^ Site-wide title.
@ -98,7 +94,6 @@ instance FromJSON AppSettings where
appMutableStatic <- o .:? "mutable-static" .!= defaultDev appMutableStatic <- o .:? "mutable-static" .!= defaultDev
appSkipCombining <- o .:? "skip-combining" .!= defaultDev appSkipCombining <- o .:? "skip-combining" .!= defaultDev
appAnalytics <- o .:? "analytics"
appFacebookId <- o .:? "fb-app-id" appFacebookId <- o .:? "fb-app-id"
siteTitle <- o .: "title" siteTitle <- o .: "title"
siteUsername <- o .: "username" siteUsername <- o .: "username"
@ -124,7 +119,7 @@ instance FromJSON AppSettings where
-- --
-- https://github.com/yesodweb/yesod/wiki/Overriding-widgetFile -- https://github.com/yesodweb/yesod/wiki/Overriding-widgetFile
widgetFileSettings :: WidgetFileSettings widgetFileSettings :: WidgetFileSettings
widgetFileSettings = def { wfsHamletSettings = defaultHamletSettings { hamletNewlines = AlwaysNewlines } } widgetFileSettings = def
-- | How static files should be combined. -- | How static files should be combined.
combineSettings :: CombineSettings combineSettings :: CombineSettings

View file

@ -9,6 +9,7 @@ import Import
import Database.Esqueleto ( (^.) ) import Database.Esqueleto ( (^.) )
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import Model.Markdown ( unMarkdown )
import Text.Mustache ( substitute ) import Text.Mustache ( substitute )
import Util ( compileMustache ) import Util ( compileMustache )
@ -37,10 +38,11 @@ hCard (Entity userId user) = do
toWidgetHead [hamlet| toWidgetHead [hamlet|
<meta name="author" content=#{userFullName user}> <meta name="author" content=#{userFullName user}>
<meta name="description" content=#{unMarkdown $ userNote user}>
<link rel="author" href=@{HomeR}> <link rel="author" href=@{HomeR}>
<meta property="og:type" content="profile"> <meta property="og:type" content="profile">
<meta property="og:title" content="#{userFullName user}"> <meta property="og:title" content="#{userFullName user}">
<meta property="og:description" content=#{userNote user}> <meta property="og:description" content=#{unMarkdown $ userNote user}>
<meta property="og:image" content=@{staticR ["img", userAvatar user]}> <meta property="og:image" content=@{staticR ["img", userAvatar user]}>
<meta property="profile:first_name" content=#{firstName}> <meta property="profile:first_name" content=#{firstName}>
<meta property="profile:last_name" content=#{T.unwords lastName}> <meta property="profile:last_name" content=#{T.unwords lastName}>

View file

@ -10,6 +10,7 @@ import Database.Esqueleto ( (^.) )
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import Data.Time.Format ( defaultTimeLocale, formatTime, iso8601DateFormat ) import Data.Time.Format ( defaultTimeLocale, formatTime, iso8601DateFormat )
import Data.Time.Format.Human ( humanReadableTime ) import Data.Time.Format.Human ( humanReadableTime )
import Model.Entry ( entryTitle )
import Web.Slug ( mkSlug ) import Web.Slug ( mkSlug )
data FormattedTime = FormattedTime data FormattedTime = FormattedTime

View file

@ -4,5 +4,9 @@ module Widget.Feed ( hFeed ) where
import Import import Import
import Widget.Entry ( hEntry ) import Widget.Entry ( hEntry )
hFeed :: [Entity Entry] -> Widget import qualified Data.Text as T
hFeed entries = $(widgetFile "mf2/h-feed")
hFeed :: T.Text -> [Entity Entry] -> Widget
hFeed name entries = do
mroute <- getCurrentRoute
$(widgetFile "mf2/h-feed")

View file

@ -25,14 +25,3 @@ $doctype 5
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"> <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous"> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous">
$maybe analytics <- appAnalytics $ appSettings master
<script>
if(!window.location.href.match(/localhost/)){
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '#{analytics}', 'auto');
ga('send', 'pageview');
}

View file

@ -7,6 +7,13 @@ a
text-decoration: none text-decoration: none
line-height: 1 line-height: 1
code, kbd, pre, samp
font-family: Monoid, Hack, Inconsolata, Menlo, Monaco, Consolas, "Liberation Mono", monospace
code, pre
color: #cccccc
code
background-color: #141414
body body
background-color: #1d1f21 background-color: #1d1f21
color: #c9cacc color: #c9cacc

View file

@ -1,5 +1,5 @@
<header> <header>
<nav .navbar .navbar-expand-lg .navbar-dark.bg-dark> <nav .navbar .navbar-expand-md .navbar-dark.bg-dark>
<a .navbar-brand rel="home" href=@{HomeR}>#{siteTitle $ appSettings master} <a .navbar-brand rel="home" href=@{HomeR}>#{siteTitle $ appSettings master}
<button type="button" .navbar-toggler data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar" aria-label="Toggle navigation"> <button type="button" .navbar-toggler data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar" aria-label="Toggle navigation">
<span .navbar-toggler-icon> <span .navbar-toggler-icon>

View file

@ -2,11 +2,10 @@ body > main
display: flex display: flex
flex-direction: column flex-direction: column
align-items: center align-items: center
> ol.h-feed > div.h-feed
flex: 1 flex: 1
margin-bottom: 0 margin-bottom: 0
> .author > aside.author
height: 100%
max-width: 25rem max-width: 25rem
margin-bottom: 2em margin-bottom: 2em
> .h-card > .h-card
@ -16,8 +15,8 @@ body > main
@media (min-width: 768px) @media (min-width: 768px)
body > main body > main
flex-direction: row-reverse flex-direction: row-reverse
align-items: flex-start align-items: unset
> ol.h-feed > div.h-feed
margin-right: 2em margin-right: 2em
> .author > aside.author
margin-bottom: 0 margin-bottom: 0

View file

@ -1,2 +1,2 @@
<div .author>^{hCard user} <aside .author>^{hCard user}
^{hFeed entries} ^{hFeed title entries}

View file

@ -1,25 +1,25 @@
<div .card.h-card .bg-dark> <article .card.h-card .bg-dark itemscope itemtype="http://schema.org/Person">
$maybe route <- mcurrentRoute $maybe route <- mcurrentRoute
<a .u-uid.u-url href=@{route} hidden> <a .u-uid.u-url itemprop="url" href=@{route} hidden>
<img .card-img-top.u-photo src=@{AvatarR userId} alt=#{userFullName user}> <img .card-img-top.u-photo itemprop="image" src=@{AvatarR userId} alt=#{userFullName user}>
<div .card-body> <div .card-body>
<h4 .card-title.p-name>#{userFullName user} <h4 .card-title.p-name itemprop="name">#{userFullName user}
$forall key <- pgpKeys $forall key <- pgpKeys
<a .card-subtitle.u-key type="application/pgp-keys" href=@{routeFromPgp key}> <a .card-subtitle.u-key type="application/pgp-keys" href=@{routeFromPgp key}>
<i .fa.fa-key> <i .fa.fa-key>
#{prettyPgp key} #{prettyPgp key}
<p .card-text.p-note .text-muted>#{userNote user} <div .p-note itemprop="description" .text-muted>#{userNote user}
<ul .profiles> <ul .profiles>
<li> <li>
<a .u-email rel="me" href="mailto:#{userEmail user}"> <a .u-email rel="me" itemprop="email" href="mailto:#{userEmail user}">
<i .fa.fa-envelope> <i .fa.fa-envelope>
#{userEmail user} #{userEmail user}
$forall (Entity _ site, Entity _ profile) <- userProfiles $forall (Entity _ site, Entity _ profile) <- userProfiles
<li> <li>
<a .u-url rel="me" href="#{profileUrl site profile}"> <a .u-url rel="me" itemprop="sameAs" href="#{profileUrl site profile}">
<i .#{siteIcon site}> <i .#{siteIcon site}>
$maybe name <- profileDisplayName profile $maybe name <- profileDisplayName profile
#{name} #{name}

View file

@ -1,4 +1,6 @@
article.h-entry article.h-entry
.e-content p:last-child
margin-bottom: 0
> .card-footer > .card-footer
display: flex display: flex
flex-wrap: wrap flex-wrap: wrap

View file

@ -1,24 +1,33 @@
<article .h-entry .card.bg-dark> <article .h-entry .card.bg-dark itemscope itemtype="http://schema.org/BlogPosting">
$maybe photo <- entryPhoto entry
<img .card-img-top.u-photo itemprop="image" src=@{staticR ["uploads", photo]} alt=#{entryTitle entry}>
<div .card-body> <div .card-body>
$maybe name <- entryName entry $maybe name <- entryName entry
<h4 .p-name .card-title>#{name} <h4 .p-name .card-title itemprop="headline">#{name}
<div .e-content> <div .e-content itemprop="articleBody">
#{entryContent entry} #{entryContent entry}
$nothing $nothing
<div .e-content.p-name> <div itemprop="headline" hidden>#{entryTitle entry}
<div .e-content.p-name itemprop="articleBody">
#{entryContent entry} #{entryContent entry}
<div .card-footer> <div .card-footer>
$maybe author <- maybeAuthor $maybe author <- maybeAuthor
<a .p-author.h-card href=@{HomeR}> <a .p-author.h-card href=@{userProfile author}>
<img .u-photo src=@{AvatarR $ entryAuthorId entry} alt=#{userFullName author}> <img .u-photo src=@{AvatarR $ entryAuthorId entry} alt=#{userFullName author}>
#{userFullName author} #{userFullName author}
<a .u-url href="@{entryR (Entity entryId entry)}"> $# Use a separate hidden block for the schema.org metadata because you
$# can't put itemprop="author" and itemprop="url" on the same element,
$# because schema.org is garbage.
<div hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<a itemprop="url" href=@{userProfile author}>
<span itemprop="name">#{userFullName author}
<a .u-url itemprop="mainEntityOfPage" href=@{entryR (Entity entryId entry)}>
<i .fa.fa-link> <i .fa.fa-link>
permalink permalink
<time .dt-published datetime=#{timeUnfriendly published} title=#{timeUnfriendly published}> <time .dt-published itemprop="datePublished" datetime=#{timeUnfriendly published} title=#{timeUnfriendly published}>
<i .fa.fa-calendar> <i .fa.fa-calendar>
#{timeFriendly published} #{timeFriendly published}
<time .dt-updated datetime=#{timeUnfriendly updated} title=#{timeUnfriendly updated} :published == updated:hidden> <time .dt-updated itemprop="dateModified" datetime=#{timeUnfriendly updated} title=#{timeUnfriendly updated} :published == updated:hidden>
<i .fa.fa-pencil> <i .fa.fa-pencil>
#{timeFriendly updated} #{timeFriendly updated}
$forall (E.Value url, E.Value icon, E.Value name) <- posses $forall (E.Value url, E.Value icon, E.Value name) <- posses

View file

@ -1,5 +1,5 @@
ol.h-feed div.h-feed
list-style: none > ol.list-unstyled
padding-left: 0 margin-bottom: 0
> li:not(:last-child) > li:not(:last-child)
margin-bottom: 1em margin-bottom: 1em

View file

@ -1,3 +1,7 @@
<ol .h-feed> <div .h-feed>
<span .p-name hidden>#{name}
$maybe route <- mroute
<a .u-url href=@{route} hidden>
<ol .list-unstyled>
$forall entry <- entries $forall entry <- entries
<li>^{hEntry entry} <li>^{hEntry entry}