Support global head tags from API Routes so plugins can add head tags like sitemap and RSS links.

This commit is contained in:
Dillon Kearns 2022-02-14 11:01:46 -08:00
parent 11de872c7c
commit 83dcf70b0e
13 changed files with 120 additions and 110 deletions

View File

@ -4,13 +4,16 @@ import ApiRoute
import Article
import DataSource exposing (DataSource)
import DataSource.Http
import Head
import Html exposing (Html)
import Json.Decode as Decode
import Json.Encode
import Manifest
import Pages
import Pages.Manifest
import Route exposing (Route)
import Rss
import Site
import SiteOld
import Sitemap
import Time
@ -95,7 +98,8 @@ routes getStaticRoutes htmlToString =
)
|> ApiRoute.literal "sitemap.xml"
|> ApiRoute.single
, Manifest.handler
|> ApiRoute.withGlobalHeadTags (DataSource.succeed [ Head.sitemapLink "/sitemap.xml" ])
, Pages.Manifest.generator Site.canonicalUrl Manifest.config
]
@ -149,3 +153,8 @@ rss options itemsRequest =
)
|> ApiRoute.literal "blog/feed.xml"
|> ApiRoute.single
|> ApiRoute.withGlobalHeadTags
(DataSource.succeed
[ Head.rssLink "/blog/feed.xml"
]
)

View File

