Merge branch 'master' into query-and-fragment

This commit is contained in:
Dillon Kearns 2020-01-25 18:47:38 -08:00
commit c7d0ddd8e6
30 changed files with 1110 additions and 318 deletions

View File

@ -9,6 +9,27 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased] ## [Unreleased]
## [2.0.0] - 2020-01-25
### Added
- There's a new `generateFiles` endpoint. You pass in a function that takes a page's path,
page metadata, and page body, and that returns a list representing the files to generate.
You can see a working example for elm-pages.com, here's the [entry point](https://github.com/dillonkearns/elm-pages/blob/master/examples/docs/src/Main.elm#L76-L92), and here's where it
[generates the RSS feed](https://github.com/dillonkearns/elm-pages/blob/master/examples/docs/src/Feed.elm).
You can pass in a no-op function like `\pages -> []` to not generate any files.
## [1.1.3] - 2020-01-23
### Fixed
- Fix missing content flash (that was partially fixed with [#48](https://github.com/dillonkearns/elm-pages/pull/48)) for
some cases where paths weren't normalized correctly.
## [1.1.2] - 2020-01-20
### Fixed
- "Missing content" message no longer flashes between pre-rendered HTML and the Elm app hydrating and taking over the page. See [#48](https://github.com/dillonkearns/elm-pages/pull/48).
## [1.1.1] - 2020-01-04 ## [1.1.1] - 2020-01-04
### Fixed ### Fixed

View File

@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased] ## [Unreleased]
## [1.2.0] - 2020-01-20
### Changed
- Changed the CLI generator to expect code from the new Elm package from the new
`generateFiles` hook in `Pages.Platform.application`.
## [1.1.8] - 2020-01-20
### Fixed
- "Missing content" message no longer flashes between pre-rendered HTML and the Elm app hydrating and taking over the page. See [#48](https://github.com/dillonkearns/elm-pages/pull/48).
## [1.1.7] - 2020-01-12
### Fixed
- Newlines and escaped double quotes (`"`s) are handled properly in content frontmatter now. See [#41](https://github.com/dillonkearns/elm-pages/pull/41). Thank you [Luke](https://github.com/lukewestby)! 🎉🙏
## [1.1.6] - 2020-01-04 ## [1.1.6] - 2020-01-04
### Added ### Added

View File

@ -3,7 +3,7 @@
"name": "dillonkearns/elm-pages", "name": "dillonkearns/elm-pages",
"summary": "A statically typed site generator.", "summary": "A statically typed site generator.",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"version": "1.1.1", "version": "2.0.0",
"exposed-modules": [ "exposed-modules": [
"Head", "Head",
"Head.Seo", "Head.Seo",

View File

@ -73,7 +73,7 @@ Here are some links:
And here's the output: And here's the output:
<ellie-output id="6RCVwj43wQfa1" /> <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! 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!
@ -148,7 +148,7 @@ Exposing the AST allows for a number of powerful use cases as well. And it does
Here are some use cases that this feature enables: 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/6QtYW8pcCDna1)) - 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) - 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 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.) - Transform the AST before rendering it, for example dropping each heading down one level (H1s become H2s, etc.)

View File

@ -0,0 +1,4 @@
---
title: elm-pages sites showcase
type: showcase
---

View File

@ -9,8 +9,12 @@
"dependencies": { "dependencies": {
"direct": { "direct": {
"avh4/elm-color": "1.0.0", "avh4/elm-color": "1.0.0",
"billstclair/elm-xml-eeue56": "1.0.1",
"dillonkearns/elm-markdown": "1.1.3", "dillonkearns/elm-markdown": "1.1.3",
"dillonkearns/elm-oembed": "1.0.0", "dillonkearns/elm-oembed": "1.0.0",
"dillonkearns/elm-rss": "1.0.0",
"dillonkearns/elm-sitemap": "1.0.0",
"dmy/elm-imf-date-time": "1.0.1",
"elm/browser": "1.0.2", "elm/browser": "1.0.2",
"elm/core": "1.0.2", "elm/core": "1.0.2",
"elm/html": "1.0.0", "elm/html": "1.0.0",
@ -42,7 +46,10 @@
"elm/regex": "1.0.0", "elm/regex": "1.0.0",
"elm/virtual-dom": "1.0.2", "elm/virtual-dom": "1.0.2",
"fredcy/elm-parseint": "2.0.1", "fredcy/elm-parseint": "2.0.1",
"mgold/elm-nonempty-list": "4.0.2" "justinmimbs/time-extra": "1.1.0",
"lazamar/dict-parser": "1.0.2",
"mgold/elm-nonempty-list": "4.0.2",
"ryannhg/date-format": "2.3.0"
} }
}, },
"test-dependencies": { "test-dependencies": {

View File

@ -4,8 +4,8 @@ import Element exposing (Element)
import Element.Border as Border import Element.Border as Border
import Element.Font import Element.Font
import Metadata exposing (Metadata) import Metadata exposing (Metadata)
import Pages.PagePath as PagePath exposing (PagePath)
import Pages import Pages
import Pages.PagePath as PagePath exposing (PagePath)
import Palette import Palette
@ -25,19 +25,10 @@ view currentPage posts =
|> List.filterMap |> List.filterMap
(\( path, metadata ) -> (\( path, metadata ) ->
case metadata of case metadata of
Metadata.Page meta ->
Nothing
Metadata.Article meta ->
Nothing
Metadata.Author _ ->
Nothing
Metadata.Doc meta -> Metadata.Doc meta ->
Just ( currentPage == path, path, meta ) Just ( currentPage == path, path, meta )
Metadata.BlogIndex -> _ ->
Nothing Nothing
) )
|> List.map postSummary |> List.map postSummary

View File

@ -0,0 +1,72 @@
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 Pages.PathKey
, 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 Pages.PathKey
, 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 Pages.PathKey
, frontmatter : Metadata
, body : String
}
-> Maybe Rss.Item
metadataToRssItem page =
case page.frontmatter of
Article article ->
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

@ -20,15 +20,6 @@ view posts =
|> List.filterMap |> List.filterMap
(\( path, metadata ) -> (\( path, metadata ) ->
case metadata of case metadata of
Metadata.Page meta ->
Nothing
Metadata.Doc meta ->
Nothing
Metadata.Author _ ->
Nothing
Metadata.Article meta -> Metadata.Article meta ->
if meta.draft then if meta.draft then
Nothing Nothing
@ -36,7 +27,7 @@ view posts =
else else
Just ( path, meta ) Just ( path, meta )
Metadata.BlogIndex -> _ ->
Nothing Nothing
) )
|> List.sortBy |> List.sortBy

View File

@ -8,8 +8,11 @@ import DocumentSvg
import Element exposing (Element) import Element exposing (Element)
import Element.Background import Element.Background
import Element.Border import Element.Border
import Element.Events
import Element.Font as Font import Element.Font as Font
import Element.Region import Element.Region
import Feed
import FontAwesome
import Head import Head
import Head.Seo as Seo import Head.Seo as Seo
import Html exposing (Html) import Html exposing (Html)
@ -19,6 +22,7 @@ import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Exploration as D import Json.Decode.Exploration as D
import MarkdownRenderer import MarkdownRenderer
import Metadata exposing (Metadata) import Metadata exposing (Metadata)
import MySitemap
import Pages exposing (images, pages) import Pages exposing (images, pages)
import Pages.Directory as Directory exposing (Directory) import Pages.Directory as Directory exposing (Directory)
import Pages.Document import Pages.Document
@ -30,6 +34,7 @@ import Pages.Platform exposing (Page)
import Pages.StaticHttp as StaticHttp import Pages.StaticHttp as StaticHttp
import Palette import Palette
import Secrets import Secrets
import Showcase
manifest : Manifest.Config Pages.PathKey manifest : Manifest.Config Pages.PathKey
@ -62,11 +67,31 @@ main =
, documents = [ markdownDocument ] , documents = [ markdownDocument ]
, manifest = manifest , manifest = manifest
, canonicalSiteUrl = canonicalSiteUrl , canonicalSiteUrl = canonicalSiteUrl
, generateFiles = generateFiles
, onPageChange = OnPageChange , onPageChange = OnPageChange
, internals = Pages.internals , internals = Pages.internals
} }
generateFiles :
List
{ path : PagePath Pages.PathKey
, frontmatter : Metadata
, body : String
}
->
List
(Result String
{ path : List String
, content : String
}
)
generateFiles siteMetadata =
[ Feed.fileToGenerate { siteTagline = siteTagline, siteUrl = canonicalSiteUrl } siteMetadata |> Ok
, MySitemap.build { siteUrl = canonicalSiteUrl } siteMetadata |> Ok
]
markdownDocument : ( String, Pages.Document.DocumentHandler Metadata ( MarkdownRenderer.TableOfContents, List (Element Msg) ) ) markdownDocument : ( String, Pages.Document.DocumentHandler Metadata ( MarkdownRenderer.TableOfContents, List (Element Msg) ) )
markdownDocument = markdownDocument =
Pages.Document.parser Pages.Document.parser
@ -77,7 +102,8 @@ markdownDocument =
type alias Model = type alias Model =
{} { showMobileMenu : Bool
}
init : init :
@ -88,7 +114,7 @@ init :
} }
-> ( Model, Cmd Msg ) -> ( Model, Cmd Msg )
init maybePagePath = init maybePagePath =
( Model, Cmd.none ) ( Model False, Cmd.none )
type Msg type Msg
@ -97,13 +123,17 @@ type Msg
, query : Maybe String , query : Maybe String
, fragment : Maybe String , fragment : Maybe String
} }
| ToggleMobileMenu
update : Msg -> Model -> ( Model, Cmd Msg ) update : Msg -> Model -> ( Model, Cmd Msg )
update msg model = update msg model =
case msg of case msg of
OnPageChange page -> OnPageChange page ->
( model, Cmd.none ) ( { model | showMobileMenu = False }, Cmd.none )
ToggleMobileMenu ->
( { model | showMobileMenu = not model.showMobileMenu }, Cmd.none )
subscriptions : Model -> Sub Msg subscriptions : Model -> Sub Msg
@ -123,17 +153,39 @@ view :
, head : List (Head.Tag Pages.PathKey) , head : List (Head.Tag Pages.PathKey)
} }
view siteMetadata page = view siteMetadata page =
StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages") case page.frontmatter of
(D.field "stargazers_count" D.int) Metadata.Showcase ->
|> StaticHttp.map StaticHttp.map2
(\stars -> (\stars showcaseData ->
{ view = { view =
\model viewForPage -> \model viewForPage ->
pageView stars model siteMetadata page viewForPage { title = "elm-pages blog"
|> wrapBody , body =
, head = head page.frontmatter Element.column [ Element.width Element.fill ]
} [ Element.column [ Element.padding 20, Element.centerX ] [ Showcase.view showcaseData ]
) ]
}
|> wrapBody stars page model
, head = head page.frontmatter
}
)
(StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
(D.field "stargazers_count" D.int)
)
Showcase.staticRequest
_ ->
StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
(D.field "stargazers_count" D.int)
|> StaticHttp.map
(\stars ->
{ view =
\model viewForPage ->
pageView stars model siteMetadata page viewForPage
|> wrapBody stars page model
, head = head page.frontmatter
}
)
@ -186,8 +238,7 @@ pageView stars model siteMetadata page viewForPage =
Metadata.Page metadata -> Metadata.Page metadata ->
{ title = metadata.title { title = metadata.title
, body = , body =
[ header stars page.path [ Element.column
, Element.column
[ Element.padding 50 [ Element.padding 50
, Element.spacing 60 , Element.spacing 60
, Element.Region.mainContent , Element.Region.mainContent
@ -203,8 +254,7 @@ pageView stars model siteMetadata page viewForPage =
{ title = metadata.title { title = metadata.title
, body = , body =
Element.column [ Element.width Element.fill ] Element.column [ Element.width Element.fill ]
[ header stars page.path [ Element.column
, Element.column
[ Element.padding 30 [ Element.padding 30
, Element.spacing 40 , Element.spacing 40
, Element.Region.mainContent , Element.Region.mainContent
@ -234,8 +284,7 @@ pageView stars model siteMetadata page viewForPage =
Metadata.Doc metadata -> Metadata.Doc metadata ->
{ title = metadata.title { title = metadata.title
, body = , body =
[ header stars page.path [ Element.row []
, Element.row []
[ DocSidebar.view page.path siteMetadata [ DocSidebar.view page.path siteMetadata
|> Element.el [ Element.width (Element.fillPortion 2), Element.alignTop, Element.height Element.fill ] |> 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 ] , Element.column [ Element.width (Element.fillPortion 8), Element.padding 35, Element.spacing 15 ]
@ -264,8 +313,7 @@ pageView stars model siteMetadata page viewForPage =
Element.column Element.column
[ Element.width Element.fill [ Element.width Element.fill
] ]
[ header stars page.path [ Element.column
, Element.column
[ Element.padding 30 [ Element.padding 30
, Element.spacing 20 , Element.spacing 20
, Element.Region.mainContent , Element.Region.mainContent
@ -283,15 +331,41 @@ pageView stars model siteMetadata page viewForPage =
{ title = "elm-pages blog" { title = "elm-pages blog"
, body = , body =
Element.column [ Element.width Element.fill ] Element.column [ Element.width Element.fill ]
[ header stars page.path [ Element.column [ Element.padding 20, Element.centerX ] [ Index.view siteMetadata ]
, Element.column [ Element.padding 20, Element.centerX ] [ Index.view siteMetadata ] ]
}
Metadata.Showcase ->
{ title = "elm-pages blog"
, body =
Element.column [ Element.width Element.fill ]
[--, Element.column [ Element.padding 20, Element.centerX ] [ Showcase.view siteMetadata ]
] ]
} }
wrapBody record = wrapBody : Int -> { a | path : PagePath Pages.PathKey } -> Model -> { c | body : Element Msg, title : String } -> { body : Html Msg, title : String }
wrapBody stars page model record =
{ body = { body =
record.body (if model.showMobileMenu then
Element.column
[ Element.width Element.fill
, Element.padding 20
]
[ Element.row [ Element.width Element.fill, Element.spaceEvenly ]
[ logoLinkMobile
, FontAwesome.styledIcon "fas fa-bars" [ Element.Events.onClick ToggleMobileMenu ]
]
, Element.column [ Element.centerX, Element.spacing 20 ]
(navbarLinks stars page.path)
]
else
Element.column [ Element.width Element.fill ]
[ header stars page.path
, record.body
]
)
|> Element.layout |> Element.layout
[ Element.width Element.fill [ Element.width Element.fill
, Font.size 20 , Font.size 20
@ -310,51 +384,92 @@ articleImageView articleImage =
} }
header : Int -> PagePath Pages.PathKey -> Element msg header : Int -> PagePath Pages.PathKey -> Element Msg
header stars currentPath = header stars currentPath =
Element.column [ Element.width Element.fill ] Element.column [ Element.width Element.fill ]
[ Element.el [ responsiveHeader
[ Element.height (Element.px 4) , Element.column
, Element.width Element.fill [ Element.width Element.fill
, Element.Background.gradient , Element.htmlAttribute (Attr.class "responsive-desktop")
{ angle = 0.2
, steps =
[ Element.rgb255 0 242 96
, Element.rgb255 5 117 230
]
}
] ]
Element.none [ Element.el
, Element.row [ Element.height (Element.px 4)
[ Element.paddingXY 25 4 , Element.width Element.fill
, Element.spaceEvenly , Element.Background.gradient
, Element.width Element.fill { angle = 0.2
, Element.Region.navigation , steps =
, Element.Border.widthEach { bottom = 1, left = 0, right = 0, top = 0 } [ Element.rgb255 0 242 96
, Element.Border.color (Element.rgba255 40 80 40 0.4) , Element.rgb255 5 117 230
]
[ Element.link []
{ url = "/"
, label =
Element.row
[ Font.size 30
, Element.spacing 16
, Element.htmlAttribute (Attr.id "navbar-title")
] ]
[ DocumentSvg.view }
, Element.text "elm-pages" ]
] Element.none
} , Element.row
, Element.row [ Element.spacing 15 ] [ Element.paddingXY 25 4
[ elmDocsLink , Element.spaceEvenly
, githubRepoLink stars , Element.width Element.fill
, highlightableLink currentPath pages.docs.directory "Docs" , Element.Region.navigation
, highlightableLink currentPath pages.blog.directory "Blog" , 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.id "navbar-title")
]
[ DocumentSvg.view
, Element.text "elm-pages"
]
}
logoLinkMobile =
Element.link []
{ url = "/"
, label =
Element.row
[ Font.size 30
, Element.spacing 16
, Element.htmlAttribute (Attr.id "navbar-title")
]
[ Element.text "elm-pages"
]
}
navbarLinks stars currentPath =
[ elmDocsLink
, githubRepoLink stars
, highlightableLink currentPath pages.docs.directory "Docs"
, highlightableLink currentPath pages.showcase.directory "Showcase"
, highlightableLink currentPath pages.blog.directory "Blog"
]
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 ]
]
highlightableLink : highlightableLink :
PagePath Pages.PathKey PagePath Pages.PathKey
-> Directory Pages.PathKey Directory.WithIndex -> Directory Pages.PathKey Directory.WithIndex
@ -379,6 +494,13 @@ highlightableLink currentPath linkDirectory displayName =
} }
commonHeadTags : List (Head.Tag Pages.PathKey)
commonHeadTags =
[ Head.rssLink "/blog/feed.xml"
, Head.sitemapLink "/sitemap.xml"
]
{-| <https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards> {-| <https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards>
<https://htmlhead.dev> <https://htmlhead.dev>
<https://html.spec.whatwg.org/multipage/semantics.html#standard-metadata-names> <https://html.spec.whatwg.org/multipage/semantics.html#standard-metadata-names>
@ -386,111 +508,129 @@ highlightableLink currentPath linkDirectory displayName =
-} -}
head : Metadata -> List (Head.Tag Pages.PathKey) head : Metadata -> List (Head.Tag Pages.PathKey)
head metadata = head metadata =
case metadata of commonHeadTags
Metadata.Page meta -> ++ (case metadata of
Seo.summaryLarge Metadata.Page meta ->
{ canonicalUrlOverride = Nothing Seo.summaryLarge
, siteName = "elm-pages" { canonicalUrlOverride = Nothing
, image = , siteName = "elm-pages"
{ url = images.iconPng , image =
, alt = "elm-pages logo" { url = images.iconPng
, dimensions = Nothing , alt = "elm-pages logo"
, mimeType = Nothing , dimensions = Nothing
} , mimeType = Nothing
, description = siteTagline }
, locale = Nothing , description = siteTagline
, title = meta.title , locale = Nothing
} , title = meta.title
|> Seo.website }
|> Seo.website
Metadata.Doc meta -> Metadata.Doc meta ->
Seo.summaryLarge Seo.summaryLarge
{ canonicalUrlOverride = Nothing { canonicalUrlOverride = Nothing
, siteName = "elm-pages" , siteName = "elm-pages"
, image = , image =
{ url = images.iconPng { url = images.iconPng
, alt = "elm pages logo" , alt = "elm pages logo"
, dimensions = Nothing , dimensions = Nothing
, mimeType = Nothing , mimeType = Nothing
} }
, locale = Nothing , locale = Nothing
, description = siteTagline , description = siteTagline
, title = meta.title , title = meta.title
} }
|> Seo.website |> Seo.website
Metadata.Article meta -> Metadata.Article meta ->
Seo.summaryLarge Seo.summaryLarge
{ canonicalUrlOverride = Nothing { canonicalUrlOverride = Nothing
, siteName = "elm-pages" , siteName = "elm-pages"
, image = , image =
{ url = meta.image { url = meta.image
, alt = meta.description , alt = meta.description
, dimensions = Nothing , dimensions = Nothing
, mimeType = Nothing , mimeType = Nothing
} }
, description = meta.description , description = meta.description
, locale = Nothing , locale = Nothing
, title = meta.title , title = meta.title
} }
|> Seo.article |> Seo.article
{ tags = [] { tags = []
, section = Nothing , section = Nothing
, publishedTime = Just (Date.toIsoString meta.published) , publishedTime = Just (Date.toIsoString meta.published)
, modifiedTime = Nothing , modifiedTime = Nothing
, expirationTime = Nothing , expirationTime = Nothing
} }
Metadata.Author meta -> Metadata.Author meta ->
let let
( firstName, lastName ) = ( firstName, lastName ) =
case meta.name |> String.split " " of case meta.name |> String.split " " of
[ first, last ] -> [ first, last ] ->
( first, last ) ( first, last )
[ first, middle, last ] -> [ first, middle, last ] ->
( first ++ " " ++ middle, last ) ( first ++ " " ++ middle, last )
[] -> [] ->
( "", "" ) ( "", "" )
_ -> _ ->
( meta.name, "" ) ( meta.name, "" )
in in
Seo.summary Seo.summary
{ canonicalUrlOverride = Nothing { canonicalUrlOverride = Nothing
, siteName = "elm-pages" , siteName = "elm-pages"
, image = , image =
{ url = meta.avatar { url = meta.avatar
, alt = meta.name ++ "'s elm-pages articles." , alt = meta.name ++ "'s elm-pages articles."
, dimensions = Nothing , dimensions = Nothing
, mimeType = Nothing , mimeType = Nothing
} }
, description = meta.bio , description = meta.bio
, locale = Nothing , locale = Nothing
, title = meta.name ++ "'s elm-pages articles." , title = meta.name ++ "'s elm-pages articles."
} }
|> Seo.profile |> Seo.profile
{ firstName = firstName { firstName = firstName
, lastName = lastName , lastName = lastName
, username = Nothing , username = Nothing
} }
Metadata.BlogIndex -> Metadata.BlogIndex ->
Seo.summaryLarge Seo.summaryLarge
{ canonicalUrlOverride = Nothing { canonicalUrlOverride = Nothing
, siteName = "elm-pages" , siteName = "elm-pages"
, image = , image =
{ url = images.iconPng { url = images.iconPng
, alt = "elm-pages logo" , alt = "elm-pages logo"
, dimensions = Nothing , dimensions = Nothing
, mimeType = Nothing , mimeType = Nothing
} }
, description = siteTagline , description = siteTagline
, locale = Nothing , locale = Nothing
, title = "elm-pages blog" , title = "elm-pages blog"
} }
|> Seo.website |> Seo.website
Metadata.Showcase ->
Seo.summaryLarge
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = images.iconPng
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = siteTagline
, locale = Nothing
, title = "elm-pages sites showcase"
}
|> Seo.website
)
canonicalSiteUrl : String canonicalSiteUrl : String

View File

@ -17,6 +17,7 @@ type Metadata
| Doc DocMetadata | Doc DocMetadata
| Author Data.Author.Author | Author Data.Author.Author
| BlogIndex | BlogIndex
| Showcase
type alias ArticleMetadata = type alias ArticleMetadata =
@ -54,6 +55,9 @@ decoder =
"blog-index" -> "blog-index" ->
Decode.succeed BlogIndex Decode.succeed BlogIndex
"showcase" ->
Decode.succeed Showcase
"author" -> "author" ->
Decode.map3 Data.Author.Author Decode.map3 Data.Author.Author
(Decode.field "name" Decode.string) (Decode.field "name" Decode.string)

View File

@ -0,0 +1,32 @@
module MySitemap exposing (..)
import Metadata exposing (Metadata(..))
import Pages
import Pages.PagePath as PagePath exposing (PagePath)
import Sitemap
build :
{ siteUrl : String
}
->
List
{ path : PagePath Pages.PathKey
, frontmatter : Metadata
, body : String
}
->
{ path : List String
, content : String
}
build config siteMetadata =
{ path = [ "sitemap.xml" ]
, content =
Sitemap.build config
(siteMetadata
|> List.map
(\page ->
{ path = PagePath.toString page.path, lastMod = Nothing }
)
)
}

View File

@ -0,0 +1,161 @@
module Showcase exposing (..)
import Element
import Element.Border
import Element.Font
import FontAwesome
import Json.Decode.Exploration as Decode
import Pages.Secrets as Secrets
import Pages.StaticHttp as StaticHttp
import Palette
import Url.Builder
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

@ -1,4 +1,5 @@
@import url("https://fonts.googleapis.com/css?family=Montserrat:400,700|Roboto&display=swap"); @import url("https://fonts.googleapis.com/css?family=Montserrat:400,700|Roboto&display=swap");
@import url("https://use.fontawesome.com/releases/v5.9.0/css/all.css");
.dotted-line { .dotted-line {
-webkit-animation: animation-yweh2o 400ms linear infinite; -webkit-animation: animation-yweh2o 400ms linear infinite;
@ -26,3 +27,14 @@
width: 20px; width: 20px;
} }
} }
@media (max-width: 600px) {
.responsive-desktop {
display: none !important;
}
}
@media (min-width: 600px) {
.responsive-mobile {
display: none !important;
}
}

View File

@ -20,8 +20,11 @@ function unpackFile(filePath) {
} }
module.exports = class AddFilesPlugin { module.exports = class AddFilesPlugin {
constructor(data) { constructor(data, filesToGenerate) {
this.pagesWithRequests = data; this.pagesWithRequests = data;
this.filesToGenerate = filesToGenerate;
console.log('this.filesToGenerate', this.filesToGenerate);
} }
apply(compiler) { apply(compiler) {
compiler.hooks.emit.tap("AddFilesPlugin", compilation => { compiler.hooks.emit.tap("AddFilesPlugin", compilation => {
@ -52,6 +55,18 @@ module.exports = class AddFilesPlugin {
size: () => rawContents.length size: () => rawContents.length
}; };
}); });
this.filesToGenerate.forEach(file => {
// Couldn't find this documented in the webpack docs,
// but I found the example code for it here:
// https://github.com/jantimon/html-webpack-plugin/blob/35a154186501fba3ecddb819b6f632556d37a58f/index.js#L470-L478
compilation.assets[file.path] = {
source: () => file.content,
size: () => file.content.length
};
});
}); });
} }
}; };

View File

@ -2,6 +2,7 @@ const webpack = require("webpack");
const middleware = require("webpack-dev-middleware"); const middleware = require("webpack-dev-middleware");
const path = require("path"); const path = require("path");
const HTMLWebpackPlugin = require("html-webpack-plugin"); const HTMLWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
const CopyPlugin = require("copy-webpack-plugin"); const CopyPlugin = require("copy-webpack-plugin");
const PrerenderSPAPlugin = require("prerender-spa-plugin"); const PrerenderSPAPlugin = require("prerender-spa-plugin");
const merge = require("webpack-merge"); const merge = require("webpack-merge");
@ -15,11 +16,12 @@ const ClosurePlugin = require("closure-webpack-plugin");
const readline = require("readline"); const readline = require("readline");
module.exports = { start, run }; module.exports = { start, run };
function start({ routes, debug, customPort, manifestConfig, routesWithRequests }) { function start({ routes, debug, customPort, manifestConfig, routesWithRequests, filesToGenerate }) {
const config = webpackOptions(false, routes, { const config = webpackOptions(false, routes, {
debug, debug,
manifestConfig, manifestConfig,
routesWithRequests routesWithRequests,
filesToGenerate
}); });
const compiler = webpack(config); const compiler = webpack(config);
@ -65,12 +67,13 @@ function start({ routes, debug, customPort, manifestConfig, routesWithRequests }
// app.use(express.static(__dirname + "/path-to-static-folder")); // app.use(express.static(__dirname + "/path-to-static-folder"));
} }
function run({ routes, manifestConfig, routesWithRequests }, callback) { function run({ routes, manifestConfig, routesWithRequests, filesToGenerate }, callback) {
webpack( webpack(
webpackOptions(true, routes, { webpackOptions(true, routes, {
debug: false, debug: false,
manifestConfig, manifestConfig,
routesWithRequests routesWithRequests,
filesToGenerate
}) })
).run((err, stats) => { ).run((err, stats) => {
if (err) { if (err) {
@ -118,12 +121,12 @@ function printProgress(progress, message) {
function webpackOptions( function webpackOptions(
production, production,
routes, routes,
{ debug, manifestConfig, routesWithRequests } { debug, manifestConfig, routesWithRequests, filesToGenerate }
) { ) {
const common = { const common = {
mode: production ? "production" : "development", mode: production ? "production" : "development",
plugins: [ plugins: [
new AddFilesPlugin(routesWithRequests), new AddFilesPlugin(routesWithRequests, filesToGenerate),
new CopyPlugin([ new CopyPlugin([
{ {
from: "static/**/*", from: "static/**/*",
@ -159,6 +162,10 @@ function webpackOptions(
inject: "head", inject: "head",
template: path.resolve(__dirname, "template.html") template: path.resolve(__dirname, "template.html")
}), }),
new ScriptExtHtmlWebpackPlugin({
preload: /\.js$/,
defaultAttribute: 'defer'
}),
new FaviconsWebpackPlugin({ new FaviconsWebpackPlugin({
logo: path.resolve(process.cwd(), `./${manifestConfig.sourceIcon}`), logo: path.resolve(process.cwd(), `./${manifestConfig.sourceIcon}`),
favicons: { favicons: {

View File

@ -86,6 +86,7 @@ function run() {
markdownContent, markdownContent,
content, content,
function(payload) { function(payload) {
console.log('@@@@@@@@@ filesToGenerate', payload.filesToGenerate);
if (contents.watch) { if (contents.watch) {
startWatchIfNeeded(); startWatchIfNeeded();
if (!devServerRunning) { if (!devServerRunning) {
@ -94,7 +95,9 @@ function run() {
routes, routes,
debug: contents.debug, debug: contents.debug,
manifestConfig: payload.manifest, manifestConfig: payload.manifest,
routesWithRequests: payload.pages routesWithRequests: payload.pages,
filesToGenerate: payload.filesToGenerate,
customPort: contents.customPort
}); });
} }
} else { } else {
@ -106,7 +109,8 @@ function run() {
{ {
routes, routes,
manifestConfig: payload.manifest, manifestConfig: payload.manifest,
routesWithRequests: payload.pages routesWithRequests: payload.pages,
filesToGenerate: payload.filesToGenerate
}, },
() => {} () => {}
); );

View File

@ -19,8 +19,8 @@ function toEntry(entry, includeBody) {
return ` return `
( [${fullPath.join(", ")}] ( [${fullPath.join(", ")}]
, { frontMatter = """${entry.metadata} , { frontMatter = ${JSON.stringify(entry.metadata)}
""" , body = ${body(entry, includeBody)} , body = ${body(entry, includeBody)}
, extension = "${extension}" , extension = "${extension}"
} ) } )
`; `;

View File

@ -4,7 +4,7 @@ workbox.precaching.precacheAndRoute(self.__precacheManifest);
workbox.routing.registerNavigationRoute( workbox.routing.registerNavigationRoute(
workbox.precaching.getCacheKeyForURL("/index.html"), workbox.precaching.getCacheKeyForURL("/index.html"),
{ {
blacklist: [/admin/] blacklist: [/admin/, /\./]
} }
); );
workbox.routing.registerRoute( workbox.routing.registerRoute(

View File

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<link rel="preload" href="content.json" as="fetch" crossorigin /> <link rel="preload" href="./content.json" as="fetch" crossorigin />
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<script> <script>

View File

@ -7,9 +7,13 @@ module.exports = function pagesInit(
let prefetchedPages = [window.location.pathname]; let prefetchedPages = [window.location.pathname];
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
httpGet(`${window.location.origin}${window.location.pathname}/content.json`, function (/** @type JSON */ contentJson) {
let app = mainElmModule.init({ let app = mainElmModule.init({
flags: { flags: {
secrets: null secrets: null,
isPrerendering: navigator.userAgent.indexOf("Headless") >= 0,
contentJson
} }
}); });
@ -33,6 +37,9 @@ module.exports = function pagesInit(
document.dispatchEvent(new Event("prerender-trigger")); document.dispatchEvent(new Event("prerender-trigger"));
}); });
})
}); });
function setupLinkPrefetching() { function setupLinkPrefetching() {
@ -130,3 +137,14 @@ module.exports = function pagesInit(
document.getElementsByTagName("head")[0].appendChild(meta); document.getElementsByTagName("head")[0].appendChild(meta);
} }
}; };
function httpGet(/** @type string */ theUrl, /** @type Function */ callback)
{
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
callback(JSON.parse(xmlHttp.responseText));
}
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.send(null);
}

10
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "elm-pages", "name": "elm-pages",
"version": "1.1.6", "version": "1.2.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -9220,6 +9220,14 @@
"ajv-keywords": "^3.1.0" "ajv-keywords": "^3.1.0"
} }
}, },
"script-ext-html-webpack-plugin": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/script-ext-html-webpack-plugin/-/script-ext-html-webpack-plugin-2.1.4.tgz",
"integrity": "sha512-7MAv3paAMfh9y2Rg+yQKp9jEGC5cEcmdge4EomRqri10qoczmliYEVPVNz0/5e9QQ202e05qDll9B8zZlY9N1g==",
"requires": {
"debug": "^4.1.1"
}
},
"scss-tokenizer": { "scss-tokenizer": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "elm-pages", "name": "elm-pages",
"version": "1.1.6", "version": "1.2.0",
"homepage": "http://elm-pages.com", "homepage": "http://elm-pages.com",
"description": "Type-safe static sites, written in pure elm with your own custom elm-markup syntax.", "description": "Type-safe static sites, written in pure elm with your own custom elm-markup syntax.",
"main": "index.js", "main": "index.js",
@ -42,6 +42,7 @@
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"prerender-spa-plugin": "^3.4.0", "prerender-spa-plugin": "^3.4.0",
"sass-loader": "^8.0.0", "sass-loader": "^8.0.0",
"script-ext-html-webpack-plugin": "^2.1.4",
"style-loader": "^1.0.0", "style-loader": "^1.0.0",
"webpack": "^4.41.5", "webpack": "^4.41.5",
"webpack-dev-middleware": "^3.7.0", "webpack-dev-middleware": "^3.7.0",

View File

@ -1,5 +1,6 @@
module Head exposing module Head exposing
( Tag, metaName, metaProperty ( Tag, metaName, metaProperty
, rssLink, sitemapLink
, AttributeValue , AttributeValue
, currentPageFullUrl, fullImageUrl, fullPageUrl, raw , currentPageFullUrl, fullImageUrl, fullPageUrl, raw
, toJson, canonicalLink , toJson, canonicalLink
@ -15,6 +16,7 @@ But this module might be useful if you have a special use case, or if you are
writing a plugin package to extend `elm-pages`. writing a plugin package to extend `elm-pages`.
@docs Tag, metaName, metaProperty @docs Tag, metaName, metaProperty
@docs rssLink, sitemapLink
## `AttributeValue`s ## `AttributeValue`s
@ -106,9 +108,49 @@ canonicalLink maybePath =
] ]
{-| Add a link to the site's RSS feed.
Example:
rssLink "/feed.xml"
```html
<link rel="alternate" type="application/rss+xml" href="/rss.xml">
```
-}
rssLink : String -> Tag pathKey
rssLink url =
node "link"
[ ( "rel", raw "alternate" )
, ( "type", raw "application/rss+xml" )
, ( "href", raw url )
]
{-| Add a link to the site's RSS feed.
Example:
sitemapLink "/feed.xml"
```html
<link rel="sitemap" type="application/xml" href="/sitemap.xml">
```
-}
sitemapLink : String -> Tag pathKey
sitemapLink url =
node "link"
[ ( "rel", raw "sitemap" )
, ( "type", raw "application/xml" )
, ( "href", raw url )
]
{-| Example: {-| Example:
Head.metaProperty "fb:app_id" ( Head.raw "123456789" ) Head.metaProperty "fb:app_id" (Head.raw "123456789")
Results in `<meta property="fb:app_id" content="123456789" />` Results in `<meta property="fb:app_id" content="123456789" />`

View File

@ -111,9 +111,10 @@ pagesWithErrors cache =
init : init :
Document metadata view Document metadata view
-> Content -> Content
-> Maybe { contentJson : ContentJson String, initialUrl : Url }
-> ContentCache metadata view -> ContentCache metadata view
init document content = init document content maybeInitialPageContent =
parseMetadata document content parseMetadata maybeInitialPageContent document content
|> List.map |> List.map
(\tuple -> (\tuple ->
Tuple.mapSecond Tuple.mapSecond
@ -149,42 +150,75 @@ createBuildError path decodeError =
parseMetadata : parseMetadata :
Document metadata view Maybe { contentJson : ContentJson String, initialUrl : Url }
-> Document metadata view
-> List ( List String, { extension : String, frontMatter : String, body : Maybe String } ) -> List ( List String, { extension : String, frontMatter : String, body : Maybe String } )
-> List ( List String, Result String (Entry metadata view) ) -> List ( List String, Result String (Entry metadata view) )
parseMetadata document content = parseMetadata maybeInitialPageContent document content =
content content
|> List.map |> List.map
(Tuple.mapSecond (\( path, { frontMatter, extension, body } ) ->
(\{ frontMatter, extension, body } -> let
let maybeDocumentEntry =
maybeDocumentEntry = Document.get extension document
Document.get extension document in
in case maybeDocumentEntry of
case maybeDocumentEntry of Just documentEntry ->
Just documentEntry -> frontMatter
frontMatter |> documentEntry.frontmatterParser
|> documentEntry.frontmatterParser |> Result.map
|> Result.map (\metadata ->
(\metadata -> let
-- TODO do I need to handle this case? renderer =
-- case body of \value ->
-- Just presentBody -> parseContent extension value document
-- Parsed metadata in
-- { body = parseContent extension presentBody document case maybeInitialPageContent of
-- , staticData = "" Just { contentJson, initialUrl } ->
-- } if normalizePath initialUrl.path == (String.join "/" path |> normalizePath) then
-- Parsed metadata
-- Nothing -> { body = renderer contentJson.body
NeedContent extension metadata , staticData = contentJson.staticData
) }
Nothing -> else
Err ("Could not find extension '" ++ extension ++ "'") NeedContent extension metadata
)
Nothing ->
NeedContent extension metadata
)
|> Tuple.pair path
Nothing ->
Err ("Could not find extension '" ++ extension ++ "'")
|> Tuple.pair path
) )
normalizePath : String -> String
normalizePath pathString =
let
hasPrefix =
String.startsWith "/" pathString
hasSuffix =
String.endsWith "/" pathString
in
String.concat
[ if hasPrefix then
""
else
"/"
, pathString
, if hasSuffix then
""
else
"/"
]
parseContent : parseContent :
String String
-> String -> String
@ -327,8 +361,8 @@ lazyLoad document url cacheResult =
|> Task.map |> Task.map
(\downloadedContent -> (\downloadedContent ->
update cacheResult update cacheResult
(\thing -> (\value ->
parseContent extension thing document parseContent extension value document
) )
url url
downloadedContent downloadedContent

View File

@ -217,6 +217,12 @@ type alias Flags =
Decode.Value Decode.Value
type alias ContentJson =
{ body : String
, staticData : Dict String String
}
init : init :
pathKey pathKey
-> String -> String
@ -256,11 +262,33 @@ init :
init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel flags url key = init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel flags url key =
let let
contentCache = contentCache =
ContentCache.init document content ContentCache.init document content (Maybe.map (\cj -> { contentJson = cj, initialUrl = url }) contentJson)
contentJson =
flags
|> Decode.decodeValue (Decode.field "contentJson" contentJsonDecoder)
|> Result.toMaybe
contentJsonDecoder : Decode.Decoder ContentJson
contentJsonDecoder =
Decode.map2 ContentJson
(Decode.field "body" Decode.string)
(Decode.field "staticData" (Decode.dict Decode.string))
in in
case contentCache of case contentCache of
Ok okCache -> Ok okCache ->
let let
phase =
case Decode.decodeValue (Decode.field "isPrerendering" Decode.bool) flags of
Ok True ->
Prerender
Ok False ->
Client
Err _ ->
Client
( userModel, userCmd ) = ( userModel, userCmd ) =
initUserModel initUserModel
(maybePagePath (maybePagePath
@ -300,6 +328,7 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
, url = url , url = url
, userModel = userModel , userModel = userModel
, contentCache = contentCache , contentCache = contentCache
, phase = phase
} }
, cmd , cmd
) )
@ -313,6 +342,7 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
, url = url , url = url
, userModel = userModel , userModel = userModel
, contentCache = contentCache , contentCache = contentCache
, phase = Client
} }
, Cmd.batch , Cmd.batch
[ userCmd |> Cmd.map UserMsg [ userCmd |> Cmd.map UserMsg
@ -349,9 +379,15 @@ type alias ModelDetails userModel metadata view =
, url : Url.Url , url : Url.Url
, contentCache : ContentCache metadata view , contentCache : ContentCache metadata view
, userModel : userModel , userModel : userModel
, phase : Phase
} }
type Phase
= Prerender
| Client
update : update :
String String
-> ->
@ -530,6 +566,19 @@ application :
, content : Content , content : Content
, toJsPort : Json.Encode.Value -> Cmd Never , toJsPort : Json.Encode.Value -> Cmd Never
, manifest : Manifest.Config pathKey , manifest : Manifest.Config pathKey
, generateFiles :
List
{ path : PagePath pathKey
, frontmatter : metadata
, body : String
}
->
List
(Result String
{ path : List String
, content : String
}
)
, canonicalSiteUrl : String , canonicalSiteUrl : String
, pathKey : pathKey , pathKey : pathKey
, onPageChange : , onPageChange :
@ -562,7 +611,20 @@ application config =
\msg outerModel -> \msg outerModel ->
case outerModel of case outerModel of
Model model -> Model model ->
update config.canonicalSiteUrl config.view config.pathKey config.onPageChange config.toJsPort config.document config.update msg model let
userUpdate =
case model.phase of
Prerender ->
noOpUpdate
Client ->
config.update
noOpUpdate =
\userMsg userModel ->
( userModel, Cmd.none )
in
update config.canonicalSiteUrl config.view config.pathKey config.onPageChange config.toJsPort config.document userUpdate msg model
|> Tuple.mapFirst Model |> Tuple.mapFirst Model
|> Tuple.mapSecond (Cmd.map AppMsg) |> Tuple.mapSecond (Cmd.map AppMsg)
@ -608,6 +670,19 @@ cliApplication :
, content : Content , content : Content
, toJsPort : Json.Encode.Value -> Cmd Never , toJsPort : Json.Encode.Value -> Cmd Never
, manifest : Manifest.Config pathKey , manifest : Manifest.Config pathKey
, generateFiles :
List
{ path : PagePath pathKey
, frontmatter : metadata
, body : String
}
->
List
(Result String
{ path : List String
, content : String
}
)
, canonicalSiteUrl : String , canonicalSiteUrl : String
, pathKey : pathKey , pathKey : pathKey
, onPageChange : , onPageChange :

View File

@ -44,6 +44,13 @@ type ToJsPayload pathKey
type alias ToJsSuccessPayload pathKey = type alias ToJsSuccessPayload pathKey =
{ pages : Dict String (Dict String String) { pages : Dict String (Dict String String)
, manifest : Manifest.Config pathKey , manifest : Manifest.Config pathKey
, filesToGenerate : List FileToGenerate
}
type alias FileToGenerate =
{ path : List String
, content : String
} }
@ -55,8 +62,8 @@ toJsCodec =
Errors errorList -> Errors errorList ->
errors errorList errors errorList
Success { pages, manifest } -> Success { pages, manifest, filesToGenerate } ->
success (ToJsSuccessPayload pages manifest) success (ToJsSuccessPayload pages manifest filesToGenerate)
) )
|> Codec.variant1 "Errors" Errors Codec.string |> Codec.variant1 "Errors" Errors Codec.string
|> Codec.variant1 "Success" |> Codec.variant1 "Success"
@ -90,6 +97,21 @@ successCodec =
|> Codec.field "manifest" |> Codec.field "manifest"
.manifest .manifest
(Codec.build Manifest.toJson (Decode.succeed stubManifest)) (Codec.build Manifest.toJson (Decode.succeed stubManifest))
|> Codec.field "filesToGenerate"
.filesToGenerate
(Codec.build
(\list ->
list
|> Json.Encode.list
(\item ->
Json.Encode.object
[ ( "path", item.path |> String.join "/" |> Json.Encode.string )
, ( "content", item.content |> Json.Encode.string )
]
)
)
(Decode.succeed [])
)
|> Codec.buildObject |> Codec.buildObject
@ -128,50 +150,66 @@ type Msg
= GotStaticHttpResponse { request : { masked : RequestDetails, unmasked : RequestDetails }, response : Result Http.Error String } = GotStaticHttpResponse { request : { masked : RequestDetails, unmasked : RequestDetails }, response : Result Http.Error String }
type alias Config pathKey userMsg userModel metadata view =
{ init :
Maybe
{ path : PagePath pathKey
, query : Maybe String
, fragment : Maybe String
}
-> ( userModel, Cmd userMsg )
, update : userMsg -> userModel -> ( userModel, Cmd userMsg )
, subscriptions : userModel -> Sub userMsg
, view :
List ( PagePath pathKey, metadata )
->
{ path : PagePath pathKey
, frontmatter : metadata
}
->
StaticHttp.Request
{ view : userModel -> view -> { title : String, body : Html userMsg }
, head : List (Head.Tag pathKey)
}
, document : Pages.Document.Document metadata view
, content : Content
, toJsPort : Json.Encode.Value -> Cmd Never
, manifest : Manifest.Config pathKey
, generateFiles :
List
{ path : PagePath pathKey
, frontmatter : metadata
, body : String
}
->
List
(Result String
{ path : List String
, content : String
}
)
, canonicalSiteUrl : String
, pathKey : pathKey
, onPageChange :
{ path : PagePath pathKey
, query : Maybe String
, fragment : Maybe String
}
-> userMsg
}
cliApplication : cliApplication :
(Msg -> msg) (Msg -> msg)
-> (msg -> Maybe Msg) -> (msg -> Maybe Msg)
-> (Model -> model) -> (Model -> model)
-> (model -> Maybe Model) -> (model -> Maybe Model)
-> -> Config pathKey userMsg userModel metadata view
{ init :
Maybe
{ path : PagePath pathKey
, query : Maybe String
, fragment : Maybe String
}
-> ( userModel, Cmd userMsg )
, update : userMsg -> userModel -> ( userModel, Cmd userMsg )
, subscriptions : userModel -> Sub userMsg
, view :
List ( PagePath pathKey, metadata )
->
{ path : PagePath pathKey
, frontmatter : metadata
}
->
StaticHttp.Request
{ view : userModel -> view -> { title : String, body : Html userMsg }
, head : List (Head.Tag pathKey)
}
, document : Pages.Document.Document metadata view
, content : Content
, toJsPort : Json.Encode.Value -> Cmd Never
, manifest : Manifest.Config pathKey
, canonicalSiteUrl : String
, pathKey : pathKey
, onPageChange :
{ path : PagePath pathKey
, query : Maybe String
, fragment : Maybe String
}
-> userMsg
}
-> Platform.Program Flags model msg -> Platform.Program Flags model msg
cliApplication cliMsgConstructor narrowMsg toModel fromModel config = cliApplication cliMsgConstructor narrowMsg toModel fromModel config =
let let
contentCache = contentCache =
ContentCache.init config.document config.content ContentCache.init config.document config.content Nothing
siteMetadata = siteMetadata =
contentCache contentCache
@ -188,7 +226,7 @@ cliApplication cliMsgConstructor narrowMsg toModel fromModel config =
\msg model -> \msg model ->
case ( narrowMsg msg, fromModel model ) of case ( narrowMsg msg, fromModel model ) of
( Just cliMsg, Just cliModel ) -> ( Just cliMsg, Just cliModel ) ->
update config cliMsg cliModel update siteMetadata config cliMsg cliModel
|> Tuple.mapSecond (perform cliMsgConstructor config.toJsPort) |> Tuple.mapSecond (perform cliMsgConstructor config.toJsPort)
|> Tuple.mapFirst toModel |> Tuple.mapFirst toModel
@ -259,21 +297,7 @@ init :
(Model -> model) (Model -> model)
-> ContentCache.ContentCache metadata view -> ContentCache.ContentCache metadata view
-> Result (List BuildError) (List ( PagePath pathKey, metadata )) -> Result (List BuildError) (List ( PagePath pathKey, metadata ))
-> -> Config pathKey userMsg userModel metadata view
{ config
| view :
List ( PagePath pathKey, metadata )
->
{ path : PagePath pathKey
, frontmatter : metadata
}
->
StaticHttp.Request
{ view : userModel -> view -> { title : String, body : Html userMsg }
, head : List (Head.Tag pathKey)
}
, manifest : Manifest.Config pathKey
}
-> Decode.Value -> Decode.Value
-> ( model, Effect pathKey ) -> ( model, Effect pathKey )
init toModel contentCache siteMetadata config flags = init toModel contentCache siteMetadata config flags =
@ -309,7 +333,7 @@ init toModel contentCache siteMetadata config flags =
staticResponsesInit [] staticResponsesInit []
( updatedRawResponses, effect ) = ( updatedRawResponses, effect ) =
sendStaticResponsesIfDone mode secrets Dict.empty [] staticResponses config.manifest sendStaticResponsesIfDone config siteMetadata mode secrets Dict.empty [] staticResponses
in in
( Model staticResponses secrets [] updatedRawResponses mode |> toModel ( Model staticResponses secrets [] updatedRawResponses mode |> toModel
, effect , effect
@ -335,6 +359,8 @@ init toModel contentCache siteMetadata config flags =
staticResponsesInit [] staticResponsesInit []
in in
updateAndSendPortIfDone updateAndSendPortIfDone
config
siteMetadata
(Model (Model
staticResponses staticResponses
secrets secrets
@ -343,10 +369,11 @@ init toModel contentCache siteMetadata config flags =
mode mode
) )
toModel toModel
config.manifest
Err metadataParserErrors -> Err metadataParserErrors ->
updateAndSendPortIfDone updateAndSendPortIfDone
config
siteMetadata
(Model Dict.empty (Model Dict.empty
secrets secrets
(metadataParserErrors |> List.map Tuple.second) (metadataParserErrors |> List.map Tuple.second)
@ -354,10 +381,11 @@ init toModel contentCache siteMetadata config flags =
mode mode
) )
toModel toModel
config.manifest
Err error -> Err error ->
updateAndSendPortIfDone updateAndSendPortIfDone
config
siteMetadata
(Model Dict.empty (Model Dict.empty
SecretsDict.masked SecretsDict.masked
[ { title = "Internal Error" [ { title = "Internal Error"
@ -368,20 +396,25 @@ init toModel contentCache siteMetadata config flags =
Dev Dev
) )
toModel toModel
config.manifest
updateAndSendPortIfDone : Model -> (Model -> model) -> Manifest.Config pathKey -> ( model, Effect pathKey ) updateAndSendPortIfDone :
updateAndSendPortIfDone model toModel manifest = Config pathKey userMsg userModel metadata view
-> Result (List BuildError) (List ( PagePath pathKey, metadata ))
-> Model
-> (Model -> model)
-> ( model, Effect pathKey )
updateAndSendPortIfDone config siteMetadata model toModel =
let let
( updatedAllRawResponses, effect ) = ( updatedAllRawResponses, effect ) =
sendStaticResponsesIfDone sendStaticResponsesIfDone
config
siteMetadata
model.mode model.mode
model.secrets model.secrets
model.allRawResponses model.allRawResponses
model.errors model.errors
model.staticResponses model.staticResponses
manifest
in in
( { model | allRawResponses = updatedAllRawResponses } |> toModel ( { model | allRawResponses = updatedAllRawResponses } |> toModel
, effect , effect
@ -393,24 +426,12 @@ type alias PageErrors =
update : update :
{ config Result (List BuildError) (List ( PagePath pathKey, metadata ))
| view : -> Config pathKey userMsg userModel metadata view
List ( PagePath pathKey, metadata )
->
{ path : PagePath pathKey
, frontmatter : metadata
}
->
StaticHttp.Request
{ view : userModel -> view -> { title : String, body : Html userMsg }
, head : List (Head.Tag pathKey)
}
, manifest : Manifest.Config pathKey
}
-> Msg -> Msg
-> Model -> Model
-> ( Model, Effect pathKey ) -> ( Model, Effect pathKey )
update config msg model = update siteMetadata config msg model =
case msg of case msg of
GotStaticHttpResponse { request, response } -> GotStaticHttpResponse { request, response } ->
let let
@ -467,7 +488,7 @@ update config msg model =
} }
( updatedAllRawResponses, effect ) = ( updatedAllRawResponses, effect ) =
sendStaticResponsesIfDone updatedModel.mode updatedModel.secrets updatedModel.allRawResponses updatedModel.errors updatedModel.staticResponses config.manifest sendStaticResponsesIfDone config siteMetadata updatedModel.mode updatedModel.secrets updatedModel.allRawResponses updatedModel.errors updatedModel.staticResponses
in in
( { updatedModel | allRawResponses = updatedAllRawResponses } ( { updatedModel | allRawResponses = updatedAllRawResponses }
, effect , effect
@ -596,8 +617,16 @@ isJust maybeValue =
False False
sendStaticResponsesIfDone : Mode -> SecretsDict -> Dict String (Maybe String) -> List BuildError -> StaticResponses -> Manifest.Config pathKey -> ( Dict String (Maybe String), Effect pathKey ) sendStaticResponsesIfDone :
sendStaticResponsesIfDone mode secrets allRawResponses errors staticResponses manifest = Config pathKey userMsg userModel metadata view
-> Result (List BuildError) (List ( PagePath pathKey, metadata ))
-> Mode
-> SecretsDict
-> Dict String (Maybe String)
-> List BuildError
-> StaticResponses
-> ( Dict String (Maybe String), Effect pathKey )
sendStaticResponsesIfDone config siteMetadata mode secrets allRawResponses errors staticResponses =
let let
pendingRequests = pendingRequests =
staticResponses staticResponses
@ -748,18 +777,86 @@ sendStaticResponsesIfDone mode secrets allRawResponses errors staticResponses ma
let let
updatedAllRawResponses = updatedAllRawResponses =
Dict.empty Dict.empty
generatedFiles =
siteMetadata
|> Result.withDefault []
|> List.map
(\( pagePath, metadata ) ->
let
contentForPage =
config.content
|> List.filterMap
(\( path, { body } ) ->
let
pagePathToGenerate =
PagePath.toString pagePath
currentContentPath =
"/" ++ (path |> String.join "/")
in
if pagePathToGenerate == currentContentPath then
Just body
else
Nothing
)
|> List.head
|> Maybe.andThen identity
in
{ path = pagePath
, frontmatter = metadata
, body = contentForPage |> Maybe.withDefault ""
}
)
|> config.generateFiles
generatedOkayFiles =
generatedFiles
|> List.filterMap
(\result ->
case result of
Ok ok ->
Just ok
_ ->
Nothing
)
generatedFileErrors =
generatedFiles
|> List.filterMap
(\result ->
case result of
Ok ok ->
Nothing
Err error ->
Just
{ title = "Generate Files Error"
, message =
[ Terminal.text "I encountered an Err from your generateFiles function. Message:\n"
, Terminal.text <| "Error: " ++ error
]
}
)
allErrors : List BuildError
allErrors =
errors ++ failedRequests ++ generatedFileErrors
in in
( updatedAllRawResponses ( updatedAllRawResponses
, SendJsData , SendJsData
(if List.isEmpty errors && List.isEmpty failedRequests then (if List.isEmpty allErrors then
Success Success
(ToJsSuccessPayload (ToJsSuccessPayload
(encodeStaticResponses mode staticResponses) (encodeStaticResponses mode staticResponses)
manifest config.manifest
generatedOkayFiles
) )
else else
Errors <| BuildError.errorsToString (failedRequests ++ errors) Errors <| BuildError.errorsToString allErrors
) )
) )

View File

@ -83,6 +83,19 @@ application :
} }
, documents : List ( String, Document.DocumentHandler metadata view ) , documents : List ( String, Document.DocumentHandler metadata view )
, manifest : Pages.Manifest.Config pathKey , manifest : Pages.Manifest.Config pathKey
, generateFiles :
List
{ path : PagePath pathKey
, frontmatter : metadata
, body : String
}
->
List
(Result String
{ path : List String
, content : String
}
)
, onPageChange : , onPageChange :
{ path : PagePath pathKey { path : PagePath pathKey
, query : Maybe String , query : Maybe String
@ -108,6 +121,7 @@ application config =
, subscriptions = config.subscriptions , subscriptions = config.subscriptions
, document = Document.fromList config.documents , document = Document.fromList config.documents
, content = config.internals.content , content = config.internals.content
, generateFiles = config.generateFiles
, toJsPort = config.internals.toJsPort , toJsPort = config.internals.toJsPort
, manifest = config.manifest , manifest = config.manifest
, canonicalSiteUrl = config.canonicalSiteUrl , canonicalSiteUrl = config.canonicalSiteUrl

View File

@ -564,9 +564,17 @@ start pages =
Debug.todo "Couldn't find page" Debug.todo "Couldn't find page"
} }
in in
{-
(Model -> model)
-> ContentCache.ContentCache metadata view
-> Result (List BuildError) (List ( PagePath pathKey, metadata ))
-> Config pathKey userMsg userModel metadata view
-> Decode.Value
-> ( model, Effect pathKey )
-}
ProgramTest.createDocument ProgramTest.createDocument
{ init = Main.init identity contentCache siteMetadata config { init = Main.init identity contentCache siteMetadata config
, update = Main.update config , update = Main.update siteMetadata config
, view = \_ -> { title = "", body = [] } , view = \_ -> { title = "", body = [] }
} }
|> ProgramTest.withSimulatedEffects simulateEffects |> ProgramTest.withSimulatedEffects simulateEffects