module Head exposing
( Tag, metaName, metaProperty, metaRedirect
, rssLink, sitemapLink, rootLanguage, manifestLink
, nonLoadingNode
, structuredData
, AttributeValue
, currentPageFullUrl, urlAttribute, raw
, appleTouchIcon, icon
, toJson, canonicalLink
)
{-| This module contains functions for building up
tags with metadata that will be rendered into the page's `
` tag
when your page is pre-rendered (or server-rendered, in the case of your server-rendered Route Modules). See also [`Head.Seo`](Head-Seo),
which has some helper functions for defining OpenGraph and Twitter tags.
One of the unique benefits of using `elm-pages` is that all of your routes (both pre-rendered and server-rendered) fully
render the HTML of your page. That includes the full initial `view` (with the BackendTask resolved, and the `Model` from `init`).
The HTML response also includes all of the `Head` tags, which are defined in two places:
1. `app/Site.elm` - there is a `head` definition in `Site.elm` where you define global head tags that will be included on every rendered page.
2. In each Route Module - there is a `head` function where you have access to both the resolved `BackendTask` and the `RouteParams` for the page and can return head tags based on that.
Here is a common set of global head tags that we can define in `Site.elm`:
module Site exposing (canonicalUrl, config)
import BackendTask exposing (BackendTask)
import Head
import MimeType
import SiteConfig exposing (SiteConfig)
config : SiteConfig
config =
{ canonicalUrl = "
, head = head
}
head : BackendTask (List Head.Tag)
head =
[ Head.metaName "viewport" (Head.raw "width=device-width,initial-scale=1")
, Head.metaName "mobile-web-app-capable" (Head.raw "yes")
, Head.metaName "theme-color" (Head.raw "#ffffff")
, Head.metaName "apple-mobile-web-app-capable" (Head.raw "yes")
, Head.metaName "apple-mobile-web-app-status-bar-style" (Head.raw "black-translucent")
, 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)
]
|> BackendTask.succeed
And here is a `head` function for a Route Module for a blog post. Note that we have access to our `BackendTask` Data and
are using it to populate article metadata like the article's image, publish date, etc.
import Article
import BackendTask
import Date
import Head
import Head.Seo
import Path
import Route exposing (Route)
import RouteBuilder exposing (App, StatelessRoute)
type alias RouteParams =
{ slug : String }
type alias Data =
{ metadata : ArticleMetadata
, body : List Markdown.Block.Block
}
route : StatelessRoute RouteParams Data ActionData
route =
RouteBuilder.preRender
{ data = data
, head = head
, pages = pages
}
|> RouteBuilder.buildNoState { view = view }
head :
App Data ActionData RouteParams
-> List Head.Tag
head static =
let
metadata =
static.data.metadata
in
Head.Seo.summaryLarge
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = metadata.image
, alt = metadata.description
, dimensions = Nothing
, mimeType = Nothing
}
, description = metadata.description
, locale = Nothing
, title = metadata.title
}
|> Head.Seo.article
{ tags = []
, section = Nothing
, publishedTime = Just (DateOrDateTime.Date metadata.published)
, modifiedTime = Nothing
, expirationTime = Nothing
}
## Why is pre-rendered HTML important? Does it still matter for SEO?
Many search engines are able to execute JavaScript now. However, not all are, and even with crawlers like Google, there
is a longer lead time for your pages to be indexed when you have HTML with a blank page that is only visible after the JavaScript executes.
But most importantly, many tools that unfurl links will not execute JavaScript at all, but rather simply do a simple pass to parse your `` tags.
It is not viable or reliable to add `` tags for metadata on the client-side, it must be present in the initial HTML payload. Otherwise you may not
get unfurling preview content when you share a link to your site on Slack, Twitter, etc.
## Building up Head Tags
@docs Tag, metaName, metaProperty, metaRedirect
@docs rssLink, sitemapLink, rootLanguage, manifestLink
@docs nonLoadingNode
## Structured Data
@docs structuredData
## `AttributeValue`s
@docs AttributeValue
@docs currentPageFullUrl, urlAttribute, raw
## Icons
@docs appleTouchIcon, icon
## Functions for use by generated code
@docs toJson, canonicalLink
-}
import Json.Encode
import LanguageTag exposing (LanguageTag)
import List.Extra
import MimeType
import Pages.Internal.String as String
import Pages.Url
import Regex
{-| Values that can be passed to the generated `Pages.application` config
through the `head` function.
-}
type Tag
= Tag Details
| StructuredData Json.Encode.Value
| RootModifier String String
| Stripped String
type alias Details =
{ name : String
, attributes : List ( String, AttributeValue )
}
{-| You can learn more about structured data in [Google's intro to structured data](https://developers.google.com/search/docs/guides/intro-structured-data).
When you add a `structuredData` item to one of your pages in `elm-pages`, it will add `json-ld` data to your document that looks like this:
```html
```
To get that data, you would write this in your `elm-pages` head tags:
import Json.Encode as Encode
{-|
-}
encodeArticle :
{ title : String
, description : String
, author : StructuredDataHelper { authorMemberOf | personOrOrganization : () } authorPossibleFields
, publisher : StructuredDataHelper { publisherMemberOf | personOrOrganization : () } publisherPossibleFields
, url : String
, imageUrl : String
, datePublished : String
, mainEntityOfPage : Encode.Value
}
-> Head.Tag
encodeArticle info =
Encode.object
[ ( "@context", Encode.string "http://schema.org/" )
, ( "@type", Encode.string "Article" )
, ( "headline", Encode.string info.title )
, ( "description", Encode.string info.description )
, ( "image", Encode.string info.imageUrl )
, ( "author", encode info.author )
, ( "publisher", encode info.publisher )
, ( "url", Encode.string info.url )
, ( "datePublished", Encode.string info.datePublished )
, ( "mainEntityOfPage", info.mainEntityOfPage )
]
|> Head.structuredData
Take a look at this [Google Search Gallery](https://developers.google.com/search/docs/guides/search-gallery)
to see some examples of how structured data can be used by search engines to give rich search results. It can help boost
your rankings, get better engagement for your content, and also make your content more accessible. For example,
voice assistant devices can make use of structured data. If you're hosting a conference and want to make the event
date and location easy for attendees to find, this can make that information more accessible.
For the current version of API, you'll need to make sure that the format is correct and contains the required and recommended
structure.
Check out for a comprehensive listing of possible data types and fields. And take a look at
Google's [Structured Data Testing Tool](https://search.google.com/structured-data/testing-tool)
too make sure that your structured data is valid and includes the recommended values.
In the future, `elm-pages` will likely support a typed API, but schema.org is a massive spec, and changes frequently.
And there are multiple sources of information on the possible and recommended structure. So it will take some time
for the right API design to evolve. In the meantime, this allows you to make use of this for SEO purposes.
-}
structuredData : Json.Encode.Value -> Tag
structuredData value =
StructuredData value
{-| Create a raw `AttributeValue` (as opposed to some kind of absolute URL).
-}
raw : String -> AttributeValue
raw value =
Raw value
{-| Create an `AttributeValue` from an `ImagePath`.
-}
urlAttribute : Pages.Url.Url -> AttributeValue
urlAttribute value =
FullUrl value
{-| Create an `AttributeValue` representing the current page's full url.
-}
currentPageFullUrl : AttributeValue
currentPageFullUrl =
FullUrlToCurrentPage
{-| Values, such as between the `<>`'s here:
```html
```
-}
type AttributeValue
= Raw String
| FullUrl Pages.Url.Url
| FullUrlToCurrentPage
{-| It's recommended that you use the `Seo` module helpers, which will provide this
for you, rather than directly using this.
Example:
Head.canonicalLink "https://elm-pages.com"
-}
canonicalLink : Maybe String -> Tag
canonicalLink maybePath =
node "link"
[ ( "rel", raw "canonical" )
, ( "href"
, maybePath |> Maybe.map raw |> Maybe.withDefault currentPageFullUrl
)
]
{-| Escape hatch for any head tags that don't have high-level helpers. This lets you build arbitrary head nodes as long as they
are not loading or preloading directives.
Tags that do loading/pre-loading will not work from this function. `elm-pages` uses ViteJS for loading assets like
script tags, stylesheets, fonts, etc., and allows you to customize which assets to preload and how through the elm-pages.config.mjs file.
See the full discussion of the design in [#339](https://github.com/dillonkearns/elm-pages/discussions/339).
So for example the following tags would _not_ load if defined through `nonLoadingNode`, and would instead need to be registered through Vite:
- `