Compare commits

...

119 commits

Author SHA1 Message Date
Danielle McLean 9f647cdf16
Add sslOnlyMiddleware to the Foundation 2017-10-19 12:21:39 +11:00
Danielle McLean 1e07c6f9c3
1.6.4 2017-10-16 23:07:29 +11:00
Danielle McLean 3c072d174a
Actually, uh, write the styles correctly for the previous commit's change -_- 2017-10-16 23:07:10 +11:00
Danielle McLean e7653e3184
1.6.3 2017-10-16 23:02:33 +11:00
Danielle McLean 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
Danielle McLean c96f81855b
1.6.2 2017-10-16 22:40:56 +11:00
Danielle McLean 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
Danielle McLean 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
Danielle McLean bd1fe8fbd8
Make the navbar expand on medium screen widths too 2017-10-16 22:22:18 +11:00
Danielle McLean a3b3986b76
1.6.1 2017-10-16 21:25:27 +11:00
Danielle McLean 99049a88ef
Give code a nice dark colour scheme like the stuff around it 2017-10-16 21:25:17 +11:00
Danielle McLean 50f2cde09c
1.6.0 2017-10-16 21:08:12 +11:00
Danielle McLean 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
Danielle McLean 2d1cbbf105
Remove a redundant import 2017-10-16 20:58:44 +11:00
Danielle McLean 5a83121431
Move the schema.org/BreadcrumbList stuff out of Foundation 2017-10-16 20:57:36 +11:00
Danielle McLean a880e66c31
Put category listings back into the sitemap.xml 2017-10-16 14:56:01 +11:00
Danielle McLean 1364a2e36b
Store categories directly in entry_category 2017-10-16 13:47:49 +11:00
Danielle McLean 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
Danielle McLean 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
Danielle McLean 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
Danielle McLean 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
Danielle McLean 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
Danielle McLean 3b00565bda
Set the meta description from the big h-card to match the og:description 2017-10-15 22:33:13 +11:00
Danielle McLean 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
Danielle McLean 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
Danielle McLean 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
Danielle McLean 068ce7a41c
Slightly more efficient implementations of Model.Entry.Shorten 2017-10-15 02:37:37 +11:00
Danielle McLean 940b62b6fc
Remove unused category ID binding to avoid compiler warnings 2017-10-15 01:41:46 +11:00
Danielle McLean e24e7db641
Render entryContent as Markdown too :3 2017-10-15 01:12:18 +11:00
Danielle McLean 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
Danielle McLean 012c62dd09
Fix inconsistent formatting of package version constraints 2017-10-15 00:10:35 +11:00
Danielle McLean 568b5dc036
1.5.0 2017-10-14 17:32:52 +11:00
Danielle McLean fa110ae820
Support setting an fb:app_id in settings.yml 2017-10-14 17:22:07 +11:00
Danielle McLean 2956550a6c
Mark up entry permalink pages with OpenGraph garbage too 2017-10-14 17:09:27 +11:00
Danielle McLean 47fe00a8b8
Support for Open Graph on the homepage - again it's gross but consuming sites understand it 2017-10-14 16:51:01 +11:00
Danielle McLean 44288b419c
Put the <link>s for PGP keys into the page header instead of the h-card itself, since it's invalid HTML to have <link> in the body 2017-10-14 15:35:16 +11:00
Danielle McLean 9d0d6ba249
Add category pages to the sitemap 2017-10-14 15:12:50 +11:00
Danielle McLean c184136f8a
Remove unnecessary templates for Handler.Entries - this allows it not to use TH at all :) 2017-10-14 15:04:01 +11:00
Danielle McLean 474ed3cf28
Add basic support for h-entry categories, which are actually more like tags than categories really 2017-10-14 14:59:51 +11:00
Danielle McLean dfafe33e06
1.4.9 2017-10-13 23:07:34 +11:00
Danielle McLean 8099912fdc
Put p-name on the e-content element as well if there's no actual entry name 2017-10-13 23:07:17 +11:00
Danielle McLean 1a9c782666
1.4.8 2017-10-13 22:48:05 +11:00
Danielle McLean a0b8425911
Change the alt on user avatars so it's just the user's name without 'Avatar for', since the h-entry parser infers that the alt is the user's name 2017-10-13 22:47:53 +11:00
Danielle McLean c977f2cd53
1.4.7 2017-10-13 22:35:23 +11:00
Danielle McLean 660b17f279
Improve AvatarR performance by only fetching the avatar from the DB, not the whole User 2017-10-13 22:29:22 +11:00
Danielle McLean 1e7482b30f
Remove the slug column from entries - compute the slug from the entry name, if there is one, and have no slug otherwise 2017-10-11 23:41:52 +11:00
Danielle McLean fa992bc861
Make entry names optional, since notes shouldn't have names and articles should 2017-10-11 23:34:36 +11:00
Danielle McLean 10b1ed834a
Display an adorable tiny sparkline-style avatar for the p-author on each h-entry 2017-10-11 23:09:22 +11:00
Danielle McLean 12bed07680
1.4.6 2017-10-11 21:39:13 +11:00
Danielle McLean e69202829a
Add an /avatars/#UserId route which redirects to the user's current avatar every time, so you don't need to update remote references to it 2017-10-11 21:38:53 +11:00
Danielle McLean 1df217ac01
1.4.5 2017-10-11 20:28:28 +11:00
Danielle McLean 08ed0b65e0
Make the slug and kind columns on Entries into varchars since they shouldn't ever get very long 2017-10-11 20:21:53 +11:00
Danielle McLean ddf9b2fada
Add the lebd logo to the repository, as an SVG 2017-10-11 20:08:30 +11:00
Danielle McLean 3830bed42e
Remove the gross schema.org attributes from the h-card, since mf2 provides the same information in a nicer format anyway 2017-10-11 13:19:13 +11:00
Danielle McLean 0055a4160b
Use an Esqueleto query to efficiently fetch and render the user profiles on h-cards, rather than several queries and lots of fussing around 2017-10-11 13:16:47 +11:00
Danielle McLean f3b12ded69
Make the user who's displayed on the homepage a configurable setting, so it's easier to use lebd if your name isn't Dani 2017-10-11 12:55:24 +11:00
Danielle McLean 66cb093387
Rename module: Entry.Kind -> Model.Entry.Kind 2017-10-11 12:44:33 +11:00
Danielle McLean 02fd91cb77
Add per-request entity caching support
Different areas of the app need access to entities - for example both
the entry handler needs the entry itself to render it, but the
breadcrumbs also need the entry to decide what to label its breadcrumb.
Previously this was achieved by fetching entities from the database
twice. This sucks, so now it's implemented by fetching entities once and
caching them using Yesod's per-request cache.
2017-10-11 12:35:44 +11:00
Danielle McLean efdca09b1c
Make slugs optional - they don't really make sense for notes, only really for entries with names like articles 2017-10-11 10:33:29 +11:00
Danielle McLean 5f4e94bfd2
Show the exact published and updated times of each h-entry as [title]s 2017-10-11 00:14:08 +11:00
Danielle McLean e565e8ccd3
1.4.4 2017-10-10 22:46:12 +11:00
Danielle McLean 09731053c2
Support non-fa site icons by giving sites an actual name separate from their icon class 2017-10-10 22:41:49 +11:00
Danielle McLean 18ffe8ea4d
A little CSS meddling so that OpenWeb icons line up with the Font Awesome ones 2017-10-10 22:41:08 +11:00
Danielle McLean 41539fe979
Load the OpenWeb Icons - they're not being used anywhere yet but will be very soon, since they include Mastodon, XMPP, and indieweb icons 2017-10-10 22:26:32 +11:00
Danielle McLean c5e1f524b1
1.4.3 2017-10-10 19:49:17 +11:00
Danielle McLean 401c544ef1
Add support for u-syndication of each entry, tracking where else it's been posted 2017-10-10 19:45:31 +11:00
Danielle McLean 716769d24e
Refactor site-fetching into a reusable module 2017-10-10 19:16:12 +11:00
Danielle McLean ddf86d901b
1.4.2 2017-10-10 15:24:31 +11:00
Danielle McLean b52a055543
Add support for displayName to profiles, so that ugly user IDs on sites like Stack Exchange can be hidden 2017-10-10 15:23:30 +11:00
Danielle McLean bbe563dee7
Use Mustache templates for site URL so that the username can be inserted anywhere in the template rather than only at the end 2017-10-10 15:16:50 +11:00
Danielle McLean 7b2c1681eb
1.4.1 2017-10-10 00:19:58 +11:00
Danielle McLean 035cc1de3b
Make generated CSS files group-readable so that nginx can actually serve them without me manually chmodding 2017-10-10 00:19:23 +11:00
Danielle McLean c517baac1c
Serve avatar locally rather than through Libravatar, it'll perform better and doesn't require the image's size to be messed with 2017-10-09 23:34:56 +11:00
Danielle McLean 20470aa852
1.4.0 2017-10-09 21:06:41 +11:00
Danielle McLean 9f015a2bc0
Use consistent spacing for all my package version constraints 2017-10-09 21:05:51 +11:00
Danielle McLean 67b61227de
Unhide the foreign-store package so that stack ghci works without errors 2017-10-09 21:05:13 +11:00
Danielle McLean 3b58b57cc5
Pretty up the login page to match everywhere else 2017-10-09 20:30:50 +11:00
Danielle McLean 400c9ca8b4
Tweak heights and margins on the home page so the h-card aligns perfectly with the h-entries 2017-10-09 20:18:46 +11:00
Danielle McLean 05c512de46
Make the home layout work properly on narrow screens 2017-10-09 20:06:19 +11:00
Danielle McLean 0e80d93994
Nicer layout for h-entry, with cute icons 2017-10-09 19:54:58 +11:00
Danielle McLean 03cd1487fe
Add the entry-kind pages and the actual entries to the generated sitemap 2017-10-09 15:40:58 +11:00
Danielle McLean b121d461e2
Apply the main content margin in the default-layout stylesheet rather than individually on every page 2017-10-09 14:10:38 +11:00
Danielle McLean 41375abdd4
Add the main entry kinds to the navbar 2017-10-09 14:03:42 +11:00
Danielle McLean 1b8a07a5f6
Simplify site title handling, automatically suffix site title to all pages 2017-10-09 13:50:26 +11:00
Danielle McLean 4088735c69
Add support for Atom and RSS content feeds - this info is available through mf2 anyway so Atom and RSS are kinda redundant, but we'll include them anyway 2017-10-09 13:32:13 +11:00
Danielle McLean 1f1108a886
Make h-entries look WAY better, the home page looks great now 2017-10-09 12:28:44 +11:00
Danielle McLean 764935adbd
Add schema.org/BreadcrumbList support (grossss) 2017-10-09 12:14:14 +11:00
Danielle McLean 2fd0ff850b
Set title for EntriesR 2017-10-09 10:08:23 +11:00
Danielle McLean c554715df7
Enable breadcrumbs for EntriesR and EntryR 2017-10-09 10:03:03 +11:00
Danielle McLean 934457f671
Pretty up the h-entry as a Bootstrap card 2017-10-09 09:55:34 +11:00
Danielle McLean 7cebf92356
Add permalinks to h-entries 2017-10-09 09:02:42 +11:00
Danielle McLean 9679234062
Add slugs to entries and use them to create individual entry routes 2017-10-09 08:51:42 +11:00
Danielle McLean 36dea2b5ad
Adjust home layout: push my h-card over to the side and display recent entries in the main content area 2017-10-08 17:19:22 +11:00
Danielle McLean 3ff560e07e
Rename the h-card implementation to match up with the naming of h-feed and h-entry 2017-10-08 16:55:21 +11:00
Danielle McLean 4b7a8a7198
Begin adding rudimentary support for posting new entries to the site - needs to be prettier and a lot smarter, but it exists now 2017-10-08 16:51:48 +11:00
Danielle McLean b027125c2e
1.3.0 2017-10-08 15:00:15 +11:00
Danielle McLean a0dd673af7
Add a few more useful rels to the layout's links 2017-10-06 15:04:46 +11:00
Danielle McLean 92373fcfa5
Simplify navbar implementation - this will also allow seamlessly adding a third, centred nav if desired 2017-10-06 14:43:00 +11:00
Danielle McLean 526e105b8b
Pretty up the breadcrumbs since you can see them now 2017-10-06 14:31:34 +11:00
Danielle McLean 1c4212f5d0
Bring back menu items for logging in and out 2017-10-06 14:23:42 +11:00
Danielle McLean ea47fb8b50
Change the login form to a Bootstrapped one - still needs some colours tweaked though 2017-10-06 12:32:27 +11:00
Danielle McLean f934632484
Enable simple username+password login - ugly but serviceable 2017-10-06 10:38:10 +11:00
Danielle McLean e141fb4666
Actually set the 'author' meta correctly in the page head 2017-10-05 23:09:44 +11:00
Danielle McLean edd82eff55
Add schema.org microdata to the h-card - it's basically a worse way to express the same information, but it's what search engines understand :/ 2017-10-05 22:39:16 +11:00
Danielle McLean d02a010b9b
Remove gitrev from dependencies since it isn't actually being used now 2017-10-05 22:06:48 +11:00
Danielle McLean 4a601f804e
1.2.0 2017-10-05 22:05:12 +11:00
Danielle McLean 2eec12a2e3
Ditch the hash field on PGP keys - the ETag will handle cache-busting, so a nice short URL for the key is fine now 2017-10-05 21:33:44 +11:00
Danielle McLean ddcca3104e
Add a utility function, staticR, which can dynamically build StaticR routes with their ETags attached (still calculated at compile-time) 2017-10-05 21:26:29 +11:00
Danielle McLean da82495fa2
1.1.1 2017-10-05 19:28:03 +11:00
Danielle McLean 43a1bb4a79
Fix the tag format in the repository URL - I thought npm version didn't include the leading v but it does 2017-10-05 16:25:28 +11:00
Danielle McLean 9861803d62
Add sync-version hook so that the version number in package.yaml will always match the one in package.json 2017-10-05 16:22:06 +11:00
Danielle McLean 4692e78503
Add a package.json, load generator info from it instead of from settings.yml 2017-10-05 16:19:15 +11:00
Danielle McLean 353f1aa994
Add license file 2017-10-05 14:07:16 +11:00
Danielle McLean 062dd10b99
Display lebd version info in the footer next to the license info 2017-10-05 09:58:32 +11:00
Danielle McLean 8c3a562c7d
Add support for generating an XML sitemap 2017-10-05 09:16:26 +11:00
Danielle McLean 1c5939d863
Remove approot-guessing support, since it should always be configured in the app settings 2017-10-05 09:01:35 +11:00
Danielle McLean d32b698545
Remove unused CSRF-handling JS from the layout, we'll probably not be making AJAX requests here anyway 2017-10-05 07:49:58 +11:00
Danielle McLean 69e3d7f267
Use a much shorter scheme for constructing PGP key URLs - hashes are now MD5 in base64, which isn't secure but doesn't need to be since it's just for cachebusting 2017-10-05 01:24:44 +11:00
Danielle McLean bac575b140
Remove useless rel="me" from u-uid and u-key - the former adds no information and the latter can't be verified 2017-10-05 01:16:53 +11:00
53 changed files with 1030 additions and 202 deletions