@ -1,4 +1,4 @@
module Site exposing (config)
module Site exposing (canonicalUrl, config)
import Cloudinary
import DataSource exposing (DataSource)
@ -8,34 +8,21 @@ import Pages.Url
import SiteConfig exposing (SiteConfig)
config : SiteConfig Data
config : SiteConfig
config =
{ data = data
, canonicalUrl = canonicalUrl
{ canonicalUrl = canonicalUrl
, head = head
}
type alias Data =
String
data : DataSource.DataSource Data
data =
--DataSource.File.rawFile "hello.txt"
DataSource.succeed "hello"
head : Data -> List Head.Tag
head static =
head : DataSource (List Head.Tag)
head =
[ Head.icon [ ( 32, 32 ) ] MimeType.Png (cloudinaryIcon MimeType.Png 32)
, Head.icon [ ( 16, 16 ) ] MimeType.Png (cloudinaryIcon MimeType.Png 16)
, Head.appleTouchIcon (Just 180) (cloudinaryIcon MimeType.Png 180)
, Head.appleTouchIcon (Just 192) (cloudinaryIcon MimeType.Png 192)
, Head.rssLink "/blog/feed.xml"
, Head.sitemapLink "/sitemap.xml"
, Head.manifestLink "manifest.json"
]
|> DataSource.succeed
canonicalUrl : String
@ -43,11 +30,6 @@ canonicalUrl =
"https://elm-pages.com"
tagline : String
tagline =
"pull in typed elm data to your pages"
cloudinaryIcon :
MimeType.MimeImage
-> Int

View File

@ -1,36 +1,15 @@
module Manifest exposing (handler)
module Manifest exposing (config)
import ApiRoute
import Cloudinary
import DataSource exposing (DataSource)
import Json.Encode
import MimeType
import Pages.Manifest as Manifest
import Pages.Url
import Route
handler : ApiRoute.ApiRoute ApiRoute.Response
handler =
ApiRoute.succeed
(manifest
|> DataSource.map (manifestToFile canonicalUrl)
)
|> ApiRoute.literal "manifest.json"
|> ApiRoute.single
manifestToFile : String -> Manifest.Config -> String
manifestToFile resolvedCanonicalUrl manifestConfig =
manifestConfig
|> Manifest.toJson resolvedCanonicalUrl
|> (\manifestJsonValue ->
Json.Encode.encode 0 manifestJsonValue
)
manifest : DataSource Manifest.Config
manifest =
config : DataSource Manifest.Config
config =
Manifest.init
{ name = "elm-pages"
, description = "elm-pages - " ++ tagline
@ -56,11 +35,6 @@ webp =
MimeType.OtherImage "webp"
canonicalUrl : String
canonicalUrl =
"https://elm-pages.com"
icon :
MimeType.MimeImage
-> Int

View File

@ -2,11 +2,9 @@ module SiteConfig exposing (SiteConfig)
import DataSource exposing (DataSource)
import Head
import Route exposing (Route)
type alias SiteConfig data =
{ data : DataSource data
, canonicalUrl : String
, head : data -> List Head.Tag
type alias SiteConfig =
{ canonicalUrl : String
, head : DataSource (List Head.Tag)
}

View File

@ -44,6 +44,7 @@ import Api
import Bytes exposing (Bytes)
import Bytes.Decode
import Bytes.Encode
import HtmlPrinter
import Pages.Internal.String
import Pages.Internal.Platform.ToJsPayload
import Pages.Internal.ResponseSketch exposing (ResponseSketch)
@ -425,6 +426,9 @@ main =
, urlToRoute = Route.urlToRoute
, routeToPath = \\route -> route |> Maybe.map Route.routeToPath |> Maybe.withDefault []
, site = ${phase === "browser" ? `Nothing` : `Just Site.config`}
, globalHeadTags = ${
phase === "browser" ? `Nothing` : `Just globalHeadTags`
}
, getStaticRoutes = ${
phase === "browser"
? `DataSource.succeed []`
@ -464,6 +468,13 @@ main =
, decodeResponse = decodeResponse
}
globalHeadTags : DataSource (List Head.Tag)
globalHeadTags =
(Site.config.head :: (Api.routes getStaticRoutes HtmlPrinter.htmlToString
|> List.filterMap ApiRoute.getGlobalHeadTagsDataSource))
|> DataSource.combine
|> DataSource.map List.concat
encodeResponse : ResponseSketch PageData Shared.Data -> Bytes.Encode.Encoder
encodeResponse =

View File

@ -3,7 +3,8 @@ module ApiRoute exposing
, capture, literal, slash, succeed
, single, preRender
, preRenderWithFallback, serverRender
, toJson, getBuildTimeRoutes
, withGlobalHeadTags
, toJson, getBuildTimeRoutes, getGlobalHeadTagsDataSource
)
{-| ApiRoute's are defined in `src/Api.elm` and are a way to generate files, like RSS feeds, sitemaps, or any text-based file that you output with an Elm function! You get access
@ -28,14 +29,20 @@ DataSources dynamically.
@docs preRenderWithFallback, serverRender
## Including Head Tags
@docs withGlobalHeadTags
## Internals
@docs toJson, getBuildTimeRoutes
@docs toJson, getBuildTimeRoutes, getGlobalHeadTagsDataSource
-}
import DataSource exposing (DataSource)
import DataSource.Http
import Head
import Internal.ApiRoute exposing (ApiRoute(..), ApiRouteBuilder(..))
import Json.Decode as Decode
import Json.Encode
@ -108,6 +115,7 @@ serverRender ((ApiRouteBuilder patterns pattern _ _ _) as fullHandler) =
)
, pattern = patterns
, kind = "serverless"
, globalHeadTags = Nothing
}
@ -141,6 +149,7 @@ preRenderWithFallback buildUrls ((ApiRouteBuilder patterns pattern _ toString co
)
, pattern = patterns
, kind = "prerender-with-fallback"
, globalHeadTags = Nothing
}
@ -202,6 +211,7 @@ preRender buildUrls ((ApiRouteBuilder patterns pattern _ toString constructor) a
|> DataSource.map (List.member matches)
, pattern = patterns
, kind = "prerender"
, globalHeadTags = Nothing
}
@ -285,6 +295,16 @@ getBuildTimeRoutes (ApiRoute handler) =
handler.buildTimeRoutes
withGlobalHeadTags : DataSource (List Head.Tag) -> ApiRoute response -> ApiRoute response
withGlobalHeadTags globalHeadTags (ApiRoute handler) =
ApiRoute { handler | globalHeadTags = Just globalHeadTags }
getGlobalHeadTagsDataSource : ApiRoute response -> Maybe (DataSource (List Head.Tag))
getGlobalHeadTagsDataSource (ApiRoute handler) =
handler.globalHeadTags
--captureRest : ApiRouteBuilder (List String -> a) b -> ApiRouteBuilder a b
--captureRest previousHandler =

