Ver código fonte

Add AI content generation for admin posts

pull/5/head
Nano 6 dias atrás
pai
commit
525b421069
20 arquivos alterados com 410 adições e 100 exclusões
  1. +136
    -0
      app/controllers/AdminController.asp
  2. +19
    -19
      app/controllers/PostsController.asp
  3. +23
    -23
      app/repositories/CommentsRepository.asp
  4. +5
    -5
      app/views/Admin/categories.asp
  5. +2
    -2
      app/views/Admin/index.asp
  6. +9
    -6
      app/views/Admin/posts.asp
  7. +3
    -3
      app/views/Categories/edit.asp
  8. +3
    -3
      app/views/Categories/index.asp
  9. +2
    -2
      app/views/Categories/new.asp
  10. +5
    -5
      app/views/Categories/show.asp
  11. +2
    -2
      app/views/Home/index.asp
  12. +8
    -7
      app/views/Posts/edit.asp
  13. +8
    -8
      app/views/Posts/index.asp
  14. +2
    -2
      app/views/Posts/new.asp
  15. +53
    -6
      app/views/Posts/show.asp
  16. +3
    -3
      app/views/shared/header.asp
  17. +66
    -1
      core/helpers.asp
  18. +51
    -3
      core/router.wsc
  19. +1
    -0
      public/Default.asp
  20. +9
    -0
      public/web.config

+ 136
- 0
app/controllers/AdminController.asp Ver arquivo

@@ -107,6 +107,142 @@ Class AdminController_Class
Response.Redirect "/admin/posts"
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

Dim AdminController_Class__Singleton


+ 19
- 19
app/controllers/PostsController.asp Ver arquivo

@@ -48,27 +48,27 @@ Class PostsController_Class
'---------------------------------------------------------------
' 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
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


+ 23
- 23
app/repositories/CommentsRepository.asp Ver arquivo

@@ -27,29 +27,29 @@ Class CommentsRepository_Class
Set GetAll = Find(Empty, orderBy)
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)
Dim sql : sql = "Select [AuthorEmail], [AuthorName], [Body], [CommentID], [CreatedDate], [IsApproved], [PostID] FROM [Comments]"


+ 5
- 5
app/views/Admin/categories.asp Ver arquivo

@@ -3,11 +3,11 @@
<h1 class="h3 mb-1">Manage Categories</h1>
<p class="text-muted mb-0">All categories &mdash; edit or delete.</p>
</div>
<a class="btn btn-primary" href="/categories/new">New Category</a>
<a class="btn btn-primary" href="<%= CategoryNewUrl() %>">New Category</a>
</div>

<% 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 %>
<div class="table-responsive">
<table class="table table-hover align-middle">
@@ -27,14 +27,14 @@
%>
<tr>
<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) %>
</a>
</td>
<td class="text-muted small"><%= H(adminCatItem.Description) %></td>
<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>
</form>
</td>


+ 2
- 2
app/views/Admin/index.asp Ver arquivo

@@ -7,7 +7,7 @@

<div class="row gy-3">
<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">
<h2 class="h5 mb-1">Posts</h2>
<p class="text-muted mb-0">Create, publish, and manage blog posts.</p>
@@ -15,7 +15,7 @@
</a>
</div>
<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">
<h2 class="h5 mb-1">Categories</h2>
<p class="text-muted mb-0">Organize posts into categories.</p>


+ 9
- 6
app/views/Admin/posts.asp Ver arquivo

@@ -3,11 +3,11 @@
<h1 class="h3 mb-1">Manage Posts</h1>
<p class="text-muted mb-0">All posts &mdash; publish, edit, or delete.</p>
</div>
<a class="btn btn-primary" href="/posts/new">New Post</a>
<a class="btn btn-primary" href="<%= PostNewUrl() %>">New Post</a>
</div>

<% 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 %>
<div class="table-responsive">
<table class="table table-hover align-middle">
@@ -44,17 +44,20 @@
<%= H(FormatDateTime(adminPostItem.CreatedDate, vbShortDate)) %>
</td>
<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 %>
<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>
</form>
<% 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>
</form>
<% 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>
</form>
</td>


+ 3
- 3
app/views/Categories/edit.asp Ver arquivo

