| @@ -107,6 +107,142 @@ Class AdminController_Class | |||||
| Response.Redirect "/admin/posts" | Response.Redirect "/admin/posts" | ||||
| End Sub | End Sub | ||||
| '--------------------------------------------------------------- | |||||
| ' Action: GenerateAIContent | |||||
| '--------------------------------------------------------------- | |||||
| Public Sub GenerateAIContent(ByVal id) | |||||
| Dim post | |||||
| On Error Resume Next | |||||
| Set post = PostsRepository().FindByID(id) | |||||
| If Err.Number <> 0 Then | |||||
| Err.Clear | |||||
| On Error GoTo 0 | |||||
| Flash().AddError "Post not found." | |||||
| Response.Redirect "/admin/posts" | |||||
| Exit Sub | |||||
| End If | |||||
| On Error GoTo 0 | |||||
| Dim generatedSummary, generatedBody | |||||
| On Error Resume Next | |||||
| GeneratePostContentFromAI post, generatedSummary, generatedBody | |||||
| If Err.Number <> 0 Then | |||||
| Flash().AddError "AI content generation failed: " & Err.Description | |||||
| Err.Clear | |||||
| On Error GoTo 0 | |||||
| Response.Redirect PostEditUrl(post.PostID) | |||||
| Exit Sub | |||||
| End If | |||||
| On Error GoTo 0 | |||||
| If Len(Trim(generatedSummary)) = 0 Or Len(Trim(generatedBody)) = 0 Then | |||||
| Flash().AddError "AI content generation returned empty content." | |||||
| Response.Redirect PostEditUrl(post.PostID) | |||||
| Exit Sub | |||||
| End If | |||||
| post.Summary = generatedSummary | |||||
| post.Body = generatedBody | |||||
| post.UpdatedDate = Now() | |||||
| PostsRepository().Update post | |||||
| Flash().Success = "AI content generated." | |||||
| Response.Redirect PostEditUrl(post.PostID) | |||||
| End Sub | |||||
| Private Sub GeneratePostContentFromAI(ByRef post, ByRef generatedSummary, ByRef generatedBody) | |||||
| Dim apiKey : apiKey = Trim(CStr(GetAppSetting("AbacusApiKey"))) | |||||
| If Len(apiKey) = 0 Or LCase(apiKey) = "nothing" Then | |||||
| Err.Raise 1, "AdminController.GeneratePostContentFromAI", "Abacus API key is not configured." | |||||
| End If | |||||
| Dim baseUrl : baseUrl = Trim(CStr(GetAppSetting("AbacusApiBaseUrl"))) | |||||
| If Len(baseUrl) = 0 Or LCase(baseUrl) = "nothing" Then baseUrl = "https://routellm.abacus.ai/v1" | |||||
| If Right(baseUrl, 1) = "/" Then baseUrl = Left(baseUrl, Len(baseUrl) - 1) | |||||
| Dim modelName : modelName = Trim(CStr(GetAppSetting("AbacusModel"))) | |||||
| If Len(modelName) = 0 Or LCase(modelName) = "nothing" Then modelName = "route-llm" | |||||
| Dim systemPrompt, userPrompt, payload, responseText, parsed, choices, choice, message, content, contentJson | |||||
| systemPrompt = "You write clear, engaging blog post content for a classic ASP blog. Return only valid JSON with two keys: summary and body. Summary must be 1 to 2 sentences. Body must be 3 to 5 short paragraphs separated by blank lines. Do not use markdown fences, bullets, or code blocks." | |||||
| userPrompt = "Create blog content for this post title: " & SafeText(post.Title) & vbCrLf & _ | |||||
| "Existing summary: " & SafeText(post.Summary) & vbCrLf & _ | |||||
| "Existing body: " & SafeText(post.Body) & vbCrLf & _ | |||||
| "Keep the title unchanged. Make the content readable and helpful for a general audience." | |||||
| payload = "{""model"":""" & JsonEscape(modelName) & """,""messages"":[{""role"":""system"",""content"":""" & JsonEscape(systemPrompt) & """},{""role"":""user"",""content"":""" & JsonEscape(userPrompt) & """}],""temperature"":0.7}" | |||||
| responseText = HttpPostJson(baseUrl & "/chat/completions", apiKey, payload) | |||||
| Set parsed = json() | |||||
| parsed.loadJSON responseText | |||||
| If Not parsed.data.Exists("choices") Then | |||||
| Err.Raise 1, "AdminController.GeneratePostContentFromAI", "Abacus API response did not include choices." | |||||
| End If | |||||
| Set choices = parsed.data.Item("choices") | |||||
| If choices.Count = 0 Then | |||||
| Err.Raise 1, "AdminController.GeneratePostContentFromAI", "Abacus API returned no choices." | |||||
| End If | |||||
| Set choice = choices.Item(0) | |||||
| If Not choice.Exists("message") Then | |||||
| Err.Raise 1, "AdminController.GeneratePostContentFromAI", "Abacus API response did not include a message." | |||||
| End If | |||||
| Set message = choice.Item("message") | |||||
| If Not message.Exists("content") Then | |||||
| Err.Raise 1, "AdminController.GeneratePostContentFromAI", "Abacus API response did not include content." | |||||
| End If | |||||
| content = Trim(CStr(message.Item("content"))) | |||||
| contentJson = ExtractJsonObject(content) | |||||
| Set parsed = json() | |||||
| parsed.loadJSON contentJson | |||||
| If parsed.data.Exists("summary") Then generatedSummary = Trim(CStr(parsed.data.Item("summary"))) | |||||
| If parsed.data.Exists("body") Then generatedBody = Trim(CStr(parsed.data.Item("body"))) | |||||
| End Sub | |||||
| Private Function HttpPostJson(ByVal url, ByVal apiKey, ByVal payload) | |||||
| Dim http | |||||
| Set http = Server.CreateObject("Msxml2.ServerXMLHTTP.6.0") | |||||
| http.setTimeouts 10000, 10000, 10000, 30000 | |||||
| http.open "POST", url, False | |||||
| http.setRequestHeader "Content-Type", "application/json" | |||||
| http.setRequestHeader "Authorization", "Bearer " & apiKey | |||||
| http.send payload | |||||
| If http.status < 200 Or http.status >= 300 Then | |||||
| Err.Raise IIf(http.status > 0, http.status, 1), "AdminController.HttpPostJson", "Abacus API returned HTTP " & http.status & ": " & Left(CStr(http.responseText), 500) | |||||
| End If | |||||
| HttpPostJson = http.responseText | |||||
| End Function | |||||
| Private Function ExtractJsonObject(ByVal text) | |||||
| Dim startPos, endPos | |||||
| startPos = InStr(text, "{") | |||||
| endPos = InStrRev(text, "}") | |||||
| If startPos > 0 And endPos > startPos Then | |||||
| ExtractJsonObject = Mid(text, startPos, endPos - startPos + 1) | |||||
| Else | |||||
| ExtractJsonObject = text | |||||
| End If | |||||
| End Function | |||||
| Private Function SafeText(ByVal value) | |||||
| If IsNull(value) Or IsEmpty(value) Then | |||||
| SafeText = "" | |||||
| Else | |||||
| SafeText = CStr(value) | |||||
| End If | |||||
| End Function | |||||
| End Class | End Class | ||||
| Dim AdminController_Class__Singleton | Dim AdminController_Class__Singleton | ||||
| @@ -48,27 +48,27 @@ Class PostsController_Class | |||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| ' Action: Show | ' Action: Show | ||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| Public Sub Show(ByVal slug) | |||||
| Dim matches | |||||
| Set matches = PostsRepository().Find(Array("Slug", slug, "IsPublished", 1), Empty) | |||||
| If matches.Count = 0 Then | |||||
| Response.Status = "404 Not Found" | |||||
| %> | |||||
| <!--#include file="../views/Error/NotFound.asp" --> | |||||
| Public Sub Show(ByVal slug) | |||||
| Dim matches | |||||
| Set matches = PostsRepository().Find(Array("Slug", slug, "IsPublished", 1), Empty) | |||||
| If matches.Count = 0 Then | |||||
| Response.Status = "404 Not Found" | |||||
| %> | |||||
| <!--#include file="../views/Error/NotFound.asp" --> | |||||
| <% | <% | ||||
| Exit Sub | Exit Sub | ||||
| End If | |||||
| Dim post | |||||
| Set post = matches.Front() | |||||
| Dim comments | |||||
| Set comments = CommentsRepository().Find(Array("PostID", post.PostID, "IsApproved", 1), Array("CreatedDate")) | |||||
| %> | |||||
| <!--#include file="../views/Posts/show.asp" --> | |||||
| <% | |||||
| End Sub | |||||
| End If | |||||
| Dim post | |||||
| Set post = matches.Front() | |||||
| Dim comments | |||||
| Set comments = CommentsRepository().Find(Array("PostID", post.PostID, "IsApproved", 1), "CreatedDate") | |||||
| %> | |||||
| <!--#include file="../views/Posts/show.asp" --> | |||||
| <% | |||||
| End Sub | |||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| ' Action: New | ' Action: New | ||||
| @@ -27,29 +27,29 @@ Class CommentsRepository_Class | |||||
| Set GetAll = Find(Empty, orderBy) | Set GetAll = Find(Empty, orderBy) | ||||
| End Function | End Function | ||||
| Public Function Find(where_kvarray, order_string_or_array) | |||||
| Dim sql : sql = "Select [AuthorEmail], [AuthorName], [Body], [CommentID], [CreatedDate], [IsApproved], [PostID] FROM [Comments]" | |||||
| Dim where_keys, where_values, i | |||||
| If Not IsEmpty(where_kvarray) Then | |||||
| KVUnzip where_kvarray, where_keys, where_values | |||||
| If Not IsEmpty(where_keys) Then | |||||
| sql = sql & " WHERE " | |||||
| For i = 0 To UBound(where_keys) | |||||
| If i > 0 Then sql = sql & " AND " | |||||
| sql = sql & " " & QI(where_keys(i)) & " = ?" | |||||
| Next | |||||
| End If | |||||
| End If | |||||
| sql = sql & BuildOrderBy(order_string_or_array, "[CommentID]") | |||||
| Dim rs : Set rs = DAL.Query(sql, where_values) | |||||
| Dim list : Set list = new LinkedList_Class | |||||
| Do Until rs.EOF | |||||
| list.Push Automapper.AutoMap(rs, "POBO_Comments") | |||||
| rs.MoveNext | |||||
| Loop | |||||
| Set Find = list | |||||
| Destroy rs | |||||
| End Function | |||||
| Public Function Find(where_kvarray, order_string_or_array) | |||||
| Dim sql : sql = "Select [AuthorEmail], [AuthorName], [Body], [CommentID], [CreatedDate], [IsApproved], [PostID] FROM [Comments]" | |||||
| Dim where_keys, where_values, i | |||||
| If Not IsEmpty(where_kvarray) Then | |||||
| KVUnzip where_kvarray, where_keys, where_values | |||||
| If Not IsEmpty(where_keys) Then | |||||
| sql = sql & " WHERE " | |||||
| For i = 0 To UBound(where_keys) | |||||
| If i > 0 Then sql = sql & " AND " | |||||
| sql = sql & " " & QI(where_keys(i)) & " = ?" | |||||
| Next | |||||
| End If | |||||
| End If | |||||
| sql = sql & BuildOrderBy(order_string_or_array, "[CommentID]") | |||||
| Dim rs : Set rs = DAL.Query(sql, where_values) | |||||
| Dim list : Set list = new LinkedList_Class | |||||
| Do Until rs.EOF | |||||
| list.Push Automapper.AutoMap(rs, "POBO_Comments") | |||||
| rs.MoveNext | |||||
| Loop | |||||
| Set Find = list | |||||
| Destroy rs | |||||
| End Function | |||||
| Public Function FindPaged(where_kvarray, order_string_or_array, per_page, page_num, ByRef page_count, ByRef record_count) | Public Function FindPaged(where_kvarray, order_string_or_array, per_page, page_num, ByRef page_count, ByRef record_count) | ||||
| Dim sql : sql = "Select [AuthorEmail], [AuthorName], [Body], [CommentID], [CreatedDate], [IsApproved], [PostID] FROM [Comments]" | Dim sql : sql = "Select [AuthorEmail], [AuthorName], [Body], [CommentID], [CreatedDate], [IsApproved], [PostID] FROM [Comments]" | ||||
| @@ -3,11 +3,11 @@ | |||||
| <h1 class="h3 mb-1">Manage Categories</h1> | <h1 class="h3 mb-1">Manage Categories</h1> | ||||
| <p class="text-muted mb-0">All categories — edit or delete.</p> | <p class="text-muted mb-0">All categories — edit or delete.</p> | ||||
| </div> | </div> | ||||
| <a class="btn btn-primary" href="/categories/new">New Category</a> | |||||
| <a class="btn btn-primary" href="<%= CategoryNewUrl() %>">New Category</a> | |||||
| </div> | </div> | ||||
| <% If categories.Count = 0 Then %> | <% If categories.Count = 0 Then %> | ||||
| <div class="alert alert-secondary">No categories yet. <a href="/categories/new">Create the first one.</a></div> | |||||
| <div class="alert alert-secondary">No categories yet. <a href="<%= CategoryNewUrl() %>">Create the first one.</a></div> | |||||
| <% Else %> | <% Else %> | ||||
| <div class="table-responsive"> | <div class="table-responsive"> | ||||
| <table class="table table-hover align-middle"> | <table class="table table-hover align-middle"> | ||||
| @@ -27,14 +27,14 @@ | |||||
| %> | %> | ||||
| <tr> | <tr> | ||||
| <td> | <td> | ||||
| <a href="/categories/<%= Server.URLEncode(CStr(adminCatItem.CategoryID)) %>" class="text-decoration-none fw-semibold"> | |||||
| <a href="<%= CategoryUrl(adminCatItem.CategoryID) %>" class="text-decoration-none fw-semibold"> | |||||
| <%= H(adminCatItem.Name) %> | <%= H(adminCatItem.Name) %> | ||||
| </a> | </a> | ||||
| </td> | </td> | ||||
| <td class="text-muted small"><%= H(adminCatItem.Description) %></td> | <td class="text-muted small"><%= H(adminCatItem.Description) %></td> | ||||
| <td class="text-end text-nowrap"> | <td class="text-end text-nowrap"> | ||||
| <a class="btn btn-sm btn-outline-secondary" href="/categories/<%= Server.URLEncode(CStr(adminCatItem.CategoryID)) %>/edit">Edit</a> | |||||
| <form class="d-inline" method="post" action="/categories/<%= H(adminCatItem.CategoryID) %>/delete"> | |||||
| <a class="btn btn-sm btn-outline-secondary" href="<%= CategoryEditUrl(adminCatItem.CategoryID) %>">Edit</a> | |||||
| <form class="d-inline" method="post" action="<%= CategoryDeleteUrl(adminCatItem.CategoryID) %>"> | |||||
| <button class="btn btn-sm btn-outline-danger" type="submit" onclick="return confirm('Delete this category?')">Delete</button> | <button class="btn btn-sm btn-outline-danger" type="submit" onclick="return confirm('Delete this category?')">Delete</button> | ||||
| </form> | </form> | ||||
| </td> | </td> | ||||
| @@ -7,7 +7,7 @@ | |||||
| <div class="row gy-3"> | <div class="row gy-3"> | ||||
| <div class="col-md-6"> | <div class="col-md-6"> | ||||
| <a class="card shadow-sm text-decoration-none h-100" href="/admin/posts"> | |||||
| <a class="card shadow-sm text-decoration-none h-100" href="<%= AdminUrl() %>/posts"> | |||||
| <div class="card-body"> | <div class="card-body"> | ||||
| <h2 class="h5 mb-1">Posts</h2> | <h2 class="h5 mb-1">Posts</h2> | ||||
| <p class="text-muted mb-0">Create, publish, and manage blog posts.</p> | <p class="text-muted mb-0">Create, publish, and manage blog posts.</p> | ||||
| @@ -15,7 +15,7 @@ | |||||
| </a> | </a> | ||||
| </div> | </div> | ||||
| <div class="col-md-6"> | <div class="col-md-6"> | ||||
| <a class="card shadow-sm text-decoration-none h-100" href="/admin/categories"> | |||||
| <a class="card shadow-sm text-decoration-none h-100" href="<%= AdminUrl() %>/categories"> | |||||
| <div class="card-body"> | <div class="card-body"> | ||||
| <h2 class="h5 mb-1">Categories</h2> | <h2 class="h5 mb-1">Categories</h2> | ||||
| <p class="text-muted mb-0">Organize posts into categories.</p> | <p class="text-muted mb-0">Organize posts into categories.</p> | ||||
| @@ -3,11 +3,11 @@ | |||||
| <h1 class="h3 mb-1">Manage Posts</h1> | <h1 class="h3 mb-1">Manage Posts</h1> | ||||
| <p class="text-muted mb-0">All posts — publish, edit, or delete.</p> | <p class="text-muted mb-0">All posts — publish, edit, or delete.</p> | ||||
| </div> | </div> | ||||
| <a class="btn btn-primary" href="/posts/new">New Post</a> | |||||
| <a class="btn btn-primary" href="<%= PostNewUrl() %>">New Post</a> | |||||
| </div> | </div> | ||||
| <% If posts.Count = 0 Then %> | <% If posts.Count = 0 Then %> | ||||
| <div class="alert alert-secondary">No posts yet. <a href="/posts/new">Create the first one.</a></div> | |||||
| <div class="alert alert-secondary">No posts yet. <a href="<%= PostNewUrl() %>">Create the first one.</a></div> | |||||
| <% Else %> | <% Else %> | ||||
| <div class="table-responsive"> | <div class="table-responsive"> | ||||
| <table class="table table-hover align-middle"> | <table class="table table-hover align-middle"> | ||||
| @@ -44,17 +44,20 @@ | |||||
| <%= H(FormatDateTime(adminPostItem.CreatedDate, vbShortDate)) %> | <%= H(FormatDateTime(adminPostItem.CreatedDate, vbShortDate)) %> | ||||
| </td> | </td> | ||||
| <td class="text-end text-nowrap"> | <td class="text-end text-nowrap"> | ||||
| <a class="btn btn-sm btn-outline-secondary" href="/posts/<%= Server.URLEncode(CStr(adminPostItem.PostID)) %>/edit">Edit</a> | |||||
| <a class="btn btn-sm btn-outline-secondary" href="<%= PostEditUrl(adminPostItem.PostID) %>">Edit</a> | |||||
| <form class="d-inline" method="post" action="<%= AdminPostAIUrl(adminPostItem.PostID) %>"> | |||||
| <button class="btn btn-sm btn-outline-info" type="submit">AI Content</button> | |||||
| </form> | |||||
| <% If adminPostItem.IsPublished = 1 Then %> | <% If adminPostItem.IsPublished = 1 Then %> | ||||
| <form class="d-inline" method="post" action="/admin/posts/<%= H(adminPostItem.PostID) %>/unpublish"> | |||||
| <form class="d-inline" method="post" action="<%= AdminPostUnpublishUrl(adminPostItem.PostID) %>"> | |||||
| <button class="btn btn-sm btn-outline-warning" type="submit">Unpublish</button> | <button class="btn btn-sm btn-outline-warning" type="submit">Unpublish</button> | ||||
| </form> | </form> | ||||
| <% Else %> | <% Else %> | ||||
| <form class="d-inline" method="post" action="/admin/posts/<%= H(adminPostItem.PostID) %>/publish"> | |||||
| <form class="d-inline" method="post" action="<%= AdminPostPublishUrl(adminPostItem.PostID) %>"> | |||||
| <button class="btn btn-sm btn-success" type="submit">Publish</button> | <button class="btn btn-sm btn-success" type="submit">Publish</button> | ||||
| </form> | </form> | ||||
| <% End If %> | <% End If %> | ||||
| <form class="d-inline" method="post" action="/posts/<%= H(adminPostItem.PostID) %>/delete"> | |||||
| <form class="d-inline" method="post" action="<%= PostDeleteUrl(adminPostItem.PostID) %>"> | |||||
| <button class="btn btn-sm btn-outline-danger" type="submit" onclick="return confirm('Delete this post?')">Delete</button> | <button class="btn btn-sm btn-outline-danger" type="submit" onclick="return confirm('Delete this post?')">Delete</button> | ||||
| </form> | </form> | ||||
| </td> | </td> | ||||
| @@ -4,7 +4,7 @@ | |||||
| <div class="card-body"> | <div class="card-body"> | ||||
| <h1 class="h3 mb-4">Edit Category</h1> | <h1 class="h3 mb-4">Edit Category</h1> | ||||
| <form method="post" action="/categories/<%= H(category.CategoryID) %>"> | |||||
| <form method="post" action="<%= CategoryUrl(category.CategoryID) %>"> | |||||
| <div class="mb-3"> | <div class="mb-3"> | ||||
| <label class="form-label" for="Name">Name</label> | <label class="form-label" for="Name">Name</label> | ||||
| <input class="form-control" type="text" id="Name" name="Name" value="<%= H(category.Name) %>" required> | <input class="form-control" type="text" id="Name" name="Name" value="<%= H(category.Name) %>" required> | ||||
| @@ -22,11 +22,11 @@ | |||||
| <div class="d-flex flex-wrap gap-2"> | <div class="d-flex flex-wrap gap-2"> | ||||
| <button class="btn btn-primary" type="submit">Update Category</button> | <button class="btn btn-primary" type="submit">Update Category</button> | ||||
| <a class="btn btn-outline-secondary" href="/categories">Cancel</a> | |||||
| <a class="btn btn-outline-secondary" href="<%= CategoriesUrl() %>">Cancel</a> | |||||
| </div> | </div> | ||||
| </form> | </form> | ||||
| <form method="post" action="/categories/<%= H(category.CategoryID) %>/delete" class="mt-3"> | |||||
| <form method="post" action="<%= CategoryDeleteUrl(category.CategoryID) %>" class="mt-3"> | |||||
| <button class="btn btn-outline-danger" type="submit">Delete Category</button> | <button class="btn btn-outline-danger" type="submit">Delete Category</button> | ||||
| </form> | </form> | ||||
| </div> | </div> | ||||
| @@ -3,7 +3,7 @@ | |||||
| <h1 class="h3 mb-1">Categories</h1> | <h1 class="h3 mb-1">Categories</h1> | ||||
| <p class="text-muted mb-0">Browse post categories from ASPBlogBrainOrdure.</p> | <p class="text-muted mb-0">Browse post categories from ASPBlogBrainOrdure.</p> | ||||
| </div> | </div> | ||||
| <a class="btn btn-primary" href="/categories/new">New Category</a> | |||||
| <a class="btn btn-primary" href="<%= CategoryNewUrl() %>">New Category</a> | |||||
| </div> | </div> | ||||
| <% | <% | ||||
| @@ -25,12 +25,12 @@ Else | |||||
| <article class="card shadow-sm"> | <article class="card shadow-sm"> | ||||
| <div class="card-body"> | <div class="card-body"> | ||||
| <h2 class="h5 mb-2"> | <h2 class="h5 mb-2"> | ||||
| <a href="/categories/<%= Server.URLEncode(CStr(categoryItem.CategoryID)) %>" class="text-decoration-none"> | |||||
| <a href="<%= CategoryUrl(categoryItem.CategoryID) %>" class="text-decoration-none"> | |||||
| <%= H(categoryItem.Name) %> | <%= H(categoryItem.Name) %> | ||||
| </a> | </a> | ||||
| </h2> | </h2> | ||||
| <p class="text-muted mb-3"><%= H(categoryItem.Description) %></p> | <p class="text-muted mb-3"><%= H(categoryItem.Description) %></p> | ||||
| <a class="btn btn-sm btn-outline-primary" href="/categories/<%= Server.URLEncode(CStr(categoryItem.CategoryID)) %>">View</a> | |||||
| <a class="btn btn-sm btn-outline-primary" href="<%= CategoryUrl(categoryItem.CategoryID) %>">View</a> | |||||
| </div> | </div> | ||||
| </article> | </article> | ||||
| </div> | </div> | ||||
| @@ -4,7 +4,7 @@ | |||||
| <div class="card-body"> | <div class="card-body"> | ||||
| <h1 class="h3 mb-4">New Category</h1> | <h1 class="h3 mb-4">New Category</h1> | ||||
| <form method="post" action="/categories"> | |||||
| <form method="post" action="<%= CategoriesUrl() %>"> | |||||
| <div class="mb-3"> | <div class="mb-3"> | ||||
| <label class="form-label" for="Name">Name</label> | <label class="form-label" for="Name">Name</label> | ||||
| <input class="form-control" type="text" id="Name" name="Name" required> | <input class="form-control" type="text" id="Name" name="Name" required> | ||||
| @@ -22,7 +22,7 @@ | |||||
| <div class="d-flex gap-2"> | <div class="d-flex gap-2"> | ||||
| <button class="btn btn-primary" type="submit">Create Category</button> | <button class="btn btn-primary" type="submit">Create Category</button> | ||||
| <a class="btn btn-outline-secondary" href="/categories">Cancel</a> | |||||
| <a class="btn btn-outline-secondary" href="<%= CategoriesUrl() %>">Cancel</a> | |||||
| </div> | </div> | ||||
| </form> | </form> | ||||
| </div> | </div> | ||||
| @@ -1,15 +1,15 @@ | |||||
| <article class="card shadow-sm mb-4"> | <article class="card shadow-sm mb-4"> | ||||
| <div class="card-body"> | <div class="card-body"> | ||||
| <div class="mb-3"> | <div class="mb-3"> | ||||
| <a href="/categories" class="small text-decoration-none">← Back to categories</a> | |||||
| <a href="<%= CategoriesUrl() %>" class="small text-decoration-none">← Back to categories</a> | |||||
| </div> | </div> | ||||
| <h1 class="h2 mb-3"><%= H(category.Name) %></h1> | <h1 class="h2 mb-3"><%= H(category.Name) %></h1> | ||||
| <p class="fs-5 text-muted mb-4"><%= H(category.Description) %></p> | <p class="fs-5 text-muted mb-4"><%= H(category.Description) %></p> | ||||
| <div class="d-flex flex-wrap gap-2"> | <div class="d-flex flex-wrap gap-2"> | ||||
| <a class="btn btn-outline-primary" href="/categories/<%= Server.URLEncode(CStr(category.CategoryID)) %>/edit">Edit Category</a> | |||||
| <form method="post" action="/categories/<%= H(category.CategoryID) %>/delete"> | |||||
| <a class="btn btn-outline-primary" href="<%= CategoryEditUrl(category.CategoryID) %>">Edit Category</a> | |||||
| <form method="post" action="<%= CategoryDeleteUrl(category.CategoryID) %>"> | |||||
| <button class="btn btn-outline-danger" type="submit">Delete Category</button> | <button class="btn btn-outline-danger" type="submit">Delete Category</button> | ||||
| </form> | </form> | ||||
| </div> | </div> | ||||
| @@ -32,14 +32,14 @@ | |||||
| <article class="card shadow-sm"> | <article class="card shadow-sm"> | ||||
| <div class="card-body"> | <div class="card-body"> | ||||
| <h3 class="h5 mb-1"> | <h3 class="h5 mb-1"> | ||||
| <a href="/posts/<%= Server.URLEncode(CStr(catPostItem.Slug)) %>" class="text-decoration-none"> | |||||
| <a href="<%= PostUrl(catPostItem.Slug) %>" class="text-decoration-none"> | |||||
| <%= H(catPostItem.Title) %> | <%= H(catPostItem.Title) %> | ||||
| </a> | </a> | ||||
| </h3> | </h3> | ||||
| <% If Len(Trim(CStr(catPostItem.Summary))) > 0 Then %> | <% If Len(Trim(CStr(catPostItem.Summary))) > 0 Then %> | ||||
| <p class="text-muted mb-2 small"><%= H(catPostItem.Summary) %></p> | <p class="text-muted mb-2 small"><%= H(catPostItem.Summary) %></p> | ||||
| <% End If %> | <% End If %> | ||||
| <a class="btn btn-sm btn-outline-primary" href="/posts/<%= Server.URLEncode(CStr(catPostItem.Slug)) %>">Read</a> | |||||
| <a class="btn btn-sm btn-outline-primary" href="<%= PostUrl(catPostItem.Slug) %>">Read</a> | |||||
| </div> | </div> | ||||
| </article> | </article> | ||||
| </div> | </div> | ||||
| @@ -2,7 +2,7 @@ | |||||
| <h1 class="display-5 fw-bold mb-2">BrainOrdure</h1> | <h1 class="display-5 fw-bold mb-2">BrainOrdure</h1> | ||||
| <p class="lead text-muted mb-4">Thoughts, notes, and things worth writing down.</p> | <p class="lead text-muted mb-4">Thoughts, notes, and things worth writing down.</p> | ||||
| <div class="d-flex gap-2"> | <div class="d-flex gap-2"> | ||||
| <a href="/posts" class="btn btn-dark px-4">Read Posts</a> | |||||
| <a href="/categories" class="btn btn-outline-secondary px-4">Browse Categories</a> | |||||
| <a href="<%= PostsUrl() %>" class="btn btn-dark px-4">Read Posts</a> | |||||
| <a href="<%= CategoriesUrl() %>" class="btn btn-outline-secondary px-4">Browse Categories</a> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -4,7 +4,7 @@ | |||||
| <div class="card-body"> | <div class="card-body"> | ||||
| <h1 class="h3 mb-4">Edit Post</h1> | <h1 class="h3 mb-4">Edit Post</h1> | ||||
| <form method="post" action="/posts/<%= H(post.PostID) %>"> | |||||
| <form method="post" action="<%= PostEditUrl(post.PostID) %>"> | |||||
| <div class="mb-3"> | <div class="mb-3"> | ||||
| <label class="form-label" for="Title">Title</label> | <label class="form-label" for="Title">Title</label> | ||||
| <input class="form-control" type="text" id="Title" name="Title" value="<%= H(post.Title) %>" required> | <input class="form-control" type="text" id="Title" name="Title" value="<%= H(post.Title) %>" required> | ||||
| @@ -25,13 +25,14 @@ | |||||
| <input class="form-control" type="number" id="CategoryID" name="CategoryID" min="0" value="<%= H(post.CategoryID) %>"> | <input class="form-control" type="number" id="CategoryID" name="CategoryID" min="0" value="<%= H(post.CategoryID) %>"> | ||||
| </div> | </div> | ||||
| <div class="d-flex flex-wrap gap-2"> | |||||
| <button class="btn btn-primary" type="submit">Update Post</button> | |||||
| <a class="btn btn-outline-secondary" href="/posts">Cancel</a> | |||||
| </div> | |||||
| </form> | |||||
| <div class="d-flex flex-wrap gap-2"> | |||||
| <button class="btn btn-primary" type="submit">Update Post</button> | |||||
| <button class="btn btn-outline-info" type="submit" formaction="<%= AdminPostAIUrl(post.PostID) %>" formmethod="post">AI Content</button> | |||||
| <a class="btn btn-outline-secondary" href="<%= PostsUrl() %>">Cancel</a> | |||||
| </div> | |||||
| </form> | |||||
| <form method="post" action="/posts/<%= H(post.PostID) %>/delete" class="mt-3"> | |||||
| <form method="post" action="<%= PostDeleteUrl(post.PostID) %>" class="mt-3"> | |||||
| <button class="btn btn-outline-danger" type="submit">Delete Post</button> | <button class="btn btn-outline-danger" type="submit">Delete Post</button> | ||||
| </form> | </form> | ||||
| </div> | </div> | ||||
| @@ -3,7 +3,7 @@ | |||||
| <h1 class="h3 mb-1">Posts</h1> | <h1 class="h3 mb-1">Posts</h1> | ||||
| <p class="text-muted mb-0">Published articles from ASPBlogBrainOrdure.</p> | <p class="text-muted mb-0">Published articles from ASPBlogBrainOrdure.</p> | ||||
| </div> | </div> | ||||
| <a class="btn btn-primary" href="/posts/new">New Post</a> | |||||
| <a class="btn btn-primary" href="<%= PostNewUrl() %>">New Post</a> | |||||
| </div> | </div> | ||||
| <% | <% | ||||
| @@ -28,9 +28,9 @@ Else | |||||
| <div class="card-body"> | <div class="card-body"> | ||||
| <div class="d-flex flex-column flex-md-row justify-content-between gap-2"> | <div class="d-flex flex-column flex-md-row justify-content-between gap-2"> | ||||
| <h2 class="h5 mb-1"> | <h2 class="h5 mb-1"> | ||||
| <a href="/posts/<%= Server.URLEncode(CStr(postItem.Slug)) %>" class="text-decoration-none"> | |||||
| <%= H(postItem.Title) %> | |||||
| </a> | |||||
| <a href="<%= PostUrl(postItem.Slug) %>" class="text-decoration-none"> | |||||
| <%= H(postItem.Title) %> | |||||
| </a> | |||||
| </h2> | </h2> | ||||
| <% | <% | ||||
| Dim publishedText | Dim publishedText | ||||
| @@ -49,10 +49,10 @@ Else | |||||
| %> | %> | ||||
| </div> | </div> | ||||
| <p class="text-muted mb-3"><%= H(postItem.Summary) %></p> | <p class="text-muted mb-3"><%= H(postItem.Summary) %></p> | ||||
| <a class="btn btn-sm btn-outline-primary" href="/posts/<%= Server.URLEncode(CStr(postItem.Slug)) %>">Read</a> | |||||
| </div> | |||||
| </article> | |||||
| </div> | |||||
| <a class="btn btn-sm btn-outline-primary" href="<%= PostUrl(postItem.Slug) %>">Read</a> | |||||
| </div> | |||||
| </article> | |||||
| </div> | |||||
| <% | <% | ||||
| Loop | Loop | ||||
| %> | %> | ||||
| @@ -4,7 +4,7 @@ | |||||
| <div class="card-body"> | <div class="card-body"> | ||||
| <h1 class="h3 mb-4">New Post</h1> | <h1 class="h3 mb-4">New Post</h1> | ||||
| <form method="post" action="/posts"> | |||||
| <form method="post" action="<%= PostsUrl() %>"> | |||||
| <div class="mb-3"> | <div class="mb-3"> | ||||
| <label class="form-label" for="Title">Title</label> | <label class="form-label" for="Title">Title</label> | ||||
| <input class="form-control" type="text" id="Title" name="Title" required> | <input class="form-control" type="text" id="Title" name="Title" required> | ||||
| @@ -27,7 +27,7 @@ | |||||
| <div class="d-flex gap-2"> | <div class="d-flex gap-2"> | ||||
| <button class="btn btn-primary" type="submit">Create Post</button> | <button class="btn btn-primary" type="submit">Create Post</button> | ||||
| <a class="btn btn-outline-secondary" href="/posts">Cancel</a> | |||||
| <a class="btn btn-outline-secondary" href="<%= PostsUrl() %>">Cancel</a> | |||||
| </div> | </div> | ||||
| </form> | </form> | ||||
| </div> | </div> | ||||
| @@ -1,7 +1,7 @@ | |||||
| <article class="card shadow-sm mb-4"> | <article class="card shadow-sm mb-4"> | ||||
| <div class="card-body"> | <div class="card-body"> | ||||
| <div class="mb-3"> | <div class="mb-3"> | ||||
| <a href="/posts" class="small text-decoration-none">← Back to posts</a> | |||||
| <a href="<%= PostsUrl() %>" class="small text-decoration-none">← Back to posts</a> | |||||
| </div> | </div> | ||||
| <h1 class="h2 mb-2"><%= H(post.Title) %></h1> | <h1 class="h2 mb-2"><%= H(post.Title) %></h1> | ||||
| @@ -34,22 +34,58 @@ | |||||
| <!-- Comments --> | <!-- Comments --> | ||||
| <section class="mt-2"> | <section class="mt-2"> | ||||
| <h2 class="h4 mb-3">Comments (<%= comments.Count %>)</h2> | |||||
| <% | |||||
| Dim commentsCount, commentsLoadFailed | |||||
| commentsCount = 0 | |||||
| commentsLoadFailed = False | |||||
| <% If comments.Count = 0 Then %> | |||||
| On Error Resume Next | |||||
| If IsObject(comments) Then commentsCount = comments.Count | |||||
| If Err.Number <> 0 Then | |||||
| commentsLoadFailed = True | |||||
| commentsCount = 0 | |||||
| Err.Clear | |||||
| End If | |||||
| On Error GoTo 0 | |||||
| %> | |||||
| <h2 class="h4 mb-3">Comments (<%= commentsCount %>)</h2> | |||||
| <% If commentsLoadFailed Then %> | |||||
| <div class="alert alert-warning mb-4">Comments are temporarily unavailable.</div> | |||||
| <% ElseIf commentsCount = 0 Then %> | |||||
| <p class="text-muted mb-4">No comments yet. Be the first to leave one below.</p> | <p class="text-muted mb-4">No comments yet. Be the first to leave one below.</p> | ||||
| <% Else %> | <% Else %> | ||||
| <% | <% | ||||
| Dim commentIter, commentItem | Dim commentIter, commentItem | ||||
| Dim commentsIterFailed | |||||
| commentsIterFailed = False | |||||
| On Error Resume Next | |||||
| Set commentIter = comments.Iterator() | Set commentIter = comments.Iterator() | ||||
| Do While commentIter.HasNext | |||||
| If Err.Number <> 0 Then | |||||
| commentsIterFailed = True | |||||
| Err.Clear | |||||
| End If | |||||
| Do While Not commentsIterFailed And commentIter.HasNext | |||||
| Set commentItem = commentIter.GetNext() | Set commentItem = commentIter.GetNext() | ||||
| If Err.Number <> 0 Then | |||||
| commentsIterFailed = True | |||||
| Err.Clear | |||||
| Exit Do | |||||
| End If | |||||
| Dim commentDateText | |||||
| commentDateText = "" | |||||
| If IsDate(commentItem.CreatedDate) Then | |||||
| commentDateText = FormatDateTime(commentItem.CreatedDate, vbLongDate) | |||||
| End If | |||||
| %> | %> | ||||
| <div class="card shadow-sm mb-3"> | <div class="card shadow-sm mb-3"> | ||||
| <div class="card-body"> | <div class="card-body"> | ||||
| <div class="d-flex justify-content-between mb-2"> | <div class="d-flex justify-content-between mb-2"> | ||||
| <strong class="small"><%= H(commentItem.AuthorName) %></strong> | <strong class="small"><%= H(commentItem.AuthorName) %></strong> | ||||
| <span class="small text-muted"><%= H(FormatDateTime(commentItem.CreatedDate, vbLongDate)) %></span> | |||||
| <span class="small text-muted"><%= H(commentDateText) %></span> | |||||
| </div> | </div> | ||||
| <% | <% | ||||
| Dim commentBody | Dim commentBody | ||||
| @@ -63,6 +99,17 @@ | |||||
| </div> | </div> | ||||
| <% | <% | ||||
| Loop | Loop | ||||
| If Err.Number <> 0 Then | |||||
| commentsIterFailed = True | |||||
| Err.Clear | |||||
| End If | |||||
| On Error GoTo 0 | |||||
| If commentsIterFailed Then | |||||
| %> | |||||
| <div class="alert alert-warning mb-4">Some comments could not be displayed.</div> | |||||
| <% | |||||
| End If | |||||
| %> | %> | ||||
| <% End If %> | <% End If %> | ||||
| @@ -70,7 +117,7 @@ | |||||
| <div class="card shadow-sm mt-4"> | <div class="card shadow-sm mt-4"> | ||||
| <div class="card-body"> | <div class="card-body"> | ||||
| <h3 class="h5 mb-3">Leave a Comment</h3> | <h3 class="h5 mb-3">Leave a Comment</h3> | ||||
| <form method="post" action="/comments"> | |||||
| <form method="post" action="<%= CommentsUrl() %>"> | |||||
| <input type="hidden" name="PostID" value="<%= H(post.PostID) %>"> | <input type="hidden" name="PostID" value="<%= H(post.PostID) %>"> | ||||
| <input type="hidden" name="PostSlug" value="<%= H(post.Slug) %>"> | <input type="hidden" name="PostSlug" value="<%= H(post.Slug) %>"> | ||||
| <div class="mb-3"> | <div class="mb-3"> | ||||
| @@ -62,13 +62,13 @@ End If | |||||
| <a class="<%= hdr_navHome %>" href="/">Home</a> | <a class="<%= hdr_navHome %>" href="/">Home</a> | ||||
| </li> | </li> | ||||
| <li class="nav-item"> | <li class="nav-item"> | ||||
| <a class="<%= hdr_navPosts %>" href="/posts">Posts</a> | |||||
| <a class="<%= hdr_navPosts %>" href="<%= PostsUrl() %>">Posts</a> | |||||
| </li> | </li> | ||||
| <li class="nav-item"> | <li class="nav-item"> | ||||
| <a class="<%= hdr_navCats %>" href="/categories">Categories</a> | |||||
| <a class="<%= hdr_navCats %>" href="<%= CategoriesUrl() %>">Categories</a> | |||||
| </li> | </li> | ||||
| <li class="nav-item"> | <li class="nav-item"> | ||||
| <a class="<%= hdr_navAdmin %>" href="/admin">Admin</a> | |||||
| <a class="<%= hdr_navAdmin %>" href="<%= AdminUrl() %>">Admin</a> | |||||
| </li> | </li> | ||||
| </ul> | </ul> | ||||
| </div> | </div> | ||||
| @@ -187,6 +187,71 @@ Function H(s) | |||||
| End If | End If | ||||
| End Function | End Function | ||||
| '------------------------------------------------------------------------------ | |||||
| ' Canonical application URL helpers | |||||
| ' - Categories use numeric IDs | |||||
| ' - Posts use slug permalinks for public links and numeric IDs for admin actions | |||||
| '------------------------------------------------------------------------------ | |||||
| Function CategoryUrl(ByVal categoryId) | |||||
| CategoryUrl = "/categories/" & Server.URLEncode(CStr(categoryId)) | |||||
| End Function | |||||
| Function CategoriesUrl() | |||||
| CategoriesUrl = "/categories" | |||||
| End Function | |||||
| Function CategoryNewUrl() | |||||
| CategoryNewUrl = "/categories/new" | |||||
| End Function | |||||
| Function CategoryEditUrl(ByVal categoryId) | |||||
| CategoryEditUrl = CategoryUrl(categoryId) & "/edit" | |||||
| End Function | |||||
| Function CategoryDeleteUrl(ByVal categoryId) | |||||
| CategoryDeleteUrl = CategoryUrl(categoryId) & "/delete" | |||||
| End Function | |||||
| Function PostUrl(ByVal slug) | |||||
| PostUrl = "/posts/" & Server.URLEncode(CStr(slug)) | |||||
| End Function | |||||
| Function PostsUrl() | |||||
| PostsUrl = "/posts" | |||||
| End Function | |||||
| Function PostNewUrl() | |||||
| PostNewUrl = "/posts/new" | |||||
| End Function | |||||
| Function PostEditUrl(ByVal postId) | |||||
| PostEditUrl = "/posts/" & Server.URLEncode(CStr(postId)) & "/edit" | |||||
| End Function | |||||
| Function PostDeleteUrl(ByVal postId) | |||||
| PostDeleteUrl = "/posts/" & Server.URLEncode(CStr(postId)) & "/delete" | |||||
| End Function | |||||
| Function AdminPostPublishUrl(ByVal postId) | |||||
| AdminPostPublishUrl = "/admin/posts/" & Server.URLEncode(CStr(postId)) & "/publish" | |||||
| End Function | |||||
| Function AdminPostUnpublishUrl(ByVal postId) | |||||
| AdminPostUnpublishUrl = "/admin/posts/" & Server.URLEncode(CStr(postId)) & "/unpublish" | |||||
| End Function | |||||
| Function AdminPostAIUrl(ByVal postId) | |||||
| AdminPostAIUrl = "/admin/posts/" & Server.URLEncode(CStr(postId)) & "/ai" | |||||
| End Function | |||||
| Function AdminUrl() | |||||
| AdminUrl = "/admin" | |||||
| End Function | |||||
| Function CommentsUrl() | |||||
| CommentsUrl = "/comments" | |||||
| End Function | |||||
| '======================================================================================================================= | '======================================================================================================================= | ||||
| ' Adapted from Tolerable library | ' Adapted from Tolerable library | ||||
| @@ -533,4 +598,4 @@ Function FormatDateForSql(vbDate) | |||||
| FormatDateForSql = "'" & yyyy & "-" & mm & "-" & dd & " " & hh & ":" & nn & ":" & ss & "'" | FormatDateForSql = "'" & yyyy & "-" & mm & "-" & dd & " " & hh & ":" & nn & ":" & ss & "'" | ||||
| End Function | End Function | ||||
| %> | |||||
| %> | |||||
| @@ -65,7 +65,7 @@ Public Sub AddRoute(method, path, controller, action) | |||||
| End If | End If | ||||
| Dim routeKey | Dim routeKey | ||||
| routeKey = methodUpper & ":" & LCase(Trim(path)) | |||||
| routeKey = methodUpper & ":" & NormalizePath(path) | |||||
| If Not routes.Exists(routeKey) Then | If Not routes.Exists(routeKey) Then | ||||
| routes.Add routeKey, Array(Trim(controller), Trim(action)) | routes.Add routeKey, Array(Trim(controller), Trim(action)) | ||||
| @@ -77,7 +77,7 @@ End Sub | |||||
| '------------------------------------------------------------ | '------------------------------------------------------------ | ||||
| Public Function Resolve(method, path) | Public Function Resolve(method, path) | ||||
| Dim routeKey, routeValue, values | Dim routeKey, routeValue, values | ||||
| routeKey = UCase(method) & ":" & LCase(path) | |||||
| routeKey = UCase(method) & ":" & NormalizePath(path) | |||||
| ' Always return a params array (empty by default) | ' Always return a params array (empty by default) | ||||
| Dim emptyParams() : ReDim emptyParams(-1) | Dim emptyParams() : ReDim emptyParams(-1) | ||||
| @@ -113,6 +113,9 @@ End Function | |||||
| '------------------------------------------------------------ | '------------------------------------------------------------ | ||||
| Private Function IsMatch(requestPath, routePattern, values) | Private Function IsMatch(requestPath, routePattern, values) | ||||
| Dim reqParts, routeParts, i, paramCount | Dim reqParts, routeParts, i, paramCount | ||||
| requestPath = NormalizePath(requestPath) | |||||
| routePattern = NormalizePath(routePattern) | |||||
| reqParts = Split(requestPath, "/") | reqParts = Split(requestPath, "/") | ||||
| routeParts = Split(routePattern, "/") | routeParts = Split(routePattern, "/") | ||||
| @@ -123,7 +126,7 @@ Private Function IsMatch(requestPath, routePattern, values) | |||||
| paramCount = 0 : ReDim values(-1) | paramCount = 0 : ReDim values(-1) | ||||
| For i = 0 To UBound(reqParts) | For i = 0 To UBound(reqParts) | ||||
| If Left(routeParts(i), 1) = ":" Then | |||||
| If IsDynamicSegment(routeParts(i)) Then | |||||
| ReDim Preserve values(paramCount) | ReDim Preserve values(paramCount) | ||||
| values(paramCount) = reqParts(i) | values(paramCount) = reqParts(i) | ||||
| paramCount = paramCount + 1 | paramCount = paramCount + 1 | ||||
| @@ -136,6 +139,51 @@ Private Function IsMatch(requestPath, routePattern, values) | |||||
| IsMatch = True | IsMatch = True | ||||
| End Function | End Function | ||||
| '------------------------------------------------------------ | |||||
| ' INTERNAL NormalizePath(path) | |||||
| ' Removes query strings, trims whitespace, and normalizes | |||||
| ' leading/trailing slashes so "/admin" and "/admin/" match. | |||||
| '------------------------------------------------------------ | |||||
| Private Function NormalizePath(path) | |||||
| Dim cleaned | |||||
| cleaned = LCase(Trim(CStr(path))) | |||||
| If Len(cleaned) = 0 Then | |||||
| NormalizePath = "" | |||||
| Exit Function | |||||
| End If | |||||
| If InStr(cleaned, "?") > 0 Then | |||||
| cleaned = Left(cleaned, InStr(cleaned, "?") - 1) | |||||
| End If | |||||
| Do While Left(cleaned, 1) = "/" | |||||
| cleaned = Mid(cleaned, 2) | |||||
| Loop | |||||
| Do While Right(cleaned, 1) = "/" And Len(cleaned) > 0 | |||||
| cleaned = Left(cleaned, Len(cleaned) - 1) | |||||
| Loop | |||||
| NormalizePath = cleaned | |||||
| End Function | |||||
| '------------------------------------------------------------ | |||||
| ' INTERNAL IsDynamicSegment(segment) | |||||
| ' Supports both ":id" and "{id}" route tokens. | |||||
| '------------------------------------------------------------ | |||||
| Private Function IsDynamicSegment(segment) | |||||
| If Len(segment) = 0 Then | |||||
| IsDynamicSegment = False | |||||
| ElseIf Left(segment, 1) = ":" Then | |||||
| IsDynamicSegment = True | |||||
| ElseIf Left(segment, 1) = "{" And Right(segment, 1) = "}" Then | |||||
| IsDynamicSegment = True | |||||
| Else | |||||
| IsDynamicSegment = False | |||||
| End If | |||||
| End Function | |||||
| '------------------------------------------------------------ | '------------------------------------------------------------ | ||||
| ' Optional lifecycle hooks | ' Optional lifecycle hooks | ||||
| '------------------------------------------------------------ | '------------------------------------------------------------ | ||||
| @@ -35,6 +35,7 @@ | |||||
| router.AddRoute "GET", "/admin/categories", "AdminController", "Categories" | router.AddRoute "GET", "/admin/categories", "AdminController", "Categories" | ||||
| router.AddRoute "POST", "/admin/posts/{id}/publish", "AdminController", "PublishPost" | router.AddRoute "POST", "/admin/posts/{id}/publish", "AdminController", "PublishPost" | ||||
| router.AddRoute "POST", "/admin/posts/{id}/unpublish", "AdminController", "UnpublishPost" | router.AddRoute "POST", "/admin/posts/{id}/unpublish", "AdminController", "UnpublishPost" | ||||
| router.AddRoute "POST", "/admin/posts/{id}/ai", "AdminController", "GenerateAIContent" | |||||
| ' Dispatch the request (resolves route and executes controller action) | ' Dispatch the request (resolves route and executes controller action) | ||||
| MVC.DispatchRequest Request.ServerVariables("REQUEST_METHOD"), _ | MVC.DispatchRequest Request.ServerVariables("REQUEST_METHOD"), _ | ||||
| @@ -39,6 +39,15 @@ | |||||
| <!-- Cache-bust parameter name (default: "v") --> | <!-- Cache-bust parameter name (default: "v") --> | ||||
| <add key="CacheBustParamName" value="v" /> | <add key="CacheBustParamName" value="v" /> | ||||
| <!-- Abacus RouteLLM endpoint for AI-generated post content --> | |||||
| <add key="AbacusApiBaseUrl" value="https://routellm.abacus.ai/v1" /> | |||||
| <!-- Abacus API key (populate locally / via deployment secret) --> | |||||
| <add key="AbacusApiKey" value="" /> | |||||
| <!-- Model name to send to the Abacus RouteLLM API --> | |||||
| <add key="AbacusModel" value="route-llm" /> | |||||
| </appSettings> | </appSettings> | ||||
| <system.webServer> | <system.webServer> | ||||
Powered by TurnKey Linux.