mirror of
https://github.com/dillonkearns/elm-pages-v3-beta.git
synced 2025-01-07 23:17:35 +03:00
Render head tags in beta pre-renderer.
This commit is contained in:
parent
7beaf5ddaf
commit
40c3f5a16c
@ -1,5 +1,6 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const seo = require("./seo-renderer.js");
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
XMLHttpRequest = require("xhr2");
|
XMLHttpRequest = require("xhr2");
|
||||||
@ -65,7 +66,7 @@ async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function outputString(/** @type { Object } */ fromElm) {
|
function outputString(/** @type { Object } */ fromElm) {
|
||||||
fs.writeFileSync("dist/index.html", wrapHtml(fromElm.html));
|
fs.writeFileSync("dist/index.html", wrapHtml(fromElm));
|
||||||
let contentJson = {};
|
let contentJson = {};
|
||||||
contentJson["body"] = "Hello!";
|
contentJson["body"] = "Hello!";
|
||||||
contentJson["staticData"] = fromElm.contentJson;
|
contentJson["staticData"] = fromElm.contentJson;
|
||||||
@ -73,7 +74,7 @@ function outputString(/** @type { Object } */ fromElm) {
|
|||||||
}
|
}
|
||||||
run();
|
run();
|
||||||
|
|
||||||
function wrapHtml(/** @type { string } */ html) {
|
function wrapHtml(/** @type { Object } */ fromElm) {
|
||||||
/*html*/
|
/*html*/
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@ -100,8 +101,9 @@ function wrapHtml(/** @type { string } */ html) {
|
|||||||
<script defer="defer" src="main.js"></script>
|
<script defer="defer" src="main.js"></script>
|
||||||
<script defer="defer" src="index.js" type="module"></script>
|
<script defer="defer" src="index.js" type="module"></script>
|
||||||
<link rel="preload" href="main.js" as="script">
|
<link rel="preload" href="main.js" as="script">
|
||||||
|
${seo.toString(fromElm.head)}
|
||||||
<body>
|
<body>
|
||||||
${html}
|
${fromElm.html}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
|
60
generator/src/seo-renderer.js
Normal file
60
generator/src/seo-renderer.js
Normal file
@ -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 `<script type="application/ld+json">
|
||||||
|
${JSON.stringify(tagDetails.contents)}
|
||||||
|
</script>`;
|
||||||
|
}
|
65
src/Head.elm
65
src/Head.elm
@ -5,6 +5,7 @@ module Head exposing
|
|||||||
, AttributeValue
|
, AttributeValue
|
||||||
, currentPageFullUrl, fullImageUrl, fullPageUrl, raw
|
, currentPageFullUrl, fullImageUrl, fullPageUrl, raw
|
||||||
, toJson, canonicalLink
|
, toJson, canonicalLink
|
||||||
|
, codec
|
||||||
)
|
)
|
||||||
|
|
||||||
{-| This module contains low-level functions for building up
|
{-| 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 Json.Encode
|
||||||
import Pages.ImagePath as ImagePath exposing (ImagePath)
|
import Pages.ImagePath as ImagePath exposing (ImagePath)
|
||||||
import Pages.Internal.String as String
|
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 : String -> String -> ( String, AttributeValue pathKey ) -> Json.Encode.Value
|
||||||
encodeProperty canonicalSiteUrl currentPagePath ( name, value ) =
|
encodeProperty canonicalSiteUrl currentPagePath ( name, value ) =
|
||||||
case value of
|
case value of
|
||||||
|
@ -10,6 +10,7 @@ module Pages.ContentCache exposing
|
|||||||
, lookup
|
, lookup
|
||||||
, lookupMetadata
|
, lookupMetadata
|
||||||
, pagesWithErrors
|
, pagesWithErrors
|
||||||
|
, parseContent
|
||||||
, pathForUrl
|
, pathForUrl
|
||||||
, routesForCache
|
, routesForCache
|
||||||
, update
|
, update
|
||||||
|
@ -244,6 +244,7 @@ perform cliMsgConstructor toJsPort effect =
|
|||||||
Json.Encode.object
|
Json.Encode.object
|
||||||
[ ( "html", Json.Encode.string info.html )
|
[ ( "html", Json.Encode.string info.html )
|
||||||
, ( "contentJson", Json.Encode.dict identity Json.Encode.string info.contentJson )
|
, ( "contentJson", Json.Encode.dict identity Json.Encode.string info.contentJson )
|
||||||
|
, ( "head", Json.Encode.list (Head.toJson "https://canonical.com/" info.route) info.head )
|
||||||
]
|
]
|
||||||
|> toJsPort
|
|> toJsPort
|
||||||
|> Cmd.map never
|
|> Cmd.map never
|
||||||
@ -520,7 +521,10 @@ nextStepToEffect config model nextStep =
|
|||||||
pending =
|
pending =
|
||||||
[]
|
[]
|
||||||
in
|
in
|
||||||
( { model | allRawResponses = updatedAllRawResponses, pendingRequests = pending }
|
( { model
|
||||||
|
| allRawResponses = updatedAllRawResponses
|
||||||
|
, pendingRequests = pending
|
||||||
|
}
|
||||||
, doNow
|
, doNow
|
||||||
|> List.map Effect.FetchHttp
|
|> List.map Effect.FetchHttp
|
||||||
|> Effect.Batch
|
|> Effect.Batch
|
||||||
@ -533,6 +537,15 @@ nextStepToEffect config model nextStep =
|
|||||||
contentCache =
|
contentCache =
|
||||||
ContentCache.init config.document config.content Nothing
|
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 =
|
siteMetadata =
|
||||||
contentCache
|
contentCache
|
||||||
|> Result.map
|
|> Result.map
|
||||||
@ -557,10 +570,55 @@ nextStepToEffect config model nextStep =
|
|||||||
_ ->
|
_ ->
|
||||||
todo "Expected exactly one item."
|
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 : { title : String, body : Html userMsg }
|
||||||
renderedView =
|
renderedView =
|
||||||
viewRequest
|
twoThings.view pageModel pageView
|
||||||
|> makeItWork
|
|
||||||
|
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 =
|
--viewFnResult =
|
||||||
-- --currentPage
|
-- --currentPage
|
||||||
@ -573,62 +631,22 @@ nextStepToEffect config model nextStep =
|
|||||||
-- |> (\request ->
|
-- |> (\request ->
|
||||||
-- StaticHttpRequest.resolve ApplicationType.Browser request viewResult.staticData
|
-- StaticHttpRequest.resolve ApplicationType.Browser request viewResult.staticData
|
||||||
-- )
|
-- )
|
||||||
makeItWork :
|
--makeItWork :
|
||||||
StaticHttp.Request
|
-- StaticHttp.Request
|
||||||
{ view :
|
-- { view :
|
||||||
userModel
|
-- userModel
|
||||||
-> view
|
-- -> view
|
||||||
-> { title : String, body : Html userMsg }
|
-- -> { title : String, body : Html userMsg }
|
||||||
, head : List (Head.Tag pathKey)
|
-- , head : List (Head.Tag pathKey)
|
||||||
}
|
-- }
|
||||||
-> { title : String, body : Html userMsg }
|
-- -> { title : String, body : Html userMsg }
|
||||||
makeItWork request =
|
makeItWork request =
|
||||||
case StaticHttpRequest.resolve ApplicationType.Browser request (staticData |> Dict.map (\k v -> Just v)) of
|
case StaticHttpRequest.resolve ApplicationType.Browser request (staticData |> Dict.map (\k v -> Just v)) of
|
||||||
Err error ->
|
Err error ->
|
||||||
todo (toString error)
|
todo (toString error)
|
||||||
|
|
||||||
Ok functions ->
|
Ok functions ->
|
||||||
let
|
functions
|
||||||
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
|
|
||||||
|
|
||||||
staticData =
|
staticData =
|
||||||
case toJsPayload of
|
case toJsPayload of
|
||||||
@ -652,6 +670,9 @@ nextStepToEffect config model nextStep =
|
|||||||
}
|
}
|
||||||
viewRequest =
|
viewRequest =
|
||||||
config.view (siteMetadata |> Result.withDefault []) currentPage
|
config.view (siteMetadata |> Result.withDefault []) currentPage
|
||||||
|
|
||||||
|
twoThings =
|
||||||
|
viewRequest |> makeItWork
|
||||||
in
|
in
|
||||||
case siteMetadata of
|
case siteMetadata of
|
||||||
Ok [ ( page, metadata ) ] ->
|
Ok [ ( page, metadata ) ] ->
|
||||||
@ -681,6 +702,7 @@ nextStepToEffect config model nextStep =
|
|||||||
renderedView.body
|
renderedView.body
|
||||||
|> viewRenderer
|
|> viewRenderer
|
||||||
, errors = []
|
, errors = []
|
||||||
|
, head = twoThings.head
|
||||||
}
|
}
|
||||||
|> Effect.SendSinglePage
|
|> Effect.SendSinglePage
|
||||||
)
|
)
|
||||||
|
@ -9,4 +9,4 @@ type Effect pathKey
|
|||||||
| SendJsData (ToJsPayload pathKey)
|
| SendJsData (ToJsPayload pathKey)
|
||||||
| FetchHttp { masked : RequestDetails, unmasked : RequestDetails }
|
| FetchHttp { masked : RequestDetails, unmasked : RequestDetails }
|
||||||
| Batch (List (Effect pathKey))
|
| Batch (List (Effect pathKey))
|
||||||
| SendSinglePage ToJsSuccessPayloadNew
|
| SendSinglePage (ToJsSuccessPayloadNew pathKey)
|
||||||
|
@ -3,6 +3,7 @@ module Pages.Internal.Platform.ToJsPayload exposing (..)
|
|||||||
import BuildError
|
import BuildError
|
||||||
import Codec exposing (Codec)
|
import Codec exposing (Codec)
|
||||||
import Dict exposing (Dict)
|
import Dict exposing (Dict)
|
||||||
|
import Head
|
||||||
import Json.Decode as Decode
|
import Json.Decode as Decode
|
||||||
import Json.Encode
|
import Json.Encode
|
||||||
import Pages.ImagePath as ImagePath
|
import Pages.ImagePath as ImagePath
|
||||||
@ -25,11 +26,12 @@ type alias ToJsSuccessPayload pathKey =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type alias ToJsSuccessPayloadNew =
|
type alias ToJsSuccessPayloadNew pathKey =
|
||||||
{ route : String
|
{ route : String
|
||||||
, html : String
|
, html : String
|
||||||
, contentJson : Dict String String
|
, contentJson : Dict String String
|
||||||
, errors : List String
|
, errors : List String
|
||||||
|
, head : List (Head.Tag pathKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -137,7 +139,7 @@ successCodec =
|
|||||||
|> Codec.buildObject
|
|> Codec.buildObject
|
||||||
|
|
||||||
|
|
||||||
successCodecNew : Codec ToJsSuccessPayloadNew
|
successCodecNew : Codec (ToJsSuccessPayloadNew pathKey)
|
||||||
successCodecNew =
|
successCodecNew =
|
||||||
Codec.object ToJsSuccessPayloadNew
|
Codec.object ToJsSuccessPayloadNew
|
||||||
|> Codec.field "route"
|
|> Codec.field "route"
|
||||||
@ -150,4 +152,5 @@ successCodecNew =
|
|||||||
.contentJson
|
.contentJson
|
||||||
(Codec.dict Codec.string)
|
(Codec.dict Codec.string)
|
||||||
|> Codec.field "errors" .errors (Codec.list Codec.string)
|
|> Codec.field "errors" .errors (Codec.list Codec.string)
|
||||||
|
|> Codec.field "head" .head (Codec.list Head.codec)
|
||||||
|> Codec.buildObject
|
|> Codec.buildObject
|
||||||
|
Loading…
Reference in New Issue
Block a user