Add a slides example project.

This commit is contained in:
Dillon Kearns 2021-04-14 07:33:51 -07:00
parent 8de4fc7b12
commit 166563ae16
71 changed files with 37461 additions and 0 deletions

View File

@ -0,0 +1,53 @@
module Data.Author exposing (Author, all, decoder, dillon, view)
import Cloudinary
import Element exposing (Element)
import Html.Attributes as Attr
import Json.Decode as Decode exposing (Decoder)
import List.Extra
import Pages.ImagePath as ImagePath exposing (ImagePath)
type alias Author =
{ name : String
, avatar : ImagePath
, bio : String
}
all : List Author
all =
[ dillon
]
dillon : Author
dillon =
{ name = "Dillon Kearns"
, avatar = Cloudinary.url "v1602899672/elm-radio/dillon-profile_n2lqst.jpg" Nothing 140
, 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 }

View File

@ -0,0 +1,124 @@
module Template.Blog exposing (Model, Msg, template)
import Article
import Document exposing (Document)
import Element
import Head
import Head.Seo as Seo
import Index
import Pages.ImagePath as ImagePath
import Pages.PagePath exposing (PagePath)
import Pages.StaticHttp as StaticHttp
import Shared
import SiteOld
import Template exposing (StaticPayload, TemplateWithState)
type Msg
= Msg
template : TemplateWithState {} StaticData Model Msg
template =
Template.withStaticData
{ head = head
, staticData = \_ -> staticData
, staticRoutes = StaticHttp.succeed []
}
|> Template.buildWithLocalState
{ view = view
, init = init
, update = update
--\_ _ _ model -> ( model, Cmd.none )
, subscriptions = \_ _ _ -> Sub.none
}
staticData : StaticHttp.Request StaticData
staticData =
--StaticFile.glob "content/blog/*.md"
Article.allMetadata
type alias StaticData =
List ( PagePath, Article.ArticleMetadata )
init : {} -> ( Model, Cmd Msg )
init _ =
( Model, Cmd.none )
update :
Shared.Model
-> {}
-> Msg
-> Model
-> ( Model, Cmd Msg )
update sharedModel metadata msg model =
( model, Cmd.none )
type alias Model =
{}
view :
Model
-> Shared.Model
-> StaticPayload StaticData {}
-> Document Msg
view thing model staticPayload =
{ title = "elm-pages blog"
, body =
[ Element.column [ Element.width Element.fill ]
[ Element.column [ Element.padding 20, Element.centerX ]
[ --Element.text
-- (staticPayload.static
-- |> String.join ", "
-- )
Index.view staticPayload.static
]
]
]
}
head : StaticPayload StaticData {} -> List Head.Tag
head staticPayload =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = ImagePath.build [ "images", "icon-png.png" ]
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = SiteOld.tagline
, locale = Nothing
, title = "elm-pages blog"
}
|> Seo.website
--fileRequest : StaticHttp.Request DataFromFile
--fileRequest =
-- StaticFile.request
-- "content/blog/extensible-markdown-parsing-in-elm.md"
-- (OptimizedDecoder.map2 DataFromFile
-- (StaticFile.body
-- |> OptimizedDecoder.andThen
-- (\rawBody ->
-- case rawBody |> MarkdownRenderer.view |> Result.map Tuple.second of
-- Ok renderedBody ->
-- OptimizedDecoder.succeed renderedBody
--
-- Err error ->
-- OptimizedDecoder.fail error
-- )
-- )
-- (StaticFile.frontmatter frontmatterDecoder)
-- )

View File

@ -0,0 +1,283 @@
module Template.Blog.Slug_ exposing (Model, Msg, articlesRequest, routes, template, toRssItem)
import Article
import Cloudinary
import Data.Author as Author exposing (Author)
import Date exposing (Date)
import Document exposing (Document)
import Element exposing (Element)
import Element.Font as Font
import Element.Region
import Glob
import Head
import Head.Seo as Seo
import MarkdownRenderer
import OptimizedDecoder
import Pages.ImagePath as ImagePath exposing (ImagePath)
import Pages.PagePath as PagePath exposing (PagePath)
import Pages.StaticFile as StaticFile
import Pages.StaticHttp as StaticHttp
import Palette
import Rss
import Shared
import SiteOld
import StructuredData
import Template exposing (StaticPayload, Template, TemplateWithState)
type alias Model =
()
type alias Msg =
Never
type alias RouteParams =
{ slug : String }
routes : StaticHttp.Request (List RouteParams)
routes =
Article.blogPostsGlob
|> StaticHttp.map
(List.map
(\globData ->
{ slug = globData.slug }
)
)
type alias BlogPost =
{ title : String
, description : String
, published : Date
, author : Author
, image : ImagePath
, draft : Bool
}
template : Template RouteParams DataFromFile
template =
Template.withStaticData
{ staticData = staticData
, head = head
, staticRoutes = routes
--, route = route
}
|> Template.buildNoState { view = view }
view :
StaticPayload DataFromFile RouteParams
-> Document msg
view { static } =
{ title = static.frontmatter.title
, body =
let
author =
Author.dillon
in
[ Element.column [ Element.width Element.fill ]
[ 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 [] author
, Element.column [ Element.spacing 10, Element.width Element.fill ]
[ Element.paragraph [ Font.bold, Font.size 24 ]
[ Element.text author.name
]
, Element.paragraph [ Font.size 16 ]
[ Element.text author.bio
]
]
]
]
:: (publishedDateView static.frontmatter |> Element.el [ Font.size 16, Font.color (Element.rgba255 0 0 0 0.6) ])
:: Palette.blogHeading static.frontmatter.title
:: articleImageView static.frontmatter.image
:: static.body
|> List.map (Element.map never)
)
]
]
}
head :
StaticPayload DataFromFile RouteParams
-> List Head.Tag
head { path, static } =
let
metadata =
static.frontmatter
in
Head.structuredData
(StructuredData.article
{ title = metadata.title
, description = metadata.description
, author = StructuredData.person { name = Author.dillon.name }
, publisher = StructuredData.person { name = Author.dillon.name }
, url = SiteOld.canonicalUrl ++ "/" ++ PagePath.toString path
, imageUrl = SiteOld.canonicalUrl ++ "/" ++ ImagePath.toString metadata.image
, datePublished = Date.toIsoString metadata.published
, mainEntityOfPage =
StructuredData.softwareSourceCode
{ codeRepositoryUrl = "https://github.com/dillonkearns/elm-pages"
, description = "A statically typed site generator for Elm."
, author = "Dillon Kearns"
, programmingLanguage = StructuredData.elmLang
}
}
)
:: (Seo.summaryLarge
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = metadata.image
, alt = metadata.description
, dimensions = Nothing
, mimeType = Nothing
}
, description = metadata.description
, locale = Nothing
, title = metadata.title
}
|> Seo.article
{ tags = []
, section = Nothing
, publishedTime = Just (Date.toIsoString metadata.published)
, modifiedTime = Nothing
, expirationTime = Nothing
}
)
publishedDateView : { a | published : Date } -> Element msg
publishedDateView metadata =
Element.text
(metadata.published
|> Date.format "MMMM ddd, yyyy"
)
articleImageView : ImagePath -> Element msg
articleImageView articleImage =
Element.image [ Element.width Element.fill ]
{ src = ImagePath.toString articleImage
, description = "Article cover photo"
}
type alias DataFromFile =
{ body : List (Element Msg)
, frontmatter : ArticleMetadata
}
staticData : RouteParams -> StaticHttp.Request DataFromFile
staticData route =
StaticFile.request
("content/blog/" ++ route.slug ++ ".md")
(OptimizedDecoder.map2 DataFromFile
(StaticFile.body
|> OptimizedDecoder.andThen
(\rawBody ->
case rawBody |> MarkdownRenderer.view |> Result.map Tuple.second of
Ok renderedBody ->
OptimizedDecoder.succeed renderedBody
Err error ->
OptimizedDecoder.fail error
)
)
(StaticFile.frontmatter frontmatterDecoder)
)
type alias ArticleMetadata =
{ title : String
, description : String
, published : Date
, image : ImagePath
, draft : Bool
}
frontmatterDecoder : OptimizedDecoder.Decoder ArticleMetadata
frontmatterDecoder =
OptimizedDecoder.map5 ArticleMetadata
(OptimizedDecoder.field "title" OptimizedDecoder.string)
(OptimizedDecoder.field "description" OptimizedDecoder.string)
(OptimizedDecoder.field "published"
(OptimizedDecoder.string
|> OptimizedDecoder.andThen
(\isoString ->
case Date.fromIsoString isoString of
Ok date ->
OptimizedDecoder.succeed date
Err error ->
OptimizedDecoder.fail error
)
)
)
(OptimizedDecoder.field "image" imageDecoder)
(OptimizedDecoder.field "draft" OptimizedDecoder.bool
|> OptimizedDecoder.maybe
|> OptimizedDecoder.map (Maybe.withDefault False)
)
imageDecoder : OptimizedDecoder.Decoder ImagePath
imageDecoder =
OptimizedDecoder.string
|> OptimizedDecoder.map (\cloudinaryAsset -> Cloudinary.url cloudinaryAsset Nothing 800)
toRssItem :
ArticleMetadata
-> Maybe Rss.Item
toRssItem article =
if article.draft then
Nothing
else
Just
{ title = article.title
, description = article.description
, url = "TODO" --PagePath.toString page.path
, categories = []
, author = Author.dillon.name
, pubDate = Rss.Date article.published
, content = Nothing
}
articlesRequest : StaticHttp.Request (List ArticleMetadata)
articlesRequest =
Glob.succeed identity
|> Glob.keep Glob.fullFilePath
|> Glob.drop (Glob.literal "content/blog/")
|> Glob.drop Glob.wildcard
|> Glob.drop (Glob.literal ".md")
|> Glob.toStaticHttp
|> StaticHttp.andThen
(\articleFilePaths ->
articleFilePaths
|> List.filter (\filePath -> filePath |> String.contains "index" |> not)
|> List.map
(\articleFilePath ->
StaticFile.request articleFilePath
(StaticFile.frontmatter frontmatterDecoder)
)
|> StaticHttp.combine
)

View File

@ -0,0 +1,144 @@
module Template.Documentation exposing (Model, Msg, template)
import DocSidebar
import Document exposing (Document)
import Element exposing (Element)
import Element.Events
import Element.Font as Font
import Head
import Head.Seo as Seo
import Json.Decode as Decode
import MarkdownRenderer
import Pages.ImagePath as ImagePath
import Pages.PagePath as PagePath exposing (PagePath)
import Pages.StaticHttp as StaticHttp
import Palette
import Shared
import SiteOld
import Template exposing (StaticPayload, TemplateWithState)
type alias Documentation =
{ title : String }
type alias StaticData =
()
type alias Model =
{}
type Msg
= Increment
template : TemplateWithState {} StaticData Model Msg
template =
Template.noStaticData
{ head = head
, staticRoutes = StaticHttp.succeed []
}
|> Template.buildWithSharedState
{ view = view
, init = init
, update = update
, subscriptions = \_ _ _ _ -> Sub.none
}
init : {} -> ( Model, Cmd Msg )
init _ =
( {}, Cmd.none )
update : {} -> Msg -> Model -> Shared.Model -> ( Model, Cmd Msg, Maybe Shared.SharedMsg )
update _ msg model sharedModel =
case msg of
Increment ->
( model, Cmd.none, Just Shared.IncrementFromChild )
decoder : Decode.Decoder Documentation
decoder =
Decode.map Documentation
(Decode.field "title" Decode.string)
head : StaticPayload StaticData {} -> List Head.Tag
head staticPayload =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = ImagePath.build [ "images", "icon-png.png" ]
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = SiteOld.tagline
, locale = Nothing
, title = "TODO title" -- staticPayload.metadata.title -- TODO
}
|> Seo.website
view :
Model
-> Shared.Model
-> StaticPayload StaticData {}
-> Document Msg
view model sharedModel staticPayload =
{ title = "TODO title" -- staticPayload.metadata.title -- TODO
, body =
[ [ Element.row []
[ --counterView sharedModel,
DocSidebar.view
staticPayload.path
-- allMetadata -- TODO
|> Element.el [ Element.width (Element.fillPortion 2), Element.alignTop, Element.height Element.fill ]
, Element.column [ Element.width (Element.fillPortion 8), Element.padding 35, Element.spacing 15 ]
[ Palette.heading 1
[ Element.text "TODO title" -- Element.text staticPayload.metadata.title -- TODO
]
, Element.column [ Element.spacing 20 ]
[--tocView staticPayload.path (Tuple.first rendered) -- TODO use StaticHttp to render view
--Element.column
-- [ Element.padding 50
-- , Element.spacing 30
-- , Element.Region.mainContent
-- ]
-- (Tuple.second rendered |> List.map (Element.map never))
]
]
]
]
|> Element.textColumn
[ Element.width Element.fill
, Element.height Element.fill
]
]
}
counterView : Shared.Model -> Element Msg
counterView sharedModel =
Element.el [ Element.Events.onClick Increment ] (Element.text <| "Docs count: " ++ String.fromInt sharedModel.counter)
tocView : PagePath -> MarkdownRenderer.TableOfContents -> Element msg
tocView path toc =
Element.column [ Element.alignTop, Element.spacing 20 ]
[ Element.el [ Font.bold, Font.size 22 ] (Element.text "Table of Contents")
, Element.column [ Element.spacing 10 ]
(toc
|> List.map
(\heading ->
Element.link [ Font.color (Element.rgb255 100 100 100) ]
{ url = PagePath.toString path ++ "#" ++ heading.anchorId
, label = Element.text heading.name
}
)
)
]

View File

@ -0,0 +1,68 @@
module Template.Hello.Name_ exposing (Model, Msg, template)
import Document exposing (Document)
import Element
import Head
import Head.Seo as Seo
import Pages.ImagePath as ImagePath
import Pages.StaticHttp as StaticHttp
import Shared
import SiteOld
import Template exposing (StaticPayload, Template)
type alias Model =
()
type alias Msg =
Never
type alias Route =
{ name : String
}
template : Template Route ()
template =
Template.noStaticData
{ head = head
, staticRoutes = StaticHttp.succeed [ { name = "world" } ]
}
|> Template.buildNoState { view = view }
head :
StaticPayload () Route
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = ImagePath.build [ "images", "icon-png.png" ]
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = SiteOld.tagline
, locale = Nothing
, title = "TODO title" -- metadata.title -- TODO
}
|> Seo.website
type alias StaticData =
()
view :
StaticPayload StaticData Route
-> Document msg
view static =
{ title = "TODO title"
, body =
[ Element.text <| "👋 " ++ static.routeParams.name
]
}

View File

@ -0,0 +1,98 @@
module Template.Index exposing (Model, Msg, template)
import Document exposing (Document)
import Element
import Element.Region
import Head
import Head.Seo as Seo
import MarkdownRenderer
import OptimizedDecoder
import Pages.ImagePath as ImagePath
import Pages.StaticFile as StaticFile
import Pages.StaticHttp as StaticHttp
import Shared
import SiteOld
import Template exposing (StaticPayload, Template)
type alias Model =
()
type alias Msg =
Never
type alias Route =
{}
type alias StaticData =
List (Element.Element Msg)
template : Template Route StaticData
template =
Template.withStaticData
{ head = head
, staticRoutes = StaticHttp.succeed []
, staticData = staticData
}
|> Template.buildNoState { view = view }
head :
StaticPayload StaticData Route
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = ImagePath.build [ "images", "icon-png.png" ]
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = SiteOld.tagline
, locale = Nothing
, title = "TODO title" -- metadata.title -- TODO
}
|> Seo.website
view :
StaticPayload StaticData Route
-> Document Msg
view static =
{ title = "elm-pages - a statically typed site generator" -- metadata.title -- TODO
, body =
[ [ Element.column
[ Element.padding 50
, Element.spacing 60
, Element.Region.mainContent
]
static.static
]
|> Element.textColumn
[ Element.width Element.fill
]
]
}
staticData : Route -> StaticHttp.Request (List (Element.Element msg))
staticData route =
StaticFile.request
"content/index.md"
(StaticFile.body
|> OptimizedDecoder.andThen
(\rawBody ->
case rawBody |> MarkdownRenderer.view |> Result.map Tuple.second of
Ok renderedBody ->
OptimizedDecoder.succeed renderedBody
Err error ->
OptimizedDecoder.fail error
)
)

View File

@ -0,0 +1,84 @@
module Template.Showcase exposing (Model, Msg, template)
import Document exposing (Document)
import Element exposing (Element)
import Head
import Head.Seo as Seo
import Pages.ImagePath as ImagePath
import Pages.StaticHttp as StaticHttp
import Shared
import Showcase
import Template exposing (StaticPayload, TemplateWithState)
type alias Model =
()
type alias Msg =
Never
template : TemplateWithState {} StaticData () Msg
template =
Template.withStaticData
{ head = head
, staticRoutes = StaticHttp.succeed []
, staticData = \_ -> staticData
}
|> Template.buildNoState { view = view }
staticData : StaticHttp.Request StaticData
staticData =
Showcase.staticRequest
--(StaticHttp.get
-- (Secrets.succeed "file://elm.json")
-- OptimizedDecoder.string
--)
type alias DataFromFile =
{ body : List (Element Msg), title : String }
type alias StaticData =
List Showcase.Entry
view :
StaticPayload StaticData {}
-> Document Msg
view static =
{ title = "elm-pages blog"
, body =
let
showcaseEntries =
static.static
in
[ Element.column [ Element.width Element.fill ]
[ Element.column [ Element.padding 20, Element.centerX ] [ Showcase.view showcaseEntries ]
]
]
}
head : StaticPayload StaticData {} -> List Head.Tag
head staticPayload =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = ImagePath.build [ "images", "icon-png.png" ]
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = "See some neat sites built using elm-pages! (Or submit yours!)"
, locale = Nothing
, title = "elm-pages sites showcase"
}
|> Seo.website

View File

@ -0,0 +1,138 @@
module Template.Slide.Number_ exposing (Model, Msg, template)
import Document exposing (Document)
import Element exposing (Element)
import Head
import Head.Seo as Seo
import Markdown.Block
import Markdown.Parser
import Markdown.Renderer
import MarkdownRenderer
import OptimizedDecoder
import Pages.ImagePath as ImagePath
import Pages.StaticFile as StaticFile
import Pages.StaticHttp as StaticHttp
import Shared
import Template exposing (StaticPayload, Template, TemplateWithState)
type alias Model =
()
type alias Msg =
Never
type alias RouteParams =
{ number : String }
template : Template RouteParams StaticData
template =
Template.withStaticData
{ head = head
, staticRoutes = StaticHttp.succeed []
, staticData = staticData
}
|> Template.buildNoState { view = view }
staticData : RouteParams -> StaticHttp.Request StaticData
staticData route =
StaticFile.request
"content/slides.md"
(StaticFile.body
|> OptimizedDecoder.andThen
(\rawBody ->
case rawBody |> Markdown.Parser.parse of
Ok okBlocks ->
case
okBlocks
|> markdownIndexedByHeading (route.number |> String.toInt |> Maybe.withDefault 1)
|> Markdown.Renderer.render MarkdownRenderer.renderer
of
Ok renderedBody ->
OptimizedDecoder.succeed renderedBody
Err error ->
OptimizedDecoder.fail error
Err _ ->
OptimizedDecoder.fail ""
)
)
markdownIndexedByHeading :
Int
-> List Markdown.Block.Block
-> List Markdown.Block.Block
markdownIndexedByHeading index markdownBlocks =
Markdown.Block.foldl
(\block ( currentIndex, markdownToKeep ) ->
case block of
Markdown.Block.Heading Markdown.Block.H2 _ ->
let
newIndex =
currentIndex + 1
in
--_ ->
if newIndex == index then
( newIndex, block :: markdownToKeep )
else
( newIndex, markdownToKeep )
_ ->
if currentIndex == index then
( currentIndex, block :: markdownToKeep )
else
( currentIndex, markdownToKeep )
)
( 0, [] )
markdownBlocks
|> Tuple.second
|> List.reverse
head :
StaticPayload StaticData RouteParams
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = ImagePath.build [ "TODO" ]
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = "TODO"
, locale = Nothing
, title = "TODO title" -- metadata.title -- TODO
}
|> Seo.website
type alias StaticData =
List (Element Msg)
view :
StaticPayload StaticData RouteParams
-> Document Msg
view static =
{ title = "TODO title"
, body =
[ Element.column
[ Element.padding 40
]
[ Element.text static.routeParams.number
, Element.column []
static.static
]
]
}

View File

