A SEO meta tag generator for Fornax
$ dotnet add package Fornax.SeoA SEO meta tag generator for Fornax
promote adoption of the Really Simple Licensing (RSL) standard
enhance the search engine visibility of Fornax-generated websites with:
<meta> tagstry to enforce some SEO best practises, e.g. requiring absolute URLs to all content items
NOTE
The following requires fornax 0.15.1 or newer.
Visit the wiki to learn how to use this package with earlier fornax versions.
Change into a project directory and scaffold a new website
fornax new
Install and set up paket:
dotnet tool install paket
dotnet paket init
Configure dependencies, e.g. at minimum:
# paket.dependencies
source https://api.nuget.org/v3/index.json
framework: net8.0, netstandard2.0, netstandard2.1
generate_load_scripts: true
storage: none
# . . .
nuget Fornax.Seo >= 1.5.0 # pulls in the Fornax.Core package
nuget Markdig
# . . .
Install the packages:
dotnet paket install
IMPORTANT
Provide the root domain of your website:
// loaders/globalloader.fsx
#load @"../.paket/load/net8.0/Fornax.Seo.fsx"
open Fornax.Seo
open Fornax.Seo.Rsl.DOM // include the RSL type library
type SiteInfo = {
title: string
/// The root domain of your website - must be an absolute URL
baseUrl: string
/// RSL-specific terms and conditions for AI user agents
robots: License
description: string
postPageSize: int
}
Add personal authorship details, e.g.:
// loaders/globalloader.fsx
// . . .
let loader (projectRoot: string) (siteContent: SiteContents) =
let siteInfo =
{ title = "Sample Fornax blog"
baseUrl = "http://example.com"
// Only search engine indexing allowed,
// provided users comply with the CC BY-ND 4.0
robots =
License.FreeAndOpenSource(@"https://creativecommons.org/licenses/by-nd/4.0/")
description = "Just a simple blog"
postPageSize = 5 }
let onTheWeb =
[ "linkedin.com/in/username"
"github.com/username"
"bitbucket.org/username"
"facebook.com/username" ]
let siteAuthor: ContentCreator =
{ Name = "Moi-même"
Email = "info@example.com"
SocialMedia = onTheWeb }
siteContent.Add(siteInfo)
siteContent.Add(siteAuthor)
siteContent
// loaders/postloader.fsx
// . . .
type Post = {
file: string
link : string
title: string
/// Provide content for "og:image" and "twitter:image" tags
image: string option
author: string option
published: System.DateTime option
modified: System.DateTime option
tags: string list
content: string
summary: string
}
// . . .
// generators/post.fsx
#load @"../.paket/load/net8.0/Fornax.Seo.fsx"
#load @"layout.fsx"
open Html
open Fornax.Seo
let generate' (ctx: SiteContents) (page: string) =
let siteInfo = ctx.TryGetValue<Globalloader.SiteInfo>()
let siteName = siteInfo |> Option.map (fun si -> si.title)
let tagline =
siteInfo
|> Option.map (fun si -> si.description)
|> Option.defaultValue ""
let siteAuthor =
ctx.TryGetValue<ContentCreator>()
|> Option.defaultValue ContentCreator.Default
let siteRoot =
siteInfo
|> Option.map (fun si -> si.baseUrl)
|> Option.defaultValue ContentObject.Default.BaseUrl
let post =
ctx.TryGetValues<Postloader.Post>()
|> Option.defaultValue Seq.empty
|> Seq.find (fun p -> p.file = page)
let postMeta: ContentObject =
{ Title = post.title
BaseUrl = siteRoot
Url = post.file.Replace(System.IO.Path.GetExtension post.file, ".html")
Description = tagline
Author = { siteAuthor with Name = defaultArg post.author siteAuthor.Name }
SiteName = siteName
Headline = Some post.summary
ObjectType = Some "Blog"
ContentType = Some "BlogPosting"
OpenGraphType = Some "article"
Locale = Some "en-us"
Published = post.published
Modified = post.modified
Tags = Some post.tags
Meta =
Some [ ("Image", defaultArg post.image $"{siteRoot}/images/avatar.jpg")
("Publisher", defaultArg siteName siteAuthor.Name) ] }
ctx.Add(postMeta)
// . . .
// generators/layout.fsx
#load @"../.paket/load/net8.0/Fornax.Seo.fsx"
open Html
open Fornax.Seo
// . . .
let layout (ctx: SiteContents) (active: string) (content: HtmlElement seq) =
let siteAuthor =
ctx.TryGetValue<ContentCreator>()
|> Option.defaultValue ContentCreator.Default
let seoData =
ctx.TryGetValues<ContentObject>()
|> Option.defaultValue Seq.empty
let license =
ctx.TryGetValue<Globalloader.SiteInfo>()
|> Option.map (fun info -> info.robots)
let pageMeta =
seoData
|> Seq.tryFind (fun p -> p.Title.Contains(active))
|> function
| Some info -> info
| _ -> { ContentObject.Default with Author = siteAuthor }
html [] [
head [] [
meta [ CharSet "utf-8" ]
meta [ Name "viewport"; Content "width=device-width, initial-scale=1" ]
// . . .
yield! seo pageMeta
rsl pageMeta license
]
body [] [
// . . .
footer [] [ yield! socialMedia siteAuthor ]
]
]
// . . .
A guide to building the project and making pull requests can be found here.
Distributed under the terms of the Mozilla Public License Version 2.0.