Copy over and modify starter from elm-pages.com site.

This commit is contained in:
Dillon Kearns 2019-09-24 10:07:37 -07:00
commit 2c2241c177
29 changed files with 14826 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
elm-stuff/
dist/
.cache/

1
.npmrc Normal file
View File

@ -0,0 +1 @@
loglevel=warn

11
README.md Normal file
View File

@ -0,0 +1,11 @@
# elm-pages-starter
This is an example repo to get you up and running with `elm-pages`.
The entrypoint file is `index.js`. That file imports `src/Main.elm`. The `content` folder is turned into your static pages. The rest is mostly determined by logic in the Elm code! Learn more with the resources below.
## Learn more about `elm-pages`
- Documentation site: https://elm-pages.com
- [Elm Package docs](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/)
- [`elm-pages` blog](https://elm-pages.com/blog)

13
content/blog/draft.md Normal file
View File

@ -0,0 +1,13 @@
---
{
"type": "blog",
"author": "Dillon Kearns",
"title": "A Draft Blog Post",
"description": "I'm not quite ready to share this post with the world",
"image": "/images/article-covers/mountains.jpg",
"draft": true,
"published": "2019-09-21",
}
---
This blog post is a draft! Check out `Index.elm` to see how it's being skipped in the `/blog/` listing page.

19
content/blog/hello.md Normal file
View File

@ -0,0 +1,19 @@
---
{
"type": "blog",
"author": "Dillon Kearns",
"title": "Hello `elm-pages`! 🚀",
"description": "Here's an intro for my blog post to get you interested in reading more...",
"image": "/images/article-covers/hello.jpg",
"published": "2019-09-21",
}
---
Welcome to my blog! It was built with `elm-pages`!
```elm
plus : number -> number -> number
plus m n =
m + n
```

4
content/blog/index.md Normal file
View File

@ -0,0 +1,4 @@
---
title: elm-pages blog
type: blog-index
---

14
content/index.md Normal file
View File

@ -0,0 +1,14 @@
---
title: elm-pages-starter - a simple blog starter
type: page
---
This is an example repo to get you up and running with `elm-pages`.
The entrypoint file is `index.js`. That file imports `src/Main.elm`. The `content` folder is turned into your static pages. The rest is mostly determined by logic in the Elm code! Learn more with the resources below.
## Learn more about `elm-pages`
- Documentation site: https://elm-pages.com
- [Elm Package docs](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/)
- [`elm-pages` blog](https://elm-pages.com/blog)

48
elm.json Normal file
View File

@ -0,0 +1,48 @@
{
"type": "application",
"source-directories": [
"src",
"gen"
],
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"avh4/elm-color": "1.0.0",
"dillonkearns/elm-pages": "1.0.0",
"elm/browser": "1.0.1",
"elm/core": "1.0.2",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3",
"elm/parser": "1.1.0",
"elm/svg": "1.0.1",
"elm/url": "1.0.0",
"elm-community/list-extra": "8.2.2",
"elm-community/result-extra": "2.2.1",
"elm-community/string-extra": "4.0.1",
"elm-explorations/markdown": "1.0.0",
"justinmimbs/date": "3.1.2",
"lukewestby/elm-string-interpolate": "1.0.4",
"mdgriffith/elm-markup": "3.0.1",
"mdgriffith/elm-ui": "1.1.5",
"noahzgordon/elm-color-extra": "1.0.2",
"rtfeldman/elm-hex": "1.0.0"
},
"indirect": {
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/regex": "1.0.0",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2",
"fredcy/elm-parseint": "2.0.1"
}
},
"test-dependencies": {
"direct": {
"elm-explorations/test": "1.2.2"
},
"indirect": {
"elm/random": "1.0.0"
}
}
}

172
gen/Pages.elm Normal file
View File

@ -0,0 +1,172 @@
port module Pages exposing (PathKey, allPages, allImages, application, images, isValidRoute, pages)
import Color exposing (Color)
import Head
import Html exposing (Html)
import Json.Decode
import Json.Encode
import Mark
import Pages.Platform
import Pages.ContentCache exposing (Page)
import Pages.Manifest exposing (DisplayMode, Orientation)
import Pages.Manifest.Category as Category exposing (Category)
import Url.Parser as Url exposing ((</>), s)
import Pages.Document as Document
import Pages.ImagePath as ImagePath exposing (ImagePath)
import Pages.PagePath as PagePath exposing (PagePath)
import Pages.Directory as Directory exposing (Directory)
type PathKey
= PathKey
buildImage : List String -> ImagePath PathKey
buildImage path =
ImagePath.build PathKey ("images" :: path)
buildPage : List String -> PagePath PathKey
buildPage path =
PagePath.build PathKey path
directoryWithIndex : List String -> Directory PathKey Directory.WithIndex
directoryWithIndex path =
Directory.withIndex PathKey allPages path
directoryWithoutIndex : List String -> Directory PathKey Directory.WithoutIndex
directoryWithoutIndex path =
Directory.withoutIndex PathKey allPages path
port toJsPort : Json.Encode.Value -> Cmd msg
application :
{ init : ( userModel, Cmd userMsg )
, update : userMsg -> userModel -> ( userModel, Cmd userMsg )
, subscriptions : userModel -> Sub userMsg
, view : userModel -> List ( PagePath PathKey, metadata ) -> Page metadata view PathKey -> { title : String, body : Html userMsg }
, head : metadata -> List (Head.Tag PathKey)
, documents : List ( String, Document.DocumentHandler metadata view )
, manifest : Pages.Manifest.Config PathKey
, canonicalSiteUrl : String
}
-> Pages.Platform.Program userModel userMsg metadata view
application config =
Pages.Platform.application
{ init = config.init
, view = config.view
, update = config.update
, subscriptions = config.subscriptions
, document = Document.fromList config.documents
, content = content
, toJsPort = toJsPort
, head = config.head
, manifest = config.manifest
, canonicalSiteUrl = config.canonicalSiteUrl
, pathKey = PathKey
}
allPages : List (PagePath PathKey)
allPages =
[ (buildPage [ "blog", "draft" ])
, (buildPage [ "blog", "hello" ])
, (buildPage [ "blog" ])
, (buildPage [ ])
]
pages =
{ blog =
{ draft = (buildPage [ "blog", "draft" ])
, hello = (buildPage [ "blog", "hello" ])
, index = (buildPage [ "blog" ])
, directory = directoryWithIndex ["blog"]
}
, index = (buildPage [ ])
, directory = directoryWithIndex []
}
images =
{ articleCovers =
{ hello = (buildImage [ "article-covers", "hello.jpg" ])
, mountains = (buildImage [ "article-covers", "mountains.jpg" ])
, directory = directoryWithoutIndex ["articleCovers"]
}
, author =
{ dillon = (buildImage [ "author", "dillon.jpg" ])
, directory = directoryWithoutIndex ["author"]
}
, elmLogo = (buildImage [ "elm-logo.svg" ])
, github = (buildImage [ "github.svg" ])
, iconPng = (buildImage [ "icon-png.png" ])
, icon = (buildImage [ "icon.svg" ])
, directory = directoryWithoutIndex []
}
allImages : List (ImagePath PathKey)
allImages =
[(buildImage [ "article-covers", "hello.jpg" ])
, (buildImage [ "article-covers", "mountains.jpg" ])
, (buildImage [ "author", "dillon.jpg" ])
, (buildImage [ "elm-logo.svg" ])
, (buildImage [ "github.svg" ])
, (buildImage [ "icon-png.png" ])
, (buildImage [ "icon.svg" ])
]
isValidRoute : String -> Result String ()
isValidRoute route =
let
validRoutes =
List.map PagePath.toString allPages
in
if
(route |> String.startsWith "http://")
|| (route |> String.startsWith "https://")
|| (route |> String.startsWith "#")
|| (validRoutes |> List.member route)
then
Ok ()
else
("Valid routes:\n"
++ String.join "\n\n" validRoutes
)
|> Err
content : List ( List String, { extension: String, frontMatter : String, body : Maybe String } )
content =
[
( ["blog", "draft"]
, { frontMatter = """{"type":"blog","author":"Dillon Kearns","title":"A Draft Blog Post","description":"I'm not quite ready to share this post with the world","image":"/images/article-covers/mountains.jpg","draft":true,"published":"2019-09-21"}
""" , body = Nothing
, extension = "md"
} )
,
( ["blog", "hello"]
, { frontMatter = """{"type":"blog","author":"Dillon Kearns","title":"Hello `elm-pages`! 🚀","description":"Here's an intro for my blog post to get you interested in reading more...","image":"/images/article-covers/hello.jpg","published":"2019-09-21"}
""" , body = Nothing
, extension = "md"
} )
,
( ["blog"]
, { frontMatter = """{"title":"elm-pages blog","type":"blog-index"}
""" , body = Nothing
, extension = "md"
} )
,
( []
, { frontMatter = """{"title":"elm-pages-starter - a simple blog starter","type":"page"}
""" , body = Nothing
, extension = "md"
} )
]

