diff --git a/app/controllers/AdminController.asp b/app/controllers/AdminController.asp
index 443f7f9..1b3ab8d 100644
--- a/app/controllers/AdminController.asp
+++ b/app/controllers/AdminController.asp
@@ -58,6 +58,43 @@ Class AdminController_Class
<%
End Sub
+ '---------------------------------------------------------------
+ ' Action: AIPrompt
+ '---------------------------------------------------------------
+ Public Sub AIPrompt()
+ m_title = "AI Prompt Settings"
+ Dim promptTemplate : promptTemplate = GetGenerationPromptTemplate()
+ %>
+
+ <%
+ End Sub
+
+ '---------------------------------------------------------------
+ ' Action: UpdateAIPrompt
+ '---------------------------------------------------------------
+ Public Sub UpdateAIPrompt()
+ Dim promptTemplate : promptTemplate = Trim(Request.Form("PromptTemplate"))
+ If Len(promptTemplate) = 0 Then
+ Flash().AddError "Prompt template cannot be empty."
+ Response.Redirect "/admin/ai-prompt"
+ Exit Sub
+ End If
+
+ On Error Resume Next
+ UpdateAppSetting "AbacusGenerationPrompt", promptTemplate
+ If Err.Number <> 0 Then
+ Flash().AddError "Unable to save prompt template: " & Err.Description
+ Err.Clear
+ On Error GoTo 0
+ Response.Redirect "/admin/ai-prompt"
+ Exit Sub
+ End If
+ On Error GoTo 0
+
+ Flash().Success = "AI prompt template saved."
+ Response.Redirect "/admin/ai-prompt"
+ End Sub
+
'---------------------------------------------------------------
' Action: PublishPost
'---------------------------------------------------------------
@@ -166,10 +203,7 @@ Class AdminController_Class
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."
+ userPrompt = BuildGenerationPrompt(post)
payload = "{""model"":""" & JsonEscape(modelName) & """,""messages"":[{""role"":""system"",""content"":""" & JsonEscape(systemPrompt) & """},{""role"":""user"",""content"":""" & JsonEscape(userPrompt) & """}],""temperature"":0.7}"
@@ -235,14 +269,6 @@ Class AdminController_Class
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
diff --git a/app/controllers/CommentsController.asp b/app/controllers/CommentsController.asp
index 5712243..f0ae0ba 100644
--- a/app/controllers/CommentsController.asp
+++ b/app/controllers/CommentsController.asp
@@ -53,7 +53,7 @@ Class CommentsController_Class
Dim slug : slug = ResolvePostSlug(comment.PostID)
If Len(slug) > 0 Then
- Response.Redirect "/posts/" & Server.URLEncode(slug)
+ Response.Redirect PostUrl(slug)
Else
Response.Redirect "/posts"
End If
@@ -82,12 +82,13 @@ Class CommentsController_Class
Private Function ResolvePostSlug(ByVal postID)
Dim slug : slug = Trim(Request.Form("Slug"))
If Len(slug) = 0 Then slug = Trim(Request.Form("PostSlug"))
+ slug = NormalizeSlug(slug)
If Len(slug) = 0 And CLng(postID) > 0 Then
Dim post
On Error Resume Next
Set post = PostsRepository().FindByID(postID)
- If Err.Number = 0 Then slug = CStr(post.Slug)
+ If Err.Number = 0 Then slug = NormalizeSlug(post.Slug)
Err.Clear
On Error GoTo 0
End If
diff --git a/app/controllers/PostsController.asp b/app/controllers/PostsController.asp
index 5492777..87f60a0 100644
--- a/app/controllers/PostsController.asp
+++ b/app/controllers/PostsController.asp
@@ -45,12 +45,19 @@ Class PostsController_Class
<%
End Sub
- '---------------------------------------------------------------
- ' Action: Show
- '---------------------------------------------------------------
+ '---------------------------------------------------------------
+ ' Action: Show
+ '---------------------------------------------------------------
Public Sub Show(ByVal slug)
+ Dim requestedSlug : requestedSlug = Trim(CStr(slug))
+ Dim canonicalSlug : canonicalSlug = NormalizeSlug(requestedSlug)
+
Dim matches
- Set matches = PostsRepository().Find(Array("Slug", slug, "IsPublished", 1), Empty)
+ Set matches = PostsRepository().Find(Array("Slug", requestedSlug, "IsPublished", 1), Empty)
+
+ If matches.Count = 0 And canonicalSlug <> requestedSlug Then
+ Set matches = PostsRepository().Find(Array("Slug", canonicalSlug, "IsPublished", 1), Empty)
+ End If
If matches.Count = 0 Then
Response.Status = "404 Not Found"
@@ -63,6 +70,10 @@ Class PostsController_Class
Dim post
Set post = matches.Front()
+ If Len(canonicalSlug) > 0 And canonicalSlug <> requestedSlug Then
+ Response.Redirect PostUrl(canonicalSlug)
+ End If
+
Dim comments
Set comments = CommentsRepository().Find(Array("PostID", post.PostID, "IsApproved", 1), "CreatedDate")
%>
diff --git a/app/views/Admin/ai-prompt.asp b/app/views/Admin/ai-prompt.asp
new file mode 100644
index 0000000..73c6d70
--- /dev/null
+++ b/app/views/Admin/ai-prompt.asp
@@ -0,0 +1,25 @@
+
+
+
+
+
AI Prompt Settings
+
Edit the template used when generating AI post content.
+
+
+
+
+
+
diff --git a/app/views/Admin/index.asp b/app/views/Admin/index.asp
index 8deb19a..aa0f30e 100644
--- a/app/views/Admin/index.asp
+++ b/app/views/Admin/index.asp
@@ -22,4 +22,12 @@
+
diff --git a/core/helpers.asp b/core/helpers.asp
index bea615b..d49aefd 100644
--- a/core/helpers.asp
+++ b/core/helpers.asp
@@ -95,6 +95,76 @@ Public Function GetSecureSetting(key, envName)
GetSecureSetting = ""
End Function
+Public Function GetGenerationPromptTemplate()
+ Dim prompt
+ prompt = Trim(CStr(GetAppSetting("AbacusGenerationPrompt")))
+
+ If Len(prompt) = 0 Or LCase(prompt) = "nothing" Then
+ prompt = "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." & vbCrLf & vbCrLf & _
+ "Create blog content for this post title: {TITLE}" & vbCrLf & _
+ "Existing summary: {SUMMARY}" & vbCrLf & _
+ "Existing body: {BODY}" & vbCrLf & _
+ "Keep the title unchanged. Make the content readable and helpful for a general audience."
+ End If
+
+ GetGenerationPromptTemplate = prompt
+End Function
+
+Public Function BuildGenerationPrompt(ByRef post)
+ Dim template
+ template = GetGenerationPromptTemplate()
+ template = Replace(template, "{TITLE}", SafePromptText(post.Title))
+ template = Replace(template, "{SUMMARY}", SafePromptText(post.Summary))
+ template = Replace(template, "{BODY}", SafePromptText(post.Body))
+ BuildGenerationPrompt = template
+End Function
+
+Public Function SafePromptText(ByVal value)
+ If IsNull(value) Or IsEmpty(value) Then
+ SafePromptText = ""
+ Else
+ SafePromptText = CStr(value)
+ End If
+End Function
+
+Public Function UpdateAppSetting(key, value)
+ Dim xml, nodes, node, appSettings, found
+ Set xml = Server.CreateObject("Microsoft.XMLDOM")
+ xml.async = False
+ xml.preserveWhiteSpace = True
+ xml.Load Server.MapPath("web.config")
+
+ If xml.parseError.errorCode <> 0 Then
+ Err.Raise 1, "UpdateAppSetting", "Unable to load web.config: " & xml.parseError.reason
+ End If
+
+ Set nodes = xml.selectNodes("//appSettings/add[@key='" & key & "']")
+ found = False
+ If Not (nodes Is Nothing) Then
+ If nodes.Length > 0 Then
+ Set node = nodes.Item(0)
+ node.setAttribute "value", value
+ found = True
+ End If
+ End If
+
+ If Not found Then
+ Set appSettings = xml.selectSingleNode("//appSettings")
+ If appSettings Is Nothing Then
+ Err.Raise 1, "UpdateAppSetting", " section not found in web.config."
+ End If
+
+ Set node = xml.createElement("add")
+ node.setAttribute "key", key
+ node.setAttribute "value", value
+ appSettings.appendChild node
+ End If
+
+ xml.Save Server.MapPath("web.config")
+ Application.Contents.Remove("AppSetting_" & key)
+ UpdateAppSetting = True
+End Function
+
Public Sub ShowServerVariables
Dim varName, htmlTable
htmlTable = ""
@@ -183,6 +253,31 @@ Function TrimQueryParams(rawPath)
End If
End Function
+Function DecodeUrlPath(ByVal rawPath)
+ Dim current, previous
+ current = Trim(CStr(rawPath))
+
+ On Error Resume Next
+ Do
+ previous = current
+ current = Server.URLDecode(current)
+ If Err.Number <> 0 Then
+ Err.Clear
+ current = previous
+ Exit Do
+ End If
+ Loop While current <> previous
+ On Error GoTo 0
+
+ current = Replace(current, "%252D", "-")
+ current = Replace(current, "%252d", "-")
+ current = Replace(current, "%2D", "-")
+ current = Replace(current, "%2d", "-")
+ current = Replace(current, "%25", "%")
+
+ DecodeUrlPath = current
+End Function
+
Sub Destroy(o)
if isobject(o) then
if not o is nothing then
@@ -252,7 +347,7 @@ Function CategoryDeleteUrl(ByVal categoryId)
End Function
Function PostUrl(ByVal slug)
- PostUrl = "/posts/" & Server.URLEncode(CStr(slug))
+ PostUrl = "/posts/" & Server.URLEncode(NormalizeSlug(slug))
End Function
Function PostsUrl()
@@ -287,6 +382,14 @@ Function AdminPostAIUrl(ByVal postId)
AdminPostAIUrl = "/admin/posts/" & Server.URLEncode(CStr(postId)) & "/ai"
End Function
+Function AdminAIPromptUrl()
+ AdminAIPromptUrl = "/admin/ai-prompt"
+End Function
+
+Function AdminAIPromptUpdateUrl()
+ AdminAIPromptUpdateUrl = "/admin/ai-prompt"
+End Function
+
Function AdminUrl()
AdminUrl = "/admin"
End Function
@@ -295,6 +398,36 @@ Function CommentsUrl()
CommentsUrl = "/comments"
End Function
+Function NormalizeSlug(ByVal slug)
+ Dim current, previous
+ current = Trim(CStr(slug))
+
+ If Len(current) = 0 Then
+ NormalizeSlug = ""
+ Exit Function
+ End If
+
+ On Error Resume Next
+ Do
+ previous = current
+ current = Server.URLDecode(current)
+ If Err.Number <> 0 Then
+ Err.Clear
+ current = previous
+ Exit Do
+ End If
+ Loop While current <> previous
+ On Error GoTo 0
+
+ current = Replace(current, "%252D", "-")
+ current = Replace(current, "%252d", "-")
+ current = Replace(current, "%2D", "-")
+ current = Replace(current, "%2d", "-")
+ current = Replace(current, "%25", "%")
+
+ NormalizeSlug = current
+End Function
+
'=======================================================================================================================
' Adapted from Tolerable library
diff --git a/core/router.wsc b/core/router.wsc
index ebb65a7..129d500 100644
--- a/core/router.wsc
+++ b/core/router.wsc
@@ -165,6 +165,19 @@ Private Function NormalizePath(path)
cleaned = Left(cleaned, Len(cleaned) - 1)
Loop
+ On Error Resume Next
+ Do
+ Dim decoded
+ decoded = Server.URLDecode(cleaned)
+ If Err.Number <> 0 Then
+ Err.Clear
+ Exit Do
+ End If
+ If decoded = cleaned Then Exit Do
+ cleaned = LCase(decoded)
+ Loop
+ On Error GoTo 0
+
NormalizePath = cleaned
End Function
diff --git a/public/Default.asp b/public/Default.asp
index 340ae9b..0e24c31 100644
--- a/public/Default.asp
+++ b/public/Default.asp
@@ -33,11 +33,13 @@
router.AddRoute "GET", "/admin", "AdminController", "Index"
router.AddRoute "GET", "/admin/posts", "AdminController", "Posts"
router.AddRoute "GET", "/admin/categories", "AdminController", "Categories"
+ router.AddRoute "GET", "/admin/ai-prompt", "AdminController", "AIPrompt"
+ router.AddRoute "POST", "/admin/ai-prompt", "AdminController", "UpdateAIPrompt"
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"), _
- TrimQueryParams(Request.ServerVariables("HTTP_X_ORIGINAL_URL"))
+MVC.DispatchRequest Request.ServerVariables("REQUEST_METHOD"), _
+ TrimQueryParams(DecodeUrlPath(Request.ServerVariables("HTTP_X_ORIGINAL_URL")))
%>
diff --git a/public/web.config b/public/web.config
index 11e40d6..fc80e4f 100644
--- a/public/web.config
+++ b/public/web.config
@@ -48,6 +48,9 @@
+
+
+