2
.gitignore vendored
View file

@ -1,6 +1,7 @@
dist*
static/tmp/
static/combined/
static/uploads/
config/client_session_key.aes
*.hi
*.o
@ -19,3 +20,4 @@ cabal.sandbox.config
*~
\#*
lebd.cabal
node_modules/

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Danielle McLean
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,23 +1,45 @@
User
username Text maxlen=190
password Text maxlen=79
fullName Text maxlen=500
email Text maxlen=190
note Text sqltype=mediumtext
UniqueUsername username
avatar Text maxlen=190
note Markdown sqltype=mediumtext
UniqueUser username
UniqueEmail email
deriving Typeable
PgpKey
userId UserId
fingerprint Text maxlen=40
hash Text maxlen=64
UniqueFingerprint fingerprint
Site
name Text sqltype=varchar(255)
icon Text sqltype=varchar(255)
url Text sqltype=varchar(255)
template Text sqltype=varchar(255)
Profile
userId UserId
siteId SiteId
username Text sqltype=varchar(255)
displayName Text Maybe sqltype=varchar(255)
Entry
kind EntryKind maxlen=255
name Text Maybe maxlen=255
content Markdown sqltype=longtext
photo Text Maybe maxlen=190
published UTCTime
updated UTCTime
authorId UserId
Syndication
entryId EntryId
profileId ProfileId
url Text sqltype=varchar(255)
EntryCategory
entryId EntryId
category Category sqltype=varchar(190)
UniqueEntryCategory entryId category