@@ -4,7 +4,7 @@
<div class="card-body">
<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">
<label class="form-label" for="Name">Name</label>
<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">
<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>
</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>
</form>
</div>


+ 3
- 3
app/views/Categories/index.asp Ver arquivo

@@ -3,7 +3,7 @@
<h1 class="h3 mb-1">Categories</h1>
<p class="text-muted mb-0">Browse post categories from ASPBlogBrainOrdure.</p>
</div>
<a class="btn btn-primary" href="/categories/new">New Category</a>
<a class="btn btn-primary" href="<%= CategoryNewUrl() %>">New Category</a>
</div>

<%
@@ -25,12 +25,12 @@ Else
<article class="card shadow-sm">
<div class="card-body">
<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) %>
</a>
</h2>
<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>
</article>
</div>


+ 2
- 2
app/views/Categories/new.asp Ver arquivo

@@ -4,7 +4,7 @@
<div class="card-body">
<h1 class="h3 mb-4">New Category</h1>

<form method="post" action="/categories">
<form method="post" action="<%= CategoriesUrl() %>">
<div class="mb-3">
<label class="form-label" for="Name">Name</label>
<input class="form-control" type="text" id="Name" name="Name" required>
@@ -22,7 +22,7 @@

<div class="d-flex gap-2">
<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>
</form>
</div>


+ 5
- 5
app/views/Categories/show.asp Ver arquivo

@@ -1,15 +1,15 @@
<article class="card shadow-sm mb-4">
<div class="card-body">
<div class="mb-3">
<a href="/categories" class="small text-decoration-none">&larr; Back to categories</a>
<a href="<%= CategoriesUrl() %>" class="small text-decoration-none">&larr; Back to categories</a>
</div>

<h1 class="h2 mb-3"><%= H(category.Name) %></h1>
<p class="fs-5 text-muted mb-4"><%= H(category.Description) %></p>

<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>
</form>
</div>
@@ -32,14 +32,14 @@
<article class="card shadow-sm">
<div class="card-body">
<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) %>
</a>
</h3>
<% If Len(Trim(CStr(catPostItem.Summary))) > 0 Then %>
<p class="text-muted mb-2 small"><%= H(catPostItem.Summary) %></p>
<% 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>
</article>
</div>


+ 2
- 2
app/views/Home/index.asp Ver arquivo

@@ -2,7 +2,7 @@
<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>
<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>

+ 8
- 7
app/views/Posts/edit.asp Ver arquivo

@@ -4,7 +4,7 @@
<div class="card-body">
<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">
<label class="form-label" for="Title">Title</label>
<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) %>">
</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>
</form>
</div>


+ 8
- 8
app/views/Posts/index.asp Ver arquivo

@@ -3,7 +3,7 @@
<h1 class="h3 mb-1">Posts</h1>
<p class="text-muted mb-0">Published articles from ASPBlogBrainOrdure.</p>
</div>
<a class="btn btn-primary" href="/posts/new">New Post</a>
<a class="btn btn-primary" href="<%= PostNewUrl() %>">New Post</a>
</div>
<%
@@ -28,9 +28,9 @@ Else
<div class="card-body">
<div class="d-flex flex-column flex-md-row justify-content-between gap-2">
<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>
<%
Dim publishedText
@@ -49,10 +49,10 @@ Else
%>
</div>
<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
%>


+ 2
- 2
app/views/Posts/new.asp Ver arquivo

@@ -4,7 +4,7 @@
<div class="card-body">
<h1 class="h3 mb-4">New Post</h1>
<form method="post" action="/posts">
<form method="post" action="<%= PostsUrl() %>">
<div class="mb-3">
<label class="form-label" for="Title">Title</label>
<input class="form-control" type="text" id="Title" name="Title" required>
@@ -27,7 +27,7 @@
<div class="d-flex gap-2">
<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>
</form>
</div>


+ 53
- 6
app/views/Posts/show.asp Ver arquivo

@@ -1,7 +1,7 @@
<article class="card shadow-sm mb-4">
<div class="card-body">
<div class="mb-3">
<a href="/posts" class="small text-decoration-none">&larr; Back to posts</a>
<a href="<%= PostsUrl() %>" class="small text-decoration-none">&larr; Back to posts</a>
</div>

