Merge branch 'master' into query-and-fragment

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

View File

@ -9,6 +9,27 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]
## [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

View File

@ -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

View File

@ -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",

View File

@ -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.)

View File

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

View File

@ -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": {

View File

@ -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

View File

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

View File

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

View File

@ -20,15 +20,6 @@ view posts =
|> List.filterMap
(\( 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

View File

@ -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 =

View File

@ -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)

View File

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

View File

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

View File

@ -1,4 +1,5 @@
@import url("https://fonts.googleapis.com/css?family=Montserrat:400,700|Roboto&display=swap");
@import url("https://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;
}
}

View File

@ -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
};
});
});
}
};

View File

@ -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: {

View File

@ -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
},
() => {}
);

View File

@ -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}"
} )
`;

View File

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

View File

@ -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>

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -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")

View File

@ -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

View File

@ -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 :

View File

@ -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
)
)

View File

@ -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

View File

@ -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