WIP on OpenGraph post/page model (#52)

- Removed SecureUrl prop; will generate if URL starts with https:
This commit is contained in:
Daniel J. Summers 2025-07-15 23:38:02 -04:00
parent 3ad6b5a521
commit e33966b3df
3 changed files with 173 additions and 72 deletions

View File

@ -399,18 +399,14 @@ type OpenGraphAudio = {
/// <summary>The URL for this audio file</summary> /// <summary>The URL for this audio file</summary>
Url: string Url: string
/// <summary>The URL for this audio file for sites that require https</summary>
SecureUrl: string option
/// <summary>The MIME type of the audio file</summary> /// <summary>The MIME type of the audio file</summary>
Type: string option Type: string option
} with } with
/// <summary>An empty audio file</summary> /// <summary>An empty audio file</summary>
static member Empty = static member Empty =
{ Url = "" { Url = ""
SecureUrl = None Type = None }
Type = None }
/// <summary>MIME types we can derive from the file extension</summary> /// <summary>MIME types we can derive from the file extension</summary>
static member private DeriveTypes = static member private DeriveTypes =
@ -423,13 +419,10 @@ type OpenGraphAudio = {
/// <summary>The <c>meta</c> properties for this image</summary> /// <summary>The <c>meta</c> properties for this image</summary>
member this.Properties = seq { member this.Properties = seq {
yield ("og:audio", this.Url) yield "og:audio", this.Url
match this.SecureUrl with if this.Url.StartsWith "https:" then yield "og:audio:secure_url", this.Url
| 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 match this.Type with
| Some typ -> yield ("og:audio:type", typ) | Some typ -> yield "og:audio:type", typ
| None -> | None ->
match deriveMimeType this.Url OpenGraphAudio.DeriveTypes with match deriveMimeType this.Url OpenGraphAudio.DeriveTypes with
| Some it -> yield "og:audio:type", it | Some it -> yield "og:audio:type", it
@ -443,9 +436,6 @@ type OpenGraphImage = {
/// <summary>The URL for this image</summary> /// <summary>The URL for this image</summary>
Url: string Url: string
/// <summary>The URL for this image for sites that require https</summary>
SecureUrl: string option
/// <summary>The MIME type of the image</summary> /// <summary>The MIME type of the image</summary>
Type: string option Type: string option
@ -461,12 +451,11 @@ type OpenGraphImage = {
/// <summary>An empty image file</summary> /// <summary>An empty image file</summary>
static member Empty = static member Empty =
{ Url = "" { Url = ""
SecureUrl = None Type = None
Type = None Width = None
Width = None Height = None
Height = None Alt = None }
Alt = None }
/// <summary>MIME types we can derive from the file extension</summary> /// <summary>MIME types we can derive from the file extension</summary>
static member private DeriveTypes = static member private DeriveTypes =
@ -485,10 +474,7 @@ type OpenGraphImage = {
/// <summary>The <c>meta</c> properties for this image</summary> /// <summary>The <c>meta</c> properties for this image</summary>
member this.Properties = seq { member this.Properties = seq {
yield "og:image", this.Url yield "og:image", this.Url
match this.SecureUrl with if this.Url.StartsWith "https:" then yield "og:image:secure_url", this.Url
| 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 match this.Type with
| Some typ -> yield "og:image:type", typ | Some typ -> yield "og:image:type", typ
| None -> | None ->
@ -507,9 +493,6 @@ type OpenGraphVideo = {
/// <summary>The URL for this video</summary> /// <summary>The URL for this video</summary>
Url: string Url: string
/// <summary>The URL for this video for sites that require https</summary>
SecureUrl: string option
/// <summary>The MIME type of the video</summary> /// <summary>The MIME type of the video</summary>
Type: string option Type: string option
@ -522,11 +505,10 @@ type OpenGraphVideo = {
/// <summary>An empty video file</summary> /// <summary>An empty video file</summary>
static member Empty = static member Empty =
{ Url = "" { Url = ""
SecureUrl = None Type = None
Type = None Width = None
Width = None Height = None }
Height = None }
/// <summary>MIME types we can derive from the file extension</summary> /// <summary>MIME types we can derive from the file extension</summary>
static member private DeriveTypes = static member private DeriveTypes =
@ -540,10 +522,7 @@ type OpenGraphVideo = {
/// <summary>The <c>meta</c> properties for this video</summary> /// <summary>The <c>meta</c> properties for this video</summary>
member this.Properties = seq { member this.Properties = seq {
yield "og:video", this.Url yield "og:video", this.Url
match this.SecureUrl with if this.Url.StartsWith "https:" then yield "og:video:secure_url", this.Url
| 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 match this.Type with
| Some typ -> yield "og:video:type", typ | Some typ -> yield "og:video:type", typ
| None -> | None ->

View File

@ -372,6 +372,63 @@ type EditCommonModel() =
/// <summary>The text of the page or post</summary> /// <summary>The text of the page or post</summary>
member val Text = "" with get, set member val Text = "" with get, set
/// <summary>Whether to assign OpenGraph properties to this page or post</summary>
member val AssignOpenGraph = false with get, set
/// <summary>The type of object represented by this page or post</summary>
member val OpenGraphType = "" with get, set
/// <summary>The URL for the image associated with this page or post</summary>
member val OpenGraphImageUrl = "" with get, set
/// <summary>The MIME type of the image associated with this page or post</summary>
member val OpenGraphImageType = "" with get, set
/// <summary>The width of the image associated with this page or post</summary>
member val OpenGraphImageWidth = "" with get, set
/// <summary>The height of the image associated with this page or post</summary>
member val OpenGraphImageHeight = "" with get, set
/// <summary>The alternate text for the image associated with this page or post</summary>
member val OpenGraphImageAlt = "" with get, set
/// <summary>The URL of an audio file associated with this page or post</summary>
member val OpenGraphAudioUrl = "" with get, set
/// <summary>The MIME type of the audio file associated with this page or post</summary>
member val OpenGraphAudioType = "" with get, set
/// <summary>A short description of this page or post</summary>
member val OpenGraphDescription = "" with get, set
/// <summary>A word excluded from the title when alphabetizing</summary>
member val OpenGraphDeterminer = "" with get, set
/// <summary>The primary locale for this page or post</summary>
member val OpenGraphLocale = "" with get, set
/// <summary>Alternate locales in which this page or post is available</summary>
member val OpenGraphAlternateLocales = "" with get, set
/// <summary>The URL of a video file associated with this page or post</summary>
member val OpenGraphVideoUrl = "" with get, set
/// <summary>The MIME type of a video file associated with this page or post</summary>
member val OpenGraphVideoType = "" with get, set
/// <summary>The width of the video file associated with this page or post</summary>
member val OpenGraphVideoWidth = "" with get, set
/// <summary>The height of the video file associated with this page or post</summary>
member val OpenGraphVideoHeight = "" with get, set
/// <summary>The names of extra OpenGraph properties for this page or post</summary>
member val OpenGraphExtraNames: string array = [||] with get, set
/// <summary>The values of extra OpenGraph properties for this page or post</summary>
member val OpenGraphExtraValues: string array = [||] with get, set
/// <summary>Names of metadata items</summary> /// <summary>Names of metadata items</summary>
member val MetaNames: string array = [||] with get, set member val MetaNames: string array = [||] with get, set
@ -381,9 +438,40 @@ type EditCommonModel() =
/// <summary>Whether this is a new page or post</summary> /// <summary>Whether this is a new page or post</summary>
member this.IsNew with get () = this.Id = "new" member this.IsNew with get () = this.Id = "new"
/// <summary>Populate the OpenGraph properties</summary>
/// <param name="og">The existing OpenGraph property set</param>
member private this.PopulateOpenGraph(og: OpenGraphProperties) =
this.AssignOpenGraph <- true
this.OpenGraphImageUrl <- og.Image.Url
this.OpenGraphImageType <- defaultArg og.Image.Type ""
this.OpenGraphImageWidth <- defaultArg (og.Image.Width |> Option.map string) ""
this.OpenGraphImageHeight <- defaultArg (og.Image.Height |> Option.map string) ""
this.OpenGraphImageAlt <- defaultArg og.Image.Alt ""
this.OpenGraphDescription <- defaultArg og.Description ""
this.OpenGraphDeterminer <- defaultArg og.Determiner ""
this.OpenGraphLocale <- defaultArg og.Locale ""
this.OpenGraphAlternateLocales <- defaultArg (og.LocaleAlternate |> Option.map (String.concat ", ")) ""
match og.Audio with
| Some audio ->
this.OpenGraphAudioUrl <- audio.Url
this.OpenGraphAudioType <- defaultArg audio.Type ""
| None -> ()
match og.Video with
| Some video ->
this.OpenGraphVideoUrl <- video.Url
this.OpenGraphVideoType <- defaultArg video.Type ""
this.OpenGraphVideoWidth <- defaultArg (video.Width |> Option.map string) ""
this.OpenGraphVideoHeight <- defaultArg (video.Height |> Option.map string) ""
| None -> ()
match og.Other with
| Some other ->
this.OpenGraphExtraNames <- other |> List.map _.Name |> Array.ofList
this.OpenGraphExtraValues <- other |> List.map _.Value |> Array.ofList
| None -> ()
/// <summary>Fill the properties of this object from a page</summary> /// <summary>Fill the properties of this object from a page</summary>
/// <param name="page">The page from which this model should be populated</param> /// <param name="page">The page from which this model should be populated</param>
member this.PopulateFromPage (page: Page) = member this.PopulateFromPage(page: Page) =
let latest = findLatestRevision page.Revisions let latest = findLatestRevision page.Revisions
let metaItems = if page.Metadata.Length = 0 then [ MetaItem.Empty ] else page.Metadata let metaItems = if page.Metadata.Length = 0 then [ MetaItem.Empty ] else page.Metadata
this.Id <- string page.Id this.Id <- string page.Id
@ -395,10 +483,11 @@ type EditCommonModel() =
this.Text <- latest.Text.Text this.Text <- latest.Text.Text
this.MetaNames <- metaItems |> List.map _.Name |> Array.ofList this.MetaNames <- metaItems |> List.map _.Name |> Array.ofList
this.MetaValues <- metaItems |> List.map _.Value |> Array.ofList this.MetaValues <- metaItems |> List.map _.Value |> Array.ofList
page.OpenGraph |> Option.iter this.PopulateOpenGraph
/// <summary>Fill the properties of this object from a post</summary> /// <summary>Fill the properties of this object from a post</summary>
/// <param name="post">The post from which this model should be populated</param> /// <param name="post">The post from which this model should be populated</param>
member this.PopulateFromPost (post: Post) = member this.PopulateFromPost(post: Post) =
let latest = findLatestRevision post.Revisions let latest = findLatestRevision post.Revisions
let metaItems = if post.Metadata.Length = 0 then [ MetaItem.Empty ] else post.Metadata let metaItems = if post.Metadata.Length = 0 then [ MetaItem.Empty ] else post.Metadata
this.Id <- string post.Id this.Id <- string post.Id
@ -411,7 +500,52 @@ type EditCommonModel() =
this.Text <- latest.Text.Text this.Text <- latest.Text.Text
this.MetaNames <- metaItems |> List.map _.Name |> Array.ofList this.MetaNames <- metaItems |> List.map _.Name |> Array.ofList
this.MetaValues <- metaItems |> List.map _.Value |> Array.ofList this.MetaValues <- metaItems |> List.map _.Value |> Array.ofList
post.OpenGraph |> Option.iter this.PopulateOpenGraph
/// <summary>Convert the properties of the model into a set of OpenGraph properties</summary>
/// <param name="webLog">The current web log</param>
member this.ToOpenGraph(webLog: WebLog) =
if this.AssignOpenGraph then
let toAbsolute (url: string) =
if url.StartsWith "http" then url else webLog.AbsoluteUrl (Permalink url)
let audio =
match this.OpenGraphAudioUrl.Trim() with
| "" -> None
| url -> Some { OpenGraphAudio.Url = toAbsolute url; Type = noneIfBlank this.OpenGraphAudioType }
let video =
match this.OpenGraphVideoUrl.Trim() with
| "" -> None
| url ->
Some {
OpenGraphVideo.Url = toAbsolute url
Type = noneIfBlank this.OpenGraphVideoType
Width = noneIfBlank this.OpenGraphVideoWidth |> Option.map int
Height = noneIfBlank this.OpenGraphVideoHeight |> Option.map int
}
Some {
Type = if this.OpenGraphType = "" then Article else OpenGraphType.Parse this.OpenGraphType
Image = {
Url = toAbsolute this.OpenGraphImageUrl
Type = noneIfBlank this.OpenGraphImageType
Width = noneIfBlank this.OpenGraphImageWidth |> Option.map int
Height = noneIfBlank this.OpenGraphImageHeight |> Option.map int
Alt = noneIfBlank this.OpenGraphImageAlt
}
Description = noneIfBlank this.OpenGraphDescription
Determiner = noneIfBlank this.OpenGraphDeterminer
Locale = noneIfBlank this.OpenGraphLocale
LocaleAlternate = noneIfBlank this.OpenGraphAlternateLocales
|> Option.map (fun alts -> alts.Split ',' |> Array.map _.Trim() |> List.ofArray)
Audio = audio
Video = video
Other = Seq.zip this.OpenGraphExtraNames this.OpenGraphExtraValues
|> Seq.filter (fun it -> fst it > "")
|> Seq.map (fun it -> { Name = fst it; Value = snd it })
|> List.ofSeq
|> function it -> match it with [] -> None | _ -> Some it
}
else
None
/// <summary>View model to edit a custom RSS feed</summary> /// <summary>View model to edit a custom RSS feed</summary>
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]

View File

@ -273,16 +273,10 @@ let openGraphAudioTests = testList "OpenGraphAudio" [
props[1] ("og:audio:secure_url", "https://test.this") "The Secure URL was not written correctly" props[1] ("og:audio:secure_url", "https://test.this") "The Secure URL was not written correctly"
} }
test "succeeds with all properties filled" { test "succeeds with all properties filled" {
let props = let props = Array.ofSeq { Url = "http://test.this"; Type = Some "audio/mpeg" }.Properties
{ Url = "http://test.this" Expect.hasLength props 2 "There should be two properties"
SecureUrl = Some "https://test.other"
Type = Some "audio/mpeg" }.Properties
|> Array.ofSeq
Expect.hasLength props 3 "There should be three properties"
Expect.equal props[0] ("og:audio", "http://test.this") "The URL was not written correctly" Expect.equal props[0] ("og:audio", "http://test.this") "The URL was not written correctly"
Expect.equal Expect.equal props[1] ("og:audio:type", "audio/mpeg") "The MIME type was not written correctly"
props[1] ("og:audio:secure_url", "https://test.other") "The Secure URL was not written correctly"
Expect.equal props[2] ("og:audio:type", "audio/mpeg") "The MIME type was not written correctly"
} }
test "succeeds when deriving AAC" { test "succeeds when deriving AAC" {
let props = Array.ofSeq { OpenGraphAudio.Empty with Url = "/this/cool.file.aac" }.Properties let props = Array.ofSeq { OpenGraphAudio.Empty with Url = "/this/cool.file.aac" }.Properties
@ -333,21 +327,18 @@ let openGraphImageTests = testList "OpenGraphImage" [
} }
test "succeeds with all properties filled" { test "succeeds with all properties filled" {
let props = let props =
{ Url = "http://test.this" { Url = "http://test.this"
SecureUrl = Some "https://test.other" Type = Some "image/jpeg"
Type = Some "image/jpeg" Width = Some 400
Width = Some 400 Height = Some 600
Height = Some 600 Alt = Some "This ought to be good" }.Properties
Alt = Some "This ought to be good" }.Properties
|> Array.ofSeq |> Array.ofSeq
Expect.hasLength props 6 "There should be six properties" Expect.hasLength props 5 "There should be five properties"
Expect.equal props[0] ("og:image", "http://test.this") "The URL was not written correctly" Expect.equal props[0] ("og:image", "http://test.this") "The URL was not written correctly"
Expect.equal Expect.equal props[1] ("og:image:type", "image/jpeg") "The MIME type was not written correctly"
props[1] ("og:image:secure_url", "https://test.other") "The Secure URL was not written correctly" Expect.equal props[2] ("og:image:width", "400") "The width was not written correctly"
Expect.equal props[2] ("og:image:type", "image/jpeg") "The MIME type was not written correctly" Expect.equal props[3] ("og:image:height", "600") "The height was not written correctly"
Expect.equal props[3] ("og:image:width", "400") "The width was not written correctly" Expect.equal props[4] ("og:image:alt", "This ought to be good") "The alt text was not written correctly"
Expect.equal props[4] ("og:image:height", "600") "The height was not written correctly"
Expect.equal props[5] ("og:image:alt", "This ought to be good") "The alt text was not written correctly"
} }
test "succeeds when deriving BMP" { test "succeeds when deriving BMP" {
let props = Array.ofSeq { OpenGraphImage.Empty with Url = "/old/windows.bmp" }.Properties let props = Array.ofSeq { OpenGraphImage.Empty with Url = "/old/windows.bmp" }.Properties
@ -419,19 +410,16 @@ let openGraphVideoTests = testList "OpenGraphVideo" [
} }
test "succeeds with all properties filled" { test "succeeds with all properties filled" {
let props = let props =
{ Url = "http://test.this" { Url = "http://test.this"
SecureUrl = Some "https://test.other" Type = Some "video/mpeg"
Type = Some "video/mpeg" Width = Some 1200
Width = Some 1200 Height = Some 900 }.Properties
Height = Some 900 }.Properties
|> Array.ofSeq |> Array.ofSeq
Expect.hasLength props 5 "There should be five properties" Expect.hasLength props 4 "There should be five properties"
Expect.equal props[0] ("og:video", "http://test.this") "The URL was not written correctly" Expect.equal props[0] ("og:video", "http://test.this") "The URL was not written correctly"
Expect.equal Expect.equal props[1] ("og:video:type", "video/mpeg") "The MIME type was not written correctly"
props[1] ("og:video:secure_url", "https://test.other") "The Secure URL was not written correctly" Expect.equal props[2] ("og:video:width", "1200") "The width was not written correctly"
Expect.equal props[2] ("og:video:type", "video/mpeg") "The MIME type was not written correctly" Expect.equal props[3] ("og:video:height", "900") "The height was not written correctly"
Expect.equal props[3] ("og:video:width", "1200") "The width was not written correctly"
Expect.equal props[4] ("og:video:height", "900") "The height was not written correctly"
} }
test "succeeds when deriving AVI" { test "succeeds when deriving AVI" {
let props = Array.ofSeq { OpenGraphVideo.Empty with Url = "/my.video.avi" }.Properties let props = Array.ofSeq { OpenGraphVideo.Empty with Url = "/my.video.avi" }.Properties