View file

@ -4,5 +4,15 @@
/favicon.ico FaviconR GET
/keybase.txt KeybaseR GET
/robots.txt RobotsR GET
/sitemap.xml SitemapR GET
/ HomeR GET
/avatars/#UserId AvatarR GET
/categories/#Category CategoryR GET
/feed FeedR GET
!/#EntryKind/feed FeedKindR GET
!/#EntryKind EntriesR GET
!/#EntryKind/#EntryId EntryR GET
!/#EntryKind/#EntryId/#Slug EntryWithSlugR GET

View file

@ -32,4 +32,7 @@ database:
poolsize: "_env:MYSQL_POOLSIZE:10"
title: 00dani.me
#analytics: UA-YOURCODE
app-name: lebd
username: dani
repository: https://gitlab.com/00dani/lebd
fb-app-id: "_env:FB_APP_ID:142105433189339"

6
lebd.svg Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg">
<title>lebd</title>
<circle id="bg" fill="#343A40" cx="100" cy="100" r="100"></circle>
<text id="l" font-family="Arial Unicode MS" font-size="190" fill="#00A6F9" x="15" y="160"></text>
</svg>

After

Width:  |  Height:  |  Size: 326 B

28
package-lock.json generated Normal file
View file

@ -0,0 +1,28 @@
{
"name": "lebd",
"version": "1.6.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"docopt": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/docopt/-/docopt-0.6.2.tgz",
"integrity": "sha1-so6eIiDaXsSffqW7JKR3h0Be6xE=",
"dev": true
},
"openwebicons": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/openwebicons/-/openwebicons-1.4.3.tgz",
"integrity": "sha1-Bs7ri4K3Vqv+mQXhq+X/U/o0Z+k="
},
"sync-version": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sync-version/-/sync-version-1.0.1.tgz",
"integrity": "sha1-Y6aglKmigcUqgA1obqu5ZgH4igs=",
"dev": true,
"requires": {
"docopt": "0.6.2"
}
}
}
}

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "lebd",
"version": "1.6.4",
"description": "the codebase backing 00dani.me, an indieweb.org site",
"repository": {
"type": "git",
"url": "https://gitlab.com/00dani/lebd"
},
"author": "Danielle McLean <dani@00dani.me>",
"license": "MIT",
"bugs": {
"url": "https://gitlab.com/00dani/lebd/issues"
},
"homepage": "https://gitlab.com/00dani/lebd#README",
"devDependencies": {
"sync-version": "^1.0.1"
},
"scripts": {
"version": "sync-version package.yaml && git add ."
},
"dependencies": {
"openwebicons": "^1.4.3"
}
}

View file

@ -1,5 +1,5 @@
name: lebd
version: "1.1.0"
version: "1.6.4"
dependencies:
@ -46,7 +46,21 @@ dependencies:
- time
- case-insensitive
- wai
- libravatar >=0.4 && <0.5
- blaze-markup >=0.8 && <0.9
- conduit-combinators >=1.1 && <1.2
- esqueleto >=2.5 && <2.6
- friendly-time >=0.4 && <0.5
- foreign-store >=0.2 && <0.3
- markdown >=0.1 && <0.2
- mustache >=2.2 && <2.3
- parsec >=3.1 && <3.2
- slug >=0.1 && <0.2
- split >=0.2 && <0.3
- unix >=2.7 && <2.8
- yesod-auth-hashdb >=1.6.2 && <1.7
- yesod-newsfeed >=1.6 && <1.7
- yesod-sitemap >=1.4 && <1.5
# The library contains all of our application code. The executable
# defined below is just a thin wrapper.

View file

