Support directory installations
- Add web log to HttpContext on first retrieval per req - Add several DotLiquid filters - Use int for page numbers - Update all themes to use rel/abs link and other filters
This commit is contained in:
		
							parent
							
								
									63425ad806
								
							
						
					
					
						commit
						cbf87f5b49
					
				| @ -540,35 +540,32 @@ module Post = | ||||
|         } | ||||
|      | ||||
|     /// Find posts to be displayed on an admin page | ||||
|     let findPageOfPosts (webLogId : WebLogId) (pageNbr : int64) postsPerPage = | ||||
|         let pg = int pageNbr | ||||
|     let findPageOfPosts (webLogId : WebLogId) (pageNbr : int) postsPerPage = | ||||
|         rethink<Post list> { | ||||
|             withTable Table.Post | ||||
|             getAll [ webLogId ] (nameof webLogId) | ||||
|             without [ "priorPermalinks"; "revisions" ] | ||||
|             orderByFuncDescending (fun row -> row["publishedOn"].Default_ "updatedOn" :> obj) | ||||
|             skip ((pg - 1) * postsPerPage) | ||||
|             skip ((pageNbr - 1) * postsPerPage) | ||||
|             limit (postsPerPage + 1) | ||||
|             result; withRetryDefault | ||||
|         } | ||||
| 
 | ||||
|     /// Find posts to be displayed on a page | ||||
|     let findPageOfPublishedPosts (webLogId : WebLogId) (pageNbr : int64) postsPerPage = | ||||
|         let pg = int pageNbr | ||||
|     let findPageOfPublishedPosts (webLogId : WebLogId) pageNbr postsPerPage = | ||||
|         rethink<Post list> { | ||||
|             withTable Table.Post | ||||
|             getAll [ webLogId ] (nameof webLogId) | ||||
|             filter "status" Published | ||||
|             without [ "priorPermalinks"; "revisions" ] | ||||
|             orderByDescending "publishedOn" | ||||
|             skip ((pg - 1) * postsPerPage) | ||||
|             skip ((pageNbr - 1) * postsPerPage) | ||||
|             limit (postsPerPage + 1) | ||||
|             result; withRetryDefault | ||||
|         } | ||||
|      | ||||
|     /// Find posts to be displayed on a tag list page | ||||
|     let findPageOfTaggedPosts (webLogId : WebLogId) (tag : string) (pageNbr : int64) postsPerPage = | ||||
|         let pg = int pageNbr | ||||
|     let findPageOfTaggedPosts (webLogId : WebLogId) (tag : string) pageNbr postsPerPage = | ||||
|         rethink<Post list> { | ||||
|             withTable Table.Post | ||||
|             getAll [ tag ] "tags" | ||||
| @ -576,7 +573,7 @@ module Post = | ||||
|             filter "status" Published | ||||
|             without [ "priorPermalinks"; "revisions" ] | ||||
|             orderByDescending "publishedOn" | ||||
|             skip ((pg - 1) * postsPerPage) | ||||
|             skip ((pageNbr - 1) * postsPerPage) | ||||
|             limit (postsPerPage + 1) | ||||
|             result; withRetryDefault | ||||
|         } | ||||
| @ -711,6 +708,12 @@ module WebLog = | ||||
|         write; withRetryOnce; ignoreResult | ||||
|     } | ||||
|      | ||||
|     /// Get all web logs | ||||
|     let all = rethink<WebLog list> { | ||||
|         withTable Table.WebLog | ||||
|         result; withRetryDefault | ||||
|     } | ||||
|      | ||||
|     /// Retrieve a web log by the URL base | ||||
|     let findByHost (url : string) = | ||||
|         rethink<WebLog list> { | ||||
|  | ||||
| @ -289,8 +289,20 @@ module WebLog = | ||||
|           timeZone     = "" | ||||
|         } | ||||
|      | ||||
|     /// Convert a permalink to an absolute URL | ||||
|     let absoluteUrl webLog = function Permalink link -> $"{webLog.urlBase}{link}" | ||||
|     /// Get the host (including scheme) and extra path from the URL base | ||||
|     let hostAndPath webLog = | ||||
|         let scheme = webLog.urlBase.Split "://" | ||||
|         let host   = scheme[1].Split "/" | ||||
|         $"{scheme[0]}://{host[0]}", if host.Length > 1 then $"""/{String.Join ("/", host |> Array.skip 1)}""" else "" | ||||
|      | ||||
|     /// Generate an absolute URL for the given link | ||||
|     let absoluteUrl webLog permalink = | ||||
|         $"{webLog.urlBase}/{Permalink.toString permalink}" | ||||
| 
 | ||||
|     /// Generate a relative URL for the given link | ||||
|     let relativeUrl webLog permalink = | ||||
|         let _, leadPath = hostAndPath webLog | ||||
|         $"{leadPath}/{Permalink.toString permalink}" | ||||
|      | ||||
|     /// Convert a date/time to the web log's local date/time | ||||
|     let localTime webLog (date : DateTime) = | ||||
|  | ||||
| @ -400,6 +400,7 @@ type PostListItem = | ||||
| 
 | ||||
