WIP on "all the feeds"
This commit is contained in:
		
							parent
							
								
									2589d388f1
								
							
						
					
					
						commit
						1cd7b06add
					
				| @ -20,6 +20,20 @@ type CommentIdConverter () = | |||||||
|     override _.ReadJson (reader : JsonReader, _ : Type, _ : CommentId, _ : bool, _ : JsonSerializer) = |     override _.ReadJson (reader : JsonReader, _ : Type, _ : CommentId, _ : bool, _ : JsonSerializer) = | ||||||
|         (string >> CommentId) reader.Value |         (string >> CommentId) reader.Value | ||||||
| 
 | 
 | ||||||
|  | type CustomFeedSourceConverter () = | ||||||
|  |     inherit JsonConverter<CustomFeedSource> () | ||||||
|  |     override _.WriteJson (writer : JsonWriter, value : CustomFeedSource, _ : JsonSerializer) = | ||||||
|  |         writer.WriteValue (CustomFeedSource.toString value) | ||||||
|  |     override _.ReadJson (reader : JsonReader, _ : Type, _ : CustomFeedSource, _ : bool, _ : JsonSerializer) = | ||||||
|  |         (string >> CustomFeedSource.parse) reader.Value | ||||||
|  |          | ||||||
|  | type ExplicitRatingConverter () = | ||||||
|  |     inherit JsonConverter<ExplicitRating> () | ||||||
|  |     override _.WriteJson (writer : JsonWriter, value : ExplicitRating, _ : JsonSerializer) = | ||||||
|  |         writer.WriteValue (ExplicitRating.toString value) | ||||||
|  |     override _.ReadJson (reader : JsonReader, _ : Type, _ : ExplicitRating, _ : bool, _ : JsonSerializer) = | ||||||
|  |         (string >> ExplicitRating.parse) reader.Value | ||||||
|  |      | ||||||
| type MarkupTextConverter () = | type MarkupTextConverter () = | ||||||
|     inherit JsonConverter<MarkupText> () |     inherit JsonConverter<MarkupText> () | ||||||
|     override _.WriteJson (writer : JsonWriter, value : MarkupText, _ : JsonSerializer) = |     override _.WriteJson (writer : JsonWriter, value : MarkupText, _ : JsonSerializer) = | ||||||
| @ -75,15 +89,17 @@ open Microsoft.FSharpLu.Json | |||||||
| let all () : JsonConverter seq = | let all () : JsonConverter seq = | ||||||
|     seq { |     seq { | ||||||
|         // Our converters |         // Our converters | ||||||
|         CategoryIdConverter   () |         CategoryIdConverter       () | ||||||
|         CommentIdConverter    () |         CommentIdConverter        () | ||||||
|         MarkupTextConverter   () |         CustomFeedSourceConverter () | ||||||
|         PermalinkConverter    () |         ExplicitRatingConverter   () | ||||||
|         PageIdConverter       () |         MarkupTextConverter       () | ||||||
|         PostIdConverter       () |         PermalinkConverter        () | ||||||
|         TagMapIdConverter     () |         PageIdConverter           () | ||||||
|         WebLogIdConverter     () |         PostIdConverter           () | ||||||
|         WebLogUserIdConverter () |         TagMapIdConverter         () | ||||||
|  |         WebLogIdConverter         () | ||||||
|  |         WebLogUserIdConverter     () | ||||||
|         // Handles DUs with no associated data, as well as option fields |         // Handles DUs with no associated data, as well as option fields | ||||||
|         CompactUnionJsonConverter () |         CompactUnionJsonConverter () | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -77,7 +77,7 @@ module Startup = | |||||||
|                 log.LogInformation $"Creating index {table}.permalink..." |                 log.LogInformation $"Creating index {table}.permalink..." | ||||||
|                 do! rethink { |                 do! rethink { | ||||||
|                     withTable table |                     withTable table | ||||||
|                     indexCreate "permalink" (fun row -> r.Array (row["webLogId"], row["permalink"]) :> obj) |                     indexCreate "permalink" (fun row -> r.Array (row["webLogId"], row["permalink"].Downcase ()) :> obj) | ||||||
|                     write; withRetryOnce; ignoreResult conn |                     write; withRetryOnce; ignoreResult conn | ||||||
|                 } |                 } | ||||||
|             // Prior permalinks are searched when a post or page permalink do not match the current URL |             // Prior permalinks are searched when a post or page permalink do not match the current URL | ||||||
| @ -85,7 +85,7 @@ module Startup = | |||||||
|                 log.LogInformation $"Creating index {table}.priorPermalinks..." |                 log.LogInformation $"Creating index {table}.priorPermalinks..." | ||||||
|                 do! rethink { |                 do! rethink { | ||||||
|                     withTable table |                     withTable table | ||||||
|                     indexCreate "priorPermalinks" [ Multi ] |                     indexCreate "priorPermalinks" (fun row -> row["priorPermalinks"].Downcase () :> obj) [ Multi ] | ||||||
|                     write; withRetryOnce; ignoreResult conn |                     write; withRetryOnce; ignoreResult conn | ||||||
|                 } |                 } | ||||||
|         // Post needs indexes by category and tag (used for counting and retrieving posts) |         // Post needs indexes by category and tag (used for counting and retrieving posts) | ||||||
|  | |||||||
| @ -272,12 +272,15 @@ type WebLog = | |||||||
| 
 | 
 | ||||||
|         /// The time zone in which dates/times should be displayed |         /// The time zone in which dates/times should be displayed | ||||||
|         timeZone : string |         timeZone : string | ||||||
|  |          | ||||||
|  |         /// The RSS options for this web log | ||||||
|  |         rss : RssOptions | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| /// Functions to support web logs | /// Functions to support web logs | ||||||
| module WebLog = | module WebLog = | ||||||
|      |      | ||||||
|     /// An empty set of web logs |     /// An empty web log | ||||||
|     let empty = |     let empty = | ||||||
|         { id           = WebLogId.empty |         { id           = WebLogId.empty | ||||||
|           name         = "" |           name         = "" | ||||||
| @ -287,6 +290,7 @@ module WebLog = | |||||||
|           themePath    = "default" |           themePath    = "default" | ||||||
|           urlBase      = "" |           urlBase      = "" | ||||||
|           timeZone     = "" |           timeZone     = "" | ||||||
|  |           rss          = RssOptions.empty | ||||||
|         } |         } | ||||||
|      |      | ||||||
|     /// Get the host (including scheme) and extra path from the URL base |     /// Get the host (including scheme) and extra path from the URL base | ||||||
|  | |||||||
| @ -187,6 +187,133 @@ module PostId = | |||||||
|     let create () = PostId (newId ()) |     let create () = PostId (newId ()) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | /// The source for a custom feed | ||||||
|  | type CustomFeedSource = | ||||||
|  |     /// A feed based on a particular category | ||||||
|  |     | Category of CategoryId | ||||||
|  |     /// A feed based on a particular tag | ||||||
|  |     | Tag of string | ||||||
|  | 
 | ||||||
|  | /// Functions to support feed sources | ||||||
|  | module CustomFeedSource = | ||||||
|  |     /// Create a string version of a feed source | ||||||
|  |     let toString : CustomFeedSource -> string = | ||||||
|  |         function | ||||||
|  |         | Category (CategoryId catId) -> $"category:{catId}" | ||||||
|  |         | Tag tag -> $"tag:{tag}" | ||||||
|  |      | ||||||
|  |     /// Parse a feed source from its string version | ||||||
|  |     let parse : string -> CustomFeedSource = | ||||||
|  |         let value (it : string) = it.Split(":").[1] | ||||||
|  |         function | ||||||
|  |         | source when source.StartsWith "category:" -> (value >> CategoryId >> Category) source | ||||||
|  |         | source when source.StartsWith "tag:"      -> (value >> Tag) source | ||||||
|  |         | source -> invalidArg "feedSource" $"{source} is not a valid feed source" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// Valid values for the iTunes explicit rating | ||||||
|  | type ExplicitRating = | ||||||
|  |     | Yes | ||||||
|  |     | No | ||||||
|  |     | Clean | ||||||
|  | 
 | ||||||
|  | /// Functions to support iTunes explicit ratings | ||||||
|  | module ExplicitRating = | ||||||
|  |     /// Convert an explicit rating to a string | ||||||
|  |     let toString : ExplicitRating -> string = | ||||||
|  |         function | ||||||
|  |         | Yes   -> "yes" | ||||||
|  |         | No    -> "no" | ||||||
|  |         | Clean -> "clean" | ||||||
|  |      | ||||||
|  |     /// Parse a string into an explicit rating | ||||||
|  |     let parse : string -> ExplicitRating = | ||||||
|  |         function | ||||||
|  |         | "yes"   -> Yes | ||||||
|  |         | "no"    -> No | ||||||
|  |         | "clean" -> Clean | ||||||
|  |         | x       -> raise (invalidArg "rating" $"{x} is not a valid explicit rating") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// Options for a feed that describes a podcast | ||||||
|  | type PodcastOptions = | ||||||
|  |     {   /// The title of the podcast | ||||||
|  |         title : string | ||||||
|  |          | ||||||
|  |         /// A subtitle for the podcast | ||||||
|  |         subtitle : string option | ||||||
|  |          | ||||||
|  |         /// The number of items in the podcast feed | ||||||
|  |         itemsInFeed : int | ||||||
|  |          | ||||||
|  |         /// A summary of the podcast (iTunes field) | ||||||
|  |         summary : string | ||||||
|  |          | ||||||
|  |         /// The display name of the podcast author (iTunes field) | ||||||
|  |         displayedAuthor : string | ||||||
|  |          | ||||||
|  |         /// The e-mail address of the user who registered the podcast at iTunes | ||||||
|  |         email : string | ||||||
|  |          | ||||||
|  |         /// The category from iTunes under which this podcast is categorized | ||||||
|  |         iTunesCategory : string | ||||||
|  |          | ||||||
|  |         /// A further refinement of the categorization of this podcast (iTunes field / values) | ||||||
|  |         iTunesSubcategory : string | ||||||
|  |          | ||||||
|  |         /// The explictness rating (iTunes field) | ||||||
|  |         explicit : ExplicitRating | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// A custom feed | ||||||
|  | type CustomFeed = | ||||||
|  |     {   /// The source for the custom feed | ||||||
|  |         source : CustomFeedSource | ||||||
|  |          | ||||||
|  |         /// The path for the custom feed | ||||||
|  |         path : Permalink | ||||||
|  |          | ||||||
|  |         /// Podcast options, if the feed defines a podcast | ||||||
|  |         podcast : PodcastOptions option | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// Really Simple Syndication (RSS) options for this web log | ||||||
|  | type RssOptions = | ||||||
|  |     {   /// Whether the site feed of posts is enabled | ||||||
|  |         feedEnabled : bool | ||||||
|  |          | ||||||
|  |         /// The name of the file generated for the site feed | ||||||
|  |         feedName : string | ||||||
|  |          | ||||||
|  |         /// Override the "posts per page" setting for the site feed | ||||||
|  |         itemsInFeed : int option | ||||||
|  |          | ||||||
|  |         /// Whether feeds are enabled for all categories | ||||||
|  |         categoryEnabled : bool | ||||||
|  |          | ||||||
|  |         /// Whether feeds are enabled for all tags | ||||||
|  |         tagEnabled : bool | ||||||
|  |          | ||||||
|  |         /// Custom feeds for this web log | ||||||
|  |         customFeeds: CustomFeed list | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | /// Functions to support RSS options | ||||||
|  | module RssOptions = | ||||||
|  |      | ||||||
|  |     /// An empty set of RSS options | ||||||
|  |     let empty = | ||||||
|  |         { feedEnabled     = true | ||||||
|  |           feedName        = "feed.xml" | ||||||
|  |           itemsInFeed     = None | ||||||
|  |           categoryEnabled = true | ||||||
|  |           tagEnabled      = true | ||||||
|  |           customFeeds     = [] | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| /// An identifier for a tag mapping | /// An identifier for a tag mapping | ||||||
| type TagMapId = TagMapId of string | type TagMapId = TagMapId of string | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -195,84 +195,119 @@ let home : HttpHandler = fun next ctx -> task { | |||||||
|         | None -> return! Error.notFound next ctx |         | None -> return! Error.notFound next ctx | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// Functions to support generating RSS feeds | ||||||
|  | module Feed = | ||||||
|      |      | ||||||
| open System.IO |     open System.IO | ||||||
| open System.ServiceModel.Syndication |     open System.ServiceModel.Syndication | ||||||
| open System.Text.RegularExpressions |     open System.Text.RegularExpressions | ||||||
| open System.Xml |     open System.Xml | ||||||
| 
 | 
 | ||||||
| // GET /feed.xml |     /// The type of feed to generate | ||||||
| //   (Routing handled by catch-all handler for future configurability) |     type FeedType = | ||||||
| let generateFeed : HttpHandler = fun next ctx -> backgroundTask { |         | Standard | ||||||
|     let  webLog  = ctx.WebLog |         | Category of CategoryId | ||||||
|     let  conn    = ctx.Conn |         | Tag      of string | ||||||
|     // TODO: hard-coded number of items |         | Custom   of CustomFeed | ||||||
|     let! posts   = Data.Post.findPageOfPublishedPosts webLog.id 1 10 conn |  | ||||||
|     let! authors = getAuthors     webLog posts conn |  | ||||||
|     let! tagMaps = getTagMappings webLog posts conn |  | ||||||
|     let  cats    = CategoryCache.get ctx |  | ||||||
|      |      | ||||||
|     let toItem (post : Post) = |     /// Derive the type of RSS feed requested | ||||||
|         let plainText = |     let deriveFeedType ctx webLog feedPath : (FeedType * int) option = | ||||||
|             Regex.Replace (post.text, "<(.|\n)*?>", "") |         let name = $"/{webLog.rss.feedName}" | ||||||
|             |> function |         let postCount = defaultArg webLog.rss.itemsInFeed webLog.postsPerPage | ||||||
|             | txt when txt.Length < 255 -> txt |         // Standard feed | ||||||
|             | txt -> $"{txt.Substring (0, 252)}..." |         match webLog.rss.feedEnabled && feedPath = name with | ||||||
|         let item = SyndicationItem ( |         | true -> Some (Standard, postCount) | ||||||
|             Id              = WebLog.absoluteUrl webLog post.permalink, |         | false -> | ||||||
|             Title           = TextSyndicationContent.CreateHtmlContent post.title, |             // Category feed | ||||||
|             PublishDate     = DateTimeOffset post.publishedOn.Value, |             match CategoryCache.get ctx |> Array.tryFind (fun cat -> cat.slug = feedPath.Replace (name, "")) with | ||||||
|             LastUpdatedTime = DateTimeOffset post.updatedOn, |             | Some cat -> Some (Category (CategoryId cat.id), postCount) | ||||||
|             Content         = TextSyndicationContent.CreatePlaintextContent plainText) |             | None -> | ||||||
|         item.AddPermalink (Uri item.Id) |                 // Tag feed | ||||||
|  |                 match feedPath.StartsWith "/tag/" with | ||||||
|  |                 | true -> Some (Tag (feedPath.Replace("/tag/", "").Replace(name, "")), postCount) | ||||||
|  |                 | false -> | ||||||
|  |                     // Custom feed | ||||||
|  |                     match webLog.rss.customFeeds | ||||||
|  |                           |> List.tryFind (fun it -> (Permalink.toString it.path).EndsWith feedPath) with | ||||||
|  |                     | Some feed -> | ||||||
|  |                         Some (Custom feed, | ||||||
|  |                               feed.podcast |> Option.map (fun p -> p.itemsInFeed) |> Option.defaultValue postCount) | ||||||
|  |                     | None -> | ||||||
|  |                         // No feed | ||||||
|  |                         None | ||||||
|          |          | ||||||
|         let encoded = |     // GET {any-prescribed-feed} | ||||||
|             post.text.Replace("src=\"/", $"src=\"{webLog.urlBase}/").Replace ("href=\"/", $"href=\"{webLog.urlBase}/") |     let generate (feedType : FeedType) postCount : HttpHandler = fun next ctx -> backgroundTask { | ||||||
|         item.ElementExtensions.Add ("encoded", "http://purl.org/rss/1.0/modules/content/", encoded) |         // TODO: stopped here; use feed type and count in the below function | ||||||
|         item.Authors.Add (SyndicationPerson ( |         let  webLog  = ctx.WebLog | ||||||
|             Name = (authors |> List.find (fun a -> a.name = WebLogUserId.toString post.authorId)).value)) |         let  conn    = ctx.Conn | ||||||
|         [ post.categoryIds |         let! posts   = Data.Post.findPageOfPublishedPosts webLog.id 1 postCount conn | ||||||
|           |> List.map (fun catId -> |         let! authors = getAuthors     webLog posts conn | ||||||
|               let cat = cats |> Array.find (fun c -> c.id = CategoryId.toString catId) |         let! tagMaps = getTagMappings webLog posts conn | ||||||
|               SyndicationCategory (cat.name, WebLog.absoluteUrl webLog (Permalink $"category/{cat.slug}/"), cat.name)) |         let  cats    = CategoryCache.get ctx | ||||||
|           post.tags |          | ||||||
|           |> List.map (fun tag -> |         let toItem (post : Post) = | ||||||
|               let urlTag = |             let plainText = | ||||||
|                   match tagMaps |> List.tryFind (fun tm -> tm.tag = tag) with |                 Regex.Replace (post.text, "<(.|\n)*?>", "") | ||||||
|                   | Some tm -> tm.urlValue |                 |> function | ||||||
|                   | None -> tag.Replace (" ", "+") |                 | txt when txt.Length < 255 -> txt | ||||||
|               SyndicationCategory (tag, WebLog.absoluteUrl webLog (Permalink $"tag/{urlTag}/"), $"{tag} (tag)")) |                 | txt -> $"{txt.Substring (0, 252)}..." | ||||||
|         ] |             let item = SyndicationItem ( | ||||||
|         |> List.concat |                 Id              = WebLog.absoluteUrl webLog post.permalink, | ||||||
|         |> List.iter item.Categories.Add |                 Title           = TextSyndicationContent.CreateHtmlContent post.title, | ||||||
|         item |                 PublishDate     = DateTimeOffset post.publishedOn.Value, | ||||||
|  |                 LastUpdatedTime = DateTimeOffset post.updatedOn, | ||||||
|  |                 Content         = TextSyndicationContent.CreatePlaintextContent plainText) | ||||||
|  |             item.AddPermalink (Uri item.Id) | ||||||
|  |              | ||||||
|  |             let encoded = | ||||||
|  |                 post.text.Replace("src=\"/", $"src=\"{webLog.urlBase}/").Replace ("href=\"/", $"href=\"{webLog.urlBase}/") | ||||||
|  |             item.ElementExtensions.Add ("encoded", "http://purl.org/rss/1.0/modules/content/", encoded) | ||||||
|  |             item.Authors.Add (SyndicationPerson ( | ||||||
|  |                 Name = (authors |> List.find (fun a -> a.name = WebLogUserId.toString post.authorId)).value)) | ||||||
|  |             [ post.categoryIds | ||||||
|  |               |> List.map (fun catId -> | ||||||
|  |                   let cat = cats |> Array.find (fun c -> c.id = CategoryId.toString catId) | ||||||
|  |                   SyndicationCategory (cat.name, WebLog.absoluteUrl webLog (Permalink $"category/{cat.slug}/"), cat.name)) | ||||||
|  |               post.tags | ||||||
|  |               |> List.map (fun tag -> | ||||||
|  |                   let urlTag = | ||||||
|  |                       match tagMaps |> List.tryFind (fun tm -> tm.tag = tag) with | ||||||
|  |                       | Some tm -> tm.urlValue | ||||||
|  |                       | None -> tag.Replace (" ", "+") | ||||||
|  |                   SyndicationCategory (tag, WebLog.absoluteUrl webLog (Permalink $"tag/{urlTag}/"), $"{tag} (tag)")) | ||||||
|  |             ] | ||||||
|  |             |> List.concat | ||||||
|  |             |> List.iter item.Categories.Add | ||||||
|  |             item | ||||||
|              |              | ||||||
|          |          | ||||||
|     let feed = SyndicationFeed () |         let feed = SyndicationFeed () | ||||||
|     feed.Title           <- TextSyndicationContent webLog.name |         feed.Title           <- TextSyndicationContent webLog.name | ||||||
|     feed.Description     <- TextSyndicationContent <| defaultArg webLog.subtitle webLog.name |         feed.Description     <- TextSyndicationContent <| defaultArg webLog.subtitle webLog.name | ||||||
|     feed.LastUpdatedTime <- DateTimeOffset <| (List.head posts).updatedOn |         feed.LastUpdatedTime <- DateTimeOffset <| (List.head posts).updatedOn | ||||||
|     feed.Generator       <- generator ctx |         feed.Generator       <- generator ctx | ||||||
|     feed.Items           <- posts |> Seq.ofList |> Seq.map toItem |         feed.Items           <- posts |> Seq.ofList |> Seq.map toItem | ||||||
|     feed.Language        <- "en" |         feed.Language        <- "en" | ||||||
|     feed.Id              <- webLog.urlBase |         feed.Id              <- webLog.urlBase | ||||||
|          |          | ||||||
|     feed.Links.Add (SyndicationLink (Uri $"{webLog.urlBase}/feed.xml", "self", "", "application/rss+xml", 0L)) |         feed.Links.Add (SyndicationLink (Uri $"{webLog.urlBase}/feed.xml", "self", "", "application/rss+xml", 0L)) | ||||||
|     feed.AttributeExtensions.Add |         feed.AttributeExtensions.Add | ||||||
|         (XmlQualifiedName ("content", "http://www.w3.org/2000/xmlns/"), "http://purl.org/rss/1.0/modules/content/") |             (XmlQualifiedName ("content", "http://www.w3.org/2000/xmlns/"), "http://purl.org/rss/1.0/modules/content/") | ||||||
|     feed.ElementExtensions.Add ("link", "", webLog.urlBase) |         feed.ElementExtensions.Add ("link", "", webLog.urlBase) | ||||||
|          |          | ||||||
|     use mem = new MemoryStream () |         use mem = new MemoryStream () | ||||||
|     use xml = XmlWriter.Create mem |         use xml = XmlWriter.Create mem | ||||||
|     feed.SaveAsRss20 xml |         feed.SaveAsRss20 xml | ||||||
|     xml.Close () |         xml.Close () | ||||||
|          |          | ||||||
|     let _ = mem.Seek (0L, SeekOrigin.Begin) |         let _ = mem.Seek (0L, SeekOrigin.Begin) | ||||||
|     let rdr = new StreamReader(mem) |         let rdr = new StreamReader(mem) | ||||||
|     let! output = rdr.ReadToEndAsync () |         let! output = rdr.ReadToEndAsync () | ||||||
|  |          | ||||||
|  |         return! ( setHttpHeader "Content-Type" "text/xml" >=> setStatusCode 200 >=> setBodyFromString output) next ctx | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     return! ( setHttpHeader "Content-Type" "text/xml" >=> setStatusCode 200 >=> setBodyFromString output) next ctx |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| /// Sequence where the first returned value is the proper handler for the link | /// Sequence where the first returned value is the proper handler for the link | ||||||
| let private deriveAction (ctx : HttpContext) : HttpHandler seq = | let private deriveAction (ctx : HttpContext) : HttpHandler seq = | ||||||
| @ -281,7 +316,7 @@ let private deriveAction (ctx : HttpContext) : HttpHandler seq = | |||||||
|     let textLink = |     let textLink = | ||||||
|         let _, extra = WebLog.hostAndPath webLog |         let _, extra = WebLog.hostAndPath webLog | ||||||
|         let url      = string ctx.Request.Path |         let url      = string ctx.Request.Path | ||||||
|         if extra = "" then url else url.Substring extra.Length |         (if extra = "" then url else url.Substring extra.Length).ToLowerInvariant () | ||||||
|     let await it = (Async.AwaitTask >> Async.RunSynchronously) it |     let await it = (Async.AwaitTask >> Async.RunSynchronously) it | ||||||
|     seq { |     seq { | ||||||
|         debug "Post" ctx (fun () -> $"Considering URL {textLink}") |         debug "Post" ctx (fun () -> $"Considering URL {textLink}") | ||||||
| @ -303,8 +338,9 @@ let private deriveAction (ctx : HttpContext) : HttpHandler seq = | |||||||
|                 |> themedView (defaultArg page.template "single-page") next ctx |                 |> themedView (defaultArg page.template "single-page") next ctx | ||||||
|         | None -> () |         | None -> () | ||||||
|         // RSS feed |         // RSS feed | ||||||
|         // TODO: configure this via web log |         match Feed.deriveFeedType ctx webLog textLink with | ||||||
|         if textLink = "/feed.xml" then yield generateFeed |         | Some (feedType, postCount) -> yield Feed.generate feedType postCount  | ||||||
|  |         | None -> () | ||||||
|         // Post differing only by trailing slash |         // Post differing only by trailing slash | ||||||
|         let altLink = Permalink (if textLink.EndsWith "/" then textLink[..textLink.Length - 2] else $"{textLink}/") |         let altLink = Permalink (if textLink.EndsWith "/" then textLink[..textLink.Length - 2] else $"{textLink}/") | ||||||
|         match Data.Post.findByPermalink altLink webLog.id conn |> await with |         match Data.Post.findByPermalink altLink webLog.id conn |> await with | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ | |||||||
|     "hostname": "data02.bitbadger.solutions", |     "hostname": "data02.bitbadger.solutions", | ||||||
|     "database": "myWebLog_dev" |     "database": "myWebLog_dev" | ||||||
|   }, |   }, | ||||||
|   "Generator": "myWebLog 2.0-alpha14", |   "Generator": "myWebLog 2.0-alpha15", | ||||||
|   "Logging": { |   "Logging": { | ||||||
|     "LogLevel": { |     "LogLevel": { | ||||||
|       "MyWebLog.Handlers": "Debug" |       "MyWebLog.Handlers": "Debug" | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user