@ -6,6 +6,7 @@
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Application
( getApplicationDev
@ -47,7 +48,11 @@ import System.Log.FastLogger (defaultBufSize, newStdoutLoggerSet,
-- Import all relevant handler modules here.
-- Don't forget to add new modules to your cabal file!
import Handler.Avatars
import Handler.Common
import Handler.Categories
import Handler.Entries
import Handler.Feed
import Handler.Home
-- This line actually creates our YesodDispatch instance. It is the second half

View file

@ -11,16 +11,27 @@ import Import.NoFoundation
import Database.Persist.Sql (ConnectionPool, runSqlPool)
import Text.Hamlet (hamletFile)
import Text.Jasmine (minifym)
import Web.Slug (Slug)
-- Used only when in "auth-dummy-login" setting is enabled.
import Yesod.Auth.Dummy
import Yesod.Auth.HashDB (authHashDBWithForm)
import qualified Yesod.Auth.Message as AuthMsg
import Yesod.Auth.OpenId (authOpenId, IdentifierType (Claimed))
import Yesod.Default.Util (addStaticContentExternal)
import Yesod.Core.Types (Logger)
import qualified Yesod.Core.Unsafe as Unsafe
import qualified Data.CaseInsensitive as CI
import qualified Data.Text.Encoding as TE
import Package
import Model.Cache ( getCached )
import Model.Category ( Category )
import Model.Entry ( entryTitle )
import Model.Entry.Kind ( EntryKind, allEntryKinds, pluralise )
import SchemaOrg.BreadcrumbList ( breadcrumbList )
import Data.Aeson ( encode )
import qualified Text.Blaze.Internal as B
import qualified Data.Text as T
import qualified Data.Text.Lazy.Encoding as E
import qualified System.Posix.Files as F
-- | The foundation datatype for your application. This can be a good place to
-- keep settings and values requiring initialization before your application
@ -37,13 +48,8 @@ data App = App
data MenuItem = MenuItem
{ menuItemLabel :: Text
, menuItemRoute :: Route App
, menuItemAccessCallback :: Bool
}
data MenuTypes
= NavbarLeft MenuItem
| NavbarRight MenuItem
-- This is where we define all of the routes in our application. For a full
-- explanation of the syntax, please see:
-- http://www.yesodweb.com/book/routing-and-handlers
@ -61,20 +67,20 @@ mkYesodData "App" $(parseRoutesFile "config/routes")
-- | A convenient synonym for creating forms.
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
-- of settings which can be configured by overriding methods here.
instance Yesod App where
-- Controls the base of generated URLs. For more information on modifying,
-- see: https://github.com/yesodweb/yesod/wiki/Overriding-approot
approot = ApprootRequest $ \app req ->
case appRoot $ appSettings app of
Nothing -> getApprootText guessApproot app req
Just root -> root
approot = ApprootMaster $ fromMaybe "localhost" . appRoot . appSettings
-- Store session data on the client in encrypted cookies,
-- default session idle timeout is 120 minutes
makeSessionBackend _ = sslOnlySessions . strictSameSiteSessions $ Just <$> defaultClientSessionBackend
120 -- timeout in minutes
sessionLifetime
"config/client_session_key.aes"
-- Redirect static requests to a subdomain - this is recommended for best
@ -94,26 +100,21 @@ instance Yesod App where
-- 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
-- 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
master <- getYesod
mmsg <- getMessage
-- muser <- maybeAuthPair
muser <- maybeAuthPair
mcurrentRoute <- getCurrentRoute
-- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance.
(title, parents) <- breadcrumbs
(title, crumbs) <- breadcrumbs
let allCrumbs = maybe crumbs (\route -> crumbs ++ [(route, title)]) mcurrentRoute
jsonCrumbs <- fmap (E.decodeUtf8 . encode) . withUrlRenderer $ breadcrumbList allCrumbs
-- Define the menu items of the header.
let menuItems = []
let navbarLeftMenuItems = [x | NavbarLeft x <- menuItems]
let navbarRightMenuItems = [x | NavbarRight x <- menuItems]
let navbarLeftFilteredMenuItems = [x | x <- navbarLeftMenuItems, menuItemAccessCallback x]
let navbarRightFilteredMenuItems = [x | x <- navbarRightMenuItems, menuItemAccessCallback x]
let navbars = [leftMenuItems, rightMenuItems] <*> [muser]
-- We break up the default layout into two components:
-- default-layout is the contents of the body tag, and
@ -122,18 +123,16 @@ instance Yesod App where
-- you to use normal widget features in default-layout.
pc <- widgetToPageContent $(widgetFile "default-layout")
let globalTitle = toHtml . siteTitle . appSettings $ master
hasPageTitle = not . B.null $ pageTitle pc
fullTitle = if hasPageTitle then mconcat [pageTitle pc, " ~ ", globalTitle] else globalTitle
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
-- The page to be redirected to when authentication is required.
authRoute _ = Just $ AuthR LoginR
-- Routes not requiring authentication.
isAuthorized (AuthR _) _ = return Authorized
isAuthorized HomeR _ = return Authorized
isAuthorized FaviconR _ = return Authorized
isAuthorized KeybaseR _ = return Authorized
isAuthorized RobotsR _ = return Authorized
isAuthorized (StaticR _) _ = return Authorized
isAuthorized _ _ = return Authorized
-- This function creates static content files in the static folder
-- and names them based on a hash of their content. This allows
@ -142,7 +141,7 @@ instance Yesod App where
addStaticContent ext mime content = do
master <- getYesod
let staticDir = appStaticDir $ appSettings master
addStaticContentExternal
external <- addStaticContentExternal
minifym
genFileName
staticDir
@ -150,6 +149,13 @@ instance Yesod App where
ext
mime
content
case external of
(Just (Right (StaticR (StaticRoute filePath _), _))) -> liftIO $ do
let staticPath = ((staticDir ++ "/") ++) . T.unpack . T.intercalate "/" $ filePath
mode <- F.fileMode <$> F.getFileStatus staticPath
F.setFileMode staticPath $ mode `F.unionFileModes` F.groupReadMode
_ -> return ()
return external
where
-- Generate a unique filename based on the content itself
genFileName lbs = "autogen-" ++ base64md5 lbs
@ -163,11 +169,27 @@ instance Yesod App where
makeLogger = return . appLogger
leftMenuItems, rightMenuItems :: Maybe (UserId, User) -> [MenuItem]
leftMenuItems _ = toMenuItem <$> take 5 allEntryKinds
where toMenuItem kind = pluralise kind `MenuItem` EntriesR kind
rightMenuItems = loggedOutItems `maybe` loggedInItems
where loggedOutItems =
[ "log in" `MenuItem` AuthR LoginR
]
loggedInItems (_id, user) =
[ userUsername user `MenuItem` HomeR
, "log out" `MenuItem` AuthR LogoutR
]
-- Define breadcrumbs.
instance YesodBreadcrumbs App where
breadcrumb HomeR = return ("Home", Nothing)
breadcrumb (AuthR _) = return ("Login", Just HomeR)
breadcrumb _ = return ("home", Nothing)
breadcrumb (AuthR _) = return ("log in", Just HomeR)
breadcrumb (EntriesR kind) = return (pluralise kind, Just HomeR)
breadcrumb (EntryR kind entryId) = do
(Entity _ entry) <- getCached entryId
return (entryTitle entry, Just $ EntriesR kind)
breadcrumb (EntryWithSlugR kind entryId _) = breadcrumb $ EntryR kind entryId
breadcrumb _ = return ("home", Nothing)
-- How to run database actions.
instance YesodPersist App where
@ -185,25 +207,19 @@ instance YesodAuth App where
loginDest _ = HomeR
-- Where to send a user after logout
logoutDest _ = HomeR
-- Override the above two destinations when a Referer: header is present
redirectToReferer _ = True
authenticate creds = runDB $ do
x <- getBy . UniqueUsername $ credsIdent creds
case x of
Just (Entity uid _) -> return $ Authenticated uid
Nothing -> Authenticated <$> insert User
{ userUsername = credsIdent creds
, userFullName = ""
, userEmail = ""
, userNote = ""
}
authenticate = fmap toResult . lookupCreds
where
lookupCreds = runDB . getBy . UniqueUser . credsIdent
toResult = UserError AuthMsg.InvalidLogin `maybe` (Authenticated . entityKey)
-- You can add other plugins like Google Email, email or OAuth here
authPlugins app = [authOpenId Claimed []] ++ extraAuthPlugins
-- Enable authDummy login if enabled.
where extraAuthPlugins = [authDummy | appAuthDummyLogin $ appSettings app]
authPlugins _ = [authHashDBWithForm loginForm (Just . UniqueUser)]
where loginForm :: Route App -> Widget
loginForm action = do
mtok <- reqToken <$> getRequest
setTitle "log in"
$(widgetFile "auth/login")
authHttpManager = getHttpManager
-- | Access function to determine if a user is logged in.
@ -237,3 +253,7 @@ unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger
-- https://github.com/yesodweb/yesod/wiki/Sending-email
-- https://github.com/yesodweb/yesod/wiki/Serve-static-files-from-a-separate-domain
-- https://github.com/yesodweb/yesod/wiki/i18n-messages-in-the-scaffolding
userProfile :: User -> Route App
userProfile user
| userUsername user == siteUsername compileTimeAppSettings = HomeR
| otherwise = error "Multiple profile pages are not yet supported"

17
src/Handler/Avatars.hs Normal file
View file

@ -0,0 +1,17 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
module Handler.Avatars where
import Import hiding ( (==.) )
import Database.Esqueleto
import Settings.StaticR ( staticR )
getAvatarR :: UserId -> Handler ()
getAvatarR = responseFrom <=< runDB . select . from . queryAvatar
where responseFrom (a:_) = redirect $ staticR ["img", unValue a]
responseFrom [] = notFound
queryAvatar userId user = do
where_ $ user ^. UserId ==. val userId
return $ user ^. UserAvatar

22
src/Handler/Categories.hs Normal file
View file

@ -0,0 +1,22 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
module Handler.Categories where
import Import hiding ( on, (==.) )
import Database.Esqueleto
import Widget.Feed ( hFeed )
import Model.Category ( Category, asTag )
import qualified Data.Text as T
getCategoryR :: Category -> Handler Html
getCategoryR tag = do
title <- asks $ siteTitle . appSettings
entries <- runDB . select . from $ \(entry `InnerJoin` category) -> do
on $ entry ^. EntryId ==. category ^. EntryCategoryEntryId
where_ $ category ^. EntryCategoryCategory ==. val tag
return entry
defaultLayout $ do
setTitle . toHtml . asTag $ tag
T.concat [asTag tag, " ~ ", title] `hFeed` entries

View file

@ -7,8 +7,15 @@
module Handler.Common where
import Data.FileEmbed (embedFile)
import Database.Esqueleto ( (^.) )
import qualified Database.Esqueleto as E
import Yesod.Sitemap
import Import
import Model.Entry.Kind ( allEntryKinds )
import Widget.Entry ( entryR )
-- These handlers embed files in the executable at compile time to avoid a
-- runtime dependency, and for efficiency.
@ -21,6 +28,35 @@ getKeybaseR :: Handler TypedContent
getKeybaseR = return $ TypedContent typePlain
$ toContent $(embedFile "config/keybase.txt")
getRobotsR :: Handler TypedContent
getRobotsR = return $ TypedContent typePlain
$ toContent $(embedFile "config/robots.txt")
getRobotsR :: Handler Text
getRobotsR = robots SitemapR
getSitemapR :: Handler TypedContent
getSitemapR = do
categories <- runDB . E.select . E.distinct . E.from $ \ec -> do
E.orderBy [E.asc $ ec ^. EntryCategoryCategory]
return $ ec ^. EntryCategoryCategory
entries <- runDB $ selectList [] [Desc EntryPublished]
sitemap $ do
yield SitemapUrl
{ sitemapLoc = HomeR
, sitemapLastMod = Nothing
, sitemapChangeFreq = Just Daily
, sitemapPriority = Nothing
}
yieldMany $ sitemapUrl . CategoryR . E.unValue <$> categories
yieldMany $ sitemapUrl . EntriesR <$> allEntryKinds
yieldMany $ entryToSitemapUrl <$> entries
sitemapUrl :: a -> SitemapUrl a
sitemapUrl loc = SitemapUrl
{ sitemapLoc = loc
, sitemapLastMod = Nothing
, sitemapChangeFreq = Nothing
, sitemapPriority = Nothing
}
entryToSitemapUrl :: Entity Entry -> SitemapUrl (Route App)
entryToSitemapUrl entry = (sitemapUrl $ entryR entry)
{ sitemapLastMod = Just . entryUpdated . entityVal $ entry
}

54
src/Handler/Entries.hs Normal file
View file

@ -0,0 +1,54 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE OverloadedStrings #-}
module Handler.Entries where
import Import
import Yesod.AtomFeed ( atomLink )
import Model.Cache ( getCached )
import Model.Entry ( entryTitle )
import Model.Markdown ( unMarkdown )
import Widget.Entry ( entryR, hEntry )
import Widget.Feed ( hFeed )
import qualified Data.Text as T
import qualified Model.Entry.Kind as K
getEntriesR :: K.EntryKind -> Handler Html
getEntriesR kind = do
entries <- runDB $ selectList [EntryKind ==. kind] [Desc EntryPublished]
title <- asks $ siteTitle . appSettings
let myTitle = T.concat [K.pluralise kind, " ~ ", title]
defaultLayout $ do
setTitle . toHtml . K.pluralise $ kind
FeedKindR kind `atomLink` myTitle
hFeed myTitle entries
getEntryR :: a -> EntryId -> Handler Html
getEntryR _ = renderEntry <=< getCached
getEntryWithSlugR :: a -> EntryId -> b -> Handler Html
getEntryWithSlugR kind = const . getEntryR kind
renderEntry :: (Entity Entry) -> Handler Html
renderEntry entry = do
let correctRoute = entryR entry
actualRoute <- getCurrentRoute
author <- getCached . entryAuthorId $ entityVal entry
when (actualRoute /= Just correctRoute) $
redirectWith movedPermanently301 correctRoute
defaultLayout $ do
setTitle . toHtml . entryTitle . entityVal $ entry
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:type" content="article">
<meta property="og:description" content=#{unMarkdown $ entryContent $ entityVal entry}>
<meta property="article:author" content=@{userProfile $ entityVal author}>
<meta property="article:section" content=#{K.pluralise $ entryKind $ entityVal entry}>
|]
hEntry entry

54
src/Handler/Feed.hs Normal file
View file

@ -0,0 +1,54 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
module Handler.Feed where
import Import
import Data.Time.Clock.POSIX ( posixSecondsToUTCTime )
import Model.Entry ( entryTitle )
import Widget.Entry ( entryR )
import qualified Data.Text as T
import qualified Model.Entry.Kind as K
getFeedR :: Handler TypedContent
getFeedR = do
entries <- runDB $ selectList [] [Desc EntryPublished]
newsFeed $ toFeed entries
getFeedKindR :: K.EntryKind -> Handler TypedContent
getFeedKindR kind = do
entries <- runDB $ selectList [EntryKind ==. kind] [Desc EntryPublished]
let basicFeed = toFeed entries
newsFeed $ basicFeed
{ feedTitle = T.concat [K.pluralise kind, " ~ ", feedTitle basicFeed]
, feedDescription = toHtml $ T.concat ["feed of all ", K.pluralise kind]
, feedLinkSelf = FeedKindR kind
, feedLinkHome = EntriesR kind
}
toFeed :: [Entity Entry] -> Feed (Route App)
toFeed entries@(latestEntry:_) = (toFeed [])
{ feedEntries = toFeedEntry <$> entries
, feedUpdated = entryUpdated $ entityVal latestEntry
}
toFeed [] = Feed
{ feedTitle = siteTitle compileTimeAppSettings
, feedLinkSelf = FeedR
, feedLinkHome = HomeR
, feedAuthor = ""
, feedDescription = "sitewide feed of all entries"
, feedLanguage = "en-au"
, feedUpdated = posixSecondsToUTCTime 0
, feedLogo = Nothing
, feedEntries = []
}
toFeedEntry :: Entity Entry -> FeedEntry (Route App)
toFeedEntry entry = FeedEntry
{ feedEntryLink = entryR entry
, feedEntryUpdated = entryUpdated $ entityVal entry
, feedEntryTitle = entryTitle $ entityVal entry
, feedEntryContent = toHtml . entryContent . entityVal $ entry
, feedEntryEnclosure = Nothing
}

View file

@ -1,19 +1,23 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
module Handler.Home where
import Import
import Widget.Hcard (hcard)
import Yesod.AtomFeed ( atomLink )
import Widget.Card ( hCard )
import Widget.Feed ( hFeed )
getHomeR :: Handler Html
getHomeR = do
user <- runDB . getBy404 $ UniqueUsername "dani"
maybeTitle <- asks $ appTitle . appSettings
settings <- asks appSettings
user <- runDB . getBy404 . UniqueUser . siteUsername $ settings
let title = siteTitle settings
entries <- runDB $ selectList [EntryAuthorId ==. entityKey user] [Desc EntryPublished]
defaultLayout $ do
case maybeTitle of
Just title -> setTitle $ toHtml title
Nothing -> return ()
atomLink FeedR title
$(widgetFile "home")

View file

@ -3,4 +3,5 @@ module Import
) where
import Foundation as Import
import Settings.StaticR as Import
import Import.NoFoundation as Import