View File

@ -9,6 +9,7 @@ module Internal.ApiRoute exposing
)
import DataSource exposing (DataSource)
import Head
import Pattern exposing (Pattern)
import Regex exposing (Regex)
@ -48,6 +49,7 @@ type ApiRoute response
, handleRoute : String -> DataSource Bool
, pattern : Pattern
, kind : String
, globalHeadTags : Maybe (DataSource (List Head.Tag))
}

View File

@ -38,7 +38,7 @@ type alias Program userModel userMsg pageData sharedData =
mainView :
ProgramConfig userMsg userModel route siteData pageData sharedData
ProgramConfig userMsg userModel route pageData sharedData
-> Model userModel pageData sharedData
-> { title : String, body : Html userMsg }
mainView config model =
@ -87,7 +87,7 @@ urlsToPagePath urls =
view :
ProgramConfig userMsg userModel route siteData pageData sharedData
ProgramConfig userMsg userModel route pageData sharedData
-> Model userModel pageData sharedData
-> Browser.Document (Msg userMsg pageData sharedData)
view config model =
@ -129,7 +129,7 @@ type InitKind shared page
{-| -}
init :
ProgramConfig userMsg userModel route staticData pageData sharedData
ProgramConfig userMsg userModel route pageData sharedData
-> Flags
-> Url
-> Browser.Navigation.Key
@ -293,7 +293,7 @@ type alias Model userModel pageData sharedData =
{-| -}
update :
ProgramConfig userMsg userModel route siteData pageData sharedData
ProgramConfig userMsg userModel route pageData sharedData
-> Msg userMsg pageData sharedData
-> Model userModel pageData sharedData
-> ( Model userModel pageData sharedData, Cmd (Msg userMsg pageData sharedData) )
@ -543,7 +543,7 @@ update config appMsg model =
{-| -}
application :
ProgramConfig userMsg userModel route staticData pageData sharedData
ProgramConfig userMsg userModel route pageData sharedData
-> Platform.Program Flags (Model userModel pageData sharedData) (Msg userMsg pageData sharedData)
application config =
Browser.application

View File