<h1 class="h2 mb-2"><%= H(post.Title) %></h1>
@@ -34,22 +34,58 @@

<!-- Comments -->
<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>
<% Else %>
<%
Dim commentIter, commentItem
Dim commentsIterFailed
commentsIterFailed = False

On Error Resume Next
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()
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-body">
<div class="d-flex justify-content-between mb-2">
<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>
<%
Dim commentBody
@@ -63,6 +99,17 @@
</div>
<%
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 %>

@@ -70,7 +117,7 @@
<div class="card shadow-sm mt-4">
<div class="card-body">
<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="PostSlug" value="<%= H(post.Slug) %>">
<div class="mb-3">


+ 3
- 3
app/views/shared/header.asp Ver arquivo

@@ -62,13 +62,13 @@ End If
<a class="<%= hdr_navHome %>" href="/">Home</a>
</li>
<li class="nav-item">
<a class="<%= hdr_navPosts %>" href="/posts">Posts</a>
<a class="<%= hdr_navPosts %>" href="<%= PostsUrl() %>">Posts</a>
</li>
<li class="nav-item">
<a class="<%= hdr_navCats %>" href="/categories">Categories</a>
<a class="<%= hdr_navCats %>" href="<%= CategoriesUrl() %>">Categories</a>
</li>
<li class="nav-item">
<a class="<%= hdr_navAdmin %>" href="/admin">Admin</a>
<a class="<%= hdr_navAdmin %>" href="<%= AdminUrl() %>">Admin</a>
</li>
</ul>
</div>


+ 66
- 1
core/helpers.asp Ver arquivo

@@ -187,6 +187,71 @@ Function H(s)
End If
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
@@ -533,4 +598,4 @@ Function FormatDateForSql(vbDate)
FormatDateForSql = "'" & yyyy & "-" & mm & "-" & dd & " " & hh & ":" & nn & ":" & ss & "'"
End Function

%>
%>

+ 51
- 3
core/router.wsc Ver arquivo

@@ -65,7 +65,7 @@ Public Sub AddRoute(method, path, controller, action)
End If

Dim routeKey
routeKey = methodUpper & ":" & LCase(Trim(path))
routeKey = methodUpper & ":" & NormalizePath(path)

If Not routes.Exists(routeKey) Then
routes.Add routeKey, Array(Trim(controller), Trim(action))
@@ -77,7 +77,7 @@ End Sub
'------------------------------------------------------------
Public Function Resolve(method, path)
Dim routeKey, routeValue, values
routeKey = UCase(method) & ":" & LCase(path)
routeKey = UCase(method) & ":" & NormalizePath(path)

' Always return a params array (empty by default)
Dim emptyParams() : ReDim emptyParams(-1)
@@ -113,6 +113,9 @@ End Function
'------------------------------------------------------------
Private Function IsMatch(requestPath, routePattern, values)
Dim reqParts, routeParts, i, paramCount
requestPath = NormalizePath(requestPath)
routePattern = NormalizePath(routePattern)

reqParts = Split(requestPath, "/")
routeParts = Split(routePattern, "/")

@@ -123,7 +126,7 @@ Private Function IsMatch(requestPath, routePattern, values)
paramCount = 0 : ReDim values(-1)

For i = 0 To UBound(reqParts)
If Left(routeParts(i), 1) = ":" Then
If IsDynamicSegment(routeParts(i)) Then
ReDim Preserve values(paramCount)
values(paramCount) = reqParts(i)
paramCount = paramCount + 1
@@ -136,6 +139,51 @@ Private Function IsMatch(requestPath, routePattern, values)
IsMatch = True
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
'------------------------------------------------------------


+ 1
- 0
public/Default.asp Ver arquivo

@@ -35,6 +35,7 @@
router.AddRoute "GET", "/admin/categories", "AdminController", "Categories"
router.AddRoute "POST", "/admin/posts/{id}/publish", "AdminController", "PublishPost"
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)
MVC.DispatchRequest Request.ServerVariables("REQUEST_METHOD"), _


+ 9
- 0
public/web.config Ver arquivo

@@ -39,6 +39,15 @@

<!-- Cache-bust parameter name (default: "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>

<system.webServer>


Carregando…
Cancelar
Salvar

Powered by TurnKey Linux.