View file

@ -11,6 +11,13 @@ module Model where
import ClassyPrelude.Yesod
import Database.Persist.Quasi
import Yesod.Auth.HashDB ( HashDBUser(..) )
import Text.Mustache ( (~>) )
import qualified Text.Mustache as M
import Model.Category ( Category )
import Model.Entry.Kind ( EntryKind )
import Model.Markdown ( Markdown )
-- You can define all of your database entities in the entities file.
-- You can find more information on persistent and how to declare entities
@ -18,3 +25,12 @@ import Database.Persist.Quasi
-- http://www.yesodweb.com/book/persistent/
share [mkPersist sqlSettings, mkMigrate "migrateAll"]
$(persistFileWith lowerCaseSettings "config/models")
instance HashDBUser User where
userPasswordHash = Just . userPassword
setPasswordHash pw u = u { userPassword = pw }
instance M.ToMustache Profile where
toMustache p = M.object
[ "username" ~> profileUsername p
]

23
src/Model/Cache.hs Normal file
View file

@ -0,0 +1,23 @@
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}
module Model.Cache ( getCached ) where
import Data.Typeable ( Typeable )
import Database.Persist ( Entity (..), Key (..), PersistStore, PersistRecordBackend, keyToValues )
import Yesod ( MonadHandler, HandlerSite, YesodPersist, YesodPersistBackend, cachedBy, get404, liftHandlerT, runDB )
import qualified Data.ByteString.Char8 as C
newtype CachedEntity t = CachedEntity { unCachedEntity :: Entity t } deriving Typeable
getCached :: ( MonadHandler m
, YesodPersist (HandlerSite m)
, PersistStore (YesodPersistBackend (HandlerSite m))
, PersistRecordBackend entity (YesodPersistBackend (HandlerSite m))
, Typeable entity
) => Key entity -> m (Entity entity)
getCached entId = liftHandlerT . cached . runDB . withId . get404 $ entId
where key = C.pack . show . keyToValues $ entId
withId = fmap $ Entity entId
cached = fmap unCachedEntity . cachedBy key . fmap CachedEntity

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