|     /// Create a post list item from a post | ||||
|     static member fromPost (webLog : WebLog) (post : Post) = | ||||
|         let _, extra = WebLog.hostAndPath webLog | ||||
|         let inTZ     = WebLog.localTime   webLog | ||||
|         { id          = PostId.toString post.id | ||||
|           authorId    = WebLogUserId.toString post.authorId | ||||
| @ -408,7 +409,7 @@ type PostListItem = | ||||
|           permalink   = Permalink.toString post.permalink | ||||
|           publishedOn = post.publishedOn |> Option.map inTZ |> Option.toNullable | ||||
|           updatedOn   = inTZ post.updatedOn | ||||
|           text        = post.text | ||||
|           text        = if extra = "" then post.text else post.text.Replace ("href=\"/", $"href=\"{extra}/") | ||||
|           categoryIds = post.categoryIds |> List.map CategoryId.toString | ||||
|           tags        = post.tags | ||||
|           meta        = post.metadata | ||||
|  | ||||
| @ -6,10 +6,12 @@ open Microsoft.AspNetCore.Http | ||||
| module Cache = | ||||
|      | ||||
|     /// Create the cache key for the web log for the current request | ||||
|     let makeKey (ctx : HttpContext) = ctx.Request.Host.ToUriComponent () | ||||
|     let makeKey (ctx : HttpContext) = (ctx.Items["webLog"] :?> WebLog).urlBase | ||||
| 
 | ||||
| 
 | ||||
| open System.Collections.Concurrent | ||||
| open Microsoft.Extensions.DependencyInjection | ||||
| open RethinkDb.Driver.Net | ||||
| 
 | ||||
| /// <summary> | ||||
| /// In-memory cache of web log details | ||||
| @ -18,21 +20,33 @@ open System.Collections.Concurrent | ||||
| /// settings update page</remarks> | ||||
| module WebLogCache = | ||||
|      | ||||
|     /// Create the full path of the request | ||||
|     let private fullPath (ctx : HttpContext) = | ||||
|         $"{ctx.Request.Scheme}://{ctx.Request.Host.Value}{ctx.Request.Path.Value}" | ||||
|      | ||||
|     /// The cache of web log details | ||||
|     let private _cache = ConcurrentDictionary<string, WebLog> () | ||||
|     let mutable private _cache : WebLog list = [] | ||||
| 
 | ||||
|     /// Does a host exist in the cache? | ||||
|     let exists ctx = _cache.ContainsKey (Cache.makeKey ctx) | ||||
|     let exists ctx = | ||||
|         let path = fullPath ctx | ||||
|         _cache |> List.exists (fun wl -> path.StartsWith wl.urlBase) | ||||
| 
 | ||||
|     /// Get the web log for the current request | ||||
|     let get ctx = _cache[Cache.makeKey ctx] | ||||
|     let get ctx = | ||||
|         let path = fullPath ctx | ||||
|         _cache |> List.find (fun wl -> path.StartsWith wl.urlBase) | ||||
| 
 | ||||
|     /// Cache the web log for a particular host | ||||
|     let set ctx webLog = _cache[Cache.makeKey ctx] <- webLog | ||||
|     let set webLog = | ||||
|         _cache <- webLog :: (_cache |> List.filter (fun wl -> wl.id <> webLog.id)) | ||||
|      | ||||
|     /// Fill the web log cache from the database | ||||
|     let fill conn = backgroundTask { | ||||
|         let! webLogs = Data.WebLog.all conn | ||||
|         _cache <- webLogs | ||||
|     } | ||||
| 
 | ||||
| open Microsoft.Extensions.DependencyInjection | ||||
| open RethinkDb.Driver.Net | ||||
| 
 | ||||
| /// A cache of page information needed to display the page list in templates | ||||
| module PageListCache = | ||||
| @ -42,12 +56,15 @@ module PageListCache = | ||||
|     /// Cache of displayed pages | ||||
|     let private _cache = ConcurrentDictionary<string, DisplayPage[]> () | ||||
|      | ||||
|     /// Are there pages cached for this web log? | ||||
|     let exists ctx = _cache.ContainsKey (Cache.makeKey ctx) | ||||
|      | ||||
|     /// Get the pages for the web log for this request | ||||
|     let get ctx = _cache[Cache.makeKey ctx] | ||||
|      | ||||
|     /// Update the pages for the current web log | ||||
|     let update ctx = task { | ||||
|         let  webLog = WebLogCache.get ctx | ||||
|     let update (ctx : HttpContext) = backgroundTask { | ||||
|         let  webLog = ctx.Items["webLog"] :?> WebLog | ||||
|         let  conn   = ctx.RequestServices.GetRequiredService<IConnection> () | ||||
|         let! pages  = Data.Page.findListed webLog.id conn | ||||
|         _cache[Cache.makeKey ctx] <- pages |> List.map (DisplayPage.fromPage webLog) |> Array.ofList | ||||
| @ -62,12 +79,15 @@ module CategoryCache = | ||||
|     /// The cache itself | ||||
|     let private _cache = ConcurrentDictionary<string, DisplayCategory[]> () | ||||
|      | ||||
|     /// Are there categories cached for this web log? | ||||
|     let exists ctx = _cache.ContainsKey (Cache.makeKey ctx) | ||||
|      | ||||
|     /// Get the categories for the web log for this request | ||||
|     let get ctx = _cache[Cache.makeKey ctx] | ||||
|      | ||||
|     /// Update the cache with fresh data | ||||
|     let update ctx = backgroundTask { | ||||
|         let  webLog = WebLogCache.get ctx | ||||
|     let update (ctx : HttpContext) = backgroundTask { | ||||
|         let  webLog = ctx.Items["webLog"] :?> WebLog | ||||
|         let  conn   = ctx.RequestServices.GetRequiredService<IConnection> () | ||||
|         let! cats   = Data.Category.findAllForView webLog.id conn | ||||
|         _cache[Cache.makeKey ctx] <- cats | ||||
| @ -84,7 +104,7 @@ module TemplateCache = | ||||
|     let private _cache = ConcurrentDictionary<string, Template> () | ||||
|      | ||||
|     /// Get a template for the given theme and template nate | ||||
|     let get (theme : string) (templateName : string) = task { | ||||
|     let get (theme : string) (templateName : string) = backgroundTask { | ||||
|         let templatePath = $"themes/{theme}/{templateName}" | ||||
|         match _cache.ContainsKey templatePath with | ||||
|         | true -> () | ||||
|  | ||||
							
								
								
									
										123
									
								
								src/MyWebLog/DotLiquidBespoke.fs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/MyWebLog/DotLiquidBespoke.fs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,123 @@ | ||||
| /// Custom DotLiquid filters and tags | ||||
| module MyWebLog.DotLiquidBespoke | ||||
| 
 | ||||
| open System | ||||
| open System.IO | ||||
| open DotLiquid | ||||
| open MyWebLog.ViewModels | ||||
| 
 | ||||
| /// Get the current web log from the DotLiquid context | ||||
| let webLog (ctx : Context) = | ||||
|     ctx.Environments[0].["web_log"] :?> WebLog | ||||
| 
 | ||||
| /// Obtain the link from known types | ||||
| let permalink (ctx : Context) (item : obj) (linkFunc : WebLog -> Permalink -> string) = | ||||
|     match item with | ||||
|     | :? String       as link  -> Some link | ||||
|     | :? DisplayPage  as page  -> Some page.permalink | ||||
|     | :? PostListItem as post  -> Some post.permalink | ||||
|     | :? DropProxy    as proxy -> Option.ofObj proxy["permalink"] |> Option.map string | ||||
|     | _ -> None | ||||
|     |> function | ||||
|     | Some link -> linkFunc (webLog ctx) (Permalink link) | ||||
|     | None      -> $"alert('unknown item type {item.GetType().Name}')" | ||||
| 
 | ||||
| /// A filter to generate an absolute link | ||||
| type AbsoluteLinkFilter () = | ||||
|     static member AbsoluteLink (ctx : Context, item : obj) = | ||||
|         permalink ctx item WebLog.absoluteUrl | ||||
| 
 | ||||
| /// A filter to generate a link with posts categorized under the given category | ||||
| type CategoryLinkFilter () = | ||||
|     static member CategoryLink (ctx : Context, catObj : obj) = | ||||
|         match catObj with | ||||
|         | :? DisplayCategory as cat   -> Some cat.slug | ||||
|         | :? DropProxy       as proxy -> Option.ofObj proxy["slug"] |> Option.map string | ||||
|         | _ -> None | ||||
|         |> function | ||||
|         | Some slug -> WebLog.relativeUrl (webLog ctx) (Permalink $"category/{slug}/") | ||||
|         | None      -> $"alert('unknown category object type {catObj.GetType().Name}')" | ||||
|          | ||||
| 
 | ||||
| /// A filter to generate a link that will edit a page | ||||
| type EditPageLinkFilter () = | ||||
|     static member EditPageLink (ctx : Context, pageObj : obj) = | ||||
|         match pageObj with | ||||
|         | :? DisplayPage as page  -> Some page.id | ||||
|         | :? DropProxy   as proxy -> Option.ofObj proxy["id"] |> Option.map string | ||||
|         | :? String      as theId -> Some theId | ||||
|         | _ -> None | ||||
|         |> function | ||||
|         | Some pageId -> WebLog.relativeUrl (webLog ctx) (Permalink $"admin/page/{pageId}/edit") | ||||
|         | None        -> $"alert('unknown page object type {pageObj.GetType().Name}')" | ||||
|      | ||||
| /// A filter to generate a link that will edit a post | ||||
| type EditPostLinkFilter () = | ||||
|     static member EditPostLink (ctx : Context, postObj : obj) = | ||||
|         match postObj with | ||||
|         | :? PostListItem as post  -> Some post.id | ||||
|         | :? DropProxy    as proxy -> Option.ofObj proxy["id"] |> Option.map string | ||||
|         | :? String       as theId -> Some theId | ||||
|         | _ -> None | ||||
|         |> function | ||||
|         | Some postId -> WebLog.relativeUrl (webLog ctx) (Permalink $"admin/post/{postId}/edit") | ||||
|         | None        -> $"alert('unknown post object type {postObj.GetType().Name}')" | ||||
|      | ||||
| /// A filter to generate nav links, highlighting the active link (exact match) | ||||
| type NavLinkFilter () = | ||||
|     static member NavLink (ctx : Context, url : string, text : string) = | ||||
|         let webLog = webLog ctx | ||||
|         seq { | ||||
|             "<li class=\"nav-item\"><a class=\"nav-link" | ||||
|             if url = string ctx.Environments[0].["current_page"] then " active" | ||||
|             "\" href=\"" | ||||
|             WebLog.relativeUrl webLog (Permalink url) | ||||
|             "\">" | ||||
|             text | ||||
|             "</a></li>" | ||||
|         } | ||||
|         |> Seq.fold (+) "" | ||||
| 
 | ||||
| /// A filter to generate a relative link | ||||
| type RelativeLinkFilter () = | ||||
|     static member RelativeLink (ctx : Context, item : obj) = | ||||
|         permalink ctx item WebLog.relativeUrl | ||||
| 
 | ||||
| /// A filter to generate a link with posts tagged with the given tag | ||||
| type TagLinkFilter () = | ||||
|     static member TagLink (ctx : Context, tag : string) = | ||||
|         ctx.Environments[0].["tag_mappings"] :?> TagMap list | ||||
|         |> List.tryFind (fun it -> it.tag = tag) | ||||
|         |> function | ||||
|         | Some tagMap -> tagMap.urlValue | ||||
|         | None        -> tag.Replace (" ", "+") | ||||
|         |> function tagUrl -> WebLog.relativeUrl (webLog ctx) (Permalink $"tag/{tagUrl}/") | ||||
|              | ||||
| /// Create links for a user to log on or off, and a dashboard link if they are logged off | ||||
| type UserLinksTag () = | ||||
|     inherit Tag () | ||||
|      | ||||
|     override this.Render (context : Context, result : TextWriter) = | ||||
|         let webLog = webLog context | ||||
|         let link it = WebLog.relativeUrl webLog (Permalink it) | ||||
|         seq { | ||||
|             """<ul class="navbar-nav flex-grow-1 justify-content-end">""" | ||||
|             match Convert.ToBoolean context.Environments[0].["logged_on"] with | ||||
|             | true -> | ||||
|                 $"""<li class="nav-item"><a class="nav-link" href="{link "admin"}">Dashboard</a></li>""" | ||||
|                 $"""<li class="nav-item"><a class="nav-link" href="{link "user/log-off"}">Log Off</a></li>""" | ||||
|             | false -> | ||||
|                 $"""<li class="nav-item"><a class="nav-link" href="{link "user/log-on"}">Log On</a></li>""" | ||||
|             "</ul>" | ||||
|         } | ||||
|         |> Seq.iter result.WriteLine | ||||
| 
 | ||||
| /// A filter to retrieve the value of a meta item from a list | ||||
| //    (shorter than `{% assign item = list | where: "name", [name] | first %}{{ item.value }}`) | ||||
| type ValueFilter () = | ||||
|     static member Value (_ : Context, items : MetaItem list, name : string) = | ||||
|         match items |> List.tryFind (fun it -> it.name = name) with | ||||
|         | Some item -> item.value | ||||
|         | None -> $"-- {name} not found --" | ||||
| 
 | ||||
| 
 | ||||
| @ -1,6 +1,8 @@ | ||||
| /// Handlers to manipulate admin functions | ||||
| module MyWebLog.Handlers.Admin | ||||
| 
 | ||||
| // TODO: remove requireUser, as this is applied in the router | ||||
| 
 | ||||
| open System.Collections.Generic | ||||
| open System.IO | ||||
| 
 | ||||
| @ -21,9 +23,9 @@ open RethinkDb.Driver.Net | ||||
| 
 | ||||
| // GET /admin | ||||
| let dashboard : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
|     let webLogId = webLogId ctx | ||||
|     let webLog = webLog ctx | ||||
|     let conn   = conn   ctx | ||||
|     let getCount (f : WebLogId -> IConnection -> Task<int>) = f webLogId conn | ||||
|     let getCount (f : WebLogId -> IConnection -> Task<int>) = f webLog.id conn | ||||
|     let! posts   = Data.Post.countByStatus Published |> getCount | ||||
|     let! drafts  = Data.Post.countByStatus Draft     |> getCount | ||||
|     let! pages   = Data.Page.countAll                |> getCount | ||||
| @ -60,13 +62,13 @@ let listCategories : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
| 
 | ||||
| // GET /admin/category/{id}/edit | ||||
| let editCategory catId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
|     let  webLogId = webLogId ctx | ||||
|     let  webLog = webLog ctx | ||||
|     let  conn   = conn   ctx | ||||
|     let! result = task { | ||||
|         match catId with | ||||
|         | "new" -> return Some ("Add a New Category", { Category.empty with id = CategoryId "new" }) | ||||
|         | _ -> | ||||
|             match! Data.Category.findById (CategoryId catId) webLogId conn with | ||||
|             match! Data.Category.findById (CategoryId catId) webLog.id conn with | ||||
|             | Some cat -> return Some ("Edit Category", cat) | ||||
|             | None -> return None | ||||
|     } | ||||
| @ -86,12 +88,12 @@ let editCategory catId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
| // POST /admin/category/save | ||||
| let saveCategory : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||
|     let! model    = ctx.BindFormAsync<EditCategoryModel> () | ||||
|     let  webLogId = webLogId ctx | ||||
|     let  webLog   = webLog ctx | ||||
|     let  conn     = conn   ctx | ||||
|     let! category = task { | ||||
|         match model.categoryId with | ||||
|         | "new" -> return Some { Category.empty with id = CategoryId.create (); webLogId = webLogId } | ||||
|         | catId -> return! Data.Category.findById (CategoryId catId) webLogId conn | ||||
|         | "new" -> return Some { Category.empty with id = CategoryId.create (); webLogId = webLog.id } | ||||
|         | catId -> return! Data.Category.findById (CategoryId catId) webLog.id conn | ||||
|     } | ||||
|     match category with | ||||
|     | Some cat -> | ||||
| @ -105,20 +107,22 @@ let saveCategory : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx - | ||||
|         do! (match model.categoryId with "new" -> Data.Category.add | _ -> Data.Category.update) cat conn | ||||
|         do! CategoryCache.update ctx | ||||
|         do! addMessage ctx { UserMessage.success with message = "Category saved successfully" } | ||||
|         return! redirectToGet $"/admin/category/{CategoryId.toString cat.id}/edit" next ctx | ||||
|         return! | ||||
|             redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/category/{CategoryId.toString cat.id}/edit")) | ||||
|                 next ctx | ||||
|     | None -> return! Error.notFound next ctx | ||||
| } | ||||
| 
 | ||||
| // POST /admin/category/{id}/delete | ||||
| let deleteCategory catId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||
|     let webLogId = webLogId ctx | ||||
|     let webLog = webLog ctx | ||||
|     let conn   = conn   ctx | ||||
|     match! Data.Category.delete (CategoryId catId) webLogId conn with | ||||
|     match! Data.Category.delete (CategoryId catId) webLog.id conn with | ||||
|     | true -> | ||||
|         do! CategoryCache.update ctx | ||||
|         do! addMessage ctx { UserMessage.success with message = "Category deleted successfully" } | ||||
|     | false -> do! addMessage ctx { UserMessage.error with message = "Category not found; cannot delete" } | ||||
|     return! redirectToGet "/admin/categories" next ctx | ||||
|     return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin/categories")) next ctx | ||||
| } | ||||
| 
 | ||||
| // -- PAGES -- | ||||
| @ -126,7 +130,7 @@ let deleteCategory catId : HttpHandler = requireUser >=> validateCsrf >=> fun ne | ||||
| // GET /admin/pages | ||||
| // GET /admin/pages/page/{pageNbr} | ||||
| let listPages pageNbr : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
|     let  webLog = WebLogCache.get ctx | ||||
|     let  webLog = webLog ctx | ||||
|     let! pages  = Data.Page.findPageOfPages webLog.id pageNbr (conn ctx) | ||||
|     return! | ||||
|         Hash.FromAnonymousObject | ||||
| @ -142,7 +146,7 @@ let editPage pgId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
|         match pgId with | ||||
|         | "new" -> return Some ("Add a New Page", { Page.empty with id = PageId "new" }) | ||||
|         | _ -> | ||||
|             match! Data.Page.findByFullId (PageId pgId) (webLogId ctx) (conn ctx) with | ||||
|             match! Data.Page.findByFullId (PageId pgId) (webLog ctx).id (conn ctx) with | ||||
|             | Some page -> return Some ("Edit Page", page) | ||||
|             | None -> return None | ||||
|     } | ||||
| @ -164,7 +168,7 @@ let editPage pgId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
| 
 | ||||
| // GET /admin/page/{id}/permalinks | ||||
| let editPagePermalinks pgId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
|     match! Data.Page.findByFullId (PageId pgId) (webLogId ctx) (conn ctx) with | ||||
|     match! Data.Page.findByFullId (PageId pgId) (webLog ctx).id (conn ctx) with | ||||
|     | Some pg -> | ||||
|         return! | ||||
|             Hash.FromAnonymousObject {| | ||||
| @ -178,31 +182,33 @@ let editPagePermalinks pgId : HttpHandler = requireUser >=> fun next ctx -> task | ||||
| 
 | ||||
| // POST /admin/page/permalinks | ||||
| let savePagePermalinks : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||
|     let  webLog = webLog ctx | ||||
|     let! model  = ctx.BindFormAsync<ManagePermalinksModel> () | ||||
|     let  links  = model.prior |> Array.map Permalink |> List.ofArray | ||||
|     match! Data.Page.updatePriorPermalinks (PageId model.id) (webLogId ctx) links (conn ctx) with | ||||
|     match! Data.Page.updatePriorPermalinks (PageId model.id) webLog.id links (conn ctx) with | ||||
|     | true -> | ||||
|         do! addMessage ctx { UserMessage.success with message = "Page permalinks saved successfully" } | ||||
|         return! redirectToGet $"/admin/page/{model.id}/permalinks" next ctx | ||||
|         return! redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/page/{model.id}/permalinks")) next ctx | ||||
|     | false -> return! Error.notFound next ctx | ||||
| } | ||||
| 
 | ||||
| // POST /admin/page/{id}/delete | ||||
| let deletePage pgId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||
|     match! Data.Page.delete (PageId pgId) (webLogId ctx) (conn ctx) with | ||||
|     let webLog = webLog ctx | ||||
|     match! Data.Page.delete (PageId pgId) webLog.id (conn ctx) with | ||||
|     | true  -> do! addMessage ctx { UserMessage.success with message = "Page deleted successfully" } | ||||
|     | false -> do! addMessage ctx { UserMessage.error with message = "Page not found; nothing deleted" } | ||||
|     return! redirectToGet "/admin/pages" next ctx | ||||
|     return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin/pages")) next ctx | ||||
| } | ||||
| 
 | ||||
| open System | ||||
| 
 | ||||
| #nowarn "3511" | ||||
| 
 | ||||
| // POST /page/save | ||||
| // POST /admin/page/save | ||||
| let savePage : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||
|     let! model  = ctx.BindFormAsync<EditPageModel> () | ||||
|     let  webLogId = webLogId ctx | ||||
|     let  webLog = webLog ctx | ||||
|     let  conn   = conn ctx | ||||
|     let  now    = DateTime.UtcNow | ||||
|     let! pg     = task { | ||||
| @ -211,11 +217,11 @@ let savePage : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> ta | ||||
|             return Some | ||||
|                 { Page.empty with | ||||
|                     id          = PageId.create () | ||||
|                     webLogId    = webLogId | ||||
|                     webLogId    = webLog.id | ||||
|                     authorId    = userId ctx | ||||
|                     publishedOn = now | ||||
|                 } | ||||
|         | pgId -> return! Data.Page.findByFullId (PageId pgId) webLogId conn | ||||
|         | pgId -> return! Data.Page.findByFullId (PageId pgId) webLog.id conn | ||||
|     } | ||||
|     match pg with | ||||
|     | Some page -> | ||||
| @ -247,7 +253,8 @@ let savePage : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> ta | ||||
|         do! (if model.pageId = "new" then Data.Page.add else Data.Page.update) page conn | ||||
|         if updateList then do! PageListCache.update ctx | ||||
|         do! addMessage ctx { UserMessage.success with message = "Page saved successfully" } | ||||
|         return! redirectToGet $"/admin/page/{PageId.toString page.id}/edit" next ctx | ||||
|         return! | ||||
|             redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/page/{PageId.toString page.id}/edit")) next ctx | ||||
|     | None -> return! Error.notFound next ctx | ||||
| } | ||||
| 
 | ||||
| @ -255,7 +262,7 @@ let savePage : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> ta | ||||
| 
 | ||||
| // GET /admin/settings | ||||
| let settings : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
|     let  webLog   = WebLogCache.get ctx | ||||
|     let  webLog   = webLog ctx | ||||
|     let! allPages = Data.Page.findAll webLog.id (conn ctx) | ||||
|     return! | ||||
|         Hash.FromAnonymousObject | ||||
| @ -278,9 +285,10 @@ let settings : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
| 
 | ||||
| // POST /admin/settings | ||||
| let saveSettings : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||
|     let  webLog = webLog ctx | ||||
|     let  conn   = conn ctx | ||||
|     let! model  = ctx.BindFormAsync<SettingsModel> () | ||||
|     match! Data.WebLog.findById (WebLogCache.get ctx).id conn with | ||||
|     match! Data.WebLog.findById webLog.id conn with | ||||
|     | Some webLog -> | ||||
|         let updated = | ||||
|             { webLog with | ||||
| @ -294,10 +302,10 @@ let saveSettings : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx - | ||||
|         do! Data.WebLog.updateSettings updated conn | ||||
| 
 | ||||
|         // Update cache | ||||
|         WebLogCache.set ctx updated | ||||
|         WebLogCache.set updated | ||||
|      | ||||
|         do! addMessage ctx { UserMessage.success with message = "Web log settings saved successfully" } | ||||
|         return! redirectToGet "/admin" next ctx | ||||
|         return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin")) next ctx | ||||
|     | None -> return! Error.notFound next ctx | ||||
| } | ||||
| 
 | ||||
| @ -305,7 +313,7 @@ let saveSettings : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx - | ||||
| 
 | ||||
| // GET /admin/tag-mappings | ||||
| let tagMappings : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
|     let! mappings = Data.TagMap.findByWebLogId (webLogId ctx) (conn ctx) | ||||
|     let! mappings = Data.TagMap.findByWebLogId (webLog ctx).id (conn ctx) | ||||
|     return! | ||||
|         Hash.FromAnonymousObject | ||||
|             {|  csrf        = csrfToken ctx | ||||
| @ -318,13 +326,12 @@ let tagMappings : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
| 
 | ||||
| // GET /admin/tag-mapping/{id}/edit | ||||
| let editMapping tagMapId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
|     let webLogId = webLogId ctx | ||||
|     let isNew  = tagMapId = "new" | ||||
|     let tagMap = | ||||
|         if isNew then | ||||
|             Task.FromResult (Some { TagMap.empty with id = TagMapId "new" }) | ||||
|         else | ||||
|             Data.TagMap.findById (TagMapId tagMapId) webLogId (conn ctx) | ||||
|             Data.TagMap.findById (TagMapId tagMapId) (webLog ctx).id (conn ctx) | ||||
|     match! tagMap with | ||||
|     | Some tm -> | ||||
|         return! | ||||
| @ -339,26 +346,29 @@ let editMapping tagMapId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
| 
 | ||||
| // POST /admin/tag-mapping/save | ||||
| let saveMapping : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||
|     let  webLogId = webLogId ctx | ||||
|     let  webLog = webLog ctx | ||||
|     let  conn   = conn   ctx | ||||
|     let! model  = ctx.BindFormAsync<EditTagMapModel> () | ||||
|     let  tagMap = | ||||
|         if model.id = "new" then | ||||
|             Task.FromResult (Some { TagMap.empty with id = TagMapId.create (); webLogId = webLogId }) | ||||
|             Task.FromResult (Some { TagMap.empty with id = TagMapId.create (); webLogId = webLog.id }) | ||||
|         else | ||||
|             Data.TagMap.findById (TagMapId model.id) webLogId conn | ||||
|             Data.TagMap.findById (TagMapId model.id) webLog.id conn | ||||
|     match! tagMap with | ||||
|     | Some tm -> | ||||
|         do! Data.TagMap.save { tm with tag = model.tag.ToLower (); urlValue = model.urlValue.ToLower () } conn | ||||
|         do! addMessage ctx { UserMessage.success with message = "Tag mapping saved successfully" } | ||||
|         return! redirectToGet $"/admin/tag-mapping/{TagMapId.toString tm.id}/edit" next ctx | ||||
|         return! | ||||
|             redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/tag-mapping/{TagMapId.toString tm.id}/edit")) | ||||
|                 next ctx | ||||
|     | None -> return! Error.notFound next ctx | ||||
| } | ||||
| 
 | ||||
| // POST /admin/tag-mapping/{id}/delete | ||||
| let deleteMapping tagMapId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||
|     match! Data.TagMap.delete (TagMapId tagMapId) (webLogId ctx) (conn ctx) with | ||||
|     let webLog = webLog ctx | ||||
|     match! Data.TagMap.delete (TagMapId tagMapId) webLog.id (conn ctx) with | ||||
|     | true  -> do! addMessage ctx { UserMessage.success with message = "Tag mapping deleted successfully" } | ||||
|     | false -> do! addMessage ctx { UserMessage.error with message = "Tag mapping not found; nothing deleted" } | ||||
|     return! redirectToGet "/admin/tag-mappings" next ctx | ||||
|     return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin/tag-mappings")) next ctx | ||||
| } | ||||
|  | ||||
| @ -3,15 +3,19 @@ module MyWebLog.Handlers.Error | ||||
| 
 | ||||
| open System.Net | ||||
| open System.Threading.Tasks | ||||
| open Microsoft.AspNetCore.Http | ||||
| open Giraffe | ||||
| open Microsoft.AspNetCore.Http | ||||
| open MyWebLog | ||||
| 
 | ||||
| /// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized response | ||||
| let notAuthorized : HttpHandler = fun next ctx -> | ||||
|     (next, ctx) | ||||
|     ||> match ctx.Request.Method with | ||||
|         | "GET" -> redirectTo false $"/user/log-on?returnUrl={WebUtility.UrlEncode ctx.Request.Path}" | ||||
|         | _ -> setStatusCode 401 >=> fun _ _ -> Task.FromResult<HttpContext option> None | ||||
| let notAuthorized : HttpHandler = fun next ctx -> task { | ||||
|     let webLog = ctx.Items["webLog"] :?> WebLog | ||||
|     if ctx.Request.Method = "GET" then | ||||
|         let returnUrl = WebUtility.UrlEncode ctx.Request.Path | ||||
|         return! redirectTo false (WebLog.relativeUrl webLog (Permalink $"user/log-on?returnUrl={returnUrl}")) next ctx | ||||
|     else | ||||
|         return! (setStatusCode 401 >=> fun _ _ -> Task.FromResult<HttpContext option> None) next ctx | ||||
| } | ||||
| 
 | ||||
| /// Handle 404s from the API, sending known URL paths to the Vue app so that they can be handled there | ||||
| let notFound : HttpHandler = | ||||
|  | ||||
| @ -67,17 +67,18 @@ let generator (ctx : HttpContext) = | ||||
|         generatorString <- Option.ofObj cfg["Generator"] | ||||
|         defaultArg generatorString "generator not configured" | ||||
| 
 | ||||
| open DotLiquid | ||||
| open MyWebLog | ||||
| 
 | ||||
| /// Get the web log for the request from the context (established by middleware) | ||||
| let webLog (ctx : HttpContext) = | ||||
|     ctx.Items["webLog"] :?> WebLog | ||||
| 
 | ||||
| open DotLiquid | ||||
| 
 | ||||
| /// Either get the web log from the hash, or get it from the cache and add it to the hash | ||||
| let private deriveWebLogFromHash (hash : Hash) ctx = | ||||
|     match hash.ContainsKey "web_log" with | ||||
|     | true -> hash["web_log"] :?> WebLog | ||||
|     | false -> | ||||
|         let wl = WebLogCache.get ctx | ||||
|         hash.Add ("web_log", wl) | ||||
|         wl | ||||
|     if hash.ContainsKey "web_log" then () else hash.Add ("web_log", webLog ctx) | ||||
|     hash["web_log"] :?> WebLog | ||||
| 
 | ||||
| open Giraffe | ||||
| 
 | ||||
| @ -118,9 +119,6 @@ let redirectToGet url : HttpHandler = fun next ctx -> task { | ||||
|     return! redirectTo false url next ctx | ||||
| } | ||||
| 
 | ||||
| /// Get the web log ID for the current request | ||||
| let webLogId ctx = (WebLogCache.get ctx).id | ||||
| 
 | ||||
| open System.Security.Claims | ||||
| 
 | ||||
| /// Get the user ID for the current request | ||||
| @ -159,7 +157,7 @@ let templatesForTheme ctx (typ : string) = | ||||
|     seq { | ||||
|         KeyValuePair.Create ("", $"- Default (single-{typ}) -") | ||||
|         yield! | ||||
|             Path.Combine ("themes", (WebLogCache.get ctx).themePath) | ||||
|             Path.Combine ("themes", (webLog ctx).themePath) | ||||
|             |> Directory.EnumerateFiles | ||||
|             |> Seq.filter (fun it -> it.EndsWith $"{typ}.liquid") | ||||
|             |> Seq.map (fun it -> | ||||
|  | ||||
| @ -2,17 +2,15 @@ | ||||
| module MyWebLog.Handlers.Post | ||||
| 
 | ||||
| open System | ||||
| open Giraffe | ||||
| open Microsoft.AspNetCore.Http | ||||
| 
 | ||||
| /// Split the "rest" capture for categories and tags into the page number and category/tag URL parts | ||||
| let private pathAndPageNumber (ctx : HttpContext) = | ||||
|     let slugs     = (string ctx.Request.RouteValues["slug"]).Split "/" |> Array.filter (fun it -> it <> "") | ||||
| /// Parse a slug and page number from an "everything else" URL | ||||
| let private parseSlugAndPage (slugAndPage : string seq) = | ||||
|     let slugs   = (slugAndPage |> Seq.skip 1 |> Seq.head).Split "/" |> Array.filter (fun it -> it <> "") | ||||
|     let pageIdx = Array.IndexOf (slugs, "page") | ||||
|     let pageNbr = | ||||
|         match pageIdx with | ||||
|         | -1 -> Some 1L | ||||
|         | idx when idx + 2 = slugs.Length -> Some (int64 slugs[pageIdx + 1]) | ||||
|         | -1 -> Some 1 | ||||
|         | idx when idx + 2 = slugs.Length -> Some (int slugs[pageIdx + 1]) | ||||
|         | _ -> None | ||||
|     let slugParts = if pageIdx > 0 then Array.truncate pageIdx slugs else slugs | ||||
|     pageNbr, String.Join ("/", slugParts) | ||||
| @ -47,9 +45,10 @@ open DotLiquid | ||||
| open MyWebLog.ViewModels | ||||
| 
 | ||||
| /// Convert a list of posts into items ready to be displayed | ||||
| let private preparePostList webLog posts listType url pageNbr perPage ctx conn = task { | ||||
| let private preparePostList webLog posts listType (url : string) pageNbr perPage ctx conn = task { | ||||
|     let! authors     = getAuthors     webLog posts conn | ||||
|     let! tagMappings = getTagMappings webLog posts conn | ||||
|     let  relUrl it   = Some <| WebLog.relativeUrl webLog (Permalink it) | ||||
|     let  postItems   = | ||||
|         posts | ||||
|         |> Seq.ofList | ||||
| @ -66,23 +65,23 @@ let private preparePostList webLog posts listType url pageNbr perPage ctx conn = | ||||
|     let newerLink = | ||||
|         match listType, pageNbr with | ||||
|         | SinglePost,   _ -> newerPost |> Option.map (fun p -> Permalink.toString p.permalink) | ||||
|         | _,            1L -> None | ||||
|         | PostList,     2L    when webLog.defaultPage = "posts" -> Some "" | ||||
|         | PostList,     _  -> Some $"page/{pageNbr - 1L}" | ||||
|         | CategoryList, 2L -> Some $"category/{url}/" | ||||
|         | CategoryList, _  -> Some $"category/{url}/page/{pageNbr - 1L}" | ||||
|         | TagList,      2L -> Some $"tag/{url}/" | ||||
|         | TagList,      _  -> Some $"tag/{url}/page/{pageNbr - 1L}" | ||||
|         | AdminList,    2L -> Some "admin/posts" | ||||
|         | AdminList,    _  -> Some $"admin/posts/page/{pageNbr - 1L}" | ||||
|         | _,            1 -> None | ||||
|         | PostList,     2    when webLog.defaultPage = "posts" -> Some "" | ||||
|         | PostList,     _ -> relUrl $"page/{pageNbr - 1}" | ||||
|         | CategoryList, 2 -> relUrl $"category/{url}/" | ||||
|         | CategoryList, _ -> relUrl $"category/{url}/page/{pageNbr - 1}" | ||||
|         | TagList,      2 -> relUrl $"tag/{url}/" | ||||
|         | TagList,      _ -> relUrl $"tag/{url}/page/{pageNbr - 1}" | ||||
|         | AdminList,    2 -> relUrl "admin/posts" | ||||
|         | AdminList,    _ -> relUrl $"admin/posts/page/{pageNbr - 1}" | ||||
|     let olderLink = | ||||
|         match listType, List.length posts > perPage with | ||||
|         | SinglePost,   _     -> olderPost |> Option.map (fun p -> Permalink.toString p.permalink) | ||||
|         | _,            false -> None | ||||
|         | PostList,     true  -> Some $"page/{pageNbr + 1L}" | ||||
|         | CategoryList, true  -> Some $"category/{url}/page/{pageNbr + 1L}" | ||||
|         | TagList,      true  -> Some $"tag/{url}/page/{pageNbr + 1L}" | ||||
|         | AdminList,    true  -> Some $"admin/posts/page/{pageNbr + 1L}" | ||||
|         | PostList,     true  -> relUrl $"page/{pageNbr + 1}" | ||||
|         | CategoryList, true  -> relUrl $"category/{url}/page/{pageNbr + 1}" | ||||
|         | TagList,      true  -> relUrl $"tag/{url}/page/{pageNbr + 1}" | ||||
|         | AdminList,    true  -> relUrl $"admin/posts/page/{pageNbr + 1}" | ||||
|     let model = | ||||
|         { posts      = postItems | ||||
|           authors    = authors | ||||
| @ -95,28 +94,30 @@ let private preparePostList webLog posts listType url pageNbr perPage ctx conn = | ||||
|     return Hash.FromAnonymousObject {| model = model; categories = CategoryCache.get ctx; tag_mappings = tagMappings |} | ||||
| } | ||||
| 
 | ||||
| open Giraffe | ||||
| 
 | ||||
| // GET /page/{pageNbr} | ||||
| let pageOfPosts pageNbr : HttpHandler = fun next ctx -> task { | ||||
|     let  webLog = WebLogCache.get ctx | ||||
|     let  webLog = webLog ctx | ||||
|     let  conn   = conn   ctx | ||||
|     let! posts  = Data.Post.findPageOfPublishedPosts webLog.id pageNbr webLog.postsPerPage conn | ||||
|     let! hash   = preparePostList webLog posts PostList "" pageNbr webLog.postsPerPage ctx conn | ||||
|     let  title  = | ||||
|         match pageNbr, webLog.defaultPage with | ||||
|         | 1L, "posts" -> None | ||||
|         | 1, "posts" -> None | ||||
|         | _, "posts" -> Some $"Page {pageNbr}" | ||||
|         | _,  _      -> Some $"Page {pageNbr} « Posts" | ||||
|     match title with Some ttl -> hash.Add ("page_title", ttl) | None -> () | ||||
|     if pageNbr = 1L && webLog.defaultPage = "posts" then hash.Add ("is_home", true) | ||||
|     if pageNbr = 1 && webLog.defaultPage = "posts" then hash.Add ("is_home", true) | ||||
|     return! themedView "index" next ctx hash | ||||
| } | ||||
| 
 | ||||
| // GET /category/{slug}/ | ||||
| // GET /category/{slug}/page/{pageNbr} | ||||
| let pageOfCategorizedPosts : HttpHandler = fun next ctx -> task { | ||||
|     let  webLog = WebLogCache.get ctx | ||||
| let pageOfCategorizedPosts slugAndPage : HttpHandler = fun next ctx -> task { | ||||
|     let webLog = webLog ctx | ||||
|     let conn   = conn   ctx | ||||
|     match pathAndPageNumber ctx with | ||||
|     match parseSlugAndPage slugAndPage with | ||||
|     | Some pageNbr, slug ->  | ||||
|         let allCats = CategoryCache.get ctx | ||||
|         let cat     = allCats |> Array.find (fun cat -> cat.slug = slug) | ||||
| @ -130,7 +131,7 @@ let pageOfCategorizedPosts : HttpHandler = fun next ctx -> task { | ||||
|         match! Data.Post.findPageOfCategorizedPosts webLog.id catIds pageNbr webLog.postsPerPage conn with | ||||
|         | posts when List.length posts > 0 -> | ||||
|             let! hash    = preparePostList webLog posts CategoryList cat.slug pageNbr webLog.postsPerPage ctx conn | ||||
|             let  pgTitle = if pageNbr = 1L then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>""" | ||||
|             let  pgTitle = if pageNbr = 1 then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>""" | ||||
|             hash.Add ("page_title", $"{cat.name}: Category Archive{pgTitle}") | ||||
|             hash.Add ("subtitle", cat.description.Value) | ||||
|             hash.Add ("is_category", true) | ||||
| @ -143,10 +144,10 @@ open System.Web | ||||
| 
 | ||||
| // GET /tag/{tag}/ | ||||
| // GET /tag/{tag}/page/{pageNbr} | ||||
| let pageOfTaggedPosts : HttpHandler = fun next ctx -> task { | ||||
|     let webLog = WebLogCache.get ctx | ||||
| let pageOfTaggedPosts slugAndPage : HttpHandler = fun next ctx -> task { | ||||
|     let webLog = webLog ctx | ||||
|     let conn   = conn   ctx | ||||
|     match pathAndPageNumber ctx with | ||||
|     match parseSlugAndPage slugAndPage with | ||||
|     | Some pageNbr, rawTag ->  | ||||
|         let  urlTag = HttpUtility.UrlDecode rawTag | ||||
|         let! tag    = backgroundTask { | ||||
| @ -157,7 +158,7 @@ let pageOfTaggedPosts : HttpHandler = fun next ctx -> task { | ||||
|         match! Data.Post.findPageOfTaggedPosts webLog.id tag pageNbr webLog.postsPerPage conn with | ||||
|         | posts when List.length posts > 0 -> | ||||
|             let! hash    = preparePostList webLog posts TagList rawTag pageNbr webLog.postsPerPage ctx conn | ||||
|             let  pgTitle = if pageNbr = 1L then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>""" | ||||
|             let  pgTitle = if pageNbr = 1 then "" else $""" <small class="archive-pg-nbr">(Page {pageNbr})</small>""" | ||||
|             hash.Add ("page_title", $"Posts Tagged “{tag}”{pgTitle}") | ||||
|             hash.Add ("is_tag", true) | ||||
|             return! themedView "index" next ctx hash | ||||
| @ -166,15 +167,18 @@ let pageOfTaggedPosts : HttpHandler = fun next ctx -> task { | ||||
|             let spacedTag = tag.Replace ("-", " ") | ||||
|             match! Data.Post.findPageOfTaggedPosts webLog.id spacedTag pageNbr 1 conn with | ||||
|             | posts when List.length posts > 0 -> | ||||
|                 let endUrl = if pageNbr = 1L then "" else $"page/{pageNbr}" | ||||
|                 return! redirectTo true $"""/tag/{spacedTag.Replace (" ", "+")}/{endUrl}""" next ctx | ||||
|                 let endUrl = if pageNbr = 1 then "" else $"page/{pageNbr}" | ||||
|                 return! | ||||
|                     redirectTo true | ||||
|                         (WebLog.relativeUrl webLog (Permalink $"""tag/{spacedTag.Replace (" ", "+")}/{endUrl}""")) | ||||
|                         next ctx | ||||
|             | _ -> return! Error.notFound next ctx | ||||
|     | None, _ -> return! Error.notFound next ctx | ||||
| } | ||||
| 
 | ||||
| // GET / | ||||
| let home : HttpHandler = fun next ctx -> task { | ||||
|     let webLog = WebLogCache.get ctx | ||||
|     let webLog = webLog ctx | ||||
|     match webLog.defaultPage with | ||||
|     | "posts" -> return! pageOfPosts 1 next ctx | ||||
|     | pageId -> | ||||
| @ -190,6 +194,7 @@ let home : HttpHandler = fun next ctx -> task { | ||||
|         | None -> return! Error.notFound next ctx | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| open System.IO | ||||
| open System.ServiceModel.Syndication | ||||
| open System.Text.RegularExpressions | ||||
| @ -199,11 +204,11 @@ open System.Xml | ||||
| //   (Routing handled by catch-all handler for future configurability) | ||||
| let generateFeed : HttpHandler = fun next ctx -> backgroundTask { | ||||
|     let  conn    = conn   ctx | ||||
|     let  webLog  = WebLogCache.get ctx | ||||
|     let  urlBase = $"https://{webLog.urlBase}/" | ||||
|     let  webLog  = webLog ctx | ||||
|     // TODO: hard-coded number of items | ||||
|     let! posts   = Data.Post.findPageOfPublishedPosts webLog.id 1L 10 conn | ||||
|     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) = | ||||
| @ -213,25 +218,29 @@ let generateFeed : HttpHandler = fun next ctx -> backgroundTask { | ||||
|             | txt when txt.Length < 255 -> txt | ||||
|             | txt -> $"{txt.Substring (0, 252)}..." | ||||
|         let item = SyndicationItem ( | ||||
|             Id              = $"{urlBase}{Permalink.toString post.permalink}", | ||||
|             Id              = WebLog.absoluteUrl webLog post.permalink, | ||||
|             Title           = TextSyndicationContent.CreateHtmlContent post.title, | ||||
|             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=\"{urlBase}").Replace ("href=\"/", $"href=\"{urlBase}") | ||||
|         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, $"{urlBase}category/{cat.slug}/", cat.name)) | ||||
|               SyndicationCategory (cat.name, WebLog.absoluteUrl webLog (Permalink $"category/{cat.slug}/"), cat.name)) | ||||
|           post.tags | ||||
|           |> List.map (fun tag -> | ||||
|               let urlTag = tag.Replace (" ", "+") | ||||
|               SyndicationCategory (tag, $"{urlBase}tag/{urlTag}/", $"{tag} (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 | ||||
| @ -245,12 +254,12 @@ let generateFeed : HttpHandler = fun next ctx -> backgroundTask { | ||||
|     feed.Generator       <- generator ctx | ||||
|     feed.Items           <- posts |> Seq.ofList |> Seq.map toItem | ||||
|     feed.Language        <- "en" | ||||
|     feed.Id              <- urlBase | ||||
|     feed.Id              <- webLog.urlBase | ||||
|      | ||||
|     feed.Links.Add (SyndicationLink (Uri $"{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 | ||||
|         (XmlQualifiedName ("content", "http://www.w3.org/2000/xmlns/"), "http://purl.org/rss/1.0/modules/content/") | ||||
|     feed.ElementExtensions.Add ("link", "", urlBase) | ||||
|     feed.ElementExtensions.Add ("link", "", webLog.urlBase) | ||||
|      | ||||
|     use mem = new MemoryStream () | ||||
|     use xml = XmlWriter.Create mem | ||||
| @ -266,12 +275,15 @@ let generateFeed : HttpHandler = fun next ctx -> backgroundTask { | ||||
| 
 | ||||
| /// Sequence where the first returned value is the proper handler for the link | ||||
| let private deriveAction ctx : HttpHandler seq = | ||||
|     let webLog    = WebLogCache.get ctx | ||||
|     let webLog    = webLog ctx | ||||
|     let conn      = conn   ctx | ||||
|     let textLink  = string ctx.Request.RouteValues["link"] | ||||
|     let permalink = Permalink textLink | ||||
|     let _, extra  = WebLog.hostAndPath webLog | ||||
|     let textLink  = if extra = "" then ctx.Request.Path.Value else ctx.Request.Path.Value.Substring extra.Length | ||||
|     let await it  = (Async.AwaitTask >> Async.RunSynchronously) it | ||||
|     seq { | ||||
|         // Home page directory without the directory slash  | ||||
|         if textLink = "" then yield redirectTo true (WebLog.relativeUrl webLog Permalink.empty) | ||||
|         let permalink = Permalink (textLink.Substring 1) | ||||
|         // Current post | ||||
|         match Data.Post.findByPermalink permalink webLog.id conn |> await with | ||||
|         | Some post -> | ||||
| @ -288,27 +300,27 @@ let private deriveAction ctx : HttpHandler seq = | ||||
|         | None -> () | ||||
|         // RSS feed | ||||
|         // TODO: configure this via web log | ||||
|         if textLink = "feed.xml" then yield generateFeed | ||||
|         if textLink = "/feed.xml" then yield generateFeed | ||||
|         // Post differing only by trailing slash | ||||
|         let altLink = Permalink (if textLink.EndsWith "/" then textLink[..textLink.Length - 2] else $"{textLink}/") | ||||
|         match Data.Post.findByPermalink altLink webLog.id conn |> await with | ||||
|         | Some post -> yield redirectTo true $"/{Permalink.toString post.permalink}" | ||||
|         | Some post -> yield redirectTo true (WebLog.relativeUrl webLog post.permalink) | ||||
|         | None -> () | ||||
|         // Page differing only by trailing slash | ||||
|         match Data.Page.findByPermalink altLink webLog.id conn |> await with | ||||
|         | Some page -> yield redirectTo true $"/{Permalink.toString page.permalink}" | ||||
|         | Some page -> yield redirectTo true (WebLog.relativeUrl webLog page.permalink) | ||||
|         | None -> () | ||||
|         // Prior post | ||||
|         match Data.Post.findCurrentPermalink [ permalink; altLink ] webLog.id conn |> await with | ||||
|         | Some link -> yield redirectTo true $"/{Permalink.toString link}" | ||||
|         | Some link -> yield redirectTo true (WebLog.relativeUrl webLog link) | ||||
|         | None -> () | ||||
|         // Prior page | ||||
|         match Data.Page.findCurrentPermalink [ permalink; altLink ] webLog.id conn |> await with | ||||
|         | Some link -> yield redirectTo true $"/{Permalink.toString link}" | ||||
|         | Some link -> yield redirectTo true (WebLog.relativeUrl webLog link) | ||||
|         | None -> () | ||||
|     } | ||||
| 
 | ||||
| // GET {**link} | ||||
| // GET {all-of-the-above} | ||||
| let catchAll : HttpHandler = fun next ctx -> task { | ||||
|     match deriveAction ctx |> Seq.tryHead with | ||||
|     | Some handler -> return! handler next ctx | ||||
| @ -318,7 +330,7 @@ let catchAll : HttpHandler = fun next ctx -> task { | ||||
| // GET /admin/posts | ||||
| // GET /admin/posts/page/{pageNbr} | ||||
| let all pageNbr : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
|     let  webLog = WebLogCache.get ctx | ||||
|     let  webLog = webLog ctx | ||||
|     let  conn   = conn   ctx | ||||
|     let! posts  = Data.Post.findPageOfPosts webLog.id pageNbr 25 conn | ||||
|     let! hash   = preparePostList webLog posts AdminList "" pageNbr 25 ctx conn | ||||
| @ -328,7 +340,7 @@ let all pageNbr : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
| 
 | ||||
| // GET /admin/post/{id}/edit | ||||
| let edit postId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
|     let  webLog = WebLogCache.get ctx | ||||
|     let  webLog = webLog ctx | ||||
|     let  conn   = conn   ctx | ||||
|     let! result = task { | ||||
|         match postId with | ||||
| @ -354,7 +366,7 @@ let edit postId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
| 
 | ||||
| // GET /admin/post/{id}/permalinks | ||||
| let editPermalinks postId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
|     match! Data.Post.findByFullId (PostId postId) (webLogId ctx) (conn ctx) with | ||||
|     match! Data.Post.findByFullId (PostId postId) (webLog ctx).id (conn ctx) with | ||||
|     | Some post -> | ||||
|         return! | ||||
|             Hash.FromAnonymousObject {| | ||||
| @ -368,21 +380,23 @@ let editPermalinks postId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||
| 
 | ||||
| // POST /admin/post/permalinks | ||||
| let savePermalinks : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||
|     let  webLog = webLog ctx | ||||
|     let! model  = ctx.BindFormAsync<ManagePermalinksModel> () | ||||
|     let  links  = model.prior |> Array.map Permalink |> List.ofArray | ||||
|     match! Data.Post.updatePriorPermalinks (PostId model.id) (webLogId ctx) links (conn ctx) with | ||||
|     match! Data.Post.updatePriorPermalinks (PostId model.id) webLog.id links (conn ctx) with | ||||
|     | true -> | ||||
|         do! addMessage ctx { UserMessage.success with message = "Post permalinks saved successfully" } | ||||
|         return! redirectToGet $"/admin/post/{model.id}/permalinks" next ctx | ||||
|         return! redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/post/{model.id}/permalinks")) next ctx | ||||
|     | false -> return! Error.notFound next ctx | ||||
| } | ||||
| 
 | ||||
| // POST /admin/post/{id}/delete | ||||
| let delete postId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||
|     match! Data.Post.delete (PostId postId) (webLogId ctx) (conn ctx) with | ||||
|     let webLog = webLog ctx | ||||
|     match! Data.Post.delete (PostId postId) webLog.id (conn ctx) with | ||||
|     | true  -> do! addMessage ctx { UserMessage.success with message = "Post deleted successfully" } | ||||
|     | false -> do! addMessage ctx { UserMessage.error with message = "Post not found; nothing deleted" } | ||||
|     return! redirectToGet "/admin/posts" next ctx | ||||
|     return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin/posts")) next ctx | ||||
| } | ||||
| 
 | ||||
| #nowarn "3511" | ||||
| @ -390,7 +404,7 @@ let delete postId : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx | ||||
| // POST /admin/post/save | ||||
| let save : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||
|     let! model  = ctx.BindFormAsync<EditPostModel> () | ||||
|     let  webLogId = webLogId ctx | ||||
|     let  webLog = webLog ctx | ||||
|     let  conn   = conn   ctx | ||||
|     let  now    = DateTime.UtcNow | ||||
|     let! pst    = task { | ||||
| @ -399,10 +413,10 @@ let save : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||
|             return Some | ||||
|                 { Post.empty with | ||||
|                     id        = PostId.create () | ||||
|                     webLogId  = webLogId | ||||
|                     webLogId  = webLog.id | ||||
|                     authorId  = userId ctx | ||||
|                 } | ||||
|         | postId -> return! Data.Post.findByFullId (PostId postId) webLogId conn | ||||
|         | postId -> return! Data.Post.findByFullId (PostId postId) webLog.id conn | ||||
|     } | ||||
|     match pst with | ||||
|     | Some post -> | ||||
| @ -460,6 +474,7 @@ let save : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||
|                    |> List.length = List.length pst.Value.categoryIds) then | ||||
|             do! CategoryCache.update ctx | ||||
|         do! addMessage ctx { UserMessage.success with message = "Post saved successfully" } | ||||
|         return! redirectToGet $"/admin/post/{PostId.toString post.id}/edit" next ctx | ||||
|         return! | ||||
|             redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/post/{PostId.toString post.id}/edit")) next ctx | ||||
|     | None -> return! Error.notFound next ctx | ||||
| } | ||||
|  | ||||
| @ -1,75 +1,89 @@ | ||||
| /// Routes for this application | ||||
| module MyWebLog.Handlers.Routes | ||||
| 
 | ||||
| open Giraffe.EndpointRouting | ||||
| open Giraffe | ||||
| open MyWebLog | ||||
| 
 | ||||
| /// The endpoints defined in the above handlers | ||||
| let endpoints = [ | ||||
|     GET [ | ||||
|         route "/" Post.home | ||||
| let router : HttpHandler = choose [ | ||||
|     GET >=> choose [ | ||||
|         route "/" >=> Post.home | ||||
|     ] | ||||
|     subRoute "/admin" [ | ||||
|         GET [ | ||||
|             route    "" Admin.dashboard | ||||
|             subRoute "/categor" [ | ||||
|                 route  "ies"       Admin.listCategories | ||||
|     subRoute "/admin" (requireUser >=> choose [ | ||||
|         GET >=> choose [ | ||||
|             route    "" >=> Admin.dashboard | ||||
|             subRoute "/categor" (choose [ | ||||
|                 route  "ies"       >=> Admin.listCategories | ||||
|                 routef "y/%s/edit"     Admin.editCategory | ||||
|             ] | ||||
|             subRoute "/page" [ | ||||
|                 route  "s"              (Admin.listPages 1) | ||||
|                 routef "s/page/%d"      Admin.listPages | ||||
|             ]) | ||||
|             subRoute "/page" (choose [ | ||||
|                 route  "s"              >=> Admin.listPages 1 | ||||
|                 routef "s/page/%i"          Admin.listPages | ||||
|                 routef "/%s/edit"           Admin.editPage | ||||
|                 routef "/%s/permalinks"     Admin.editPagePermalinks | ||||
|             ] | ||||
|             subRoute "/post" [ | ||||
|                 route  "s"              (Post.all 1) | ||||
|                 routef "s/page/%d"      Post.all | ||||
|             ]) | ||||
|             subRoute "/post" (choose [ | ||||
|                 route  "s"              >=> Post.all 1 | ||||
|                 routef "s/page/%i"          Post.all | ||||
|                 routef "/%s/edit"           Post.edit | ||||
|                 routef "/%s/permalinks"     Post.editPermalinks | ||||
|             ] | ||||
|             route    "/settings" Admin.settings | ||||
|             subRoute "/tag-mapping" [ | ||||
|                 route  "s"        Admin.tagMappings | ||||
|             ]) | ||||
|             route    "/settings" >=> Admin.settings | ||||
|             subRoute "/tag-mapping" (choose [ | ||||
|                 route  "s"        >=> Admin.tagMappings | ||||
|                 routef "/%s/edit"     Admin.editMapping | ||||
|             ]) | ||||
|             route    "/user/edit" >=> User.edit | ||||
|         ] | ||||
|             route    "/user/edit" User.edit | ||||
|         ] | ||||
|         POST [ | ||||
|             subRoute "/category" [ | ||||
|                 route  "/save"      Admin.saveCategory | ||||
|         POST >=> choose [ | ||||
|             subRoute "/category" (choose [ | ||||
|                 route  "/save"      >=> Admin.saveCategory | ||||
|                 routef "/%s/delete"     Admin.deleteCategory | ||||
|             ] | ||||
|             subRoute "/page" [ | ||||
|                 route  "/save"       Admin.savePage | ||||
|                 route  "/permalinks" Admin.savePagePermalinks | ||||
|             ]) | ||||
|             subRoute "/page" (choose [ | ||||
|                 route  "/save"       >=> Admin.savePage | ||||
|                 route  "/permalinks" >=> Admin.savePagePermalinks | ||||
|                 routef "/%s/delete"      Admin.deletePage | ||||
|             ] | ||||
|             subRoute "/post" [ | ||||
|                 route  "/save"       Post.save | ||||
|                 route  "/permalinks" Post.savePermalinks | ||||
|             ]) | ||||
|             subRoute "/post" (choose [ | ||||
|                 route  "/save"       >=> Post.save | ||||
|                 route  "/permalinks" >=> Post.savePermalinks | ||||
|                 routef "/%s/delete"      Post.delete | ||||
|             ] | ||||
|             route    "/settings" Admin.saveSettings | ||||
|             subRoute "/tag-mapping" [ | ||||
|                 route  "/save"      Admin.saveMapping | ||||
|             ]) | ||||
|             route    "/settings" >=> Admin.saveSettings | ||||
|             subRoute "/tag-mapping" (choose [ | ||||
|                 route  "/save"      >=> Admin.saveMapping | ||||
|                 routef "/%s/delete"     Admin.deleteMapping | ||||
|             ]) | ||||
|             route    "/user/save" >=> User.save | ||||
|         ] | ||||
|             route    "/user/save" User.save | ||||
|     ]) | ||||
|     GET >=> routexp "/category/(.*)"  Post.pageOfCategorizedPosts | ||||
|     GET >=> routef  "/page/%i"        Post.pageOfPosts | ||||
|     GET >=> routexp "/tag/(.*)"       Post.pageOfTaggedPosts | ||||
|     subRoute "/user" (choose [ | ||||
|         GET >=> choose [ | ||||
|             route "/log-on"  >=> User.logOn None | ||||
|             route "/log-off" >=> User.logOff | ||||
|         ] | ||||
|         POST >=> choose [ | ||||
|             route "/log-on" >=> User.doLogOn | ||||
|         ] | ||||
|     GET [ | ||||
|         route  "/category/{**slug}" Post.pageOfCategorizedPosts | ||||
|         routef "/page/%d"           Post.pageOfPosts | ||||
|         route  "/tag/{**slug}"      Post.pageOfTaggedPosts | ||||
|     ] | ||||
|     subRoute "/user" [ | ||||
|         GET [ | ||||
|             route "/log-on"  (User.logOn None) | ||||
|             route "/log-off" User.logOff | ||||
|         ] | ||||
|         POST [ | ||||
|             route "/log-on" User.doLogOn | ||||
|         ] | ||||
|     ] | ||||
|     route "{**link}" Post.catchAll | ||||
|     ]) | ||||
|     GET >=> Post.catchAll | ||||
|     Error.notFound | ||||
| ] | ||||
| 
 | ||||
| /// Wrap a router in a sub-route | ||||
| let routerWithPath extraPath : HttpHandler = | ||||
|     subRoute extraPath router | ||||
| 
 | ||||
| /// Handler to apply Giraffe routing with a possible sub-route | ||||
| let handleRoute : HttpHandler = fun next ctx -> task { | ||||
|     let _, extraPath = WebLog.hostAndPath (webLog ctx) | ||||
|     return! (if extraPath = "" then router else routerWithPath extraPath) next ctx | ||||
| } | ||||
| 
 | ||||
| open Giraffe.EndpointRouting | ||||
| 
 | ||||
| /// Endpoint-routed handler to deal with sub-routes | ||||
| let endpoint = [ route "{**url}" handleRoute ] | ||||
|  | ||||
| @ -41,7 +41,7 @@ open MyWebLog | ||||
| // POST /user/log-on | ||||
| let doLogOn : HttpHandler = validateCsrf >=> fun next ctx -> task { | ||||
|     let! model  = ctx.BindFormAsync<LogOnModel> () | ||||
|     let  webLog = WebLogCache.get ctx | ||||
|     let  webLog = webLog ctx | ||||
|     match! Data.WebLogUser.findByEmail model.emailAddress webLog.id (conn ctx) with  | ||||
|     | Some user when user.passwordHash = hashedPassword model.password user.userName user.salt -> | ||||
|         let claims = seq { | ||||
| @ -56,7 +56,7 @@ let doLogOn : HttpHandler = validateCsrf >=> fun next ctx -> task { | ||||
|             AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow)) | ||||
|         do! addMessage ctx | ||||
|                 { UserMessage.success with message = $"Logged on successfully | Welcome to {webLog.name}!" } | ||||
|         return! redirectToGet (match model.returnTo with Some url -> url | None -> "/admin") next ctx | ||||
|         return! redirectToGet (defaultArg model.returnTo (WebLog.relativeUrl webLog (Permalink "admin"))) next ctx | ||||
|     | _ -> | ||||
|         do! addMessage ctx { UserMessage.error with message = "Log on attempt unsuccessful" } | ||||
|         return! logOn model.returnTo next ctx | ||||
| @ -66,7 +66,7 @@ let doLogOn : HttpHandler = validateCsrf >=> fun next ctx -> task { | ||||
| let logOff : HttpHandler = fun next ctx -> task { | ||||
|     do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme | ||||
|     do! addMessage ctx { UserMessage.info with message = "Log off successful" } | ||||
|     return! redirectToGet "/" next ctx | ||||
|     return! redirectToGet (WebLog.relativeUrl (webLog ctx) Permalink.empty) next ctx | ||||
| } | ||||
| 
 | ||||
| /// Display the user edit page, with information possibly filled in | ||||
| @ -107,7 +107,7 @@ let save : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||
|             do! Data.WebLogUser.update user conn | ||||
|             let pwMsg = if model.newPassword = "" then "" else " and updated your password" | ||||
|             do! addMessage ctx { UserMessage.success with message = $"Saved your information{pwMsg} successfully" } | ||||
|             return! redirectToGet "/admin/user/edit" next ctx | ||||
|             return! redirectToGet (WebLog.relativeUrl (webLog ctx) (Permalink "admin/user/edit")) next ctx | ||||
|         | None -> return! Error.notFound next ctx | ||||
|     else | ||||
|         do! addMessage ctx { UserMessage.error with message = "Passwords did not match; no updates made" } | ||||
|  | ||||
| @ -15,6 +15,7 @@ | ||||
|     <Compile Include="Handlers\Post.fs" /> | ||||
|     <Compile Include="Handlers\User.fs" /> | ||||
|     <Compile Include="Handlers\Routes.fs" /> | ||||
|     <Compile Include="DotLiquidBespoke.fs" /> | ||||
|     <Compile Include="Program.fs" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|  | ||||
| @ -1,100 +1,23 @@ | ||||
| open System.Collections.Generic | ||||
| open Microsoft.AspNetCore.Http | ||||
| open Microsoft.Extensions.DependencyInjection | ||||
| open Microsoft.AspNetCore.Http | ||||
| open MyWebLog | ||||
| open RethinkDb.Driver.Net | ||||
| open System | ||||
| 
 | ||||
| /// Middleware to derive the current web log | ||||
| type WebLogMiddleware (next : RequestDelegate) = | ||||
| 
 | ||||
|     member this.InvokeAsync (ctx : HttpContext) = task { | ||||
|         match WebLogCache.exists ctx with | ||||
|         | true -> return! next.Invoke ctx | ||||
|         | false -> | ||||
|             let conn = ctx.RequestServices.GetRequiredService<IConnection> () | ||||
|             match! Data.WebLog.findByHost (Cache.makeKey ctx) conn with | ||||
|             | Some webLog -> | ||||
|                 WebLogCache.set ctx webLog | ||||
|                 do! PageListCache.update ctx | ||||
|                 do! CategoryCache.update ctx | ||||
|         if WebLogCache.exists ctx then | ||||
|             ctx.Items["webLog"] <- WebLogCache.get ctx | ||||
|             if PageListCache.exists ctx then () else do! PageListCache.update ctx | ||||
|             if CategoryCache.exists ctx then () else do! CategoryCache.update ctx | ||||
|             return! next.Invoke ctx | ||||
|             | None -> ctx.Response.StatusCode <- 404 | ||||
|         else | ||||
|             ctx.Response.StatusCode <- 404 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| /// DotLiquid filters | ||||
| module DotLiquidBespoke = | ||||
|      | ||||
|     open System.IO | ||||
|     open DotLiquid | ||||
|     open MyWebLog.ViewModels | ||||
| 
 | ||||
|     /// A filter to generate a link with posts categorized under the given category | ||||
|     type CategoryLinkFilter () = | ||||
|         static member CategoryLink (_ : Context, catObj : obj) = | ||||
|             match catObj with | ||||
|             | :? DisplayCategory as cat -> $"/category/{cat.slug}/" | ||||
|             | :? DropProxy as proxy -> $"""/category/{proxy["slug"]}/""" | ||||
|             | _ -> $"alert('unknown category object type {catObj.GetType().Name}')" | ||||
|      | ||||
|     /// A filter to generate a link that will edit a page | ||||
|     type EditPageLinkFilter () = | ||||
|         static member EditPageLink (_ : Context, postId : string) = | ||||
|             $"/admin/page/{postId}/edit" | ||||
|          | ||||
|     /// A filter to generate a link that will edit a post | ||||
|     type EditPostLinkFilter () = | ||||
|         static member EditPostLink (_ : Context, postId : string) = | ||||
|             $"/admin/post/{postId}/edit" | ||||
|          | ||||
|     /// A filter to generate nav links, highlighting the active link (exact match) | ||||
|     type NavLinkFilter () = | ||||
|         static member NavLink (ctx : Context, url : string, text : string) = | ||||
|             seq { | ||||
|                 "<li class=\"nav-item\"><a class=\"nav-link" | ||||
|                 if url = string ctx.Environments[0].["current_page"] then " active" | ||||
|                 "\" href=\"/" | ||||
|                 url | ||||
|                 "\">" | ||||
|                 text | ||||
|                 "</a></li>" | ||||
|             } | ||||
|             |> Seq.fold (+) "" | ||||
|      | ||||
|     /// A filter to generate a link with posts tagged with the given tag | ||||
|     type TagLinkFilter () = | ||||
|         static member TagLink (ctx : Context, tag : string) = | ||||
|             match ctx.Environments[0].["tag_mappings"] :?> TagMap list | ||||
|                   |> List.tryFind (fun it -> it.tag = tag) with | ||||
|             | Some tagMap -> $"/tag/{tagMap.urlValue}/" | ||||
|             | None -> $"""/tag/{tag.Replace (" ", "+")}/""" | ||||
|                  | ||||
|     /// Create links for a user to log on or off, and a dashboard link if they are logged off | ||||
|     type UserLinksTag () = | ||||
|         inherit Tag () | ||||
|          | ||||
|         override this.Render (context : Context, result : TextWriter) = | ||||
|             seq { | ||||
|                 """<ul class="navbar-nav flex-grow-1 justify-content-end">""" | ||||
|                 match Convert.ToBoolean context.Environments[0].["logged_on"] with | ||||
|                 | true -> | ||||
|                     """<li class="nav-item"><a class="nav-link" href="/admin">Dashboard</a></li>""" | ||||
|                     """<li class="nav-item"><a class="nav-link" href="/user/log-off">Log Off</a></li>""" | ||||
|                 | false -> | ||||
|                     """<li class="nav-item"><a class="nav-link" href="/user/log-on">Log On</a></li>""" | ||||
|                 "</ul>" | ||||
|             } | ||||
|             |> Seq.iter result.WriteLine | ||||
|      | ||||
|     /// A filter to retrieve the value of a meta item from a list | ||||
|     //    (shorter than `{% assign item = list | where: "name", [name] | first %}{{ item.value }}`) | ||||
|     type ValueFilter () = | ||||
|         static member Value (_ : Context, items : MetaItem list, name : string) = | ||||
|             match items |> List.tryFind (fun it -> it.name = name) with | ||||
|             | Some item -> item.value | ||||
|             | None -> $"-- {name} not found --" | ||||
| 
 | ||||
| open System | ||||
| open Microsoft.Extensions.DependencyInjection | ||||
| open RethinkDb.Driver.Net | ||||
| 
 | ||||
| /// Create the default information for a new web log | ||||
| module NewWebLog = | ||||
| @ -220,7 +143,9 @@ module NewWebLog = | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| open System.Collections.Generic | ||||
| open DotLiquid | ||||
| open DotLiquidBespoke | ||||
| open Giraffe | ||||
| open Giraffe.EndpointRouting | ||||
| open Microsoft.AspNetCore.Antiforgery | ||||
| @ -257,6 +182,7 @@ let main args = | ||||
|         task { | ||||
|             let! conn = rethinkCfg.CreateConnectionAsync () | ||||
|             do! Data.Startup.ensureDb rethinkCfg (loggerFac.CreateLogger (nameof Data.Startup)) conn | ||||
|             do! WebLogCache.fill conn | ||||
|             return conn | ||||
|         } |> Async.AwaitTask |> Async.RunSynchronously | ||||
|     let _ = builder.Services.AddSingleton<IConnection> conn | ||||
| @ -273,13 +199,12 @@ let main args = | ||||
|     let _ = builder.Services.AddGiraffe () | ||||
|      | ||||
|     // Set up DotLiquid | ||||
|     [ typeof<DotLiquidBespoke.CategoryLinkFilter>; typeof<DotLiquidBespoke.EditPageLinkFilter> | ||||
|       typeof<DotLiquidBespoke.EditPostLinkFilter>; typeof<DotLiquidBespoke.NavLinkFilter> | ||||
|       typeof<DotLiquidBespoke.TagLinkFilter>;      typeof<DotLiquidBespoke.ValueFilter> | ||||
|     [ typeof<AbsoluteLinkFilter>; typeof<CategoryLinkFilter>; typeof<EditPageLinkFilter>; typeof<EditPostLinkFilter> | ||||
|       typeof<NavLinkFilter>;      typeof<RelativeLinkFilter>; typeof<TagLinkFilter>;      typeof<ValueFilter> | ||||
|     ] | ||||
|     |> List.iter Template.RegisterFilter | ||||
|      | ||||
|     Template.RegisterTag<DotLiquidBespoke.UserLinksTag> "user_links" | ||||
|     Template.RegisterTag<UserLinksTag> "user_links" | ||||
|      | ||||
|     [   // Domain types | ||||
|         typeof<MetaItem>; typeof<Page>; typeof<TagMap>; typeof<WebLog> | ||||
| @ -308,7 +233,7 @@ let main args = | ||||
|         let _ = app.UseStaticFiles () | ||||
|         let _ = app.UseRouting () | ||||
|         let _ = app.UseSession () | ||||
|         let _ = app.UseGiraffe Handlers.Routes.endpoints | ||||
|         let _ = app.UseGiraffe Handlers.Routes.endpoint | ||||
| 
 | ||||
|         app.Run() | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <h2 class="my-3">{{ page_title }}</h2> | ||||
| <article> | ||||
|   <form action="/admin/category/save" method="post"> | ||||
|   <form action="{{ "admin/category/save" | relative_link }}" method="post"> | ||||
|     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> | ||||
|     <input type="hidden" name="categoryId" value="{{ model.category_id }}"> | ||||
|     <div class="container"> | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <h2 class="my-3">{{ page_title }}</h2> | ||||
| <article class="container"> | ||||
|   <a href="/admin/category/new/edit" class="btn btn-primary btn-sm mb-3">Add a New Category</a> | ||||
|   <a href="{{ "admin/category/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3">Add a New Category</a> | ||||
|   <table class="table table-sm table-hover"> | ||||
|     <thead> | ||||
|       <tr> | ||||
| @ -23,10 +23,13 @@ | ||||
|                 </a> | ||||
|                 <span class="text-muted"> • </span> | ||||
|               {%- endif %} | ||||
|               <a href="/admin/category/{{ cat.id }}/edit">Edit</a> | ||||
|               {%- capture cat_edit %}admin/category/{{ cat.id }}/edit{% endcapture -%} | ||||
|               <a href="{{ cat_edit | relative_link }}">Edit</a> | ||||
|               <span class="text-muted"> • </span> | ||||
|               <a href="/admin/category/{{ cat.id }}/delete" class="text-danger" | ||||
|                  onclick="return Admin.deleteCategory('{{ cat.id }}', '{{ cat.name }}')"> | ||||
|               {%- capture cat_del %}admin/category/{{ cat.id }}/delete{% endcapture -%} | ||||
|               {%- capture cat_del_link %}{{ cat_del | relative_link }}{% endcapture -%} | ||||
|               <a href="{{ cat_del_link }}" class="text-danger" | ||||
|                  onclick="return Admin.deleteCategory('{{ cat.name }}', '{{ cat_del_link }}')"> | ||||
|                 Delete | ||||
|               </a> | ||||
|             </small> | ||||
|  | ||||
| @ -9,8 +9,8 @@ | ||||
|             Published <span class="badge rounded-pill bg-secondary">{{ model.posts }}</span> | ||||
|               Drafts <span class="badge rounded-pill bg-secondary">{{ model.drafts }}</span> | ||||
|           </h6> | ||||
|           <a href="/admin/posts" class="btn btn-secondary me-2">View All</a> | ||||
|           <a href="/admin/post/new/edit" class="btn btn-primary">Write a New Post</a> | ||||
|           <a href="{{ "admin/posts" | relative_permalink }}" class="btn btn-secondary me-2">View All</a> | ||||
|           <a href="{{ "admin/post/new/edit" | relative_permalink }}" class="btn btn-primary">Write a New Post</a> | ||||
|         </div> | ||||
|       </div> | ||||
|     </section> | ||||
| @ -22,8 +22,8 @@ | ||||
|             All <span class="badge rounded-pill bg-secondary">{{ model.pages }}</span> | ||||
|               Shown in Page List <span class="badge rounded-pill bg-secondary">{{ model.listed_pages }}</span> | ||||
|           </h6> | ||||
|           <a href="/admin/pages" class="btn btn-secondary me-2">View All</a> | ||||
|           <a href="/admin/page/new/edit" class="btn btn-primary">Create a New Page</a> | ||||
|           <a href="{{ "admin/pages" | relative_link }}" class="btn btn-secondary me-2">View All</a> | ||||
|           <a href="{{ "admin/page/new/edit" | relative_link }}" class="btn btn-primary">Create a New Page</a> | ||||
|         </div> | ||||
|       </div> | ||||
|     </section> | ||||
| @ -37,15 +37,15 @@ | ||||
|             All <span class="badge rounded-pill bg-secondary">{{ model.categories }}</span> | ||||
|               Top Level <span class="badge rounded-pill bg-secondary">{{ model.top_level_categories }}</span> | ||||
|           </h6> | ||||
|           <a href="/admin/categories" class="btn btn-secondary me-2">View All</a> | ||||
|           <a href="/admin/category/new/edit" class="btn btn-secondary">Add a New Category</a> | ||||
|           <a href="{{ "admin/categories" | relative_link }}" class="btn btn-secondary me-2">View All</a> | ||||
|           <a href="{{ "admin/category/new/edit" | relative_link }}" class="btn btn-secondary">Add a New Category</a> | ||||
|         </div> | ||||
|       </div> | ||||
|     </section> | ||||
|   </div> | ||||
|   <div class="row pb-3"> | ||||
|     <div class="col text-end"> | ||||
|       <a href="/admin/settings" class="btn btn-secondary">Modify Settings</a> | ||||
|       <a href="{{ "admin/settings" | relative_link }}" class="btn btn-secondary">Modify Settings</a> | ||||
|     </div> | ||||
|   </div> | ||||
| </article> | ||||
|  | ||||
| @ -12,7 +12,7 @@ | ||||
|     <header> | ||||
|       <nav class="navbar navbar-dark bg-dark navbar-expand-md justify-content-start px-2 position-fixed top-0 w-100"> | ||||
|         <div class="container-fluid"> | ||||
|           <a class="navbar-brand" href="/">{{ web_log.name }}</a> | ||||
|           <a class="navbar-brand" href="{{ "" | relative_link }}">{{ web_log.name }}</a> | ||||
|           <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" | ||||
|                   aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation"> | ||||
|             <span class="navbar-toggler-icon"></span> | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <h2 class="my-3">Log On to {{ web_log.name }}</h2> | ||||
| <article class="py-3"> | ||||
|   <form action="/user/log-on" method="post"> | ||||
|   <form action="{{ "user/log-on" | relative_link }}" method="post"> | ||||
|     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> | ||||
|     {% if model.return_to %} | ||||
|       <input type="hidden" name="returnTo" value="{{ model.return_to.value }}"> | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <h2 class="my-3">{{ page_title }}</h2> | ||||
| <article> | ||||
|   <form action="/admin/page/save" method="post"> | ||||
|   <form action="{{ "admin/page/save" | relative_link }}" method="post"> | ||||
|     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> | ||||
|     <input type="hidden" name="pageId" value="{{ model.page_id }}"> | ||||
|     <div class="container"> | ||||
| @ -16,7 +16,8 @@ | ||||
|                    value="{{ model.permalink }}"> | ||||
|             <label for="permalink">Permalink</label> | ||||
|             {%- if model.page_id != "new" %} | ||||
|               <span class="form-text"><a href="/admin/page/{{ model.page_id }}/permalinks">Manage Permalinks</a></span> | ||||
|               {%- capture perm_edit %}admin/page/{{ model.page_id }}/permalinks{% endcapture -%} | ||||
|               <span class="form-text"><a href="{{ perm_edit | relative_link }}">Manage Permalinks</a></span> | ||||
|             {% endif -%} | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <h2 class="my-3">{{ page_title }}</h2> | ||||
| <article class="container"> | ||||
|   <a href="/admin/page/new/edit" class="btn btn-primary btn-sm mb-3">Create a New Page</a> | ||||
|   <a href="{{ "admin/page/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3">Create a New Page</a> | ||||
|   <table class="table table-sm table-hover"> | ||||
|     <thead> | ||||
|       <tr> | ||||
| @ -17,12 +17,15 @@ | ||||
|             {%- if pg.is_default %}   <span class="badge bg-success">HOME PAGE</span>{% endif -%} | ||||
|             {%- if pg.show_in_page_list %}   <span class="badge bg-primary">IN PAGE LIST</span> {% endif -%}<br> | ||||
|             <small> | ||||
|               <a href="/{% unless pg.is_default %}{{ pg.permalink }}{% endunless %}" target="_blank">View Page</a> | ||||
|               {%- capture pg_link %}{% unless pg.is_default %}{{ pg.permalink }}{% endunless %}{% endcapture -%} | ||||
|               <a href="{{ pg_link | relative_link }}" target="_blank">View Page</a> | ||||
|               <span class="text-muted"> • </span> | ||||
|               <a href="{{ pg.id | edit_page_link }}">Edit</a> | ||||
|               <a href="{{ pg | edit_page_link }}">Edit</a> | ||||
|               <span class="text-muted"> • </span> | ||||
|               <a href="/admin/page/{{ pg.id }}/delete" class="text-danger" | ||||
|                  onclick="return Admin.deletePage('{{ pg.id }}', '{{ pg.title }}')"> | ||||
|               {%- capture pg_del %}admin/page/{{ pg.id }}/delete{% endcapture -%} | ||||
|               {%- capture pg_del_link %}{{ pg_del | relative_link }}{% endcapture -%} | ||||
|               <a href="{{ pg_del_link }}" class="text-danger" | ||||
|                  onclick="return Admin.deletePage('{{ pg.title }}', '{{ pg_del_link }}')"> | ||||
|                 Delete | ||||
|               </a> | ||||
|             </small> | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| <h2 class="my-3">{{ page_title }}</h2> | ||||
| <article> | ||||
|   <form action="/admin/{{ model.entity }}/permalinks" method="post"> | ||||
|   {%- capture form_action %}admin/{{ model.entity }}/permalinks{% endcapture -%} | ||||
|   <form action="{{ form_action | relative_link }}" method="post"> | ||||
|     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> | ||||
|     <input type="hidden" name="id" value="{{ model.id }}"> | ||||
|     <div class="container"> | ||||
| @ -10,7 +11,8 @@ | ||||
|             <strong>{{ model.current_title }}</strong><br> | ||||
|             <small class="text-muted"> | ||||
|               <span class="fst-italic">{{ model.current_permalink }}</span><br> | ||||
|               <a href="/admin/{{ model.entity }}/{{ model.id }}/edit">« Back to Edit {{ model.entity | capitalize }}</a> | ||||
|               {%- capture back_link %}admin/{{ model.entity }}/{{ model.id }}/edit{% endcapture -%} | ||||
|               <a href="{{ back_link | relative_link }}">« Back to Edit {{ model.entity | capitalize }}</a> | ||||
|             </small> | ||||
|           </p> | ||||
|         </div> | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <h2 class="my-3">{{ page_title }}</h2> | ||||
| <article> | ||||
|   <form action="/admin/post/save" method="post"> | ||||
|   <form action="{{ "/admin/post/save" | relative_link }}" method="post"> | ||||
|     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> | ||||
|     <input type="hidden" name="postId" value="{{ model.post_id }}"> | ||||
|     <div class="container"> | ||||
| @ -16,7 +16,8 @@ | ||||
|                    value="{{ model.permalink }}"> | ||||
|             <label for="permalink">Permalink</label> | ||||
|             {%- if model.page_id != "new" %} | ||||
|               <span class="form-text"><a href="/admin/post/{{ model.post_id }}/permalinks">Manage Permalinks</a></span> | ||||
|               {%- capture perm_edit %}admin/post/{{ model.post_id }}/permalinks{% endcapture -%} | ||||
|               <span class="form-text"><a href="{{ perm_edit | relative_link }}">Manage Permalinks</a></span> | ||||
|             {% endif -%} | ||||
|           </div> | ||||
|           <div class="mb-2"> | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <h2 class="my-3">{{ page_title }}</h2> | ||||
| <article class="container"> | ||||
|   <a href="/admin/post/new/edit" class="btn btn-primary btn-sm mb-3">Write a New Post</a> | ||||
|   <a href="{{ "/admin/post/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3">Write a New Post</a> | ||||
|   <table class="table table-sm table-hover"> | ||||
|     <thead> | ||||
|       <tr> | ||||
| @ -23,12 +23,14 @@ | ||||
|           <td> | ||||
|             {{ post.title }}<br> | ||||
|             <small> | ||||
|               <a href="/{{ post.permalink }}" target="_blank">View Post</a> | ||||
|               <a href="{{ post | relative_link }}" target="_blank">View Post</a> | ||||
|               <span class="text-muted"> • </span> | ||||
|               <a href="{{ post.id | edit_post_link }}">Edit</a> | ||||
|               <a href="{{ post | edit_post_link }}">Edit</a> | ||||
|               <span class="text-muted"> • </span> | ||||
|               <a href="/admin/post/{{ pg.id }}/delete" class="text-danger" | ||||
|                  onclick="return Admin.deletePost('{{ post.id }}', '{{ post.title }}')"> | ||||
|               {%- capture post_del %}admin/post/{{ pg.id }}/delete{% endcapture -%} | ||||
|               {%- capture post_del_link %}{{ post_del | relative_link }}{% endcapture -%} | ||||
|               <a href="{{ post_del_link }}" class="text-danger" | ||||
|                  onclick="return Admin.deletePost('{{ post.title }}', '{{ post_del_link }}')"> | ||||
|                 Delete | ||||
|               </a> | ||||
|             </small> | ||||
| @ -44,12 +46,12 @@ | ||||
|     <div class="d-flex justify-content-evenly"> | ||||
|       <div> | ||||
|         {% if model.newer_link %} | ||||
|           <p><a class="btn btn-default" href="/{{ model.newer_link.value }}">« Newer Posts</a></p> | ||||
|           <p><a class="btn btn-default" href="{{ model.newer_link.value }}">« Newer Posts</a></p> | ||||
|         {% endif %} | ||||
|       </div> | ||||
|       <div class="text-right"> | ||||
|         {% if model.older_link %} | ||||
|           <p><a class="btn btn-default" href="/{{ model.older_link.value }}">Older Posts »</a></p> | ||||
|           <p><a class="btn btn-default" href="{{ model.older_link.value }}">Older Posts »</a></p> | ||||
|         {% endif %} | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <h2 class="my-3">{{ web_log.name }} Settings</h2> | ||||
| <article> | ||||
|   <form action="/admin/settings" method="post"> | ||||
|   <form action="{{ "admin/settings" | relative_link }}" method="post"> | ||||
|     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> | ||||
|     <div class="container"> | ||||
|       <div class="row"> | ||||
|  | ||||
| @ -1,12 +1,12 @@ | ||||
| <h2 class="my-3">{{ page_title }}</h2> | ||||
| <article> | ||||
|   <form action="/admin/tag-mapping/save" method="post"> | ||||
|   <form action="{{ "admin/tag-mapping/save" | relative_link }}" method="post"> | ||||
|     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> | ||||
|     <input type="hidden" name="id" value="{{ model.id }}"> | ||||
|     <div class="container"> | ||||
|       <div class="row mb-3"> | ||||
|         <div class="col"> | ||||
|           <a href="/admin/tag-mappings">« Back to Tag Mappings</a> | ||||
|           <a href="{{ "admin/tag-mappings" | relative_link }}">« Back to Tag Mappings</a> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="row mb-3"> | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| <h2 class="my-3">{{ page_title }}</h2> | ||||
| <article class="container"> | ||||
|   <a href="/admin/tag-mapping/new/edit" class="btn btn-primary btn-sm mb-3">Add a New Tag Mapping</a> | ||||
|   <a href="{{ "admin/tag-mapping/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3"> | ||||
|     Add a New Tag Mapping | ||||
|   </a> | ||||
|   <table class="table table-sm table-hover"> | ||||
|     <thead> | ||||
|       <tr> | ||||
| @ -15,8 +17,13 @@ | ||||
|           <td class="no-wrap"> | ||||
|             {{ map.tag }}<br> | ||||
|             <small> | ||||
|               <a href="/admin/tag-mapping/{{ map_id }}/delete" class="text-danger" | ||||
|                  onclick="return Admin.deleteTagMapping('{{ map_id }}', '{{ map.tag }}')"> | ||||
|               {%- capture map_edit %}admin/tag-mapping/{{ map_id }}/edit{% endcapture -%} | ||||
|               <a href="{{ map_edit | relative_link }}">Edit</a> | ||||
|               <span class="text-muted"> • </span> | ||||
|               {%- capture map_del %}admin/tag-mapping/{{ map_id }}/delete{% endcapture -%} | ||||
|               {%- capture map_del_link %}{{ map_del | relative_link }}{% endcapture -%} | ||||
|               <a href="{{ map_del_link }}" class="text-danger" | ||||
|                  onclick="return Admin.deleteTagMapping('{{ map.tag }}', '{{ map_del_link }}')"> | ||||
|                 Delete | ||||
|               </a> | ||||
|             </small> | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <h2 class="my-3">{{ page_title }}</h2> | ||||
| <article> | ||||
|   <form action="/admin/user/save" method="post"> | ||||
|   <form action="{{ "admin/user/save" | relative_link }}" method="post"> | ||||
|     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> | ||||
|     <div class="container"> | ||||
|       <div class="row mb-3"> | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|   <article class="content auto"> | ||||
|     {{ page.text }} | ||||
|     {% if logged_on -%} | ||||
|       <p><small><a href="{{ page.id | edit_page_link }}">Edit This Page</a></small></p> | ||||
|       <p><small><a href="{{ page | edit_page_link }}">Edit This Page</a></small></p> | ||||
|     {% endif %} | ||||
|   </article> | ||||
|   <aside class="app-sidebar"> | ||||
| @ -11,7 +11,10 @@ | ||||
|       <div> | ||||
|         <p class="app-sidebar-name"> | ||||
|           <strong>PrayerTracker</strong><br> | ||||
|           <a href="/solutions/prayer-tracker" title="About PrayerTracker • Bit Badger Solutions">About</a> • | ||||
|           <a href="{{ "solutions/prayer-tracker" | relative_link }}" | ||||
|              title="About PrayerTracker • Bit Badger Solutions"> | ||||
|             About | ||||
|           </a> • | ||||
|           <a href="https://prayer.bitbadger.solutions" title="PrayerTracker" target="_blank" rel="noopener">Visit</a> | ||||
|         </p> | ||||
|         <p class="app-sidebar-description"> | ||||
| @ -21,7 +24,10 @@ | ||||
|       <div> | ||||
|         <p class="app-sidebar-name"> | ||||
|           <strong>myPrayerJournal</strong><br> | ||||
|           <a href="/solutions/my-prayer-journal" title="About myPrayerJournal • Bit Badger Solutions">About</a> • | ||||
|           <a href="{{ "solutions/my-prayer-journal" | relative_link }}" | ||||
|              title="About myPrayerJournal • Bit Badger Solutions"> | ||||
|             About | ||||
|           </a> • | ||||
|           <a href="https://prayerjournal.me" title="myPrayerJournal" target="_blank" rel="noopener">Visit</a> | ||||
|         </p> | ||||
|         <p class="app-sidebar-description">Minimalist personal prayer journal</p> | ||||
| @ -29,7 +35,9 @@ | ||||
|       <div> | ||||
|         <p class="app-sidebar-name"> | ||||
|           <strong>Linux Resources</strong><br> | ||||
|           <a href="https://blog.bitbadger.solutions/linux/" title="Linux Resources" target="_blank" rel="noopener">Visit</a> | ||||
|           <a href="https://blog.bitbadger.solutions/linux/" title="Linux Resources" target="_blank" rel="noopener"> | ||||
|             Visit | ||||
|           </a> | ||||
|         </p> | ||||
|         <p class="app-sidebar-description">Handy information for Linux folks</p> | ||||
|       </div> | ||||
| @ -39,7 +47,10 @@ | ||||
|       <div> | ||||
|         <p class="app-sidebar-name"> | ||||
|           <strong>Futility Closet</strong><br> | ||||
|           <a href="/solutions/futility-closet" title="About Futility Closet • Bit Badger Solutions">About</a> • | ||||
|           <a href="{{ "solutions/futility-closet" | relative_link }}" | ||||
|              title="About Futility Closet • Bit Badger Solutions"> | ||||
|             About | ||||
|           </a> • | ||||
|           <a href="https://www.futilitycloset.com" title="Futility Closet" target="_blank" rel="noopener">Visit</a> | ||||
|         </p> | ||||
|         <p class="app-sidebar-description">An idler’s miscellany of compendious amusements</p> | ||||
| @ -47,7 +58,10 @@ | ||||
|       <div> | ||||
|         <p class="app-sidebar-name"> | ||||
|           <strong>Mindy Mackenzie</strong><br> | ||||
|           <a href="/solutions/mindy-mackenzie" title="About Mindy Mackenzie • Bit Badger Solutions">About</a> • | ||||
|           <a href="{{ "solutions/mindy-mackenzie" | relative_link }}" | ||||
|              title="About Mindy Mackenzie • Bit Badger Solutions"> | ||||
|             About | ||||
|           </a> • | ||||
|           <a href="https://mindymackenzie.com" title="Mindy Mackenzie" target="_blank" rel="noopener">Visit</a> | ||||
|         </p> | ||||
|         <p class="app-sidebar-description"><em>WSJ</em>-best-selling author of <em>The Courage Solution</em></p> | ||||
| @ -55,7 +69,10 @@ | ||||
|       <div> | ||||
|         <p class="app-sidebar-name"> | ||||
|           <strong>Riehl World News</strong><br> | ||||
|           <a href="/solutions/riehl-world-news" title="About Riehl World News • Bit Badger Solutions">About</a> • | ||||
|           <a href="{{ "solutions/riehl-world-news" | relative_link }}" | ||||
|              title="About Riehl World News • Bit Badger Solutions"> | ||||
|             About | ||||
|           </a> • | ||||
|           <a href="http://riehlworldview.com" title="Riehl World News" target="_blank" rel="noopener">Visit</a> | ||||
|         </p> | ||||
|         <p class="app-sidebar-description">Riehl news for real people</p> | ||||
| @ -66,7 +83,10 @@ | ||||
|       <div> | ||||
|         <p class="app-sidebar-name"> | ||||
|           <strong>Bay Vista Baptist Church</strong><br> | ||||
|           <a href="/solutions/bay-vista" title="About Bay Vista Baptist Church • Bit Badger Solutions">About</a> • | ||||
|           <a href="{{ "solutions/bay-vista" | relative_link }}" | ||||
|              title="About Bay Vista Baptist Church • Bit Badger Solutions"> | ||||
|             About | ||||
|           </a> • | ||||
|           <a href="https://bayvista.org" title="Bay Vista Baptist Church" target="_blank" rel="noopener">Visit</a> | ||||
|         </p> | ||||
|         <p class="app-sidebar-description">Biloxi, Mississippi</p> | ||||
| @ -74,8 +94,13 @@ | ||||
|       <div> | ||||
|         <p class="app-sidebar-name"> | ||||
|           <strong>The Bit Badger Blog</strong><br> | ||||
|           <a href="/solutions/tech-blog" title="About The Bit Badger Blog • Bit Badger Solutions">About</a> • | ||||
|           <a href="https://blog.bitbadger.solutions" title="The Bit Badger Blog" target="_blank" rel="noopener">Visit</a> | ||||
|           <a href="{{ "solutions/tech-blog" | relative_link }}" | ||||
|              title="About The Bit Badger Blog • Bit Badger Solutions"> | ||||
|             About | ||||
|           </a> • | ||||
|           <a href="https://blog.bitbadger.solutions" title="The Bit Badger Blog" target="_blank" rel="noopener"> | ||||
|             Visit | ||||
|           </a> | ||||
|         </p> | ||||
|         <p class="app-sidebar-description">Technical information (“geek stuff”) from Bit Badger Solutions</p> | ||||
|       </div> | ||||
| @ -92,7 +117,9 @@ | ||||
|       <div> | ||||
|         <p class="app-sidebar-name"> | ||||
|           <strong>A Word from the Word</strong><br> | ||||
|           <a href="https://devotions.summershome.org" title="A Word from the Word" target="_blank" rel="noopener">Visit</a> | ||||
|           <a href="https://devotions.summershome.org" title="A Word from the Word" target="_blank" rel="noopener"> | ||||
|             Visit | ||||
|           </a> | ||||
|         </p> | ||||
|         <p class="app-sidebar-description">Devotions by Daniel</p> | ||||
|       </div> | ||||
|  | ||||
| @ -11,13 +11,13 @@ | ||||
|   <body> | ||||
|     <header class="site-header"> | ||||
|       <div class="header-logo"> | ||||
|         <a href="/"> | ||||
|         <a href="{{ "" | relative_link }}"> | ||||
|           <img src="/themes/{{ web_log.theme_path }}/bitbadger.png" | ||||
|                alt="A cartoon badger looking at a computer screen, with his paw on a mouse" | ||||
|                title="Bit Badger Solutions"> | ||||
|         </a> | ||||
|       </div> | ||||
|       <div class="header-title"><a href="/">Bit Badger Solutions</a></div> | ||||
|       <div class="header-title"><a href="{{ "" | relative_link }}">Bit Badger Solutions</a></div> | ||||
|       <div class="header-spacer">   </div> | ||||
|       <div class="header-social"> | ||||
|         <a href="https://twitter.com/Bit_Badger" title="Bit_Badger on Twitter" target="_blank"> | ||||
| @ -33,14 +33,15 @@ | ||||
|       <div> | ||||
|         <small> | ||||
|           {% if logged_on -%} | ||||
|             <a href="/admin">Dashboard</a> ~ <a href="/user/log-off">Log Off</a> | ||||
|             <a href="{{ "admin" | relative_link }}">Dashboard</a> ~ | ||||
|             <a href="{{ "user/log-off" | relative_link }}">Log Off</a> | ||||
|           {% else %} | ||||
|             <a href="/user/log-on">Log On</a> | ||||
|             <a href="{{ "user/log-on" | relative_link }}">Log On</a> | ||||
|           {% endif %} | ||||
|         </small> | ||||
|       </div> | ||||
|       <div class="footer-by"> | ||||
|         A <strong><a href="/">Bit Badger Solutions</a></strong> original design | ||||
|         A <strong><a href="{{ "" | relative_link }}">Bit Badger Solutions</a></strong> original design | ||||
|       </div> | ||||
|     </footer> | ||||
|   </body> | ||||
|  | ||||
| @ -1,8 +1,8 @@ | ||||
| <article class="content auto"> | ||||
|   <h1>{{ page.title }}</h1> | ||||
|   {{ page.text }} | ||||
|   <p><br><a href="/" title="Home">« Home</a></p> | ||||
|   <p><br><a href="{{ "" | relative_link }}" title="Home">« Home</a></p> | ||||
|   {% if logged_on -%} | ||||
|     <p><small><a href="{{ page.id | edit_page_link }}">Edit This Page</a></small></p> | ||||
|     <p><small><a href="{{ page | edit_page_link }}">Edit This Page</a></small></p> | ||||
|   {% endif %} | ||||
| </article> | ||||
|  | ||||
| @ -92,9 +92,9 @@ | ||||
|         </div> | ||||
|       </section> | ||||
|     {%- endif %} | ||||
|     <p><br><a href="/solutions">« Back to All Solutions</a></p> | ||||
|     <p><br><a href="{{ "solutions" | relative_link }}">« Back to All Solutions</a></p> | ||||
|     {% if logged_on -%} | ||||
|       <p><small><a href="{{ page.id | edit_page_link }}">Edit This Page</a></small></p> | ||||
|       <p><small><a href="{{ page | edit_page_link }}">Edit This Page</a></small></p> | ||||
|     {% endif %} | ||||
|   </article> | ||||
| </div> | ||||
|  | ||||
| @ -8,7 +8,8 @@ | ||||
|   {%- for post in model.posts %} | ||||
|     <article class="item"> | ||||
|       <h1 class="item-heading"> | ||||
|         <a href="/{{ post.permalink }}" title="Permanent Link to "{{ post.title | strip_html | escape }}""> | ||||
|         <a href="{{ post | relative_link }}" | ||||
|            title="Permanent Link to "{{ post.title | strip_html | escape }}""> | ||||
|           {{ post.title }} | ||||
|         </a> | ||||
|       </h1> | ||||
| @ -24,7 +25,7 @@ | ||||
|         </span> | ||||
|         {% if logged_on %} | ||||
|           <span> | ||||
|             <a href="{{ post.id | edit_post_link }}"><i class="fa fa-pencil-square-o"></i> Edit Post</a> | ||||
|             <a href="{{ post | edit_post_link }}"><i class="fa fa-pencil-square-o"></i> Edit Post</a> | ||||
|           </span> | ||||
|         {% endif %} | ||||
|       </h4> | ||||
| @ -34,12 +35,12 @@ | ||||
|   <nav aria-label="pagination"> | ||||
|     <ul class="pager"> | ||||
|       {% if model.newer_link -%} | ||||
|         <li class="previous item"><a href="/{{ model.newer_link.value }}">« Newer Posts</a></li> | ||||
|         <li class="previous item"><a href="{{ model.newer_link.value }}">« Newer Posts</a></li> | ||||
|       {%- else -%} | ||||
|         <li></li> | ||||
|       {% endif %} | ||||
|       {% if model.older_link -%} | ||||
|         <li class="next item"><a href="/{{ model.older_link.value }}">Older Posts »</a></li> | ||||
|         <li class="next item"><a href="{{ model.older_link.value }}">Older Posts »</a></li> | ||||
|       {%- endif -%} | ||||
|     </ul> | ||||
|   </nav> | ||||
| @ -74,7 +75,7 @@ | ||||
|       {% for cat in categories -%} | ||||
|         {%- assign indent = cat.parent_names | size -%} | ||||
|         <li class="cat-list-item"{% if indent > 0 %} style="padding-left:{{ indent }}rem;"{% endif %}> | ||||
|           <a href="/category/{{ cat.slug }}/" class="cat-list-link">{{ cat.name }}</a> | ||||
|           <a href="{{ cat | category_link }}" class="cat-list-link">{{ cat.name }}</a> | ||||
|           <span class="cat-list-count">{{ cat.post_count }}</span> | ||||
|         </li> | ||||
|       {%- endfor %} | ||||
|  | ||||
| @ -18,17 +18,19 @@ | ||||
|     <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"> | ||||
|     <link rel="stylesheet" href="/themes/{{ web_log.theme_path }}/style.css"> | ||||
|     {%- if is_home %} | ||||
|       <link rel="alternate" type="application/rss+xml" title="{{ web_log.name | escape }}" href="/feed.xml"> | ||||
|       <link rel="alternate" type="application/rss+xml" title="{{ web_log.name | escape }}" | ||||
|             href="{{ "feed.xml" | relative_link }}"> | ||||
|       <link rel="canonical" href="{{ "" | absolute_url }}"> | ||||
|     {%- endif %} | ||||
|     <script src="/themes/{{ web_log.theme_path }}/djs.js"></script> | ||||
|   </head> | ||||
|   <body> | ||||
|     <nav class="site-header" role="navigation"> | ||||
|       <p><a class="nav-home" href="/">{{ web_log.name }}</a></p> | ||||
|       <p><a class="nav-home" href="{{ "" | relative_link }}">{{ web_log.name }}</a></p> | ||||
|       {%- if web_log.subtitle %}<p>{{ web_log.subtitle.value }}</p>{% endif -%} | ||||
|       <p class="nav-spacer"></p> | ||||
|       {%- for page in page_list %} | ||||
|         <p class="desktop"><a href="/{{ page.permalink }}">{{ page.title }}</a></p> | ||||
|         <p class="desktop"><a href="{{ page | relative_link }}">{{ page.title }}</a></p> | ||||
|       {%- endfor %} | ||||
|       <p class="desktop"> | ||||
|         <a href="https://devotions.summershome.org" target="_blank" rel="noopener">A Word from the Word</a> | ||||
| @ -38,7 +40,7 @@ | ||||
|     {{ content }} | ||||
|     <footer class="part-1" id="links"> | ||||
|       {%- for page in page_list %} | ||||
|         <p class="mobile"><a href="{{ page.permalink }}">{{ page.title }}</a></p> | ||||
|         <p class="mobile"><a href="{{ page | relative_link }}">{{ page.title }}</a></p> | ||||
|       {%- endfor %} | ||||
|       <p class="mobile"> | ||||
|         <a href="https://devotions.summershome.org" target="_blank" rel="noopener">A Word from the Word</a> | ||||
| @ -143,9 +145,9 @@ | ||||
|         </a> | ||||
|         • Powered by <a href="https://github.com/bit-badger/myWebLog/tree/v2">myWebLog</a> • | ||||
|         {% if logged_on %} | ||||
|           <a href="/admin">Dashboard</a> | ||||
|           <a href="{{ "admin" | relative_link }}">Dashboard</a> | ||||
|         {% else %} | ||||
|           <a href="/user/log-on">Log On</a> | ||||
|           <a href="{{ "user/log-on" | relative_link }}">Log On</a> | ||||
|         {%- endif %} | ||||
|       </div> | ||||
|     </footer> | ||||
|  | ||||
| @ -15,7 +15,7 @@ | ||||
|       {% endif %} | ||||
|       <span title="Author"><i class="fa fa-user"></i> {{ model.authors | value: post.author_id }}</span> | ||||
|       {% if logged_on %} | ||||
|         <span><a href="{{ post.id | edit_post_link }}"><i class="fa fa-pencil-square-o"></i> Edit Post</a></span> | ||||
|         <span><a href="{{ post | edit_post_link }}"><i class="fa fa-pencil-square-o"></i> Edit Post</a></span> | ||||
|       {% endif %} | ||||
|     </h4> | ||||
|     <div>{{ post.text }}</div> | ||||
| @ -27,7 +27,7 @@ | ||||
|           {% assign cat = categories | where: "id", cat_id | first %} | ||||
|           <span class="no-wrap"> | ||||
|             <i class="fa fa-folder-open-o" title="Category"></i> | ||||
|             <a href="{{ cat.slug | category_link }}" title="Categorized under “{{ cat.name | escape }}”"> | ||||
|             <a href="{{ cat | category_link }}" title="Categorized under “{{ cat.name | escape }}”"> | ||||
|              {{ cat.name }} | ||||
|             </a>     | ||||
|           </span> | ||||
| @ -53,16 +53,16 @@ | ||||
|       <ul class="pager"> | ||||
|         {% if model.newer_link -%} | ||||
|           <li class="previous item"> | ||||
|             <h4 class="item-heading"><a href="/{{ model.newer_link.value }}">«</a> Previous Post</h4> | ||||
|             <a href="/{{ model.newer_link.value }}">{{ model.newer_name.value }}</a> | ||||
|             <h4 class="item-heading"><a href="{{ model.newer_link.value }}">«</a> Previous Post</h4> | ||||
|             <a href="{{ model.newer_link.value }}">{{ model.newer_name.value }}</a> | ||||
|           </li> | ||||
|         {%- else -%} | ||||
|           <li></li> | ||||
|         {% endif %} | ||||
|         {% if model.older_link -%} | ||||
|           <li class="next item"> | ||||
|             <h4 class="item-heading">Next Post <a href="/{{ model.older_link.value }}">»</a></h4> | ||||
|             <a href="/{{ model.older_link.value }}">{{ model.older_name.value }}</a> | ||||
|             <h4 class="item-heading">Next Post <a href="{{ model.older_link.value }}">»</a></h4> | ||||
|             <a href="{{ model.older_link.value }}">{{ model.older_name.value }}</a> | ||||
|           </li> | ||||
|         {%- endif -%} | ||||
|       </ul> | ||||
|  | ||||
| @ -7,7 +7,7 @@ | ||||
|       <div class="col"> | ||||
|         <article> | ||||
|           <h1> | ||||
|             <a href="/{{ post.permalink }}" title="Permanent link to "{{ post.title | escape }}""> | ||||
|             <a href="{{ post | relative_link }}" title="Permanent link to "{{ post.title | escape }}""> | ||||
|               {{ post.title }} | ||||
|             </a> | ||||
|           </h1> | ||||
|  | ||||
| @ -13,7 +13,7 @@ | ||||
|     <header> | ||||
|       <nav class="navbar navbar-light bg-light navbar-expand-md justify-content-start px-2"> | ||||
|         <div class="container-fluid"> | ||||
|           <a class="navbar-brand" href="/">{{ web_log.name }}</a> | ||||
|           <a class="navbar-brand" href="{{ "" | relative_link }}">{{ web_log.name }}</a> | ||||
|           <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" | ||||
|                   aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation"> | ||||
|             <span class="navbar-toggler-icon"></span> | ||||
|  | ||||
| @ -10,7 +10,8 @@ | ||||
|       <h1 class="home-title"> | ||||
|         <small class="home-lead">{{ post.published_on | date: "dddd, MMMM d, yyyy" }}</small><br> | ||||
|           | ||||
|         <a href="/{{ post.permalink }}" title="Permanent Link to "{{ post.title | strip_html | escape }}""> | ||||
|         <a href="{{ post | relative_link }}" | ||||
|            title="Permanent Link to "{{ post.title | strip_html | escape }}""> | ||||
|           {{ post.title }} | ||||
|         </a> | ||||
|       </h1> | ||||
| @ -39,18 +40,18 @@ | ||||
|         {%- endfor %} | ||||
|       </small><br> | ||||
|     {%- endif %} | ||||
|     {%- if logged_on %}<small><a href="{{ post.id | edit_post_link }}">Edit Post</a></small>{% endif %} | ||||
|     {%- if logged_on %}<small><a href="{{ post | edit_post_link }}">Edit Post</a></small>{% endif %} | ||||
|   </article> | ||||
| {%- endfor %} | ||||
| <div class="bottom-nav" role="navigation"> | ||||
|   <div class="nav-previous"> | ||||
|     {% if model.newer_link -%} | ||||
|       <a href="/{{ model.newer_link.value }}">« Newer Posts</a> | ||||
|       <a href="{{ model.newer_link.value }}">« Newer Posts</a> | ||||
|     {% endif %} | ||||
|   </div> | ||||
|   <div class="nav-next"> | ||||
|     {% if model.older_link -%} | ||||
|       <a href="/{{ model.older_link.value }}">Older Posts »</a> | ||||
|       <a href="{{ model.older_link.value }}">Older Posts »</a> | ||||
|     {% endif %} | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| @ -12,26 +12,26 @@ | ||||
|         {{ page_title | strip_html }}{% if page_title and page_title != "" %} » {% endif %}{{ web_log.name }} | ||||
|       {%- endif %} | ||||
|     </title> | ||||
|     <link rel="stylesheet" href="/themes/tech-blog/style.css"> | ||||
|     {% comment %}link(rel='canonical' href=config.url + url_for(page.path.replace('index.html', ''))) {% endcomment %} | ||||
|     <link rel="stylesheet" href="/themes/{{ web_log.theme_path }}/style.css"> | ||||
|     {%- if is_home %} | ||||
|       <link rel="alternate" type="application/rss+xml" | ||||
|             title="{{ web_log.name }}" href="https://{{ web_log.urlBase }}/feed.xml"> | ||||
|       <link rel="alternate" type="application/rss+xml" title="{{ web_log.name }}" | ||||
|             href="{{ "feed.xml" | absolute_link }}"> | ||||
|       <link rel="canonical" href="{{ "" | absolute_link }}"> | ||||
|     {%- endif %} | ||||
|   </head> | ||||
|   <body> | ||||
|     <header class="site-header"> | ||||
|       <div class="header-logo"> | ||||
|         <a href="/"> | ||||
|         <a href="{{ "" | relative_link }}"> | ||||
|           <img src="/themes/tech-blog/img/bitbadger.png" | ||||
|                alt="A cartoon badger looking at a computer screen, with his paw on a mouse" | ||||
|                title="Bit Badger Solutions"> | ||||
|         </a> | ||||
|       </div> | ||||
|       <div class="header-title"><a href="/">The Bit Badger Blog</a></div> | ||||
|       <div class="header-title"><a href="{{ "" | relative_link }}">The Bit Badger Blog</a></div> | ||||
|       <div class="header-spacer"> </div> | ||||
|       <div class="header-social"> | ||||
|         <a href="/feed.xml" title="Subscribe to The Bit Badger Blog via RSS"> | ||||
|         <a href="{{ "feed.xml" | relative_link }}" title="Subscribe to The Bit Badger Blog via RSS"> | ||||
|           <img src="/themes/tech-blog/img/rss.png" alt="RSS"> | ||||
|         </a>     | ||||
|         <a href="https://twitter.com/Bit_Badger" title="Bit_Badger on Twitter"> | ||||
| @ -49,7 +49,7 @@ | ||||
|       <aside class="blog-sidebar"> | ||||
|         <div> | ||||
|           <div class="sidebar-head">Linux Resources</div> | ||||
|           <ul><li><a href="/linux/">Browse Resources</a></li></ul> | ||||
|           <ul><li><a href="{{ "linux/" | relative_link }}">Browse Resources</a></li></ul> | ||||
|         </div> | ||||
|         <div> | ||||
|           <div class="sidebar-head">Categories</div> | ||||
| @ -76,9 +76,9 @@ | ||||
|         Powered by <a href="https://github.com/bit-badger/myWebLog/tree/v2" target="_blank" rel="noopener">myWebLog</a> | ||||
|         • | ||||
|         {% if logged_on %} | ||||
|           <a href="/admin">Dashboard</a> | ||||
|           <a href="{{ "admin" | relative_link }}">Dashboard</a> | ||||
|         {% else %} | ||||
|           <a href="/user/log-on">Log On</a> | ||||
|           <a href="{{ "user/log-on" | relative_link }}">Log On</a> | ||||
|         {%- endif %} | ||||
|       </span> | ||||
|     </footer> | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <article class="auto"> | ||||
|   <h1 class="entry-title">{{ page.title }}</h1> | ||||
|   <div class="entry-content">{{ page.text }}</div> | ||||
|   {%- if logged_on %}<p><small><a href="{{ page.id | edit_page_link }}">Edit Page</a></small></p>{% endif %} | ||||
|   {%- if logged_on %}<p><small><a href="{{ page | edit_page_link }}">Edit Page</a></small></p>{% endif %} | ||||
| </article> | ||||
| @ -37,8 +37,10 @@ | ||||
|       </span> • | ||||
|     {%- endif %} | ||||
|     Bookmark the | ||||
|     <a href="/{{ post.permalink }}" rel="bookmark" | ||||
|        title="Permanent link to “{{ post.title | strip_html | escape }}”">permalink</a> | ||||
|     {%- if logged_on %} • <a href="{{ post.id | edit_post_link }}">Edit Post</a>{% endif %} | ||||
|     <a href="{{ post | absolute_link }}" rel="bookmark" | ||||
|        title="Permanent link to “{{ post.title | strip_html | escape }}”"> | ||||
|       permalink | ||||
|     </a> | ||||
|     {%- if logged_on %} • <a href="{{ post | edit_post_link }}">Edit Post</a>{% endif %} | ||||
|   </div> | ||||
| </article> | ||||
|  | ||||
| @ -156,58 +156,52 @@ | ||||
|   }, | ||||
| 
 | ||||
|   /** | ||||
|    * Confirm and delete a category | ||||
|    * @param id The ID of the category to be deleted | ||||
|    * @param name The name of the category to be deleted | ||||
|    * Confirm and delete an item | ||||
|    * @param name The name of the item to be deleted | ||||
|    * @param url The URL to which the form should be posted | ||||
|    */ | ||||
|   deleteCategory(id, name) { | ||||
|     if (confirm(`Are you sure you want to delete the category "${name}"? This action cannot be undone.`)) { | ||||
|   deleteItem(name, url) { | ||||
|     if (confirm(`Are you sure you want to delete the ${name}? This action cannot be undone.`)) { | ||||
|       const form = document.getElementById("deleteForm") | ||||
|       form.action = `/admin/category/${id}/delete` | ||||
|       form.action = url | ||||
|       form.submit() | ||||
|     } | ||||
|     return false | ||||
|   }, | ||||
|    | ||||
|   /** | ||||
|    * Confirm and delete a category | ||||
|    * @param name The name of the category to be deleted | ||||
|    * @param url The URL to which the form should be posted | ||||
|    */ | ||||
|   deleteCategory(name, url) { | ||||
|     return this.deleteItem(`category "${name}"`, url) | ||||
|   }, | ||||
| 
 | ||||
|   /** | ||||
|    * Confirm and delete a page | ||||
|    * @param id The ID of the page to be deleted | ||||
|    * @param title The title of the page to be deleted | ||||
|    * @param url The URL to which the form should be posted | ||||
|    */ | ||||
|   deletePage(id, title) { | ||||
|     if (confirm(`Are you sure you want to delete the page "${name}"? This action cannot be undone.`)) { | ||||
|       const form = document.getElementById("deleteForm") | ||||
|       form.action = `/admin/page/${id}/delete` | ||||
|       form.submit() | ||||
|     } | ||||
|     return false | ||||
|   deletePage(title, url) { | ||||
|     return this.deleteItem(`page "${title}"`, url) | ||||
|   }, | ||||
| 
 | ||||
|   /** | ||||
|    * Confirm and delete a post | ||||
|    * @param id The ID of the post to be deleted | ||||
|    * @param title The title of the post to be deleted | ||||
|    * @param url The URL to which the form should be posted | ||||
|    */ | ||||
|   deletePost(id, title) { | ||||
|     if (confirm(`Are you sure you want to delete the post "${name}"? This action cannot be undone.`)) { | ||||
|       const form = document.getElementById("deleteForm") | ||||
|       form.action = `/admin/post/${id}/delete` | ||||
|       form.submit() | ||||
|     } | ||||
|     return false | ||||
|   deletePost(title, url) { | ||||
|     return this.deleteItem(`post "${title}"`, url) | ||||
|   }, | ||||
| 
 | ||||
|   /** | ||||
|    * Confirm and delete a tag mapping | ||||
|    * @param id The ID of the mapping to be deleted | ||||
|    * @param tag The tag for which the mapping will be deleted | ||||
|    * @param url The URL to which the form should be posted | ||||
|    */ | ||||
|   deleteTagMapping(id, tag) { | ||||
|     if (confirm(`Are you sure you want to delete the mapping for "${tag}"? This action cannot be undone.`)) { | ||||
|       const form = document.getElementById("deleteForm") | ||||
|       form.action = `/admin/tag-mapping/${id}/delete` | ||||
|       form.submit() | ||||
|     } | ||||
|     return false | ||||
|   deleteTagMapping(tag, url) { | ||||
|     return this.deleteItem(`mapping for "${tag}"`, url) | ||||
|   } | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user