@ -0,0 +1,72 @@
module Template.Time exposing (Model, Msg, template)
import Document exposing (Document)
import Element exposing (Element)
import Head
import Head.Seo as Seo
import Pages.ImagePath as ImagePath
import Pages.StaticHttp as StaticHttp
import Shared
import Template exposing (StaticPayload, Template, TemplateWithState)
type alias Model =
()
type alias Msg =
Never
type alias StaticData =
String
template : Template {} StaticData
template =
Template.withStaticData
{ head = head
, staticRoutes = StaticHttp.succeed []
, staticData = staticData
}
|> Template.buildNoState { view = view }
staticData routeParams =
StaticHttp.succeed "TIME RESPONSE"
--StaticHttp.get (Secrets.succeed "http://worldtimeapi.org/api/timezone/America/Los_Angeles")
-- (OptimizedDecoder.field "datetime" OptimizedDecoder.string)
head :
StaticPayload StaticData {}
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = ImagePath.build [ "images", "icon-png.png" ]
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = "TODO"
, locale = Nothing
, title = "TODO title" -- metadata.title -- TODO
}
|> Seo.website
view :
StaticPayload StaticData {}
-> Document msg
view static =
{ title = "TODO title"
, body =
[ Element.text static.static
]
}

View File

@ -0,0 +1,188 @@
---
{
"author": "Dillon Kearns",
"title": "Extensible Markdown Parsing in Pure Elm",
"description": "Introducing a new parser that extends your palette with no additional syntax",
"image": "v1603304397/elm-pages/article-covers/extensible-markdown-parsing_x9oolz.jpg",
"published": "2019-10-08",
}
---
I'm excited to share a new approach to markdown parsing for the Elm ecosystem: [`dillonkearns/elm-markdown`](https://package.elm-lang.org/packages/dillonkearns/elm-markdown/latest/)!
As a matter of fact, the blog post you're reading right now is being rendered with it.
## Why does Elm need another markdown parser?
I built this tool so that I could:
- Render markdown blocks using my preferred UI library (`elm-ui`, in my case, but you could use `elm-css` or anything else!)
- Extend what can be expressed beyond the standard markdown blocks like headings, lists, etc.
- Inject data from my Elm model into my markdown
And yet, I wanted to do all of this with the benefits that come from using standard Markdown:
- Familiar syntax
- Great editor tooling (I write my blog posts in Ulysses using Markdown, and I have prettier set up to auto format markdown when I'm tweaking markdown directly in my code projects)
- Previews render in Github
- Easy for others to contribute (for example, to the [`elm-pages` docs](https://elm-pages.com/docs))
## Core Features
So how do you get the best of both worlds? There are three key features that give `dillonkearns/elm-markdown` rich extensibility without actually adding to the Markdown syntax:
- ⚙️ **Map HTML to custom Elm rendering functions** (for extensible markdown!)
- 🎨 **Use [custom renderers](https://package.elm-lang.org/packages/dillonkearns/elm-markdown/latest/Markdown-Parser#Renderer)** (for custom rendering with your preferred styles and UI library)
- 🌳 **Give users access to the parsed Markdown Blocks before rendering** (for inspecting, transforming, or extracting data from the parsed Markdown before passing it to your Markdown Renderer)
Let's explore these three key features in more depth.
## ⚙️ Map HTML to custom Elm rendering functions
I didn't want to add additional features that weren't already a part of Markdown syntax. Since HTML is already valid Markdown, it seemed ideal to just use declarative HTML tags to express these custom view elements. `dillonkearns/elm-markdown` leverages that to give you a declarative Elm syntax to explicitly say what kind of HTML is accepted (think JSON Decoders) and, given that accepted HTML, how to render it.
## Markdown Within HTML Tags
What makes this especially useful is that we can render any Markdown content within our HTML tags. So you could have a Markdown file that looks like this.
```markdown
## Markdown Within HTML (Within Markdown)
You can now:
- Render HTML within your Markdown
- Render Markdown within that HTML!
<bio
name="Dillon Kearns"
photo="https://avatars2.githubusercontent.com/u/1384166"
twitter="dillontkearns"
github="dillonkearns"
>
Dillon really likes building things with Elm!
Here are some links:
- [Articles](https://incrementalelm.com/articles)
</bio>
```
And here's the output:
<ellie-output id="7PLvgQ2kSzja1" />
This is a nice way to abstract the presentation logic for team members' bios on an `about-us` page. We want richer presentation logic than plain markdown provides (for example, showing icons with the right dimensions, and displaying them in a row not column view, etc.) Also, since we're using Elm, we get pretty spoiled by explicit and precise error messages. So we'd like to get an error message if we don't provide a required attribute!
Here's the relevant code for handling the `<bio>` HTML tag in our Markdown:
```elm
Markdown.Html.oneOf
[ Markdown.Html.tag "bio"
(\name photoUrl twitter github dribbble renderedChildren ->
bioView renderedChildren name photoUrl twitter github dribbble
)
|> Markdown.Html.withAttribute "name"
|> Markdown.Html.withAttribute "photo"
|> Markdown.Html.withOptionalAttribute "twitter"
|> Markdown.Html.withOptionalAttribute "github"
|> Markdown.Html.withOptionalAttribute "dribbble"
]
```
If we forget to pass in the required `photo` attribute, we'll get an error message like this:
```
Problem with the given value:
<bio
name="Dillon Kearns"
twitter="dillontkearns"
github="dillonkearns"
>
Expecting attribute "photo".
```
### Avoiding low-level HTML in markdown
If you're familiar with [MDX](https://mdxjs.com) (it's Markdown syntax, but extended with some extra syntax from JSX, including like JS `import`s and JSX HTML tags). Guillermo Rauch, the creator of MDX even talks about the benefits that a more declarative approach, like the one `dillonkearns/elm-markdown` takes, could have over the current MDX approach of using low-level `import` statements and JSX syntax [in this talk (around 20:36 - 22:30)](https://www.youtube.com/watch?v=8oFJPVOT7FU&feature=youtu.be&t=1236).
Even with this declarative approach to explicitly allowing the HTML tags you want, it's possible to get very low-level and just create mappings to standard HTML tags. I like to treat the HTML tags within these markdown documents like Web Components rather than raw HTML. That means using it as a very high-level way of expressing your custom views. With standard Github-flavored markdown, you'll often see people injecting `<div>` tags with styles, or `<img>` tags, etc. I consider this too low-level to be injecting into Markdown in most cases. The Markdown document should be more declarative, concerned only with _what_ to render, not _how_ to render it.
## 🎨 Use custom renderers
Many Markdown libraries just give you the rendered HTML directly. With `dillonkearns/elm-markdown`, one of the main goals was to give you full control over presentation at the initial render (rather than needing to add CSS rules to apply to your rendered output). I personally like to use `elm-ui` whenever I can, so I wanted to use that directly not just for my navbar, but to style my rendered markdown blocks.
Beyond just rendering directly to your preferred UI library, custom Renderers also open up a number of new potential uses. You can render your Markdown into `elm-ui` `Element`s, but you could also render it to any other Elm type. That could be data, or even functions. Why would you render a function? Well, that would allow you to inject dynamic data from your Elm model!
Some other use cases that custom Renderers enable:
- Regular old `Html` (using the [`defaultHtmlRenderer`](https://package.elm-lang.org/packages/dillonkearns/elm-markdown/latest/Markdown-Parser#defaultHtmlRenderer))
- Render into [`elm-ui`](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/) `Element`s
- Render into ANSI color codes for rich formatting in terminal output
- Render into plain text with all formatting stripped out (for search functionality)
### Performing validations in Renderers
Another goal with `dillonkearns/elm-markdown` is to allow early and precise feedback. One of my favorite uses of Custom Renderers is to catch dead links (or images). `elm-pages` will stop the production build when the Renderer fails. [Here's the relevant code](https://github.com/dillonkearns/elm-pages/blob/c76e96af497406fb9acf294acebbcb0c0e391197/examples/docs/src/MarkdownRenderer.elm#L90-L93) from elm-pages.com
```elm
renderer : Markdown.Parser.Renderer (Element msg)
renderer =
{
link =
\link body ->
Pages.isValidRoute link.destination
|> Result.map
, -- rest of the Renderer definition
}
```
## 🌳 Give users access to the parsed Markdown Blocks before rendering
Exposing the AST allows for a number of powerful use cases as well. And it does so without requiring you to dig into the internals. You just get access to a nice Elm custom type and you can do what you want with it before passing it on to your Custom Renderer.
Here are some use cases that this feature enables:
- Extract metadata before rendering, like building a table of contents data structure with proper links ([here's an Ellie demo of that!](https://ellie-app.com/7LDzS6r48n8a1))
- Run a validation and turn it into an `Err`, for example, if there are multiple level 1 headings (having multiple `h1`s on a page causes accessibility problems)
- Transform the blocks by applying formatting rules, for example use a title casing function on all headings
- Transform the AST before rendering it, for example dropping each heading down one level (H1s become H2s, etc.)
## The future of `dillonkearns/elm-markdown`
I've been really enjoying using this in production for several weeks. But it certainly isn't fully handling all cases in Github-flavored markdown.
I'm running all 1400 end-to-end test cases from the Marked.js test suite (which is what `elm-explorations/markdown` runs under the hood). And that test suite includes running through every example in the [Github-flavored markdown spec](https://github.github.com/gfm/). You can see nicely formatted markdown with all of the current failures [here](https://github.com/dillonkearns/elm-markdown/tree/master/test-results/failing/GFM). It includes all failures from the Marked.js test suite, organized by feature area. I'm working through handling more of these cases to make it more widely useful, but feel free to use it now with that caveat in mind.
Pull requests are very welcome, I would love community contributions on this project! If you're interested in contributing, check out [the contributing guide in the Github repo](https://github.com/dillonkearns/elm-markdown/blob/master/CONTRIBUTING.md).
### Fault-Tolerance Versus Helpful Errors
That said, the goal is not to get to 100% compliance with the Github-Flavored Markdown Spec. Markdown has a goal of being Fault-Tolerant, meaning it will always try to "do the best it can" rather than giving an error message when something unexpected happens. That means there's no such thing as "invalid markdown." But there is most certainly **"markup that probably doesn't do what you expected."** For example
```
[My link](/home oh wait I forgot to close this link tag...
```
⚠️ This is technically **valid** Markdown!
It "does the best it can" with the input and renders to a raw string rather than rendering a link. So this is an example that is squarely in the category of markup that **"probably doesn't do what you expected."**
The goal of `dillonkearns/elm-markdown` is not fault-tolerance. It prioritizes **helpful error messages** over fault-tolerance. Sound familiar? There is a very similar difference in philosophy between JavaScript and Elm.
So the rule of thumb for `dillonkearns/elm-markdown` is:
- Follow the Github-Flavored Markdown Spec whenever it doesn't cover up feedback about something that "probably doesn't do what you expected"
- Otherwise, break with the Github-Flavored Markdown Spec and instead give a helpful error message
You can follow along with the [current GFM Spec Compliance here](https://github.com/dillonkearns/elm-markdown#current-github-flavored-markdown-compliance).
Thanks for reading! If you give this library a try, let me know what you think. I'd love to hear from you!
You can keep the conversation going on the #elm-pages channel on [the Elm Slack](http://elmlang.herokuapp.com/), or on this Twitter thread 👇
<Oembed url="https://twitter.com/dillontkearns/status/1181588809349091328" />

View File

@ -0,0 +1,189 @@
---
{
"draft": true,
"author": "Dillon Kearns",
"title": "Generating Files with Pure Elm",
"description": "Introducing a new parser that extends your palette with no additional syntax",
"image": "v1603304397/elm-pages/article-covers/generating-files_blzn2d.jpg",
"published": "2020-01-28",
}
---
I'm excited to share a new approach to markdown parsing for the Elm ecosystem: [`dillonkearns/elm-markdown`](https://package.elm-lang.org/packages/dillonkearns/elm-markdown/latest/)!
As a matter of fact, the blog post you're reading right now is being rendered with it.
## Why does Elm need another markdown parser?
I built this tool so that I could:
- Render markdown blocks using my preferred UI library (`elm-ui`, in my case, but you could use `elm-css` or anything else!)
- Extend what can be expressed beyond the standard markdown blocks like headings, lists, etc.
- Inject data from my Elm model into my markdown
And yet, I wanted to do all of this with the benefits that come from using standard Markdown:
- Familiar syntax
- Great editor tooling (I write my blog posts in Ulysses using Markdown, and I have prettier set up to auto format markdown when I'm tweaking markdown directly in my code projects)
- Previews render in Github
- Easy for others to contribute (for example, to the [`elm-pages` docs](https://elm-pages.com/docs))
## Core Features
So how do you get the best of both worlds? There are three key features that give `dillonkearns/elm-markdown` rich extensibility without actually adding to the Markdown syntax:
- ⚙️ **Map HTML to custom Elm rendering functions** (for extensible markdown!)
- 🎨 **Use [custom renderers](https://package.elm-lang.org/packages/dillonkearns/elm-markdown/latest/Markdown-Parser#Renderer)** (for custom rendering with your preferred styles and UI library)
- 🌳 **Give users access to the parsed Markdown Blocks before rendering** (for inspecting, transforming, or extracting data from the parsed Markdown before passing it to your Markdown Renderer)
Let's explore these three key features in more depth.
## ⚙️ Map HTML to custom Elm rendering functions
I didn't want to add additional features that weren't already a part of Markdown syntax. Since HTML is already valid Markdown, it seemed ideal to just use declarative HTML tags to express these custom view elements. `dillonkearns/elm-markdown` leverages that to give you a declarative Elm syntax to explicitly say what kind of HTML is accepted (think JSON Decoders) and, given that accepted HTML, how to render it.
## Markdown Within HTML Tags
What makes this especially useful is that we can render any Markdown content within our HTML tags. So you could have a Markdown file that looks like this.
```markdown
## Markdown Within HTML (Within Markdown)
You can now:
- Render HTML within your Markdown
- Render Markdown within that HTML!
<bio
name="Dillon Kearns"
photo="https://avatars2.githubusercontent.com/u/1384166"
twitter="dillontkearns"
github="dillonkearns"
>
Dillon really likes building things with Elm!
Here are some links:
- [Articles](https://incrementalelm.com/articles)
</bio>
```
And here's the output:
<ellie-output id="7PLvgQ2kSzja1" />
This is a nice way to abstract the presentation logic for team members' bios on an `about-us` page. We want richer presentation logic than plain markdown provides (for example, showing icons with the right dimensions, and displaying them in a row not column view, etc.) Also, since we're using Elm, we get pretty spoiled by explicit and precise error messages. So we'd like to get an error message if we don't provide a required attribute!
Here's the relevant code for handling the `<bio>` HTML tag in our Markdown:
```elm
Markdown.Html.oneOf
[ Markdown.Html.tag "bio"
(\name photoUrl twitter github dribbble renderedChildren ->
bioView renderedChildren name photoUrl twitter github dribbble
)
|> Markdown.Html.withAttribute "name"
|> Markdown.Html.withAttribute "photo"
|> Markdown.Html.withOptionalAttribute "twitter"
|> Markdown.Html.withOptionalAttribute "github"
|> Markdown.Html.withOptionalAttribute "dribbble"
]
```
If we forget to pass in the required `photo` attribute, we'll get an error message like this:
```
Problem with the given value:
<bio
name="Dillon Kearns"
twitter="dillontkearns"
github="dillonkearns"
>
Expecting attribute "photo".
```
### Avoiding low-level HTML in markdown
If you're familiar with [MDX](https://mdxjs.com) (it's Markdown syntax, but extended with some extra syntax from JSX, including like JS `import`s and JSX HTML tags). Guillermo Rauch, the creator of MDX even talks about the benefits that a more declarative approach, like the one `dillonkearns/elm-markdown` takes, could have over the current MDX approach of using low-level `import` statements and JSX syntax [in this talk (around 20:36 - 22:30)](https://www.youtube.com/watch?v=8oFJPVOT7FU&feature=youtu.be&t=1236).
Even with this declarative approach to explicitly allowing the HTML tags you want, it's possible to get very low-level and just create mappings to standard HTML tags. I like to treat the HTML tags within these markdown documents like Web Components rather than raw HTML. That means using it as a very high-level way of expressing your custom views. With standard Github-flavored markdown, you'll often see people injecting `<div>` tags with styles, or `<img>` tags, etc. I consider this too low-level to be injecting into Markdown in most cases. The Markdown document should be more declarative, concerned only with _what_ to render, not _how_ to render it.
## 🎨 Use custom renderers
Many Markdown libraries just give you the rendered HTML directly. With `dillonkearns/elm-markdown`, one of the main goals was to give you full control over presentation at the initial render (rather than needing to add CSS rules to apply to your rendered output). I personally like to use `elm-ui` whenever I can, so I wanted to use that directly not just for my navbar, but to style my rendered markdown blocks.
Beyond just rendering directly to your preferred UI library, custom Renderers also open up a number of new potential uses. You can render your Markdown into `elm-ui` `Element`s, but you could also render it to any other Elm type. That could be data, or even functions. Why would you render a function? Well, that would allow you to inject dynamic data from your Elm model!
Some other use cases that custom Renderers enable:
- Regular old `Html` (using the [`defaultHtmlRenderer`](https://package.elm-lang.org/packages/dillonkearns/elm-markdown/latest/Markdown-Parser#defaultHtmlRenderer))
- Render into [`elm-ui`](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/) `Element`s
- Render into ANSI color codes for rich formatting in terminal output
- Render into plain text with all formatting stripped out (for search functionality)
### Performing validations in Renderers
Another goal with `dillonkearns/elm-markdown` is to allow early and precise feedback. One of my favorite uses of Custom Renderers is to catch dead links (or images). `elm-pages` will stop the production build when the Renderer fails. [Here's the relevant code](https://github.com/dillonkearns/elm-pages/blob/c76e96af497406fb9acf294acebbcb0c0e391197/examples/docs/src/MarkdownRenderer.elm#L90-L93) from elm-pages.com
```elm
renderer : Markdown.Parser.Renderer (Element msg)
renderer =
{
link =
\link body ->
Pages.isValidRoute link.destination
|> Result.map
, -- rest of the Renderer definition
}
```
## 🌳 Give users access to the parsed Markdown Blocks before rendering
Exposing the AST allows for a number of powerful use cases as well. And it does so without requiring you to dig into the internals. You just get access to a nice Elm custom type and you can do what you want with it before passing it on to your Custom Renderer.
Here are some use cases that this feature enables:
- Extract metadata before rendering, like building a table of contents data structure with proper links ([here's an Ellie demo of that!](https://ellie-app.com/7LDzS6r48n8a1))
- Run a validation and turn it into an `Err`, for example, if there are multiple level 1 headings (having multiple `h1`s on a page causes accessibility problems)
- Transform the blocks by applying formatting rules, for example use a title casing function on all headings
- Transform the AST before rendering it, for example dropping each heading down one level (H1s become H2s, etc.)
## The future of `dillonkearns/elm-markdown`
I've been really enjoying using this in production for several weeks. But it certainly isn't fully handling all cases in Github-flavored markdown.
I'm running all 1400 end-to-end test cases from the Marked.js test suite (which is what `elm-explorations/markdown` runs under the hood). And that test suite includes running through every example in the [Github-flavored markdown spec](https://github.github.com/gfm/). You can see nicely formatted markdown with all of the current failures [here](https://github.com/dillonkearns/elm-markdown/tree/master/test-results/failing/GFM). It includes all failures from the Marked.js test suite, organized by feature area. I'm working through handling more of these cases to make it more widely useful, but feel free to use it now with that caveat in mind.
Pull requests are very welcome, I would love community contributions on this project! If you're interested in contributing, check out [the contributing guide in the Github repo](https://github.com/dillonkearns/elm-markdown/blob/master/CONTRIBUTING.md).
### Fault-Tolerance Versus Helpful Errors
That said, the goal is not to get to 100% compliance with the Github-Flavored Markdown Spec. Markdown has a goal of being Fault-Tolerant, meaning it will always try to "do the best it can" rather than giving an error message when something unexpected happens. That means there's no such thing as "invalid markdown." But there is most certainly **"markup that probably doesn't do what you expected."** For example
```
[My link](/home oh wait I forgot to close this link tag...
```
⚠️ This is technically **valid** Markdown!
It "does the best it can" with the input and renders to a raw string rather than rendering a link. So this is an example that is squarely in the category of markup that **"probably doesn't do what you expected."**
The goal of `dillonkearns/elm-markdown` is not fault-tolerance. It prioritizes **helpful error messages** over fault-tolerance. Sound familiar? There is a very similar difference in philosophy between JavaScript and Elm.
So the rule of thumb for `dillonkearns/elm-markdown` is:
- Follow the Github-Flavored Markdown Spec whenever it doesn't cover up feedback about something that "probably doesn't do what you expected"
- Otherwise, break with the Github-Flavored Markdown Spec and instead give a helpful error message
You can follow along with the [current GFM Spec Compliance here](https://github.com/dillonkearns/elm-markdown#current-github-flavored-markdown-compliance).
Thanks for reading! If you give this library a try, let me know what you think. I'd love to hear from you!
You can keep the conversation going on the #elm-pages channel on [the Elm Slack](http://elmlang.herokuapp.com/), or on this Twitter thread 👇
<Oembed url="https://twitter.com/dillontkearns/status/1181588809349091328" />

View File

@ -0,0 +1,164 @@
---
{
"author": "Dillon Kearns",
"title": "Introducing elm-pages 🚀 - a type-centric static site generator",
"description": "Elm is the perfect fit for a static site generator. Learn about some of the features and philosophy behind elm-pages.",
"image": "v1603304397/elm-pages/article-covers/introducing-elm-pages_ceksg2.jpg",
"published": "2019-09-24",
}
---
After a round of closed beta testing (thank you to [Brian](https://twitter.com/brianhicks) and the [`elm-conf 2019`](https://2019.elm-conf.com/) organizing team!), I'm excited to share a new static site generator for Elm!
[Matthew Griffith](https://twitter.com/mech_elephant) and I have had a lot of design discussions and sending code snippets back-and-forth to get to the current design. A big thank you to Matthew for the great discussions and, as always, his ability to look at the bigger picture and question basic assumptions to come up with awesome innovations!
## What is `elm-pages` exactly?
Well, this site you're looking at _right now_ is built with `elm-pages`! For example, the raw content for this post is from [`content/blog/introducing-elm-pages.md`](https://github.com/dillonkearns/elm-pages/blob/master/examples/docs/content/blog/introducing-elm-pages.md).
`elm-pages` takes your static content and turns it into a modern, performant, single-page app. You can do anything you can with a regular Elm site, and yet the framework does a lot for you to optimize site performance and minimize tedious work.
I see a lot of "roll your own" Elm static sites out there these days. When you roll your own Elm static site, you often:
- Manage Strings for each page's content (rather than just having a file for each page)
- Wire up the routing for each page manually (or with a hand-made script)
- Add `<meta>` tags for SEO and to make Twitter/Slack/etc. link shares display the right image and title (or just skip it because it's a pain)
I hope that `elm-pages` will make people's lives easier (and their load times faster). But `elm-pages` is for more than just building your blog or portfolio site. There's a movement now called JAMstack (JavaScript, APIs, and Markup) that is solving a broader set of problems with static sites. JAMstack apps do this by pulling in data from external sources, and using modern frontend frameworks to render the content (which then rehydrate into interactive apps). The goal is to move as much work as possible away from the user's browser and into a build step before pushing static files to your CDN host (but without sacrificing functionality). More and more sites are seeing that optimizing performance improves conversion rates and user engagement, and it can also make apps simpler to maintain.
This is just the first release of `elm-pages`, but I've built a prototype for pulling in external data and am refining the design in preparation for the next release. Once that ships, the use cases `elm-pages` can handle will expand to things like ecommerce sites, job boards, and sites with content written by non-technical content editors. You can find a very informative FAQ and resources page about these ideas at [jamstack.org](https://jamstack.org/) (plus a more in-depth definition of the term JAMstack).
## Comparing `elm-pages` and `elmstatic`
`elm-pages` and [`elmstatic`](https://korban.net/elm/elmstatic/) have a lot of differences. At the core, they have two different goals. `elmstatic` generates HTML for you that doesn't include an Elm runtime. It uses Elm as a templating engine to do page layouts. It also makes some assumptions about the structure of your page content, separating `posts` and `pages` and automatically generating post indexes based on the top-level directories within the `posts` folder. It's heavily inspired by traditional static site generators like Jekyll.
`elm-pages` hydrates into a single-page app that includes a full Elm runtime, meaning that you can have whatever client-side interactivity you want. It supports similar use cases to static site generators like [Gatsby](http://gatsbyjs.org). `elm-pages` makes a lot of optimizations by splitting and lazy-loading pages, optimizing image assets, and using service workers for repeat visits. It pre-renders HTML for fast first renders, but because it ships with JavaScript code it is also able to do some performance optimizations to make page changes faster (and without page flashes). So keep in mind that shipping without JavaScript doesn't necessarily mean your site performance suffers! You may have good reasons to want a static site with no JavaScript, but open up a Lighthouse audit and try it out for yourself rather than speculating about performance!
Either framework might be the right fit depending on your goals. I hope this helps illuminate the differences!
## How does `elm-pages` work?
The flow is something like this:
- Put your static content in your `content` folder (it could be Markdown, `elm-markup`, or something else entirely)
- Register Elm functions that define what to do with the [frontmatter](https://jekyllrb.com/docs/front-matter/) (that YAML data at the top of your markup files) and the body of each type of file you want to handle
- Define your app's configuration in pure Elm (just like a regular Elm `Browser.application` but with a few extra functions for SEO and site configuration)
- Run `elm-pages build` and ship your static files (JS, HTML, etc.) to Netlify, Github Pages, or your CDN of choice!
The result is a blazing fast static site that is optimized both for the first load experience, and also uses some caching strategies to improve site performance for repeat visitors. You can look in your dev tools or run a Lighthouse audit on this page to see some of the performance optimizations `elm-pages` does for you!
The way you set up an `elm-pages` app will look familiar if you have some experience with wiring up standard Elm boilerplate:
```elm
main : Pages.Platform.Program Model Msg Metadata (List (Element Msg))
main =
Pages.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, documents = [ markdownHandler ]
, head = head
, manifest = manifest
, canonicalSiteUrl = "https://elm-pages.com"
}
```
You can take a look at [the `Main.elm` file for this site](https://github.com/dillonkearns/elm-pages/blob/master/examples/docs/src/Main.elm#L52) to get a better sense of the bigger picture. I'll do a more in-depth explanation of this setup in a future post. The short version is that
`init`, `update`, and `subscriptions` are as you would expect (but maybe a bit simpler since `elm-pages` manages things like the URL for you).
`documents` are where you define how to handle the frontmatter and body of the files in your `content` folder. And the `view` function gives you the result from your frontmatter and body, as well as your `Model`.
`head` is just a function that passes you the metadata for a given page and lets you define tags to put in the `<head>` (mostly for SEO).
`manifest` lets you configure some settings that allow your app to be installed for offline use.
And the end result is that `elm-pages` gets everything it needs about your site in order to optimize it and turn it into a modern, performant site that will get a great Lighthouse audit score! The goal is to make following best practices for a modern, performant static site one of the following:
- Built-in
- Enforced by the Elm compiler
- Or at the very least the path of least resistence
## What makes Elm awesome for building static sites
JAMstack frameworks, like [Gatsby](http://gatsbyjs.org), can make powerful optimizations because they are dealing with strong constraints (specifically, content that is known at build time). Elm is the perfect tool for the JAMstack because it can leverage those constraints and turn them into compiler guarantees. Not only can we do more with static guarantees using Elm, but we can get additional guarantees using Elm's type-system and managed side-effects. It's a virtuous cycle that enables a lot of innovation.
## Why use `elm-pages`?
Let's take a look at a few of the features that make `elm-pages` worthwhile for the users (both the end users, and the team using it to build their site).
### Performance
- Pre-rendered pages for blazing fast first renders and improved SEO
- Your content is loaded as a single-page app behind the scenes, giving you smooth page changes
- Split individual page content and lazy load each page
- Prefetch page content on link hover so page changes are almost instant
- Image assets are optimized
- App skeleton is cached with a service worker (with zero configuration) so it's available offline
One of the early beta sites that used `elm-pages` instantly shaved off over a megabyte for the images on a single page! Optimizations like that need to be built-in and automatic otherwise some things inevitably slip through the cracks.
### Type-safety and simplicity
- The type system guarantees that you use valid images and routes in the right places
- You can even set up a validation to give build errors if there are any broken links or images in your markdown
- You can set up validations to define your own custom rules for your domain! (Maximum title length, tag name from a set to avoid multiple tags with different wording, etc.)
## Progressive Web Apps
[Lighthouse recommends having a Web Manifest file](https://developers.google.com/web/tools/lighthouse/audits/manifest-exists) for your app to allow users to install the app to your home screen and have an appropriate icon, app name, etc.
Elm pages gives you a type-safe way to define a web manifest for your app:
```elm
manifest : Manifest.Config PagesNew.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.icon
}
```
Lighthouse will also ding you [if you don't have the appropriately sized icons and favicon images](https://developers.google.com/web/tools/lighthouse/audits/manifest-contains-192px-icon). `elm-pages` guarantees that you will follow these best practices (and gives you the confidence that you haven't made any mistakes). It will automatically generate the recommended set of icons and favicons for you, based on a single source image. And, of course, you get a compile-time guarantee that you are using an image that exists! For example, here's what happens if we try to access an image as `logo` when the actual file is called `icon`.
```haskell
sourceIcon = images.logo
```
We then get this elm compiler error:
![Missing image compiler error](/images/compiler-error.png)
## `elm-pages` is just Elm!
`elm-pages` hydrates into a full-fledged Elm app (the pre-rendered pages are just for faster loads and better SEO). So you can do whatever you need to using Elm and the typed data that `elm-pages` provides you with. In a future post, I'll explain some of the ways that `elm-pages` leverages the Elm type system for a better developer experience. There's a lot to explore here, this really just scratches the surface!
## SEO
One of the main motivations for building `elm-pages` was to make SEO easier and less error-prone. Have you ever seen a link shared on Twitter or elsewhere online that just renders like a plain link? No image, no title, no description. As a user, I'm a little afraid to click those links because I don't have any clues about where it will take me. As a user posting those links, it's very anticlimactic to share the blog post that I lovingly wrote only to see a boring link there in my tweet sharing it with the world.
I'll also be digging into the topic of SEO in a future post, showing how `elm-pages` makes SEO dead simple. For now, you can take a look at [the built-in `elm-pages` SEO module](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/Head-Seo) or take a look at [how this site uses the SEO module](https://github.com/dillonkearns/elm-pages/blob/8448bb60b680fb171319988fb716cb21e0345826/examples/docs/src/Main.elm#L294-L400).
## Next steps
There are so many possibilities when you pair Elm with static content! I'm excited to explore this area further with the help of the community. Here are some features that are on my radar.
- Allow users to pass a set of HTTP requests to fetch during the build step (for making CMS or API data available statically in the build)
- An API to programmatically add pages from metadata (rather than just from files in the `content` folder)
- Allow users to configure the caching strategy for service workers (through pure Elm config of course)
- More SEO features (possibly an API for adding structured data, i.e. JSON-LD, for more interactive and engaging search results)
And of course, responding to your feedback! Please don't hesitate to share your thoughts, on everything from the documentation to the developer experience. I'd love to hear from you!
## Getting started with `elm-pages`
If you'd like to try out `elm-pages` for yourself, or look at some code, the best place to start is the [`elm-pages-starter` repo](https://github.com/dillonkearns/elm-pages-starter). See the site live at [elm-pages-starter.netlify.com](https://elm-pages-starter.netlify.com). Let me know your thoughts on Slack, I'd love to hear from you! Or continue the conversation on Twitter!
<Oembed url="https://twitter.com/dillontkearns/status/1176556756249432065" />

View File

@ -0,0 +1,205 @@
---
{
"author": "Dillon Kearns",
"title": "A is for API - Introducing Static HTTP Requests",
"description": "The new StaticHttp API lets you fetch data when your site is built. That lets you remove loading spinners, and even access environment variables.",
"image": "v1603304397/elm-pages/article-covers/static-http_ohefua.jpg",
"published": "2019-12-10",
}
---
I'm excited to announce a new feature that brings `elm-pages` solidly into the JAMstack: Static HTTP requests. JAMstack stands for JavaScript, APIs, and Markup. And Static HTTP is all about pulling API data into your `elm-pages` site.
If youve tried `elm-pages`, you may be thinking, "elm-pages hydrates into a full Elm app... so couldnt you already make HTTP requests to fetch API data, like you would in any Elm app?" Very astute observation! You absolutely could.
So what's new? It all comes down to these key points:
- Less boilerplate
- Improved reliability
- Better performance
Let's dive into these points in more detail.
## Less boilerplate
Let's break down how you perform HTTP requests in vanilla Elm, and compare that to how you perform a Static HTTP request with `elm-pages`.
### Anatomy of HTTP Requests in Vanilla Elm
- Cmd for an HTTP request on init (or update)
- You receive a `Msg` in `update` with the payload
- Store the data in `Model`
- Tell Elm how to handle `Http.Error`s (including JSON decoding failures)
### Anatomy of Static HTTP Requests in `elm-pages`
- `view` function specifies some `StaticHttp` data, and a function to turn that data into your `view` and `head` tags for that page
That's actually all of the boilerplate for `StaticHttp` requests!
There is a lifecycle, because things can still fail. But the entire Static HTTP lifecycle happens _before your users have even requested a page_. The requests are performed at build-time, and that means less boilerplate for you to maintain in your Elm code!
### Let's see some code!
Here's a code snippet for making a StaticHttp request. This code makes an HTTP request to the Github API to grab the current number of stars for the `elm-pages` repo.
```elm
import Pages.StaticHttp as StaticHttp
import Pages
import Head
import Secrets
import Json.Decode.Exploration as Decode
view :
{ path : PagePath Pages.PathKey
, frontmatter : Metadata
}
->
StaticHttp.Request
{ view : Model ->
View -> { title : String, body : Html Msg }
, head : List (Head.Tag Pages.PathKey)
}
view page =
(StaticHttp.get
(Secrets.succeed
"https://api.github.com/repos/dillonkearns/elm-pages")
(Decode.field "stargazers_count" Decode.int)
)
|> StaticHttp.map
(\starCount ->
{ view =
\model renderedMarkdown ->
{ title = "Landing Page"
, body =
[ header starCount
, pageView model renderedMarkdown
]
}
, head = head starCount
}
)
head : Int -> List (Head.Tag Pages.PathKey)
head starCount =
Seo.summaryLarge
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages - "
++ String.fromInt starCount
++ " GitHub Stars"
, image =
{ url = images.iconPng
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = siteTagline
, locale = Nothing
, title = "External Data Example"
}
|> Seo.website
```
The data is baked into our built code, which means that the star count will only update when we trigger a new build. This is a common JAMstack technique. Many sites will trigger builds periodically to refresh data. Or better yet, use a webhook to trigger new builds whenever new data is available (for example, if you add a new blog post or a new page using a service like Contentful).
Notice that this app's `Msg`, `Model`, and `update` function are not involved in the process at all! It's also worth noting that we are passing that data into our `head` function, which allows us to use it in our `<meta>` tags for the page.
The `StaticHttp` functions are very similar to Elm libraries
you've likely used already, such as `elm/json` or `elm/random`.
If you don't depend on any StaticHttp data, you use `StaticHttp.succeed`,
similar to how you might use `Json.Decode.succeed`, `Random.constant`,
etc.
```elm
import Pages.StaticHttp as StaticHttp
StaticHttp.succeed
{ view =
\model renderedMarkdown ->
{ title = "Landing Page"
, body =
[ header
, pageView model renderedMarkdown
]
}
, head = head
}
```
This is actually the same as our previous example that had a `StaticHttp.request`, except that it doesn't make a request or have the
stargazer count data.
### Secure Secrets
A common pattern is to use environment variables in your local environment or your CI environment in order to securely manage
auth tokens and other secure data. `elm-pages` provides an API for accessing this data directly from your environment variables.
You don't need to wire through any flags or ports, simply use the [`Pages.Secrets` module (see the docs for more info)](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/Pages-Secrets). It will take care of masking the secret data for you
so that it won't be accessible in the bundled assets (it's just used to perform the requests during the build step, and then
it's masked in the production assets).
### The Static HTTP Lifecycle
If you have a bad auth token in your URL, or your JSON decoder fails, then that code will never run for your `elm-pages` site. Instead, you'll get a friendly `elm-pages` build-time error telling you exactly what the problem was and where it occurred (as you're familiar with in Elm).
![StaticHttp build error](/images/static-http-error.png)
These error messages are inspired by Elm's famously helpful errors. They're designed to point you in the right direction, and provide as much context as possible.
Which brings us to our next key point...
## Improved reliability
Static HTTP requests are performed at build-time. Which means that if you have a problem with one of your Static HTTP requests, _your users will never see it_. Even if a JSON decoder fails, `elm-pages` will report back the decode failure and wait until its fixed before it allows you to create your production build.
Your API might go down, but your Static HTTP requests will always be up (assuming your site is up). The responses from your Static requests are baked into the static files for your `elm-pages` build. If there is an API outage, you of course won't be able to rebuild your site with fresh data from that API. But you can be confident that, though your build may break, your site will always have a working set of Static HTTP data.
Compare this to an HTTP request in a vanilla Elm app. Elm can guarantee that you've handled all error cases. But you still need to handle the case where you have a bad HTTP response, or a JSON decoder fails. That's the best that Elm can do because it can't guarantee anything about the data you'll receive at runtime. But `elm-pages` _can_ make guarantees about the data you'll receive! Because it introduces a new concept of data that you get a snapshot of during your build step. `elm-pages` guarantees that this frozen moment of time has no problems before the build succeeds, so we can make even stronger guarantees than we can with plain Elm.
## Better performance
The StaticHttp API also comes with some significant performance boosts. StaticHttp data is just a static JSON file for each page in your `elm-pages` site. That means that:
- No waiting on database queries to fetch API data
- Your site, including API responses, is just static files so it can be served through a blazing-fast CDN (which serves files from the nearest server in the user's region)
- Scaling is cheap and doesn't require an Ops team
- `elm-pages` intelligently prefetches the Static HTTP data for a page when you're likely to navigate to that page, so page loads are instant and there's no spinner waiting to load that initial data
- `elm-pages` optimizes your `StaticHttp` JSON data, stripping out everything but what you use in your JSON decoder
### JSON Optimization
The JSON optimization is made possible by a JSON parsing library created by Ilias Van Peer. Here's the pull request where he introduced the JSON optimization functionality: [github.com/zwilias/json-decode-exploration/pull/9](https://github.com/zwilias/json-decode-exploration/pull/9).
Let's take our Github API request as an example. Our Github API request from our previous code snippet ([https://api.github.com/repos/dillonkearns/elm-pages](https://api.github.com/repos/dillonkearns/elm-pages)) has a payload of 5.6KB (2.4KB gzipped). That size of the optimized JSON drops down to about 3% of that.
You can inspect the network tab on this page and you'll see something like this:
![StaticHttp content request](/images/static-http-content-requests.png)
If you click on Github API link above and compare it, you'll see that it's quite a bit smaller! It just has the one field that we grabbed in our JSON decoder.
This is quite nice for privacy and security purposes as well because any personally identifying information that might be included in an API response you consume won't show up in your production bundle (unless you were to explicitly include it in a JSON decoder).
### Comparing StaticHttp to other JAMstack data source strategies
You may be familiar with frameworks like Gatsby or Gridsome which also allow you to build data from external sources into your static site. Those frameworks, however, use a completely different approach, [using a GraphQL layer to store data from those data sources](https://www.gatsbyjs.org/docs/page-query/), and then looking that data up in GraphQL queries from within your static pages.
This approach makes sense for those frameworks. But since `elm-pages` is built on top of a language that already has an excellent type system, I wanted to remove that additional layer of abstraction and provide a simpler way to consume static data. The fact that Elm functions are all deterministic (i.e. given the same inputs they will always have the same outputs) opens up exciting new approaches to these problems as well. One of Gatsby's stated reasons for encouraging the use of their GraphQL layer is that it allows you to have your data all in one place. But the `elm-pages` StaticHttp API gives you similar benefits, using familiar Elm techniques like `map`, `andThen`, etc to massage your data into the desired format.
## Future plans
I'm looking forward to exploring more possibilities for using static data in `elm-pages`. Some things I plan to explore are:
- Programatically creating pages using the Static HTTP API
- Configurable image optimization (including producing multiple dimensions for `srcset`s) using a similar API
- Optimizing the page metadata that is included for each page (i.e. code splitting) by explicitly specifying what metadata the page depends on using an API similar to StaticHttp
## Getting started with StaticHttp
You can [take a look at this an end-to-end example app that uses the new `StaticHttp` library](https://github.com/dillonkearns/elm-pages/blob/master/examples/external-data/src/Main.elm) to get started.
Or just use the [`elm-pages-starter` repo](https://github.com/dillonkearns/elm-pages-starter) and start building something cool! Let me know your thoughts on Slack, I'd love to hear from you! Or continue the conversation on Twitter!
<Oembed url="https://twitter.com/dillontkearns/status/1214238507163471872" />

View File

@ -0,0 +1,169 @@
---
{
"author": "Dillon Kearns",
"title": "Types Over Conventions",
"description": "How elm-pages approaches configuration, using type-safe Elm.",
"image": "v1603304397/elm-pages/article-covers/introducing-elm-pages_ceksg2.jpg",
"draft": true,
"published": "2019-09-21",
}
---
Rails started a movement of simplifying project setup with [a philosophy of "Convention Over Configuration"](https://rubyonrails.org/doctrine/#convention-over-configuration). This made for a very easy experience bootstrapping a new web server. The downside is that you have a lot of implicit rules that can be hard to follow.
`elm-pages` takes a different approach. Rather than implicit conventions, or verbose configuration, `elm-pages` is centered around letting you explicitly configure your project using Elm's type system. This makes it a lot easier to configure because the Elm compiler will give you feedback on what the valid options are. And it also gives you the ability to define your own defaults and conventions explicitly, giving you the confidence getting started that Rails gives you, but the explicitness and helpful compiler support we're accustomed to in Elm.
**Note:** `elm-pages` currently relies on a few basic conventions such as the name of the `content` folder which has your markup. Convention over configuration isn't evil. It just has a set of tradeoffs, like any other design. `elm-pages` shares the Elm philosophy's idea that ["There are worse things than being explicit"](https://twitter.com/czaplic/status/928359289135046656). In other words, implicit rules that are hard to trace is more likely to cause maintainability issues than a little extra typing to explicitly lay out some core rules. (As long as that extra typing is nice, type-safe Elm code!)
Consider how `elm-pages` handles choosing a template for your pages. Many static site generators use [a special framework-provided frontmatter directive](https://jekyllrb.com/docs/front-matter/#predefined-global-variables) that determines which layout to use. And a special file naming convention will be used as the fallback for the default layout if you don't specify a layout in the frontmatter.
With `elm-pages`, there are no magic frontmatter directives. The way you define and handle your metadata is completely up to you. `elm-pages` simply hands you the metadata types you define and allows you to choose how to handle them with the Elm compiler there to support you.
## Let's see the code!
If we wanted to define a particular layout for blog posts, and a different layout for podcast episodes, then it's as simple as defining a JSON decoder for the data in our frontmatter.
So here's the frontmatter for a blog post:
```markdown
---
author: dillon
title: Types Over Conventions
published: 2019-09-21
---
```
And here's the frontmatter for a regular page:
```markdown
---
title: About elm-pages
---
```
As far as `elm-pages` is concerned, this is just data. We define the rules for what to do with those different data types in our code.
```elm
import Author
import Json.Decode
type Metadata
= Page { title : String }
| BlogPost { author : String, title : String }
document =
Pages.Document.parser
{ extension = "md"
, metadata =
Json.Decode.oneOf
[
Json.Decode.map
(\title -> Page { title = title })
(Json.Decode.field "title" Json.Decode.string)
, Json.Decode.map2
(\author title ->
BlogPost { author = author, title = title }
)
(Json.Decode.field "author" Author.decoder)
(Json.Decode.field "title" Json.Decode.string)
]
, body = markdownView
}
markdownView : String -> Result String (List (Html Msg))
markdownView markdownBody =
MarkdownRenderer.view markdownBody
```
Each file in the `content` folder will result in a new route for your static site. You can define how to render the types of document in the `content` folder based on the extension any way you like.
Now, in our `elm-pages` app, our `view` function will get the markdown that we rendered for a given page along with the corresponding `Metadata`. It's completely in our hands what we want to do with that data.
Rails started a movement of simplifying project setup with [a philosophy of "Convention Over Configuration"](https://rubyonrails.org/doctrine/#convention-over-configuration). This made for a very easy experience bootstrapping a new web server. The downside is that you have a lot of implicit rules that can be hard to follow.
`elm-pages` gives you the best of both worlds. Rather than implicit conventions, or verbose configuration, `elm-pages` is centered around letting you explicitly configure your project using Elm's type system. This makes it a lot easier to configure because the Elm compiler will give you feedback on what the valid options are. And it also gives you the ability to define your own defaults and conventions explicitly, giving you the simplicity of the Rails philosophy, but the explicitness and helpful compiler support we're accustomed to in Elm.
**Note:** `elm-pages` currently relies on a few basic conventions such as the name of the `content` folder which has your markup. Convention over configuration isn't evil. It just has a set of tradeoffs, like any other design. `elm-pages` shares the Elm philosophy's idea that ["There are worse things than being explicit"](https://twitter.com/czaplic/status/928359289135046656). In other words, implicit rules that are hard to trace is more likely to cause maintainability issues than a little extra typing to explicitly lay out some core rules. As long as that extra typing is nice, type-safe Elm code!
<Oembed url="https://twitter.com/czaplic/status/928359289135046656" />
Consider how `elm-pages` handles choosing a template for your pages. Many static site generators use [a special framework-provided frontmatter directive](https://jekyllrb.com/docs/front-matter/#predefined-global-variables) that determines which layout to use. And a special file naming convention will be used as the fallback for the default layout if you don't specify a layout in the frontmatter.
With `elm-pages`, there are no magic frontmatter directives. The way you define and handle your metadata is completely up to you. `elm-pages` simply hands you the metadata types you define and allows you to choose how to handle them with the Elm compiler there to support you.
## Let's see the code!
If we wanted to define a particular layout for blog posts, and a different layout for regular pages, then it's as simple as defining a JSON decoder for the data in our frontmatter.
So here's the frontmatter for a blog post:
```markdown
---
author: dillon
title: Types Over Conventions
published: 2019-09-21
---
```
And here's the frontmatter for a regular page:
```markdown
---
title: About elm-pages
---
```
As far as `elm-pages` is concerned, this is just data. We define the rules for what to do with those different data types in our code.
Here's how we set up a parser to handle the frontmatter and body of our `.md` files in our `content` folder.
The raw frontmatter can be a variety of formats, including YAML, TOML, and JSON. But in our Elm code,
we turn that data into the data representing our app's metadata using a `Json.Decoder`.
```elm
import Author
-- Author is our custom module that looks
-- up author data from their first name
import Json.Decode
type Metadata
= Page { title : String }
| BlogPost { author : String, title : String }
document =
Pages.Document.parser
{ extension = "md"
, metadata =
Json.Decode.oneOf
[ Json.Decode.map
(\title ->
Page { title = title }
)
(Json.Decode.field "title" Json.Decode.string)
, Json.Decode.map2
(\author title ->
BlogPost { author = author, title = title }
)
(Json.Decode.field "author" Author.decoder)
(Json.Decode.field "title" Json.Decode.string)
]
, body = markdownView
}
markdownView : String -> Result String (List (Html Msg))
markdownView markdownBody =
MarkdownRenderer.view markdownBody
```
Each file in the `content` folder will result in a new route for your static site. You can define how to render the types of document in the `content` folder based on the extension any way you like.
Now, in our `elm-pages` app, our `view` function will get the markdown that we rendered for a given page along with the corresponding `Metadata`. It's completely in our hands what we want to do with that data.
## Takeaways
So which is better, configuration through types or configuration by convention?
They both have their benefits. If you're like me, then you enjoy being able to figure out what your Elm code is doing by just following the types. And I hope you'll agree that `elm-pages` gives you that experience for wiring up your content and your parsers.
And when you need to do something more advanced, you've got all the typed data right there and you're empowered to solve the problem using Elm!

31150
examples/slides/browser-elm.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,7 @@
---
title: Core Concepts
type: doc
---
## StaticHttp
Gives you a way to pull in data during the build step. This data changes every time you run a build. You won't see a loading spinner or error with this data in your built production site. You might get a build error that you can fix.

View File

@ -0,0 +1,150 @@
---
title: Directory Structure
type: doc
---
## Philosophy
As a general rule, `elm-pages` strives to be unopinionated about how you organize
your files (both code and content).
```shell
.
├── content/
├── elm.json
├── images/
├── static/
├── index.js
├── package.json
└── src/
└── Template/
├── Bio.elm # user-defined template modules
└── Catalog.elm
└── Main.elm
```
## `content` folder
Each file in the `content` folder will result in a new route for your static site. You can define how to render the types of document in the `content` folder based on the extension any way you like.
```elm
helloDocument : Pages.Document.DocumentParser Metadata (List (Html Msg))
helloDocument =
Pages.Document.parser
{ extension = "txt"
, metadata =
-- pages will use the layout for Docs if they have
-- `type: doc` in their markdown frontmatter
Json.Decode.map2
(\title maybeType ->
case maybeType of
Just "doc" ->
Metadata.Doc { title = title }
_ ->
Metadata.Page { title = title }
)
(Json.Decode.field "title" Json.Decode.string)
(Json.Decode.field "type" Json.Decode.string
|> Json.Decode.maybe
)
, body = MarkdownRenderer.view
}
```
```elm
markdownDocument : Pages.Document.DocumentParser Metadata (List (Element Msg))
markdownDocument =
Pages.Document.parser
{ extension = "md"
, metadata =
Json.Decode.map2
(\title maybeType ->
case maybeType of
Just "doc" ->
Metadata.Doc { title = title }
_ ->
Metadata.Page { title = title }
)
(Json.Decode.field "title" Json.Decode.string)
(Json.Decode.field "type" Json.Decode.string
|> Json.Decode.maybe
)
, body = MarkdownRenderer.view
}
```
## Modules
### Templates
`src/Template/*.elm`
A template represents a type of page. For example, a BlogPost template could live in `src/Template/BlogPost.elm`. Any files in your `content/` folder with frontmatter that you decode into type `TemplateType.BlogPost` will be rendered using your `BlogPost` template.
Think of each template as having its own mini `elm-pages architecture` lifecycle.
Imagine you have a site called thegreatcomposers.com that lists the greatest works of Classical composers.
Let's say you have a file called `content/catalog/sibelius.md` with these contents:
```markdown
---
template: catalog
composer: Sibelius
---
## Symphony 2, Op. 47
### Notable Recordings
Bernstein Vienna Philharmonic
```
You have a metadata decoder like this:
```elm
module Metadata exposing (Metadata, decoder)
type Metadata = Catalog Composer | Bio Composer
type Composer = Sibelius | Mozart
decoder =
Decode.string
|> Decode.field "template"
|> Decode.andThen (\template ->
case template of
"catalog" -> Decode.map Catalog decodeComposer
"bio" -> Decode.map Bio decodeComposer
)
```
Now say you navigate to `/catalog/sibelius`. Let's look at the `elm-pages architecture` lifecycle steps that kick in.
### Build
* `staticData` - When you build your site (using `elm-pages build` for prod or `elm-pages develop` in dev mode), the `staticData` will be fetched for this page. Your `staticData` request has access to the page's `Metadata`. So if you wanted to request `api.composers.com/portrait-images/<composer-name>` to get the list of images for each composer's catalog page, you could. Behind the scenes, `elm-pages` will make sure this data is loaded for you in the browser so you have access to this data, even though the API is only hit during the initial build and then stored as a JSON asset for your site.
#### Page Load
* `init` - the page for Sibelius' catalog has its own state. Let's display a Carousel that shows photos of the composer. `init` is called when you navigate to this page. If you navigate to another composer's catalog page, like Mozart, it will call the same `init` function to get a fresh Model for the new page, passing in the metadata for the Mozart page (from the frontmatter in `content/catalog/mozart`.
* `view` given the page's state, metadata, and StaticHttp data, you can render the catalog for Sibelius.
#### Page Interaction
* `update` - if you click the Carousel, the page's state gets updated.
### Shared
`src/Shared.elm`
* `staticData` (loaded per-app, not per-page)
* `View` - the data type that pages render to in your app
* `view` - the top-level view function for your app
### Build
`src/Build.elm`
* `staticData` (build-only)
* `manifest`
* `generateFiles`
### Global Metadata
`src/TemplateType.elm`
This module must define a variant for each template module.

View File

@ -0,0 +1,19 @@
---
title: Quick Start
type: doc
---
## Installing
The easiest way to get set up is to use the starter template. Just go to the [`elm-pages-starter` repo](https://github.com/dillonkearns/elm-pages-starter) and click "Use this template" to fork the repo.
Or clone down the repo:
```
git clone git@github.com:dillonkearns/elm-pages-starter.git
cd elm-pages-starter
npm install
npm start # starts a local dev server using `elm-pages develop`
```
From there, start editing the posts in the `content` folder. You can change the types of content in `src/Metadata.elm`, or render your content using a different renderer (the template uses `elm-explorations/markdown`) by changing [the configuring the document handlers](https://github.com/dillonkearns/elm-pages-starter/blob/2c2241c177cf8e0144af4a8afec0115f93169ac5/src/Main.elm#L70-L80).

View File

@ -0,0 +1,8 @@
{
"tools": {
"elm": "0.19.1",
"elm-format": "0.8.4",
"elm-json": "0.2.10",
"elm-test-rs": "1.0.0"
}
}

64
examples/slides/elm.json Normal file
View File

@ -0,0 +1,64 @@
{
"type": "application",
"source-directories": [
"src",
"../../src",
"gen",
"../../plugins"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"ThinkAlexandria/elm-html-in-elm": "1.0.1",
"avh4/elm-color": "1.0.0",
"billstclair/elm-xml-eeue56": "1.0.1",
"danyx23/elm-mimetype": "4.0.1",
"dillonkearns/elm-markdown": "4.0.2",
"dillonkearns/elm-oembed": "1.0.0",
"dillonkearns/elm-rss": "1.0.1",
"dillonkearns/elm-sitemap": "1.0.1",
"dmy/elm-imf-date-time": "1.0.1",
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"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/time": "1.0.0",
"elm/url": "1.0.0",
"elm/virtual-dom": "1.0.2",
"elm-community/dict-extra": "2.4.0",
"elm-community/list-extra": "8.3.0",
"elm-community/result-extra": "2.4.0",
"elm-community/string-extra": "4.0.1",
"elm-explorations/markdown": "1.0.0",
"justinmimbs/date": "3.2.0",
"lukewestby/elm-string-interpolate": "1.0.4",
"mdgriffith/elm-ui": "1.1.8",
"miniBill/elm-codec": "1.2.0",
"noahzgordon/elm-color-extra": "1.0.2",
"pablohirafuji/elm-syntax-highlight": "3.3.0",
"rtfeldman/elm-hex": "1.0.0",
"tripokey/elm-fuzzy": "5.2.1",
"zwilias/json-decode-exploration": "6.0.0"
},
"indirect": {
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/random": "1.0.0",
"elm/regex": "1.0.0",
"fredcy/elm-parseint": "2.0.1",
"justinmimbs/time-extra": "1.1.0",
"lazamar/dict-parser": "1.0.2",
"mgold/elm-nonempty-list": "4.1.0",
"ryannhg/date-format": "2.3.0"
}
},
"test-dependencies": {
"direct": {
"elm-explorations/test": "1.2.2"
},
"indirect": {}
}
}

View File

@ -0,0 +1,8 @@
module Pages exposing (builtAt)
import Time
builtAt : Time.Posix
builtAt =
Time.millisToPosix 1618410729151

View File

@ -0,0 +1,29 @@
module Route exposing (..)
import Url
import Url.Parser as Parser exposing ((</>), Parser)
type Route
= Slide__Number_ { number : String }
urlToRoute : Url.Url -> Maybe Route
urlToRoute url =
Parser.parse (Parser.oneOf routes) url
routes : List (Parser (Route -> a) a)
routes =
[ Parser.map (\number -> Slide__Number_ { number = number }) (Parser.s "slide" </> Parser.string)
]
routeToPath : Maybe Route -> List String
routeToPath maybeRoute =
case maybeRoute of
Nothing ->
[]
Just (Slide__Number_ params) ->
[ "slide", params.number ]

View File

@ -0,0 +1,206 @@
module Template exposing
( Builder(..)
, StaticPayload
, withStaticData, noStaticData
, Template, buildNoState
, TemplateWithState, buildWithLocalState, buildWithSharedState
)
{-|
## Building a Template
@docs Builder
## Static Data
Every template will have access to a `StaticPayload`.
@docs StaticPayload
Since this data is _static_, you have access to it before the user has loaded the page, including at build time.
An example of dynamic data would be keyboard input from the user, query params, or any other data that comes from the app running in the browser.
But before the user even requests the page, we have the following data:
- `path` - these paths are static. In other words, we know every single path when we build an elm-pages site.
- `metadata` - we have a decoded Elm value for the page's metadata.
- `sharedStatic` - we can access any shared data between pages. For example, you may have fetched the name of a blog ("Jane's Blog") from the API for a Content Management System (CMS).
- `static` - this is the static data for this specific page. If you use `noStaticData`, then this will be `()`, meaning there is no page-specific static data.
@docs withStaticData, noStaticData
## Stateless Templates
@docs Template, buildNoState
## Stateful Templates
@docs TemplateWithState, buildWithLocalState, buildWithSharedState
-}
import Document exposing (Document)
import Head
import Pages.PagePath exposing (PagePath)
import Pages.StaticHttp as StaticHttp
import Shared
{-| -}
type alias TemplateWithState routeParams templateStaticData templateModel templateMsg =
{ staticData : routeParams -> StaticHttp.Request templateStaticData
, staticRoutes : StaticHttp.Request (List routeParams)
, view :
templateModel
-> Shared.Model
-> StaticPayload templateStaticData routeParams
-> Document templateMsg
, head :
StaticPayload templateStaticData routeParams
-> List Head.Tag
, init : routeParams -> ( templateModel, Cmd templateMsg )
, update : routeParams -> templateMsg -> templateModel -> Shared.Model -> ( templateModel, Cmd templateMsg, Maybe Shared.SharedMsg )
, subscriptions : routeParams -> PagePath -> templateModel -> Shared.Model -> Sub templateMsg
}
{-| -}
type alias Template routeParams staticData =
TemplateWithState routeParams staticData () Never
{-| -}
type alias StaticPayload staticData routeParams =
{ static : staticData -- local
, sharedStatic : Shared.StaticData -- share
, routeParams : routeParams
, path : PagePath
}
{-| -}
type Builder routeParams templateStaticData
= WithStaticData
{ staticData : routeParams -> StaticHttp.Request templateStaticData
, staticRoutes : StaticHttp.Request (List routeParams)
, head :
StaticPayload templateStaticData routeParams
-> List Head.Tag
}
{-| -}
buildNoState :
{ view :
StaticPayload templateStaticData routeParams
-> Document Never
}
-> Builder routeParams templateStaticData
-> TemplateWithState routeParams templateStaticData () Never
buildNoState { view } builderState =
case builderState of
WithStaticData record ->
{ view = \() _ -> view
, head = record.head
, staticData = record.staticData
, staticRoutes = record.staticRoutes
, init = \_ -> ( (), Cmd.none )
, update = \_ _ _ _ -> ( (), Cmd.none, Nothing )
, subscriptions = \_ _ _ _ -> Sub.none
}
{-| -}
buildWithLocalState :
{ view :
templateModel
-> Shared.Model
-> StaticPayload templateStaticData routeParams
-> Document templateMsg
, init : routeParams -> ( templateModel, Cmd templateMsg )
, update : Shared.Model -> routeParams -> templateMsg -> templateModel -> ( templateModel, Cmd templateMsg )
, subscriptions : routeParams -> PagePath -> templateModel -> Sub templateMsg
}
-> Builder routeParams templateStaticData
-> TemplateWithState routeParams templateStaticData templateModel templateMsg
buildWithLocalState config builderState =
case builderState of
WithStaticData record ->
{ view =
\model sharedModel staticPayload ->
config.view model sharedModel staticPayload
, head = record.head
, staticData = record.staticData
, staticRoutes = record.staticRoutes
, init = config.init
, update =
\metadata msg templateModel sharedModel ->
let
( updatedModel, cmd ) =
config.update sharedModel metadata msg templateModel
in
( updatedModel, cmd, Nothing )
, subscriptions =
\routeParams path templateModel sharedModel ->
config.subscriptions routeParams path templateModel
}
{-| -}
buildWithSharedState :
{ view :
templateModel
-> Shared.Model
-> StaticPayload templateStaticData routeParams
-> Document templateMsg
, init : routeParams -> ( templateModel, Cmd templateMsg )
, update : routeParams -> templateMsg -> templateModel -> Shared.Model -> ( templateModel, Cmd templateMsg, Maybe Shared.SharedMsg )
, subscriptions : routeParams -> PagePath -> templateModel -> Shared.Model -> Sub templateMsg
}
-> Builder routeParams templateStaticData
-> TemplateWithState routeParams templateStaticData templateModel templateMsg
buildWithSharedState config builderState =
case builderState of
WithStaticData record ->
{ view = config.view
, head = record.head
, staticData = record.staticData
, staticRoutes = record.staticRoutes
, init = config.init
, update = config.update
, subscriptions = config.subscriptions
}
{-| -}
withStaticData :
{ staticData : routeParams -> StaticHttp.Request templateStaticData
, staticRoutes : StaticHttp.Request (List routeParams)
, head : StaticPayload templateStaticData routeParams -> List Head.Tag
}
-> Builder routeParams templateStaticData
withStaticData { staticData, head, staticRoutes } =
WithStaticData
{ staticData = staticData
, staticRoutes = staticRoutes
, head = head
}
{-| -}
noStaticData :
{ head : StaticPayload () routeParams -> List Head.Tag
, staticRoutes : StaticHttp.Request (List routeParams)
}
-> Builder routeParams ()
noStaticData { head, staticRoutes } =
WithStaticData
{ staticData = \_ -> StaticHttp.succeed ()
, staticRoutes = staticRoutes
, head = head
}

View File

@ -0,0 +1,337 @@
port module TemplateModulesBeta exposing (..)
import Browser
import Route exposing (Route)
import Document
import Json.Decode
import Json.Encode
import Pages.Internal.Platform
import Pages.Internal.Platform.ToJsPayload
import Pages.Manifest as Manifest
import Shared
import Site
import Head
import Html exposing (Html)
import Pages.PagePath exposing (PagePath)
import Url
import Url.Parser as Parser exposing ((</>), Parser)
import Pages.StaticHttp as StaticHttp
import Template.Slide.Number_
type alias Model =
{ global : Shared.Model
, page : TemplateModel
, current :
Maybe
{ path :
{ path : PagePath
, query : Maybe String
, fragment : Maybe String
}
, metadata : Maybe Route
}
}
type TemplateModel
= ModelSlide__Number_ Template.Slide.Number_.Model
| NotFound
type Msg
= MsgGlobal Shared.Msg
| OnPageChange
{ path : PagePath
, query : Maybe String
, fragment : Maybe String
, metadata : Maybe Route
}
| MsgSlide__Number_ Template.Slide.Number_.Msg
view :
{ path : PagePath
, frontmatter : Maybe Route
}
->
StaticHttp.Request
{ view : Model -> { title : String, body : Html Msg }
, head : List Head.Tag
}
view page =
case page.frontmatter of
Nothing ->
StaticHttp.fail <| "Page not found: " ++ Pages.PagePath.toString page.path
Just (Route.Slide__Number_ s) ->
StaticHttp.map2
(\data globalData ->
{ view =
\model ->
case model.page of
ModelSlide__Number_ subModel ->
Template.Slide.Number_.template.view
subModel
model.global
{ static = data
, sharedStatic = globalData
, routeParams = s
, path = page.path
}
|> (\{ title, body } ->
Shared.template.view
globalData
page
model.global
MsgGlobal
({ title = title, body = body }
|> Document.map MsgSlide__Number_
)
)
_ ->
{ title = "Model mismatch", body = Html.text <| "Model mismatch" }
, head = Template.Slide.Number_.template.head
{ static = data
, sharedStatic = globalData
, routeParams = s
, path = page.path
}
}
)
(Template.Slide.Number_.template.staticData s)
(Shared.template.staticData)
init :
Maybe Shared.Model
->
Maybe
{ path :
{ path : PagePath
, query : Maybe String
, fragment : Maybe String
}
, metadata : Maybe Route
}
-> ( Model, Cmd Msg )
init currentGlobalModel maybePagePath =
let
( sharedModel, globalCmd ) =
currentGlobalModel |> Maybe.map (\m -> ( m, Cmd.none )) |> Maybe.withDefault (Shared.template.init maybePagePath)
( templateModel, templateCmd ) =
case maybePagePath |> Maybe.andThen .metadata of
Nothing ->
( NotFound, Cmd.none )
Just (Route.Slide__Number_ routeParams) ->
Template.Slide.Number_.template.init routeParams
|> Tuple.mapBoth ModelSlide__Number_ (Cmd.map MsgSlide__Number_)
in
( { global = sharedModel
, page = templateModel
, current = maybePagePath
}
, Cmd.batch
[ templateCmd
, globalCmd |> Cmd.map MsgGlobal
]
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
MsgGlobal msg_ ->
let
( sharedModel, globalCmd ) =
Shared.template.update msg_ model.global
in
( { model | global = sharedModel }
, globalCmd |> Cmd.map MsgGlobal
)
OnPageChange record ->
(init (Just model.global) <|
Just
{ path =
{ path = record.path
, query = record.query
, fragment = record.fragment
}
, metadata = record.metadata
}
)
|> (\( updatedModel, cmd ) ->
case Shared.template.onPageChange of
Nothing ->
( updatedModel, cmd )
Just thingy ->
let
( updatedGlobalModel, globalCmd ) =
Shared.template.update
(thingy
{ path = record.path
, query = record.query
, fragment = record.fragment
}
)
model.global
in
( { updatedModel
| global = updatedGlobalModel
}
, Cmd.batch [ cmd, Cmd.map MsgGlobal globalCmd ]
)
)
MsgSlide__Number_ msg_ ->
let
( updatedPageModel, pageCmd, ( newGlobalModel, newGlobalCmd ) ) =
case ( model.page, model.current |> Maybe.andThen .metadata ) of
( ModelSlide__Number_ pageModel, Just (Route.Slide__Number_ routeParams) ) ->
Template.Slide.Number_.template.update
routeParams
msg_
pageModel
model.global
|> mapBoth ModelSlide__Number_ (Cmd.map MsgSlide__Number_)
|> (\( a, b, c ) ->
case c of
Just sharedMsg ->
( a, b, Shared.template.update (Shared.SharedMsg sharedMsg) model.global )
Nothing ->
( a, b, ( model.global, Cmd.none ) )
)
_ ->
( model.page, Cmd.none, ( model.global, Cmd.none ) )
in
( { model | page = updatedPageModel, global = newGlobalModel }
, Cmd.batch [ pageCmd, newGlobalCmd |> Cmd.map MsgGlobal ]
)
type alias SiteConfig =
{ canonicalUrl : String
, manifest : Manifest.Config
}
templateSubscriptions : Route -> PagePath -> Model -> Sub Msg
templateSubscriptions route path model =
case ( model.page, route ) of
( ModelSlide__Number_ templateModel, Route.Slide__Number_ routeParams ) ->
Template.Slide.Number_.template.subscriptions
routeParams
path
templateModel
model.global
|> Sub.map MsgSlide__Number_
_ ->
Sub.none
main : Pages.Internal.Platform.Program Model Msg (Maybe Route)
main =
Pages.Internal.Platform.application
{ init = init Nothing
, urlToRoute = Route.urlToRoute
, routeToPath = Route.routeToPath
, site = Site.config
, getStaticRoutes = getStaticRoutes
, view = view
, update = update
, subscriptions =
\path model ->
Sub.batch
[ Shared.template.subscriptions path model.global |> Sub.map MsgGlobal
-- , templateSubscriptions (Route.Blog {}) path model
]
, onPageChange = Just OnPageChange
, canonicalSiteUrl = "TODO"
, toJsPort = toJsPort
, fromJsPort = fromJsPort identity
, generateFiles =
getStaticRoutes
|> StaticHttp.andThen
(\resolvedStaticRoutes ->
StaticHttp.map2 (::)
(manifestGenerator
resolvedStaticRoutes
)
(Site.config
resolvedStaticRoutes
|> .generateFiles
)
)
}
getStaticRoutes =
StaticHttp.combine
[ StaticHttp.succeed
[
]
, Template.Slide.Number_.template.staticRoutes |> StaticHttp.map (List.map Route.Slide__Number_)
]
|> StaticHttp.map List.concat
|> StaticHttp.map (List.map Just)
manifestGenerator : List ( Maybe Route ) -> StaticHttp.Request (Result anyError { path : List String, content : String })
manifestGenerator resolvedRoutes =
Site.config resolvedRoutes
|> .staticData
|> StaticHttp.map
(\data ->
(Site.config resolvedRoutes |> .manifest) data
|> manifestToFile ((Site.config resolvedRoutes |> .canonicalUrl) data)
)
manifestToFile : String -> Manifest.Config -> Result anyError { path : List String, content : String }
manifestToFile resolvedCanonicalUrl manifestConfig =
manifestConfig
|> Manifest.toJson resolvedCanonicalUrl
|> (\manifestJsonValue ->
Ok
{ path = [ "manifest.json" ]
, content = Json.Encode.encode 0 manifestJsonValue
}
)
port toJsPort : Json.Encode.Value -> Cmd msg
port fromJsPort : (Json.Decode.Value -> msg) -> Sub msg
mapDocument : Browser.Document Never -> Browser.Document mapped
mapDocument document =
{ title = document.title
, body = document.body |> List.map (Html.map never)
}
mapBoth fnA fnB ( a, b, c ) =
( fnA a, fnB b, c )

68
examples/slides/index.md Normal file
View File

@ -0,0 +1,68 @@
<Banner>A **statically typed** site generator</Banner>
<Boxes>
<Box>
### Pure Elm Configuration
Layouts, styles, even a full-fledged elm application.
### 📄 Type-Safe Content
Configuration, errors for broken links
</Box>
<Box>
### 🚀 `elm-pages build`
No `elm make` or `webpack` setup needed! Just one simple command.
</Box>
<Box>
### 📦 Optimized Elm Progressive Web App
Layouts, styles, even a full-fledged elm application.
### Deploy anywhere
Ship to Netlify, Github Pages, or any host that will serve up static files!
</Box>
</Boxes>
<Values>
<Value>
# No magic, just types
The magic is in how the pieces snap together. The basic platform provided is simple, letting you compose exactly what you need with types to support you.
</Value>
<Value>
# Extensible through pure elm
Behavior is shared through packages exposing simple helper functions to help you build up your data.
</Value>
<Value>
# If it compiles, it works
`elm-pages` just makes more of the things you do in your static site feel like elm. Did you misspell the name of an image asset or a link to a blog post? `elm-pages` will give you a friendly error message and some helpful suggestions.
</Value>
<Value>
# An extended elm platform
`elm-pages` is just elm, but with a broader set of primitives for declaring meta tags to improve SEO, or generate RSS feeds and other files based on your static content.
</Value>
<Value>
# Blazing fast
All you have to do is create your content and choose how to present it. Optimized images, pre-rendered pages, and a snappy lightweight single-page app all come for free.
</Value>
<Value>
# Simple
`elm-pages` gives you the smallest set of core concepts possible, and a type system to make sure all the pieces fit together just right. The rest is up to you. Rather than remember a lot of magic and special cases, you can just rely on your elm types to build what you need with a set of simple primitives.
</Value>
</Values>

537
examples/slides/package-lock.json generated Normal file
View File

@ -0,0 +1,537 @@
{
"name": "elm-pages-example",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "elm-pages-example",
"version": "1.0.0",
"license": "BSD-3",
"devDependencies": {
"elm-oembed": "0.0.6",
"elm-pages": "file:../..",
"elm-tooling": "^1.3.0",
"http-server": "^0.11.1"
}
},
"../..": {
"version": "1.5.5",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"connect": "^3.7.0",
"cross-spawn": "7.0.3",
"elm-hot": "^1.1.6",
"elm-optimize-level-2": "^0.1.5",
"globby": "^11.0.3",
"gray-matter": "^4.0.2",
"kleur": "^3.0.3",
"micromatch": "^4.0.2",
"serve-static": "^1.14.1",
"terser": "^5.6.1",
"xhr2": "^0.2.1"
},
"bin": {
"elm-pages": "generator/src/cli.js"
},
"devDependencies": {
"@types/cross-spawn": "^6.0.2",
"@types/jest": "^25.2.2",
"@types/micromatch": "^4.0.1",
"@types/node": "^12.7.7",
"elm-review": "^2.5.0",
"elm-test": "^0.19.1-revision6",
"elm-tooling": "^1.3.0",
"jest": "^26.0.1",
"typescript": "^4.2.3"
}
},
"node_modules/async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"dev": true,
"dependencies": {
"lodash": "^4.17.14"
}
},
"node_modules/colors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
"integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=",
"dev": true,
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/corser": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
"integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=",
"dev": true,
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/ecstatic": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-3.3.2.tgz",
"integrity": "sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog==",
"dev": true,
"dependencies": {
"he": "^1.1.1",
"mime": "^1.6.0",
"minimist": "^1.1.0",
"url-join": "^2.0.5"
},
"bin": {
"ecstatic": "lib/ecstatic.js"
}
},
"node_modules/elm-oembed": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/elm-oembed/-/elm-oembed-0.0.6.tgz",
"integrity": "sha512-EbDvhEgs5FmUF7T45HzcynsGnm/hqFslYU9z81UkjiE74/czedrQr8RXfMjSqnZ+Oloo7pi7ClO6xHHHyxcGKA==",
"dev": true
},
"node_modules/elm-pages": {
"resolved": "../..",
"link": true
},
"node_modules/elm-tooling": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/elm-tooling/-/elm-tooling-1.3.0.tgz",
"integrity": "sha512-OLRg8D7QCbzMmi8QTaebIIsH2qaIj4NiltuJ8vZj4JFHOIVa3utyD5tlfySBGZNfDG7+vZ2K8iDk82TalFaAbw==",
"dev": true,
"bin": {
"elm-tooling": "index.js"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz",
"integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true,
"bin": {
"he": "bin/he"
}
},
"node_modules/http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"dev": true,
"dependencies": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-server": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/http-server/-/http-server-0.11.1.tgz",
"integrity": "sha512-6JeGDGoujJLmhjiRGlt8yK8Z9Kl0vnl/dQoQZlc4oeqaUoAKQg94NILLfrY3oWzSyFaQCVNTcKE5PZ3cH8VP9w==",
"dev": true,
"dependencies": {
"colors": "1.0.3",
"corser": "~2.0.0",
"ecstatic": "^3.0.0",
"http-proxy": "^1.8.1",
"opener": "~1.4.0",
"optimist": "0.6.x",
"portfinder": "^1.0.13",
"union": "~0.4.3"
},
"bin": {
"hs": "bin/http-server",
"http-server": "bin/http-server"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"node_modules/mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"dependencies": {
"minimist": "^1.2.5"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"node_modules/opener": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz",
"integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=",
"dev": true,
"bin": {
"opener": "opener.js"
}
},
"node_modules/optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
"integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
"dev": true,
"dependencies": {
"minimist": "~0.0.1",
"wordwrap": "~0.0.2"
}
},
"node_modules/optimist/node_modules/minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=",
"dev": true
},
"node_modules/portfinder": {
"version": "1.0.28",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
"integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
"dev": true,
"dependencies": {
"async": "^2.6.2",
"debug": "^3.1.1",
"mkdirp": "^0.5.5"
},
"engines": {
"node": ">= 0.12.0"
}
},
"node_modules/qs": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz",
"integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ=",
"dev": true
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
"node_modules/union": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/union/-/union-0.4.6.tgz",
"integrity": "sha1-GY+9rrolTniLDvy2MLwR8kopWeA=",
"dev": true,
"dependencies": {
"qs": "~2.3.3"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/url-join": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz",
"integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=",
"dev": true
},
"node_modules/wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
"integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
}
},
"dependencies": {
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
}
},
"colors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
"integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=",
"dev": true
},
"corser": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
"integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=",
"dev": true
},
"debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"ecstatic": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-3.3.2.tgz",
"integrity": "sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog==",
"dev": true,
"requires": {
"he": "^1.1.1",
"mime": "^1.6.0",
"minimist": "^1.1.0",
"url-join": "^2.0.5"
}
},
"elm-oembed": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/elm-oembed/-/elm-oembed-0.0.6.tgz",
"integrity": "sha512-EbDvhEgs5FmUF7T45HzcynsGnm/hqFslYU9z81UkjiE74/czedrQr8RXfMjSqnZ+Oloo7pi7ClO6xHHHyxcGKA==",
"dev": true
},
"elm-pages": {
"version": "file:../..",
"requires": {
"@types/cross-spawn": "^6.0.2",
"@types/jest": "^25.2.2",
"@types/micromatch": "^4.0.1",
"@types/node": "^12.7.7",
"connect": "^3.7.0",
"cross-spawn": "7.0.3",
"elm-hot": "^1.1.6",
"elm-optimize-level-2": "^0.1.5",
"elm-review": "^2.5.0",
"elm-test": "^0.19.1-revision6",
"elm-tooling": "^1.3.0",
"globby": "^11.0.3",
"gray-matter": "^4.0.2",
"jest": "^26.0.1",
"kleur": "^3.0.3",
"micromatch": "^4.0.2",
"serve-static": "^1.14.1",
"terser": "^5.6.1",
"typescript": "^4.2.3",
"xhr2": "^0.2.1"
}
},
"elm-tooling": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/elm-tooling/-/elm-tooling-1.3.0.tgz",
"integrity": "sha512-OLRg8D7QCbzMmi8QTaebIIsH2qaIj4NiltuJ8vZj4JFHOIVa3utyD5tlfySBGZNfDG7+vZ2K8iDk82TalFaAbw==",
"dev": true
},
"eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"dev": true
},
"follow-redirects": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz",
"integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==",
"dev": true
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"dev": true,
"requires": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
}
},
"http-server": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/http-server/-/http-server-0.11.1.tgz",
"integrity": "sha512-6JeGDGoujJLmhjiRGlt8yK8Z9Kl0vnl/dQoQZlc4oeqaUoAKQg94NILLfrY3oWzSyFaQCVNTcKE5PZ3cH8VP9w==",
"dev": true,
"requires": {
"colors": "1.0.3",
"corser": "~2.0.0",
"ecstatic": "^3.0.0",
"http-proxy": "^1.8.1",
"opener": "~1.4.0",
"optimist": "0.6.x",
"portfinder": "^1.0.13",
"union": "~0.4.3"
}
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"opener": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz",
"integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=",
"dev": true
},
"optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
"integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
"dev": true,
"requires": {
"minimist": "~0.0.1",
"wordwrap": "~0.0.2"
},
"dependencies": {
"minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=",
"dev": true
}
}
},
"portfinder": {
"version": "1.0.28",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
"integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
"dev": true,
"requires": {
"async": "^2.6.2",
"debug": "^3.1.1",
"mkdirp": "^0.5.5"
}
},
"qs": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz",
"integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ=",
"dev": true
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
"union": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/union/-/union-0.4.6.tgz",
"integrity": "sha1-GY+9rrolTniLDvy2MLwR8kopWeA=",
"dev": true,
"requires": {
"qs": "~2.3.3"
}
},
"url-join": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz",
"integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=",
"dev": true
},
"wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
"integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
"dev": true
}
}
}

View File

@ -0,0 +1,18 @@
{
"name": "elm-pages-example",
"version": "1.0.0",
"description": "Example site built with elm-pages.",
"scripts": {
"start": "elm-pages develop --port 1234",
"serve": "npm run build && http-server ./dist -a localhost -p 3000 -c-1",
"build": "node ../../generator/src/cli.js"
},
"author": "Dillon Kearns",
"license": "BSD-3",
"devDependencies": {
"elm-oembed": "0.0.6",
"elm-pages": "file:../..",
"elm-tooling": "^1.3.0",
"http-server": "^0.11.1"
}
}

View File

@ -0,0 +1,7 @@
## Slide 1
Here's some content
## Another slide
Here's another body

View File

@ -0,0 +1,137 @@
module Article exposing (..)
import Cloudinary
import Date exposing (Date)
import Element exposing (Element)
import Glob
import OptimizedDecoder
import Pages.ImagePath exposing (ImagePath)
import Pages.PagePath as PagePath exposing (PagePath)
import Pages.StaticFile as StaticFile
import Pages.StaticHttp as StaticHttp
type alias BlogPost =
{ filePath : String
, slug : String
}
blogPostsGlob : StaticHttp.Request (List { filePath : String, slug : String })
blogPostsGlob =
Glob.succeed BlogPost
|> Glob.keep Glob.fullFilePath
|> Glob.drop (Glob.literal "content/blog/")
|> Glob.keep Glob.wildcard
|> Glob.drop (Glob.literal ".md")
|> Glob.toStaticHttp
allMetadata : StaticHttp.Request (List ( PagePath, ArticleMetadata ))
allMetadata =
--StaticFile.glob "content/blog/*.md"
blogPostsGlob
|> StaticHttp.map
(\paths ->
paths
|> List.map
(\{ filePath, slug } ->
StaticHttp.map2 Tuple.pair
(StaticHttp.succeed <| "blog/" ++ slug)
(StaticFile.request filePath (StaticFile.frontmatter frontmatterDecoder))
)
)
|> StaticHttp.resolve
|> StaticHttp.map
(\articles ->
articles
|> List.filterMap
(\( path, metadata ) ->
if metadata.draft then
Nothing
else
Just
( path |> PagePath.external
, metadata
)
)
)
type alias DataFromFile msg =
{ body : List (Element msg)
, metadata : ArticleMetadata
}
--fileRequest : String -> StaticHttp.Request (DataFromFile msg)
fileRequest : String -> StaticHttp.Request ArticleMetadata
fileRequest filePath =
StaticFile.request
--"content/blog/extensible-markdown-parsing-in-elm.md"
filePath
--(OptimizedDecoder.map2 DataFromFile
-- (StaticFile.body
-- |> OptimizedDecoder.andThen
-- (\rawBody ->
-- case
-- rawBody
-- |> MarkdownRenderer.view
-- |> Result.map Tuple.second
-- of
-- Ok renderedBody ->
-- OptimizedDecoder.succeed renderedBody
--
-- Err error ->
-- OptimizedDecoder.fail error
-- )
-- )
(StaticFile.frontmatter frontmatterDecoder)
--)
type alias ArticleMetadata =
{ title : String
, description : String
, published : Date
, image : ImagePath
, draft : Bool
}
frontmatterDecoder : OptimizedDecoder.Decoder ArticleMetadata
frontmatterDecoder =
OptimizedDecoder.map5 ArticleMetadata
(OptimizedDecoder.field "title" OptimizedDecoder.string)
(OptimizedDecoder.field "description" OptimizedDecoder.string)
(OptimizedDecoder.field "published"
(OptimizedDecoder.string
|> OptimizedDecoder.andThen
(\isoString ->
case Date.fromIsoString isoString of
Ok date ->
OptimizedDecoder.succeed date
Err error ->
OptimizedDecoder.fail error
)
)
)
(OptimizedDecoder.field "image" imageDecoder)
(OptimizedDecoder.field "draft" OptimizedDecoder.bool
|> OptimizedDecoder.maybe
|> OptimizedDecoder.map (Maybe.withDefault False)
)
imageDecoder : OptimizedDecoder.Decoder ImagePath
imageDecoder =
OptimizedDecoder.string
|> OptimizedDecoder.map (\cloudinaryAsset -> Cloudinary.url cloudinaryAsset Nothing 800)

View File

@ -0,0 +1,53 @@
module Data.Author exposing (Author, all, decoder, dillon, view)
import Cloudinary
import Element exposing (Element)
import Html.Attributes as Attr
import Json.Decode as Decode exposing (Decoder)
import List.Extra
import Pages.ImagePath as ImagePath exposing (ImagePath)
type alias Author =
{ name : String
, avatar : ImagePath
, bio : String
}
all : List Author
all =
[ dillon
]
dillon : Author
dillon =
{ name = "Dillon Kearns"
, avatar = Cloudinary.url "v1602899672/elm-radio/dillon-profile_n2lqst.jpg" Nothing 140
, 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 }

View File

@ -0,0 +1,71 @@
module DocSidebar exposing (view)
import Element exposing (Element)
import Element.Border as Border
import Element.Font
import Pages.PagePath as PagePath exposing (PagePath)
import Palette
view :
PagePath
-> Element msg
view currentPage =
Element.column
[ Element.spacing 10
, Border.widthEach { bottom = 0, left = 0, right = 1, top = 0 }
, Border.color (Element.rgba255 40 80 40 0.4)
, Element.padding 12
, Element.height Element.fill
]
[]
--(posts
-- |> List.filterMap
-- (\( path, metadata ) ->
-- case metadata of
-- TemplateType.Documentation meta ->
-- Just ( currentPage == path, path, meta )
--
-- _ ->
-- Nothing
-- )
-- |> List.map postSummary
--)
postSummary :
( Bool, PagePath, { title : String } )
-> Element msg
postSummary ( isCurrentPage, postPath, post ) =
[ Element.text post.title ]
|> Element.paragraph
([ Element.Font.size 18
, Element.Font.family [ Element.Font.typeface "Roboto" ]
, Element.Font.semiBold
, Element.padding 16
]
++ (if isCurrentPage then
[ Element.Font.underline
, Element.Font.color Palette.color.primary
]
else
[]
)
)
|> linkToPost postPath
linkToPost : PagePath -> Element msg -> Element msg
linkToPost postPath content =
Element.link [ Element.width Element.fill ]
{ url = PagePath.toString postPath, label = content }
docUrl : List String -> String
docUrl postPath =
"/"
++ String.join "/" postPath

View File

@ -0,0 +1,16 @@
module Document exposing (Document, map)
import Element exposing (Element)
type alias Document msg =
{ title : String
, body : List (Element msg)
}
map : (msg1 -> msg2) -> Document msg1 -> Document msg2
map fn doc =
{ title = doc.title
, body = List.map (Element.map fn) doc.body
}

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 []

View File

@ -0,0 +1,54 @@
module Dotted exposing (lines)
import Element
import Svg
import Svg.Attributes as Attr
{-
.css-m2heu9 {
stroke: #8a4baf;
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 0.5 10;
-webkit-animation: animation-yweh2o 400ms linear infinite;
animation: animation-yweh2o 400ms linear infinite;
}
-}
{-
<svg width="20" height="30" viewBox="0 0 20 30" class="css-p2euw5">
<path d="M10 40 L10 -10" class="css-m2heu9"></path>
</svg>
-}
lines =
Svg.svg
[ Attr.width "20"
, Attr.height "30"
, Attr.viewBox "0 0 20 30"
]
[ Svg.path
[ Attr.stroke "#2a75ff"
, Attr.strokeWidth "4"
, Attr.strokeLinecap "round"
, Attr.strokeDasharray "0.5 10"
, Attr.d "M10 40 L10 -10"
, Attr.class "dotted-line"
]
[]
]
|> Element.html
|> Element.el
[ Element.centerX
]
-- rgb(0, 36, 71)
-- #002447
{-
.css-m2heu9{stroke:#8a4baf;stroke-width:3;stroke-linecap:round;stroke-dasharray:0.5 10;-webkit-animation:animation-yweh2o 400ms linear infinite;animation:animation-yweh2o 400ms linear infinite;}@-webkit-keyframes animation-yweh2o{to{stroke-dashoffset:10;}}@keyframes animation-yweh2o{to{stroke-dashoffset:10;}}
-}

View File

@ -0,0 +1,20 @@
module Ellie exposing (outputTab)
import Element exposing (Element)
import Html
import Html.Attributes as Attr
outputTab : String -> Element msg
outputTab ellieId =
Html.iframe
[ Attr.src <| "https://ellie-app.com/embed/" ++ ellieId ++ "?panel=output"
, Attr.style "width" "100%"
, Attr.style "height" "400px"
, Attr.style "border" "0"
, Attr.style "overflow" "hidden"
, Attr.attribute "sandbox" "allow-modals allow-forms allow-popups allow-scripts allow-same-origin"
]
[]
|> Element.html
|> Element.el [ Element.width Element.fill ]

View File

@ -0,0 +1,76 @@
module Feed exposing (fileToGenerate)
import Metadata exposing (Metadata(..))
import Pages
import Pages.PagePath as PagePath exposing (PagePath)
import Rss
fileToGenerate :
{ siteTagline : String
, siteUrl : String
}
->
List
{ path : PagePath
, frontmatter : Metadata
, body : String
}
->
{ path : List String
, content : String
}
fileToGenerate config siteMetadata =
{ path = [ "blog", "feed.xml" ]
, content = generate config siteMetadata
}
generate :
{ siteTagline : String
, siteUrl : String
}
->
List
{ path : PagePath
, frontmatter : Metadata
, body : String
}
-> String
generate { siteTagline, siteUrl } siteMetadata =
Rss.generate
{ title = "elm-pages Blog"
, description = siteTagline
, url = "https://elm-pages.com/blog"
, lastBuildTime = Pages.builtAt
, generator = Just "elm-pages"
, items = siteMetadata |> List.filterMap metadataToRssItem
, siteUrl = siteUrl
}
metadataToRssItem :
{ path : PagePath
, frontmatter : Metadata
, body : String
}
-> Maybe Rss.Item
metadataToRssItem page =
case page.frontmatter of
Article article ->
if article.draft then
Nothing
else
Just
{ title = article.title
, description = article.description
, url = PagePath.toString page.path
, categories = []
, author = article.author.name
, pubDate = Rss.Date article.published
, content = Nothing
}
_ ->
Nothing

View File

@ -0,0 +1,18 @@
module FontAwesome exposing (icon, styledIcon)
import Element exposing (Element)
import Html
import Html.Attributes
styledIcon : String -> List (Element.Attribute msg) -> Element msg
styledIcon classString styles =
Html.i [ Html.Attributes.class classString ] []
|> Element.html
|> Element.el styles
icon : String -> Element msg
icon classString =
Html.i [ Html.Attributes.class classString ] []
|> Element.html

View File

@ -0,0 +1,106 @@
module Index exposing (view)
import Article
import Data.Author
import Date
import Element exposing (Element)
import Element.Border
import Element.Font
import Pages
import Pages.ImagePath as ImagePath
import Pages.PagePath as PagePath exposing (PagePath)
view :
List ( PagePath, Article.ArticleMetadata )
-> Element msg
view posts =
Element.column [ Element.spacing 20 ]
(posts
|> List.sortBy
(\( _, metadata ) -> -(metadata.published |> Date.toRataDie))
|> List.map postSummary
)
postSummary :
( PagePath, Article.ArticleMetadata )
-> Element msg
postSummary ( path, post ) =
articleIndex post |> linkToPost path
-- postPath
linkToPost : PagePath -> 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 "Montserrat" ]
, Element.Font.semiBold
, Element.padding 16
]
articleIndex : Article.ArticleMetadata -> Element msg
articleIndex metadata =
Element.el
[ Element.centerX
, Element.width (Element.maximum 600 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)
grey =
Element.Font.color (Element.rgba255 0 0 0 0.5)
postPreview : Article.ArticleMetadata -> Element msg
postPreview post =
let
author =
Data.Author.dillon
in
Element.textColumn
[ Element.centerX
, Element.width Element.fill
, Element.spacing 30
, Element.Font.size 18
]
[ title post.title
, Element.image [ Element.width Element.fill ] { src = post.image |> ImagePath.toString, description = "Blog post cover photo" }
, Element.row
[ Element.spacing 10
, Element.centerX
, grey
]
[ Data.Author.view [ Element.width (Element.px 40) ] author
, Element.text 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
]
]

View File

@ -0,0 +1,313 @@
module MarkdownRenderer exposing (TableOfContents, renderer, view)
import Dotted
import Element exposing (Element)
import Element.Background
import Element.Border
import Element.Font as Font
import Element.Input
import Element.Region
import Ellie
import Html
import Html.Attributes
import Markdown.Block as Block exposing (Block, Inline, ListItem(..), Task(..))
import Markdown.Html
import Markdown.Parser
import Markdown.Renderer
import Oembed
import Palette
import SyntaxHighlight
buildToc : List Block -> TableOfContents
buildToc blocks =
let
headings =
gatherHeadings blocks
in
headings
|> List.map Tuple.second
|> List.map
(\styledList ->
{ anchorId = styledToString styledList |> rawTextToId
, name = styledToString styledList
, level = 1
}
)
type alias TableOfContents =
List { anchorId : String, name : String, level : Int }
view : String -> Result String ( TableOfContents, List (Element msg) )
view markdown =
case
markdown
|> Markdown.Parser.parse
of
Ok okAst ->
case Markdown.Renderer.render renderer okAst of
Ok rendered ->
Ok ( buildToc okAst, rendered )
Err errors ->
Err errors
Err error ->
Err (error |> List.map Markdown.Parser.deadEndToString |> String.join "\n")
renderer : Markdown.Renderer.Renderer (Element msg)
renderer =
{ heading = heading
, paragraph =
Element.paragraph
[ Element.spacing 15 ]
, thematicBreak = Element.none
, text = \value -> Element.paragraph [] [ Element.text value ]
, strong = \content -> Element.paragraph [ Font.bold ] content
, emphasis = \content -> Element.paragraph [ Font.italic ] content
, codeSpan = code
, link =
\{ destination } body ->
Element.newTabLink []
{ url = destination
, label =
Element.paragraph
[ Font.color (Element.rgb255 0 0 255)
, Element.htmlAttribute (Html.Attributes.style "overflow-wrap" "break-word")
, Element.htmlAttribute (Html.Attributes.style "word-break" "break-word")
]
body
}
, hardLineBreak = Html.br [] [] |> Element.html
, image =
\image ->
case image.title of
Just _ ->
Element.image [ Element.width Element.fill ] { src = image.src, description = image.alt }
Nothing ->
Element.image [ Element.width Element.fill ] { src = image.src, description = image.alt }
, blockQuote =
\children ->
Element.column
[ Element.Border.widthEach { top = 0, right = 0, bottom = 0, left = 10 }
, Element.padding 10
, Element.Border.color (Element.rgb255 145 145 145)
, Element.Background.color (Element.rgb255 245 245 245)
]
children
, unorderedList =
\items ->
Element.column [ Element.spacing 15 ]
(items
|> List.map
(\(ListItem task children) ->
Element.paragraph [ Element.spacing 5 ]
[ Element.row
[ Element.alignTop ]
((case task of
IncompleteTask ->
Element.Input.defaultCheckbox False
CompletedTask ->
Element.Input.defaultCheckbox True
NoTask ->
Element.text ""
)
:: Element.text " "
:: children
)
]
)
)
, orderedList =
\startingIndex items ->
Element.column [ Element.spacing 15 ]
(items
|> List.indexedMap
(\index itemBlocks ->
Element.row [ Element.spacing 5 ]
[ Element.row [ Element.alignTop ]
(Element.text (String.fromInt (index + startingIndex) ++ " ") :: itemBlocks)
]
)
)
, codeBlock = codeBlock
, table = Element.column []
, tableHeader = Element.column []
, tableBody = Element.column []
, tableRow = Element.row []
, tableHeaderCell =
\_ children ->
Element.paragraph [] children
, tableCell = Element.paragraph []
, html =
Markdown.Html.oneOf
[ Markdown.Html.tag "banner"
(\children ->
Element.paragraph
[ Font.center
, Font.size 47
, Font.family [ Font.typeface "Montserrat" ]
, Font.color Palette.color.primary
]
children
)
, Markdown.Html.tag "boxes"
(\children ->
children
|> List.indexedMap
(\index aBox ->
let
isLast =
index == (List.length children - 1)
in
[ Just aBox
, if isLast then
Nothing
else
Just Dotted.lines
]
|> List.filterMap identity
)
|> List.concat
|> List.reverse
|> Element.column [ Element.centerX ]
)
, Markdown.Html.tag "box"
(\children ->
Element.textColumn
[ Element.centerX
, Font.center
, Element.padding 30
, Element.Border.shadow { offset = ( 2, 2 ), size = 3, blur = 3, color = Element.rgba255 40 80 80 0.1 }
, Element.spacing 15
]
children
)
, Markdown.Html.tag "values"
(\children ->
Element.row
[ Element.spacing 30
, Element.htmlAttribute (Html.Attributes.style "flex-wrap" "wrap")
]
children
)
, Markdown.Html.tag "value"
(\children ->
Element.column
[ Element.width Element.fill
, Element.padding 20
, Element.spacing 20
, Element.height Element.fill
, Element.centerX
]
children
)
, Markdown.Html.tag "oembed"
(\url _ ->
Oembed.view [] Nothing url
|> Maybe.map Element.html
|> Maybe.withDefault Element.none
|> Element.el [ Element.centerX ]
)
|> Markdown.Html.withAttribute "url"
, Markdown.Html.tag "ellie-output"
(\ellieId _ ->
-- Oembed.view [] Nothing url
-- |> Maybe.map Element.html
-- |> Maybe.withDefault Element.none
-- |> Element.el [ Element.centerX ]
Ellie.outputTab ellieId
)
|> Markdown.Html.withAttribute "id"
]
}
styledToString : List Inline -> String
styledToString inlines =
--List.map .string list
--|> String.join "-"
-- TODO do I need to hyphenate?
inlines
|> Block.extractInlineText
gatherHeadings : List Block -> List ( Block.HeadingLevel, List Inline )
gatherHeadings blocks =
List.filterMap
(\block ->
case block of
Block.Heading level content ->
Just ( level, content )
_ ->
Nothing
)
blocks
rawTextToId : String -> String
rawTextToId rawText =
rawText
|> String.split " "
|> String.join "-"
|> String.toLower
heading : { level : Block.HeadingLevel, rawText : String, children : List (Element msg) } -> Element msg
heading { level, rawText, children } =
Element.paragraph
[ Font.size
(case level of
Block.H1 ->
36
Block.H2 ->
24
_ ->
20
)
, Font.bold
, Font.family [ Font.typeface "Montserrat" ]
, Element.Region.heading (Block.headingLevelToInt level)
, Element.htmlAttribute
(Html.Attributes.attribute "name" (rawTextToId rawText))
, Element.htmlAttribute
(Html.Attributes.id (rawTextToId rawText))
]
children
code : String -> Element msg
code snippet =
Element.el
[ Element.Background.color
(Element.rgba255 50 50 50 0.07)
, Element.Border.rounded 2
, Element.paddingXY 5 3
, Font.family [ Font.typeface "Roboto Mono", Font.monospace ]
]
(Element.text snippet)
codeBlock : { body : String, language : Maybe String } -> Element msg
codeBlock details =
Element.paragraph [] [ Element.text details.body ]
-- TODO turn this back on - it's off for now to get more accurate performance benchmarks
--SyntaxHighlight.elm details.body
-- |> Result.map (SyntaxHighlight.toBlockHtml (Just 1))
-- |> Result.withDefault
-- (Html.pre [] [ Html.code [] [ Html.text details.body ] ])
-- |> Element.html
-- |> Element.el [ Element.width Element.fill ]

View File

@ -0,0 +1,29 @@
module MySitemap exposing (install)
import Head
import Pages.Platform exposing (Builder)
import Pages.StaticHttp as StaticHttp
import Sitemap
install :
{ siteUrl : String
}
-> (List item -> List { path : String, lastMod : Maybe String })
-> StaticHttp.Request (List item)
-> Builder pathKey userModel userMsg route
-> Builder pathKey userModel userMsg route
install config toSitemapEntry request builder =
builder
|> Pages.Platform.withGlobalHeadTags [ Head.sitemapLink "/sitemap.xml" ]
|> Pages.Platform.withFileGenerator
(request
|> StaticHttp.map
(\items ->
[ Ok
{ path = [ "sitemap.xml" ]
, content = Sitemap.build config (toSitemapEntry items)
}
]
)
)

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 0 6 255
, secondary = Element.rgb255 0 242 96
}
heading : Int -> List (Element msg) -> Element msg
heading level content =
Element.paragraph
([ Font.bold
, Font.family [ Font.typeface "Montserrat" ]
, 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 "Montserrat" ]
, Element.Region.heading 1
, Font.size 36
, Font.center
]
[ Element.text title ]

View File

@ -0,0 +1,53 @@
module RssPlugin exposing (generate)
import Head
import Pages.PagePath as PagePath exposing (PagePath)
import Pages.Platform exposing (Builder)
import Pages.StaticHttp as StaticHttp
import Rss
import Time
generate :
{ siteTagline : String
, siteUrl : String
, title : String
, builtAt : Time.Posix
, indexPage : PagePath
}
-> (item -> Maybe Rss.Item)
-> StaticHttp.Request (List item)
-> Builder pathKey userModel userMsg route
-> Builder pathKey userModel userMsg route
generate options metadataToRssItem itemsRequest builder =
let
feedFilePath =
(options.indexPage
|> PagePath.toPath
)
++ [ "feed.xml" ]
in
builder
|> Pages.Platform.withFileGenerator
(itemsRequest
|> StaticHttp.map
(\items ->
{ path = feedFilePath
, content =
Rss.generate
{ title = options.title
, description = options.siteTagline
-- TODO make sure you don't add an extra "/"
, url = options.siteUrl ++ "/" ++ PagePath.toString options.indexPage
, lastBuildTime = options.builtAt
, generator = Just "elm-pages"
, items = items |> List.filterMap metadataToRssItem
, siteUrl = options.siteUrl
}
}
|> Ok
|> List.singleton
)
)
|> Pages.Platform.withGlobalHeadTags [ Head.rssLink (feedFilePath |> String.join "/") ]

View File

@ -0,0 +1,48 @@
module ServerRequest exposing (ServerRequest, expectHeader, init, optionalHeader, staticData, toStaticHttp)
import Internal.OptimizedDecoder exposing (OptimizedDecoder)
import OptimizedDecoder
import Pages.StaticHttp as StaticHttp
import Secrets
type ServerRequest decodesTo
= ServerRequest (OptimizedDecoder.Decoder decodesTo)
init : constructor -> ServerRequest constructor
init constructor =
ServerRequest (OptimizedDecoder.succeed constructor)
staticData : StaticHttp.Request String
staticData =
StaticHttp.get (Secrets.succeed "$$elm-pages$$headers")
(OptimizedDecoder.field "headers"
(OptimizedDecoder.field "accept-language" OptimizedDecoder.string)
)
toStaticHttp : ServerRequest decodesTo -> StaticHttp.Request decodesTo
toStaticHttp (ServerRequest decoder) =
StaticHttp.get (Secrets.succeed "$$elm-pages$$headers") decoder
expectHeader : String -> ServerRequest (String -> value) -> ServerRequest value
expectHeader headerName (ServerRequest decoder) =
decoder
|> OptimizedDecoder.andMap
(OptimizedDecoder.field headerName OptimizedDecoder.string
|> OptimizedDecoder.field "headers"
)
|> ServerRequest
optionalHeader : String -> ServerRequest (Maybe String -> value) -> ServerRequest value
optionalHeader headerName (ServerRequest decoder) =
decoder
|> OptimizedDecoder.andMap
(OptimizedDecoder.optionalField headerName OptimizedDecoder.string
|> OptimizedDecoder.field "headers"
)
|> ServerRequest

View File

@ -0,0 +1,296 @@
module Shared exposing (Model, Msg(..), SharedMsg(..), StaticData, template)
import Document exposing (Document)
import DocumentSvg
import Element exposing (Element)
import Element.Background
import Element.Border
import Element.Events
import Element.Font as Font
import Element.Region
import FontAwesome
import Html exposing (Html)
import Html.Attributes as Attr
import OptimizedDecoder as D
import Pages.PagePath as PagePath exposing (PagePath)
import Pages.StaticHttp as StaticHttp
import Palette
import Secrets
import SharedTemplate exposing (SharedTemplate)
template : SharedTemplate Msg Model StaticData msg
template =
{ init = init
, update = update
, view = view
, staticData = staticData
, subscriptions = subscriptions
, onPageChange = Just OnPageChange
}
type Msg
= OnPageChange
{ path : PagePath
, query : Maybe String
, fragment : Maybe String
}
| ToggleMobileMenu
| Increment
| SharedMsg SharedMsg
type alias StaticData =
Int
type SharedMsg
= IncrementFromChild
type alias Model =
{ showMobileMenu : Bool
, counter : Int
}
init :
Maybe
{ path :
{ path : PagePath
, query : Maybe String
, fragment : Maybe String
}
, metadata : route
}
-> ( Model, Cmd Msg )
init maybePagePath =
( { showMobileMenu = False
, counter = 0
}
, Cmd.none
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
OnPageChange _ ->
( { model | showMobileMenu = False }, Cmd.none )
ToggleMobileMenu ->
( { model | showMobileMenu = not model.showMobileMenu }, Cmd.none )
Increment ->
( { model | counter = model.counter + 1 }, Cmd.none )
SharedMsg globalMsg ->
case globalMsg of
IncrementFromChild ->
( { model | counter = model.counter + 1 }, Cmd.none )
subscriptions : PagePath -> Model -> Sub Msg
subscriptions _ _ =
Sub.none
staticData : StaticHttp.Request StaticData
staticData =
StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
(D.field "stargazers_count" D.int)
view :
StaticData
->
{ path : PagePath
, frontmatter : route
}
-> Model
-> (Msg -> msg)
-> Document msg
-> { body : Html msg, title : String }
view stars page model toMsg pageView =
{ body =
(if model.showMobileMenu then
Element.column
[ Element.width Element.fill
, Element.padding 20
]
[ Element.row [ Element.width Element.fill, Element.spaceEvenly ]
[ logoLinkMobile |> Element.map toMsg
, FontAwesome.styledIcon "fas fa-bars" [ Element.Events.onClick ToggleMobileMenu ]
|> Element.map toMsg
]
, Element.column [ Element.centerX, Element.spacing 20 ]
(navbarLinks stars page.path)
]
else
Element.column [ Element.width Element.fill ]
(List.concat
[ [ header stars page.path |> Element.map toMsg
--, incrementView model |> Element.map toMsg
]
, pageView.body
]
)
)
|> Element.layout
[ Element.width Element.fill
, Font.size 20
, Font.family [ Font.typeface "Roboto" ]
, Font.color (Element.rgba255 0 0 0 0.8)
]
, title = pageView.title
}
logoLinkMobile =
Element.link []
{ url = "/"
, label =
Element.row
[ Font.size 30
, Element.spacing 16
, Element.htmlAttribute (Attr.class "navbar-title")
]
[ Element.text "elm-pages"
]
}
navbarLinks stars currentPath =
[ elmDocsLink
, githubRepoLink stars
, highlightableLink currentPath [ "docs" ] "Docs"
, highlightableLink currentPath [ "showcase" ] "Showcase"
, highlightableLink currentPath [ "blog" ] "Blog"
]
header : Int -> PagePath -> Element Msg
header stars currentPath =
Element.column [ Element.width Element.fill ]
[ responsiveHeader
, Element.column
[ Element.width Element.fill
, Element.htmlAttribute (Attr.class "responsive-desktop")
]
[ 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)
]
[ logoLink
, Element.row [ Element.spacing 15 ] (navbarLinks stars currentPath)
]
]
]
logoLink =
Element.link []
{ url = "/"
, label =
Element.row
[ Font.size 30
, Element.spacing 16
, Element.htmlAttribute (Attr.class "navbar-title")
]
[ DocumentSvg.view
, Element.text "elm-pages"
]
}
responsiveHeader =
Element.row
[ Element.width Element.fill
, Element.spaceEvenly
, Element.htmlAttribute (Attr.class "responsive-mobile")
, Element.width Element.fill
, Element.padding 20
]
[ logoLinkMobile
, FontAwesome.icon "fas fa-bars" |> Element.el [ Element.alignRight, Element.Events.onClick ToggleMobileMenu ]
]
githubRepoLink : Int -> Element msg
githubRepoLink starCount =
Element.newTabLink []
{ url = "https://github.com/dillonkearns/elm-pages"
, label =
Element.row [ Element.spacing 5 ]
[ Element.image
[ Element.width (Element.px 22)
, Font.color Palette.color.primary
]
{ src = "/images/github.svg", description = "Github repo" }
, Element.text <| String.fromInt starCount
]
}
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 = "/images/elm-logo.svg", description = "Elm Package Docs" }
}
highlightableLink :
PagePath
-> List String
-> String
-> Element msg
highlightableLink currentPath linkDirectory displayName =
let
isHighlighted =
(currentPath |> PagePath.toPath)
== linkDirectory
|| (currentPath
|> PagePath.toPath
|> List.reverse
|> List.drop 1
|> List.reverse
)
== linkDirectory
in
Element.link
(if isHighlighted then
[ Font.underline
, Font.color Palette.color.primary
]
else
[]
)
{ url = linkDirectory |> String.join "/"
, label = Element.text displayName
}

View File

@ -0,0 +1,42 @@
module SharedTemplate exposing (..)
import Document exposing (Document)
import Html exposing (Html)
import Pages.PagePath exposing (PagePath)
import Pages.StaticHttp as StaticHttp
import Route exposing (Route)
type alias SharedTemplate sharedMsg sharedModel sharedStaticData mappedMsg =
{ init :
Maybe
{ path :
{ path : PagePath
, query : Maybe String
, fragment : Maybe String
}
, metadata : Maybe Route
}
-> ( sharedModel, Cmd sharedMsg )
, update : sharedMsg -> sharedModel -> ( sharedModel, Cmd sharedMsg )
, view :
sharedStaticData
->
{ path : PagePath
, frontmatter : Maybe Route
}
-> sharedModel
-> (sharedMsg -> mappedMsg)
-> Document mappedMsg
-> { body : Html mappedMsg, title : String }
, staticData : StaticHttp.Request sharedStaticData
, subscriptions : PagePath -> sharedModel -> Sub sharedMsg
, onPageChange :
Maybe
({ path : PagePath
, query : Maybe String
, fragment : Maybe String
}
-> sharedMsg
)
}

View File

@ -0,0 +1,160 @@
module Showcase exposing (..)
import Element
import Element.Border
import Element.Font
import FontAwesome
import OptimizedDecoder as Decode
import Pages.Secrets as Secrets
import Pages.StaticHttp as StaticHttp
import Palette
view : List Entry -> Element.Element msg
view entries =
Element.column
[ Element.spacing 30
]
(submitShowcaseItemButton
:: List.map entryView entries
)
submitShowcaseItemButton =
Element.newTabLink
[ Element.Font.color Palette.color.primary
, Element.Font.underline
]
{ url = "https://airtable.com/shrPSenIW2EQqJ083"
, label = Element.text "Submit your site to the showcase"
}
entryView : Entry -> Element.Element msg
entryView entry =
Element.column
[ Element.spacing 15
, Element.Border.shadow { offset = ( 2, 2 ), size = 3, blur = 3, color = Element.rgba255 40 80 80 0.1 }
, Element.padding 40
, Element.width (Element.maximum 700 Element.fill)
]
[ Element.newTabLink [ Element.Font.size 14, Element.Font.color Palette.color.primary ]
{ url = entry.liveUrl
, label =
Element.image [ Element.width Element.fill ]
{ src = "https://image.thum.io/get/width/800/crop/800/" ++ entry.screenshotUrl
, description = "Site Screenshot"
}
}
, Element.text entry.displayName |> Element.el [ Element.Font.extraBold ]
, Element.newTabLink [ Element.Font.size 14, Element.Font.color Palette.color.primary ]
{ url = entry.liveUrl
, label = Element.text entry.liveUrl
}
, Element.paragraph [ Element.Font.size 14 ]
[ Element.text "By "
, Element.newTabLink [ Element.Font.color Palette.color.primary ]
{ url = entry.authorUrl
, label = Element.text entry.authorName
}
]
, Element.row [ Element.width Element.fill ]
[ categoriesView entry.categories
, Element.row [ Element.alignRight ]
[ case entry.repoUrl of
Just repoUrl ->
Element.newTabLink []
{ url = repoUrl
, label = FontAwesome.icon "fas fa-code-branch"
}
Nothing ->
Element.none
]
]
]
categoriesView : List String -> Element.Element msg
categoriesView categories =
categories
|> List.map
(\category ->
Element.text category
)
|> Element.wrappedRow
[ Element.spacing 7
, Element.Font.size 14
, Element.Font.color (Element.rgba255 0 0 0 0.6)
, Element.width (Element.fillPortion 8)
]
type alias Entry =
{ screenshotUrl : String
, displayName : String
, liveUrl : String
, authorName : String
, authorUrl : String
, categories : List String
, repoUrl : Maybe String
}
decoder : Decode.Decoder (List Entry)
decoder =
Decode.field "records" <|
Decode.list entryDecoder
entryDecoder : Decode.Decoder Entry
entryDecoder =
Decode.field "fields" <|
Decode.map7 Entry
(Decode.field "Screenshot URL" Decode.string)
(Decode.field "Site Display Name" Decode.string)
(Decode.field "Live URL" Decode.string)
(Decode.field "Author" Decode.string)
(Decode.field "Author URL" Decode.string)
(Decode.field "Categories" (Decode.list Decode.string))
(Decode.maybe (Decode.field "Repository URL" Decode.string))
staticRequest : StaticHttp.Request (List Entry)
staticRequest =
StaticHttp.request
(Secrets.succeed
(\airtableToken ->
{ url = "https://api.airtable.com/v0/appDykQzbkQJAidjt/elm-pages%20showcase?maxRecords=100&view=Grid%202"
, method = "GET"
, headers = [ ( "Authorization", "Bearer " ++ airtableToken ), ( "view", "viwayJBsr63qRd7q3" ) ]
, body = StaticHttp.emptyBody
}
)
|> Secrets.with "AIRTABLE_TOKEN"
)
decoder
allCategroies : List String
allCategroies =
[ "Documentation"
, "eCommerce"
, "Conference"
, "Consulting"
, "Education"
, "Entertainment"
, "Event"
, "Food"
, "Freelance"
, "Gallery"
, "Landing Page"
, "Music"
, "Nonprofit"
, "Podcast"
, "Portfolio"
, "Programming"
, "Sports"
, "Travel"
, "Blog"
]

View File

@ -0,0 +1,143 @@
module Site exposing (config)
import Cloudinary
import Color
import Head
import Json.Encode
import MimeType
import Pages.ImagePath as ImagePath exposing (ImagePath)
import Pages.Manifest as Manifest
import Pages.Manifest.Category
import Pages.PagePath as PagePath
import Pages.StaticHttp as StaticHttp
import Route exposing (Route)
import SiteConfig exposing (SiteConfig)
import Sitemap
config : SiteConfig StaticData
config =
\routes ->
{ staticData = staticData
, canonicalUrl = \_ -> canonicalUrl
, manifest = manifest
, head = head
, generateFiles = generateFiles routes
}
-- TODO wire this in as part of the config
generateFiles :
List (Maybe Route)
->
StaticHttp.Request
(List
(Result
String
{ path : List String
, content : String
}
)
)
generateFiles allRoutes =
StaticHttp.succeed
[ siteMap allRoutes |> Ok
]
type alias StaticData =
{ siteName : String
}
staticData : StaticHttp.Request StaticData
staticData =
StaticHttp.map StaticData
--(StaticFile.request "site-name.txt" StaticFile.body)
(StaticHttp.succeed "site-name")
head : StaticData -> List Head.Tag
head static =
[ Head.icon [ ( 32, 32 ) ] MimeType.Png (cloudinaryIcon MimeType.Png 32)
, Head.icon [ ( 16, 16 ) ] MimeType.Png (cloudinaryIcon MimeType.Png 16)
, Head.appleTouchIcon (Just 180) (cloudinaryIcon MimeType.Png 180)
, Head.appleTouchIcon (Just 192) (cloudinaryIcon MimeType.Png 192)
, Head.sitemapLink "/sitemap.xml"
]
canonicalUrl : String
canonicalUrl =
"https://elm-pages.com"
manifest : StaticData -> Manifest.Config
manifest static =
{ backgroundColor = Just Color.white
, categories = [ Pages.Manifest.Category.education ]
, displayMode = Manifest.Standalone
, orientation = Manifest.Portrait
, description = "elm-pages - " ++ tagline
, iarcRatingId = Nothing
, name = static.siteName
, themeColor = Just Color.white
, startUrl = PagePath.build []
, shortName = Just "elm-pages"
, sourceIcon = ImagePath.build [ "images", "icon-png.png" ]
, icons =
[ icon webp 192
, icon webp 512
, icon MimeType.Png 192
, icon MimeType.Png 512
]
}
tagline : String
tagline =
"A statically typed site generator"
webp : MimeType.MimeImage
webp =
MimeType.OtherImage "webp"
icon :
MimeType.MimeImage
-> Int
-> Manifest.Icon
icon format width =
{ src = cloudinaryIcon format width
, sizes = [ ( width, width ) ]
, mimeType = format |> Just
, purposes = [ Manifest.IconPurposeAny, Manifest.IconPurposeMaskable ]
}
cloudinaryIcon :
MimeType.MimeImage
-> Int
-> ImagePath
cloudinaryIcon mimeType width =
Cloudinary.urlSquare "v1603234028/elm-pages/elm-pages-icon" (Just mimeType) width
siteMap :
List (Maybe Route)
-> { path : List String, content : String }
siteMap allRoutes =
allRoutes
|> List.filterMap identity
|> List.map
(\route ->
{ path = Route.routeToPath (Just route) |> String.join "/"
, lastMod = Nothing
}
)
|> Sitemap.build { siteUrl = "https://elm-pages.com" }
|> (\sitemapXmlString -> { path = [ "sitemap.xml" ], content = sitemapXmlString })

View File

@ -0,0 +1,8 @@
module SiteConfig exposing (SiteConfig)
import Pages.SiteConfig
import Route exposing (Route)
type alias SiteConfig staticData =
Pages.SiteConfig.SiteConfig (Maybe Route) staticData

View File

@ -0,0 +1,11 @@
module SiteOld exposing (canonicalUrl, tagline)
canonicalUrl : String
canonicalUrl =
"https://elm-pages.com"
tagline : String
tagline =
"A statically typed site generator - elm-pages"

View File

@ -0,0 +1,138 @@
module Template.Slide.Number_ exposing (Model, Msg, template)
import Document exposing (Document)
import Element exposing (Element)
import Head
import Head.Seo as Seo
import Markdown.Block
import Markdown.Parser
import Markdown.Renderer
import MarkdownRenderer
import OptimizedDecoder
import Pages.ImagePath as ImagePath
import Pages.StaticFile as StaticFile
import Pages.StaticHttp as StaticHttp
import Shared
import Template exposing (StaticPayload, Template, TemplateWithState)
type alias Model =
()
type alias Msg =
Never
type alias RouteParams =
{ number : String }
template : Template RouteParams StaticData
template =
Template.withStaticData
{ head = head
, staticRoutes = StaticHttp.succeed [ { number = "1" } ]
, staticData = staticData
}
|> Template.buildNoState { view = view }
staticData : RouteParams -> StaticHttp.Request StaticData
staticData route =
StaticFile.request
"slides.md"
(StaticFile.body
|> OptimizedDecoder.andThen
(\rawBody ->
case rawBody |> Markdown.Parser.parse of
Ok okBlocks ->
case
okBlocks
|> markdownIndexedByHeading (route.number |> String.toInt |> Maybe.withDefault 1)
|> Markdown.Renderer.render MarkdownRenderer.renderer
of
Ok renderedBody ->
OptimizedDecoder.succeed renderedBody
Err error ->
OptimizedDecoder.fail error
Err _ ->
OptimizedDecoder.fail ""
)
)
markdownIndexedByHeading :
Int
-> List Markdown.Block.Block
-> List Markdown.Block.Block
markdownIndexedByHeading index markdownBlocks =
Markdown.Block.foldl
(\block ( currentIndex, markdownToKeep ) ->
case block of
Markdown.Block.Heading Markdown.Block.H2 _ ->
let
newIndex =
currentIndex + 1
in
--_ ->
if newIndex == index then
( newIndex, block :: markdownToKeep )
else
( newIndex, markdownToKeep )
_ ->
if currentIndex == index then
( currentIndex, block :: markdownToKeep )
else
( currentIndex, markdownToKeep )
)
( 0, [] )
markdownBlocks
|> Tuple.second
|> List.reverse
head :
StaticPayload StaticData RouteParams
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = ImagePath.build [ "TODO" ]
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = "TODO"
, locale = Nothing
, title = "TODO title" -- metadata.title -- TODO
}
|> Seo.website
type alias StaticData =
List (Element Msg)
view :
StaticPayload StaticData RouteParams
-> Document Msg
view static =
{ title = "TODO title"
, body =
[ Element.column
[ Element.padding 40
]
[ Element.text static.routeParams.number
, Element.column []
static.static
]
]
}

View File

@ -0,0 +1,52 @@
module TemplateHardcoded exposing (..)
import Head
import Pages
import Pages.PagePath exposing (PagePath)
import Pages.StaticHttp as StaticHttp
import TemplateType
template :
{ staticData :
List ( PagePath, TemplateType.Metadata )
-> StaticHttp.Request staticData
, view :
List ( PagePath, TemplateType.Metadata )
-> staticData
-> model
-> metadata
-> renderedTemplate
-> view
, head :
staticData
-> PagePath
-> metadata
-> List (Head.Tag Pages.PathKey)
, init : metadata -> ( model, Cmd templateMsg )
, update : metadata -> templateMsg -> model -> ( model, Cmd templateMsg )
}
-> Template metadata renderedTemplate staticData model view templateMsg
template config =
config
type alias Template metadata renderedTemplate staticData model view templateMsg =
{ staticData :
List ( PagePath, TemplateType.Metadata )
-> StaticHttp.Request staticData
, view :
List ( PagePath, TemplateType.Metadata )
-> staticData
-> model
-> metadata
-> renderedTemplate
-> view
, head :
staticData
-> PagePath
-> metadata
-> List (Head.Tag Pages.PathKey)
, init : metadata -> ( model, Cmd templateMsg )
, update : metadata -> templateMsg -> model -> ( model, Cmd templateMsg )
}

View File

@ -0,0 +1,46 @@
backend:
name: git-gateway
media_folder: "examples/docs/images" # Folder where user uploaded files should go
public_folder: "examples/docs/images"
publish_mode: "editorial_workflow" # see https://www.netlifycms.org/docs/open-authoring/
collections: # A list of collections the CMS should be able to edit
- name: "post" # Used in routes, ie.: /admin/collections/:slug/edit
label: "Post" # Used in the UI, ie.: "New Post"
folder: "examples/docs/content/blog" # The path to the folder where the documents are stored
filter: {field: "type", value: "blog"}
create: true # Allow users to create new documents in this collection
fields: # The fields each document in this collection have
- { label: "Title", name: "title", widget: "string" }
- { label: "Publish Date", name: "published", widget: "date" }
- { label: "Intro Blurb", name: "description", widget: "text" }
- { label: "Image", name: "image", widget: "image", required: true }
- label: "Author"
name: "author"
widget: "select"
options: ["Dillon Kearns"]
default: "Dillon Kearns"
- { label: "Body", name: "body", widget: "markdown" }
- {
label: "Type",
name: "type",
widget: "hidden",
default: "blog",
required: false,
}
- name: "docs" # Used in routes, ie.: /admin/collections/:slug/edit
label: "Docs" # Used in the UI, ie.: "New Post"
folder: "examples/docs/content/docs" # The path to the folder where the documents are stored
filter: {field: "type", value: "doc"}
create: true # Allow users to create new documents in this collection
fields: # The fields each document in this collection have
- { label: "Title", name: "title", widget: "string" }
- { label: "Body", name: "body", widget: "markdown" }
- {
label: "Type",
name: "type",
widget: "hidden",
default: "doc",
required: false,
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Content Manager</title>
</head>
<body>
<script src="https://identity-js.netlify.com/v1/netlify-identity-widget.js"></script>
<!-- Include the script that builds the page and powers Netlify CMS -->
<script src="https://unpkg.com/netlify-cms@^2.0.0/dist/netlify-cms.js"></script>
</body>
</html>

View File

@ -0,0 +1,149 @@
export function setup() {
customElements.define(
"oembed-element",
class extends HTMLElement {
connectedCallback() {
let shadow = this.attachShadow({ mode: "closed" });
const urlAttr = this.getAttribute("url");
if (urlAttr) {
renderOembed(shadow, urlAttr, {
maxwidth: this.getAttribute("maxwidth"),
maxheight: this.getAttribute("maxheight"),
});
} else {
const discoverUrl = this.getAttribute("discover-url");
if (discoverUrl) {
getDiscoverUrl(discoverUrl, function (discoveredUrl) {
if (discoveredUrl) {
renderOembed(shadow, discoveredUrl, null);
}
});
}
}
}
}
);
/**
*
* @param {ShadowRoot} shadow
* @param {string} urlToEmbed
* @param {{maxwidth: string?; maxheight: string?}?} options
*/
function renderOembed(shadow, urlToEmbed, options) {
let apiUrlBuilder = new URL(
`https://cors-anywhere.herokuapp.com/${urlToEmbed}`
);
if (options && options.maxwidth) {
apiUrlBuilder.searchParams.set("maxwidth", options.maxwidth);
}
if (options && options.maxheight) {
apiUrlBuilder.searchParams.set("maxheight", options.maxheight);
}
const apiUrl = apiUrlBuilder.toString();
httpGetAsync(apiUrl, (rawResponse) => {
const response = JSON.parse(rawResponse);
switch (response.type) {
case "rich":
tryRenderingHtml(shadow, response);
break;
case "video":
tryRenderingHtml(shadow, response);
break;
case "photo":
let img = document.createElement("img");
img.setAttribute("src", response.url);
if (options) {
img.setAttribute(
"style",
`max-width: ${options.maxwidth}px; max-height: ${options.maxheight}px;`
);
}
shadow.appendChild(img);
break;
default:
break;
}
});
}
/**
* @param {{
height: ?number;
width: ?number;
html: any;
}} response
* @param {ShadowRoot} shadow
*/
function tryRenderingHtml(shadow, response) {
if (response && typeof response.html) {
let iframe = createIframe(response);
shadow.appendChild(iframe);
setTimeout(() => {
let refetchedIframe = shadow.querySelector("iframe");
if (refetchedIframe && !response.height) {
refetchedIframe.setAttribute(
"height",
// @ts-ignore
(iframe.contentWindow.document.body.scrollHeight + 10).toString()
);
}
if (refetchedIframe && !response.width) {
refetchedIframe.setAttribute(
"width",
// @ts-ignore
(iframe.contentWindow.document.body.scrollWidth + 10).toString()
);
}
}, 1000);
}
}
/**
* @param {{ height: number?; width: number?; html: string; }} response
* @returns {HTMLIFrameElement}
*/
function createIframe(response) {
let iframe = document.createElement("iframe");
iframe.setAttribute("border", "0");
iframe.setAttribute("frameborder", "0");
iframe.setAttribute("height", ((response.height || 500) + 20).toString());
iframe.setAttribute("width", ((response.width || 500) + 20).toString());
iframe.setAttribute("style", "max-width: 100%;");
iframe.srcdoc = response.html;
return iframe;
}
/**
* @param {string} url
* @param {{ (discoveredUrl: string?): void;}} callback
*/
function getDiscoverUrl(url, callback) {
let apiUrl = new URL(
`https://cors-anywhere.herokuapp.com/${url}`
).toString();
httpGetAsync(apiUrl, function (response) {
let dom = document.createElement("html");
dom.innerHTML = response;
/** @type {HTMLLinkElement | null} */ const oembedTag = dom.querySelector(
'link[type="application/json+oembed"]'
);
callback(oembedTag && oembedTag.href);
});
}
/**
* @param {string} theUrl
* @param {{ (rawResponse: string): void }} callback
*/
function httpGetAsync(theUrl, callback) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
callback(xmlHttp.responseText);
};
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.send(null);
}
}

View File

@ -0,0 +1,553 @@
console.log("Loaded HMR");
var eventSource = null;
/** @type {Promise<() => void>} */
let updateAppContentJson = new Promise((resolve, reject) => resolve(() => {}));
function connect(sendContentJsonPort) {
// Listen for the server to tell us that an HMR update is available
eventSource = new EventSource("stream");
eventSource.onmessage = async function (evt) {
showCompiling("");
if (evt.data === "content.json") {
const elmJsRequest = elmJsFetch();
const fetchContentJson = fetchContentJsonForCurrentPage();
updateAppContentJson = updateContentJsonWith(
fetchContentJson,
sendContentJsonPort
);
try {
await fetchContentJson;
const elmJsResponse = await elmJsRequest;
thenApplyHmr(elmJsResponse);
} catch (errorJson) {
console.log({ errorJson });
showError({
type: "compile-errors",
errors: errorJson,
});
}
} else {
elmJsFetch().then(thenApplyHmr);
}
};
}
/**
*
* @param {*} fetchContentJsonPromise
* @param {*} sendContentJsonPort
* @returns {Promise<() => void>}
*/
async function updateContentJsonWith(
fetchContentJsonPromise,
sendContentJsonPort
) {
return new Promise(async (resolve, reject) => {
try {
const newContentJson = await fetchContentJsonPromise;
hideError();
resolve(() => {
sendContentJsonPort(newContentJson);
hideCompiling("fast");
});
} catch (errorJson) {
showError({
type: "compile-errors",
errors: errorJson,
});
}
});
}
function fetchContentJsonForCurrentPage() {
return new Promise(async (resolve, reject) => {
let currentPath = window.location.pathname.replace(/(\w)$/, "$1/");
const contentJsonForPage = await fetch(
`${window.location.origin}${currentPath}content.json`
);
if (contentJsonForPage.ok) {
resolve(await contentJsonForPage.json());
} else {
try {
reject(await contentJsonForPage.json());
} catch (error) {
resolve(null);
}
}
});
}
// Expose the Webpack HMR API
// var myDisposeCallback = null;
var myDisposeCallback = function () {
console.log("dispose...");
};
// simulate the HMR api exposed by webpack
var module = {
hot: {
accept: async function () {
const sendInUpdatedContentJson = await updateAppContentJson;
sendInUpdatedContentJson();
},
dispose: function (callback) {
myDisposeCallback = callback;
},
data: null,
apply: function () {
var newData = {};
myDisposeCallback(newData);
module.hot.data = newData;
},
verbose: true,
},
};
// Thanks to the elm-live maintainers and contributors for this code for rendering errors as an HTML overlay
// https://github.com/wking-io/elm-live/blob/e317b4914c471addea7243c47f28dcebe27a5d36/lib/src/websocket.js
const pipe = (...fns) => (x) => fns.reduce((y, f) => f(y), x);
function elmJsFetch() {
var elmJsRequest = new Request("/elm.js");
elmJsRequest.cache = "no-cache";
return fetch(elmJsRequest);
}
async function waitFor(millis) {
return new Promise((resolve) => {
setTimeout(resolve, millis);
});
}
async function thenApplyHmr(response) {
if (response.ok) {
response.text().then(function (value) {
module.hot.apply();
delete Elm;
eval(value);
});
} else {
try {
const errorJson = await response.json();
console.error("JSON", errorJson);
showError(errorJson);
} catch (jsonParsingError) {
console.log("Couldn't parse error", jsonParsingError);
}
}
}
function colorConverter(color) {
return {
black: "#000000",
red: "#F77F00",
green: "#33ff00",
yellow: "#ffff00",
blue: "#99B1BC",
magenta: "#cc00ff",
cyan: "#00ffff",
white: "#d0d0d0",
BLACK: "#808080",
RED: "#ff0000",
GREEN: "#33ff00",
YELLOW: "#ffff00",
BLUE: "#0066ff",
MAGENTA: "#cc00ff",
CYAN: "#00ffff",
WHITE: "#ffffff",
}[color];
}
const addNewLine = (str) => str + "\n";
const styleColor = (str = "WHITE") => `color: ${colorConverter(str)};`;
const styleUnderline = `text-decoration: underline;`;
const styleBold = `text-decoration: bold;`;
const parseStyle = ({ underline, color, bold }) =>
`${underline ? styleUnderline : ""}${color ? styleColor(color) : ""}${
bold ? styleBold : ""
}`;
function capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function consoleSanitize(str) {
return str.replace(/<(http[^>]*)>/, "$1");
}
function htmlSanitize(str, type) {
var temp = document.createElement("div");
temp.textContent = str;
return temp.innerHTML.replace(
/&lt;(http[^>]*)&gt;/,
"&lt;<a style='color: inherit' target='_blank' href='$1'>$1</a>&gt;"
);
}
const parseHeader = (title, path) =>
`-- ${title.replace("-", " ")} --------------- ${path}`;
/*
|-------------------------------------------------------------------------------
| Console Logging
|-------------------------------------------------------------------------------
*/
const wrapConsole = (str) => `%c${str}`;
const consoleHeader = pipe(parseHeader, wrapConsole, addNewLine, addNewLine);
const parseMsg = pipe(consoleSanitize, wrapConsole);
const consoleMsg = ({ error, style }, msg) => ({
error: error.concat(parseMsg(typeof msg === "string" ? msg : msg.string)),
style: style.concat(
parseStyle(typeof msg === "string" ? { color: "black" } : msg)
),
});
const joinMessage = ({ error, style }) => [error.join("")].concat(style);
const parseConsoleErrors = (path) => ({ title, message }) =>
joinMessage(
message.reduce(consoleMsg, {
error: [consoleHeader(title, path)],
style: [styleColor("blue")],
})
);
const restoreColorConsole = ({ errors }) =>
errors.reduce(
(acc, { problems, path }) =>
acc.concat(problems.map(parseConsoleErrors(path))),
[]
);
/*
|-------------------------------------------------------------------------------
| Html Logging
|-------------------------------------------------------------------------------
*/
const htmlHeader = (title, path) =>
`<span style="${parseStyle({ color: "blue" })}">${parseHeader(
title,
path
)}</span>\n\n`;
const htmlMsg = (acc, msg) =>
`${acc}<span style="${parseStyle(
typeof msg === "string" ? { color: "WHITE" } : msg
)}">${htmlSanitize(typeof msg === "string" ? msg : msg.string)}</span>`;
const parseHtmlErrors = (path) => ({ title, message }) =>
message.reduce(htmlMsg, htmlHeader(title, path));
const restoreColorHtml = ({ errors }) =>
errors.reduce(
(acc, { problems, path }) =>
acc.concat(problems.map(parseHtmlErrors(path))),
[]
);
/*
|-------------------------------------------------------------------------------
| TODO: Refactor Below
|-------------------------------------------------------------------------------
*/
var speed = 400;
var delay = 20;
function showError(error) {
restoreColorConsole(error).forEach((error) => {
console.log.apply(this, error);
});
hideCompiling("fast");
setTimeout(function () {
showError_(restoreColorHtml(error));
}, delay);
}
function showError_(error) {
var nodeContainer = document.getElementById("elm-live:elmErrorContainer");
if (!nodeContainer) {
nodeContainer = document.createElement("div");
nodeContainer.id = "elm-live:elmErrorContainer";
document.body.appendChild(nodeContainer);
}
nodeContainer.innerHTML = `
<div
id="elm-live:elmErrorBackground"
style="
z-index: 100;
perspective: 500px;
transition: opacity 400ms;
position: fixed;
top: 0;
left: 0;
background-color: rgba(13,31,45,0.2);
width: 100%;
height: 100%;
display: flex;
justify-content:center;
align-items: center;
"
>
<div
onclick="elmLive.hideError()"
style="
background-color: rgba(0,0,0,0);
position: fixed;
top:0;
left:0;
bottom:0;
right:0
"
></div>
<pre
id="elm-live:elmError"
style="
white-space: pre-wrap;
transform: rotateX(0deg);
transition: transform 400ms;
transform-style: preserve-3d;
font-size: 16px;
overflow: scroll;
background-color: rgba(13, 31, 45, 0.9);
color: #ddd;
width: calc(100% - 150px);
height: calc(100% - 150px);
margin: 0;
padding: 30px;
font-family: 'Fira Mono', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
"
>${error}</pre>
</div>
`;
setTimeout(function () {
document.getElementById("elm-live:elmErrorBackground").style.opacity = 1;
document.getElementById("elm-live:elmError").style.transform =
"rotateX(0deg)";
}, delay);
}
function hideError(velocity) {
var node = document.getElementById("elm-live:elmErrorContainer");
if (node) {
if (velocity === "fast") {
document.getElementById("elm-live:elmErrorContainer").remove();
} else {
document.getElementById("elm-live:elmErrorBackground").style.opacity = 0;
document.getElementById("elm-live:elmError").style.transform =
"rotateX(90deg)";
setTimeout(function () {
document.getElementById("elm-live:elmErrorContainer").remove();
}, speed);
}
}
}
function showCompiling(message) {
hideError("fast");
setTimeout(function () {
showCompiling_(message);
}, delay);
}
function showCompiling_(message) {
var nodeContainer = document.getElementById("__elm-pages-loading");
if (!nodeContainer) {
nodeContainer = document.createElement("div");
nodeContainer.id = "__elm-pages-loading";
nodeContainer.class = "lds-default";
nodeContainer.style = `
position: fixed;
bottom: 10px;
right: 110px;
width: 80px;
height: 80px;
background-color: white;
display: block;
box-shadow: rgba(0, 0, 0, 0.25) 0px 8px 15px 0px,
rgba(0, 0, 0, 0.12) 0px 2px 10px 0px;
`;
document.body.appendChild(nodeContainer);
}
nodeContainer.innerHTML = `
<div
style="
animation: 1.2s linear 0s infinite normal none running lds-default;
background: rgb(0, 0, 0);
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
top: 37px;
left: 66px;
"
></div>
<div
style="
animation: 1.2s linear -0.1s infinite normal none running lds-default;
background: rgb(0, 0, 0);
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
top: 22px;
left: 62px;
"
></div>
<div
style="
animation: 1.2s linear -0.2s infinite normal none running lds-default;
background: rgb(0, 0, 0);
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
top: 11px;
left: 52px;
"
></div>
<div
style="
animation: 1.2s linear -0.3s infinite normal none running lds-default;
background: rgb(0, 0, 0);
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
top: 7px;
left: 37px;
"
></div>
<div
style="
animation: 1.2s linear -0.4s infinite normal none running lds-default;
background: rgb(0, 0, 0);
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
top: 11px;
left: 22px;
"
></div>
<div
style="
animation: 1.2s linear -0.5s infinite normal none running lds-default;
background: rgb(0, 0, 0);
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
top: 22px;
left: 11px;
"
></div>
<div
style="
animation: 1.2s linear -0.6s infinite normal none running lds-default;
background: rgb(0, 0, 0);
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
top: 37px;
left: 7px;
"
></div>
<div
style="
animation: 1.2s linear -0.7s infinite normal none running lds-default;
background: rgb(0, 0, 0);
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
top: 52px;
left: 11px;
"
></div>
<div
style="
animation: 1.2s linear -0.8s infinite normal none running lds-default;
background: rgb(0, 0, 0);
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
top: 62px;
left: 22px;
"
></div>
<div
style="
animation: 1.2s linear -0.9s infinite normal none running lds-default;
background: rgb(0, 0, 0);
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
top: 66px;
left: 37px;
"
></div>
<div
style="
animation: 1.2s linear -1s infinite normal none running lds-default;
background: rgb(0, 0, 0);
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
top: 62px;
left: 52px;
"
></div>
<div
style="
animation: 1.2s linear -1.1s infinite normal none running lds-default;
background: rgb(0, 0, 0);
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
top: 52px;
left: 62px;
"
></div>
`;
setTimeout(function () {
document.getElementById("__elm-pages-loading").style.opacity = 1;
}, delay);
}
function hideCompiling(velocity) {
const node = document.getElementById("__elm-pages-loading");
if (node) {
if (velocity === "fast") {
node.remove();
} else {
document.getElementById("__elm-pages-loading").style.opacity = 0;
setTimeout(function () {
node.remove();
}, speed);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,8 @@
import * as elmOembed from "/elm-oembed.js";
// import "./lib/native-shim.js";
export default function (elmLoaded) {
document.addEventListener("DOMContentLoaded", function (event) {
elmOembed.setup();
});
}

View File

@ -0,0 +1,41 @@
@import "/syntax.css";
@import url("https://fonts.googleapis.com/css?family=Montserrat:400,700|Roboto|Roboto+Mono&display=swap");
@import url("https://use.fontawesome.com/releases/v5.9.0/css/all.css");
.dotted-line {
-webkit-animation: animation-yweh2o 400ms linear infinite;
animation: animation-yweh2o 400ms linear infinite;
}
@-webkit-keyframes animation-yweh2o {
to {
stroke-dashoffset: 10;
}
}
@keyframes animation-yweh2o {
to {
stroke-dashoffset: 10;
}
}
.avatar img {
border-radius: 50%;
}
@media all and (max-width: 600px) {
.navbar-title {
font-size: 20px !important;
}
.navbar-title svg {
width: 20px;
}
}
@media (max-width: 600px) {
.responsive-desktop {
display: none !important;
}
}
@media (min-width: 600px) {
.responsive-mobile {
display: none !important;
}
}

View File

@ -0,0 +1,43 @@
pre.elmsh {
padding: 10px;
margin: 0;
text-align: left;
overflow: auto;
padding: 20px !important;
}
code.elmsh {
padding: 0;
}
code {
font-family: 'Roboto Mono' !important;
font-size: 20px !important;
line-height: 28px;
}
.elmsh-line:before {
/* content: attr(data-elmsh-lc); */
display: inline-block;
text-align: right;
width: 40px;
padding: 0 20px 0 0;
opacity: 0.3;
}
.elmsh {
color: #f8f8f2;
background: #000;
}
.elmsh-hl {background: #343434;}
.elmsh-add {background: #003800;}
.elmsh-del {background: #380000;}
.elmsh-comm {color: #75715e;}
.elmsh1 {color: #ae81ff;}
.elmsh2 {color: #e6db74;}
.elmsh3 {color: #66d9ef;}
.elmsh4 {color: #f92672;}
.elmsh5 {color: #a6e22e;}
.elmsh6 {color: #ae81ff;}
.elmsh7 {color: #fd971f;}