30
src/Model/Entry.hs Normal file
View file

@ -0,0 +1,30 @@
{-# LANGUAGE OverloadedStrings #-}
module Model.Entry where
import Model ( Entry, entryName, entryContent )
import Model.Markdown ( Markdown(Markdown), unMarkdown )
import Data.Maybe ( fromMaybe )
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
entryTitle :: Entry -> T.Text
entryTitle = fromMaybe <$> TL.toStrict . unMarkdown . shorten 30 . entryContent <*> entryName
class Shorten a where
shorten :: Int -> a -> a
instance Shorten T.Text where
shorten i t
| T.compareLength t n == GT = flip T.append "..." . T.take (n - 1) $ 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

32
src/Model/Entry/Kind.hs Normal file
View file

@ -0,0 +1,32 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Model.Entry.Kind where
import Database.Persist.TH ( derivePersistField )
import Yesod.Core.Dispatch ( PathPiece, toPathPiece, fromPathPiece )
import qualified Data.Text as T
import Text.Read ( readMaybe )
data EntryKind = Note | Article | Photo | Video | Audio
| Reply | Repost | Like | Favourite | Bookmark | Quote | Rsvp
| Listen | Jam | Watch | Play | Read
deriving (Enum, Eq, Read, Show)
derivePersistField "EntryKind"
allEntryKinds :: [EntryKind]
allEntryKinds = [Note ..]
singularise :: T.Text -> Maybe EntryKind
singularise "replies" = Just Reply
singularise "watches" = Just Watch
singularise k = readMaybe . T.unpack . T.toTitle . T.init $ k
pluralise :: EntryKind -> T.Text
pluralise Reply = "replies"
pluralise Watch = "watches"
pluralise k = T.toLower . flip T.snoc 's' . T.pack . show $ k
instance PathPiece EntryKind where
toPathPiece = pluralise
fromPathPiece = singularise

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

21
src/Package.hs Normal file
View file

@ -0,0 +1,21 @@
{-# LANGUAGE TemplateHaskell #-}
module Package ( Package(..)
, Repository(..)
, package
) where
import Data.Aeson ( eitherDecodeStrict )
import Data.Either ( either )
import Language.Haskell.TH.Syntax ( addDependentFile, lift, runIO )
import Package.Types
import qualified Data.ByteString as B
package :: Package
package = $(do
let f = "package.json"
addDependentFile f
json <- runIO $ B.readFile f
let result = eitherDecodeStrict json :: Either String Package
either fail lift result)

27
src/Package/Types.hs Normal file
View file

@ -0,0 +1,27 @@
{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
module Package.Types where
import Data.Aeson
import Data.Aeson.Types ( fieldLabelModifier )
import Data.Aeson.TH ( deriveJSON )
import Data.Char ( toLower )
import Language.Haskell.TH.Syntax ( Lift )
import Util ( mapFirst )
data Package = Package
{ packageName :: !String
, packageVersion :: !String
, packageRepository :: !Repository
} deriving (Show, Lift)
data Repository = Repository
{ repositoryType :: !String
, repositoryUrl :: !String
} deriving (Show, Lift)
$(deriveJSON defaultOptions { fieldLabelModifier = mapFirst toLower . drop 7 } ''Package)
$(deriveJSON defaultOptions { fieldLabelModifier = mapFirst toLower . drop 10 } ''Repository)

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 Language.Haskell.TH.Syntax (Exp, Name, Q)
import Network.Wai.Handler.Warp (HostPreference)
import Text.Hamlet (HamletSettings(hamletNewlines), NewlineStyle(AlwaysNewlines), defaultHamletSettings)
import Yesod.Default.Config2 (applyEnvValue, configSettingsYml)
import Yesod.Default.Util (WidgetFileSettings(wfsHamletSettings), widgetFileNoReload,
import Yesod.Default.Util (WidgetFileSettings, widgetFileNoReload,
widgetFileReload)
import qualified Database.MySQL.Base as MySQL
@ -59,11 +58,14 @@ data AppSettings = AppSettings
, appSkipCombining :: Bool
-- ^ Perform no stylesheet/script combining
-- Example app-specific configuration values.
, appAnalytics :: Maybe Text
-- ^ Google Analytics code
, appTitle :: Maybe Text
, appFacebookId :: Maybe Int
-- ^ Facebook app ID.
, siteTitle :: Text
-- ^ Site-wide title.
, siteUsername :: Text
-- ^ Username of the site's main user, whose h-card will appear on the
-- homepage.
, appAuthDummyLogin :: Bool
-- ^ Indicate if auth dummy login should be enabled.
@ -92,8 +94,9 @@ instance FromJSON AppSettings where
appMutableStatic <- o .:? "mutable-static" .!= defaultDev
appSkipCombining <- o .:? "skip-combining" .!= defaultDev
appAnalytics <- o .:? "analytics"
appTitle <- o .:? "title"
appFacebookId <- o .:? "fb-app-id"
siteTitle <- o .: "title"
siteUsername <- o .: "username"
-- This code enables MySQL's strict mode, without which MySQL will truncate data.
-- See https://github.com/yesodweb/persistent/wiki/Database-Configuration#strict-mode for details
@ -116,7 +119,7 @@ instance FromJSON AppSettings where
--
-- https://github.com/yesodweb/yesod/wiki/Overriding-widgetFile
widgetFileSettings :: WidgetFileSettings
widgetFileSettings = def { wfsHamletSettings = defaultHamletSettings { hamletNewlines = AlwaysNewlines } }
widgetFileSettings = def
-- | How static files should be combined.
combineSettings :: CombineSettings

21
src/Settings/StaticR.hs Normal file
View file

@ -0,0 +1,21 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Settings.StaticR ( staticR ) where
import Foundation ( App, Route(StaticR) )
import Settings ( appStaticDir, compileTimeAppSettings )
import Yesod.Static ( Route(StaticRoute) )
import Settings.StaticR.TH ( mkHashMap )
import qualified Data.Map as M
import qualified Data.Text as T
staticR :: [T.Text] -> Route App
staticR pieces = StaticR $ StaticRoute pieces params
where params = case pieces `M.lookup` staticMap of
Just etag -> [("etag", etag)]
Nothing -> []
staticMap :: M.Map [T.Text] T.Text
staticMap = M.fromList $(mkHashMap . appStaticDir $ compileTimeAppSettings)

View file

@ -0,0 +1,25 @@
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
module Settings.StaticR.TH ( mkHashMap ) where
import Conduit hiding ( lift )
import Data.List.Split ( splitOn )
import Language.Haskell.TH.Syntax ( Q, Exp, lift, runIO )
import Yesod.Static ( base64md5 )
import qualified Data.ByteString.Lazy as L
base64md5File :: MonadIO m => FilePath -> m String
base64md5File = fmap base64md5 . liftIO . L.readFile
genHashPair :: MonadIO m => FilePath -> m ([String], String)
genHashPair fp = (tail $ splitOn "/" fp,) <$> base64md5File fp
genHashMap :: FilePath -> IO [([String], String)]
genHashMap dir = runConduitRes
$ sourceDirectoryDeep True dir
.| mapMC genHashPair
.| sinkList
mkHashMap :: FilePath -> Q Exp
mkHashMap fp = lift =<< runIO (genHashMap fp)

26
src/Util.hs Normal file
View file

@ -0,0 +1,26 @@
module Util ( compileMustache, entityToTuple, mapFirst ) where
import Database.Persist ( Entity(..), Key )
import Text.Mustache ( Template(..), compileTemplate )
import Text.Mustache.Types ( Node(TextBlock) )
import Text.Parsec.Error ( ParseError )
import qualified Data.Text as T
import qualified Data.HashMap.Lazy as M
mapFirst :: (a -> a) -> [a] -> [a]
mapFirst f (x:xs) = f x : xs
mapFirst _ [] = []
compileMustache :: String -> T.Text -> Template
compileMustache n = either errorTemplate id . compileTemplate n
errorTemplate :: ParseError -> Template
errorTemplate err = Template
{ name = "error"
, ast = [TextBlock . T.pack $ show err]
, partials = M.empty
}
entityToTuple :: Entity t -> (Key t, t)
entityToTuple (Entity key value) = (key, value)

56
src/Widget/Card.hs Normal file
View file

@ -0,0 +1,56 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
module Widget.Card ( hCard ) where
import Import
import Database.Esqueleto ( (^.) )
import qualified Database.Esqueleto as E
import Model.Markdown ( unMarkdown )
import Text.Mustache ( substitute )
import Util ( compileMustache )
import qualified Data.Text as T
prettyPgp :: PgpKey -> Text
prettyPgp = T.unwords . T.chunksOf 4 . pgpKeyFingerprint
routeFromPgp :: PgpKey -> Route App
routeFromPgp PgpKey { pgpKeyFingerprint = f } = staticR ["pgp", T.takeEnd 8 f ++ ".asc"]
profileUrl :: Site -> Profile -> Text
profileUrl site = substitute $ T.unpack (siteName site) `compileMustache` siteTemplate site
hCard :: Entity User -> Widget
hCard (Entity userId user) = do
let (firstName:lastName) = T.words $ userFullName user
mcurrentRoute <- getCurrentRoute
userProfiles <- handlerToWidget . runDB . E.select . E.from $ \(profile `E.InnerJoin` site) -> do
E.on $ profile ^. ProfileSiteId E.==. site ^. SiteId
E.where_ $ profile ^. ProfileUserId E.==. E.val userId
E.orderBy [E.asc $ site ^. SiteName]
return (site, profile)
pgpKeys <- handlerToWidget . runDB $ map entityVal <$> selectList [PgpKeyUserId ==. userId] []
let maybeFb = find (\(Entity _ site, _) -> "Facebook" == siteName site) userProfiles
toWidgetHead [hamlet|
<meta name="author" content=#{userFullName user}>
<meta name="description" content=#{unMarkdown $ userNote user}>
<link rel="author" href=@{HomeR}>
<meta property="og:type" content="profile">
<meta property="og:title" content="#{userFullName user}">
<meta property="og:description" content=#{unMarkdown $ userNote user}>
<meta property="og:image" content=@{staticR ["img", userAvatar user]}>
<meta property="profile:first_name" content=#{firstName}>
<meta property="profile:last_name" content=#{T.unwords lastName}>
<meta property="profile:username" content=#{userUsername user}>
$maybe (_, Entity _ fb) <- maybeFb
<meta property="fb:profile_id" content=#{profileUsername fb}>
$forall key <- pgpKeys
<link rel="pgpkey" type="application/pgp-keys" href=@{routeFromPgp key}>
|]
$(widgetFile "mf2/h-card")

42
src/Widget/Entry.hs Normal file
View file

@ -0,0 +1,42 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
module Widget.Entry ( entryR, hEntry ) where
import Import
import Database.Esqueleto ( (^.) )
import qualified Database.Esqueleto as E
import Data.Time.Format ( defaultTimeLocale, formatTime, iso8601DateFormat )
import Data.Time.Format.Human ( humanReadableTime )
import Model.Entry ( entryTitle )
import Web.Slug ( mkSlug )
data FormattedTime = FormattedTime
{ timeUnfriendly :: String
, timeFriendly :: String
} deriving Eq
toFormattedTime :: MonadIO m => UTCTime -> m FormattedTime
toFormattedTime time = FormattedTime (unfriendly time) <$> friendly time
where unfriendly = formatTime defaultTimeLocale . iso8601DateFormat . Just $ "%H:%M:%S%z"
friendly = liftIO . humanReadableTime
entryR :: Entity Entry -> Route App
entryR (Entity entryId Entry {..}) = route (entryName >>= mkSlug) entryKind entryId
where route (Just s) = \k i -> EntryWithSlugR k i s
route Nothing = EntryR
hEntry :: Entity Entry -> Widget
hEntry (Entity entryId entry) = do
published <- toFormattedTime . entryPublished $ entry
updated <- toFormattedTime . entryUpdated $ entry
posses <- handlerToWidget . runDB . E.select . E.from $ \(syndication `E.InnerJoin` profile `E.InnerJoin` site) -> do
E.on $ profile ^. ProfileSiteId E.==. site ^. SiteId
E.on $ syndication ^. SyndicationProfileId E.==. profile ^. ProfileId
E.where_ $ syndication ^. SyndicationEntryId E.==. E.val entryId
E.orderBy [E.asc $ site ^. SiteName]
return (syndication ^. SyndicationUrl, site ^. SiteIcon, E.coalesceDefault [profile ^. ProfileDisplayName] (profile ^. ProfileUsername))
maybeAuthor <- handlerToWidget . runDB . get . entryAuthorId $ entry
$(widgetFile "mf2/h-entry")

12
src/Widget/Feed.hs Normal file
View file

@ -0,0 +1,12 @@
{-# LANGUAGE TemplateHaskell #-}
module Widget.Feed ( hFeed ) where
import Import
import Widget.Entry ( hEntry )
import qualified Data.Text as T
hFeed :: T.Text -> [Entity Entry] -> Widget
hFeed name entries = do
mroute <- getCurrentRoute
$(widgetFile "mf2/h-feed")

View file

@ -1,41 +0,0 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Widget.Hcard (hcard) where
import Import
import Data.Maybe (fromJust)
import qualified Data.Map as M
import qualified Data.Text as T
import qualified Network.Libravatar as L
libravatar :: (MonadIO m) => User -> m (Maybe Text)
libravatar = liftIO . flip L.avatarUrl opts . L.Email . userEmail
where opts = def { L.optSecure = True, L.optSize = L.Size 512, L.optTryGravatar = False }
entityToTuple :: Entity t -> (Key t, t)
entityToTuple (Entity key value) = (key, value)
arrangeProfiles :: M.Map (Key Site) Site -> [Profile] -> [(Site,Profile)]
arrangeProfiles sites profiles = sortBy icon $ zip profileSites profiles
where findSite = fromJust . flip M.lookup sites . profileSiteId
profileSites = findSite <$> profiles
icon = comparing $ siteIcon . fst
prettyPgp :: PgpKey -> Text
prettyPgp = T.unwords . T.chunksOf 4 . pgpKeyFingerprint
routeFromPgp :: PgpKey -> Route App
routeFromPgp PgpKey { pgpKeyFingerprint = f, pgpKeyHash = h } =
StaticR $ StaticRoute ["pgp", f, h, T.takeEnd 8 f ++ ".asc"] []
hcard :: Entity User -> Widget
hcard (Entity userId user) = do
maybeAvatar <- libravatar user
mcurrentRoute <- getCurrentRoute
userProfiles <- handlerToWidget . runDB $ do
profiles <- map entityVal <$> selectList [ProfileUserId ==. userId] []
sites <- M.fromList . map entityToTuple <$> selectList [SiteId <-. (profileSiteId <$> profiles)] []
return $ arrangeProfiles sites profiles
pgpKeys <- handlerToWidget . runDB $ map entityVal <$> selectList [PgpKeyUserId ==. userId] []
$(widgetFile "hcard")

View file

@ -39,8 +39,7 @@ packages:
- .
# Dependency packages to be pulled from upstream that are not in the resolver
# (e.g., acme-missiles-0.3)
extra-deps:
- libravatar-0.4.0.1
extra-deps: []
# Override default flag values for local packages and extra-deps
flags: {}

BIN
static/img/dewdrop wink.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

1
static/openwebicons Symbolic link
View file

@ -0,0 +1 @@
../node_modules/openwebicons/

View file

@ -0,0 +1,14 @@
body > main
display: flex
align-items: center
justify-content: center
.form-control
background-color: #1d1f21
color: #c9cacc
&:focus
background-color: #404449
color: #c9cacc
.input-group-addon
background-color: #000
color: #fff
width: 2.4rem

View file

@ -0,0 +1,24 @@
<form .container method="post" action=@{action}>
$maybe tok <- mtok
<input type="hidden" name=#{defaultCsrfParamName} value=#{tok}>
<div .card.bg-dark>
<div .card-body>
<div .form-group>
<label .sr-only for="auth-login-username">username
<div .input-group>
<span .input-group-addon aria-hidden>
<i .fa.fa-at>
<input #auth-login-username .form-control type="text" name="username" placeholder="username" required>
<div .form-group>
<label .sr-only for="auth-login-password">password
<div .input-group>
<span .input-group-addon aria-hidden>
<i .fa.fa-asterisk>
<input #auth-login-password .form-control type="password" name="password" placeholder="password" required>
<div .card-footer>
<button type="submit" class="btn btn-primary">
<i .fa.fa-sign-in>
log in

View file

@ -3,50 +3,25 @@ $doctype 5
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="generator" content="#{packageName package} #{packageVersion package}">
<title>#{pageTitle pc}
<meta name="description" content="">
<meta name="author" content="">
<title>#{fullTitle}
$maybe route <- mcurrentRoute
<link rel="canonical" href=@{route}>
<meta property="og:url" content=@{route}>
$maybe fb <- appFacebookId $ appSettings master
<meta property="fb:app_id" content=#{fb}>
<link rel="sitemap" href=@{SitemapR}>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<link rel="stylesheet" href=@{StaticR openwebicons_css_openwebicons_min_css}>
<script src="https://use.fontawesome.com/4fbab4ae27.js">
^{pageHead pc}
<script>
/* The `defaultCsrfMiddleware` Middleware added in Foundation.hs adds a CSRF token to the request cookies. */
/* AJAX requests should add that token to a header to be validated by the server. */
/* See the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package for details. */
var csrfHeaderName = "#{TE.decodeUtf8 $ CI.foldedCase defaultCsrfHeaderName}";
var csrfCookieName = "#{TE.decodeUtf8 defaultCsrfCookieName}";
var csrfToken = Cookies.get(csrfCookieName);
if (csrfToken) {
\ $.ajaxPrefilter(function( options, originalOptions, jqXHR ) {
\ if (!options.crossDomain) {
\ jqXHR.setRequestHeader(csrfHeaderName, csrfToken);
\ }
\ });
}
<body>
^{pageBody pc}
<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://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

@ -1,7 +1,19 @@
a
color: #00a6f9
a:hover
color: #0084d6
&:hover
color: #0084d6
[class^="openwebicons-"], [class*=" openwebicons-"]
text-decoration: none
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
background-color: #1d1f21
color: #c9cacc
@ -10,6 +22,17 @@ body
min-height: 100vh
> main
flex: 1
margin: 2em
> footer
text-align: center
padding-bottom: 1em
display: flex
justify-content: space-evenly
margin: auto 1em
#navbar
justify-content: space-between
.breadcrumb
background-color: #404449
border-radius: 0
.breadcrumb-item.active
color: #fff

View file

@ -1,26 +1,23 @@
<header>
<nav .navbar .navbar-expand-lg .navbar-dark.bg-dark>
$maybe title <- appTitle $ appSettings master
<a .navbar-brand href=@{HomeR}>#{title}
<nav .navbar .navbar-expand-md .navbar-dark.bg-dark>
<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">
<span .navbar-toggler-icon>
<div #navbar .collapse.navbar-collapse>
<ul .navbar-nav.mr-auto>
$forall MenuItem label route _ <- navbarLeftFilteredMenuItems
<li .nav-item :Just route == mcurrentRoute:.active>
<a .nav-link href=@{route}>#{label}
$forall bar <- navbars
<ul .navbar-nav>
$forall MenuItem label route <- bar
<li .nav-item :Just route == mcurrentRoute:.active>
<a .nav-link href=@{route}>#{label}
<ul .navbar-nav>
$forall MenuItem label route _ <- navbarRightFilteredMenuItems
<li .nav-item :Just route == mcurrentRoute:.active>
<a .nav-link href=@{route}>#{label}
$if not $ null parents
<ul .breadcrumb.rounded-0>
$forall bc <- parents
$if not $ null crumbs
<ol .breadcrumb>
$forall (route, title) <- crumbs
<li .breadcrumb-item>
<a href=@{fst bc}>#{snd bc}
<a href=@{route}>#{title}
<li .breadcrumb-item.active>#{title}
<script type="application/ld+json">#{preEscapedToMarkup jsonCrumbs}
$maybe msg <- mmsg
<div .alert.alert-info #message>#{msg}
@ -28,5 +25,10 @@
^{widget}
<footer>
All content on this site is licensed under
a <a rel="license" href="//creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>.
<p>
all content licensed under #
<a rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/">cc by-sa 4.0
$with Package { packageName = n, packageVersion = v, packageRepository = r } <- package
<p>
powered by #
<a rel="code-repository" href="#{repositoryUrl r}/tree/v#{v}">#{n} #{v}

View file

@ -1,26 +0,0 @@
<div .card.h-card .bg-dark>
$maybe route <- mcurrentRoute
<a .u-uid.u-url rel="me" href=@{route} hidden>
$maybe avatar <- maybeAvatar
<img .card-img-top.u-photo src=#{avatar} alt="Avatar for #{userFullName user}">
<div .card-body>
<h4 .card-title.p-name>#{userFullName user}
$forall key <- pgpKeys
<a .card-subtitle.u-key rel="me" href=@{routeFromPgp key}>
<i .fa.fa-key>
#{prettyPgp key}
<link rel="pgpkey" type="application/pgp-keys" href=@{routeFromPgp key}>
<p .card-text.p-note .text-muted>#{userNote user}
<ul .profiles>
<li>
<a .u-email rel="me" href="mailto:#{userEmail user}">
<i .fa.fa-envelope>
#{userEmail user}
$forall (site, profile) <- userProfiles
<li>
<a .u-url rel="me" href="#{siteUrl site}#{profileUsername profile}">
<i .fa.fa-#{siteIcon site}>
#{profileUsername profile}

22
templates/home.cassius Normal file
View file

@ -0,0 +1,22 @@
body > main
display: flex
flex-direction: column
align-items: center
> div.h-feed
flex: 1
margin-bottom: 0
> aside.author
max-width: 25rem
margin-bottom: 2em
> .h-card
position: sticky
top: 1em
@media (min-width: 768px)
body > main
flex-direction: row-reverse
align-items: unset
> div.h-feed
margin-right: 2em
> aside.author
margin-bottom: 0

View file

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

View file

@ -1,7 +1,4 @@
.card.h-card
margin: 2em auto
width: 22rem
ul.profiles
list-style: none
padding-left: 0

View file

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

View file

@ -0,0 +1,12 @@
article.h-entry
.e-content p:last-child
margin-bottom: 0
> .card-footer
display: flex
flex-wrap: wrap
justify-content: space-evenly
> *
margin-right: 1em
> .p-author img
height: 1em
vertical-align: -0.1em

View file

@ -0,0 +1,36 @@
<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>
$maybe name <- entryName entry
<h4 .p-name .card-title itemprop="headline">#{name}
<div .e-content itemprop="articleBody">
#{entryContent entry}
$nothing
<div itemprop="headline" hidden>#{entryTitle entry}
<div .e-content.p-name itemprop="articleBody">
#{entryContent entry}
<div .card-footer>
$maybe author <- maybeAuthor
<a .p-author.h-card href=@{userProfile author}>
<img .u-photo src=@{AvatarR $ entryAuthorId entry} alt=#{userFullName author}>
#{userFullName author}
$# 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>
permalink
<time .dt-published itemprop="datePublished" datetime=#{timeUnfriendly published} title=#{timeUnfriendly published}>
<i .fa.fa-calendar>
#{timeFriendly published}
<time .dt-updated itemprop="dateModified" datetime=#{timeUnfriendly updated} title=#{timeUnfriendly updated} :published == updated:hidden>
<i .fa.fa-pencil>
#{timeFriendly updated}
$forall (E.Value url, E.Value icon, E.Value name) <- posses
<a .u-syndication href=#{url}>
<i .#{icon}>
#{name}

View file

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

View file

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