459
gen/Pages/ContentCache.elm Normal file
View File

@ -0,0 +1,459 @@
module Pages.ContentCache exposing
( ContentCache
, Entry(..)
, Page
, Path
, errorView
, extractMetadata
, init
, lazyLoad
, lookup
, lookupMetadata
, pagesWithErrors
, pathForUrl
, routesForCache
, update
)
import Dict exposing (Dict)
import Html exposing (Html)
import Html.Attributes as Attr
import Http
import Json.Decode
import Mark
import Mark.Error
import Pages.Document as Document exposing (Document)
import Pages.PagePath as PagePath exposing (PagePath)
import Result.Extra
import Task exposing (Task)
import Url exposing (Url)
import Url.Builder
type alias Content =
List ( List String, { extension : String, frontMatter : String, body : Maybe String } )
type alias ContentCache metadata view =
Result Errors (Dict Path (Entry metadata view))
type alias Errors =
Dict Path String
type alias ContentCacheInner metadata view =
Dict Path (Entry metadata view)
type Entry metadata view
= NeedContent String metadata
| Unparsed String metadata String
-- TODO need to have an UnparsedMarkup entry type so the right parser is applied
| Parsed metadata (Result ParseError view)
type alias ParseError =
String
type alias Path =
List String
extractMetadata : pathKey -> ContentCacheInner metadata view -> List ( PagePath pathKey, metadata )
extractMetadata pathKey cache =
cache
|> Dict.toList
|> List.map (\( path, entry ) -> ( PagePath.build pathKey path, getMetadata entry ))
getMetadata : Entry metadata view -> metadata
getMetadata entry =
case entry of
NeedContent extension metadata ->
metadata
Unparsed extension metadata _ ->
metadata
Parsed metadata _ ->
metadata
pagesWithErrors : ContentCache metadata view -> Maybe (Dict (List String) String)
pagesWithErrors cache =
cache
|> Result.map
(\okCache ->
okCache
|> Dict.toList
|> List.filterMap
(\( path, value ) ->
case value of
Parsed metadata (Err parseError) ->
Just ( path, parseError )
_ ->
Nothing
)
)
|> Result.map
(\errors ->
case errors of
[] ->
Nothing
_ ->
errors
|> Dict.fromList
|> Just
)
|> Result.withDefault Nothing
init :
Document metadata view
-> Content
-> ContentCache metadata view
init document content =
parseMetadata document content
|> List.map
(\tuple ->
Tuple.mapSecond
(\result ->
result
|> Result.mapError (\error -> ( Tuple.first tuple, error ))
)
tuple
)
|> combineTupleResults
|> Result.mapError Dict.fromList
|> Result.map Dict.fromList
parseMetadata :
Document metadata view
-> List ( List String, { extension : String, frontMatter : String, body : Maybe String } )
-> List ( List String, Result String (Entry metadata view) )
parseMetadata document content =
content
|> List.map
(Tuple.mapSecond
(\{ frontMatter, extension, body } ->
let
maybeDocumentEntry =
Document.get extension document
in
case maybeDocumentEntry of
Just documentEntry ->
frontMatter
|> documentEntry.frontmatterParser
|> Result.map
(\metadata ->
case body of
Just presentBody ->
Parsed metadata
(parseContent extension presentBody document)
Nothing ->
NeedContent extension metadata
)
Nothing ->
Err ("Could not find extension '" ++ extension ++ "'")
)
)
parseContent :
String
-> String
-> Document metadata view
-> Result String view
parseContent extension body document =
let
maybeDocumentEntry =
Document.get extension document
in
case maybeDocumentEntry of
Just documentEntry ->
documentEntry.contentParser body
Nothing ->
Err ("Could not find extension '" ++ extension ++ "'")
errorView : Errors -> Html msg
errorView errors =
errors
|> Dict.toList
|> List.map errorEntryView
|> Html.div
[ Attr.style "padding" "20px 100px"
]
errorEntryView : ( Path, String ) -> Html msg
errorEntryView ( path, error ) =
Html.div []
[ Html.h2 []
[ Html.text ("/" ++ (path |> String.join "/"))
]
, Html.p [] [ Html.text "I couldn't parse the frontmatter in this page. I ran into this error with your JSON decoder:" ]
, Html.pre [] [ Html.text error ]
]
routes : List ( List String, anything ) -> List String
routes record =
record
|> List.map Tuple.first
|> List.map (String.join "/")
|> List.map (\route -> "/" ++ route)
routesForCache : ContentCache metadata view -> List String
routesForCache cacheResult =
case cacheResult of
Ok cache ->
cache
|> Dict.toList
|> routes
Err _ ->
[]
type alias Page metadata view pathKey =
{ metadata : metadata
, path : PagePath pathKey
, view : view
}
renderErrors : ( List String, List Mark.Error.Error ) -> Html msg
renderErrors ( path, errors ) =
Html.div []
[ Html.text (path |> String.join "/")
, errors
|> List.map (Mark.Error.toHtml Mark.Error.Light)
|> Html.div []
]
combineTupleResults :
List ( List String, Result error success )
-> Result (List error) (List ( List String, success ))
combineTupleResults input =
input
|> List.map
(\( path, result ) ->
result
|> Result.map (\success -> ( path, success ))
)
|> combine
combine : List (Result error ( List String, success )) -> Result (List error) (List ( List String, success ))
combine list =
list
|> List.foldr resultFolder (Ok [])
resultFolder : Result error a -> Result (List error) (List a) -> Result (List error) (List a)
resultFolder current soFarResult =
case soFarResult of
Ok soFarOk ->
case current of
Ok currentOk ->
currentOk
:: soFarOk
|> Ok
Err error ->
Err [ error ]
Err soFarErr ->
case current of
Ok currentOk ->
Err soFarErr
Err error ->
error
:: soFarErr
|> Err
{-| Get from the Cache... if it's not already parsed, it will
parse it before returning it and store the parsed version in the Cache
-}
lazyLoad :
Document metadata view
-> Url
-> ContentCache metadata view
-> Task Http.Error (ContentCache metadata view)
lazyLoad document url cacheResult =
case cacheResult of
Err _ ->
Task.succeed cacheResult
Ok cache ->
case Dict.get (pathForUrl url) cache of
Just entry ->
case entry of
NeedContent extension _ ->
httpTask url
|> Task.map
(\downloadedContent ->
update cacheResult
(\thing ->
parseContent extension thing document
)
url
downloadedContent
)
Unparsed extension metadata content ->
update cacheResult
(\thing ->
parseContent extension thing document
)
url
content
|> Task.succeed
Parsed _ _ ->
Task.succeed cacheResult
Nothing ->
Task.succeed cacheResult
httpTask url =
Http.task
{ method = "GET"
, headers = []
, url =
Url.Builder.absolute
((url.path |> String.split "/" |> List.filter (not << String.isEmpty))
++ [ "content.txt"
]
)
[]
, body = Http.emptyBody
, resolver =
Http.stringResolver
(\response ->
case response of
Http.BadUrl_ url_ ->
Err (Http.BadUrl url_)
Http.Timeout_ ->
Err Http.Timeout
Http.NetworkError_ ->
Err Http.NetworkError
Http.BadStatus_ metadata body ->
Err (Http.BadStatus metadata.statusCode)
Http.GoodStatus_ metadata body ->
Ok body
)
, timeout = Nothing
}
update :
ContentCache metadata view
-> (String -> Result ParseError view)
-> Url
-> String
-> ContentCache metadata view
update cacheResult renderer url rawContent =
case cacheResult of
Ok cache ->
Dict.update (pathForUrl url)
(\entry ->
case entry of
Just (Parsed metadata view) ->
entry
Just (Unparsed extension metadata content) ->
Parsed metadata (renderer content)
|> Just
Just (NeedContent extension metadata) ->
Parsed metadata (renderer rawContent)
|> Just
Nothing ->
-- TODO this should never happen
Nothing
)
cache
|> Ok
Err error ->
-- TODO update this ever???
-- Should this be something other than the raw HTML, or just concat the error HTML?
Err error
pathForUrl : Url -> Path
pathForUrl url =
url.path
|> dropTrailingSlash
|> String.split "/"
|> List.drop 1
lookup :
pathKey
-> ContentCache metadata view
-> Url
-> Maybe ( PagePath pathKey, Entry metadata view )
lookup pathKey content url =
case content of
Ok dict ->
let
path =
pathForUrl url
in
Dict.get path dict
|> Maybe.map
(\entry ->
( PagePath.build pathKey path, entry )
)
Err _ ->
Nothing
lookupMetadata :
ContentCache metadata view
-> Url
-> Maybe metadata
lookupMetadata content url =
lookup () content url
|> Maybe.map
(\( pagePath, entry ) ->
case entry of
NeedContent _ metadata ->
metadata
Unparsed _ metadata _ ->
metadata
Parsed metadata _ ->
metadata
)
dropTrailingSlash path =
if path |> String.endsWith "/" then
String.dropRight 1 path
else
path