@ -78,15 +78,15 @@ type alias Program route =
{-| -}
cliApplication :
ProgramConfig userMsg userModel (Maybe route) siteData pageData sharedData
ProgramConfig userMsg userModel (Maybe route) pageData sharedData
-> Program (Maybe route)
cliApplication config =
let
site : SiteConfig siteData
site : SiteConfig
site =
getSiteConfig config
getSiteConfig : ProgramConfig userMsg userModel (Maybe route) siteData pageData sharedData -> SiteConfig siteData
getSiteConfig : ProgramConfig userMsg userModel (Maybe route) pageData sharedData -> SiteConfig
getSiteConfig fullConfig =
case fullConfig.site of
Just mySite ->
@ -172,12 +172,12 @@ requestDecoder =
|> Codec.decoder
flatten : SiteConfig siteData -> RenderRequest route -> ProgramConfig userMsg userModel route siteData pageData sharedData -> List Effect -> Cmd Msg
flatten : SiteConfig -> RenderRequest route -> ProgramConfig userMsg userModel route pageData sharedData -> List Effect -> Cmd Msg
flatten site renderRequest config list =
Cmd.batch (flattenHelp [] site renderRequest config list)
flattenHelp : List (Cmd Msg) -> SiteConfig siteData -> RenderRequest route -> ProgramConfig userMsg userModel route siteData pageData sharedData -> List Effect -> List (Cmd Msg)
flattenHelp : List (Cmd Msg) -> SiteConfig -> RenderRequest route -> ProgramConfig userMsg userModel route pageData sharedData -> List Effect -> List (Cmd Msg)
flattenHelp soFar site renderRequest config list =
case list of
first :: rest ->
@ -193,9 +193,9 @@ flattenHelp soFar site renderRequest config list =
perform :
SiteConfig siteData
SiteConfig
-> RenderRequest route
-> ProgramConfig userMsg userModel route siteData pageData sharedData
-> ProgramConfig userMsg userModel route pageData sharedData
-> Effect
-> Cmd Msg
perform site renderRequest config effect =
@ -330,9 +330,9 @@ flagsDecoder =
{-| -}
init :
SiteConfig siteData
SiteConfig
-> RenderRequest route
-> ProgramConfig userMsg userModel route siteData pageData sharedData
-> ProgramConfig userMsg userModel route pageData sharedData
-> Decode.Value
-> ( Model route, Effect )
init site renderRequest config flags =
@ -360,10 +360,10 @@ init site renderRequest config flags =
initLegacy :
SiteConfig siteData
SiteConfig
-> RenderRequest route
-> { staticHttpCache : Dict String (Maybe String), isDevServer : Bool }
-> ProgramConfig userMsg userModel route siteData pageData sharedData
-> ProgramConfig userMsg userModel route pageData sharedData
-> ( Model route, Effect )
initLegacy site renderRequest { staticHttpCache, isDevServer } config =
let
@ -377,7 +377,7 @@ initLegacy site renderRequest { staticHttpCache, isDevServer } config =
(DataSource.map3 (\_ _ _ -> ())
(config.data serverRequestPayload.frontmatter)
config.sharedData
site.data
(config.globalHeadTags |> Maybe.withDefault (DataSource.succeed []))
)
(if isDevServer then
config.handleRoute serverRequestPayload.frontmatter
@ -390,14 +390,14 @@ initLegacy site renderRequest { staticHttpCache, isDevServer } config =
StaticResponses.renderApiRequest
(DataSource.map2 (\_ _ -> ())
(apiRequest.matchesToResponse path)
site.data
(config.globalHeadTags |> Maybe.withDefault (DataSource.succeed []))
)
RenderRequest.NotFound _ ->
StaticResponses.renderApiRequest
(DataSource.map2 (\_ _ -> ())
(DataSource.succeed [])
site.data
(config.globalHeadTags |> Maybe.withDefault (DataSource.succeed []))
)
unprocessedPages : List ( Path, route )
@ -431,8 +431,8 @@ initLegacy site renderRequest { staticHttpCache, isDevServer } config =
updateAndSendPortIfDone :
SiteConfig siteData
-> ProgramConfig userMsg userModel route siteData pageData sharedData
SiteConfig
-> ProgramConfig userMsg userModel route pageData sharedData
-> Model route
-> ( Model route, Effect )
updateAndSendPortIfDone site config model =
@ -444,8 +444,8 @@ updateAndSendPortIfDone site config model =
{-| -}
update :
SiteConfig siteData
-> ProgramConfig userMsg userModel route siteData pageData sharedData
SiteConfig
-> ProgramConfig userMsg userModel route pageData sharedData
-> Msg
-> Model route
-> ( Model route, Effect )
@ -490,8 +490,8 @@ update site config msg model =
nextStepToEffect :
SiteConfig siteData
-> ProgramConfig userMsg userModel route siteData pageData sharedData
SiteConfig
-> ProgramConfig userMsg userModel route pageData sharedData
-> Model route
-> ( StaticResponses, StaticResponses.NextStep route )
-> ( Model route, Effect )
@ -583,6 +583,7 @@ nextStepToEffect site config model ( updatedStaticResponsesModel, nextStep ) =
pageFoundResult =
StaticHttpRequest.resolve
(if model.isDevServer then
-- TODO OPTIMIZATION this is redundant
config.handleRoute payload.frontmatter
else
@ -627,9 +628,9 @@ nextStepToEffect site config model ( updatedStaticResponsesModel, nextStep ) =
sendSinglePageProgress :
SiteConfig siteData
SiteConfig
-> RequestsAndPending
-> ProgramConfig userMsg userModel route siteData pageData sharedData
-> ProgramConfig userMsg userModel route pageData sharedData
-> Model route
-> { path : Path, frontmatter : route }
-> Effect
@ -643,6 +644,7 @@ sendSinglePageProgress site contentJson config model info =
let
pageFoundResult : Result BuildError (Maybe NotFoundReason)
pageFoundResult =
-- TODO OPTIMIZATION this is redundant
StaticHttpRequest.resolve
(if model.isDevServer then
config.handleRoute route
@ -721,6 +723,7 @@ sendSinglePageProgress site contentJson config model info =
pageDataResult : Result BuildError (PageServerResponse pageData)
pageDataResult =
-- TODO OPTIMIZATION can these three be included in StaticResponses.Finish?
StaticHttpRequest.resolve
(config.data (config.urlToRoute currentUrl))
contentJson
@ -733,12 +736,19 @@ sendSinglePageProgress site contentJson config model info =
contentJson
|> Result.mapError (StaticHttpRequest.toBuildError currentUrl.path)
siteDataResult : Result BuildError siteData
siteDataResult : Result BuildError (List Head.Tag)
siteDataResult =
StaticHttpRequest.resolve
site.data
(config.globalHeadTags |> Maybe.withDefault (DataSource.succeed []))
model.allRawResponses
|> Result.mapError (StaticHttpRequest.toBuildError "Site.elm")
apiRouteHeadTags : DataSource (List Head.Tag)
apiRouteHeadTags =
config.apiRoutes HtmlPrinter.htmlToString
|> List.filterMap ApiRoute.getGlobalHeadTagsDataSource
|> DataSource.combine
|> DataSource.map List.concat
in
case Result.map3 (\a b c -> ( a, b, c )) pageFoundResult renderedResult siteDataResult of
Ok ( maybeNotFoundReason, renderedOrApiResponse, siteData ) ->
@ -790,7 +800,7 @@ sendSinglePageProgress site contentJson config model info =
, contentJson = Dict.empty
, html = rendered.view
, errors = []
, head = rendered.head ++ site.head siteData
, head = rendered.head ++ siteData
, title = rendered.title
, staticHttpCache = model.allRawResponses |> Dict.Extra.filterMap (\_ v -> v)
, is404 = False
@ -818,7 +828,7 @@ sendSinglePageProgress site contentJson config model info =
render404Page :
ProgramConfig userMsg userModel route siteData pageData sharedData
ProgramConfig userMsg userModel route pageData sharedData
-> Model route
-> Path
-> NotFoundReason

View File

@ -60,7 +60,8 @@ You pass your `Pages.Manifest.Config` record into the `Pages.application` functi
import ApiRoute
import Color exposing (Color)
import Color.Convert
import DataSource
import DataSource exposing (DataSource)
import Head
import Json.Encode as Encode
import LanguageTag exposing (LanguageTag, emptySubtags)
import LanguageTag.Country as Country
@ -323,16 +324,19 @@ nonEmptyList list =
{-| A generator for Api.elm to include a manifest.json.
-}
generator : String -> Config -> ApiRoute.ApiRoute ApiRoute.Response
generator : String -> DataSource Config -> ApiRoute.ApiRoute ApiRoute.Response
generator canonicalSiteUrl config =
ApiRoute.succeed
(config
|> toJson canonicalSiteUrl
|> Encode.encode 0
|> DataSource.succeed
|> DataSource.map (toJson canonicalSiteUrl >> Encode.encode 0)
)
|> ApiRoute.literal "manifest.json"
|> ApiRoute.single
|> ApiRoute.withGlobalHeadTags
(DataSource.succeed
[ Head.manifestLink "manifest.json"
]
)
{-| Feel free to use this, but in 99% of cases you won't need it. The generated

View File

@ -5,7 +5,7 @@ import Browser.Navigation
import Bytes exposing (Bytes)
import Bytes.Decode
import Bytes.Encode
import DataSource
import DataSource exposing (DataSource)
import Head
import Html exposing (Html)
import Http
@ -24,7 +24,7 @@ import Task exposing (Task)
import Url exposing (Url)
type alias ProgramConfig userMsg userModel route siteData pageData sharedData =
type alias ProgramConfig userMsg userModel route pageData sharedData =
{ init :
Pages.Flags.Flags
-> sharedData
@ -43,8 +43,8 @@ type alias ProgramConfig userMsg userModel route siteData pageData sharedData =
-> ( userModel, Cmd userMsg )
, update : sharedData -> pageData -> Maybe Browser.Navigation.Key -> userMsg -> userModel -> ( userModel, Cmd userMsg )
, subscriptions : route -> Path -> userModel -> Sub userMsg
, sharedData : DataSource.DataSource sharedData
, data : route -> DataSource.DataSource (PageServerResponse pageData)
, sharedData : DataSource sharedData
, data : route -> DataSource (PageServerResponse pageData)
, view :
{ path : Path
, route : route
@ -56,11 +56,11 @@ type alias ProgramConfig userMsg userModel route siteData pageData sharedData =
{ view : userModel -> { title : String, body : Html userMsg }
, head : List Head.Tag
}
, handleRoute : route -> DataSource.DataSource (Maybe NotFoundReason)
, getStaticRoutes : DataSource.DataSource (List route)
, handleRoute : route -> DataSource (Maybe NotFoundReason)
, getStaticRoutes : DataSource (List route)
, urlToRoute : Url -> route
, routeToPath : route -> List String
, site : Maybe (SiteConfig siteData)
, site : Maybe SiteConfig
, toJsPort : Json.Encode.Value -> Cmd Never
, fromJsPort : Sub Decode.Value
, hotReloadData : Sub Bytes
@ -85,4 +85,5 @@ type alias ProgramConfig userMsg userModel route siteData pageData sharedData =
, byteDecodePageData : route -> Bytes.Decode.Decoder pageData
, encodeResponse : ResponseSketch pageData sharedData -> Bytes.Encode.Encoder
, decodeResponse : Bytes.Decode.Decoder (ResponseSketch pageData sharedData)
, globalHeadTags : Maybe (DataSource (List Head.Tag))
}

View File

@ -4,8 +4,7 @@ import DataSource exposing (DataSource)
import Head
type alias SiteConfig data =
{ data : DataSource data
, canonicalUrl : String
, head : data -> List Head.Tag
type alias SiteConfig =
{ canonicalUrl : String
, head : DataSource (List Head.Tag)
}

View File

@ -49,7 +49,7 @@ type IncludeHtml
decoder :
ProgramConfig userMsg userModel (Maybe route) siteData pageData sharedData
ProgramConfig userMsg userModel (Maybe route) pageData sharedData
-> Decode.Decoder (RenderRequest (Maybe route))
decoder config =
Decode.field "request"
@ -90,7 +90,7 @@ decoder config =
requestPayloadDecoder :
ProgramConfig userMsg userModel (Maybe route) siteData pageData sharedData
ProgramConfig userMsg userModel (Maybe route) pageData sharedData
-> Decode.Decoder (RequestPayload (Maybe route))
requestPayloadDecoder config =
(Decode.string