Head

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:

  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 = "<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
            }

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 <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.

Building up Head Tags

type Tag

Values that can be passed to the generated Pages.application config through the head function.

metaName : String -> AttributeValue -> Tag

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" />

rootLanguage : LanguageTag -> Tag

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>
nonLoadingNode : String -> List ( String, AttributeValue ) -> Tag

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">

Structured Data

structuredData : Value -> Tag

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.

AttributeValues

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.

raw : String -> AttributeValue

Create a raw AttributeValue (as opposed to some kind of absolute URL).

Icons

appleTouchIcon : Maybe Int -> Url -> Tag

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.

icon : List ( Int, Int ) -> MimeImage -> Url -> Tag

Functions for use by generated code

toJson : String -> String -> Tag -> Value

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!