mirror of
https://github.com/dillonkearns/elm-pages-v3-beta.git
synced 2024-12-26 13:21:42 +03:00
Merge branch 'master' into query-and-fragment
This commit is contained in:
commit
c7d0ddd8e6
@ -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
|
||||||
|
@ -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
|
||||||
|
2
elm.json
2
elm.json
@ -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",
|
||||||
|
@ -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.)
|
||||||
|
4
examples/docs/content/showcase/index.md
Normal file
4
examples/docs/content/showcase/index.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: elm-pages sites showcase
|
||||||
|
type: showcase
|
||||||
|
---
|
@ -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": {
|
||||||
|
@ -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
|
||||||
|
72
examples/docs/src/Feed.elm
Normal file
72
examples/docs/src/Feed.elm
Normal 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
|
18
examples/docs/src/FontAwesome.elm
Normal file
18
examples/docs/src/FontAwesome.elm
Normal 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
|
@ -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
|
||||||
|
@ -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,6 +153,28 @@ view :
|
|||||||
, head : List (Head.Tag Pages.PathKey)
|
, head : List (Head.Tag Pages.PathKey)
|
||||||
}
|
}
|
||||||
view siteMetadata page =
|
view siteMetadata page =
|
||||||
|
case page.frontmatter of
|
||||||
|
Metadata.Showcase ->
|
||||||
|
StaticHttp.map2
|
||||||
|
(\stars showcaseData ->
|
||||||
|
{ view =
|
||||||
|
\model viewForPage ->
|
||||||
|
{ title = "elm-pages blog"
|
||||||
|
, body =
|
||||||
|
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")
|
StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
|
||||||
(D.field "stargazers_count" D.int)
|
(D.field "stargazers_count" D.int)
|
||||||
|> StaticHttp.map
|
|> StaticHttp.map
|
||||||
@ -130,7 +182,7 @@ view siteMetadata page =
|
|||||||
{ view =
|
{ view =
|
||||||
\model viewForPage ->
|
\model viewForPage ->
|
||||||
pageView stars model siteMetadata page viewForPage
|
pageView stars model siteMetadata page viewForPage
|
||||||
|> wrapBody
|
|> wrapBody stars page model
|
||||||
, head = head page.frontmatter
|
, 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,9 +384,14 @@ 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 ]
|
||||||
|
[ responsiveHeader
|
||||||
|
, Element.column
|
||||||
|
[ Element.width Element.fill
|
||||||
|
, Element.htmlAttribute (Attr.class "responsive-desktop")
|
||||||
|
]
|
||||||
[ Element.el
|
[ Element.el
|
||||||
[ Element.height (Element.px 4)
|
[ Element.height (Element.px 4)
|
||||||
, Element.width Element.fill
|
, Element.width Element.fill
|
||||||
@ -333,7 +412,15 @@ header stars currentPath =
|
|||||||
, Element.Border.widthEach { bottom = 1, left = 0, right = 0, top = 0 }
|
, Element.Border.widthEach { bottom = 1, left = 0, right = 0, top = 0 }
|
||||||
, Element.Border.color (Element.rgba255 40 80 40 0.4)
|
, Element.Border.color (Element.rgba255 40 80 40 0.4)
|
||||||
]
|
]
|
||||||
[ Element.link []
|
[ logoLink
|
||||||
|
, Element.row [ Element.spacing 15 ] (navbarLinks stars currentPath)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
logoLink =
|
||||||
|
Element.link []
|
||||||
{ url = "/"
|
{ url = "/"
|
||||||
, label =
|
, label =
|
||||||
Element.row
|
Element.row
|
||||||
@ -345,13 +432,41 @@ header stars currentPath =
|
|||||||
, Element.text "elm-pages"
|
, Element.text "elm-pages"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
, Element.row [ Element.spacing 15 ]
|
|
||||||
|
|
||||||
|
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
|
[ elmDocsLink
|
||||||
, githubRepoLink stars
|
, githubRepoLink stars
|
||||||
, highlightableLink currentPath pages.docs.directory "Docs"
|
, highlightableLink currentPath pages.docs.directory "Docs"
|
||||||
|
, highlightableLink currentPath pages.showcase.directory "Showcase"
|
||||||
, highlightableLink currentPath pages.blog.directory "Blog"
|
, 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 ]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -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,7 +508,8 @@ 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
|
||||||
|
++ (case metadata of
|
||||||
Metadata.Page meta ->
|
Metadata.Page meta ->
|
||||||
Seo.summaryLarge
|
Seo.summaryLarge
|
||||||
{ canonicalUrlOverride = Nothing
|
{ canonicalUrlOverride = Nothing
|
||||||
@ -492,6 +615,23 @@ head metadata =
|
|||||||
}
|
}
|
||||||
|> 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
|
||||||
canonicalSiteUrl =
|
canonicalSiteUrl =
|
||||||
|
@ -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)
|
||||||
|
32
examples/docs/src/MySitemap.elm
Normal file
32
examples/docs/src/MySitemap.elm
Normal 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 }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
161
examples/docs/src/Showcase.elm
Normal file
161
examples/docs/src/Showcase.elm
Normal 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"
|
||||||
|
]
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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: {
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
() => {}
|
() => {}
|
||||||
);
|
);
|
||||||
|
@ -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}"
|
||||||
} )
|
} )
|
||||||
`;
|
`;
|
||||||
|
@ -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(
|
||||||
|
@ -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>
|
||||||
|
20
index.js
20
index.js
@ -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
10
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
44
src/Head.elm
44
src/Head.elm
@ -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" />`
|
||||||
|
|
||||||
|
@ -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,14 +150,14 @@ 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
|
||||||
@ -167,22 +168,55 @@ parseMetadata document content =
|
|||||||
|> documentEntry.frontmatterParser
|
|> documentEntry.frontmatterParser
|
||||||
|> Result.map
|
|> Result.map
|
||||||
(\metadata ->
|
(\metadata ->
|
||||||
-- TODO do I need to handle this case?
|
let
|
||||||
-- case body of
|
renderer =
|
||||||
-- Just presentBody ->
|
\value ->
|
||||||
-- Parsed metadata
|
parseContent extension value document
|
||||||
-- { body = parseContent extension presentBody document
|
in
|
||||||
-- , staticData = ""
|
case maybeInitialPageContent of
|
||||||
-- }
|
Just { contentJson, initialUrl } ->
|
||||||
--
|
if normalizePath initialUrl.path == (String.join "/" path |> normalizePath) then
|
||||||
-- Nothing ->
|
Parsed metadata
|
||||||
|
{ body = renderer contentJson.body
|
||||||
|
, staticData = contentJson.staticData
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
NeedContent extension metadata
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
NeedContent extension metadata
|
NeedContent extension metadata
|
||||||
)
|
)
|
||||||
|
|> Tuple.pair path
|
||||||
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
Err ("Could not find extension '" ++ extension ++ "'")
|
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 :
|
||||||
@ -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
|
||||||
|
@ -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 :
|
||||||
|
@ -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,12 +150,7 @@ type Msg
|
|||||||
= GotStaticHttpResponse { request : { masked : RequestDetails, unmasked : RequestDetails }, response : Result Http.Error String }
|
= GotStaticHttpResponse { request : { masked : RequestDetails, unmasked : RequestDetails }, response : Result Http.Error String }
|
||||||
|
|
||||||
|
|
||||||
cliApplication :
|
type alias Config pathKey userMsg userModel metadata view =
|
||||||
(Msg -> msg)
|
|
||||||
-> (msg -> Maybe Msg)
|
|
||||||
-> (Model -> model)
|
|
||||||
-> (model -> Maybe Model)
|
|
||||||
->
|
|
||||||
{ init :
|
{ init :
|
||||||
Maybe
|
Maybe
|
||||||
{ path : PagePath pathKey
|
{ path : PagePath pathKey
|
||||||
@ -158,6 +175,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 :
|
||||||
@ -167,11 +197,19 @@ cliApplication :
|
|||||||
}
|
}
|
||||||
-> userMsg
|
-> userMsg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
cliApplication :
|
||||||
|
(Msg -> msg)
|
||||||
|
-> (msg -> Maybe Msg)
|
||||||
|
-> (Model -> model)
|
||||||
|
-> (model -> Maybe Model)
|
||||||
|
-> Config pathKey userMsg userModel metadata view
|
||||||
-> 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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user