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

@ViewBag.Title

+ @if (Model.Links.English.IsActive) + { +
+ @if (Model.Links.Russian.IsActive) + { + Rus + } +
+ } +
+ + @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 + + + + + + + + +
+
+

@ViewBag.Title

+ @if (Model.Links.Russian.IsActive) + { +
+ @if (Model.Links.English.IsActive) + { + Eng + } +
+ } +
+ + @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 = "Ошибка сервера"; -} - -

При обработке запроса произошла внутренняя ошибка сервера.