Support relative URLs in OpenGraph properties (#52)

This commit is contained in:
Daniel J. Summers 2025-08-01 22:16:19 -04:00
parent 8b190a6c23
commit cba1bbfa28
9 changed files with 172 additions and 140 deletions

View File

@ -388,6 +388,12 @@ type WebLog = {
member this.AbsoluteUrl(permalink: Permalink) = member this.AbsoluteUrl(permalink: Permalink) =
$"{this.UrlBase}/{permalink}" $"{this.UrlBase}/{permalink}"
/// <summary>Convert a string URL to an absolute URL for this web log if required</summary>
/// <param name="url">The URL which may be translated to an absolute one</param>
/// <returns>The given URL if it was already absolute, or a corresponding absolute URL if not</returns>
member this.UrlToAbsolute(url: string) =
if url.StartsWith "http" then url else this.AbsoluteUrl(Permalink url)
/// <summary>Generate a relative URL for the given link</summary> /// <summary>Generate a relative URL for the given link</summary>
/// <param name="permalink">The permalink for which a relative URL should be generated</param> /// <param name="permalink">The permalink for which a relative URL should be generated</param>
/// <returns>A relative URL for the given link</returns> /// <returns>A relative URL for the given link</returns>

View File

@ -418,9 +418,12 @@ type OpenGraphAudio = {
|> dict |> dict
/// <summary>The <c>meta</c> properties for this image</summary> /// <summary>The <c>meta</c> properties for this image</summary>
member this.Properties = seq { /// <param name="urlTransform">A function to convert relative URLs to absolute URLs</param>
yield "og:audio", this.Url /// <returns>A sequence of key/value pairs for this OpenGraph audio file</returns>
if this.Url.StartsWith "https:" then yield "og:audio:secure_url", this.Url member this.ToProperties(urlTransform: string -> string) = seq {
let url = urlTransform this.Url
yield "og:audio", url
if url.StartsWith "https:" then yield "og:audio:secure_url", url
match this.Type with match this.Type with
| Some typ -> yield "og:audio:type", typ | Some typ -> yield "og:audio:type", typ
| None -> | None ->
@ -472,9 +475,12 @@ type OpenGraphImage = {
|> dict |> dict
/// <summary>The <c>meta</c> properties for this image</summary> /// <summary>The <c>meta</c> properties for this image</summary>
member this.Properties = seq { /// <param name="urlTransform">A function to convert relative URLs to absolute URLs</param>
yield "og:image", this.Url /// <returns>A sequence of key/value pairs for this OpenGraph image file</returns>
if this.Url.StartsWith "https:" then yield "og:image:secure_url", this.Url member this.ToProperties(urlTransform: string -> string) = seq {
let url = urlTransform this.Url
yield "og:image", url
if url.StartsWith "https:" then yield "og:image:secure_url", url
match this.Type with match this.Type with
| Some typ -> yield "og:image:type", typ | Some typ -> yield "og:image:type", typ
| None -> | None ->
@ -520,9 +526,12 @@ type OpenGraphVideo = {
|> dict |> dict
/// <summary>The <c>meta</c> properties for this video</summary> /// <summary>The <c>meta</c> properties for this video</summary>
member this.Properties = seq { /// <param name="urlTransform">A function to convert relative URLs to absolute URLs</param>
yield "og:video", this.Url /// <returns>A sequence of key/value pairs for this OpenGraph video file</returns>
if this.Url.StartsWith "https:" then yield "og:video:secure_url", this.Url member this.ToProperties(urlTransform: string -> string) = seq {
let url = urlTransform this.Url
yield "og:video", url
if url.StartsWith "https:" then yield "og:video:secure_url", url
match this.Type with match this.Type with
| Some typ -> yield "og:video:type", typ | Some typ -> yield "og:video:type", typ
| None -> | None ->
@ -633,17 +642,19 @@ type OpenGraphProperties = {
Other = None } Other = None }
/// <summary>The <c>meta</c> properties for this page or post</summary> /// <summary>The <c>meta</c> properties for this page or post</summary>
member this.Properties = seq { /// <param name="urlTransform">A function to convert relative URLs to absolute URLs</param>
/// <returns>A sequence of key/value pairs for this set of OpenGraph properties</returns>
member this.ToProperties urlTransform = seq {
yield "og:type", string this.Type yield "og:type", string this.Type
yield! this.Image.Properties yield! this.Image.ToProperties urlTransform
match this.Description with Some desc -> yield "og:description", desc | None -> () match this.Description with Some desc -> yield "og:description", desc | None -> ()
match this.Determiner with Some det -> yield "og:determiner", det | None -> () match this.Determiner with Some det -> yield "og:determiner", det | None -> ()
match this.Locale with Some loc -> yield "og:locale", loc | None -> () match this.Locale with Some loc -> yield "og:locale", loc | None -> ()
match this.LocaleAlternate with match this.LocaleAlternate with
| Some alt -> yield! alt |> List.map (fun it -> "og:locale:alternate", it) | Some alt -> yield! alt |> List.map (fun it -> "og:locale:alternate", it)
| None -> () | None -> ()
match this.Audio with Some audio -> yield! audio.Properties | None -> () match this.Audio with Some audio -> yield! audio.ToProperties urlTransform | None -> ()
match this.Video with Some video -> yield! video.Properties | None -> () match this.Video with Some video -> yield! video.ToProperties urlTransform | None -> ()
match this.Other with Some oth -> yield! oth |> List.map (fun it -> it.Name, it.Value) | None -> () match this.Other with Some oth -> yield! oth |> List.map (fun it -> it.Name, it.Value) | None -> ()
} }

View File

@ -504,21 +504,18 @@ type EditCommonModel() =
post.OpenGraph |> Option.iter this.PopulateOpenGraph post.OpenGraph |> Option.iter this.PopulateOpenGraph
/// <summary>Convert the properties of the model into a set of OpenGraph properties</summary> /// <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() =
member this.ToOpenGraph(webLog: WebLog) =
if this.AssignOpenGraph then if this.AssignOpenGraph then
let toAbsolute (url: string) =
if url.StartsWith "http" then url else webLog.AbsoluteUrl (Permalink url)
let audio = let audio =
match this.OpenGraphAudioUrl.Trim() with match this.OpenGraphAudioUrl.Trim() with
| "" -> None | "" -> None
| url -> Some { OpenGraphAudio.Url = toAbsolute url; Type = noneIfBlank this.OpenGraphAudioType } | url -> Some { OpenGraphAudio.Url = url; Type = noneIfBlank this.OpenGraphAudioType }
let video = let video =
match this.OpenGraphVideoUrl.Trim() with match this.OpenGraphVideoUrl.Trim() with
| "" -> None | "" -> None
| url -> | url ->
Some { Some {
OpenGraphVideo.Url = toAbsolute url OpenGraphVideo.Url = url
Type = noneIfBlank this.OpenGraphVideoType Type = noneIfBlank this.OpenGraphVideoType
Width = noneIfBlank this.OpenGraphVideoWidth |> Option.map int Width = noneIfBlank this.OpenGraphVideoWidth |> Option.map int
Height = noneIfBlank this.OpenGraphVideoHeight |> Option.map int Height = noneIfBlank this.OpenGraphVideoHeight |> Option.map int
@ -526,7 +523,7 @@ type EditCommonModel() =
Some { Some {
Type = if this.OpenGraphType = "" then Article else OpenGraphType.Parse this.OpenGraphType Type = if this.OpenGraphType = "" then Article else OpenGraphType.Parse this.OpenGraphType
Image = { Image = {
Url = toAbsolute this.OpenGraphImageUrl Url = this.OpenGraphImageUrl
Type = noneIfBlank this.OpenGraphImageType Type = noneIfBlank this.OpenGraphImageType
Width = noneIfBlank this.OpenGraphImageWidth |> Option.map int Width = noneIfBlank this.OpenGraphImageWidth |> Option.map int
Height = noneIfBlank this.OpenGraphImageHeight |> Option.map int Height = noneIfBlank this.OpenGraphImageHeight |> Option.map int
@ -749,10 +746,9 @@ type EditPageModel() =
/// <summary>Update a page with values from this model</summary> /// <summary>Update a page with values from this model</summary>
/// <param name="page">The page to be updated</param> /// <param name="page">The page to be updated</param>
/// <param name="webLog">The web log to which this page belongs</param>
/// <param name="now">The <c>Instant</c> to use for this particular update</param> /// <param name="now">The <c>Instant</c> to use for this particular update</param>
/// <returns>The page, updated with the values from this model</returns> /// <returns>The page, updated with the values from this model</returns>
member this.UpdatePage (page: Page) webLog now = member this.UpdatePage (page: Page) now =
let revision = { AsOf = now; Text = MarkupText.Parse $"{this.Source}: {this.Text}" } let revision = { AsOf = now; Text = MarkupText.Parse $"{this.Source}: {this.Text}" }
// Detect a permalink change, and add the prior one to the prior list // Detect a permalink change, and add the prior one to the prior list
match string page.Permalink with match string page.Permalink with
@ -768,7 +764,7 @@ type EditPageModel() =
IsInPageList = this.IsShownInPageList IsInPageList = this.IsShownInPageList
Template = match this.Template with "" -> None | tmpl -> Some tmpl Template = match this.Template with "" -> None | tmpl -> Some tmpl
Text = revision.Text.AsHtml() Text = revision.Text.AsHtml()
OpenGraph = this.ToOpenGraph webLog OpenGraph = this.ToOpenGraph()
Metadata = Seq.zip this.MetaNames this.MetaValues Metadata = Seq.zip this.MetaNames this.MetaValues
|> Seq.filter (fun it -> fst it > "") |> Seq.filter (fun it -> fst it > "")
|> Seq.map (fun it -> { Name = fst it; Value = snd it }) |> Seq.map (fun it -> { Name = fst it; Value = snd it })
@ -908,10 +904,9 @@ type EditPostModel() =
/// <summary>Update a post with values from the submitted form</summary> /// <summary>Update a post with values from the submitted form</summary>
/// <param name="post">The post which should be updated</param> /// <param name="post">The post which should be updated</param>
/// <param name="webLog">The web log to which this post belongs</param>
/// <param name="now">The <c>Instant</c> to use for this particular update</param> /// <param name="now">The <c>Instant</c> to use for this particular update</param>
/// <returns>The post, updated with the values from this model</returns> /// <returns>The post, updated with the values from this model</returns>
member this.UpdatePost (post: Post) (webLog: WebLog) now = member this.UpdatePost (post: Post) now =
let revision = { AsOf = now; Text = MarkupText.Parse $"{this.Source}: {this.Text}" } let revision = { AsOf = now; Text = MarkupText.Parse $"{this.Source}: {this.Text}" }
// Detect a permalink change, and add the prior one to the prior list // Detect a permalink change, and add the prior one to the prior list
match string post.Permalink with match string post.Permalink with
@ -935,7 +930,7 @@ type EditPostModel() =
Template = match this.Template.Trim() with "" -> None | tmpl -> Some tmpl Template = match this.Template.Trim() with "" -> None | tmpl -> Some tmpl
CategoryIds = this.CategoryIds |> Array.map CategoryId |> List.ofArray CategoryIds = this.CategoryIds |> Array.map CategoryId |> List.ofArray
Status = if this.DoPublish then Published else post.Status Status = if this.DoPublish then Published else post.Status
OpenGraph = this.ToOpenGraph webLog OpenGraph = this.ToOpenGraph()
Metadata = Seq.zip this.MetaNames this.MetaValues Metadata = Seq.zip this.MetaNames this.MetaValues
|> Seq.filter (fun it -> fst it > "") |> Seq.filter (fun it -> fst it > "")
|> Seq.map (fun it -> { Name = fst it; Value = snd it }) |> Seq.map (fun it -> { Name = fst it; Value = snd it })

View File

@ -34,6 +34,18 @@ let webLogTests = testList "WebLog" [
"https://my.site/blog/page.html" "https://my.site/blog/page.html"
"Absolute URL is incorrect" "Absolute URL is incorrect"
} }
testList "UrlToAbsolute" [
test "succeeds for relative URL" {
Expect.equal
({ WebLog.Empty with UrlBase = "https://my.site" }.UrlToAbsolute "blog/page.html")
"https://my.site/blog/page.html"
"Absolute URL is incorrect"
}
test "succeeds for absolute URL" {
Expect.equal
(WebLog.Empty.UrlToAbsolute "https://test.units") "https://test.units" "Absolute URL is incorrect"
}
]
testList "RelativeUrl" [ testList "RelativeUrl" [
test "succeeds for domain root URL" { test "succeeds for domain root URL" {
Expect.equal Expect.equal

View File

@ -259,67 +259,79 @@ let markupTextTests = testList "MarkupText" [
/// Unit tests for the OpenGraphAudio type /// Unit tests for the OpenGraphAudio type
let openGraphAudioTests = testList "OpenGraphAudio" [ let openGraphAudioTests = testList "OpenGraphAudio" [
testList "Properties" [ let webLog = { WebLog.Empty with UrlBase = "https://unit.test/taco" }
let transform = webLog.UrlToAbsolute
testList "ToProperties" [
test "succeeds with minimum required" { test "succeeds with minimum required" {
let props = Array.ofSeq { OpenGraphAudio.Empty with Url = "http://test.this" }.Properties let props = Array.ofSeq ({ OpenGraphAudio.Empty with Url = "http://test.this" }.ToProperties transform)
Expect.hasLength props 1 "There should be one property" Expect.hasLength props 1 "There should be one property"
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"
} }
test "succeeds with secure URL" { test "succeeds with secure URL" {
let props = Array.ofSeq { OpenGraphAudio.Empty with Url = "https://test.this" }.Properties let props = Array.ofSeq ({ OpenGraphAudio.Empty with Url = "https://test.this" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 2 "There should be two properties"
Expect.equal props[0] ("og:audio", "https://test.this") "The URL was not written correctly" Expect.equal props[0] ("og:audio", "https://test.this") "The URL was not written correctly"
Expect.equal Expect.equal
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 = Array.ofSeq { Url = "http://test.this"; Type = Some "audio/mpeg" }.Properties let props = Array.ofSeq ({ Url = "http://test.this"; Type = Some "audio/mpeg" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 2 "There should be two 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 props[1] ("og:audio:type", "audio/mpeg") "The MIME type was not written correctly" Expect.equal props[1] ("og:audio:type", "audio/mpeg") "The MIME type was not written correctly"
} }
test "succeeds when deriving AAC" { test "succeeds when deriving AAC and transforming URL" {
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" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:audio:type", "audio/aac") "The MIME type for AAC was not derived correctly" Expect.equal
props[0]
("og:audio", "https://unit.test/taco/this/cool.file.aac")
"The URL was not transformed correctly"
Expect.equal
props[1]
("og:audio:secure_url", "https://unit.test/taco/this/cool.file.aac")
"The URL was not transformed correctly"
Expect.equal props[2] ("og:audio:type", "audio/aac") "The MIME type for AAC was not derived correctly"
} }
test "succeeds when deriving MP3" { test "succeeds when deriving MP3" {
let props = Array.ofSeq { OpenGraphAudio.Empty with Url = "/an.other/song.mp3" }.Properties let props = Array.ofSeq ({ OpenGraphAudio.Empty with Url = "an.other/song.mp3" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:audio:type", "audio/mpeg") "The MIME type for MP3 was not derived correctly" Expect.equal props[2] ("og:audio:type", "audio/mpeg") "The MIME type for MP3 was not derived correctly"
} }
test "succeeds when deriving OGA" { test "succeeds when deriving OGA" {
let props = Array.ofSeq { OpenGraphAudio.Empty with Url = "/talks/speex.oga" }.Properties let props = Array.ofSeq ({ OpenGraphAudio.Empty with Url = "talks/speex.oga" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:audio:type", "audio/ogg") "The MIME type for OGA was not derived correctly" Expect.equal props[2] ("og:audio:type", "audio/ogg") "The MIME type for OGA was not derived correctly"
} }
test "succeeds when deriving WAV" { test "succeeds when deriving WAV" {
let props = Array.ofSeq { OpenGraphAudio.Empty with Url = "/some/old.school.wav" }.Properties let props = Array.ofSeq ({ OpenGraphAudio.Empty with Url = "some/old.school.wav" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:audio:type", "audio/wav") "The MIME type for WAV was not derived correctly" Expect.equal props[2] ("og:audio:type", "audio/wav") "The MIME type for WAV was not derived correctly"
} }
test "succeeds when deriving WEBA" { test "succeeds when deriving WEBA" {
let props = Array.ofSeq { OpenGraphAudio.Empty with Url = "/new/format/file.weba" }.Properties let props = Array.ofSeq ({ OpenGraphAudio.Empty with Url = "new/format/file.weba" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:audio:type", "audio/webm") "The MIME type for WEBA was not derived correctly" Expect.equal props[2] ("og:audio:type", "audio/webm") "The MIME type for WEBA was not derived correctly"
} }
test "succeeds when type cannot be derived" { test "succeeds when type cannot be derived" {
let props = Array.ofSeq { OpenGraphAudio.Empty with Url = "/profile.jpg" }.Properties let props = Array.ofSeq ({ OpenGraphAudio.Empty with Url = "profile.jpg" }.ToProperties transform)
Expect.hasLength props 1 "There should be one property (only URL; no type derived)" Expect.hasLength props 2 "There should be two properties (only URLs; no type derived)"
} }
] ]
] ]
/// Tests for the OpenGraphImage type /// Tests for the OpenGraphImage type
let openGraphImageTests = testList "OpenGraphImage" [ let openGraphImageTests = testList "OpenGraphImage" [
testList "Properties" [ let webLog = { WebLog.Empty with UrlBase = "https://unit.test/taco" }
let transform = webLog.UrlToAbsolute
testList "ToProperties" [
test "succeeds with minimum required" { test "succeeds with minimum required" {
let props = Array.ofSeq { OpenGraphImage.Empty with Url = "http://test.url" }.Properties let props = Array.ofSeq ({ OpenGraphImage.Empty with Url = "http://test.url" }.ToProperties transform)
Expect.hasLength props 1 "There should be one property" Expect.hasLength props 1 "There should be one property"
Expect.equal props[0] ("og:image", "http://test.url") "The URL was not written correctly" Expect.equal props[0] ("og:image", "http://test.url") "The URL was not written correctly"
} }
test "succeeds with secure URL" { test "succeeds with secure URL" {
let props = Array.ofSeq { OpenGraphImage.Empty with Url = "https://secure.url" }.Properties let props = Array.ofSeq ({ OpenGraphImage.Empty with Url = "https://secure.url" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 2 "There should be two properties"
Expect.equal props[0] ("og:image", "https://secure.url") "The URL was not written correctly" Expect.equal props[0] ("og:image", "https://secure.url") "The URL was not written correctly"
Expect.equal Expect.equal
@ -331,7 +343,7 @@ let openGraphImageTests = testList "OpenGraphImage" [
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" }.ToProperties transform
|> Array.ofSeq |> Array.ofSeq
Expect.hasLength props 5 "There should be five 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"
@ -340,69 +352,77 @@ let openGraphImageTests = testList "OpenGraphImage" [
Expect.equal props[3] ("og:image:height", "600") "The height was not written correctly" Expect.equal props[3] ("og:image:height", "600") "The height 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:alt", "This ought to be good") "The alt text was not written correctly"
} }
test "succeeds when deriving BMP" { test "succeeds when deriving BMP and transforming URL" {
let props = Array.ofSeq { OpenGraphImage.Empty with Url = "/old/windows.bmp" }.Properties let props = Array.ofSeq ({ OpenGraphImage.Empty with Url = "old/windows.bmp" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:image:type", "image/bmp") "The MIME type for BMP was not derived correctly" Expect.equal
props[0] ("og:image", "https://unit.test/taco/old/windows.bmp") "The URL was not transformed correctly"
Expect.equal
props[1]
("og:image:secure_url", "https://unit.test/taco/old/windows.bmp")
"The URL was not transformed correctly"
Expect.equal props[2] ("og:image:type", "image/bmp") "The MIME type for BMP was not derived correctly"
} }
test "succeeds when deriving GIF" { test "succeeds when deriving GIF" {
let props = Array.ofSeq { OpenGraphImage.Empty with Url = "/its.a.soft.g.gif" }.Properties let props = Array.ofSeq ({ OpenGraphImage.Empty with Url = "its.a.soft.g.gif" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:image:type", "image/gif") "The MIME type for GIF was not derived correctly" Expect.equal props[2] ("og:image:type", "image/gif") "The MIME type for GIF was not derived correctly"
} }
test "succeeds when deriving ICO" { test "succeeds when deriving ICO" {
let props = Array.ofSeq { OpenGraphImage.Empty with Url = "/favicon.ico" }.Properties let props = Array.ofSeq ({ OpenGraphImage.Empty with Url = "favicon.ico" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal Expect.equal
props[1] ("og:image:type", "image/vnd.microsoft.icon") "The MIME type for ICO was not derived correctly" props[2] ("og:image:type", "image/vnd.microsoft.icon") "The MIME type for ICO was not derived correctly"
} }
test "succeeds when deriving JPEG" { test "succeeds when deriving JPEG" {
let props = Array.ofSeq { OpenGraphImage.Empty with Url = "/big/name/photo.jpeg" }.Properties let props = Array.ofSeq ({ OpenGraphImage.Empty with Url = "big/name/photo.jpeg" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:image:type", "image/jpeg") "The MIME type for JPEG was not derived correctly" Expect.equal props[2] ("og:image:type", "image/jpeg") "The MIME type for JPEG was not derived correctly"
} }
test "succeeds when deriving PNG" { test "succeeds when deriving PNG" {
let props = Array.ofSeq { OpenGraphImage.Empty with Url = "/some/nice/graphic.png" }.Properties let props = Array.ofSeq ({ OpenGraphImage.Empty with Url = "some/nice/graphic.png" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:image:type", "image/png") "The MIME type for PNG was not derived correctly" Expect.equal props[2] ("og:image:type", "image/png") "The MIME type for PNG was not derived correctly"
} }
test "succeeds when deriving SVG" { test "succeeds when deriving SVG" {
let props = Array.ofSeq { OpenGraphImage.Empty with Url = "/fancy-new-vector.svg" }.Properties let props = Array.ofSeq ({ OpenGraphImage.Empty with Url = "fancy-new-vector.svg" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:image:type", "image/svg+xml") "The MIME type for SVG was not derived correctly" Expect.equal props[2] ("og:image:type", "image/svg+xml") "The MIME type for SVG was not derived correctly"
} }
test "succeeds when deriving TIF" { test "succeeds when deriving TIF" {
let props = Array.ofSeq { OpenGraphImage.Empty with Url = "/tagged/file.tif" }.Properties let props = Array.ofSeq ({ OpenGraphImage.Empty with Url = "tagged/file.tif" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:image:type", "image/tiff") "The MIME type for TIF was not derived correctly" Expect.equal props[2] ("og:image:type", "image/tiff") "The MIME type for TIF was not derived correctly"
} }
test "succeeds when deriving TIFF" { test "succeeds when deriving TIFF" {
let props = Array.ofSeq { OpenGraphImage.Empty with Url = "/tagged/file.two.tiff" }.Properties let props = Array.ofSeq ({ OpenGraphImage.Empty with Url = "tagged/file.two.tiff" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:image:type", "image/tiff") "The MIME type for TIFF was not derived correctly" Expect.equal props[2] ("og:image:type", "image/tiff") "The MIME type for TIFF was not derived correctly"
} }
test "succeeds when deriving WEBP" { test "succeeds when deriving WEBP" {
let props = Array.ofSeq { OpenGraphImage.Empty with Url = "/modern/photo.webp" }.Properties let props = Array.ofSeq ({ OpenGraphImage.Empty with Url = "modern/photo.webp" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:image:type", "image/webp") "The MIME type for WEBP was not derived correctly" Expect.equal props[2] ("og:image:type", "image/webp") "The MIME type for WEBP was not derived correctly"
} }
test "succeeds when type cannot be derived" { test "succeeds when type cannot be derived" {
let props = Array.ofSeq { OpenGraphImage.Empty with Url = "/intro.mp3" }.Properties let props = Array.ofSeq ({ OpenGraphImage.Empty with Url = "intro.mp3" }.ToProperties transform)
Expect.hasLength props 1 "There should be one property (only URL; no type derived)" Expect.hasLength props 2 "There should be two properties (only URLs; no type derived)"
} }
] ]
] ]
/// Unit tests for the OpenGraphVideo type /// Unit tests for the OpenGraphVideo type
let openGraphVideoTests = testList "OpenGraphVideo" [ let openGraphVideoTests = testList "OpenGraphVideo" [
testList "Properties" [ let webLog = { WebLog.Empty with UrlBase = "https://unit.test/taco" }
let transform = webLog.UrlToAbsolute
testList "ToProperties" [
test "succeeds with minimum required" { test "succeeds with minimum required" {
let props = Array.ofSeq { OpenGraphVideo.Empty with Url = "http://url.test" }.Properties let props = Array.ofSeq ({ OpenGraphVideo.Empty with Url = "http://url.test" }.ToProperties transform)
Expect.hasLength props 1 "There should be one property" Expect.hasLength props 1 "There should be one property"
Expect.equal props[0] ("og:video", "http://url.test") "The URL was not written correctly" Expect.equal props[0] ("og:video", "http://url.test") "The URL was not written correctly"
} }
test "succeeds with secure URL" { test "succeeds with secure URL" {
let props = Array.ofSeq { OpenGraphVideo.Empty with Url = "https://url.secure" }.Properties let props = Array.ofSeq ({ OpenGraphVideo.Empty with Url = "https://url.secure" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 2 "There should be two properties"
Expect.equal props[0] ("og:video", "https://url.secure") "The URL was not written correctly" Expect.equal props[0] ("og:video", "https://url.secure") "The URL was not written correctly"
Expect.equal Expect.equal
@ -413,7 +433,7 @@ let openGraphVideoTests = testList "OpenGraphVideo" [
{ Url = "http://test.this" { Url = "http://test.this"
Type = Some "video/mpeg" Type = Some "video/mpeg"
Width = Some 1200 Width = Some 1200
Height = Some 900 }.Properties Height = Some 900 }.ToProperties transform
|> Array.ofSeq |> Array.ofSeq
Expect.hasLength props 4 "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"
@ -421,34 +441,41 @@ let openGraphVideoTests = testList "OpenGraphVideo" [
Expect.equal props[2] ("og:video:width", "1200") "The width was not written correctly" Expect.equal props[2] ("og:video:width", "1200") "The width was not written correctly"
Expect.equal props[3] ("og:video:height", "900") "The height was not written correctly" Expect.equal props[3] ("og:video:height", "900") "The height was not written correctly"
} }
test "succeeds when deriving AVI" { test "succeeds when deriving AVI and transforming URL" {
let props = Array.ofSeq { OpenGraphVideo.Empty with Url = "/my.video.avi" }.Properties let props = Array.ofSeq ({ OpenGraphVideo.Empty with Url = "my.video.avi" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:video:type", "video/x-msvideo") "The MIME type for AVI was not derived correctly" Expect.equal
props[0] ("og:video", "https://unit.test/taco/my.video.avi") "The URL not transformed correctly"
Expect.equal
props[1]
("og:video:secure_url", "https://unit.test/taco/my.video.avi")
"The URL not transformed correctly"
Expect.equal props[2] ("og:video:type", "video/x-msvideo") "The MIME type for AVI was not derived correctly"
} }
test "succeeds when deriving MP4" { test "succeeds when deriving MP4" {
let props = Array.ofSeq { OpenGraphVideo.Empty with Url = "/chapters/1/01.mp4" }.Properties let props = Array.ofSeq ({ OpenGraphVideo.Empty with Url = "chapters/1/01.mp4" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:video:type", "video/mp4") "The MIME type for MP4 was not derived correctly" Expect.equal props[2] ("og:video:type", "video/mp4") "The MIME type for MP4 was not derived correctly"
} }
test "succeeds when deriving MPEG" { test "succeeds when deriving MPEG" {
let props = Array.ofSeq { OpenGraphVideo.Empty with Url = "/viral/video.mpeg" }.Properties let props = Array.ofSeq ({ OpenGraphVideo.Empty with Url = "viral/video.mpeg" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:video:type", "video/mpeg") "The MIME type for MPEG was not derived correctly" Expect.equal props[2] ("og:video:type", "video/mpeg") "The MIME type for MPEG was not derived correctly"
} }
test "succeeds when deriving OGV" { test "succeeds when deriving OGV" {
let props = Array.ofSeq { OpenGraphVideo.Empty with Url = "/open/video/example.ogv" }.Properties let props =
Expect.hasLength props 2 "There should be two properties" Array.ofSeq ({ OpenGraphVideo.Empty with Url = "open/video/example.ogv" }.ToProperties transform)
Expect.equal props[1] ("og:video:type", "video/ogg") "The MIME type for OGV was not derived correctly" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[2] ("og:video:type", "video/ogg") "The MIME type for OGV was not derived correctly"
} }
test "succeeds when deriving WEBM" { test "succeeds when deriving WEBM" {
let props = Array.ofSeq { OpenGraphVideo.Empty with Url = "/images/hero.webm" }.Properties let props = Array.ofSeq ({ OpenGraphVideo.Empty with Url = "images/hero.webm" }.ToProperties transform)
Expect.hasLength props 2 "There should be two properties" Expect.hasLength props 3 "There should be three properties"
Expect.equal props[1] ("og:video:type", "video/webm") "The MIME type for WEBM was not derived correctly" Expect.equal props[2] ("og:video:type", "video/webm") "The MIME type for WEBM was not derived correctly"
} }
test "succeeds when type cannot be derived" { test "succeeds when type cannot be derived" {
let props = Array.ofSeq { OpenGraphVideo.Empty with Url = "/favicon.ico" }.Properties let props = Array.ofSeq ({ OpenGraphVideo.Empty with Url = "favicon.ico" }.ToProperties transform)
Expect.hasLength props 1 "There should be one property (only URL; no type derived)" Expect.hasLength props 2 "There should be two properties (only URLs; no type derived)"
} }
] ]
] ]
@ -552,7 +579,8 @@ let openGraphPropertiesTests = testList "OpenGraphProperties" [
test "succeeds with minimal values" { test "succeeds with minimal values" {
let props = let props =
{ OpenGraphProperties.Empty with { OpenGraphProperties.Empty with
Image = { OpenGraphImage.Empty with Url = "http://this.aint.nothing" } }.Properties Image = { OpenGraphImage.Empty with Url = "http://this.aint.nothing" } }
.ToProperties WebLog.Empty.UrlToAbsolute
|> Array.ofSeq |> Array.ofSeq
Expect.hasLength props 2 "There should have been two properties" Expect.hasLength props 2 "There should have been two properties"
Expect.equal props[0] ("og:type", "article") "Type not written correctly" Expect.equal props[0] ("og:type", "article") "Type not written correctly"
@ -568,7 +596,8 @@ let openGraphPropertiesTests = testList "OpenGraphProperties" [
Locale = Some "en_US" Locale = Some "en_US"
LocaleAlternate = Some [ "en_UK"; "es_MX" ] LocaleAlternate = Some [ "en_UK"; "es_MX" ]
Video = Some { OpenGraphVideo.Empty with Url = "http://this.video.file" } Video = Some { OpenGraphVideo.Empty with Url = "http://this.video.file" }
Other = Some [ { Name = "book.publisher"; Value = "Yep" } ] }.Properties Other = Some [ { Name = "book.publisher"; Value = "Yep" } ] }
.ToProperties WebLog.Empty.UrlToAbsolute
|> Array.ofSeq |> Array.ofSeq
Expect.hasLength props 10 "There should have been ten properties" Expect.hasLength props 10 "There should have been ten properties"
Expect.equal props[0] ("og:type", "book") "Type not written correctly" Expect.equal props[0] ("og:type", "book") "Type not written correctly"

View File

@ -438,15 +438,14 @@ let editCommonModelTests = testList "EditCommonModel" [
] ]
testList "ToOpenGraph" [ testList "ToOpenGraph" [
test "succeeds when OpenGraph properties are not assigned" { test "succeeds when OpenGraph properties are not assigned" {
Expect.isNone Expect.isNone (EditCommonModel().ToOpenGraph()) "No OpenGraph properties should have returned None"
(EditCommonModel().ToOpenGraph WebLog.Empty) "No OpenGraph properties should have returned None"
} }
test "succeeds when minimal OpenGraph properties are assigned" { test "succeeds when minimal OpenGraph properties are assigned" {
let model = EditCommonModel() let model = EditCommonModel()
model.AssignOpenGraph <- true model.AssignOpenGraph <- true
model.OpenGraphType <- string Article model.OpenGraphType <- string Article
model.OpenGraphImageUrl <- "https://unit.test/img.tiff" model.OpenGraphImageUrl <- "https://unit.test/img.tiff"
let tryOg = model.ToOpenGraph WebLog.Empty let tryOg = model.ToOpenGraph()
Expect.isSome tryOg "There should have been a set of OpenGraph properties returned" Expect.isSome tryOg "There should have been a set of OpenGraph properties returned"
let og = tryOg.Value let og = tryOg.Value
Expect.equal og.Type Article "OpenGraph type not filled correctly" Expect.equal og.Type Article "OpenGraph type not filled correctly"
@ -484,7 +483,7 @@ let editCommonModelTests = testList "EditCommonModel" [
model.OpenGraphVideoHeight <- "768" model.OpenGraphVideoHeight <- "768"
model.OpenGraphExtraNames <- [| "og:duration"; "og:rating" |] model.OpenGraphExtraNames <- [| "og:duration"; "og:rating" |]
model.OpenGraphExtraValues <- [| "1:30:27"; "G" |] model.OpenGraphExtraValues <- [| "1:30:27"; "G" |]
let tryOg = model.ToOpenGraph WebLog.Empty let tryOg = model.ToOpenGraph()
Expect.isSome tryOg "There should have been a set of OpenGraph properties returned" Expect.isSome tryOg "There should have been a set of OpenGraph properties returned"
let og = tryOg.Value let og = tryOg.Value
Expect.equal og.Type VideoMovie "OpenGraph type not filled correctly" Expect.equal og.Type VideoMovie "OpenGraph type not filled correctly"
@ -514,24 +513,6 @@ let editCommonModelTests = testList "EditCommonModel" [
[ { Name = "og:duration"; Value = "1:30:27" }; { Name = "og:rating"; Value = "G" } ] [ { Name = "og:duration"; Value = "1:30:27" }; { Name = "og:rating"; Value = "G" } ]
"OpenGraph extra properties not filled properly" "OpenGraph extra properties not filled properly"
} }
test "succeeds when relative URLs are assigned" {
let model = EditCommonModel()
model.AssignOpenGraph <- true
model.OpenGraphType <- string Article
model.OpenGraphImageUrl <- "image.jpg"
model.OpenGraphAudioUrl <- "tunes/sound.ogg"
model.OpenGraphVideoUrl <- "teaser.mp4"
let tryOg = model.ToOpenGraph { WebLog.Empty with UrlBase = "https://test.units/verify" }
Expect.isSome tryOg "There should have been a set of OpenGraph properties returned"
let og = tryOg.Value
Expect.equal og.Image.Url "https://test.units/verify/image.jpg" "OpenGraph image URL not filled properly"
Expect.isSome og.Audio "OpenGraph audio should have been filled"
Expect.equal
og.Audio.Value.Url "https://test.units/verify/tunes/sound.ogg" "OpenGraph audio URL not filled properly"
Expect.isSome og.Video "OpenGraph video should have been filled"
Expect.equal
og.Video.Value.Url "https://test.units/verify/teaser.mp4" "OpenGraph video URL not filled properly"
}
] ]
] ]
@ -743,7 +724,7 @@ let editPageModelTests = testList "EditPageModel" [
let model = EditPageModel.FromPage testFullPage let model = EditPageModel.FromPage testFullPage
model.Title <- "Updated Page" model.Title <- "Updated Page"
model.IsShownInPageList <- false model.IsShownInPageList <- false
let page = model.UpdatePage testFullPage WebLog.Empty (Noda.epoch + Duration.FromHours 4) let page = model.UpdatePage testFullPage (Noda.epoch + Duration.FromHours 4)
Expect.equal page.Title "Updated Page" "Title not filled properly" Expect.equal page.Title "Updated Page" "Title not filled properly"
Expect.equal page.Permalink (Permalink "blog/page.html") "Permalink not filled properly" Expect.equal page.Permalink (Permalink "blog/page.html") "Permalink not filled properly"
Expect.isEmpty page.PriorPermalinks "PriorPermalinks should be empty" Expect.isEmpty page.PriorPermalinks "PriorPermalinks should be empty"
@ -778,7 +759,7 @@ let editPageModelTests = testList "EditPageModel" [
model.MetaNames <- [| "banana"; "apple"; "grape" |] model.MetaNames <- [| "banana"; "apple"; "grape" |]
model.MetaValues <- [| "monkey"; "zebra"; "ape" |] model.MetaValues <- [| "monkey"; "zebra"; "ape" |]
let now = Noda.epoch + Duration.FromDays 7 let now = Noda.epoch + Duration.FromDays 7
let page = model.UpdatePage testFullPage WebLog.Empty now let page = model.UpdatePage testFullPage now
Expect.equal page.Title "My Updated Page" "Title not filled properly" Expect.equal page.Title "My Updated Page" "Title not filled properly"
Expect.equal page.Permalink (Permalink "blog/updated.html") "Permalink not filled properly" Expect.equal page.Permalink (Permalink "blog/updated.html") "Permalink not filled properly"
Expect.equal page.PriorPermalinks [ Permalink "blog/page.html" ] "PriorPermalinks not filled properly" Expect.equal page.PriorPermalinks [ Permalink "blog/page.html" ] "PriorPermalinks not filled properly"
@ -919,7 +900,7 @@ let editPostModelTests = testList "EditPostModel" [
model model
testList "UpdatePost" [ testList "UpdatePost" [
test "succeeds for a full podcast episode" { test "succeeds for a full podcast episode" {
let post = (updatedModel ()).UpdatePost testFullPost WebLog.Empty (Noda.epoch + Duration.FromDays 400) let post = (updatedModel ()).UpdatePost testFullPost (Noda.epoch + Duration.FromDays 400)
Expect.equal post.Title "An Updated Post" "Title not filled properly" Expect.equal post.Title "An Updated Post" "Title not filled properly"
Expect.equal post.Permalink (Permalink "1970/01/updated-post.html") "Permalink not filled properly" Expect.equal post.Permalink (Permalink "1970/01/updated-post.html") "Permalink not filled properly"
Expect.equal post.PriorPermalinks [ Permalink "1970/01/a-post.html" ] "PriorPermalinks not filled properly" Expect.equal post.PriorPermalinks [ Permalink "1970/01/a-post.html" ] "PriorPermalinks not filled properly"
@ -980,7 +961,7 @@ let editPostModelTests = testList "EditPostModel" [
minModel.SeasonDescription <- "" minModel.SeasonDescription <- ""
minModel.EpisodeNumber <- "" minModel.EpisodeNumber <- ""
minModel.EpisodeDescription <- "" minModel.EpisodeDescription <- ""
let post = minModel.UpdatePost testFullPost WebLog.Empty (Noda.epoch + Duration.FromDays 500) let post = minModel.UpdatePost testFullPost (Noda.epoch + Duration.FromDays 500)
Expect.isSome post.Episode "There should have been a podcast episode" Expect.isSome post.Episode "There should have been a podcast episode"
let ep = post.Episode.Value let ep = post.Episode.Value
Expect.equal ep.Media "an-updated-ep.mp3" "Media not filled properly" Expect.equal ep.Media "an-updated-ep.mp3" "Media not filled properly"
@ -1007,7 +988,7 @@ let editPostModelTests = testList "EditPostModel" [
minModel.ChapterSource <- "internal" minModel.ChapterSource <- "internal"
minModel.ChapterFile <- "" minModel.ChapterFile <- ""
minModel.ChapterType <- "" minModel.ChapterType <- ""
let post = minModel.UpdatePost testFullPost WebLog.Empty (Noda.epoch + Duration.FromDays 500) let post = minModel.UpdatePost testFullPost (Noda.epoch + Duration.FromDays 500)
Expect.isSome post.Episode "There should have been a podcast episode" Expect.isSome post.Episode "There should have been a podcast episode"
let ep = post.Episode.Value let ep = post.Episode.Value
Expect.equal ep.Chapters (Some []) "Chapters not filled properly" Expect.equal ep.Chapters (Some []) "Chapters not filled properly"
@ -1020,7 +1001,6 @@ let editPostModelTests = testList "EditPostModel" [
let post = let post =
minModel.UpdatePost minModel.UpdatePost
{ testFullPost with Episode = Some { testFullPost.Episode.Value with Chapters = Some [] } } { testFullPost with Episode = Some { testFullPost.Episode.Value with Chapters = Some [] } }
WebLog.Empty
(Noda.epoch + Duration.FromDays 500) (Noda.epoch + Duration.FromDays 500)
Expect.isSome post.Episode "There should have been a podcast episode" Expect.isSome post.Episode "There should have been a podcast episode"
let ep = post.Episode.Value let ep = post.Episode.Value
@ -1033,15 +1013,14 @@ let editPostModelTests = testList "EditPostModel" [
let model = updatedModel () let model = updatedModel ()
model.IsEpisode <- false model.IsEpisode <- false
model.Template <- "" model.Template <- ""
let post = model.UpdatePost testFullPost WebLog.Empty Noda.epoch let post = model.UpdatePost testFullPost Noda.epoch
Expect.isNone post.Template "Template not filled properly" Expect.isNone post.Template "Template not filled properly"
Expect.isNone post.Episode "Episode not filled properly" Expect.isNone post.Episode "Episode not filled properly"
} }
test "succeeds when publishing a draft" { test "succeeds when publishing a draft" {
let model = updatedModel () let model = updatedModel ()
model.DoPublish <- true model.DoPublish <- true
let post = let post = model.UpdatePost { testFullPost with Status = Draft } (Noda.epoch + Duration.FromDays 375)
model.UpdatePost { testFullPost with Status = Draft } WebLog.Empty (Noda.epoch + Duration.FromDays 375)
Expect.equal post.Status Published "Status not set properly" Expect.equal post.Status Published "Status not set properly"
Expect.equal post.PublishedOn (Some (Noda.epoch + Duration.FromDays 375)) "PublishedOn not set properly" Expect.equal post.PublishedOn (Some (Noda.epoch + Duration.FromDays 375)) "PublishedOn not set properly"
} }

View File

@ -164,7 +164,7 @@ let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
match! tryPage with match! tryPage with
| Some page when canEdit page.AuthorId ctx -> | Some page when canEdit page.AuthorId ctx ->
let updateList = page.IsInPageList <> model.IsShownInPageList let updateList = page.IsInPageList <> model.IsShownInPageList
let updatedPage = model.UpdatePage page ctx.WebLog now let updatedPage = model.UpdatePage page now
do! (if model.IsNew then data.Page.Add else data.Page.Update) updatedPage do! (if model.IsNew then data.Page.Add else data.Page.Update) updatedPage
if updateList then do! PageListCache.update ctx if updateList then do! PageListCache.update ctx
do! addMessage ctx { UserMessage.Success with Message = "Page saved successfully" } do! addMessage ctx { UserMessage.Success with Message = "Page saved successfully" }

View File

@ -495,7 +495,7 @@ let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
| Some post when canEdit post.AuthorId ctx -> | Some post when canEdit post.AuthorId ctx ->
let priorCats = post.CategoryIds let priorCats = post.CategoryIds
let updatedPost = let updatedPost =
model.UpdatePost post ctx.WebLog (Noda.now ()) model.UpdatePost post (Noda.now ())
|> function |> function
| post -> | post ->
if model.SetPublished then if model.SetPublished then

View File

@ -207,7 +207,7 @@ let parser =
|> app.WebLog.AbsoluteUrl |> app.WebLog.AbsoluteUrl
|> function url -> writeOgProp ("og:url", url) |> function url -> writeOgProp ("og:url", url)
match if app.IsPage then app.Page.OpenGraph else app.Posts.Posts[0].OpenGraph with match if app.IsPage then app.Page.OpenGraph else app.Posts.Posts[0].OpenGraph with
| Some props -> props.Properties |> Seq.iter writeOgProp | Some props -> props.ToProperties app.WebLog.UrlToAbsolute |> Seq.iter writeOgProp
| None -> () | None -> ()
writer.WriteLine $"""{s}<meta name=generator content="{app.Generator}">""" writer.WriteLine $"""{s}<meta name=generator content="{app.Generator}">"""