WIP on endpoint routing conversion (#59)

This commit is contained in:
2026-07-01 23:43:48 -04:00
parent cb9ec52a7d
commit be094fb07e
8 changed files with 118 additions and 153 deletions
+2 -2
View File
@@ -11,8 +11,8 @@
<PackageVersion Include="Fluid.Core" Version="2.31.0" /> <PackageVersion Include="Fluid.Core" Version="2.31.0" />
<PackageVersion Include="FSharp.Core" Version="10.1.301" /> <PackageVersion Include="FSharp.Core" Version="10.1.301" />
<PackageVersion Include="Giraffe" Version="8.2.0" /> <PackageVersion Include="Giraffe" Version="8.2.0" />
<PackageVersion Include="Giraffe.Htmx" Version="4.0.0-beta4" /> <PackageVersion Include="Giraffe.Htmx" Version="4.0.0-beta5" />
<PackageVersion Include="Giraffe.ViewEngine.Htmx" Version="4.0.0-beta4" /> <PackageVersion Include="Giraffe.ViewEngine.Htmx" Version="4.0.0-beta5" />
<PackageVersion Include="Markdig" Version="1.3.2" /> <PackageVersion Include="Markdig" Version="1.3.2" />
<PackageVersion Include="Markdown.ColorCode" Version="3.0.1" /> <PackageVersion Include="Markdown.ColorCode" Version="3.0.1" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.9" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.9" />
+7 -7
View File
@@ -96,13 +96,13 @@ module Category =
open MyWebLog.Data open MyWebLog.Data
// GET /admin/categories // GET /admin/categories
let all : HttpHandler = fun next ctx -> let all : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
let response = fun next ctx -> let response = fun next ctx ->
adminPage "Categories" next ctx (Views.WebLog.categoryList (ctx.Request.Query.ContainsKey "new")) adminPage "Categories" next ctx (Views.WebLog.categoryList (ctx.Request.Query.ContainsKey "new"))
(withHxPushUrl (ctx.WebLog.RelativeUrl (Permalink "admin/categories")) >=> response) next ctx (withHxPushUrl (ctx.WebLog.RelativeUrl (Permalink "admin/categories")) >=> response) next ctx
// GET /admin/category/{id}/edit // GET /admin/category/{id}/edit
let edit catId : HttpHandler = fun next ctx -> task { let edit catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
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" })
@@ -166,11 +166,11 @@ module RedirectRules =
open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Http
// GET /admin/settings/redirect-rules // GET /admin/settings/redirect-rules
let all : HttpHandler = fun next ctx -> let all : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
adminPage "Redirect Rules" next ctx (Views.WebLog.redirectList ctx.WebLog.RedirectRules) adminPage "Redirect Rules" next ctx (Views.WebLog.redirectList ctx.WebLog.RedirectRules)
// GET /admin/settings/redirect-rules/[index] // GET /admin/settings/redirect-rules/[index]
let edit idx : HttpHandler = fun next ctx -> let edit idx : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
let titleAndView = let titleAndView =
if idx = -1 then if idx = -1 then
Some ("Add", Views.WebLog.redirectEdit (EditRedirectRuleModel.FromRule -1 RedirectRule.Empty)) Some ("Add", Views.WebLog.redirectEdit (EditRedirectRuleModel.FromRule -1 RedirectRule.Empty))
@@ -245,13 +245,13 @@ module RedirectRules =
module TagMapping = module TagMapping =
// GET /admin/settings/tag-mappings // GET /admin/settings/tag-mappings
let all : HttpHandler = fun next ctx -> task { let all : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let! mappings = ctx.Data.TagMap.FindByWebLog ctx.WebLog.Id let! mappings = ctx.Data.TagMap.FindByWebLog ctx.WebLog.Id
return! adminBarePage "Tag Mapping List" next ctx (Views.WebLog.tagMapList mappings) return! adminBarePage "Tag Mapping List" next ctx (Views.WebLog.tagMapList mappings)
} }
// GET /admin/settings/tag-mapping/{id}/edit // GET /admin/settings/tag-mapping/{id}/edit
let edit tagMapId : HttpHandler = fun next ctx -> task { let edit tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let isNew = tagMapId = "new" let isNew = tagMapId = "new"
let tagMap = let tagMap =
if isNew then someTask { TagMap.Empty with Id = TagMapId "new" } if isNew then someTask { TagMap.Empty with Id = TagMapId "new" }
@@ -452,7 +452,7 @@ module WebLog =
open System.IO open System.IO
// GET /admin/settings // GET /admin/settings
let settings : HttpHandler = fun next ctx -> task { let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let data = ctx.Data let data = ctx.Data
let! allPages = data.Page.All ctx.WebLog.Id let! allPages = data.Page.All ctx.WebLog.Id
let pages = let pages =
+18 -15
View File
@@ -96,22 +96,25 @@ let preparePostList webLog posts listType (url: string) pageNbr perPage (data: I
open Giraffe 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 count = ctx.WebLog.PostsPerPage if ctx.Request.Path.Value.EndsWith "/" then
let data = ctx.Data return! redirectTo true (ctx.WebLog.RelativeUrl(Permalink $"page/{pageNbr}")) next ctx
let! posts = data.Post.FindPageOfPublishedPosts ctx.WebLog.Id pageNbr count else
let! viewCtx = preparePostList ctx.WebLog posts PostList "" pageNbr count data let count = ctx.WebLog.PostsPerPage
let title = let data = ctx.Data
match pageNbr, ctx.WebLog.DefaultPage with let! posts = data.Post.FindPageOfPublishedPosts ctx.WebLog.Id pageNbr count
| 1, "posts" -> None let! viewCtx = preparePostList ctx.WebLog posts PostList "" pageNbr count data
| _, "posts" -> Some $"Page {pageNbr}" let title =
| _, _ -> Some $"Page {pageNbr} &laquo; Posts" match pageNbr, ctx.WebLog.DefaultPage with
return! | 1, "posts" -> None
{ viewCtx with | _, "posts" -> Some $"Page {pageNbr}"
PageTitle = defaultArg title viewCtx.PageTitle | _, _ -> Some $"Page {pageNbr} &laquo; Posts"
IsHome = pageNbr = 1 && ctx.WebLog.DefaultPage = "posts" } return!
|> themedView "index" next ctx { viewCtx with
PageTitle = defaultArg title viewCtx.PageTitle
IsHome = pageNbr = 1 && ctx.WebLog.DefaultPage = "posts" }
|> themedView "index" next ctx
} }
// GET /page/{pageNbr}/ // GET /page/{pageNbr}/
+65 -106
View File
@@ -1,10 +1,8 @@
/// Routes for this application /// Routes for this application
module MyWebLog.Handlers.Routes module MyWebLog.Handlers.Routes
open System.Reflection
open Giraffe open Giraffe
open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Http
open Microsoft.Extensions.FileProviders
open MyWebLog open MyWebLog
/// Module to resolve routes that do not match any other known route (web blog content) /// Module to resolve routes that do not match any other known route (web blog content)
@@ -13,18 +11,12 @@ module CatchAll =
open System.IO open System.IO
open MyWebLog.ViewModels open MyWebLog.ViewModels
/// URLs for the embedded htmx scripts
let htmxUrls = [ "giraffe.htmx.common/htmx.min.js"; "giraffe.htmx.common/htmax.min.js" ]
/// Sequence where the first returned value is the proper handler for the link /// Sequence where the first returned value is the proper handler for the link
let private deriveAction (ctx: HttpContext) : HttpHandler seq = let private deriveAction (ctx: HttpContext) : HttpHandler seq =
let webLog = ctx.WebLog let webLog = ctx.WebLog
let data = ctx.Data let data = ctx.Data
let debug = debug "Routes.CatchAll" ctx let debug = debug "Routes.CatchAll" ctx
let textLink = let textLink = string ctx.Request.Path
let extra = webLog.ExtraPath
let url = string ctx.Request.Path
(if extra = "" then url else url[extra.Length..]).ToLowerInvariant()
let await it = (Async.AwaitTask >> Async.RunSynchronously) it let await it = (Async.AwaitTask >> Async.RunSynchronously) it
seq { seq {
// Static file // Static file
@@ -41,32 +33,6 @@ module CatchAll =
|> System.DateTimeOffset |> System.DateTimeOffset
|> Some |> Some
|> streamFile true staticFileName None |> streamFile true staticFileName None
// Embedded htmx library
if htmxUrls |> List.exists textLink.EndsWith then
debug (fun () -> "Embedded htmx script")
let asm = Assembly.Load "Giraffe.Htmx.Common"
asm.GetManifestResourceNames() |> Array.iter (fun it -> debug (fun () -> $"Resource {it}"))
let name = textLink.Split("/") |> Array.last |> sprintf "Giraffe.Htmx.Common/%s"
use fs = asm.GetManifestResourceStream name
if isNull fs then
debug (fun () -> $"No stream found for {name}")
else
debug (fun () -> $"Found {name}")
yield streamData true fs None None
// let files = EmbeddedFileProvider(Assembly.Load "Giraffe.Htmx.Common")
// let cont = files.GetDirectoryContents "."
// if cont.Exists then
// debug (fun () -> "Contents found in file provider, here comes the list")
// debug (fun () -> $"%A{cont}")
// cont |> Seq.iter (fun it -> debug (fun () -> "Path = " + it.PhysicalPath))
// else
// debug (fun () -> "No contents found in file provider")
// let file = files.GetFileInfo (textLink.Replace("giraffe.htmx.common", "Giraffe.Htmx.Common"))
// debug (fun () -> $"File info created; {textLink} exists = {file.Exists}")
// if file.Exists then
// use contents = file.CreateReadStream()
// yield streamData true contents None None
//yield None
// Home page directory without the directory slash // Home page directory without the directory slash
if textLink = "" then yield redirectTo true (webLog.RelativeUrl Permalink.Empty) if textLink = "" then yield redirectTo true (webLog.RelativeUrl Permalink.Empty)
let permalink = Permalink textLink[1..] let permalink = Permalink textLink[1..]
@@ -149,61 +115,7 @@ module Asset =
/// The primary myWebLog router /// The primary myWebLog router
let router : HttpHandler = choose [ let router : HttpHandler = choose [
GET_HEAD >=> choose [
route "/" >=> Post.home
]
subRoute "/admin" (requireUser >=> choose [ subRoute "/admin" (requireUser >=> choose [
GET_HEAD >=> choose [
route "/administration" >=> Admin.Dashboard.admin
subRoute "/categor" (requireAccess WebLogAdmin >=> choose [
route "ies" >=> Admin.Category.all
routef "y/%s/edit" Admin.Category.edit
])
route "/dashboard" >=> Admin.Dashboard.user
route "/my-info" >=> User.myInfo
subRoute "/page" (choose [
route "s" >=> Page.all 1
routef "s/page/%i" Page.all
routef "/%s/edit" Page.edit
routef "/%s/permalinks" Page.editPermalinks
routef "/%s/revision/%s/preview" Page.previewRevision
routef "/%s/revisions" Page.editRevisions
])
subRoute "/post" (choose [
route "s" >=> Post.all 1
routef "s/page/%i" Post.all
routef "/%s/edit" Post.edit
routef "/%s/permalinks" Post.editPermalinks
routef "/%s/revision/%s/preview" Post.previewRevision
routef "/%s/revisions" Post.editRevisions
routef "/%s/chapter/%i" Post.editChapter
routef "/%s/chapters" Post.manageChapters
])
subRoute "/settings" (requireAccess WebLogAdmin >=> choose [
route "" >=> Admin.WebLog.settings
routef "/rss/%s/edit" Feed.editCustomFeed
subRoute "/redirect-rules" (choose [
route "" >=> Admin.RedirectRules.all
routef "/%i" Admin.RedirectRules.edit
])
subRoute "/tag-mapping" (choose [
route "s" >=> Admin.TagMapping.all
routef "/%s/edit" Admin.TagMapping.edit
])
subRoute "/user" (choose [
route "s" >=> User.all
routef "/%s/edit" User.edit
])
])
subRoute "/theme" (choose [
route "/list" >=> Admin.Theme.all
route "/new" >=> Admin.Theme.add
])
subRoute "/upload" (choose [
route "s" >=> Upload.list
route "/new" >=> Upload.showNew
])
]
POST >=> validateCsrf >=> choose [ POST >=> validateCsrf >=> choose [
subRoute "/cache" (choose [ subRoute "/cache" (choose [
routef "/theme/%s/refresh" Admin.Cache.refreshTheme routef "/theme/%s/refresh" Admin.Cache.refreshTheme
@@ -271,16 +183,10 @@ let router : HttpHandler = choose [
] ]
]) ])
GET_HEAD >=> routexp "/category/(.*)" Post.pageOfCategorizedPosts GET_HEAD >=> routexp "/category/(.*)" Post.pageOfCategorizedPosts
GET_HEAD >=> routef "/page/%i" Post.pageOfPosts
GET_HEAD >=> routef "/page/%i/" Post.redirectToPageOfPosts
GET_HEAD >=> routexp "/tag/(.*)" Post.pageOfTaggedPosts GET_HEAD >=> routexp "/tag/(.*)" Post.pageOfTaggedPosts
GET_HEAD >=> routexp "/themes/(.*)" Asset.serve GET_HEAD >=> routexp "/themes/(.*)" Asset.serve
GET_HEAD >=> routexp "/upload/(.*)" Upload.serve GET_HEAD >=> routexp "/upload/(.*)" Upload.serve
subRoute "/user" (choose [ subRoute "/user" (choose [
GET_HEAD >=> choose [
route "/log-on" >=> User.logOn None
route "/log-off" >=> User.logOff
]
POST >=> validateCsrf >=> choose [ POST >=> validateCsrf >=> choose [
route "/log-on" >=> User.doLogOn route "/log-on" >=> User.doLogOn
] ]
@@ -289,17 +195,70 @@ let router : HttpHandler = choose [
//Error.notFound //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 ->
let extraPath = ctx.WebLog.ExtraPath
(if extraPath = "" then router else routerWithPath extraPath) next ctx
open Giraffe.EndpointRouting open Giraffe.EndpointRouting
/// Endpoint-routed handler to deal with sub-routes /// Endpoint-routed handler to deal with sub-routes
let endpoint = [ route "{**url}" handleRoute ] let endpoints = [
GET_HEAD [ route "/" Post.home ]
subRoute "/admin" [
GET_HEAD [
route "/administration" Admin.Dashboard.admin
subRoute "/categor" [
route "ies" Admin.Category.all
routef "y/%s/edit" Admin.Category.edit
]
route "/dashboard" Admin.Dashboard.user
route "/my-info" User.myInfo
subRoute "/page" [
route "s" (Page.all 1)
routef "s/page/%i" Page.all
routef "/%s/edit" Page.edit
routef "/%s/permalinks" Page.editPermalinks
routef "/%s/revision/%s/preview" Page.previewRevision
routef "/%s/revisions" Page.editRevisions
]
subRoute "/post" [
route "s" (Post.all 1)
routef "s/page/%i" Post.all
routef "/%s/edit" Post.edit
routef "/%s/permalinks" Post.editPermalinks
routef "/%s/revision/%s/preview" Post.previewRevision
routef "/%s/revisions" Post.editRevisions
routef "/%s/chapter/%i" Post.editChapter
routef "/%s/chapters" Post.manageChapters
]
subRoute "/settings" [
route "" Admin.WebLog.settings
routef "/rss/%s/edit" Feed.editCustomFeed
subRoute "/redirect-rules" [
route "" Admin.RedirectRules.all
routef "/%i" Admin.RedirectRules.edit
]
subRoute "/tag-mapping" [
route "s" Admin.TagMapping.all
routef "/%s/edit" Admin.TagMapping.edit
]
subRoute "/user" [
route "s" User.all
routef "/%s/edit" User.edit
]
]
subRoute "/theme" [
route "/list" Admin.Theme.all
route "/new" Admin.Theme.add
]
subRoute "/upload" [
route "s" Upload.list
route "/new" Upload.showNew
]
]
]
subRoute "/user" [
GET_HEAD [ route "/log-on" (User.logOn None) ]
POST [
route "/log-off" User.logOff
]
]
GET_HEAD [ routef "/page/%i" Post.pageOfPosts ]
route "{**url}" router
]
+2 -2
View File
@@ -89,7 +89,7 @@ open Giraffe.Htmx
let private goAway : HttpHandler = RequestErrors.BAD_REQUEST "really?" let private goAway : HttpHandler = RequestErrors.BAD_REQUEST "really?"
// GET /admin/settings/users // GET /admin/settings/users
let all : HttpHandler = fun next ctx -> task { let all : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let! users = ctx.Data.WebLogUser.FindByWebLog ctx.WebLog.Id let! users = ctx.Data.WebLogUser.FindByWebLog ctx.WebLog.Id
return! adminBarePage "User Administration" next ctx (Views.User.userList users) return! adminBarePage "User Administration" next ctx (Views.User.userList users)
} }
@@ -99,7 +99,7 @@ let private showEdit (model: EditUserModel) : HttpHandler = fun next ctx ->
adminBarePage (if model.IsNew then "Add a New User" else "Edit User") next ctx (Views.User.edit model) adminBarePage (if model.IsNew then "Add a New User" else "Edit User") next ctx (Views.User.edit model)
// GET /admin/settings/user/{id}/edit // GET /admin/settings/user/{id}/edit
let edit usrId : HttpHandler = fun next ctx -> task { let edit usrId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let isNew = usrId = "new" let isNew = usrId = "new"
let userId = WebLogUserId usrId let userId = WebLogUserId usrId
let tryUser = let tryUser =
+4 -1
View File
@@ -20,6 +20,9 @@ type WebLogMiddleware(next: RequestDelegate, log: ILogger<WebLogMiddleware>) =
ctx.Items["webLog"] <- webLog ctx.Items["webLog"] <- webLog
if PageListCache.exists ctx then () else do! PageListCache.update ctx if PageListCache.exists ctx then () else do! PageListCache.update ctx
if CategoryCache.exists ctx then () else do! CategoryCache.update ctx if CategoryCache.exists ctx then () else do! CategoryCache.update ctx
if webLog.ExtraPath <> "" then
ctx.Request.PathBase <- PathString(webLog.ExtraPath)
ctx.Request.Path <- PathString(ctx.Request.Path.Value[webLog.ExtraPath.Length ..])
return! next.Invoke ctx return! next.Invoke ctx
| None -> | None ->
if isDebug then log.LogDebug $"No resolved web log for {path}" if isDebug then log.LogDebug $"No resolved web log for {path}"
@@ -242,7 +245,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.endpoint let _ = app.UseGiraffe Handlers.Routes.endpoints
app.Run() app.Run()
} }
+2 -1
View File
@@ -174,6 +174,7 @@ let options () =
it it
open Giraffe.Htmx.Common
/// <summary>Fluid parser customized with myWebLog filters and tags</summary> /// <summary>Fluid parser customized with myWebLog filters and tags</summary>
let parser = let parser =
@@ -255,7 +256,7 @@ let parser =
fun writer encoder context -> fun writer encoder context ->
let webLog = context.App.WebLog let webLog = context.App.WebLog
if webLog.AutoHtmx then if webLog.AutoHtmx then
context.App.WebLog.RelativeUrl(Permalink "htmx.min.js") context.App.WebLog.RelativeUrl(Permalink StaticAssetUrl.htmx[1..])
|> sprintf "%s<script src=\"%s\"></script>" s |> sprintf "%s<script src=\"%s\"></script>" s
|> writer.WriteLine |> writer.WriteLine
if assetExists "script.js" webLog then if assetExists "script.js" webLog then
+3 -4
View File
@@ -2,6 +2,7 @@
[<AutoOpen>] [<AutoOpen>]
module MyWebLog.Views.Helpers module MyWebLog.Views.Helpers
open Giraffe.Htmx
open Giraffe.ViewEngine open Giraffe.ViewEngine
open Giraffe.ViewEngine.Accessibility open Giraffe.ViewEngine.Accessibility
open Giraffe.ViewEngine.Htmx open Giraffe.ViewEngine.Htmx
@@ -243,7 +244,7 @@ module Layout =
] ]
if app.IsLoggedOn then if app.IsLoggedOn then
li [ _class "nav-item" ] [ li [ _class "nav-item" ] [
a [ _class "nav-link"; _href (relUrl app "user/log-off"); _hxNoBoost ] [ a [ _class "nav-link"; _hxPost (relUrl app "user/log-off"); _hxNoBoost ] [
raw "Log Off" raw "Log Off"
] ]
] ]
@@ -317,8 +318,7 @@ module Layout =
script [ _src "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" script [ _src "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
_integrity "sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" _integrity "sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
_crossorigin "anonymous" ] [] _crossorigin "anonymous" ] []
//script [ _src (relUrl app "htmx.min.js") ] [] script [ _src (relUrl app StaticAssetUrl.htmx[1..]) ] []
Script.local
script [ _src (relUrl app "themes/admin/admin.js") ] [] script [ _src (relUrl app "themes/admin/admin.js") ] []
] ]
] ]
@@ -335,7 +335,6 @@ module Layout =
// ~~ SHARED TEMPLATES BETWEEN POSTS AND PAGES // ~~ SHARED TEMPLATES BETWEEN POSTS AND PAGES
open Giraffe.Htmx.Common
/// <summary>The round-trip instant pattern</summary> /// <summary>The round-trip instant pattern</summary>
let roundTrip = InstantPattern.CreateWithInvariantCulture "uuuu'-'MM'-'dd'T'HH':'mm':'ss'.'fffffff" let roundTrip = InstantPattern.CreateWithInvariantCulture "uuuu'-'MM'-'dd'T'HH':'mm':'ss'.'fffffff"