415
gen/Pages/Platform.elm Normal file
View File

@ -0,0 +1,415 @@
module Pages.Platform exposing (Flags, Model, Msg, Page, Parser, Program, application, cliApplication)
import Browser
import Browser.Navigation
import Dict exposing (Dict)
import Head
import Html exposing (Html)
import Html.Attributes
import Http
import Json.Decode
import Json.Encode
import List.Extra
import Mark
import Pages.ContentCache as ContentCache exposing (ContentCache)
import Pages.Document
import Pages.Manifest as Manifest
import Pages.PagePath as PagePath exposing (PagePath)
import Result.Extra
import Task exposing (Task)
import Url exposing (Url)
dropTrailingSlash path =
if path |> String.endsWith "/" then
String.dropRight 1 path
else
path
type alias Page metadata view pathKey =
{ metadata : metadata
, path : PagePath pathKey
, view : view
}
type alias Content =
List ( List String, { extension : String, frontMatter : String, body : Maybe String } )
type alias Program userModel userMsg metadata view =
Platform.Program Flags (Model userModel userMsg metadata view) (Msg userMsg metadata view)
mainView :
pathKey
-> (userModel -> List ( PagePath pathKey, metadata ) -> Page metadata view pathKey -> { title : String, body : Html userMsg })
-> ModelDetails userModel metadata view
-> { title : String, body : Html userMsg }
mainView pathKey pageView model =
case model.contentCache of
Ok site ->
pageViewOrError pathKey pageView model model.contentCache
-- TODO these lookup helpers should not need it to be a Result
Err errors ->
{ title = "Error parsing"
, body = ContentCache.errorView errors
}
pageViewOrError :
pathKey
-> (userModel -> List ( PagePath pathKey, metadata ) -> Page metadata view pathKey -> { title : String, body : Html userMsg })
-> ModelDetails userModel metadata view
-> ContentCache metadata view
-> { title : String, body : Html userMsg }
pageViewOrError pathKey pageView model cache =
case ContentCache.lookup pathKey cache model.url of
Just ( pagePath, entry ) ->
case entry of
ContentCache.Parsed metadata viewResult ->
case viewResult of
Ok viewList ->
pageView model.userModel
(cache
|> Result.map (ContentCache.extractMetadata pathKey)
|> Result.withDefault []
-- TODO handle error better
)
{ metadata = metadata
, path = pagePath
, view = viewList
}
Err error ->
{ title = "Parsing error"
, body = Html.text error
}
ContentCache.NeedContent extension _ ->
{ title = "", body = Html.text "" }
ContentCache.Unparsed extension _ _ ->
{ title = "", body = Html.text "" }
Nothing ->
{ title = "Page not found"
, body =
Html.div []
[ Html.text "Page not found. Valid routes:\n\n"
, cache
|> ContentCache.routesForCache
|> String.join ", "
|> Html.text
]
}
view :
pathKey
-> Content
-> (userModel -> List ( PagePath pathKey, metadata ) -> Page metadata view pathKey -> { title : String, body : Html userMsg })
-> ModelDetails userModel metadata view
-> Browser.Document (Msg userMsg metadata view)
view pathKey content pageView model =
let
{ title, body } =
mainView pathKey pageView model
in
{ title = title
, body =
[ Html.div
[ Html.Attributes.attribute "data-url" (Url.toString model.url)
]
[ body
|> Html.map UserMsg
]
]
}
encodeHeads : String -> String -> List (Head.Tag pathKey) -> Json.Encode.Value
encodeHeads canonicalSiteUrl currentPagePath head =
Json.Encode.list (Head.toJson canonicalSiteUrl currentPagePath) head
type alias Flags =
{}
combineTupleResults :
List ( List String, Result error success )
-> Result error (List ( List String, success ))
combineTupleResults input =
input
|> List.map
(\( path, result ) ->
result
|> Result.map (\success -> ( path, success ))
)
|> Result.Extra.combine
init :
pathKey
-> String
-> Pages.Document.Document metadata view
-> (Json.Encode.Value -> Cmd (Msg userMsg metadata view))
-> (metadata -> List (Head.Tag pathKey))
-> Content
-> ( userModel, Cmd userMsg )
-> Flags
-> Url
-> Browser.Navigation.Key
-> ( ModelDetails userModel metadata view, Cmd (Msg userMsg metadata view) )
init pathKey canonicalSiteUrl document toJsPort head content initUserModel flags url key =
let
( userModel, userCmd ) =
initUserModel
contentCache =
ContentCache.init document content
in
case contentCache of
Ok okCache ->
( { key = key
, url = url
, userModel = userModel
, contentCache = contentCache
}
, Cmd.batch
([ ContentCache.lookupMetadata (Ok okCache) url
|> Maybe.map head
|> Maybe.map (encodeHeads canonicalSiteUrl url.path)
|> Maybe.map toJsPort
, userCmd |> Cmd.map UserMsg |> Just
, contentCache
|> ContentCache.lazyLoad document url
|> Task.attempt UpdateCache
|> Just
]
|> List.filterMap identity
)
)
Err _ ->
( { key = key
, url = url
, userModel = userModel
, contentCache = contentCache
}
, Cmd.batch
[ userCmd |> Cmd.map UserMsg
]
-- TODO handle errors better
)
type Msg userMsg metadata view
= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
| UserMsg userMsg
| UpdateCache (Result Http.Error (ContentCache metadata view))
| UpdateCacheAndUrl Url (Result Http.Error (ContentCache metadata view))
type Model userModel userMsg metadata view
= Model (ModelDetails userModel metadata view)
| CliModel
type alias ModelDetails userModel metadata view =
{ key : Browser.Navigation.Key
, url : Url.Url
, contentCache : ContentCache metadata view
, userModel : userModel
}
update :
(Json.Encode.Value -> Cmd (Msg userMsg metadata view))
-> Pages.Document.Document metadata view
-> (userMsg -> userModel -> ( userModel, Cmd userMsg ))
-> Msg userMsg metadata view
-> ModelDetails userModel metadata view
-> ( ModelDetails userModel metadata view, Cmd (Msg userMsg metadata view) )
update toJsPort document userUpdate msg model =
case msg of
LinkClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
let
navigatingToSamePage =
url.path == model.url.path
in
if navigatingToSamePage then
-- this is a workaround for an issue with anchor fragment navigation
-- see https://github.com/elm/browser/issues/39
( model, Browser.Navigation.load (Url.toString url) )
else
( model, Browser.Navigation.pushUrl model.key (Url.toString url) )
Browser.External href ->
( model, Browser.Navigation.load href )
UrlChanged url ->
( model
, model.contentCache
|> ContentCache.lazyLoad document url
|> Task.attempt (UpdateCacheAndUrl url)
)
UserMsg userMsg ->
let
( userModel, userCmd ) =
userUpdate userMsg model.userModel
in
( { model | userModel = userModel }, userCmd |> Cmd.map UserMsg )
UpdateCache cacheUpdateResult ->
case cacheUpdateResult of
-- TODO can there be race conditions here? Might need to set something in the model
-- to keep track of the last url change
Ok updatedCache ->
( { model | contentCache = updatedCache }, Cmd.none )
Err _ ->
-- TODO handle error
( model, Cmd.none )
UpdateCacheAndUrl url cacheUpdateResult ->
case cacheUpdateResult of
-- TODO can there be race conditions here? Might need to set something in the model
-- to keep track of the last url change
Ok updatedCache ->
( { model | url = url, contentCache = updatedCache }
, Cmd.none
)
Err _ ->
-- TODO handle error
( { model | url = url }, Cmd.none )
type alias Parser metadata view =
Dict String String
-> List String
-> List ( List String, metadata )
-> Mark.Document view
application :
{ init : ( userModel, Cmd userMsg )
, update : userMsg -> userModel -> ( userModel, Cmd userMsg )
, subscriptions : userModel -> Sub userMsg
, view : userModel -> List ( PagePath pathKey, metadata ) -> Page metadata view pathKey -> { title : String, body : Html userMsg }
, document : Pages.Document.Document metadata view
, content : Content
, toJsPort : Json.Encode.Value -> Cmd (Msg userMsg metadata view)
, head : metadata -> List (Head.Tag pathKey)
, manifest : Manifest.Config pathKey
, canonicalSiteUrl : String
, pathKey : pathKey
}
-> Program userModel userMsg metadata view
application config =
Browser.application
{ init =
\flags url key ->
init config.pathKey config.canonicalSiteUrl config.document config.toJsPort config.head config.content config.init flags url key
|> Tuple.mapFirst Model
, view =
\outerModel ->
case outerModel of
Model model ->
view config.pathKey config.content config.view model
CliModel ->
{ title = "Error"
, body = [ Html.text "Unexpected state" ]
}
, update =
\msg outerModel ->
case outerModel of
Model model ->
update config.toJsPort config.document config.update msg model |> Tuple.mapFirst Model
CliModel ->
( outerModel, Cmd.none )
, subscriptions =
\outerModel ->
case outerModel of
Model model ->
config.subscriptions model.userModel
|> Sub.map UserMsg
CliModel ->
Sub.none
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
cliApplication :
{ init : ( userModel, Cmd userMsg )
, update : userMsg -> userModel -> ( userModel, Cmd userMsg )
, subscriptions : userModel -> Sub userMsg
, view : userModel -> List ( PagePath pathKey, metadata ) -> Page metadata view pathKey -> { title : String, body : Html userMsg }
, document : Pages.Document.Document metadata view
, content : Content
, toJsPort : Json.Encode.Value -> Cmd (Msg userMsg metadata view)
, head : metadata -> List (Head.Tag pathKey)
, manifest : Manifest.Config pathKey
, canonicalSiteUrl : String
, pathKey : pathKey
}
-> Program userModel userMsg metadata view
cliApplication config =
let
contentCache =
ContentCache.init config.document config.content
in
Platform.worker
{ init =
\flags ->
( CliModel
, case contentCache of
Ok _ ->
case contentCache |> ContentCache.pagesWithErrors of
Just pageErrors ->
config.toJsPort
(Json.Encode.object
[ ( "errors", encodeErrors pageErrors )
, ( "manifest", Manifest.toJson config.manifest )
]
)
Nothing ->
config.toJsPort
(Json.Encode.object
[ ( "manifest", Manifest.toJson config.manifest )
]
)
Err error ->
config.toJsPort
(Json.Encode.object
[ ( "errors", encodeErrors error )
, ( "manifest", Manifest.toJson config.manifest )
]
)
)
, update = \msg model -> ( model, Cmd.none )
, subscriptions = \_ -> Sub.none
}
encodeErrors errors =
errors
|> Json.Encode.dict
(\path -> "/" ++ String.join "/" path)
(\errorsForPath -> Json.Encode.string errorsForPath)

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

BIN
images/author/dillon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

39
images/elm-logo.svg Normal file
View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 323.141 322.95" enable-background="new 0 0 323.141 322.95" xml:space="preserve">
<g>
<polygon
fill="#F0AD00"
points="161.649,152.782 231.514,82.916 91.783,82.916"/>
<polygon
fill="#7FD13B"
points="8.867,0 79.241,70.375 232.213,70.375 161.838,0"/>
<rect
fill="#7FD13B"
x="192.99"
y="107.392"
transform="matrix(0.7071 0.7071 -0.7071 0.7071 186.4727 -127.2386)"
width="107.676"
height="108.167"/>
<polygon
fill="#60B5CC"
points="323.298,143.724 323.298,0 179.573,0"/>
<polygon
fill="#5A6378"
points="152.781,161.649 0,8.868 0,314.432"/>
<polygon
fill="#F0AD00"
points="255.522,246.655 323.298,314.432 323.298,178.879"/>
<polygon
fill="#60B5CC"
points="161.649,170.517 8.869,323.298 314.43,323.298"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

1
images/github.svg Normal file
View File

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub icon</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>

After

Width:  |  Height:  |  Size: 827 B

BIN
images/icon-png.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

2
images/icon.svg Normal file
View File

@ -0,0 +1,2 @@
<svg version="1.1" viewBox="251.0485 144.52063 56.114286 74.5" width="50px" height="74.5"><defs><linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="10%" style="stop-color:rgba(1.96%,45.88%,90.2%,1);stop-opacity:1"></stop><stop offset="100%" style="stop-color:rgba(0%,94.9%,37.65%,1);stop-opacity:1"></stop></linearGradient></defs><metadata></metadata><g id="Canvas_11" stroke="none" fill="url(#grad1)" stroke-opacity="1" fill-opacity="1" stroke-dasharray="none"><g id="Canvas_11: Layer 1"><g id="Group_38"><g id="Graphic_32"><path d="M 252.5485 146.02063 L 252.5485 217.52063 L 305.66277 217.52063 L 305.66277 161.68254 L 290.00087 146.02063 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"></path></g><g id="Line_34"><line x1="266.07286" y1="182.8279" x2="290.75465" y2="183.00997" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g><g id="Line_35"><line x1="266.07286" y1="191.84156" x2="290.75465" y2="192.02363" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g><g id="Line_36"><line x1="266.07286" y1="200.85522" x2="290.75465" y2="201.0373" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g><g id="Line_37"><line x1="266.07286" y1="164.80058" x2="278.3874" y2="164.94049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

11
index.js Normal file
View File

@ -0,0 +1,11 @@
import hljs from "highlight.js";
import "highlight.js/styles/github.css";
import "./style.css";
// @ts-ignore
window.hljs = hljs;
const { Elm } = require("./src/Main.elm");
const pagesInit = require("elm-pages");
pagesInit({
mainElmModule: Elm.Main
});

3
netlify.toml Normal file
View File

@ -0,0 +1,3 @@
[build]
publish = "./dist/"
command = "npm run build"

12757
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "elm-pages-example",
"version": "1.0.0",
"description": "Example site built with elm-pages.",
"scripts": {
"start": "elm-pages develop",
"serve": "npm run build && http-server ./dist -a localhost -p 3000 -c-1",
"build": "elm-pages build"
},
"author": "Dillon Kearns",
"license": "BSD-3",
"dependencies": {
"highlight.js": "^9.15.10",
"node-sass": "^4.12.0"
},
"devDependencies": {
"elm": "^0.19.0-no-deps",
"elm-pages": "1.0.35",
"http-server": "^0.11.1"
}
}

