V2 #1
|
@ -752,6 +752,7 @@ module WebLog =
|
||||||
"postsPerPage", webLog.postsPerPage
|
"postsPerPage", webLog.postsPerPage
|
||||||
"timeZone", webLog.timeZone
|
"timeZone", webLog.timeZone
|
||||||
"themePath", webLog.themePath
|
"themePath", webLog.themePath
|
||||||
|
"autoHtmx", webLog.autoHtmx
|
||||||
]
|
]
|
||||||
write; withRetryDefault; ignoreResult
|
write; withRetryDefault; ignoreResult
|
||||||
}
|
}
|
||||||
|
|
|
@ -275,6 +275,9 @@ type WebLog =
|
||||||
|
|
||||||
/// The RSS options for this web log
|
/// The RSS options for this web log
|
||||||
rss : RssOptions
|
rss : RssOptions
|
||||||
|
|
||||||
|
/// Whether to automatically load htmx
|
||||||
|
autoHtmx : bool
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to support web logs
|
/// Functions to support web logs
|
||||||
|
@ -291,6 +294,7 @@ module WebLog =
|
||||||
urlBase = ""
|
urlBase = ""
|
||||||
timeZone = ""
|
timeZone = ""
|
||||||
rss = RssOptions.empty
|
rss = RssOptions.empty
|
||||||
|
autoHtmx = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the host (including scheme) and extra path from the URL base
|
/// Get the host (including scheme) and extra path from the URL base
|
||||||
|
|
|
@ -676,7 +676,11 @@ type SettingsModel =
|
||||||
|
|
||||||
/// The theme to use to display the web log
|
/// The theme to use to display the web log
|
||||||
themePath : string
|
themePath : string
|
||||||
|
|
||||||
|
/// Whether to automatically load htmx
|
||||||
|
autoHtmx : bool
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a settings model from a web log
|
/// Create a settings model from a web log
|
||||||
static member fromWebLog (webLog : WebLog) =
|
static member fromWebLog (webLog : WebLog) =
|
||||||
{ name = webLog.name
|
{ name = webLog.name
|
||||||
|
@ -685,6 +689,19 @@ type SettingsModel =
|
||||||
postsPerPage = webLog.postsPerPage
|
postsPerPage = webLog.postsPerPage
|
||||||
timeZone = webLog.timeZone
|
timeZone = webLog.timeZone
|
||||||
themePath = webLog.themePath
|
themePath = webLog.themePath
|
||||||
|
autoHtmx = webLog.autoHtmx
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a web log with settings from the form
|
||||||
|
member this.update (webLog : WebLog) =
|
||||||
|
{ webLog with
|
||||||
|
name = this.name
|
||||||
|
subtitle = if this.subtitle = "" then None else Some this.subtitle
|
||||||
|
defaultPage = this.defaultPage
|
||||||
|
postsPerPage = this.postsPerPage
|
||||||
|
timeZone = this.timeZone
|
||||||
|
themePath = this.themePath
|
||||||
|
autoHtmx = this.autoHtmx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,17 @@ open System
|
||||||
open System.IO
|
open System.IO
|
||||||
open System.Web
|
open System.Web
|
||||||
open DotLiquid
|
open DotLiquid
|
||||||
|
open Giraffe.ViewEngine
|
||||||
open MyWebLog.ViewModels
|
open MyWebLog.ViewModels
|
||||||
|
|
||||||
/// Get the current web log from the DotLiquid context
|
/// Get the current web log from the DotLiquid context
|
||||||
let webLog (ctx : Context) =
|
let webLog (ctx : Context) =
|
||||||
ctx.Environments[0].["web_log"] :?> WebLog
|
ctx.Environments[0].["web_log"] :?> WebLog
|
||||||
|
|
||||||
|
/// Does an asset exist for the current theme?
|
||||||
|
let assetExists fileName (webLog : WebLog) =
|
||||||
|
File.Exists (Path.Combine ("wwwroot", "themes", webLog.themePath, fileName))
|
||||||
|
|
||||||
/// Obtain the link from known types
|
/// Obtain the link from known types
|
||||||
let permalink (ctx : Context) (item : obj) (linkFunc : WebLog -> Permalink -> string) =
|
let permalink (ctx : Context) (item : obj) (linkFunc : WebLog -> Permalink -> string) =
|
||||||
match item with
|
match item with
|
||||||
|
@ -72,9 +77,11 @@ type EditPostLinkFilter () =
|
||||||
type NavLinkFilter () =
|
type NavLinkFilter () =
|
||||||
static member NavLink (ctx : Context, url : string, text : string) =
|
static member NavLink (ctx : Context, url : string, text : string) =
|
||||||
let webLog = webLog ctx
|
let webLog = webLog ctx
|
||||||
|
let _, path = WebLog.hostAndPath webLog
|
||||||
|
let path = if path = "" then path else $"{path.Substring 1}/"
|
||||||
seq {
|
seq {
|
||||||
"<li class=\"nav-item\"><a class=\"nav-link"
|
"<li class=\"nav-item\"><a class=\"nav-link"
|
||||||
if url = string ctx.Environments[0].["current_page"] then " active"
|
if (string ctx.Environments[0].["current_page"]).StartsWith $"{path}{url}" then " active"
|
||||||
"\" href=\""
|
"\" href=\""
|
||||||
WebLog.relativeUrl webLog (Permalink url)
|
WebLog.relativeUrl webLog (Permalink url)
|
||||||
"\">"
|
"\">"
|
||||||
|
@ -98,10 +105,9 @@ type PageHeadTag () =
|
||||||
result.WriteLine $"""<meta name="generator" content="{context.Environments[0].["generator"]}">"""
|
result.WriteLine $"""<meta name="generator" content="{context.Environments[0].["generator"]}">"""
|
||||||
|
|
||||||
// Theme assets
|
// Theme assets
|
||||||
let has fileName = File.Exists (Path.Combine ("wwwroot", "themes", webLog.themePath, fileName))
|
if assetExists "style.css" webLog then
|
||||||
if has "style.css" then
|
|
||||||
result.WriteLine $"""{s}<link rel="stylesheet" href="/themes/{webLog.themePath}/style.css">"""
|
result.WriteLine $"""{s}<link rel="stylesheet" href="/themes/{webLog.themePath}/style.css">"""
|
||||||
if has "favicon.ico" then
|
if assetExists "favicon.ico" webLog then
|
||||||
result.WriteLine $"""{s}<link rel="icon" href="/themes/{webLog.themePath}/favicon.ico">"""
|
result.WriteLine $"""{s}<link rel="icon" href="/themes/{webLog.themePath}/favicon.ico">"""
|
||||||
|
|
||||||
// RSS feeds and canonical URLs
|
// RSS feeds and canonical URLs
|
||||||
|
@ -133,6 +139,22 @@ type PageHeadTag () =
|
||||||
result.WriteLine $"""{s}<link rel="canonical" href="{url}">"""
|
result.WriteLine $"""{s}<link rel="canonical" href="{url}">"""
|
||||||
|
|
||||||
|
|
||||||
|
/// Create various items in the page header based on the state of the page being generated
|
||||||
|
type PageFootTag () =
|
||||||
|
inherit Tag ()
|
||||||
|
|
||||||
|
override this.Render (context : Context, result : TextWriter) =
|
||||||
|
let webLog = webLog context
|
||||||
|
// spacer
|
||||||
|
let s = " "
|
||||||
|
|
||||||
|
if webLog.autoHtmx then
|
||||||
|
result.WriteLine $"{s}{RenderView.AsString.htmlNode Htmx.Script.minified}"
|
||||||
|
|
||||||
|
if assetExists "script.js" webLog then
|
||||||
|
result.WriteLine $"""{s}<script src="/themes/{webLog.themePath}/script.js"></script>"""
|
||||||
|
|
||||||
|
|
||||||
/// A filter to generate a relative link
|
/// A filter to generate a relative link
|
||||||
type RelativeLinkFilter () =
|
type RelativeLinkFilter () =
|
||||||
static member RelativeLink (ctx : Context, item : obj) =
|
static member RelativeLink (ctx : Context, item : obj) =
|
||||||
|
@ -167,7 +189,7 @@ type UserLinksTag () =
|
||||||
"""<ul class="navbar-nav flex-grow-1 justify-content-end">"""
|
"""<ul class="navbar-nav flex-grow-1 justify-content-end">"""
|
||||||
match Convert.ToBoolean context.Environments[0].["logged_on"] with
|
match Convert.ToBoolean context.Environments[0].["logged_on"] with
|
||||||
| true ->
|
| 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 "admin/dashboard"}">Dashboard</a></li>"""
|
||||||
$"""<li class="nav-item"><a class="nav-link" href="{link "user/log-off"}">Log Off</a></li>"""
|
$"""<li class="nav-item"><a class="nav-link" href="{link "user/log-off"}">Log Off</a></li>"""
|
||||||
| false ->
|
| false ->
|
||||||
$"""<li class="nav-item"><a class="nav-link" href="{link "user/log-on"}">Log On</a></li>"""
|
$"""<li class="nav-item"><a class="nav-link" href="{link "user/log-on"}">Log On</a></li>"""
|
||||||
|
@ -184,3 +206,31 @@ type ValueFilter () =
|
||||||
| None -> $"-- {name} not found --"
|
| None -> $"-- {name} not found --"
|
||||||
|
|
||||||
|
|
||||||
|
open System.Collections.Generic
|
||||||
|
open Microsoft.AspNetCore.Antiforgery
|
||||||
|
|
||||||
|
/// Register custom filters/tags and safe types
|
||||||
|
let register () =
|
||||||
|
[ typeof<AbsoluteLinkFilter>; typeof<CategoryLinkFilter>; typeof<EditPageLinkFilter>; typeof<EditPostLinkFilter>
|
||||||
|
typeof<NavLinkFilter>; typeof<RelativeLinkFilter>; typeof<TagLinkFilter>; typeof<ThemeAssetFilter>
|
||||||
|
typeof<ValueFilter>
|
||||||
|
]
|
||||||
|
|> List.iter Template.RegisterFilter
|
||||||
|
|
||||||
|
Template.RegisterTag<PageHeadTag> "page_head"
|
||||||
|
Template.RegisterTag<PageFootTag> "page_foot"
|
||||||
|
Template.RegisterTag<UserLinksTag> "user_links"
|
||||||
|
|
||||||
|
[ // Domain types
|
||||||
|
typeof<CustomFeed>; typeof<MetaItem>; typeof<Page>; typeof<RssOptions>; typeof<TagMap>; typeof<WebLog>
|
||||||
|
// View models
|
||||||
|
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
|
||||||
|
typeof<EditCategoryModel>; typeof<EditCustomFeedModel>; typeof<EditPageModel>; typeof<EditPostModel>
|
||||||
|
typeof<EditRssModel>; typeof<EditTagMapModel>; typeof<EditUserModel>; typeof<LogOnModel>
|
||||||
|
typeof<ManagePermalinksModel>; typeof<PostDisplay>; typeof<PostListItem>; typeof<SettingsModel>
|
||||||
|
typeof<UserMessage>
|
||||||
|
// Framework types
|
||||||
|
typeof<AntiforgeryTokenSet>; typeof<int option>; typeof<KeyValuePair>; typeof<MetaItem list>
|
||||||
|
typeof<string list>; typeof<string option>; typeof<TagMap list>
|
||||||
|
]
|
||||||
|
|> List.iter (fun it -> Template.RegisterSafeType (it, [| "*" |]))
|
||||||
|
|
|
@ -291,19 +291,11 @@ let saveSettings : HttpHandler = fun next ctx -> task {
|
||||||
let! model = ctx.BindFormAsync<SettingsModel> ()
|
let! model = ctx.BindFormAsync<SettingsModel> ()
|
||||||
match! Data.WebLog.findById webLog.id conn with
|
match! Data.WebLog.findById webLog.id conn with
|
||||||
| Some webLog ->
|
| Some webLog ->
|
||||||
let updated =
|
let webLog = model.update webLog
|
||||||
{ webLog with
|
do! Data.WebLog.updateSettings webLog conn
|
||||||
name = model.name
|
|
||||||
subtitle = if model.subtitle = "" then None else Some model.subtitle
|
|
||||||
defaultPage = model.defaultPage
|
|
||||||
postsPerPage = model.postsPerPage
|
|
||||||
timeZone = model.timeZone
|
|
||||||
themePath = model.themePath
|
|
||||||
}
|
|
||||||
do! Data.WebLog.updateSettings updated conn
|
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
WebLogCache.set updated
|
WebLogCache.set webLog
|
||||||
|
|
||||||
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 (WebLog.relativeUrl webLog (Permalink "admin")) next ctx
|
return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin")) next ctx
|
||||||
|
|
|
@ -79,6 +79,11 @@ let private deriveWebLogFromHash (hash : Hash) (ctx : HttpContext) =
|
||||||
hash["web_log"] :?> WebLog
|
hash["web_log"] :?> WebLog
|
||||||
|
|
||||||
open Giraffe
|
open Giraffe
|
||||||
|
open Giraffe.Htmx
|
||||||
|
open Giraffe.ViewEngine
|
||||||
|
|
||||||
|
/// htmx script tag
|
||||||
|
let private htmxScript = RenderView.AsString.htmlNode Htmx.Script.minified
|
||||||
|
|
||||||
/// Render a view for the specified theme, using the specified template, layout, and hash
|
/// Render a view for the specified theme, using the specified template, layout, and hash
|
||||||
let viewForTheme theme template next ctx = fun (hash : Hash) -> task {
|
let viewForTheme theme template next ctx = fun (hash : Hash) -> task {
|
||||||
|
@ -90,6 +95,7 @@ let viewForTheme theme template next ctx = fun (hash : Hash) -> task {
|
||||||
hash.Add ("current_page", ctx.Request.Path.Value.Substring 1)
|
hash.Add ("current_page", ctx.Request.Path.Value.Substring 1)
|
||||||
hash.Add ("messages", messages)
|
hash.Add ("messages", messages)
|
||||||
hash.Add ("generator", generator ctx)
|
hash.Add ("generator", generator ctx)
|
||||||
|
hash.Add ("htmx_script", htmxScript)
|
||||||
|
|
||||||
do! commitSession ctx
|
do! commitSession ctx
|
||||||
|
|
||||||
|
@ -101,7 +107,9 @@ let viewForTheme theme template next ctx = fun (hash : Hash) -> task {
|
||||||
hash.Add ("content", contentTemplate.Render hash)
|
hash.Add ("content", contentTemplate.Render hash)
|
||||||
|
|
||||||
// ...then render that content with its layout
|
// ...then render that content with its layout
|
||||||
let! layoutTemplate = TemplateCache.get theme "layout"
|
let isHtmx = ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh
|
||||||
|
let layout = if isHtmx then "layout-partial" else "layout"
|
||||||
|
let! layoutTemplate = TemplateCache.get theme layout
|
||||||
|
|
||||||
return! htmlString (layoutTemplate.Render hash) next ctx
|
return! htmlString (layoutTemplate.Render hash) next ctx
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,11 +95,11 @@ let router : HttpHandler = choose [
|
||||||
]
|
]
|
||||||
subRoute "/admin" (requireUser >=> choose [
|
subRoute "/admin" (requireUser >=> choose [
|
||||||
GET >=> choose [
|
GET >=> choose [
|
||||||
route "" >=> Admin.dashboard
|
|
||||||
subRoute "/categor" (choose [
|
subRoute "/categor" (choose [
|
||||||
route "ies" >=> Admin.listCategories
|
route "ies" >=> Admin.listCategories
|
||||||
routef "y/%s/edit" Admin.editCategory
|
routef "y/%s/edit" Admin.editCategory
|
||||||
])
|
])
|
||||||
|
route "/dashboard" >=> Admin.dashboard
|
||||||
subRoute "/page" (choose [
|
subRoute "/page" (choose [
|
||||||
route "s" >=> Admin.listPages 1
|
route "s" >=> Admin.listPages 1
|
||||||
routef "s/page/%i" Admin.listPages
|
routef "s/page/%i" Admin.listPages
|
||||||
|
|
|
@ -56,7 +56,8 @@ let doLogOn : HttpHandler = 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 (defaultArg model.returnTo (WebLog.relativeUrl webLog (Permalink "admin"))) next ctx
|
return! redirectToGet (defaultArg model.returnTo (WebLog.relativeUrl webLog (Permalink "admin/dashboard")))
|
||||||
|
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
|
||||||
|
|
126
src/MyWebLog/Maintenance.fs
Normal file
126
src/MyWebLog/Maintenance.fs
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
module MyWebLog.Maintenance
|
||||||
|
|
||||||
|
open System
|
||||||
|
open Microsoft.Extensions.DependencyInjection
|
||||||
|
open RethinkDb.Driver.Net
|
||||||
|
|
||||||
|
|
||||||
|
open System.IO
|
||||||
|
open RethinkDb.Driver.FSharp
|
||||||
|
|
||||||
|
/// Create the web log information
|
||||||
|
let private doCreateWebLog (args : string[]) (sp : IServiceProvider) = task {
|
||||||
|
|
||||||
|
let conn = sp.GetRequiredService<IConnection> ()
|
||||||
|
|
||||||
|
let timeZone =
|
||||||
|
let local = TimeZoneInfo.Local.Id
|
||||||
|
match TimeZoneInfo.Local.HasIanaId with
|
||||||
|
| true -> local
|
||||||
|
| false ->
|
||||||
|
match TimeZoneInfo.TryConvertWindowsIdToIanaId local with
|
||||||
|
| true, ianaId -> ianaId
|
||||||
|
| false, _ -> raise <| TimeZoneNotFoundException $"Cannot find IANA timezone for {local}"
|
||||||
|
|
||||||
|
// Create the web log
|
||||||
|
let webLogId = WebLogId.create ()
|
||||||
|
let userId = WebLogUserId.create ()
|
||||||
|
let homePageId = PageId.create ()
|
||||||
|
|
||||||
|
do! Data.WebLog.add
|
||||||
|
{ WebLog.empty with
|
||||||
|
id = webLogId
|
||||||
|
name = args[2]
|
||||||
|
urlBase = args[1]
|
||||||
|
defaultPage = PageId.toString homePageId
|
||||||
|
timeZone = timeZone
|
||||||
|
} conn
|
||||||
|
|
||||||
|
// Create the admin user
|
||||||
|
let salt = Guid.NewGuid ()
|
||||||
|
|
||||||
|
do! Data.WebLogUser.add
|
||||||
|
{ WebLogUser.empty with
|
||||||
|
id = userId
|
||||||
|
webLogId = webLogId
|
||||||
|
userName = args[3]
|
||||||
|
firstName = "Admin"
|
||||||
|
lastName = "User"
|
||||||
|
preferredName = "Admin"
|
||||||
|
passwordHash = Handlers.User.hashedPassword args[4] args[3] salt
|
||||||
|
salt = salt
|
||||||
|
authorizationLevel = Administrator
|
||||||
|
} conn
|
||||||
|
|
||||||
|
// Create the default home page
|
||||||
|
do! Data.Page.add
|
||||||
|
{ Page.empty with
|
||||||
|
id = homePageId
|
||||||
|
webLogId = webLogId
|
||||||
|
authorId = userId
|
||||||
|
title = "Welcome to myWebLog!"
|
||||||
|
permalink = Permalink "welcome-to-myweblog.html"
|
||||||
|
publishedOn = DateTime.UtcNow
|
||||||
|
updatedOn = DateTime.UtcNow
|
||||||
|
text = "<p>This is your default home page.</p>"
|
||||||
|
revisions = [
|
||||||
|
{ asOf = DateTime.UtcNow
|
||||||
|
text = Html "<p>This is your default home page.</p>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} conn
|
||||||
|
|
||||||
|
printfn $"Successfully initialized database for {args[2]} with URL base {args[1]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new web log
|
||||||
|
let createWebLog args sp = task {
|
||||||
|
match args |> Array.length with
|
||||||
|
| 5 -> return! doCreateWebLog args sp
|
||||||
|
| _ ->
|
||||||
|
printfn "Usage: MyWebLog init [url] [name] [admin-email] [admin-pw]"
|
||||||
|
return! System.Threading.Tasks.Task.CompletedTask
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import prior permalinks from a text files with lines in the format "[old] [new]"
|
||||||
|
let importPriorPermalinks urlBase file (sp : IServiceProvider) = task {
|
||||||
|
let conn = sp.GetRequiredService<IConnection> ()
|
||||||
|
|
||||||
|
match! Data.WebLog.findByHost urlBase conn with
|
||||||
|
| Some webLog ->
|
||||||
|
|
||||||
|
let mapping =
|
||||||
|
File.ReadAllLines file
|
||||||
|
|> Seq.ofArray
|
||||||
|
|> Seq.map (fun it ->
|
||||||
|
let parts = it.Split " "
|
||||||
|
Permalink parts[0], Permalink parts[1])
|
||||||
|
|
||||||
|
for old, current in mapping do
|
||||||
|
match! Data.Post.findByPermalink current webLog.id conn with
|
||||||
|
| Some post ->
|
||||||
|
let! withLinks = rethink<Post> {
|
||||||
|
withTable Data.Table.Post
|
||||||
|
get post.id
|
||||||
|
result conn
|
||||||
|
}
|
||||||
|
do! rethink {
|
||||||
|
withTable Data.Table.Post
|
||||||
|
get post.id
|
||||||
|
update [ "priorPermalinks", old :: withLinks.priorPermalinks :> obj]
|
||||||
|
write; ignoreResult conn
|
||||||
|
}
|
||||||
|
printfn $"{Permalink.toString old} -> {Permalink.toString current}"
|
||||||
|
| None -> printfn $"Cannot find current post for {Permalink.toString current}"
|
||||||
|
printfn "Done!"
|
||||||
|
| None -> printfn $"No web log found at {urlBase}"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import permalinks if all is well
|
||||||
|
let importPermalinks args sp = task {
|
||||||
|
match args |> Array.length with
|
||||||
|
| 3 -> return! importPriorPermalinks args[1] args[2] sp
|
||||||
|
| _ ->
|
||||||
|
printfn "Usage: MyWebLog import-permalinks [url] [file-name]"
|
||||||
|
return! System.Threading.Tasks.Task.CompletedTask
|
||||||
|
}
|
|
@ -17,12 +17,15 @@
|
||||||
<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="DotLiquidBespoke.fs" />
|
||||||
|
<Compile Include="Maintenance.fs" />
|
||||||
<Compile Include="Program.fs" />
|
<Compile Include="Program.fs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DotLiquid" Version="2.2.656" />
|
<PackageReference Include="DotLiquid" Version="2.2.656" />
|
||||||
<PackageReference Include="Giraffe" Version="6.0.0" />
|
<PackageReference Include="Giraffe" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Giraffe.Htmx" Version="1.7.0" />
|
||||||
|
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.7.0" />
|
||||||
<PackageReference Include="RethinkDB.DistributedCache" Version="1.0.0-rc1" />
|
<PackageReference Include="RethinkDB.DistributedCache" Version="1.0.0-rc1" />
|
||||||
<PackageReference Update="FSharp.Core" Version="6.0.4" />
|
<PackageReference Update="FSharp.Core" Version="6.0.4" />
|
||||||
<PackageReference Include="System.ServiceModel.Syndication" Version="6.0.0" />
|
<PackageReference Include="System.ServiceModel.Syndication" Version="6.0.0" />
|
||||||
|
|
|
@ -23,148 +23,17 @@ type WebLogMiddleware (next : RequestDelegate, log : ILogger<WebLogMiddleware>)
|
||||||
ctx.Response.StatusCode <- 404
|
ctx.Response.StatusCode <- 404
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
open System
|
open System
|
||||||
open Microsoft.Extensions.DependencyInjection
|
|
||||||
open RethinkDb.Driver.Net
|
|
||||||
|
|
||||||
/// Create the default information for a new web log
|
|
||||||
module NewWebLog =
|
|
||||||
|
|
||||||
open System.IO
|
|
||||||
open RethinkDb.Driver.FSharp
|
|
||||||
|
|
||||||
/// Create the web log information
|
|
||||||
let private createWebLog (args : string[]) (sp : IServiceProvider) = task {
|
|
||||||
|
|
||||||
let conn = sp.GetRequiredService<IConnection> ()
|
|
||||||
|
|
||||||
let timeZone =
|
|
||||||
let local = TimeZoneInfo.Local.Id
|
|
||||||
match TimeZoneInfo.Local.HasIanaId with
|
|
||||||
| true -> local
|
|
||||||
| false ->
|
|
||||||
match TimeZoneInfo.TryConvertWindowsIdToIanaId local with
|
|
||||||
| true, ianaId -> ianaId
|
|
||||||
| false, _ -> raise <| TimeZoneNotFoundException $"Cannot find IANA timezone for {local}"
|
|
||||||
|
|
||||||
// Create the web log
|
|
||||||
let webLogId = WebLogId.create ()
|
|
||||||
let userId = WebLogUserId.create ()
|
|
||||||
let homePageId = PageId.create ()
|
|
||||||
|
|
||||||
do! Data.WebLog.add
|
|
||||||
{ WebLog.empty with
|
|
||||||
id = webLogId
|
|
||||||
name = args[2]
|
|
||||||
urlBase = args[1]
|
|
||||||
defaultPage = PageId.toString homePageId
|
|
||||||
timeZone = timeZone
|
|
||||||
} conn
|
|
||||||
|
|
||||||
// Create the admin user
|
|
||||||
let salt = Guid.NewGuid ()
|
|
||||||
|
|
||||||
do! Data.WebLogUser.add
|
|
||||||
{ WebLogUser.empty with
|
|
||||||
id = userId
|
|
||||||
webLogId = webLogId
|
|
||||||
userName = args[3]
|
|
||||||
firstName = "Admin"
|
|
||||||
lastName = "User"
|
|
||||||
preferredName = "Admin"
|
|
||||||
passwordHash = Handlers.User.hashedPassword args[4] args[3] salt
|
|
||||||
salt = salt
|
|
||||||
authorizationLevel = Administrator
|
|
||||||
} conn
|
|
||||||
|
|
||||||
// Create the default home page
|
|
||||||
do! Data.Page.add
|
|
||||||
{ Page.empty with
|
|
||||||
id = homePageId
|
|
||||||
webLogId = webLogId
|
|
||||||
authorId = userId
|
|
||||||
title = "Welcome to myWebLog!"
|
|
||||||
permalink = Permalink "welcome-to-myweblog.html"
|
|
||||||
publishedOn = DateTime.UtcNow
|
|
||||||
updatedOn = DateTime.UtcNow
|
|
||||||
text = "<p>This is your default home page.</p>"
|
|
||||||
revisions = [
|
|
||||||
{ asOf = DateTime.UtcNow
|
|
||||||
text = Html "<p>This is your default home page.</p>"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
} conn
|
|
||||||
|
|
||||||
printfn $"Successfully initialized database for {args[2]} with URL base {args[1]}"
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new web log
|
|
||||||
let create args sp = task {
|
|
||||||
match args |> Array.length with
|
|
||||||
| 5 -> return! createWebLog args sp
|
|
||||||
| _ ->
|
|
||||||
printfn "Usage: MyWebLog init [url] [name] [admin-email] [admin-pw]"
|
|
||||||
return! System.Threading.Tasks.Task.CompletedTask
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Import prior permalinks from a text files with lines in the format "[old] [new]"
|
|
||||||
let importPriorPermalinks urlBase file (sp : IServiceProvider) = task {
|
|
||||||
let conn = sp.GetRequiredService<IConnection> ()
|
|
||||||
|
|
||||||
match! Data.WebLog.findByHost urlBase conn with
|
|
||||||
| Some webLog ->
|
|
||||||
|
|
||||||
let mapping =
|
|
||||||
File.ReadAllLines file
|
|
||||||
|> Seq.ofArray
|
|
||||||
|> Seq.map (fun it ->
|
|
||||||
let parts = it.Split " "
|
|
||||||
Permalink parts[0], Permalink parts[1])
|
|
||||||
|
|
||||||
for old, current in mapping do
|
|
||||||
match! Data.Post.findByPermalink current webLog.id conn with
|
|
||||||
| Some post ->
|
|
||||||
let! withLinks = rethink<Post> {
|
|
||||||
withTable Data.Table.Post
|
|
||||||
get post.id
|
|
||||||
result conn
|
|
||||||
}
|
|
||||||
do! rethink {
|
|
||||||
withTable Data.Table.Post
|
|
||||||
get post.id
|
|
||||||
update [ "priorPermalinks", old :: withLinks.priorPermalinks :> obj]
|
|
||||||
write; ignoreResult conn
|
|
||||||
}
|
|
||||||
printfn $"{Permalink.toString old} -> {Permalink.toString current}"
|
|
||||||
| None -> printfn $"Cannot find current post for {Permalink.toString current}"
|
|
||||||
printfn "Done!"
|
|
||||||
| None -> printfn $"No web log found at {urlBase}"
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Import permalinks if all is well
|
|
||||||
let importPermalinks args sp = task {
|
|
||||||
match args |> Array.length with
|
|
||||||
| 3 -> return! importPriorPermalinks args[1] args[2] sp
|
|
||||||
| _ ->
|
|
||||||
printfn "Usage: MyWebLog import-permalinks [url] [file-name]"
|
|
||||||
return! System.Threading.Tasks.Task.CompletedTask
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
open System.Collections.Generic
|
|
||||||
open DotLiquid
|
|
||||||
open DotLiquidBespoke
|
|
||||||
open Giraffe
|
open Giraffe
|
||||||
open Giraffe.EndpointRouting
|
open Giraffe.EndpointRouting
|
||||||
open Microsoft.AspNetCore.Antiforgery
|
|
||||||
open Microsoft.AspNetCore.Authentication.Cookies
|
open Microsoft.AspNetCore.Authentication.Cookies
|
||||||
open Microsoft.AspNetCore.Builder
|
open Microsoft.AspNetCore.Builder
|
||||||
open Microsoft.AspNetCore.HttpOverrides
|
open Microsoft.AspNetCore.HttpOverrides
|
||||||
open Microsoft.Extensions.Configuration
|
open Microsoft.Extensions.Configuration
|
||||||
open MyWebLog.ViewModels
|
open Microsoft.Extensions.DependencyInjection
|
||||||
open RethinkDB.DistributedCache
|
open RethinkDB.DistributedCache
|
||||||
open RethinkDb.Driver.FSharp
|
open RethinkDb.Driver.FSharp
|
||||||
|
open RethinkDb.Driver.Net
|
||||||
|
|
||||||
[<EntryPoint>]
|
[<EntryPoint>]
|
||||||
let main args =
|
let main args =
|
||||||
|
@ -210,36 +79,15 @@ let main args =
|
||||||
let _ = builder.Services.AddGiraffe ()
|
let _ = builder.Services.AddGiraffe ()
|
||||||
|
|
||||||
// Set up DotLiquid
|
// Set up DotLiquid
|
||||||
[ typeof<AbsoluteLinkFilter>; typeof<CategoryLinkFilter>; typeof<EditPageLinkFilter>; typeof<EditPostLinkFilter>
|
DotLiquidBespoke.register ()
|
||||||
typeof<NavLinkFilter>; typeof<RelativeLinkFilter>; typeof<TagLinkFilter>; typeof<ThemeAssetFilter>
|
|
||||||
typeof<ValueFilter>
|
|
||||||
]
|
|
||||||
|> List.iter Template.RegisterFilter
|
|
||||||
|
|
||||||
Template.RegisterTag<PageHeadTag> "page_head"
|
|
||||||
Template.RegisterTag<UserLinksTag> "user_links"
|
|
||||||
|
|
||||||
[ // Domain types
|
|
||||||
typeof<CustomFeed>; typeof<MetaItem>; typeof<Page>; typeof<RssOptions>; typeof<TagMap>; typeof<WebLog>
|
|
||||||
// View models
|
|
||||||
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
|
|
||||||
typeof<EditCategoryModel>; typeof<EditCustomFeedModel>; typeof<EditPageModel>; typeof<EditPostModel>
|
|
||||||
typeof<EditRssModel>; typeof<EditTagMapModel>; typeof<EditUserModel>; typeof<LogOnModel>
|
|
||||||
typeof<ManagePermalinksModel>; typeof<PostDisplay>; typeof<PostListItem>; typeof<SettingsModel>
|
|
||||||
typeof<UserMessage>
|
|
||||||
// Framework types
|
|
||||||
typeof<AntiforgeryTokenSet>; typeof<int option>; typeof<KeyValuePair>; typeof<MetaItem list>
|
|
||||||
typeof<string list>; typeof<string option>; typeof<TagMap list>
|
|
||||||
]
|
|
||||||
|> List.iter (fun it -> Template.RegisterSafeType (it, [| "*" |]))
|
|
||||||
|
|
||||||
let app = builder.Build ()
|
let app = builder.Build ()
|
||||||
|
|
||||||
match args |> Array.tryHead with
|
match args |> Array.tryHead with
|
||||||
| Some it when it = "init" ->
|
| Some it when it = "init" ->
|
||||||
NewWebLog.create args app.Services |> Async.AwaitTask |> Async.RunSynchronously
|
Maintenance.createWebLog args app.Services |> Async.AwaitTask |> Async.RunSynchronously
|
||||||
| Some it when it = "import-permalinks" ->
|
| Some it when it = "import-permalinks" ->
|
||||||
NewWebLog.importPermalinks args app.Services |> Async.AwaitTask |> Async.RunSynchronously
|
Maintenance.importPermalinks args app.Services |> Async.AwaitTask |> Async.RunSynchronously
|
||||||
| _ ->
|
| _ ->
|
||||||
let _ = app.UseForwardedHeaders ()
|
let _ = app.UseForwardedHeaders ()
|
||||||
let _ = app.UseCookiePolicy (CookiePolicyOptions (MinimumSameSitePolicy = SameSiteMode.Strict))
|
let _ = app.UseCookiePolicy (CookiePolicyOptions (MinimumSameSitePolicy = SameSiteMode.Strict))
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"hostname": "data02.bitbadger.solutions",
|
"hostname": "data02.bitbadger.solutions",
|
||||||
"database": "myWebLog_dev"
|
"database": "myWebLog_dev"
|
||||||
},
|
},
|
||||||
"Generator": "myWebLog 2.0-alpha19",
|
"Generator": "myWebLog 2.0-alpha20",
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"MyWebLog.Handlers": "Debug"
|
"MyWebLog.Handlers": "Debug"
|
||||||
|
|
62
src/MyWebLog/themes/admin/layout-partial.liquid
Normal file
62
src/MyWebLog/themes/admin/layout-partial.liquid
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{{ page_title | escape }} « Admin « {{ web_log.name | escape }}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<nav class="navbar navbar-dark bg-dark navbar-expand-md justify-content-start px-2 position-fixed top-0 w-100">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="{{ "" | relative_link }}" hx-boost="false">{{ web_log.name }}</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText"
|
||||||
|
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarText">
|
||||||
|
{% if logged_on -%}
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
{{ "admin/dashboard" | nav_link: "Dashboard" }}
|
||||||
|
{{ "admin/pages" | nav_link: "Pages" }}
|
||||||
|
{{ "admin/posts" | nav_link: "Posts" }}
|
||||||
|
{{ "admin/categories" | nav_link: "Categories" }}
|
||||||
|
{{ "admin/settings" | nav_link: "Settings" }}
|
||||||
|
</ul>
|
||||||
|
{%- endif %}
|
||||||
|
<ul class="navbar-nav flex-grow-1 justify-content-end">
|
||||||
|
{% if logged_on -%}
|
||||||
|
{{ "admin/user/edit" | nav_link: "Edit User" }}
|
||||||
|
{{ "user/log-off" | nav_link: "Log Off" }}
|
||||||
|
{%- else -%}
|
||||||
|
{{ "user/log-on" | nav_link: "Log On" }}
|
||||||
|
{%- endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main class="mx-3">
|
||||||
|
{% if messages %}
|
||||||
|
<div class="messages mt-2">
|
||||||
|
{% for msg in messages %}
|
||||||
|
<div role="alert" class="alert alert-{{ msg.level }} alert-dismissible fade show">
|
||||||
|
{{ msg.message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
{% if msg.detail %}
|
||||||
|
<hr>
|
||||||
|
{{ msg.detail.value }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{{ content }}
|
||||||
|
</main>
|
||||||
|
<footer class="position-fixed bottom-0 w-100">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 text-end"><img src="/img/logo-light.png" alt="myWebLog" width="120" height="34"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -8,11 +8,11 @@
|
||||||
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" href="/themes/admin/admin.css">
|
<link rel="stylesheet" href="/themes/admin/admin.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body hx-boost="true">
|
||||||
<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="{{ "" | relative_link }}">{{ web_log.name }}</a>
|
<a class="navbar-brand" href="{{ "" | relative_link }}" hx-boost="false">{{ 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>
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
<div class="collapse navbar-collapse" id="navbarText">
|
<div class="collapse navbar-collapse" id="navbarText">
|
||||||
{% if logged_on -%}
|
{% if logged_on -%}
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
{{ "admin" | nav_link: "Dashboard" }}
|
{{ "admin/dashboard" | nav_link: "Dashboard" }}
|
||||||
{{ "admin/pages" | nav_link: "Pages" }}
|
{{ "admin/pages" | nav_link: "Pages" }}
|
||||||
{{ "admin/posts" | nav_link: "Posts" }}
|
{{ "admin/posts" | nav_link: "Posts" }}
|
||||||
{{ "admin/categories" | nav_link: "Categories" }}
|
{{ "admin/categories" | nav_link: "Categories" }}
|
||||||
|
@ -66,6 +66,7 @@
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
|
||||||
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
|
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
{{ htmx_script }}
|
||||||
<script>
|
<script>
|
||||||
const cssLoaded = [...document.styleSheets].filter(it => it.href.indexOf("bootstrap.min.css") > -1).length > 0
|
const cssLoaded = [...document.styleSheets].filter(it => it.href.indexOf("bootstrap.min.css") > -1).length > 0
|
||||||
if (!cssLoaded) {
|
if (!cssLoaded) {
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
<label for="postsPerPage">Posts per Page</label>
|
<label for="postsPerPage">Posts per Page</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4 col-xl-3 offset-xl-1 pb-3">
|
<div class="col-12 col-md-4 col-xl-3 pb-3">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<select name="themePath" id="themePath" class="form-control" required>
|
<select name="themePath" id="themePath" class="form-control" required>
|
||||||
{% for theme in themes -%}
|
{% for theme in themes -%}
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
<label for="timeZone">Time Zone</label>
|
<label for="timeZone">Time Zone</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4 col-xl-4 pb-3">
|
<div class="col-12 col-md-4 pb-3">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<select name="defaultPage" id="defaultPage" class="form-control" required>
|
<select name="defaultPage" id="defaultPage" class="form-control" required>
|
||||||
{% for pg in pages -%}
|
{% for pg in pages -%}
|
||||||
|
@ -59,6 +59,16 @@
|
||||||
<label for="defaultPage">Default Page</label>
|
<label for="defaultPage">Default Page</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 col-md-4 col-xl-2">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" name="autoHtmx" id="autoHtmx" class="form-check-input" value="true"
|
||||||
|
{%- if model.auto_htmx %} checked="checked"{% endif %}>
|
||||||
|
<label for="autoHtmx" class="form-check-label">Auto-Load htmx</label>
|
||||||
|
</div>
|
||||||
|
<span class="form-text fst-italic">
|
||||||
|
<a href="https://htmx.org" target="_blank" rel="noopener">What is this?</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row pb-3">
|
<div class="row pb-3">
|
||||||
<div class="col text-center">
|
<div class="col text-center">
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<div>
|
<div>
|
||||||
<small>
|
<small>
|
||||||
{% if logged_on -%}
|
{% if logged_on -%}
|
||||||
<a href="{{ "admin" | relative_link }}">Dashboard</a> ~
|
<a href="{{ "admin/dashboard" | relative_link }}">Dashboard</a> ~
|
||||||
<a href="{{ "user/log-off" | relative_link }}">Log Off</a>
|
<a href="{{ "user/log-off" | relative_link }}">Log Off</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ "user/log-on" | relative_link }}">Log On</a>
|
<a href="{{ "user/log-on" | relative_link }}">Log On</a>
|
||||||
|
|
|
@ -139,7 +139,7 @@
|
||||||
</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" | relative_link }}">Dashboard</a>
|
<a href="{{ "admin/dashboard" | relative_link }}">Dashboard</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ "user/log-on" | relative_link }}">Log On</a>
|
<a href="{{ "user/log-on" | relative_link }}">Log On</a>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
14
src/MyWebLog/themes/tech-blog/layout-partial.liquid
Normal file
14
src/MyWebLog/themes/tech-blog/layout-partial.liquid
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>
|
||||||
|
{%- if is_home -%}
|
||||||
|
{{ web_log.name }}{% if web_log.subtitle %} | {{ web_log.subtitle.value }}{% endif %}
|
||||||
|
{%- else -%}
|
||||||
|
{{ page_title | strip_html }}{% if page_title and page_title != "" %} » {% endif %}{{ web_log.name }}
|
||||||
|
{%- endif -%}
|
||||||
|
</title>
|
||||||
|
</head>
|
||||||
|
<body>{{ content }}</body>
|
||||||
|
</html>
|
|
@ -14,15 +14,21 @@
|
||||||
{% page_head -%}
|
{% page_head -%}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header" id="top">
|
||||||
<div class="header-logo">
|
<div class="header-logo">
|
||||||
<a href="{{ "" | relative_link }}">
|
<a href="{{ "" | relative_link }}" hx-boost="true" hx-target="#content" hx-swap="innerHTML show:#top:top"
|
||||||
|
hx-indicator="#loadOverlay">
|
||||||
<img src="{{ "img/bitbadger.png" | theme_asset }}"
|
<img src="{{ "img/bitbadger.png" | theme_asset }}"
|
||||||
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="{{ "" | relative_link }}">The Bit Badger Blog</a></div>
|
<div class="header-title">
|
||||||
|
<a href="{{ "" | relative_link }}" hx-boost="true" hx-target="#content" hx-swap="innerHTML show:#top:top"
|
||||||
|
hx-indicator="#loadOverlay">
|
||||||
|
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" | relative_link }}" title="Subscribe to The Bit Badger Blog via RSS">
|
<a href="{{ "feed.xml" | relative_link }}" title="Subscribe to The Bit Badger Blog via RSS">
|
||||||
|
@ -36,8 +42,12 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="content-wrapper">
|
<div class="content-wrapper" hx-boost="true" hx-target="#content" hx-swap="innerHTML show:#top:top"
|
||||||
<main class="content" role="main">
|
hx-indicator="#loadOverlay">
|
||||||
|
<div class="load-overlay" id="loadOverlay">
|
||||||
|
<h1>Loading…</h1>
|
||||||
|
</div>
|
||||||
|
<main class="content" id="content" role="main">
|
||||||
{{ content }}
|
{{ content }}
|
||||||
</main>
|
</main>
|
||||||
<aside class="blog-sidebar">
|
<aside class="blog-sidebar">
|
||||||
|
@ -77,5 +87,6 @@
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</span>
|
</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
{% page_foot %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -204,6 +204,64 @@ li {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.load-overlay {
|
||||||
|
position: fixed;
|
||||||
|
display: block;
|
||||||
|
width: 90%;
|
||||||
|
height: 0;
|
||||||
|
z-index: 2000;
|
||||||
|
background-color: rgba(0, 0, 0, .5);
|
||||||
|
border-radius: 1rem;
|
||||||
|
animation: fadeOut .25s ease-in-out;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.load-overlay h1 {
|
||||||
|
color: white;
|
||||||
|
background-color: rgba(0, 0, 0, .75);
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
border-radius: 1rem;
|
||||||
|
width: 50%;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.load-overlay.htmx-request {
|
||||||
|
display: block;
|
||||||
|
height: 80vh;
|
||||||
|
animation: fadeIn .25s ease-in-out;
|
||||||
|
}
|
||||||
|
@keyframes fadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
height: 80vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeOut {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
height: 80vh;
|
||||||
|
}
|
||||||
|
99% {
|
||||||
|
opacity: 0;
|
||||||
|
height: 80vh;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media all and (min-width: 68rem) {
|
||||||
|
.content {
|
||||||
|
width: 66rem;
|
||||||
|
}
|
||||||
|
.load-overlay {
|
||||||
|
width: 60rem;
|
||||||
|
left: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
.home-title {
|
.home-title {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user