diff --git a/CHANGELOG-ELM.md b/CHANGELOG-ELM.md index 3a900894..50037c08 100644 --- a/CHANGELOG-ELM.md +++ b/CHANGELOG-ELM.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +## [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 diff --git a/CHANGELOG-NPM.md b/CHANGELOG-NPM.md index 2840e4ea..67bf0dd9 100644 --- a/CHANGELOG-NPM.md +++ b/CHANGELOG-NPM.md @@ -9,6 +9,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### 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.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 diff --git a/elm.json b/elm.json index 800f82b8..f060aac6 100644 --- a/elm.json +++ b/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": "1.1.2", "exposed-modules": [ "Head", "Head.Seo", diff --git a/examples/docs/content/blog/extensible-markdown-parsing-in-elm.md b/examples/docs/content/blog/extensible-markdown-parsing-in-elm.md index 8ba645fd..e6c72e09 100644 --- a/examples/docs/content/blog/extensible-markdown-parsing-in-elm.md +++ b/examples/docs/content/blog/extensible-markdown-parsing-in-elm.md @@ -73,7 +73,7 @@ Here are some links: And here's the output: - + 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.) diff --git a/examples/docs/content/showcase/index.md b/examples/docs/content/showcase/index.md new file mode 100644 index 00000000..0157c4af --- /dev/null +++ b/examples/docs/content/showcase/index.md @@ -0,0 +1,4 @@ +--- +title: elm-pages sites showcase +type: showcase +--- diff --git a/examples/docs/src/DocSidebar.elm b/examples/docs/src/DocSidebar.elm index a6fad82b..8eb136ff 100644 --- a/examples/docs/src/DocSidebar.elm +++ b/examples/docs/src/DocSidebar.elm @@ -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 diff --git a/examples/docs/src/FontAwesome.elm b/examples/docs/src/FontAwesome.elm new file mode 100644 index 00000000..a8150b77 --- /dev/null +++ b/examples/docs/src/FontAwesome.elm @@ -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 diff --git a/examples/docs/src/Index.elm b/examples/docs/src/Index.elm index eec52f9c..ac439780 100644 --- a/examples/docs/src/Index.elm +++ b/examples/docs/src/Index.elm @@ -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 diff --git a/examples/docs/src/Main.elm b/examples/docs/src/Main.elm index 6cd8a35f..98eaa276 100644 --- a/examples/docs/src/Main.elm +++ b/examples/docs/src/Main.elm @@ -8,9 +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) @@ -32,6 +34,7 @@ import Pages.Platform exposing (Page) import Pages.StaticHttp as StaticHttp import Palette import Secrets +import Showcase manifest : Manifest.Config Pages.PathKey @@ -97,23 +100,28 @@ markdownDocument = type alias Model = - {} + { showMobileMenu : Bool + } init : Maybe (PagePath Pages.PathKey) -> ( Model, Cmd Msg ) init maybePagePath = - ( Model, Cmd.none ) + ( Model False, Cmd.none ) type Msg = OnPageChange (PagePath Pages.PathKey) + | 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 @@ -133,17 +141,39 @@ view : , head : List (Head.Tag Pages.PathKey) } view siteMetadata page = - StaticHttp.get (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages") - (D.field "stargazers_count" D.int) - |> StaticHttp.map - (\stars -> - { view = - \model viewForPage -> - pageView stars model siteMetadata page viewForPage - |> wrapBody - , head = head page.frontmatter - } - ) + 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 + (\stars -> + { view = + \model viewForPage -> + pageView stars model siteMetadata page viewForPage + |> wrapBody stars page model + , head = head page.frontmatter + } + ) @@ -196,8 +226,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 @@ -213,8 +242,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 @@ -244,8 +272,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 ] @@ -274,8 +301,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 @@ -293,15 +319,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 @@ -320,51 +372,92 @@ 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 ] - [ Element.el - [ Element.height (Element.px 4) - , Element.width Element.fill - , Element.Background.gradient - { angle = 0.2 - , steps = - [ Element.rgb255 0 242 96 - , Element.rgb255 5 117 230 - ] - } + [ responsiveHeader + , Element.column + [ Element.width Element.fill + , Element.htmlAttribute (Attr.class "responsive-desktop") ] - Element.none - , Element.row - [ Element.paddingXY 25 4 - , Element.spaceEvenly - , Element.width Element.fill - , Element.Region.navigation - , Element.Border.widthEach { bottom = 1, left = 0, right = 0, top = 0 } - , Element.Border.color (Element.rgba255 40 80 40 0.4) - ] - [ Element.link [] - { url = "/" - , label = - Element.row - [ Font.size 30 - , Element.spacing 16 - , Element.htmlAttribute (Attr.id "navbar-title") + [ Element.el + [ Element.height (Element.px 4) + , Element.width Element.fill + , Element.Background.gradient + { angle = 0.2 + , steps = + [ Element.rgb255 0 242 96 + , Element.rgb255 5 117 230 ] - [ DocumentSvg.view - , Element.text "elm-pages" - ] - } - , Element.row [ Element.spacing 15 ] - [ elmDocsLink - , githubRepoLink stars - , highlightableLink currentPath pages.docs.directory "Docs" - , highlightableLink currentPath pages.blog.directory "Blog" + } + ] + Element.none + , Element.row + [ Element.paddingXY 25 4 + , Element.spaceEvenly + , Element.width Element.fill + , Element.Region.navigation + , Element.Border.widthEach { bottom = 1, left = 0, right = 0, top = 0 } + , Element.Border.color (Element.rgba255 40 80 40 0.4) + ] + [ logoLink + , Element.row [ Element.spacing 15 ] (navbarLinks stars currentPath) ] ] ] +logoLink = + Element.link [] + { url = "/" + , label = + Element.row + [ Font.size 30 + , Element.spacing 16 + , Element.htmlAttribute (Attr.id "navbar-title") + ] + [ DocumentSvg.view + , Element.text "elm-pages" + ] + } + + +logoLinkMobile = + Element.link [] + { url = "/" + , label = + Element.row + [ Font.size 30 + , Element.spacing 16 + , Element.htmlAttribute (Attr.id "navbar-title") + ] + [ Element.text "elm-pages" + ] + } + + +navbarLinks stars currentPath = + [ elmDocsLink + , githubRepoLink stars + , highlightableLink currentPath pages.docs.directory "Docs" + , highlightableLink currentPath pages.showcase.directory "Showcase" + , highlightableLink currentPath pages.blog.directory "Blog" + ] + + +responsiveHeader = + Element.row + [ Element.width Element.fill + , Element.spaceEvenly + , Element.htmlAttribute (Attr.class "responsive-mobile") + , Element.width Element.fill + , Element.padding 20 + ] + [ logoLinkMobile + , FontAwesome.icon "fas fa-bars" |> Element.el [ Element.alignRight, Element.Events.onClick ToggleMobileMenu ] + ] + + highlightableLink : PagePath Pages.PathKey -> Directory Pages.PathKey Directory.WithIndex @@ -510,6 +603,22 @@ 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 = diff --git a/examples/docs/src/Metadata.elm b/examples/docs/src/Metadata.elm index 47182df4..81010743 100644 --- a/examples/docs/src/Metadata.elm +++ b/examples/docs/src/Metadata.elm @@ -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) diff --git a/examples/docs/src/Showcase.elm b/examples/docs/src/Showcase.elm new file mode 100644 index 00000000..c9d266f2 --- /dev/null +++ b/examples/docs/src/Showcase.elm @@ -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" + ] diff --git a/examples/docs/style.css b/examples/docs/style.css index 71e52056..bd5c322a 100644 --- a/examples/docs/style.css +++ b/examples/docs/style.css @@ -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; + } +} diff --git a/generator/src/develop.js b/generator/src/develop.js index 0b1fd143..775daece 100644 --- a/generator/src/develop.js +++ b/generator/src/develop.js @@ -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"); @@ -161,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: { diff --git a/generator/src/generate-raw-content.js b/generator/src/generate-raw-content.js index d7e6c716..8aa84438 100644 --- a/generator/src/generate-raw-content.js +++ b/generator/src/generate-raw-content.js @@ -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}" } ) `; diff --git a/generator/src/template.html b/generator/src/template.html index 5859a472..08520225 100644 --- a/generator/src/template.html +++ b/generator/src/template.html @@ -1,7 +1,7 @@ - +