48
src/Data/Author.elm Normal file
View File

@ -0,0 +1,48 @@
module Data.Author exposing (Author, all, decoder, view)
import Element exposing (Element)
import Html.Attributes as Attr
import Json.Decode as Decode exposing (Decoder)
import List.Extra
import Pages
import Pages.ImagePath as ImagePath exposing (ImagePath)
type alias Author =
{ name : String
, avatar : ImagePath Pages.PathKey
, bio : String
}
all : List Author
all =
[ { name = "Dillon Kearns"
, avatar = Pages.images.author.dillon
, bio = "Elm developer and educator. Founder of Incremental Elm Consulting."
}
]
decoder : Decoder Author
decoder =
Decode.string
|> Decode.andThen
(\lookupName ->
case List.Extra.find (\currentAuthor -> currentAuthor.name == lookupName) all of
Just author ->
Decode.succeed author
Nothing ->
Decode.fail ("Couldn't find author with name " ++ lookupName ++ ". Options are " ++ String.join ", " (List.map .name all))
)
view : List (Element.Attribute msg) -> Author -> Element msg
view attributes author =
Element.image
(Element.width (Element.px 70)
:: Element.htmlAttribute (Attr.class "avatar")
:: attributes
)
{ src = ImagePath.toString author.avatar, description = author.name }

