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