diff --git a/.github/workflows/webapp-prod.yml b/.github/workflows/webapp-prod.yml index db9ad3e48..fd04c0ee8 100644 --- a/.github/workflows/webapp-prod.yml +++ b/.github/workflows/webapp-prod.yml @@ -1,7 +1,7 @@ name: Azure Web App (PROD) env: - AZURE_WEBAPP_NAME: moonglade-ediwang-us + AZURE_WEBAPP_NAME: ediwang AZURE_WEBAPP_PACKAGE_PATH: '.' DOTNET_VERSION: '8' @@ -66,5 +66,5 @@ jobs: with: app-name: ${{ env.AZURE_WEBAPP_NAME }} clean: true - publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }} + publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE2 }} package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} diff --git a/README.md b/README.md index 027478d64..72ae29f33 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ A personal blog system that optimized for [**Microsoft Azure**](https://azure.mi This is the way https://edi.wang is deployed, by taking advantage of as many Azure services as possible, the blog can run very fast and secure. There is no automated script to deploy it, you need to manually create all the resources. -![image](https://cdn.edi.wang/web-assets/ediwang-azure-arch-visio-nov2022.png) +![image](https://cdn.edi.wang/web-assets/ediwang-azure-arch-visio-oct2024.svg) ### Quick Deploy on Azure (App Service on Linux) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index d06c5a8fd..d2b87bf72 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,8 +3,8 @@ Edi Wang edi.wang (C) 2024 edi.wang@outlook.com - 14.11.0.0 - 14.11.0.0 - 14.11.0 + 14.12.0.0 + 14.12.0.0 + 14.12.0 \ No newline at end of file diff --git a/src/Moonglade.Configuration/BlogConfig.cs b/src/Moonglade.Configuration/BlogConfig.cs index e76004eb8..6ae3a5d40 100644 --- a/src/Moonglade.Configuration/BlogConfig.cs +++ b/src/Moonglade.Configuration/BlogConfig.cs @@ -14,6 +14,7 @@ public interface IBlogConfig CustomStyleSheetSettings CustomStyleSheetSettings { get; set; } CustomMenuSettings CustomMenuSettings { get; set; } LocalAccountSettings LocalAccountSettings { get; set; } + SocialLinkSettings SocialLinkSettings { get; set; } SystemManifestSettings SystemManifestSettings { get; set; } IEnumerable LoadFromConfig(IDictionary config); @@ -40,6 +41,8 @@ public class BlogConfig : IBlogConfig public LocalAccountSettings LocalAccountSettings { get; set; } + public SocialLinkSettings SocialLinkSettings { get; set; } + public SystemManifestSettings SystemManifestSettings { get; set; } public IEnumerable LoadFromConfig(IDictionary config) @@ -53,6 +56,7 @@ public IEnumerable LoadFromConfig(IDictionary config) CustomStyleSheetSettings = AssignValueForConfigItem(7, CustomStyleSheetSettings.DefaultValue, config); CustomMenuSettings = AssignValueForConfigItem(10, CustomMenuSettings.DefaultValue, config); LocalAccountSettings = AssignValueForConfigItem(11, LocalAccountSettings.DefaultValue, config); + SocialLinkSettings = AssignValueForConfigItem(12, SocialLinkSettings.DefaultValue, config); // Special case SystemManifestSettings = AssignValueForConfigItem(99, SystemManifestSettings.DefaultValue, config); diff --git a/src/Moonglade.Configuration/SocialLink.cs b/src/Moonglade.Configuration/SocialLink.cs new file mode 100644 index 000000000..984bbce6d --- /dev/null +++ b/src/Moonglade.Configuration/SocialLink.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; + +namespace Moonglade.Configuration; + +public class SocialLinkSettings : IBlogSettings +{ + public bool IsEnabled { get; set; } + + public SocialLink[] Links { get; set; } = []; + + public static SocialLinkSettings DefaultValue => + new() + { + IsEnabled = false, + Links = [] + }; +} + +public class SocialLink +{ + public string Name { get; set; } + + public string Icon { get; set; } + + public string Url { get; set; } +} + +public class SocialLinkSettingsJsonModel +{ + [Display(Name = "Enable Social Links")] + public bool IsEnabled { get; set; } + + [MaxLength(1024)] + public string JsonData { get; set; } +} \ No newline at end of file diff --git a/src/Moonglade.Core/GetAllSocialLinksQuery.cs b/src/Moonglade.Core/GetAllSocialLinksQuery.cs new file mode 100644 index 000000000..2302d6c71 --- /dev/null +++ b/src/Moonglade.Core/GetAllSocialLinksQuery.cs @@ -0,0 +1,21 @@ +using Moonglade.Configuration; + +namespace Moonglade.Core; + +public record GetAllSocialLinksQuery : IRequest; + +public class GetAllSocialLinksQueryHandler(IBlogConfig blogConfig) : IRequestHandler +{ + public Task Handle(GetAllSocialLinksQuery request, CancellationToken ct) + { + var section = blogConfig.SocialLinkSettings; + + if (!section.IsEnabled) + { + return Task.FromResult(Array.Empty()); + } + + var links = blogConfig.SocialLinkSettings.Links; + return Task.FromResult(links); + } +} \ No newline at end of file diff --git a/src/Moonglade.Core/PostFeature/CreatePostCommand.cs b/src/Moonglade.Core/PostFeature/CreatePostCommand.cs index 164b12f30..c47ffb63f 100644 --- a/src/Moonglade.Core/PostFeature/CreatePostCommand.cs +++ b/src/Moonglade.Core/PostFeature/CreatePostCommand.cs @@ -33,20 +33,21 @@ public async Task Handle(CreatePostCommand request, CancellationToke abs = request.Payload.Abstract.Trim(); } + var utcNow = DateTime.UtcNow; var post = new PostEntity { CommentEnabled = request.Payload.EnableComment, Id = Guid.NewGuid(), PostContent = request.Payload.EditorContent, ContentAbstract = abs, - CreateTimeUtc = DateTime.UtcNow, - LastModifiedUtc = DateTime.UtcNow, // Fix draft orders + CreateTimeUtc = utcNow, + LastModifiedUtc = utcNow, // Fix draft orders Slug = request.Payload.Slug.ToLower().Trim(), Author = request.Payload.Author?.Trim(), Title = request.Payload.Title.Trim(), ContentLanguageCode = request.Payload.LanguageCode, IsFeedIncluded = request.Payload.FeedIncluded, - PubDateUtc = request.Payload.IsPublished ? DateTime.UtcNow : null, + PubDateUtc = request.Payload.IsPublished ? utcNow : null, IsDeleted = false, IsPublished = request.Payload.IsPublished, IsFeatured = request.Payload.Featured, diff --git a/src/Moonglade.Core/PostFeature/PostEditModel.cs b/src/Moonglade.Core/PostFeature/PostEditModel.cs index 6451f5ada..ab9e5588d 100644 --- a/src/Moonglade.Core/PostFeature/PostEditModel.cs +++ b/src/Moonglade.Core/PostFeature/PostEditModel.cs @@ -67,4 +67,7 @@ public class PostEditModel public bool IsOutdated { get; set; } public bool WarnSlugModification => PublishDate.HasValue && (DateTime.UtcNow - PublishDate.Value).Days > 3; + + [HiddenInput] + public string LastModifiedUtc { get; set; } } \ No newline at end of file diff --git a/src/Moonglade.Core/PostFeature/UnpublishPostCommand.cs b/src/Moonglade.Core/PostFeature/UnpublishPostCommand.cs new file mode 100644 index 000000000..46cb6efd2 --- /dev/null +++ b/src/Moonglade.Core/PostFeature/UnpublishPostCommand.cs @@ -0,0 +1,24 @@ +using Edi.CacheAside.InMemory; +using Moonglade.Data; + +namespace Moonglade.Core.PostFeature; + +public record UnpublishPostCommand(Guid Id) : IRequest; + +public class UnpublishPostCommandHandler(MoongladeRepository repo, ICacheAside cache) : IRequestHandler +{ + public async Task Handle(UnpublishPostCommand request, CancellationToken ct) + { + var post = await repo.GetByIdAsync(request.Id, ct); + if (null == post) return; + + post.IsPublished = false; + post.PubDateUtc = null; + post.RouteLink = null; + post.LastModifiedUtc = DateTime.UtcNow; + + await repo.UpdateAsync(post, ct); + + cache.Remove(BlogCachePartition.Post.ToString(), request.Id.ToString()); + } +} \ No newline at end of file diff --git a/src/Moonglade.Core/PostFeature/UpdatePostCommand.cs b/src/Moonglade.Core/PostFeature/UpdatePostCommand.cs index 1a388b48a..cb81ee2d2 100644 --- a/src/Moonglade.Core/PostFeature/UpdatePostCommand.cs +++ b/src/Moonglade.Core/PostFeature/UpdatePostCommand.cs @@ -48,6 +48,7 @@ public UpdatePostCommandHandler( public async Task Handle(UpdatePostCommand request, CancellationToken ct) { + var utcNow = DateTime.UtcNow; var (guid, postEditModel) = request; var post = await _postRepo.GetByIdAsync(guid, ct); if (null == post) @@ -73,7 +74,7 @@ public async Task Handle(UpdatePostCommand request, CancellationToke if (postEditModel.IsPublished && !post.IsPublished) { post.IsPublished = true; - post.PubDateUtc = DateTime.UtcNow; + post.PubDateUtc = utcNow; } // #325: Allow changing publish date for published posts @@ -87,7 +88,7 @@ public async Task Handle(UpdatePostCommand request, CancellationToke post.Author = postEditModel.Author?.Trim(); post.Slug = postEditModel.Slug.ToLower().Trim(); post.Title = postEditModel.Title.Trim(); - post.LastModifiedUtc = DateTime.UtcNow; + post.LastModifiedUtc = utcNow; post.IsFeedIncluded = postEditModel.FeedIncluded; post.ContentLanguageCode = postEditModel.LanguageCode; post.IsFeatured = postEditModel.Featured; diff --git a/src/Moonglade.Data.PostgreSql/Moonglade.Data.PostgreSql.csproj b/src/Moonglade.Data.PostgreSql/Moonglade.Data.PostgreSql.csproj index 25fbfcdd5..775db1296 100644 --- a/src/Moonglade.Data.PostgreSql/Moonglade.Data.PostgreSql.csproj +++ b/src/Moonglade.Data.PostgreSql/Moonglade.Data.PostgreSql.csproj @@ -10,7 +10,7 @@ enable - + diff --git a/src/Moonglade.ImageStorage/Moonglade.ImageStorage.csproj b/src/Moonglade.ImageStorage/Moonglade.ImageStorage.csproj index bdc8dd966..12cf10599 100644 --- a/src/Moonglade.ImageStorage/Moonglade.ImageStorage.csproj +++ b/src/Moonglade.ImageStorage/Moonglade.ImageStorage.csproj @@ -10,7 +10,7 @@ - + \ No newline at end of file diff --git a/src/Moonglade.Setup/BlogConfigInitializer.cs b/src/Moonglade.Setup/BlogConfigInitializer.cs index 70945dcd5..fb7e225a6 100644 --- a/src/Moonglade.Setup/BlogConfigInitializer.cs +++ b/src/Moonglade.Setup/BlogConfigInitializer.cs @@ -59,6 +59,10 @@ await mediator.Send(new AddDefaultConfigurationCommand(key, nameof(CustomMenuSet await mediator.Send(new AddDefaultConfigurationCommand(key, nameof(LocalAccountSettings), LocalAccountSettings.DefaultValue.ToJson())); break; + case 12: + await mediator.Send(new AddDefaultConfigurationCommand(key, nameof(SocialLinkSettings), + SocialLinkSettings.DefaultValue.ToJson())); + break; case 99: await mediator.Send(new AddDefaultConfigurationCommand(key, nameof(SystemManifestSettings), isNew ? diff --git a/src/Moonglade.Utils/ContentProcessor.cs b/src/Moonglade.Utils/ContentProcessor.cs index 883184db1..54ebee947 100644 --- a/src/Moonglade.Utils/ContentProcessor.cs +++ b/src/Moonglade.Utils/ContentProcessor.cs @@ -1,5 +1,6 @@ using Markdig; using System.Text.RegularExpressions; +using System.Web; using System.Xml.Linq; namespace Moonglade.Utils; @@ -26,10 +27,14 @@ public static string GetPostAbstract(string content, int wordCount, bool useMark MarkdownToContent(content, MarkdownConvertType.Text) : RemoveTags(content); - var result = plainText.Ellipsize(wordCount); + var decodedText = HtmlDecode(plainText); + var result = decodedText.Ellipsize(wordCount); return result; } + // Fix #833 - umlauts like (ä,ö,ü). are not displayed correctly in the abstract + public static string HtmlDecode(string content) => HttpUtility.HtmlDecode(content); + public static string RemoveTags(string html) { if (string.IsNullOrEmpty(html)) diff --git a/src/Moonglade.Web/Configuration/ConfigureEndpoints.cs b/src/Moonglade.Web/Configuration/ConfigureEndpoints.cs index a4af2d815..5c75d1ed6 100644 --- a/src/Moonglade.Web/Configuration/ConfigureEndpoints.cs +++ b/src/Moonglade.Web/Configuration/ConfigureEndpoints.cs @@ -9,7 +9,6 @@ public static Task WriteResponse(HttpContext context, HealthReport result) var obj = new { Helper.AppVersion, - DotNetVersion = Environment.Version.ToString(), EnvironmentTags = Helper.GetEnvironmentTags(), GeoMatch = context.Request.Headers["x-geo-match"] }; diff --git a/src/Moonglade.Web/Controllers/PostController.cs b/src/Moonglade.Web/Controllers/PostController.cs index 1ea6385da..78c48f13f 100644 --- a/src/Moonglade.Web/Controllers/PostController.cs +++ b/src/Moonglade.Web/Controllers/PostController.cs @@ -11,6 +11,7 @@ namespace Moonglade.Web.Controllers; [ApiController] [Route("api/[controller]")] public class PostController( + IConfiguration configuration, IMediator mediator, IBlogConfig blogConfig, ITimeZoneResolver timeZoneResolver, @@ -61,7 +62,20 @@ await mediator.Send(new CreatePostCommand(model)) : cannonService.FireAsync(async sender => await sender.SendWebmentionAsync(link.ToString(), postEntity.PostContent)); } - cannonService.FireAsync(async sender => await sender.SendRequestAsync(link)); + var isNewPublish = postEntity.LastModifiedUtc == postEntity.PubDateUtc; + + bool indexCoolDown = true; + var minimalIntervalMinutes = int.Parse(configuration["IndexNow:MinimalIntervalMinutes"]!); + if (!string.IsNullOrWhiteSpace(model.LastModifiedUtc)) + { + var lastSavedInterval = DateTime.Parse(model.LastModifiedUtc) - DateTime.UtcNow; + indexCoolDown = lastSavedInterval.TotalMinutes > minimalIntervalMinutes; + } + + if (isNewPublish || indexCoolDown) + { + cannonService.FireAsync(async sender => await sender.SendRequestAsync(link)); + } } return Ok(new { PostId = postEntity.Id }); @@ -117,6 +131,15 @@ public async Task EmptyRecycleBin() return NoContent(); } + [TypeFilter(typeof(ClearBlogCache), Arguments = [BlogCacheType.Subscription | BlogCacheType.SiteMap])] + [HttpPut("{postId:guid}/unpublish")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task Unpublish([NotEmpty] Guid postId) + { + await mediator.Send(new UnpublishPostCommand(postId)); + return NoContent(); + } + [IgnoreAntiforgeryToken] [HttpPost("keep-alive")] [ProducesResponseType(StatusCodes.Status200OK)] diff --git a/src/Moonglade.Web/Controllers/SettingsController.cs b/src/Moonglade.Web/Controllers/SettingsController.cs index 2e35972ae..2241a6a85 100644 --- a/src/Moonglade.Web/Controllers/SettingsController.cs +++ b/src/Moonglade.Web/Controllers/SettingsController.cs @@ -153,6 +153,26 @@ public async Task Advanced(AdvancedSettings model) return NoContent(); } + [HttpPost("social-link")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task SocialLink(SocialLinkSettingsJsonModel model) + { + if (model.IsEnabled && string.IsNullOrWhiteSpace(model.JsonData)) + { + ModelState.AddModelError(nameof(SocialLinkSettingsJsonModel.JsonData), "JsonData is required"); + return BadRequest(ModelState.CombineErrorMessages()); + } + + blogConfig.SocialLinkSettings = new() + { + IsEnabled = model.IsEnabled, + Links = model.JsonData.FromJson() + }; + + await SaveConfigAsync(blogConfig.SocialLinkSettings); + return NoContent(); + } + [HttpPost("reset")] [ProducesResponseType(StatusCodes.Status202Accepted)] public async Task Reset(BlogDbContext context, IHostApplicationLifetime applicationLifetime) diff --git a/src/Moonglade.Web/Middleware/PrefersColorSchemeMiddleware.cs b/src/Moonglade.Web/Middleware/PrefersColorSchemeMiddleware.cs index 593eafd29..ca2fbb833 100644 --- a/src/Moonglade.Web/Middleware/PrefersColorSchemeMiddleware.cs +++ b/src/Moonglade.Web/Middleware/PrefersColorSchemeMiddleware.cs @@ -2,13 +2,19 @@ public class PrefersColorSchemeMiddleware(RequestDelegate next) { - public async Task InvokeAsync(HttpContext context) + public async Task InvokeAsync(HttpContext context, IConfiguration configuration) { + var headerName = configuration["PrefersColorSchemeHeader:HeaderName"]; + if (string.IsNullOrWhiteSpace(headerName)) + { + await next(context); + } + context.Response.OnStarting(() => { - context.Response.Headers["Accept-CH"] = "Sec-CH-Prefers-Color-Scheme"; - context.Response.Headers["Vary"] = "Sec-CH-Prefers-Color-Scheme"; - context.Response.Headers["Critical-CH"] = "Sec-CH-Prefers-Color-Scheme"; + context.Response.Headers["Accept-CH"] = headerName; + context.Response.Headers["Vary"] = headerName; + context.Response.Headers["Critical-CH"] = headerName; return Task.CompletedTask; }); diff --git a/src/Moonglade.Web/Moonglade.Web.csproj b/src/Moonglade.Web/Moonglade.Web.csproj index 14ec8b136..87b9f87d5 100644 --- a/src/Moonglade.Web/Moonglade.Web.csproj +++ b/src/Moonglade.Web/Moonglade.Web.csproj @@ -43,7 +43,7 @@ - + diff --git a/src/Moonglade.Web/Pages/Admin/EditPost.cshtml b/src/Moonglade.Web/Pages/Admin/EditPost.cshtml index 1541f6ea9..d6f86b9ed 100644 --- a/src/Moonglade.Web/Pages/Admin/EditPost.cshtml +++ b/src/Moonglade.Web/Pages/Admin/EditPost.cshtml @@ -194,6 +194,7 @@
+
@@ -326,7 +327,7 @@ name="SelectedCatIds" value="@cat.Id" class="form-check-input" - @(cat.IsChecked ? "checked" : null)> + @(cat.IsChecked ? "checked" : null)>
+ + @if (Model.ViewModel.IsPublished) + { + + } @@ -362,4 +370,5 @@
+ \ No newline at end of file diff --git a/src/Moonglade.Web/Pages/Admin/EditPost.cshtml.cs b/src/Moonglade.Web/Pages/Admin/EditPost.cshtml.cs index 2b65e00df..3758f9f5d 100644 --- a/src/Moonglade.Web/Pages/Admin/EditPost.cshtml.cs +++ b/src/Moonglade.Web/Pages/Admin/EditPost.cshtml.cs @@ -58,7 +58,8 @@ public async Task OnGetAsync(Guid? id) Abstract = post.ContentAbstract.Replace("\u00A0\u2026", string.Empty), Featured = post.IsFeatured, HeroImageUrl = post.HeroImageUrl, - IsOutdated = post.IsOutdated + IsOutdated = post.IsOutdated, + LastModifiedUtc = post.LastModifiedUtc?.ToString("u") }; if (post.PubDateUtc is not null) diff --git a/src/Moonglade.Web/Pages/Admin/_UnpublishPostModal.cshtml b/src/Moonglade.Web/Pages/Admin/_UnpublishPostModal.cshtml new file mode 100644 index 000000000..2758e288a --- /dev/null +++ b/src/Moonglade.Web/Pages/Admin/_UnpublishPostModal.cshtml @@ -0,0 +1,36 @@ +@model Moonglade.Web.Pages.Admin.EditPostModel + + + + \ No newline at end of file diff --git a/src/Moonglade.Web/Pages/Components/SocialLink/Default.cshtml b/src/Moonglade.Web/Pages/Components/SocialLink/Default.cshtml new file mode 100644 index 000000000..26ce18ab1 --- /dev/null +++ b/src/Moonglade.Web/Pages/Components/SocialLink/Default.cshtml @@ -0,0 +1,19 @@ +@using Moonglade.Utils +@model SocialLink[] + +@if (Model.Any()) +{ +
+
@SharedLocalizer["Social Links"]
+ +
+ @foreach (var item in Model.OrderBy(c => c.Name)) + { + + + @item.Name + + } +
+
+} \ No newline at end of file diff --git a/src/Moonglade.Web/Pages/Settings/Advanced.cshtml b/src/Moonglade.Web/Pages/Settings/Advanced.cshtml index 9f6a7656d..6ad2d1951 100644 --- a/src/Moonglade.Web/Pages/Settings/Advanced.cshtml +++ b/src/Moonglade.Web/Pages/Settings/Advanced.cshtml @@ -1,5 +1,7 @@ @page "/admin/settings/advanced" @using System.Reflection +@inject IConfiguration Configuration + @Html.AntiForgeryToken() @{ var settings = BlogConfig.AdvancedSettings; @@ -102,7 +104,7 @@
- +
@@ -117,7 +119,7 @@
- +
@@ -144,6 +146,25 @@
+ +
+
+ +
+
+ +
+
+ @if (string.IsNullOrWhiteSpace(Configuration["IndexNow:ApiKey"])) + { + Not configured + } + else + { + @Configuration["IndexNow:ApiKey"] + } +
+
diff --git a/src/Moonglade.Web/Pages/Settings/SocialLinks.cshtml b/src/Moonglade.Web/Pages/Settings/SocialLinks.cshtml new file mode 100644 index 000000000..a74cef2f6 --- /dev/null +++ b/src/Moonglade.Web/Pages/Settings/SocialLinks.cshtml @@ -0,0 +1,83 @@ +@page "/admin/settings/social-links" +@Html.AntiForgeryToken() +@{ + var bc = BlogConfig.SocialLinkSettings; + var settings = new SocialLinkSettingsJsonModel + { + IsEnabled = bc.IsEnabled, + JsonData = bc.Links.ToJson(true) + }; +} + +@section scripts { + + + +} + +@section head { + +} + +@section admintoolbar { + +} + +
+
+ +
+ This feature is under development. It is currently in preview. A GUI editor will be available in the future. +
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+ +
+
+
+ + + +
+ +
+
\ No newline at end of file diff --git a/src/Moonglade.Web/Pages/Settings/_SettingsHeader.cshtml b/src/Moonglade.Web/Pages/Settings/_SettingsHeader.cshtml index ba7999249..1c01cdb6b 100644 --- a/src/Moonglade.Web/Pages/Settings/_SettingsHeader.cshtml +++ b/src/Moonglade.Web/Pages/Settings/_SettingsHeader.cshtml @@ -22,6 +22,9 @@ + diff --git a/src/Moonglade.Web/Pages/Shared/_Aside.cshtml b/src/Moonglade.Web/Pages/Shared/_Aside.cshtml index 6a47ad07d..18c2c2726 100644 --- a/src/Moonglade.Web/Pages/Shared/_Aside.cshtml +++ b/src/Moonglade.Web/Pages/Shared/_Aside.cshtml @@ -40,6 +40,13 @@ } + @if (BlogConfig.SocialLinkSettings.IsEnabled) + { + + } + @if (BlogConfig.GeneralSettings.WidgetsFriendLink) {