Fornax.Seo
A SEO meta tag generator for Fornax
Goals
promote adoption of the Really Simple Licensing (RSL) standard
-
enhance the search engine visibility of Fornax-generated websites with:
structured data in JSON-LD format
OpenGraph
<meta>tags- personalized social media links
try to enforce some SEO best practises, e.g. requiring absolute URLs to all content items
Usage example
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
Collect metadata from a content item (e.g., a blog posting)
// 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)
// . . .
Render SEO metadata in your page layout
// 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 ]
]
]
// . . .
Similar NuGet libraries (by framework)
.NET
.NET Framework
ASP.NET
Contributing
A guide to building the project and making pull requests can be found here.
License
Distributed under the terms of the Mozilla Public License Version 2.0.
Multiple items
val string: value: 'T -> string
--------------------
type string = System.String
val string: value: 'T -> string
--------------------
type string = System.String
Multiple items
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
type 'T option = Option<'T>
namespace System
Multiple items
[<Struct>] type DateTime = new: date: DateOnly * time: TimeOnly -> unit + 16 overloads member Add: value: TimeSpan -> DateTime member AddDays: value: float -> DateTime member AddHours: value: float -> DateTime member AddMicroseconds: value: float -> DateTime member AddMilliseconds: value: float -> DateTime member AddMinutes: value: float -> DateTime member AddMonths: months: int -> DateTime member AddSeconds: value: float -> DateTime member AddTicks: value: int64 -> DateTime ...
<summary>Represents an instant in time, typically expressed as a date and time of day.</summary>
--------------------
System.DateTime ()
(+0 other overloads)
System.DateTime(ticks: int64) : System.DateTime
(+0 other overloads)
System.DateTime(date: System.DateOnly, time: System.TimeOnly) : System.DateTime
(+0 other overloads)
System.DateTime(ticks: int64, kind: System.DateTimeKind) : System.DateTime
(+0 other overloads)
System.DateTime(date: System.DateOnly, time: System.TimeOnly, kind: System.DateTimeKind) : System.DateTime
(+0 other overloads)
System.DateTime(year: int, month: int, day: int) : System.DateTime
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, calendar: System.Globalization.Calendar) : System.DateTime
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : System.DateTime
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: System.DateTimeKind) : System.DateTime
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: System.Globalization.Calendar) : System.DateTime
(+0 other overloads)
[<Struct>] type DateTime = new: date: DateOnly * time: TimeOnly -> unit + 16 overloads member Add: value: TimeSpan -> DateTime member AddDays: value: float -> DateTime member AddHours: value: float -> DateTime member AddMicroseconds: value: float -> DateTime member AddMilliseconds: value: float -> DateTime member AddMinutes: value: float -> DateTime member AddMonths: months: int -> DateTime member AddSeconds: value: float -> DateTime member AddTicks: value: int64 -> DateTime ...
<summary>Represents an instant in time, typically expressed as a date and time of day.</summary>
--------------------
System.DateTime ()
(+0 other overloads)
System.DateTime(ticks: int64) : System.DateTime
(+0 other overloads)
System.DateTime(date: System.DateOnly, time: System.TimeOnly) : System.DateTime
(+0 other overloads)
System.DateTime(ticks: int64, kind: System.DateTimeKind) : System.DateTime
(+0 other overloads)
System.DateTime(date: System.DateOnly, time: System.TimeOnly, kind: System.DateTimeKind) : System.DateTime
(+0 other overloads)
System.DateTime(year: int, month: int, day: int) : System.DateTime
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, calendar: System.Globalization.Calendar) : System.DateTime
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : System.DateTime
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: System.DateTimeKind) : System.DateTime
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: System.Globalization.Calendar) : System.DateTime
(+0 other overloads)
type 'T list = List<'T>
module Option
from Microsoft.FSharp.Core
val map: mapping: ('T -> 'U) -> option: 'T option -> 'U option
val defaultValue: value: 'T -> option: 'T option -> 'T
module Seq
from Microsoft.FSharp.Collections
val empty<'T> : 'T seq
val find: predicate: ('T -> bool) -> source: 'T seq -> 'T
namespace System.IO
type Path =
static member ChangeExtension: path: string * extension: string -> string
static member Combine: path1: string * path2: string -> string + 4 overloads
static member EndsInDirectorySeparator: path: ReadOnlySpan<char> -> bool + 1 overload
static member Exists: path: string -> bool
static member GetDirectoryName: path: ReadOnlySpan<char> -> ReadOnlySpan<char> + 1 overload
static member GetExtension: path: ReadOnlySpan<char> -> ReadOnlySpan<char> + 1 overload
static member GetFileName: path: ReadOnlySpan<char> -> ReadOnlySpan<char> + 1 overload
static member GetFileNameWithoutExtension: path: ReadOnlySpan<char> -> ReadOnlySpan<char> + 1 overload
static member GetFullPath: path: string -> string + 1 overload
static member GetInvalidFileNameChars: unit -> char array
...
<summary>Performs operations on <see cref="T:System.String" /> instances that contain file or directory path information. These operations are performed in a cross-platform manner.</summary>
<summary>Performs operations on <see cref="T:System.String" /> instances that contain file or directory path information. These operations are performed in a cross-platform manner.</summary>
[<return:NotNullIfNotNullAttribute ("path")>]
System.IO.Path.GetExtension(path: string) : string
System.IO.Path.GetExtension(path: System.ReadOnlySpan<char>) : System.ReadOnlySpan<char>
System.IO.Path.GetExtension(path: System.ReadOnlySpan<char>) : System.ReadOnlySpan<char>
val defaultArg: arg: 'T option -> defaultValue: 'T -> 'T
union case Option.Some: Value: 'T -> Option<'T>
Multiple items
val seq: sequence: 'T seq -> 'T seq
--------------------
type 'T seq = System.Collections.Generic.IEnumerable<'T>
val seq: sequence: 'T seq -> 'T seq
--------------------
type 'T seq = System.Collections.Generic.IEnumerable<'T>
val tryFind: predicate: ('T -> bool) -> source: 'T seq -> 'T option
Fornax.Seo