From 40c3f5a16cf3e31479cff85216bac22080175229 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Sun, 11 Oct 2020 08:43:38 -0700 Subject: [PATCH] Render head tags in beta pre-renderer. --- generator/src/cli.js | 8 +- generator/src/seo-renderer.js | 60 +++++++++ src/Head.elm | 65 ++++++++++ src/Pages/ContentCache.elm | 1 + src/Pages/Internal/Platform/Cli.elm | 128 ++++++++++++-------- src/Pages/Internal/Platform/Effect.elm | 2 +- src/Pages/Internal/Platform/ToJsPayload.elm | 7 +- 7 files changed, 212 insertions(+), 59 deletions(-) create mode 100644 generator/src/seo-renderer.js diff --git a/generator/src/cli.js b/generator/src/cli.js index 60581914..70498115 100644 --- a/generator/src/cli.js +++ b/generator/src/cli.js @@ -1,5 +1,6 @@ const fs = require("fs"); const path = require("path"); +const seo = require("./seo-renderer.js"); async function run() { XMLHttpRequest = require("xhr2"); @@ -65,7 +66,7 @@ async function run() { } function outputString(/** @type { Object } */ fromElm) { - fs.writeFileSync("dist/index.html", wrapHtml(fromElm.html)); + fs.writeFileSync("dist/index.html", wrapHtml(fromElm)); let contentJson = {}; contentJson["body"] = "Hello!"; contentJson["staticData"] = fromElm.contentJson; @@ -73,7 +74,7 @@ function outputString(/** @type { Object } */ fromElm) { } run(); -function wrapHtml(/** @type { string } */ html) { +function wrapHtml(/** @type { Object } */ fromElm) { /*html*/ return ` @@ -100,8 +101,9 @@ function wrapHtml(/** @type { string } */ html) { + ${seo.toString(fromElm.head)} - ${html} + ${fromElm.html} `; diff --git a/generator/src/seo-renderer.js b/generator/src/seo-renderer.js new file mode 100644 index 00000000..772b3268 --- /dev/null +++ b/generator/src/seo-renderer.js @@ -0,0 +1,60 @@ +const elmPagesVersion = "TODO"; + +module.exports = { toString }; + +function toString(/** @type { SeoTag[] } */ tags) { + // appendTag({ + // type: "head", + // name: "meta", + // attributes: [ + // ["name", "generator"], + // ["content", `elm-pages v${elmPagesVersion}`], + // ], + // }); + + const generatorTag /** @type { HeadTag } */ = { + type: "head", + name: "meta", + attributes: [ + ["name", "generator"], + ["content", `elm-pages v${elmPagesVersion}`], + ], + }; + // tags.concat([generatorTag]); + + return tags + .map((headTag) => { + if (headTag.type === "head") { + return appendTag(headTag); + } else if (headTag.type === "json-ld") { + return appendJsonLdTag(headTag); + } else { + throw new Error(`Unknown tag type ${JSON.stringify(headTag)}`); + } + }) + .join("\n"); +} + +/** @typedef {HeadTag | JsonLdTag} SeoTag */ + +/** @typedef {{ name: string; attributes: string[][]; type: 'head' }} HeadTag */ +function appendTag(/** @type {HeadTag} */ tagDetails) { + // const meta = document.createElement(tagDetails.name); + const tagsString = tagDetails.attributes.map(([name, value]) => { + // meta.setAttribute(name, value); + return `${name}="${value}"`; + }); + return ` <${tagDetails.name} ${tagsString.join(" ")} />`; + // document.getElementsByTagName("head")[0].appendChild(meta); +} + +/** @typedef {{ contents: Object; type: 'json-ld' }} JsonLdTag */ +function appendJsonLdTag(/** @type {JsonLdTag} */ tagDetails) { + // let jsonLdScript = document.createElement("script"); + // jsonLdScript.type = "application/ld+json"; + // jsonLdScript.innerHTML = JSON.stringify(tagDetails.contents); + // document.getElementsByTagName("head")[0].appendChild(jsonLdScript); + return ``; +} diff --git a/src/Head.elm b/src/Head.elm index d3cdfc47..93074128 100644 --- a/src/Head.elm +++ b/src/Head.elm @@ -5,6 +5,7 @@ module Head exposing , AttributeValue , currentPageFullUrl, fullImageUrl, fullPageUrl, raw , toJson, canonicalLink + , codec ) {-| This module contains low-level functions for building up @@ -37,6 +38,7 @@ writing a plugin package to extend `elm-pages`. -} +import Codec exposing (Codec) import Json.Encode import Pages.ImagePath as ImagePath exposing (ImagePath) import Pages.Internal.String as String @@ -313,6 +315,69 @@ toJson canonicalSiteUrl currentPagePath tag = ] +codec : Codec (Tag pathKey) +codec = + Codec.custom + (\fTag fStructuredData v -> + case v of + Tag err -> + fTag err + + StructuredData ok -> + fStructuredData ok + ) + |> Codec.variant1 "Tag" Tag tagCodec + |> Codec.variant1 "StructuredData" StructuredData codecStructuredData + |> Codec.buildCustom + + +tagCodec : Codec (Details pathKey) +tagCodec = + --Debug.todo "" + Codec.object Details + |> Codec.field "name" .name Codec.string + |> Codec.field "attributes" .attributes (Codec.list (Codec.tuple Codec.string attributeCodec)) + |> Codec.buildObject + + + +--{ name : String +--, attributes : List ( String, AttributeValue pathKey ) +--} + + +attributeCodec : Codec (AttributeValue pathKey) +attributeCodec = + Codec.custom + (\fRaw fFull fCurrent v -> + case v of + Raw string -> + fRaw string + + FullUrl string -> + fFull string + + FullUrlToCurrentPage -> + fCurrent + ) + |> Codec.variant1 "Raw" Raw Codec.string + |> Codec.variant1 "FullUrl" FullUrl Codec.string + |> Codec.variant0 "FullUrlToCurrentPage" FullUrlToCurrentPage + |> Codec.buildCustom + + + +--type AttributeValue pathKey +-- = Raw String +-- | FullUrl String +-- | FullUrlToCurrentPage + + +codecStructuredData : Codec Json.Encode.Value +codecStructuredData = + Codec.value + + encodeProperty : String -> String -> ( String, AttributeValue pathKey ) -> Json.Encode.Value encodeProperty canonicalSiteUrl currentPagePath ( name, value ) = case value of diff --git a/src/Pages/ContentCache.elm b/src/Pages/ContentCache.elm index 80c6a92e..d244ab19 100644 --- a/src/Pages/ContentCache.elm +++ b/src/Pages/ContentCache.elm @@ -10,6 +10,7 @@ module Pages.ContentCache exposing , lookup , lookupMetadata , pagesWithErrors + , parseContent , pathForUrl , routesForCache , update diff --git a/src/Pages/Internal/Platform/Cli.elm b/src/Pages/Internal/Platform/Cli.elm index d30d8e7c..9ba88528 100644 --- a/src/Pages/Internal/Platform/Cli.elm +++ b/src/Pages/Internal/Platform/Cli.elm @@ -244,6 +244,7 @@ perform cliMsgConstructor toJsPort effect = Json.Encode.object [ ( "html", Json.Encode.string info.html ) , ( "contentJson", Json.Encode.dict identity Json.Encode.string info.contentJson ) + , ( "head", Json.Encode.list (Head.toJson "https://canonical.com/" info.route) info.head ) ] |> toJsPort |> Cmd.map never @@ -520,7 +521,10 @@ nextStepToEffect config model nextStep = pending = [] in - ( { model | allRawResponses = updatedAllRawResponses, pendingRequests = pending } + ( { model + | allRawResponses = updatedAllRawResponses + , pendingRequests = pending + } , doNow |> List.map Effect.FetchHttp |> Effect.Batch @@ -533,6 +537,15 @@ nextStepToEffect config model nextStep = contentCache = ContentCache.init config.document config.content Nothing + renderer value = + ContentCache.parseContent "md" value config.document + + updatedCache = + ContentCache.update contentCache + renderer + urls + { body = "", staticData = model.allRawResponses } + siteMetadata = contentCache |> Result.map @@ -557,10 +570,55 @@ nextStepToEffect config model nextStep = _ -> todo "Expected exactly one item." + pageModel : userModel + pageModel = + config.init + (Just + { path = currentPage.path + , query = Nothing + , fragment = Nothing + } + ) + |> Tuple.first + renderedView : { title : String, body : Html userMsg } renderedView = - viewRequest - |> makeItWork + twoThings.view pageModel pageView + + pageView : view + pageView = + case ContentCache.lookup config.pathKey updatedCache urls of + Just ( pagePath, entry ) -> + case entry of + ContentCache.Parsed frontmatter viewResult -> + expectOk viewResult.body + + _ -> + todo <| "Unhandled content cache state - Not Parsed" ++ toString entry + + _ -> + todo "Unhandled content cache state - Nothing" + + fakeUrl = + { protocol = Url.Https + , host = config.canonicalSiteUrl + , port_ = Nothing + , path = currentPage.path |> PagePath.toString + , query = Nothing + , fragment = Nothing + } + + urls = + { currentUrl = fakeUrl + , baseUrl = + { protocol = Url.Https + , host = config.canonicalSiteUrl + , port_ = Nothing + , path = "" + , query = Nothing + , fragment = Nothing + } + } --viewFnResult = -- --currentPage @@ -573,62 +631,22 @@ nextStepToEffect config model nextStep = -- |> (\request -> -- StaticHttpRequest.resolve ApplicationType.Browser request viewResult.staticData -- ) - makeItWork : - StaticHttp.Request - { view : - userModel - -> view - -> { title : String, body : Html userMsg } - , head : List (Head.Tag pathKey) - } - -> { title : String, body : Html userMsg } + --makeItWork : + -- StaticHttp.Request + -- { view : + -- userModel + -- -> view + -- -> { title : String, body : Html userMsg } + -- , head : List (Head.Tag pathKey) + -- } + -- -> { title : String, body : Html userMsg } makeItWork request = case StaticHttpRequest.resolve ApplicationType.Browser request (staticData |> Dict.map (\k v -> Just v)) of Err error -> todo (toString error) Ok functions -> - let - pageModel : userModel - pageModel = - config.init - (Just - { path = currentPage.path - , query = Nothing - , fragment = Nothing - } - ) - |> Tuple.first - - fakeUrl = - { protocol = Url.Https - , host = "" - , port_ = Nothing - , path = "" -- TODO - , query = Nothing - , fragment = Nothing - } - - urls = - { currentUrl = fakeUrl - , baseUrl = fakeUrl - } - - pageView : view - pageView = - case ContentCache.lookup config.pathKey contentCache urls of - Just ( pagePath, entry ) -> - case entry of - ContentCache.Parsed frontmatter viewResult -> - expectOk viewResult.body - - _ -> - todo "Unhandled content cache state" - - _ -> - todo "Unhandled content cache state" - in - functions.view pageModel pageView + functions staticData = case toJsPayload of @@ -652,6 +670,9 @@ nextStepToEffect config model nextStep = } viewRequest = config.view (siteMetadata |> Result.withDefault []) currentPage + + twoThings = + viewRequest |> makeItWork in case siteMetadata of Ok [ ( page, metadata ) ] -> @@ -681,6 +702,7 @@ nextStepToEffect config model nextStep = renderedView.body |> viewRenderer , errors = [] + , head = twoThings.head } |> Effect.SendSinglePage ) diff --git a/src/Pages/Internal/Platform/Effect.elm b/src/Pages/Internal/Platform/Effect.elm index 5d49dbfc..41aa6602 100644 --- a/src/Pages/Internal/Platform/Effect.elm +++ b/src/Pages/Internal/Platform/Effect.elm @@ -9,4 +9,4 @@ type Effect pathKey | SendJsData (ToJsPayload pathKey) | FetchHttp { masked : RequestDetails, unmasked : RequestDetails } | Batch (List (Effect pathKey)) - | SendSinglePage ToJsSuccessPayloadNew + | SendSinglePage (ToJsSuccessPayloadNew pathKey) diff --git a/src/Pages/Internal/Platform/ToJsPayload.elm b/src/Pages/Internal/Platform/ToJsPayload.elm index 4d359bb7..f1309f11 100644 --- a/src/Pages/Internal/Platform/ToJsPayload.elm +++ b/src/Pages/Internal/Platform/ToJsPayload.elm @@ -3,6 +3,7 @@ module Pages.Internal.Platform.ToJsPayload exposing (..) import BuildError import Codec exposing (Codec) import Dict exposing (Dict) +import Head import Json.Decode as Decode import Json.Encode import Pages.ImagePath as ImagePath @@ -25,11 +26,12 @@ type alias ToJsSuccessPayload pathKey = } -type alias ToJsSuccessPayloadNew = +type alias ToJsSuccessPayloadNew pathKey = { route : String , html : String , contentJson : Dict String String , errors : List String + , head : List (Head.Tag pathKey) } @@ -137,7 +139,7 @@ successCodec = |> Codec.buildObject -successCodecNew : Codec ToJsSuccessPayloadNew +successCodecNew : Codec (ToJsSuccessPayloadNew pathKey) successCodecNew = Codec.object ToJsSuccessPayloadNew |> Codec.field "route" @@ -150,4 +152,5 @@ successCodecNew = .contentJson (Codec.dict Codec.string) |> Codec.field "errors" .errors (Codec.list Codec.string) + |> Codec.field "head" .head (Codec.list Head.codec) |> Codec.buildObject