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.

+ +
+
+ + +
+ Placeholders: {TITLE}, {SUMMARY}, {BODY} +
+
+ +
+ + Cancel +
+
+
+
+
+
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 @@ +
+ +
+

AI Prompt

+

Edit the prompt used to generate AI post content.

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