This module contains functions for building up
tags with metadata that will be rendered into the page's <head>
tag
when your page is pre-rendered (or server-rendered, in the case of your server-rendered Route Modules). See also 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:
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.
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 = "<https://elm-pages.com">
, 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 (StatelessRoute, StaticPayload)
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 :
StaticPayload 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
}
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 <head>
tags.
It is not viable or reliable to add <head>
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.
Example:
Head.metaName "twitter:card" (Head.raw "summary_large_image")
Results in <meta name="twitter:card" content="summary_large_image" />
Example:
Head.metaProperty "fb:app_id" (Head.raw "123456789")
Results in <meta property="fb:app_id" content="123456789" />
Example:
metaRedirect (Raw "0; url=https://google.com")
Results in <meta http-equiv="refresh" content="0; url=https://google.com" />
Add a link to the site's RSS feed.
Example:
rssLink "/feed.xml"
<link rel="alternate" type="application/rss+xml" href="/rss.xml">
Add a link to the site's RSS feed.
Example:
sitemapLink "/feed.xml"
<link rel="sitemap" type="application/xml" href="/sitemap.xml">
Set the language for a page.
https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang
import Head
import LanguageTag
import LanguageTag.Language
LanguageTag.Language.de -- sets the page's language to German
|> LanguageTag.build LanguageTag.emptySubtags
|> Head.rootLanguage
This results pre-rendered HTML with a global lang tag set.
<html lang="no">
...
</html>
Let's you link to your manifest.json file, see https://developer.mozilla.org/en-US/docs/Web/Manifest#deploying_a_manifest.
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.
So for example the following tags would not load if defined through nonLoadingNode
, and would instead need to be registered through Vite:
<script src="...">
<link href="/style.css">
<link rel="preload">
The following tag would successfully render as it is a non-loading tag:
Head.nonLoadingNode "link"
[ ( "rel", Head.raw "alternate" )
, ( "type", Head.raw "application/atom+xml" )
, ( "title", Head.raw "News" )
, ( "href", Head.raw "/news/atom" )
]
Renders the head tag:
<link rel="alternate" type="application/atom+xml" title="News" href="/news/atom">
You can learn more about structured data in Google's intro to 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:
<script type="application/ld+json">
{
"@context":"http://schema.org/",
"@type":"Article",
"headline":"Extensible Markdown Parsing in Pure Elm",
"description":"Introducing a new parser that extends your palette with no additional syntax",
"image":"https://elm-pages.com/images/article-covers/extensible-markdown-parsing.jpg",
"author":{
"@type":"Person",
"name":"Dillon Kearns"
},
"publisher":{
"@type":"Person",
"name":"Dillon Kearns"
},
"url":"https://elm-pages.com/blog/extensible-markdown-parsing-in-elm",
"datePublished":"2019-10-08",
"mainEntityOfPage":{
"@type":"SoftwareSourceCode",
"codeRepository":"https://github.com/dillonkearns/elm-pages",
"description":"A statically typed site generator for Elm.",
"author":"Dillon Kearns",
"programmingLanguage":{
"@type":"ComputerLanguage",
"url":"http://elm-lang.org/",
"name":"Elm",
"image":"http://elm-lang.org/",
"identifier":"http://elm-lang.org/"
}
}
}
</script>
To get that data, you would write this in your elm-pages
head tags:
import Json.Encode as Encode
{-| <https://schema.org/Article>
-}
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 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 https://schema.org for a comprehensive listing of possible data types and fields. And take a look at Google's 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.
AttributeValue
s Values, such as between the <>
's here:
<meta name="<THIS IS A VALUE>" content="<THIS IS A VALUE>" />
Create an AttributeValue
representing the current page's full url.
Create an AttributeValue
from an ImagePath
.
Create a raw AttributeValue
(as opposed to some kind of absolute URL).
Note: the type must be png. See https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html.
If a size is provided, it will be turned into square dimensions as per the recommendations here: https://developers.google.com/web/fundamentals/design-and-ux/browser-customization/#safari
Images must be png's, and non-transparent images are recommended. Current recommended dimensions are 180px and 192px.
Feel free to use this, but in 99% of cases you won't need it. The generated
code will run this for you to generate your manifest.json
file automatically!
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"
Values that can be passed to the generated
Pages.application
config through thehead
function.