diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d4a90d0..5feb1989 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,10 @@ The format is based on [Keep a Changelog v1.1.0][keep-a-changelog]. See [the REA
This file only documents changes in the site engine, not any changes in the
content or the hosting infrastructure.
+## [Unreleased]
+### Changed
+- Minor text updates.
+
## [5.0.0] - 2025-03-27
### Removed
- **Breaking change:** removed the EvilPlanner functionality, including everything (server API, pages, resources) related to daily quotes.
diff --git a/ForneverMind.Core/ForneverMind.Core.fsproj b/ForneverMind.Core/ForneverMind.Core.fsproj
new file mode 100644
index 00000000..e5bcc8e1
--- /dev/null
+++ b/ForneverMind.Core/ForneverMind.Core.fsproj
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/ForneverMind.Core/LanguageLinks.fs b/ForneverMind.Core/LanguageLinks.fs
new file mode 100644
index 00000000..4e57ccc1
--- /dev/null
+++ b/ForneverMind.Core/LanguageLinks.fs
@@ -0,0 +1,11 @@
+namespace ForneverMind.Core
+
+type LanguageLink = {
+ IsActive: bool
+ Link: string
+}
+
+type LanguageLinks = {
+ English: LanguageLink
+ Russian: LanguageLink
+}
diff --git a/ForneverMind.Razor/BasePageModel.cs b/ForneverMind.Razor/BasePageModel.cs
new file mode 100644
index 00000000..b42993d5
--- /dev/null
+++ b/ForneverMind.Razor/BasePageModel.cs
@@ -0,0 +1,18 @@
+using ForneverMind.Core;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace ForneverMind.Razor;
+
+public class BasePageModel : PageModel
+{
+ public LanguageLinks Links => new(
+ english: new LanguageLink(isActive: false, link: ProduceLink(PageContext, "en")),
+ russian: new LanguageLink(isActive: false, link: ProduceLink(PageContext, "ru"))
+ );
+
+ private static string ProduceLink(PageContext context, string newLanguage)
+ {
+ var currentUrl = context.HttpContext.Request.Path.Value;
+ return $"{newLanguage}/{currentUrl?.Substring("/en".Length)}";
+ }
+}
diff --git a/ForneverMind.Razor/ForneverMind.Razor.csproj b/ForneverMind.Razor/ForneverMind.Razor.csproj
new file mode 100644
index 00000000..89a4d805
--- /dev/null
+++ b/ForneverMind.Razor/ForneverMind.Razor.csproj
@@ -0,0 +1,17 @@
+
+
+
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ForneverMind.Razor/Pages/Shared/en/_Layout.cshtml b/ForneverMind.Razor/Pages/Shared/en/_Layout.cshtml
new file mode 100644
index 00000000..a4fb74f2
--- /dev/null
+++ b/ForneverMind.Razor/Pages/Shared/en/_Layout.cshtml
@@ -0,0 +1,65 @@
+
+
+@model ForneverMind.Razor.BasePageModel
+
+
+
+
+
+ F. von Never — @ViewBag.Title
+
+
+
+
+
+
+
+
+
+
+
+ @RenderBody()
+
+
+@RenderSection("scripts", false)
+
+
+
diff --git a/ForneverMind.Razor/Pages/Shared/ru/_Layout.cshtml b/ForneverMind.Razor/Pages/Shared/ru/_Layout.cshtml
new file mode 100644
index 00000000..5b713ae3
--- /dev/null
+++ b/ForneverMind.Razor/Pages/Shared/ru/_Layout.cshtml
@@ -0,0 +1,65 @@
+
+
+@model ForneverMind.Razor.BasePageModel
+
+
+
+
+
+ F. von Never — @ViewBag.Title
+
+
+
+
+
+
+
+
+
+
+
+ @RenderBody()
+
+
+@RenderSection("scripts", false)
+
+
+
diff --git a/ForneverMind/views/en/404.cshtml b/ForneverMind.Razor/Pages/en/404.cshtml
similarity index 69%
rename from ForneverMind/views/en/404.cshtml
rename to ForneverMind.Razor/Pages/en/404.cshtml
index 7720a9c0..cc1e3a4a 100644
--- a/ForneverMind/views/en/404.cshtml
+++ b/ForneverMind.Razor/Pages/en/404.cshtml
@@ -1,3 +1,6 @@
+@page
+@model ForneverMind.Razor.BasePageModel
+
@{
- Layout = "en/_Layout.cshtml";
ViewBag.Title = "Page Not Found";
+ Model.Response.StatusCode = 404;
}
The requested page does not exist.
diff --git a/ForneverMind.Razor/Pages/en/_ViewStart.cshtml b/ForneverMind.Razor/Pages/en/_ViewStart.cshtml
new file mode 100644
index 00000000..701921ad
--- /dev/null
+++ b/ForneverMind.Razor/Pages/en/_ViewStart.cshtml
@@ -0,0 +1,3 @@
+@{
+ Layout = "en/_Layout";
+}
diff --git a/ForneverMind/views/ru/404.cshtml b/ForneverMind.Razor/Pages/ru/404.cshtml
similarity index 56%
rename from ForneverMind/views/ru/404.cshtml
rename to ForneverMind.Razor/Pages/ru/404.cshtml
index f2fce117..d3fcf09b 100644
--- a/ForneverMind/views/ru/404.cshtml
+++ b/ForneverMind.Razor/Pages/ru/404.cshtml
@@ -1,3 +1,6 @@
+@page
+@model ForneverMind.Razor.BasePageModel
+
@{
- Layout = "ru/_Layout.cshtml";
ViewBag.Title = "Страница не найдена";
+ Model.Response.StatusCode = 404;
}
-Страница по этому адресу не существует.
+Страницы по этому адресу не существует.
diff --git a/ForneverMind.Razor/Pages/ru/_ViewStart.cshtml b/ForneverMind.Razor/Pages/ru/_ViewStart.cshtml
new file mode 100644
index 00000000..1faa30eb
--- /dev/null
+++ b/ForneverMind.Razor/Pages/ru/_ViewStart.cshtml
@@ -0,0 +1,3 @@
+@{
+ Layout = "ru/_Layout";
+}
diff --git a/ForneverMind.Tests/RouteTests.fs b/ForneverMind.Tests/RouteTests.fs
index e263f4af..6c2ed04c 100644
--- a/ForneverMind.Tests/RouteTests.fs
+++ b/ForneverMind.Tests/RouteTests.fs
@@ -5,6 +5,7 @@
[]
module ForneverMind.Tests.RouteTests
+open System.Net
open System.Threading.Tasks
open Xunit
@@ -14,5 +15,24 @@ open ForneverMind.TestFramework.IntegrationTestUtil
[]
let ``Index page should resolve correctly``(): Task = withWebApp(fun client -> task {
let! result = client.GetAsync "/"
+ Assert.Equal("/en/", result.RequestMessage.RequestUri.PathAndQuery)
Assert.Equal("text/html", result.Content.Headers.ContentType.MediaType)
})
+
+[]
+let ``404 page should be resolved``(): Task = withWebApp(fun client -> task {
+ let doTest (url: string) message = task {
+ let! result = client.GetAsync url
+ let! content = result.Content.ReadAsStringAsync()
+
+ Assert.Equal(HttpStatusCode.NotFound, result.StatusCode)
+ Assert.Equal("text/html", result.Content.Headers.ContentType.MediaType)
+ Assert.Contains(message, content)
+ }
+
+ do! doTest "/en/404" "The requested page does not exist."
+ do! doTest "/ru/404" "Страницы по этому адресу не существует."
+ do! doTest "/en/blah-blah" "The requested page does not exist."
+ do! doTest "/ru/blah-blah" "Страницы по этому адресу не существует."
+ do! doTest "/blah-blah" "The requested page does not exist."
+})
diff --git a/ForneverMind.sln b/ForneverMind.sln
index c7e57e20..62f85751 100644
--- a/ForneverMind.sln
+++ b/ForneverMind.sln
@@ -52,6 +52,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EEF302B2-6
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ForneverMind.TestFramework", "ForneverMind.TestFramework\ForneverMind.TestFramework.fsproj", "{CC92AD22-D198-46B2-8F33-45F6B5BF1EEE}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ForneverMind.Razor", "ForneverMind.Razor\ForneverMind.Razor.csproj", "{41A78B9F-701F-41A2-B776-185780EBE19D}"
+EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ForneverMind.Core", "ForneverMind.Core\ForneverMind.Core.fsproj", "{AAFD0512-7132-45E1-A91A-BFCAD532EA4F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -74,6 +78,14 @@ Global
{CC92AD22-D198-46B2-8F33-45F6B5BF1EEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC92AD22-D198-46B2-8F33-45F6B5BF1EEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC92AD22-D198-46B2-8F33-45F6B5BF1EEE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {41A78B9F-701F-41A2-B776-185780EBE19D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {41A78B9F-701F-41A2-B776-185780EBE19D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {41A78B9F-701F-41A2-B776-185780EBE19D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {41A78B9F-701F-41A2-B776-185780EBE19D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AAFD0512-7132-45E1-A91A-BFCAD532EA4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AAFD0512-7132-45E1-A91A-BFCAD532EA4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AAFD0512-7132-45E1-A91A-BFCAD532EA4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AAFD0512-7132-45E1-A91A-BFCAD532EA4F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/ForneverMind/Controllers/PagesController.fs b/ForneverMind/Controllers/PagesController.fs
new file mode 100644
index 00000000..b746ff20
--- /dev/null
+++ b/ForneverMind/Controllers/PagesController.fs
@@ -0,0 +1,13 @@
+namespace ForneverMind.Controllers
+
+open Microsoft.AspNetCore.Mvc
+
+open ForneverMind
+
+[]
+type PagesController() =
+ inherit Controller()
+
+ []
+ member this.Get(): IActionResult =
+ RedirectResult $"/{Common.defaultLanguage}/"
diff --git a/ForneverMind/ForneverMind.fsproj b/ForneverMind/ForneverMind.fsproj
index a5e72870..ea35a8e4 100644
--- a/ForneverMind/ForneverMind.fsproj
+++ b/ForneverMind/ForneverMind.fsproj
@@ -30,6 +30,7 @@ SPDX-License-Identifier: MIT
+
@@ -50,8 +51,8 @@ SPDX-License-Identifier: MIT
-
+
+
diff --git a/ForneverMind/PagesModule.fs b/ForneverMind/PagesModule.fs
index d64977ad..3825f6f7 100644
--- a/ForneverMind/PagesModule.fs
+++ b/ForneverMind/PagesModule.fs
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2025 Friedrich von Never
+// SPDX-FileCopyrightText: 2025 Friedrich von Never
//
// SPDX-License-Identifier: MIT
@@ -14,6 +14,7 @@ open Freya.Optics.Http
open Freya.Types.Http
open Freya.Types.Uri
+open ForneverMind.Core
open ForneverMind.Models
type PagesModule(posts : PostsModule, templates : TemplatingModule, markdown : MarkdownModule) =
@@ -67,21 +68,6 @@ type PagesModule(posts : PostsModule, templates : TemplatingModule, markdown : M
return known
}
- let notFoundHandler =
- let language =
- freya {
- let! language = Common.routeLanguageOpt
- let language =
- language
- |> Option.map (fun lang ->
- if Array.contains lang Common.supportedLanguages
- then lang
- else Common.defaultLanguage)
-
- return Option.defaultValue Common.defaultLanguage language
- }
- handleStaticPage language "404" (Freya.init None) false
-
let page templateName model additionalModificationDate =
freyaMachine {
including Common.machine
@@ -93,7 +79,6 @@ type PagesModule(posts : PostsModule, templates : TemplatingModule, markdown : M
return max templateModificationDate (Option.defaultValue DateTime.MinValue modificationDate)
})
handleOk (handleStaticPage Common.routeLanguage templateName model true)
- handleNotFound notFoundHandler
}
let postCache = ConcurrentDictionary()
@@ -139,25 +124,6 @@ type PagesModule(posts : PostsModule, templates : TemplatingModule, markdown : M
let contact = page "Contact" (Freya.init None) (Freya.init None)
let talks = page "Talks" (Freya.init None) (Freya.init None)
- let notFound =
- let pageRequestedExplicitly =
- freya {
- let supportedUrls =
- Common.supportedLanguages
- |> Seq.map (sprintf "/%s/404.html")
- let! url = Request.path_ |> Freya.Optic.get
- return Seq.contains url supportedUrls
- }
- freyaMachine {
- including Common.machine
- methods Common.methods
- exists pageRequestedExplicitly
- handleNotFound notFoundHandler
- handleOk notFoundHandler
- }
-
- let error = page "Error" (Freya.init None) (Freya.init None)
-
let post =
freyaMachine {
including Common.machine
@@ -166,7 +132,6 @@ type PagesModule(posts : PostsModule, templates : TemplatingModule, markdown : M
lastModified (posts.PostLastModified <| lastModificationDate "Post")
handleOk handlePost
- handleNotFound notFoundHandler
}
let redirectToDefaultLanguageIndex =
@@ -186,7 +151,5 @@ type PagesModule(posts : PostsModule, templates : TemplatingModule, markdown : M
member __.Index = index
member __.Archive = archive
member __.Contact = contact
- member __.Error = error
member __.Talks = talks
- member __.NotFound = notFound
member __.RedirectToDefaultLanguageIndex : HttpMachine = redirectToDefaultLanguageIndex
diff --git a/ForneverMind/PostsModule.fs b/ForneverMind/PostsModule.fs
index 1ab28b26..3c98ec1b 100644
--- a/ForneverMind/PostsModule.fs
+++ b/ForneverMind/PostsModule.fs
@@ -6,6 +6,7 @@ namespace ForneverMind
open System.IO
+open ForneverMind.Core
open Freya.Core
open Freya.Routers.Uri.Template
diff --git a/ForneverMind/Program.fs b/ForneverMind/Program.fs
index 68047d5b..53261d36 100644
--- a/ForneverMind/Program.fs
+++ b/ForneverMind/Program.fs
@@ -7,9 +7,12 @@ module ForneverMind.Program
open System
open System.IO
+open System.Threading.Tasks
open Freya.Core
open Microsoft.AspNetCore.Builder
+open Microsoft.AspNetCore.Diagnostics
open Microsoft.AspNetCore.Hosting
+open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Logging
@@ -46,6 +49,7 @@ let private configure (configuration: IConfigurationRoot) (builder: WebApplicati
.AddSingleton(configModule)
|> ignore
+ builder.Services.AddRazorPages() |> ignore
builder.Services.AddMvc() |> ignore
builder
@@ -53,10 +57,35 @@ let private configure (configuration: IConfigurationRoot) (builder: WebApplicati
let private build (builder: WebApplicationBuilder) =
let app = builder.Build()
app.UseStaticFiles() |> ignore
+
+ // To use custom error page addresses, first apply StatusCodePagesWithReExecute and then read the original routing
+ // data from IStatusCodeReExecuteFeature.
+ app
+ .UseStatusCodePagesWithReExecute("/error/{0}")
+ .Use(fun (context: HttpContext) (next: RequestDelegate) ->
+ (task {
+ let statusCode = context.Response.StatusCode
+ if statusCode = 404 then
+ let errorInfo = context.Features.Get() |> ValueOption.ofObj
+ match errorInfo with
+ | ValueSome error ->
+ let language =
+ if error.OriginalPath.StartsWith "/ru/" then "ru"
+ else "en"
+ context.Response.Redirect $"/{language}/404"
+ return ()
+ | ValueNone -> return! next.Invoke context
+ else
+ return! next.Invoke context
+ }) : Task
+ ) |> ignore
+
let router = createRouter app.Services
+ useFreya router app
+
app.UseRouting() |> ignore
app.MapControllers() |> ignore
- useFreya router app
+ app.MapRazorPages() |> ignore
app
let private run(app: WebApplication) =
diff --git a/ForneverMind/RoutesModule.fs b/ForneverMind/RoutesModule.fs
index 22b311de..266f963b 100644
--- a/ForneverMind/RoutesModule.fs
+++ b/ForneverMind/RoutesModule.fs
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2025 Friedrich von Never
+// SPDX-FileCopyrightText: 2025 Friedrich von Never
//
// SPDX-License-Identifier: MIT
@@ -10,16 +10,12 @@ type RoutesModule(pages: PagesModule, rss: RssModule) =
let router =
freyaRouter {
resource "/{language}/posts/{name}" pages.Post
- resource "/{language}/" pages.Index
+ resource "/{language}/" pages.Index // TODO: Migrate this
resource "/{language}/archive.html" pages.Archive
resource "/{language}/contact.html" pages.Contact
- resource "/{language}/error.html" pages.Error
resource "/{language}/rss.xml" rss.Feed
resource "/{language}/talks.html" pages.Talks
- resource "/{language}/{q*}" pages.NotFound
- resource "/" pages.RedirectToDefaultLanguageIndex
resource "/rss.xml" rss.Feed
- resource "/{q*}" pages.NotFound
}
member __.Router = router
diff --git a/ForneverMind/TemplatingModule.fs b/ForneverMind/TemplatingModule.fs
index 4370cfe8..6a560dcd 100644
--- a/ForneverMind/TemplatingModule.fs
+++ b/ForneverMind/TemplatingModule.fs
@@ -8,16 +8,9 @@ open System
open System.Collections.Generic
open System.IO
+open ForneverMind.Core
open RazorLight
-type LanguageLink =
- { IsActive : bool
- Link : string }
-
-type LanguageLinks =
- { English : LanguageLink
- Russian : LanguageLink }
-
type TemplatingModule (config: ConfigurationModule) =
let razor =
RazorLightEngineBuilder()
diff --git a/ForneverMind/views/en/Error.cshtml b/ForneverMind/views/en/Error.cshtml
deleted file mode 100644
index 4db0f58f..00000000
--- a/ForneverMind/views/en/Error.cshtml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-@{
- Layout = "en/_Layout.cshtml";
- ViewBag.Title = "Server Error";
-}
-
-An internal server error has been occured while processing the request.
diff --git a/ForneverMind/views/ru/Error.cshtml b/ForneverMind/views/ru/Error.cshtml
deleted file mode 100644
index 686f57ee..00000000
--- a/ForneverMind/views/ru/Error.cshtml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-@{
- Layout = "ru/_Layout.cshtml";
- ViewBag.Title = "Ошибка сервера";
-}
-
-При обработке запроса произошла внутренняя ошибка сервера.