mirror of
https://github.com/dillonkearns/elm-pages-v3-beta.git
synced 2024-12-25 21:02:33 +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]
|
||||
|
||||
## [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
|
||||
|
||||
### Fixed
|
||||
|
@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||
|
||||
## [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
|
||||
|
||||
### Added
|
||||
|
2
elm.json
2
elm.json
@ -3,7 +3,7 @@
|
||||
"name": "dillonkearns/elm-pages",
|
||||
"summary": "A statically typed site generator.",
|
||||
"license": "BSD-3-Clause",
|
||||
"version": "1.1.1",
|
||||
"version": "2.0.0",
|
||||
"exposed-modules": [
|
||||
"Head",
|
||||
"Head.Seo",
|
||||
|
@ -73,7 +73,7 @@ Here are some links:
|
||||
|
||||
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!
|
||||
|
||||
@ -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:
|
||||
|
||||
- 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)
|
||||
- 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.)
|
||||
|
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": {
|
||||
"direct": {
|
||||
"avh4/elm-color": "1.0.0",
|
||||
"billstclair/elm-xml-eeue56": "1.0.1",
|
||||
"dillonkearns/elm-markdown": "1.1.3",
|
||||
"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/core": "1.0.2",
|
||||
"elm/html": "1.0.0",
|
||||
@ -42,7 +46,10 @@
|
||||
"elm/regex": "1.0.0",
|
||||
"elm/virtual-dom": "1.0.2",
|
||||
"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": {
|
||||
|
@ -4,8 +4,8 @@ import Element exposing (Element)
|
||||
import Element.Border as Border
|
||||
import Element.Font
|
||||
import Metadata exposing (Metadata)
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Pages
|
||||
import Pages.PagePath as PagePath exposing (PagePath)
|
||||
import Palette
|
||||
|
||||
|
||||
@ -25,19 +25,10 @@ view currentPage posts =
|
||||
|> List.filterMap
|
||||
(\( path, metadata ) ->
|
||||
case metadata of
|
||||
Metadata.Page meta ->
|
||||
Nothing
|
||||
|
||||
Metadata.Article meta ->
|
||||
Nothing
|
||||
|
||||
Metadata.Author _ ->
|
||||
Nothing
|
||||
|
||||
Metadata.Doc meta ->
|
||||
Just ( currentPage == path, path, meta )
|
||||
|
||||
Metadata.BlogIndex ->
|
||||
_ ->
|
||||
Nothing
|
||||
)
|
||||
|> 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
|
||||
(\( path, metadata ) ->
|
||||
case metadata of
|
||||
Metadata.Page meta ->
|
||||
Nothing
|
||||
|
||||
Metadata.Doc meta ->
|
||||
Nothing
|
||||
|
||||
Metadata.Author _ ->
|
||||
Nothing
|
||||
|
||||
Metadata.Article meta ->
|
||||
if meta.draft then
|
||||
Nothing
|
||||
@ -36,7 +27,7 @@ view posts =
|
||||
else
|
||||
Just ( path, meta )
|
||||
|
||||
Metadata.BlogIndex ->
|
||||
_ ->
|
||||
Nothing
|
||||
)
|
||||
|> List.sortBy
|
||||
|
@ -8,8 +8,11 @@ import DocumentSvg
|
||||
import Element exposing (Element)
|
||||
import Element.Background
|
||||
import Element.Border
|
||||
import Element.Events
|
||||
import Element.Font as Font
|
||||
import Element.Region
|
||||
import Feed
|
||||
import FontAwesome
|
||||
import Head
|
||||
import Head.Seo as Seo
|
||||
import Html exposing (Html)
|
||||
@ -19,6 +22,7 @@ import Json.Decode as Decode exposing (Decoder)
|
||||
import Json.Decode.Exploration as D
|
||||
import MarkdownRenderer
|
||||
import Metadata exposing (Metadata)
|
||||
import MySitemap
|
||||
import Pages exposing (images, pages)
|
||||
import Pages.Directory as Directory exposing (Directory)
|
||||
import Pages.Document
|
||||
@ -30,6 +34,7 @@ import Pages.Platform exposing (Page)
|
||||
import Pages.StaticHttp as StaticHttp
|
||||
import Palette
|
||||
import Secrets
|
||||
import Showcase
|
||||
|
||||
|
||||
manifest : Manifest.Config Pages.PathKey
|
||||
@ -62,11 +67,31 @@ main =
|
||||
, documents = [ markdownDocument ]
|
||||
, manifest = manifest
|
||||
, canonicalSiteUrl = canonicalSiteUrl
|
||||
, generateFiles = generateFiles
|
||||
, onPageChange = OnPageChange
|
||||
, 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 =
|
||||
Pages.Document.parser
|
||||
@ -77,7 +102,8 @@ markdownDocument =
|
||||
|
||||
|
||||
type alias Model =
|
||||
{}
|
||||
{ showMobileMenu : Bool
|
||||
}
|
||||
|
||||
|
||||
init :
|
||||
@ -88,7 +114,7 @@ init :
|
||||
}
|
||||
-> ( Model, Cmd Msg )
|
||||
init maybePagePath =
|
||||
( Model, Cmd.none )
|
||||
( Model False, Cmd.none )
|
||||
|
||||
|
||||
type Msg
|
||||
@ -97,13 +123,17 @@ type Msg
|
||||
, query : Maybe String
|
||||
, fragment : Maybe String
|
||||
}
|
||||
| ToggleMobileMenu
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
OnPageChange page ->
|
||||
( model, Cmd.none )
|
||||
( { model | showMobileMenu = False }, Cmd.none )
|
||||
|
||||
ToggleMobileMenu ->
|
||||
( { model | showMobileMenu = not model.showMobileMenu }, Cmd.none )
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
@ -123,6 +153,28 @@ view :
|
||||
, head : List (Head.Tag Pages.PathKey)
|
||||
}
|
||||
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")
|
||||
(D.field "stargazers_count" D.int)
|
||||
|> StaticHttp.map
|
||||
@ -130,7 +182,7 @@ view siteMetadata page =
|
||||
{ view =
|
||||
\model viewForPage ->
|
||||
pageView stars model siteMetadata page viewForPage
|
||||
|> wrapBody
|
||||
|> wrapBody stars page model
|
||||
, head = head page.frontmatter
|
||||
}
|
||||
)
|
||||
@ -186,8 +238,7 @@ pageView stars model siteMetadata page viewForPage =
|
||||
Metadata.Page metadata ->
|
||||
{ title = metadata.title
|
||||
, body =
|
||||
[ header stars page.path
|
||||
, Element.column
|
||||
[ Element.column
|
||||
[ Element.padding 50
|
||||
, Element.spacing 60
|
||||
, Element.Region.mainContent
|
||||
@ -203,8 +254,7 @@ pageView stars model siteMetadata page viewForPage =
|
||||
{ title = metadata.title
|
||||
, body =
|
||||
Element.column [ Element.width Element.fill ]
|
||||
[ header stars page.path
|
||||
, Element.column
|
||||
[ Element.column
|
||||
[ Element.padding 30
|
||||
, Element.spacing 40
|
||||
, Element.Region.mainContent
|
||||
@ -234,8 +284,7 @@ pageView stars model siteMetadata page viewForPage =
|
||||
Metadata.Doc metadata ->
|
||||
{ title = metadata.title
|
||||
, body =
|
||||
[ header stars page.path
|
||||
, Element.row []
|
||||
[ Element.row []
|
||||
[ DocSidebar.view page.path siteMetadata
|
||||
|> 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 ]
|
||||
@ -264,8 +313,7 @@ pageView stars model siteMetadata page viewForPage =
|
||||
Element.column
|
||||
[ Element.width Element.fill
|
||||
]
|
||||
[ header stars page.path
|
||||
, Element.column
|
||||
[ Element.column
|
||||
[ Element.padding 30
|
||||
, Element.spacing 20
|
||||
, Element.Region.mainContent
|
||||
@ -283,15 +331,41 @@ pageView stars model siteMetadata page viewForPage =
|
||||
{ title = "elm-pages blog"
|
||||
, body =
|
||||
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 =
|
||||
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.width Element.fill
|
||||
, 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 =
|
||||
Element.column [ Element.width Element.fill ]
|
||||
[ responsiveHeader
|
||||
, Element.column
|
||||
[ Element.width Element.fill
|
||||
, Element.htmlAttribute (Attr.class "responsive-desktop")
|
||||
]
|
||||
[ Element.el
|
||||
[ Element.height (Element.px 4)
|
||||
, Element.width Element.fill
|
||||
@ -333,7 +412,15 @@ header stars currentPath =
|
||||
, Element.Border.widthEach { bottom = 1, left = 0, right = 0, top = 0 }
|
||||
, 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 = "/"
|
||||
, label =
|
||||
Element.row
|
||||
@ -345,13 +432,41 @@ header stars currentPath =
|
||||
, 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
|
||||
, 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 ]
|
||||
]
|
||||
|
||||
|
||||
@ -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://htmlhead.dev>
|
||||
<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 =
|
||||
case metadata of
|
||||
commonHeadTags
|
||||
++ (case metadata of
|
||||
Metadata.Page meta ->
|
||||
Seo.summaryLarge
|
||||
{ canonicalUrlOverride = Nothing
|
||||
@ -492,6 +615,23 @@ head metadata =
|
||||
}
|
||||
|> 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 =
|
||||
|
@ -17,6 +17,7 @@ type Metadata
|
||||
| Doc DocMetadata
|
||||
| Author Data.Author.Author
|
||||
| BlogIndex
|
||||
| Showcase
|
||||
|
||||
|
||||
type alias ArticleMetadata =
|
||||
@ -54,6 +55,9 @@ decoder =
|
||||
"blog-index" ->
|
||||
Decode.succeed BlogIndex
|
||||
|
||||
"showcase" ->
|
||||
Decode.succeed Showcase
|
||||
|
||||
"author" ->
|
||||
Decode.map3 Data.Author.Author
|
||||
(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://use.fontawesome.com/releases/v5.9.0/css/all.css");
|
||||
|
||||
.dotted-line {
|
||||
-webkit-animation: animation-yweh2o 400ms linear infinite;
|
||||
@ -26,3 +27,14 @@
|
||||
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 {
|
||||
constructor(data) {
|
||||
constructor(data, filesToGenerate) {
|
||||
this.pagesWithRequests = data;
|
||||
this.filesToGenerate = filesToGenerate;
|
||||
console.log('this.filesToGenerate', this.filesToGenerate);
|
||||
|
||||
}
|
||||
apply(compiler) {
|
||||
compiler.hooks.emit.tap("AddFilesPlugin", compilation => {
|
||||
@ -52,6 +55,18 @@ module.exports = class AddFilesPlugin {
|
||||
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 path = require("path");
|
||||
const HTMLWebpackPlugin = require("html-webpack-plugin");
|
||||
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
|
||||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
const PrerenderSPAPlugin = require("prerender-spa-plugin");
|
||||
const merge = require("webpack-merge");
|
||||
@ -15,11 +16,12 @@ const ClosurePlugin = require("closure-webpack-plugin");
|
||||
const readline = require("readline");
|
||||
|
||||
module.exports = { start, run };
|
||||
function start({ routes, debug, customPort, manifestConfig, routesWithRequests }) {
|
||||
function start({ routes, debug, customPort, manifestConfig, routesWithRequests, filesToGenerate }) {
|
||||
const config = webpackOptions(false, routes, {
|
||||
debug,
|
||||
manifestConfig,
|
||||
routesWithRequests
|
||||
routesWithRequests,
|
||||
filesToGenerate
|
||||
});
|
||||
|
||||
const compiler = webpack(config);
|
||||
@ -65,12 +67,13 @@ function start({ routes, debug, customPort, manifestConfig, routesWithRequests }
|
||||
// app.use(express.static(__dirname + "/path-to-static-folder"));
|
||||
}
|
||||
|
||||
function run({ routes, manifestConfig, routesWithRequests }, callback) {
|
||||
function run({ routes, manifestConfig, routesWithRequests, filesToGenerate }, callback) {
|
||||
webpack(
|
||||
webpackOptions(true, routes, {
|
||||
debug: false,
|
||||
manifestConfig,
|
||||
routesWithRequests
|
||||
routesWithRequests,
|
||||
filesToGenerate
|
||||
})
|
||||
).run((err, stats) => {
|
||||
if (err) {
|
||||
@ -118,12 +121,12 @@ function printProgress(progress, message) {
|
||||
function webpackOptions(
|
||||
production,
|
||||
routes,
|
||||
{ debug, manifestConfig, routesWithRequests }
|
||||
{ debug, manifestConfig, routesWithRequests, filesToGenerate }
|
||||
) {
|
||||
const common = {
|
||||
mode: production ? "production" : "development",
|
||||
plugins: [
|
||||
new AddFilesPlugin(routesWithRequests),
|
||||
new AddFilesPlugin(routesWithRequests, filesToGenerate),
|
||||
new CopyPlugin([
|
||||
{
|
||||
from: "static/**/*",
|
||||
@ -159,6 +162,10 @@ function webpackOptions(
|
||||
inject: "head",
|
||||
template: path.resolve(__dirname, "template.html")
|
||||
}),
|
||||
new ScriptExtHtmlWebpackPlugin({
|
||||
preload: /\.js$/,
|
||||
defaultAttribute: 'defer'
|
||||
}),
|
||||
new FaviconsWebpackPlugin({
|
||||
logo: path.resolve(process.cwd(), `./${manifestConfig.sourceIcon}`),
|
||||
favicons: {
|
||||
|
@ -86,6 +86,7 @@ function run() {
|
||||
markdownContent,
|
||||
content,
|
||||
function(payload) {
|
||||
console.log('@@@@@@@@@ filesToGenerate', payload.filesToGenerate);
|
||||
if (contents.watch) {
|
||||
startWatchIfNeeded();
|
||||
if (!devServerRunning) {
|
||||
@ -94,7 +95,9 @@ function run() {
|
||||
routes,
|
||||
debug: contents.debug,
|
||||
manifestConfig: payload.manifest,
|
||||
routesWithRequests: payload.pages
|
||||
routesWithRequests: payload.pages,
|
||||
filesToGenerate: payload.filesToGenerate,
|
||||
customPort: contents.customPort
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@ -106,7 +109,8 @@ function run() {
|
||||
{
|
||||
routes,
|
||||
manifestConfig: payload.manifest,
|
||||
routesWithRequests: payload.pages
|
||||
routesWithRequests: payload.pages,
|
||||
filesToGenerate: payload.filesToGenerate
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
|
@ -19,8 +19,8 @@ function toEntry(entry, includeBody) {
|
||||
|
||||
return `
|
||||
( [${fullPath.join(", ")}]
|
||||
, { frontMatter = """${entry.metadata}
|
||||
""" , body = ${body(entry, includeBody)}
|
||||
, { frontMatter = ${JSON.stringify(entry.metadata)}
|
||||
, body = ${body(entry, includeBody)}
|
||||
, extension = "${extension}"
|
||||
} )
|
||||
`;
|
||||
|
@ -4,7 +4,7 @@ workbox.precaching.precacheAndRoute(self.__precacheManifest);
|
||||
workbox.routing.registerNavigationRoute(
|
||||
workbox.precaching.getCacheKeyForURL("/index.html"),
|
||||
{
|
||||
blacklist: [/admin/]
|
||||
blacklist: [/admin/, /\./]
|
||||
}
|
||||
);
|
||||
workbox.routing.registerRoute(
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="preload" href="content.json" as="fetch" crossorigin />
|
||||
<link rel="preload" href="./content.json" as="fetch" crossorigin />
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script>
|
||||
|
20
index.js
20
index.js
@ -7,9 +7,13 @@ module.exports = function pagesInit(
|
||||
let prefetchedPages = [window.location.pathname];
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
httpGet(`${window.location.origin}${window.location.pathname}/content.json`, function (/** @type JSON */ contentJson) {
|
||||
|
||||
let app = mainElmModule.init({
|
||||
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"));
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
function setupLinkPrefetching() {
|
||||
@ -130,3 +137,14 @@ module.exports = function pagesInit(
|
||||
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",
|
||||
"version": "1.1.6",
|
||||
"version": "1.2.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@ -9220,6 +9220,14 @@
|
||||
"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": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "elm-pages",
|
||||
"version": "1.1.6",
|
||||
"version": "1.2.0",
|
||||
"homepage": "http://elm-pages.com",
|
||||
"description": "Type-safe static sites, written in pure elm with your own custom elm-markup syntax.",
|
||||
"main": "index.js",
|
||||
@ -42,6 +42,7 @@
|
||||
"node-sass": "^4.12.0",
|
||||
"prerender-spa-plugin": "^3.4.0",
|
||||
"sass-loader": "^8.0.0",
|
||||
"script-ext-html-webpack-plugin": "^2.1.4",
|
||||
"style-loader": "^1.0.0",
|
||||
"webpack": "^4.41.5",
|
||||
"webpack-dev-middleware": "^3.7.0",
|
||||
|
42
src/Head.elm
42
src/Head.elm
@ -1,5 +1,6 @@
|
||||
module Head exposing
|
||||
( Tag, metaName, metaProperty
|
||||
, rssLink, sitemapLink
|
||||
, AttributeValue
|
||||
, currentPageFullUrl, fullImageUrl, fullPageUrl, raw
|
||||
, 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`.
|
||||
|
||||
@docs Tag, metaName, metaProperty
|
||||
@docs rssLink, sitemapLink
|
||||
|
||||
|
||||
## `AttributeValue`s
|
||||
@ -106,6 +108,46 @@ 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:
|
||||
|
||||
Head.metaProperty "fb:app_id" (Head.raw "123456789")
|
||||
|
@ -111,9 +111,10 @@ pagesWithErrors cache =
|
||||
init :
|
||||
Document metadata view
|
||||
-> Content
|
||||
-> Maybe { contentJson : ContentJson String, initialUrl : Url }
|
||||
-> ContentCache metadata view
|
||||
init document content =
|
||||
parseMetadata document content
|
||||
init document content maybeInitialPageContent =
|
||||
parseMetadata maybeInitialPageContent document content
|
||||
|> List.map
|
||||
(\tuple ->
|
||||
Tuple.mapSecond
|
||||
@ -149,14 +150,14 @@ createBuildError path decodeError =
|
||||
|
||||
|
||||
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, Result String (Entry metadata view) )
|
||||
parseMetadata document content =
|
||||
parseMetadata maybeInitialPageContent document content =
|
||||
content
|
||||
|> List.map
|
||||
(Tuple.mapSecond
|
||||
(\{ frontMatter, extension, body } ->
|
||||
(\( path, { frontMatter, extension, body } ) ->
|
||||
let
|
||||
maybeDocumentEntry =
|
||||
Document.get extension document
|
||||
@ -167,22 +168,55 @@ parseMetadata document content =
|
||||
|> documentEntry.frontmatterParser
|
||||
|> Result.map
|
||||
(\metadata ->
|
||||
-- TODO do I need to handle this case?
|
||||
-- case body of
|
||||
-- Just presentBody ->
|
||||
-- Parsed metadata
|
||||
-- { body = parseContent extension presentBody document
|
||||
-- , staticData = ""
|
||||
-- }
|
||||
--
|
||||
-- Nothing ->
|
||||
let
|
||||
renderer =
|
||||
\value ->
|
||||
parseContent extension value document
|
||||
in
|
||||
case maybeInitialPageContent of
|
||||
Just { contentJson, initialUrl } ->
|
||||
if normalizePath initialUrl.path == (String.join "/" path |> normalizePath) then
|
||||
Parsed metadata
|
||||
{ body = renderer contentJson.body
|
||||
, staticData = contentJson.staticData
|
||||
}
|
||||
|
||||
else
|
||||
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 :
|
||||
@ -327,8 +361,8 @@ lazyLoad document url cacheResult =
|
||||
|> Task.map
|
||||
(\downloadedContent ->
|
||||
update cacheResult
|
||||
(\thing ->
|
||||
parseContent extension thing document
|
||||
(\value ->
|
||||
parseContent extension value document
|
||||
)
|
||||
url
|
||||
downloadedContent
|
||||
|
@ -217,6 +217,12 @@ type alias Flags =
|
||||
Decode.Value
|
||||
|
||||
|
||||
type alias ContentJson =
|
||||
{ body : String
|
||||
, staticData : Dict String String
|
||||
}
|
||||
|
||||
|
||||
init :
|
||||
pathKey
|
||||
-> String
|
||||
@ -256,11 +262,33 @@ init :
|
||||
init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel flags url key =
|
||||
let
|
||||
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
|
||||
case contentCache of
|
||||
Ok okCache ->
|
||||
let
|
||||
phase =
|
||||
case Decode.decodeValue (Decode.field "isPrerendering" Decode.bool) flags of
|
||||
Ok True ->
|
||||
Prerender
|
||||
|
||||
Ok False ->
|
||||
Client
|
||||
|
||||
Err _ ->
|
||||
Client
|
||||
|
||||
( userModel, userCmd ) =
|
||||
initUserModel
|
||||
(maybePagePath
|
||||
@ -300,6 +328,7 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
|
||||
, url = url
|
||||
, userModel = userModel
|
||||
, contentCache = contentCache
|
||||
, phase = phase
|
||||
}
|
||||
, cmd
|
||||
)
|
||||
@ -313,6 +342,7 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla
|
||||
, url = url
|
||||
, userModel = userModel
|
||||
, contentCache = contentCache
|
||||
, phase = Client
|
||||
}
|
||||
, Cmd.batch
|
||||
[ userCmd |> Cmd.map UserMsg
|
||||
@ -349,9 +379,15 @@ type alias ModelDetails userModel metadata view =
|
||||
, url : Url.Url
|
||||
, contentCache : ContentCache metadata view
|
||||
, userModel : userModel
|
||||
, phase : Phase
|
||||
}
|
||||
|
||||
|
||||
type Phase
|
||||
= Prerender
|
||||
| Client
|
||||
|
||||
|
||||
update :
|
||||
String
|
||||
->
|
||||
@ -530,6 +566,19 @@ application :
|
||||
, 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 :
|
||||
@ -562,7 +611,20 @@ application config =
|
||||
\msg outerModel ->
|
||||
case outerModel of
|
||||
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.mapSecond (Cmd.map AppMsg)
|
||||
|
||||
@ -608,6 +670,19 @@ cliApplication :
|
||||
, 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 :
|
||||
|
@ -44,6 +44,13 @@ type ToJsPayload pathKey
|
||||
type alias ToJsSuccessPayload pathKey =
|
||||
{ pages : Dict String (Dict String String)
|
||||
, manifest : Manifest.Config pathKey
|
||||
, filesToGenerate : List FileToGenerate
|
||||
}
|
||||
|
||||
|
||||
type alias FileToGenerate =
|
||||
{ path : List String
|
||||
, content : String
|
||||
}
|
||||
|
||||
|
||||
@ -55,8 +62,8 @@ toJsCodec =
|
||||
Errors errorList ->
|
||||
errors errorList
|
||||
|
||||
Success { pages, manifest } ->
|
||||
success (ToJsSuccessPayload pages manifest)
|
||||
Success { pages, manifest, filesToGenerate } ->
|
||||
success (ToJsSuccessPayload pages manifest filesToGenerate)
|
||||
)
|
||||
|> Codec.variant1 "Errors" Errors Codec.string
|
||||
|> Codec.variant1 "Success"
|
||||
@ -90,6 +97,21 @@ successCodec =
|
||||
|> Codec.field "manifest"
|
||||
.manifest
|
||||
(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
|
||||
|
||||
|
||||
@ -128,12 +150,7 @@ type Msg
|
||||
= GotStaticHttpResponse { request : { masked : RequestDetails, unmasked : RequestDetails }, response : Result Http.Error String }
|
||||
|
||||
|
||||
cliApplication :
|
||||
(Msg -> msg)
|
||||
-> (msg -> Maybe Msg)
|
||||
-> (Model -> model)
|
||||
-> (model -> Maybe Model)
|
||||
->
|
||||
type alias Config pathKey userMsg userModel metadata view =
|
||||
{ init :
|
||||
Maybe
|
||||
{ path : PagePath pathKey
|
||||
@ -158,6 +175,19 @@ cliApplication :
|
||||
, 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 :
|
||||
@ -167,11 +197,19 @@ cliApplication :
|
||||
}
|
||||
-> userMsg
|
||||
}
|
||||
|
||||
|
||||
cliApplication :
|
||||
(Msg -> msg)
|
||||
-> (msg -> Maybe Msg)
|
||||
-> (Model -> model)
|
||||
-> (model -> Maybe Model)
|
||||
-> Config pathKey userMsg userModel metadata view
|
||||
-> Platform.Program Flags model msg
|
||||
cliApplication cliMsgConstructor narrowMsg toModel fromModel config =
|
||||
let
|
||||
contentCache =
|
||||
ContentCache.init config.document config.content
|
||||
ContentCache.init config.document config.content Nothing
|
||||
|
||||
siteMetadata =
|
||||
contentCache
|
||||
@ -188,7 +226,7 @@ cliApplication cliMsgConstructor narrowMsg toModel fromModel config =
|
||||
\msg model ->
|
||||
case ( narrowMsg msg, fromModel model ) of
|
||||
( Just cliMsg, Just cliModel ) ->
|
||||
update config cliMsg cliModel
|
||||
update siteMetadata config cliMsg cliModel
|
||||
|> Tuple.mapSecond (perform cliMsgConstructor config.toJsPort)
|
||||
|> Tuple.mapFirst toModel
|
||||
|
||||
@ -259,21 +297,7 @@ init :
|
||||
(Model -> model)
|
||||
-> ContentCache.ContentCache metadata view
|
||||
-> Result (List BuildError) (List ( PagePath pathKey, metadata ))
|
||||
->
|
||||
{ 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
|
||||
}
|
||||
-> Config pathKey userMsg userModel metadata view
|
||||
-> Decode.Value
|
||||
-> ( model, Effect pathKey )
|
||||
init toModel contentCache siteMetadata config flags =
|
||||
@ -309,7 +333,7 @@ init toModel contentCache siteMetadata config flags =
|
||||
staticResponsesInit []
|
||||
|
||||
( updatedRawResponses, effect ) =
|
||||
sendStaticResponsesIfDone mode secrets Dict.empty [] staticResponses config.manifest
|
||||
sendStaticResponsesIfDone config siteMetadata mode secrets Dict.empty [] staticResponses
|
||||
in
|
||||
( Model staticResponses secrets [] updatedRawResponses mode |> toModel
|
||||
, effect
|
||||
@ -335,6 +359,8 @@ init toModel contentCache siteMetadata config flags =
|
||||
staticResponsesInit []
|
||||
in
|
||||
updateAndSendPortIfDone
|
||||
config
|
||||
siteMetadata
|
||||
(Model
|
||||
staticResponses
|
||||
secrets
|
||||
@ -343,10 +369,11 @@ init toModel contentCache siteMetadata config flags =
|
||||
mode
|
||||
)
|
||||
toModel
|
||||
config.manifest
|
||||
|
||||
Err metadataParserErrors ->
|
||||
updateAndSendPortIfDone
|
||||
config
|
||||
siteMetadata
|
||||
(Model Dict.empty
|
||||
secrets
|
||||
(metadataParserErrors |> List.map Tuple.second)
|
||||
@ -354,10 +381,11 @@ init toModel contentCache siteMetadata config flags =
|
||||
mode
|
||||
)
|
||||
toModel
|
||||
config.manifest
|
||||
|
||||
Err error ->
|
||||
updateAndSendPortIfDone
|
||||
config
|
||||
siteMetadata
|
||||
(Model Dict.empty
|
||||
SecretsDict.masked
|
||||
[ { title = "Internal Error"
|
||||
@ -368,20 +396,25 @@ init toModel contentCache siteMetadata config flags =
|
||||
Dev
|
||||
)
|
||||
toModel
|
||||
config.manifest
|
||||
|
||||
|
||||
updateAndSendPortIfDone : Model -> (Model -> model) -> Manifest.Config pathKey -> ( model, Effect pathKey )
|
||||
updateAndSendPortIfDone model toModel manifest =
|
||||
updateAndSendPortIfDone :
|
||||
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
|
||||
( updatedAllRawResponses, effect ) =
|
||||
sendStaticResponsesIfDone
|
||||
config
|
||||
siteMetadata
|
||||
model.mode
|
||||
model.secrets
|
||||
model.allRawResponses
|
||||
model.errors
|
||||
model.staticResponses
|
||||
manifest
|
||||
in
|
||||
( { model | allRawResponses = updatedAllRawResponses } |> toModel
|
||||
, effect
|
||||
@ -393,24 +426,12 @@ type alias PageErrors =
|
||||
|
||||
|
||||
update :
|
||||
{ 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
|
||||
}
|
||||
Result (List BuildError) (List ( PagePath pathKey, metadata ))
|
||||
-> Config pathKey userMsg userModel metadata view
|
||||
-> Msg
|
||||
-> Model
|
||||
-> ( Model, Effect pathKey )
|
||||
update config msg model =
|
||||
update siteMetadata config msg model =
|
||||
case msg of
|
||||
GotStaticHttpResponse { request, response } ->
|
||||
let
|
||||
@ -467,7 +488,7 @@ update config msg model =
|
||||
}
|
||||
|
||||
( 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
|
||||
( { updatedModel | allRawResponses = updatedAllRawResponses }
|
||||
, effect
|
||||
@ -596,8 +617,16 @@ isJust maybeValue =
|
||||
False
|
||||
|
||||
|
||||
sendStaticResponsesIfDone : Mode -> SecretsDict -> Dict String (Maybe String) -> List BuildError -> StaticResponses -> Manifest.Config pathKey -> ( Dict String (Maybe String), Effect pathKey )
|
||||
sendStaticResponsesIfDone mode secrets allRawResponses errors staticResponses manifest =
|
||||
sendStaticResponsesIfDone :
|
||||
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
|
||||
pendingRequests =
|
||||
staticResponses
|
||||
@ -748,18 +777,86 @@ sendStaticResponsesIfDone mode secrets allRawResponses errors staticResponses ma
|
||||
let
|
||||
updatedAllRawResponses =
|
||||
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
|
||||
( updatedAllRawResponses
|
||||
, SendJsData
|
||||
(if List.isEmpty errors && List.isEmpty failedRequests then
|
||||
(if List.isEmpty allErrors then
|
||||
Success
|
||||
(ToJsSuccessPayload
|
||||
(encodeStaticResponses mode staticResponses)
|
||||
manifest
|
||||
config.manifest
|
||||
generatedOkayFiles
|
||||
)
|
||||
|
||||
else
|
||||
Errors <| BuildError.errorsToString (failedRequests ++ errors)
|
||||
Errors <| BuildError.errorsToString allErrors
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -83,6 +83,19 @@ application :
|
||||
}
|
||||
, documents : List ( String, Document.DocumentHandler metadata view )
|
||||
, manifest : Pages.Manifest.Config pathKey
|
||||
, generateFiles :
|
||||
List
|
||||
{ path : PagePath pathKey
|
||||
, frontmatter : metadata
|
||||
, body : String
|
||||
}
|
||||
->
|
||||
List
|
||||
(Result String
|
||||
{ path : List String
|
||||
, content : String
|
||||
}
|
||||
)
|
||||
, onPageChange :
|
||||
{ path : PagePath pathKey
|
||||
, query : Maybe String
|
||||
@ -108,6 +121,7 @@ application config =
|
||||
, subscriptions = config.subscriptions
|
||||
, document = Document.fromList config.documents
|
||||
, content = config.internals.content
|
||||
, generateFiles = config.generateFiles
|
||||
, toJsPort = config.internals.toJsPort
|
||||
, manifest = config.manifest
|
||||
, canonicalSiteUrl = config.canonicalSiteUrl
|
||||
|
@ -564,9 +564,17 @@ start pages =
|
||||
Debug.todo "Couldn't find page"
|
||||
}
|
||||
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
|
||||
{ init = Main.init identity contentCache siteMetadata config
|
||||
, update = Main.update config
|
||||
, update = Main.update siteMetadata config
|
||||
, view = \_ -> { title = "", body = [] }
|
||||
}
|
||||
|> ProgramTest.withSimulatedEffects simulateEffects
|
||||
|
Loading…
Reference in New Issue
Block a user