91
src/DocumentSvg.elm Normal file
View File

@ -0,0 +1,91 @@
module DocumentSvg exposing (view)
import Color
import Element exposing (Element)
import Svg exposing (..)
import Svg.Attributes exposing (..)
strokeColor =
-- "url(#grad1)"
"black"
pageTextColor =
"black"
fillColor =
"url(#grad1)"
-- "none"
fillGradient =
gradient
(Color.rgb255 5 117 230)
(Color.rgb255 0 242 96)
-- (Color.rgb255 252 0 255)
-- (Color.rgb255 0 219 222)
-- (Color.rgb255 255 93 194)
-- (Color.rgb255 255 150 250)
gradient color1 color2 =
linearGradient [ id "grad1", x1 "0%", y1 "0%", x2 "100%", y2 "0%" ]
[ stop
[ offset "10%"
, Svg.Attributes.style ("stop-color:" ++ Color.toCssString color1 ++ ";stop-opacity:1")
]
[]
, stop [ offset "100%", Svg.Attributes.style ("stop-color:" ++ Color.toCssString color2 ++ ";stop-opacity:1") ] []
]
view : Element msg
view =
svg
[ version "1.1"
, viewBox "251.0485 144.52063 56.114286 74.5"
, width "56.114286"
, height "74.5"
, Svg.Attributes.width "30px"
]
[ defs []
[ fillGradient ]
, metadata [] []
, g
[ id "Canvas_11"
, stroke "none"
, fill fillColor
, strokeOpacity "1"
, fillOpacity "1"
, strokeDasharray "none"
]
[ g [ id "Canvas_11: Layer 1" ]
[ g [ id "Group_38" ]
[ g [ id "Graphic_32" ]
[ Svg.path
[ d "M 252.5485 146.02063 L 252.5485 217.52063 L 305.66277 217.52063 L 305.66277 161.68254 L 290.00087 146.02063 Z"
, stroke strokeColor
, strokeLinecap "round"
, strokeLinejoin "round"
, strokeWidth "3"
]
[]
]
, g [ id "Line_34" ] [ line [ x1 "266.07286", y1 "182.8279", x2 "290.75465", y2 "183.00997", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
, g [ id "Line_35" ] [ line [ x1 "266.07286", y1 "191.84156", x2 "290.75465", y2 "192.02363", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
, g [ id "Line_36" ] [ line [ x1 "266.07286", y1 "200.85522", x2 "290.75465", y2 "201.0373", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
, g [ id "Line_37" ] [ line [ x1 "266.07286", y1 "164.80058", x2 "278.3874", y2 "164.94049", stroke pageTextColor, strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [] ]
]
]
]
]
|> Element.html
|> Element.el []

121
src/Index.elm Normal file
View File

@ -0,0 +1,121 @@
module Index exposing (view)
import Data.Author
import Date
import Element exposing (Element)
import Element.Border
import Element.Font
import Metadata exposing (Metadata)
import Pages
import Pages.PagePath as PagePath exposing (PagePath)
import Pages.Platform exposing (Page)
view :
List ( PagePath Pages.PathKey, Metadata )
-> Element msg
view posts =
Element.column [ Element.spacing 20 ]
(posts
|> List.filterMap
(\( path, metadata ) ->
case metadata of
Metadata.Page meta ->
Nothing
Metadata.Author _ ->
Nothing
Metadata.Article meta ->
if meta.draft then
Nothing
else
Just ( path, meta )
Metadata.BlogIndex ->
Nothing
)
|> List.map postSummary
)
postSummary :
( PagePath Pages.PathKey, Metadata.ArticleMetadata )
-> Element msg
postSummary ( postPath, post ) =
articleIndex post
|> linkToPost postPath
linkToPost : PagePath Pages.PathKey -> Element msg -> Element msg
linkToPost postPath content =
Element.link [ Element.width Element.fill ]
{ url = PagePath.toString postPath, label = content }
title : String -> Element msg
title text =
[ Element.text text ]
|> Element.paragraph
[ Element.Font.size 36
, Element.Font.center
, Element.Font.family [ Element.Font.typeface "Raleway" ]
, Element.Font.semiBold
, Element.padding 16
]
articleIndex : Metadata.ArticleMetadata -> Element msg
articleIndex metadata =
Element.el
[ Element.centerX
, Element.width (Element.maximum 800 Element.fill)
, Element.padding 40
, Element.spacing 10
, Element.Border.width 1
, Element.Border.color (Element.rgba255 0 0 0 0.1)
, Element.mouseOver
[ Element.Border.color (Element.rgba255 0 0 0 1)
]
]
(postPreview metadata)
readMoreLink =
Element.text "Continue reading >>"
|> Element.el
[ Element.centerX
, Element.Font.size 18
, Element.alpha 0.6
, Element.mouseOver [ Element.alpha 1 ]
, Element.Font.underline
, Element.Font.center
]
postPreview : Metadata.ArticleMetadata -> Element msg
postPreview post =
Element.textColumn
[ Element.centerX
, Element.width Element.fill
, Element.spacing 30
, Element.Font.size 18
]
[ title post.title
, Element.row [ Element.spacing 10, Element.centerX ]
[ Data.Author.view [ Element.width (Element.px 40) ] post.author
, Element.text post.author.name
, Element.text ""
, Element.text (post.published |> Date.format "MMMM ddd, yyyy")
]
, post.description
|> Element.text
|> List.singleton
|> Element.paragraph
[ Element.Font.size 22
, Element.Font.center
, Element.Font.family [ Element.Font.typeface "Raleway" ]
]
, readMoreLink
]

419
src/Main.elm Normal file
View File

@ -0,0 +1,419 @@
module Main exposing (main)
import Color
import Data.Author as Author
import Date
import DocumentSvg
import Element exposing (Element)
import Element.Background
import Element.Border
import Element.Font as Font
import Element.Region
import Head
import Head.Seo as Seo
import Html exposing (Html)
import Html.Attributes as Attr
import Index
import Json.Decode
import Markdown
import Metadata exposing (Metadata)
import Pages exposing (images, pages)
import Pages.Directory as Directory exposing (Directory)
import Pages.Document
import Pages.ImagePath as ImagePath exposing (ImagePath)
import Pages.Manifest as Manifest
import Pages.Manifest.Category
import Pages.PagePath as PagePath exposing (PagePath)
import Pages.Platform exposing (Page)
import Palette
manifest : Manifest.Config Pages.PathKey
manifest =
{ backgroundColor = Just Color.white
, categories = [ Pages.Manifest.Category.education ]
, displayMode = Manifest.Standalone
, orientation = Manifest.Portrait
, description = "elm-pages - A statically typed site generator."
, iarcRatingId = Nothing
, name = "elm-pages docs"
, themeColor = Just Color.white
, startUrl = pages.index
, shortName = Just "elm-pages"
, sourceIcon = images.iconPng
}
type alias Rendered =
Element Msg
-- the intellij-elm plugin doesn't support type aliases for Programs so we need to use this line
-- main : Platform.Program Pages.Platform.Flags (Pages.Platform.Model Model Msg Metadata Rendered) (Pages.Platform.Msg Msg Metadata Rendered)
main : Pages.Platform.Program Model Msg Metadata Rendered
main =
Pages.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, documents = [ markdownDocument ]
, head = head
, manifest = manifest
, canonicalSiteUrl = canonicalSiteUrl
}
markdownDocument : ( String, Pages.Document.DocumentHandler Metadata Rendered )
markdownDocument =
Pages.Document.parser
{ extension = "md"
, metadata = Metadata.decoder
, body =
\markdownBody ->
Html.div [] [ Markdown.toHtml [] markdownBody ]
|> Element.html
|> Ok
}
type alias Model =
{}
init : ( Model, Cmd Msg )
init =
( Model, Cmd.none )
type alias Msg =
()
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
() ->
( model, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
view : Model -> List ( PagePath Pages.PathKey, Metadata ) -> Page Metadata Rendered Pages.PathKey -> { title : String, body : Html Msg }
view model siteMetadata page =
let
{ title, body } =
pageView model siteMetadata page
in
{ title = title
, body =
body
|> Element.layout
[ Element.width Element.fill
, Font.size 20
, Font.family [ Font.typeface "Roboto" ]
, Font.color (Element.rgba255 0 0 0 0.8)
]
}
pageView : Model -> List ( PagePath Pages.PathKey, Metadata ) -> Page Metadata Rendered Pages.PathKey -> { title : String, body : Element Msg }
pageView model siteMetadata page =
case page.metadata of
Metadata.Page metadata ->
{ title = metadata.title
, body =
[ header page.path
, Element.column
[ Element.padding 50
, Element.spacing 60
, Element.Region.mainContent
]
[ page.view
]
]
|> Element.textColumn
[ Element.width Element.fill
]
}
Metadata.Article metadata ->
{ title = metadata.title
, body =
Element.column [ Element.width Element.fill ]
[ header page.path
, Element.column
[ Element.padding 30
, Element.spacing 40
, Element.Region.mainContent
, Element.width (Element.fill |> Element.maximum 800)
, Element.centerX
]
(Element.column [ Element.spacing 10 ]
[ Element.row [ Element.spacing 10 ]
[ Author.view [] metadata.author
, Element.column [ Element.spacing 10, Element.width Element.fill ]
[ Element.paragraph [ Font.bold, Font.size 24 ]
[ Element.text metadata.author.name
]
, Element.paragraph [ Font.size 16 ]
[ Element.text metadata.author.bio ]
]
]
]
:: (publishedDateView metadata |> Element.el [ Font.size 16, Font.color (Element.rgba255 0 0 0 0.6) ])
:: Palette.blogHeading metadata.title
:: articleImageView metadata.image
:: [ page.view ]
)
]
}
Metadata.Author author ->
{ title = author.name
, body =
Element.column
[ Element.width Element.fill
]
[ header page.path
, Element.column
[ Element.padding 30
, Element.spacing 20
, Element.Region.mainContent
, Element.width (Element.fill |> Element.maximum 800)
, Element.centerX
]
[ Palette.blogHeading author.name
, Author.view [] author
, Element.paragraph [ Element.centerX, Font.center ] [ page.view ]
]
]
}
Metadata.BlogIndex ->
{ title = "elm-pages blog"
, body =
Element.column [ Element.width Element.fill ]
[ header page.path
, Element.column [ Element.padding 20, Element.centerX ] [ Index.view siteMetadata ]
]
}
articleImageView : ImagePath Pages.PathKey -> Element msg
articleImageView articleImage =
Element.image [ Element.width Element.fill ]
{ src = ImagePath.toString articleImage
, description = "Article cover photo"
}
header : PagePath Pages.PathKey -> Element msg
header currentPath =
Element.column [ Element.width Element.fill ]
[ Element.el
[ Element.height (Element.px 4)
, Element.width Element.fill
, Element.Background.gradient
{ angle = 0.2
, steps =
[ Element.rgb255 0 242 96
, Element.rgb255 5 117 230
]
}
]
Element.none
, Element.row
[ Element.paddingXY 25 4
, Element.spaceEvenly
, Element.width Element.fill
, Element.Region.navigation
, Element.Border.widthEach { bottom = 1, left = 0, right = 0, top = 0 }
, Element.Border.color (Element.rgba255 40 80 40 0.4)
]
[ Element.link []
{ url = "/"
, label =
Element.row [ Font.size 30, Element.spacing 16 ]
[ DocumentSvg.view
, Element.text "elm-pages-starter"
]
}
, Element.row [ Element.spacing 15 ]
[ elmDocsLink
, githubRepoLink
, highlightableLink currentPath pages.blog.directory "Blog"
]
]
]
highlightableLink :
PagePath Pages.PathKey
-> Directory Pages.PathKey Directory.WithIndex
-> String
-> Element msg
highlightableLink currentPath linkDirectory displayName =
let
isHighlighted =
currentPath |> Directory.includes linkDirectory
in
Element.link
(if isHighlighted then
[ Font.underline
, Font.color Palette.color.primary
]
else
[]
)
{ url = linkDirectory |> Directory.indexPath |> PagePath.toString
, label = Element.text displayName
}
{-| <https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards>
<https://htmlhead.dev>
<https://html.spec.whatwg.org/multipage/semantics.html#standard-metadata-names>
<https://ogp.me/>
-}
head : Metadata -> List (Head.Tag Pages.PathKey)
head metadata =
case metadata of
Metadata.Page meta ->
Seo.summaryLarge
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = images.iconPng
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = siteTagline
, locale = Nothing
, title = meta.title
}
|> Seo.website
Metadata.Article meta ->
Seo.summaryLarge
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = meta.image
, alt = meta.description
, dimensions = Nothing
, mimeType = Nothing
}
, description = meta.description
, locale = Nothing
, title = meta.title
}
|> Seo.article
{ tags = []
, section = Nothing
, publishedTime = Just (Date.toIsoString meta.published)
, modifiedTime = Nothing
, expirationTime = Nothing
}
Metadata.Author meta ->
let
( firstName, lastName ) =
case meta.name |> String.split " " of
[ first, last ] ->
( first, last )
[ first, middle, last ] ->
( first ++ " " ++ middle, last )
[] ->
( "", "" )
_ ->
( meta.name, "" )
in
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = meta.avatar
, alt = meta.name ++ "'s elm-pages articles."
, dimensions = Nothing
, mimeType = Nothing
}
, description = meta.bio
, locale = Nothing
, title = meta.name ++ "'s elm-pages articles."
}
|> Seo.profile
{ firstName = firstName
, lastName = lastName
, username = Nothing
}
Metadata.BlogIndex ->
Seo.summaryLarge
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = images.iconPng
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = siteTagline
, locale = Nothing
, title = "elm-pages blog"
}
|> Seo.website
canonicalSiteUrl : String
canonicalSiteUrl =
"https://elm-pages.com"
siteTagline : String
siteTagline =
"A statically typed site generator - elm-pages"
publishedDateView metadata =
Element.text
(metadata.published
|> Date.format "MMMM ddd, yyyy"
)
githubRepoLink : Element msg
githubRepoLink =
Element.newTabLink []
{ url = "https://github.com/dillonkearns/elm-pages"
, label =
Element.image
[ Element.width (Element.px 22)
, Font.color Palette.color.primary
]
{ src = ImagePath.toString Pages.images.github, description = "Github repo" }
}
elmDocsLink : Element msg
elmDocsLink =
Element.newTabLink []
{ url = "https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/"
, label =
Element.image
[ Element.width (Element.px 22)
, Font.color Palette.color.primary
]
{ src = ImagePath.toString Pages.images.elmLogo, description = "Elm Package Docs" }
}

104
src/Metadata.elm Normal file
View File

@ -0,0 +1,104 @@
module Metadata exposing (ArticleMetadata, Metadata(..), PageMetadata, decoder)
import Data.Author
import Date exposing (Date)
import Dict exposing (Dict)
import Element exposing (Element)
import Json.Decode as Decode exposing (Decoder)
import List.Extra
import Pages
import Pages.ImagePath as ImagePath exposing (ImagePath)
type Metadata
= Page PageMetadata
| Article ArticleMetadata
| Author Data.Author.Author
| BlogIndex
type alias ArticleMetadata =
{ title : String
, description : String
, published : Date
, author : Data.Author.Author
, image : ImagePath Pages.PathKey
, draft : Bool
}
type alias PageMetadata =
{ title : String }
decoder =
Decode.field "type" Decode.string
|> Decode.andThen
(\pageType ->
case pageType of
"page" ->
Decode.field "title" Decode.string
|> Decode.map (\title -> Page { title = title })
"blog-index" ->
Decode.succeed BlogIndex
"author" ->
Decode.map3 Data.Author.Author
(Decode.field "name" Decode.string)
(Decode.field "avatar" imageDecoder)
(Decode.field "bio" Decode.string)
|> Decode.map Author
"blog" ->
Decode.map6 ArticleMetadata
(Decode.field "title" Decode.string)
(Decode.field "description" Decode.string)
(Decode.field "published"
(Decode.string
|> Decode.andThen
(\isoString ->
case Date.fromIsoString isoString of
Ok date ->
Decode.succeed date
Err error ->
Decode.fail error
)
)
)
(Decode.field "author" Data.Author.decoder)
(Decode.field "image" imageDecoder)
(Decode.field "draft" Decode.bool
|> Decode.maybe
|> Decode.map (Maybe.withDefault False)
)
|> Decode.map Article
_ ->
Decode.fail <| "Unexpected page type " ++ pageType
)
imageDecoder : Decoder (ImagePath Pages.PathKey)
imageDecoder =
Decode.string
|> Decode.andThen
(\imageAssetPath ->
case findMatchingImage imageAssetPath of
Nothing ->
Decode.fail "Couldn't find image."
Just imagePath ->
Decode.succeed imagePath
)
findMatchingImage : String -> Maybe (ImagePath Pages.PathKey)
findMatchingImage imageAssetPath =
Pages.allImages
|> List.Extra.find
(\image ->
ImagePath.toString image
== imageAssetPath
)

44
src/Palette.elm Normal file
View File

@ -0,0 +1,44 @@
module Palette exposing (blogHeading, color, heading)
import Element exposing (Element)
import Element.Font as Font
import Element.Region
color =
{ primary = Element.rgb255 5 117 230
, secondary = Element.rgb255 0 242 96
}
heading : Int -> List (Element msg) -> Element msg
heading level content =
Element.paragraph
([ Font.bold
, Font.family [ Font.typeface "Raleway" ]
, Element.Region.heading level
]
++ (case level of
1 ->
[ Font.size 36 ]
2 ->
[ Font.size 24 ]
_ ->
[ Font.size 20 ]
)
)
content
blogHeading : String -> Element msg
blogHeading title =
Element.paragraph
[ Font.bold
, Font.family [ Font.typeface "Raleway" ]
, Element.Region.heading 1
, Font.size 36
, Font.center
]
[ Element.text title ]

5
style.css Normal file
View File

@ -0,0 +1,5 @@
@import url("https://fonts.googleapis.com/css?family=Montserrat:400,700|Roboto&display=swap");
.avatar img {
border-radius: 50%;
}