diff --git a/src/MyWebLog.Domain/DataTypes.fs b/src/MyWebLog.Domain/DataTypes.fs index e1d6463..37abd69 100644 --- a/src/MyWebLog.Domain/DataTypes.fs +++ b/src/MyWebLog.Domain/DataTypes.fs @@ -120,6 +120,9 @@ type Page = { /// Revisions of this page Revisions: Revision list + + /// Common OpenGraph information for this post + OpenGraph: OpenGraphProperties option } with /// An empty page @@ -136,7 +139,8 @@ type Page = { Text = "" Metadata = [] PriorPermalinks = [] - Revisions = [] } + Revisions = [] + OpenGraph = None } /// A web log post @@ -189,6 +193,9 @@ type Post = { /// The revisions for this post Revisions: Revision list + + /// OpenGraph information for this post + OpenGraph: OpenGraphProperties option } with /// An empty post @@ -208,7 +215,8 @@ type Post = { Episode = None Metadata = [] PriorPermalinks = [] - Revisions = [] } + Revisions = [] + OpenGraph = None } /// diff --git a/src/MyWebLog.Domain/SupportTypes.fs b/src/MyWebLog.Domain/SupportTypes.fs index 775c916..b39d4ff 100644 --- a/src/MyWebLog.Domain/SupportTypes.fs +++ b/src/MyWebLog.Domain/SupportTypes.fs @@ -387,43 +387,57 @@ type Revision = { type OpenGraphAudio = { /// The URL for this audio file Url: string - + /// The URL for this audio file for sites that require https SecureUrl: string option - + /// The MIME type of the audio file Type: string option } with - + /// An empty audio file static member Empty = { Url = "" SecureUrl = None Type = None } + /// The meta properties for this image + member this.Properties = seq { + yield ("og:audio", this.Url) + match this.SecureUrl with + | Some url -> yield ("og:audio:secure_url", url) + | None when this.Url.StartsWith "https:" -> yield ("og:audio:secure_url", this.Url) + | None -> () + match this.Type with + | Some typ -> yield ("og:audio:type", typ) + | None -> + // TODO: derive mime type from extension + () + } + /// Properties for an OpenGraph image [] type OpenGraphImage = { /// The URL for this image Url: string - + /// The URL for this image for sites that require https SecureUrl: string option - + /// The MIME type of the image Type: string option - + /// The image width (in pixels) Width: int option - + /// The image height (in pixels) Height: int option - + /// Alternative text for the image Alt: string option } with - + /// An empty image file static member Empty = { Url = "" @@ -433,26 +447,43 @@ type OpenGraphImage = { Height = None Alt = None } + /// The meta properties for this image + member this.Properties = seq { + yield ("og:image", this.Url) + match this.SecureUrl with + | Some url -> yield ("og:image:secure_url", url) + | None when this.Url.StartsWith "https:" -> yield ("og:image:secure_url", this.Url) + | None -> () + match this.Type with + | Some typ -> yield ("og:image:type", typ) + | None -> + // TODO: derive mime type based on common image extensions + () + match this.Width with Some width -> yield ("og:image:width", string width) | None -> () + match this.Height with Some height -> yield ("og:image:height", string height) | None -> () + match this.Alt with Some alt -> yield ("og:image:alt", alt) | None -> () + } + /// Properties for an OpenGraph video [] type OpenGraphVideo = { /// The URL for this video Url: string - + /// The URL for this video for sites that require https SecureUrl: string option - + /// The MIME type of the video Type: string option - + /// The video width (in pixels) Width: int option - + /// The video height (in pixels) Height: int option } with - + /// An empty video file static member Empty = { Url = "" @@ -461,6 +492,22 @@ type OpenGraphVideo = { Width = None Height = None } + /// The meta properties for this video + member this.Properties = seq { + yield ("og:video", this.Url) + match this.SecureUrl with + | Some url -> yield ("og:video:secure_url", url) + | None when this.Url.StartsWith "https:" -> yield ("og:video:secure_url", this.Url) + | None -> () + match this.Type with + | Some typ -> yield ("og:video:type", typ) + | None -> + // TODO: derive mime type based on common video extensions + () + match this.Width with Some width -> yield ("og:video:width", string width) | None -> () + match this.Height with Some height -> yield ("og:video:height", string height) | None -> () + } + /// Valid og:type values [] @@ -478,7 +525,7 @@ type OpenGraphType = | VideoOther | VideoTvShow | Website - + /// Parse a string into an OpenGraph type /// The string to be parsed /// The OpenGraphType parsed from the string @@ -499,7 +546,7 @@ type OpenGraphType = | "video.tv_show" -> VideoTvShow | "website" -> Website | _ -> invalidArg (nameof typ) $"{typ} is not a valid OpenGraph type" - + override this.ToString() = match this with | Article -> "article" @@ -517,36 +564,39 @@ type OpenGraphType = | Website -> "website" -/// Top-level properties for OpenGraph +/// Properties for OpenGraph [] -type OpenGraphTopLevel = { - +type OpenGraphProperties = { + /// The type of object represented Type: OpenGraphType - + /// An image representing the object Image: OpenGraphImage - + /// An audio file associated with the object Audio: OpenGraphAudio option - + /// A short description of the object Description: string option - + /// The article (a, an, the, etc.) which should be used to refer to this object Determiner: string option - + /// The primary locale of the content of the object Locale: string option - + /// Other locales in which the content of the object is available LocaleAlternate: string list option - + /// A video file assigned with the object Video: OpenGraphVideo option + + /// Free-form items + Other: MetaItem list option } - + /// A permanent link [] type Permalink = diff --git a/src/MyWebLog.Domain/ViewModels.fs b/src/MyWebLog.Domain/ViewModels.fs index 3972f4e..9d81220 100644 --- a/src/MyWebLog.Domain/ViewModels.fs +++ b/src/MyWebLog.Domain/ViewModels.fs @@ -112,6 +112,9 @@ type DisplayPage = { /// The metadata for the page Metadata: MetaItem list + + /// The OpenGraph properties for the page + OpenGraph: OpenGraphProperties option } with /// Create a minimal display page (no text or metadata) from a database page @@ -128,7 +131,8 @@ type DisplayPage = { IsInPageList = page.IsInPageList IsDefault = string page.Id = webLog.DefaultPage Text = "" - Metadata = [] } + Metadata = [] + OpenGraph = None } /// Create a display page from a database page /// The web log to which the page belongs @@ -136,8 +140,9 @@ type DisplayPage = { /// A DisplayPage with text and metadata static member FromPage webLog page = { DisplayPage.FromPageMinimal webLog page with - Text = addBaseToRelativeUrls webLog.ExtraPath page.Text - Metadata = page.Metadata + Text = addBaseToRelativeUrls webLog.ExtraPath page.Text + Metadata = page.Metadata + OpenGraph = page.OpenGraph } @@ -1165,6 +1170,9 @@ type PostListItem = { /// Metadata for the post Metadata: MetaItem list + + /// OpenGraph properties for the post + OpenGraph: OpenGraphProperties option } with /// Create a post list item from a post @@ -1183,7 +1191,8 @@ type PostListItem = { CategoryIds = post.CategoryIds |> List.map string Tags = post.Tags Episode = post.Episode - Metadata = post.Metadata } + Metadata = post.Metadata + OpenGraph = post.OpenGraph } /// View model for displaying posts diff --git a/src/MyWebLog/Template.fs b/src/MyWebLog/Template.fs index feab8f1..5529b39 100644 --- a/src/MyWebLog/Template.fs +++ b/src/MyWebLog/Template.fs @@ -188,9 +188,35 @@ let parser = // Create various items in the page header based on the state of the page being generated it.RegisterEmptyTag("page_head", fun writer encoder context -> - let app = context.App - // let getBool name = - // defaultArg (context.Environments[0].[name] |> Option.ofObj |> Option.map Convert.ToBoolean) false + let app = context.App + let attrEnc = System.Web.HttpUtility.HtmlAttributeEncode + + // OpenGraph tags + if app.IsPage || app.IsPost then + let writeOgProp (name, value) = + writer.WriteLine $"""{s}""" + writeOgProp ("og:title", if app.IsPage then app.Page.Title else app.Posts.Posts[0].Title) + writeOgProp ("og:site_name", app.WebLog.Name) + if app.IsPage then app.Page.Permalink else app.Posts.Posts[0].Permalink + |> Permalink + |> app.WebLog.AbsoluteUrl + |> function url -> writeOgProp ("og:url", url) + match if app.IsPage then app.Page.OpenGraph else app.Posts.Posts[0].OpenGraph with + | Some props -> + writeOgProp ("og:type", string props.Type) + props.Image.Properties |> Seq.iter writeOgProp + match props.Description with Some desc -> writeOgProp ("og:description", desc) | None -> () + match props.Determiner with Some det -> writeOgProp ("og:determiner", det) | None -> () + match props.Locale with Some loc -> writeOgProp ("og:locale", loc) | None -> () + match props.LocaleAlternate with + | Some alt -> alt |> List.iter (fun it -> writeOgProp ("og:locale:alternate", it)) + | None -> () + match props.Audio with Some audio -> audio.Properties |> Seq.iter writeOgProp | None -> () + match props.Video with Some video -> video.Properties |> Seq.iter writeOgProp | None -> () + match props.Other with + | Some oth -> oth |> List.iter (fun it -> writeOgProp (it.Name, it.Value)) + | None -> () + | None -> () writer.WriteLine $"""{s}"""