diff --git a/.gitignore b/.gitignore index 0ef59e9d..699ed78c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,7 @@ - -# Created by https://www.gitignore.io/api/csharp,aspnetcore,powershell,visualstudio -# Edit at https://www.gitignore.io/?templates=csharp,aspnetcore,powershell,visualstudio - -### ASPNETCore ### -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files *.suo *.user *.userosscache *.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ @@ -26,618 +12,8 @@ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ - -# Visual Studio 2015 cache/options directory .vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -project.fragment.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/ - -### Csharp ### -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser - -# User-specific files (MonoDevelop/Xamarin Studio) - -# Mono auto generated files -mono_crash.* - -# Build results -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ - -# Visual Studio 2015/2017 cache/options directory -# Uncomment if you have tasks that create the project's static files in wwwroot - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results - -# NUnit -nunit-*.xml - -# Build Results of an ATL Project - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_h.h -*.iobj -*.ipdb -*_wpftmp.csproj - -# Chutzpah Test files - -# Visual C++ cache files - -# Visual Studio profiler - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace - -# Guidance Automation Toolkit - -# ReSharper is a .NET coding add-in - -# JustCode is a .NET coding add-in - -# TeamCity is a build add-in - -# DotCover is a Code Coverage Tool - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results - -# NCrunch - -# MightyMoose - -# Web workbench (sass) - -# Installshield output folder - -# DocProject is a documentation generator add-in - -# Click-Once directory - -# Publish Web Output -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted - -# NuGet Packages -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files - -# Microsoft Azure Build Output - -# Microsoft Azure Emulator - -# Windows Store app package directories and files -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) - -# RIA/Silverlight projects - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.ndf - -# Business Intelligence projects -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes - -# GhostDoc plugin setting file - -# Node.js Tools for Visual Studio - -# Visual Studio 6 build log - -# Visual Studio 6 workspace options file - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output - -# Paket dependency manager - -# FAKE - F# Make - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -### PowerShell ### -# Exclude packaged modules -*.zip - -# Exclude .NET assemblies from source -*.dll - -### VisualStudio ### - -# User-specific files - -# User-specific files (MonoDevelop/Xamarin Studio) - -# Mono auto generated files - -# Build results - -# Visual Studio 2015/2017 cache/options directory -# Uncomment if you have tasks that create the project's static files in wwwroot - -# Visual Studio 2017 auto generated files - -# MSTest test Results - -# NUnit - -# Build Results of an ATL Project - -# Benchmark Results - -# .NET Core - -# StyleCop - -# Files built by Visual Studio - -# Chutzpah Test files - -# Visual C++ cache files - -# Visual Studio profiler - -# Visual Studio Trace Files - -# TFS 2012 Local Workspace - -# Guidance Automation Toolkit - -# ReSharper is a .NET coding add-in - -# JustCode is a .NET coding add-in - -# TeamCity is a build add-in - -# DotCover is a Code Coverage Tool - -# AxoCover is a Code Coverage Tool - -# Visual Studio code coverage results - -# NCrunch - -# MightyMoose - -# Web workbench (sass) - -# Installshield output folder - -# DocProject is a documentation generator add-in - -# Click-Once directory - -# Publish Web Output -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted - -# NuGet Packages -# NuGet Symbol Packages -# The packages folder can be ignored because of Package Restore -# except build/, which is used as an MSBuild target. -# Uncomment if necessary however generally it will be regenerated when needed -# NuGet v3's project.json files produces more ignorable files - -# Microsoft Azure Build Output - -# Microsoft Azure Emulator - -# Windows Store app package directories and files - -# Visual Studio cache files -# files ending in .cache can be ignored -# but keep track of directories ending in .cache - -# Others - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) - -# RIA/Silverlight projects - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) - -# SQL Server files - -# Business Intelligence projects - -# Microsoft Fakes - -# GhostDoc plugin setting file - -# Node.js Tools for Visual Studio - -# Visual Studio 6 build log - -# Visual Studio 6 workspace options file - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) - -# Visual Studio LightSwitch build output - -# Paket dependency manager - -# FAKE - F# Make - -# CodeRush personal settings - -# Python Tools for Visual Studio (PTVS) - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio - -# Telerik's JustMock configuration file - -# BizTalk build output - -# OpenCover UI analysis results - -# Azure Stream Analytics local run output - -# MSBuild Binary and Structured Log - -# NVidia Nsight GPU debugger configuration file - -# MFractors (Xamarin productivity tool) working folder - -# Local History for Visual Studio - -# BeatPulse healthcheck temp database - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 - -# End of https://www.gitignore.io/api/csharp,aspnetcore,powershell,visualstudio \ No newline at end of file +UpgradeLog*.htm \ No newline at end of file diff --git a/src/blog/LICENSE b/LICENSE similarity index 100% rename from src/blog/LICENSE rename to LICENSE diff --git a/README.md b/README.md new file mode 100644 index 00000000..137123ac --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +Source of building my personal websites. + +There are three branches for different purposes: + +- master: This is the source for production environment, running at https://blog.laobian.me +- stage: This is the source for staging environment. While new development is kicked off, we can reach it at https://stage.blog.laobian.me. Having said that, it was disabled by default. +- dev: This is the latest source code for development. + +### Build + +Clone repository to locally. You need tools like Microsoft Visual Studio 2019+, and npm, they will be used in normal development. + +Before building the source, make sure you have .NET Core 3.0 installed. + +```cs +dotnet --info +``` + +Now just start the msbuild, either in the CLI or visual studio. + +For the assets configuration, you can just refer to the code... + +### License + +MIT. \ No newline at end of file diff --git a/src/blog/BlogServiceRegister.cs b/src/blog/BlogServiceRegister.cs deleted file mode 100644 index 69d32483..00000000 --- a/src/blog/BlogServiceRegister.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Text.Encodings.Web; -using System.Text.Unicode; -using Laobian.Blog.HostedService; -using Laobian.Share.BlogEngine; -using Laobian.Share.Config; -using Laobian.Share.Infrastructure.Cache; -using Laobian.Share.Infrastructure.Command; -using Laobian.Share.Infrastructure.GitHub; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Laobian.Blog -{ - public class BlogServiceRegister - { - public static void Register(IServiceCollection services, IConfiguration config) - { - services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.BasicLatin, UnicodeRanges.CjkUnifiedIdeographs)); - services.Configure(ac => - { - ac.SendGridApiKey = config.GetValue("SEND_GRID_API_KEY"); - ac.AssetGitHubRepoApiToken = config.GetValue("ASSET_GITHUB_REPO_API_TOKEN"); - ac.AssetGitHubRepoBranch = config.GetValue("ASSET_GITHUB_REPO_BRANCH"); - ac.AssetGitHubRepoOwner = config.GetValue("ASSET_GITHUB_REPO_OWNER"); - ac.AssetGitHubRepoName = config.GetValue("ASSET_GITHUB_REPO_NAME"); - ac.AssetRepoLocalDir = config.GetValue("ASSET_REPO_LOCAL_DIR"); - }); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddHostedService(); - } - } -} diff --git a/src/blog/Controllers/AboutController.cs b/src/blog/Controllers/AboutController.cs index 320c9823..01eee8e0 100644 --- a/src/blog/Controllers/AboutController.cs +++ b/src/blog/Controllers/AboutController.cs @@ -1,41 +1,26 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Laobian.Share.BlogEngine; -using Laobian.Share.Config; -using Laobian.Share.Infrastructure.Cache; -using Markdig; +using Laobian.Share.BlogEngine; +using Laobian.Share.BlogEngine.Model; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; namespace Laobian.Blog.Controllers { public class AboutController : Controller { - private readonly AppConfig _appConfig; - private readonly IMemoryCacheClient _cacheClient; + private readonly IBlogService _blogService; - public AboutController(IMemoryCacheClient cacheClient, IOptions appConfig) + public AboutController(IBlogService blogService) { - _appConfig = appConfig.Value; - _cacheClient = cacheClient; + _blogService = blogService; } - public async Task Index() + [ResponseCache(CacheProfileName = "Cache1Day")] + public IActionResult Index() { - var html = await _cacheClient.GetOrAddAsync(BlogConstant.EnglishAboutMemCacheKey, async () => - { - var localPath = Path.Combine(_appConfig.AssetRepoLocalDir, BlogConstant.EnAboutGitHub); - if (!System.IO.File.Exists(localPath)) - { - return string.Empty; - } - - var md = await System.IO.File.ReadAllTextAsync(localPath); - return Markdown.ToHtml(md); - }, TimeSpan.FromDays(1)); + var html = _blogService.GetAboutHtml(RequestLang.English); ViewData["Title"] = "关于"; + ViewData["Canonical"] = "/about/"; + ViewData["Description"] = "关于作者以及这个博客的一切"; return View(model: html); } } diff --git a/src/blog/Controllers/ArchiveController.cs b/src/blog/Controllers/ArchiveController.cs index 4b8139d9..d12c1273 100644 --- a/src/blog/Controllers/ArchiveController.cs +++ b/src/blog/Controllers/ArchiveController.cs @@ -17,57 +17,82 @@ public ArchiveController(IBlogService blogService) } [Route("/category")] + [ResponseCache(CacheProfileName = "Cache10Sec")] public IActionResult Category() { var model = new List(); - var posts = _blogService.GetPosts().Where(_ => _.IsPublic).ToList(); + var posts = _blogService.GetPublishedPosts(); var cats = _blogService.GetCategories(); foreach (var blogCategory in cats) { var catModel = new ArchiveViewModel(blogCategory.Name, blogCategory.Link); - catModel.Posts.AddRange(posts.Where(p=>p.CategoryNames.Contains(blogCategory.Name, StringComparer.OrdinalIgnoreCase))); + catModel.Posts.AddRange(posts.Where(p => p.CategoryNames.Contains(blogCategory.Name, StringComparer.OrdinalIgnoreCase))); model.Add(catModel); } + var remainingPosts = posts.Except(model.SelectMany(_ => _.Posts).Distinct()).ToList(); + if (remainingPosts.Any()) + { + var catModel = new ArchiveViewModel(BlogConstant.DefaultCategoryName, BlogConstant.DefaultCategoryLink); + catModel.Posts.AddRange(remainingPosts); + model.Add(catModel); + } + ViewData["Title"] = "分类"; + ViewData["Canonical"] = "/category/"; + ViewData["Description"] = "所有文章以分类的形式展现"; return View("Index", model); } [Route("/tag")] + [ResponseCache(CacheProfileName = "Cache10Sec")] public IActionResult Tag() { var model = new List(); - var posts = _blogService.GetPosts().Where(_ => _.IsPublic).ToList(); + var posts = _blogService.GetPublishedPosts(); var tags = _blogService.GetTags(); foreach (var tag in tags) { - var catModel = new ArchiveViewModel(tag.Name, tag.Link); - catModel.Posts.AddRange(posts.Where(p => p.TagNames.Contains(tag.Name, StringComparer.OrdinalIgnoreCase))); + var tagModel = new ArchiveViewModel(tag.Name, tag.Link); + tagModel.Posts.AddRange(posts.Where(p => p.TagNames.Contains(tag.Name, StringComparer.OrdinalIgnoreCase))); - model.Add(catModel); + model.Add(tagModel); + } + + var remainingPosts = posts.Except(model.SelectMany(_ => _.Posts).Distinct()).ToList(); + if (remainingPosts.Any()) + { + var tagModel = new ArchiveViewModel(BlogConstant.DefaultTagName, BlogConstant.DefaultTagLink); + tagModel.Posts.AddRange(remainingPosts); + model.Add(tagModel); } ViewData["Title"] = "标签"; + ViewData["Canonical"] = "/tag/"; + ViewData["Description"] = "所有文章以标签归类的形式展现"; return View("Index", model); } [Route("/archive")] + [ResponseCache(CacheProfileName = "Cache10Sec")] public IActionResult Date() { var model = new List(); - var posts = _blogService.GetPosts().Where(_ => _.IsPublic).ToList(); + var posts = _blogService.GetPublishedPosts(); var dates = posts.Select(_ => _.CreationTimeUtc.Year).Distinct(); foreach (var date in dates) { var catModel = new ArchiveViewModel($"{date} 年", date.ToString()); - catModel.Posts.AddRange(posts.Where(p => p.CreationTimeUtc.Year == date)); + catModel.Posts.AddRange(posts.Where(p => p.CreationTimeUtc.Year == date).OrderByDescending(p => p.CreationTimeUtc)); model.Add(catModel); } ViewData["Title"] = "存档"; - return View("Index", model); + ViewData["Canonical"] = "/archive/"; + ViewData["Description"] = "所有文章以发表日期归类的形式展现"; + return View("Index", model.OrderByDescending(m => m.Name)); } } } \ No newline at end of file diff --git a/src/blog/Controllers/GitHubController.cs b/src/blog/Controllers/GitHubController.cs index 936a4bee..738ba16a 100644 --- a/src/blog/Controllers/GitHubController.cs +++ b/src/blog/Controllers/GitHubController.cs @@ -1,11 +1,16 @@ using System; +using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using Laobian.Share.BlogEngine; -using Laobian.Share.Infrastructure.GitHub; +using Laobian.Share.Config; +using Laobian.Share.Helper; +using Laobian.Share.Infrastructure.Git; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Laobian.Blog.Controllers { @@ -13,65 +18,106 @@ namespace Laobian.Blog.Controllers [Route("github")] public class GitHubController : ControllerBase { + private readonly AppConfig _appConfig; private readonly IBlogService _blogService; + private readonly ILogger _logger; - public GitHubController(IBlogService blogService) + public GitHubController(IBlogService blogService, IOptions appConfig, ILogger logger) { + _logger = logger; _blogService = blogService; + _appConfig = appConfig.Value; } // http://michaco.net/blog/HowToValidateGitHubWebhooksInCSharpWithASPNETCoreMVC [HttpPost] [Route("hook")] - public async Task Hook(GitHubPayload payload) + public async Task Hook() { - if (!Request.Headers.ContainsKey("X-GitHub-Event")) + if (!Request.Headers.ContainsKey("X-GitHub-Event") || + !Request.Headers.ContainsKey("X-Hub-Signature") || + !Request.Headers.ContainsKey("X-GitHub-Delivery")) { - return BadRequest(); + _logger.LogWarning("Headers are not completed."); + return BadRequest("Invalid Request."); } - var gitHubEvent = Request.Headers["X-GitHub-Event"]; - if (!string.Equals("push", gitHubEvent, StringComparison.OrdinalIgnoreCase)) + + if (!StringEqualsHelper.IgnoreCase("push", Request.Headers["X-GitHub-Event"])) { - return BadRequest(); + _logger.LogWarning("Invalid github event {Event}", Request.Headers["X-GitHub-Event"]); + return BadRequest("Only support push event."); } - if (!Request.Headers.ContainsKey("X-Hub-Signature")) + var signature = Request.Headers["X-Hub-Signature"].ToString(); + if (!signature.StartsWith("sha1=", StringComparison.OrdinalIgnoreCase)) { - return BadRequest(); + _logger.LogWarning("Invalid github signature {Signature}", signature); + return BadRequest("Invalid signature."); } - using (var sha1 = SHA1.Create()) + using (var reader = new StreamReader(Request.Body)) { - var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes("abc")); - var a = Convert.ToBase64String(hash); - var d = string.Concat(hash.Select(b => b.ToString("x2"))); + var body = await reader.ReadToEndAsync(); + signature = signature.Substring("sha1=".Length); + var secret = Encoding.UTF8.GetBytes(_appConfig.AssetGitHubHookSecret); + var bodyBytes = Encoding.UTF8.GetBytes(body); - } - //if(!string.Equals()) + using (var hmacSha1 = new HMACSHA1(secret)) + { + var hash = hmacSha1.ComputeHash(bodyBytes); + var builder = new StringBuilder(hash.Length * 2); + foreach (var b in hash) + { + builder.AppendFormat("{0:x2}", b); + } - if (payload.Commits.Any(_ => GitHubMessageProvider.IsServerCommit(_.Message))) - { - return Ok("Server update, no need to update local."); - } + var hashStr = builder.ToString(); - await _blogService.UpdateLocalAssetsAsync(); + if (!hashStr.Equals(signature)) + { + _logger.LogWarning("Invalid github signature {Signature}, {HashString}", signature, hashStr); + return BadRequest("Invalid signature."); + } + } - var modifiedPosts = payload.Commits.SelectMany(c => c.Modified).ToList(); - if (modifiedPosts.Any()) - { - var posts = _blogService.GetPosts(); - foreach (var blogPost in posts) + var payload = SerializeHelper.FromJson(body); + if (payload.Commits.Any(c => + StringEqualsHelper.IgnoreCase(_appConfig.AssetGitCommitEmail, c.Author.Email) && + StringEqualsHelper.IgnoreCase(_appConfig.AssetGitCommitUser, c.Author.User))) { - var modifiedPost = modifiedPosts.FirstOrDefault(p => - string.Equals(p, blogPost.GitHubPath, StringComparison.OrdinalIgnoreCase)); - if (modifiedPost != null) + _logger.LogInformation("Got request from server, no need to refresh."); + return Ok("No need to refresh."); + } + + var modifiedPosts = payload.Commits.SelectMany(c => c.Modified).Distinct().ToList(); + await _blogService.UpdateMemoryAssetsAsync(); + _logger.LogInformation("Local assets refreshed."); + + bool requireCommit = false; + + if (modifiedPosts.Any()) + { + var posts = _blogService.GetPosts(); + foreach (var blogPost in posts) { - blogPost.LastUpdateTimeUtc = DateTime.UtcNow; + var modifiedPost = modifiedPosts.FirstOrDefault(p => + string.Equals(p, blogPost.GitHubPath, StringComparison.OrdinalIgnoreCase)); + if (modifiedPost != null) + { + requireCommit = true; + blogPost.LastUpdateTimeUtc = DateTime.UtcNow; + } } } - } - return Ok("Local updated."); + if (requireCommit || payload.Commits.SelectMany(c => c.Added).Any()) + { + await _blogService.UpdateCloudAssetsAsync(); + _logger.LogInformation("Cloud assets updated."); + } + + return Ok("Local updated."); + } } } } diff --git a/src/blog/Controllers/HomeController.cs b/src/blog/Controllers/HomeController.cs index cd6640e1..6158eb1c 100644 --- a/src/blog/Controllers/HomeController.cs +++ b/src/blog/Controllers/HomeController.cs @@ -1,29 +1,32 @@ using Microsoft.AspNetCore.Mvc; using Laobian.Blog.Models; -using System.Threading.Tasks; using System; using System.Collections.Generic; using System.Linq; +using System.Text; using Laobian.Share.BlogEngine; +using Laobian.Share.Config; using Laobian.Share.Extension; +using Laobian.Share.Helper; +using Microsoft.Extensions.Options; namespace Laobian.Blog.Controllers { public class HomeController : Controller { + private readonly AppConfig _appConfig; private readonly IBlogService _blogService; - public HomeController(IBlogService blogService) + public HomeController(IBlogService blogService, IOptions appConfig) { + _appConfig = appConfig.Value; _blogService = blogService; } + [ResponseCache(CacheProfileName = "Cache10Sec")] public IActionResult Index([FromQuery] int p) { - const int pageSize = 8; - var publishedPosts = _blogService.GetPosts().Where(_ => _.IsPublic).OrderByDescending(_ => _.CreationTimeUtc).ToList(); - var pagination = new Pagination(p, (int)Math.Ceiling(publishedPosts.Count() / (double)pageSize)); - var posts = publishedPosts.ToPaged(pageSize, pagination.CurrentPage); + var posts = _blogService.GetPagedPublishedPosts(ref p, out var totalPages); var categories = _blogService.GetCategories(); var tags = _blogService.GetTags(); var postViewModels = new List(); @@ -33,7 +36,7 @@ public IActionResult Index([FromQuery] int p) foreach (var blogPostCategoryName in blogPost.CategoryNames) { var cat = categories.FirstOrDefault(_ => - string.Equals(_.Name, blogPostCategoryName, StringComparison.OrdinalIgnoreCase)); + StringEqualsHelper.IgnoreCase(_.Name, blogPostCategoryName)); if (cat != null) { postViewModel.Categories.Add(cat); @@ -43,7 +46,7 @@ public IActionResult Index([FromQuery] int p) foreach (var blogPostTagName in blogPost.TagNames) { var tag = tags.FirstOrDefault(_ => - string.Equals(_.Name, blogPostTagName, StringComparison.OrdinalIgnoreCase)); + StringEqualsHelper.IgnoreCase(_.Name, blogPostTagName)); if (tag != null) { postViewModel.Tags.Add(tag); @@ -53,33 +56,76 @@ public IActionResult Index([FromQuery] int p) postViewModels.Add(postViewModel); } - ViewData["robots"] = "index,follow,archive"; - ViewData["canonical"] = "/"; - - if (pagination.CurrentPage > 1) + if (p > 1) { - ViewData["Title"] = $"第{pagination.CurrentPage}页"; - ViewData["robots"] = "noindex,nofollow"; + ViewData["Title"] = $"第{p}页"; + ViewData["Robots"] = "noindex, nofollow"; } - return View(new PagedPostViewModel { Pagination = pagination, Posts = postViewModels, Url = Request.Path }); + ViewData["Canonical"] = "/"; + return View(new PagedPostViewModel { CurrentPage = p, TotalPages = totalPages, Posts = postViewModels, Url = Request.Path }); } - [Route("{year:int}/{month:int}/{url}.html")] - public async Task Post(int year, int month, string url) + [Route("/sitemap")] + [Route("/sitemap.xml")] + [ResponseCache(VaryByHeader = "Accept-Encoding", Duration = 60 * 60 * 24, Location = ResponseCacheLocation.Any)] + public IActionResult SiteMap() { - var post = _blogService.GetPost(year, month, url); + var publishedPosts = _blogService.GetPublishedPosts(); + var urlSet = new SiteMapUrlSet(); + var urls = new List + { + new SiteMapUrl + { + Loc = AddressHelper.GetAddress(_appConfig.BlogAddress), + ChangeFreq = "weekly", + LastMod = DateTime.UtcNow.ToChinaTime().ToDate(), + Priority = 1.0 + }, + new SiteMapUrl + { + Loc = AddressHelper.GetAddress(_appConfig.BlogAddress, true, "about"), + ChangeFreq = "monthly", + LastMod = DateTime.UtcNow.ToChinaTime().ToDate(), + Priority = 0.9 + }, + new SiteMapUrl + { + Loc = AddressHelper.GetAddress(_appConfig.BlogAddress, true, "archive"), + ChangeFreq = "weekly", + LastMod = DateTime.UtcNow.ToChinaTime().ToDate(), + Priority = 0.8 + }, + new SiteMapUrl + { + Loc = AddressHelper.GetAddress(_appConfig.BlogAddress, true, "category"), + ChangeFreq = "weekly", + LastMod = DateTime.UtcNow.ToChinaTime().ToDate(), + Priority = 0.7 + }, + new SiteMapUrl + { + Loc = AddressHelper.GetAddress(_appConfig.BlogAddress, true, "tag"), + ChangeFreq = "weekly", + LastMod = DateTime.UtcNow.ToChinaTime().ToDate(), + Priority = 0.6 + } + }; - if (post == null) + foreach (var publishedPost in publishedPosts) { - return NotFound(); + urls.Add(new SiteMapUrl + { + Loc = publishedPost.FullUrlWithBaseAddress, + ChangeFreq = "daily", + LastMod = publishedPost.LastUpdateTimeUtc.ToChinaTime().ToDate(), + Priority = 0.5 + }); } - ViewData["robots"] = "index,follow,archive"; - ViewData["canonical"] = post.FullUrl; - ViewData["Title"] = post.Title; - - return View(post); + urlSet.Urls = urls; + var xml = SerializeHelper.ToXml(urlSet, ns: "http://www.sitemaps.org/schemas/sitemap/0.9"); + return Content(xml, "text/xml", Encoding.UTF8); } } } diff --git a/src/blog/Controllers/PostController.cs b/src/blog/Controllers/PostController.cs new file mode 100644 index 00000000..d73124f5 --- /dev/null +++ b/src/blog/Controllers/PostController.cs @@ -0,0 +1,63 @@ +using System.Linq; +using Laobian.Blog.Models; +using Laobian.Share.BlogEngine; +using Laobian.Share.Helper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Laobian.Blog.Controllers +{ + public class PostController : Controller + { + private readonly IBlogService _blogService; + private readonly ILogger _logger; + + public PostController(IBlogService blogService, ILogger logger) + { + _logger = logger; + _blogService = blogService; + } + + [Route("{year:int}/{month:int}/{url}.html")] + [ResponseCache(CacheProfileName = "Cache10Sec")] + public IActionResult Index(int year, int month, string url) + { + var post = _blogService.GetPost(year, month, url); + + if (post == null) + { + _logger.LogWarning("Request post not exists. {Year}, {Month}, {Link}", year, month, url); + return NotFound(); + } + + var categories = _blogService.GetCategories(); + var tags = _blogService.GetTags(); + var postViewModel = new PostViewModel(post); + foreach (var blogPostCategoryName in post.CategoryNames) + { + var cat = categories.FirstOrDefault(_ => + StringEqualsHelper.IgnoreCase(_.Name, blogPostCategoryName)); + if (cat != null) + { + postViewModel.Categories.Add(cat); + } + } + + foreach (var blogPostTagName in post.TagNames) + { + var tag = tags.FirstOrDefault(_ => + StringEqualsHelper.IgnoreCase(_.Name, blogPostTagName)); + if (tag != null) + { + postViewModel.Tags.Add(tag); + } + } + + ViewData["Canonical"] = post.FullUrlWithBaseAddress; + ViewData["Title"] = post.Title; + ViewData["Description"] = post.ExcerptText; + + return View(postViewModel); + } + } +} diff --git a/src/blog/Controllers/SubscribeController.cs b/src/blog/Controllers/SubscribeController.cs new file mode 100644 index 00000000..633d4825 --- /dev/null +++ b/src/blog/Controllers/SubscribeController.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.ServiceModel.Syndication; +using System.Text; +using System.Xml; +using Laobian.Share.BlogEngine; +using Laobian.Share.Config; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace Laobian.Blog.Controllers +{ + public class SubscribeController : Controller + { + private readonly AppConfig _appConfig; + private readonly IBlogService _blogService; + + public SubscribeController(IOptions appConfig, IBlogService blogService) + { + _appConfig = appConfig.Value; + _blogService = blogService; + } + + [Route("/rss")] + [ResponseCache(CacheProfileName = "Cache1Hour")] + public IActionResult Rss() + { + var feed = GetFeed(); + var rssFormatter = new Rss20FeedFormatter(feed) { SerializeExtensionsAsAtom = false }; + using (var ms = new MemoryStream()) + { + using (var xmlWriter = XmlWriter.Create(ms, new XmlWriterSettings { Async = true, Encoding = Encoding.UTF8 })) + { + rssFormatter.WriteTo(xmlWriter); + } + + return Content(Encoding.UTF8.GetString(ms.ToArray()), "application/rss+xml", Encoding.UTF8); + } + } + + [Route("/atom")] + [ResponseCache(CacheProfileName = "Cache1Hour")] + public IActionResult Atom() + { + var feed = GetFeed(); + var atomFormatter = new Atom10FeedFormatter(feed); + using (var ms = new MemoryStream()) + { + using (var xmlWriter = XmlWriter.Create(ms, new XmlWriterSettings { Async = true, Encoding = Encoding.UTF8 })) + { + atomFormatter.WriteTo(xmlWriter); + } + + return Content(Encoding.UTF8.GetString(ms.ToArray()), "application/atom+xml", Encoding.UTF8); + } + } + + private SyndicationFeed GetFeed() + { + var feed = new SyndicationFeed("", "", new Uri(_appConfig.BlogAddress)) + { + Title = new TextSyndicationContent(BlogConstant.BlogName, TextSyndicationContentKind.Plaintext), + Copyright = new TextSyndicationContent( + $"Copyright {DateTime.UtcNow.Year} {BlogConstant.AuthorChineseName}", + TextSyndicationContentKind.Plaintext), + Description = + new TextSyndicationContent(BlogConstant.BlogDescription, TextSyndicationContentKind.Plaintext), + Generator = "ASP.NET CORE", + BaseUri = new Uri(_appConfig.BlogAddress), + Id = _appConfig.BlogAddress, + Language = "zh-cn", + LastUpdatedTime = DateTimeOffset.UtcNow, + TimeToLive = TimeSpan.FromHours(1) + }; + + var sp = new SyndicationPerson(BlogConstant.AuthorEmail, BlogConstant.AuthorChineseName, _appConfig.BlogAddress); + feed.Authors.Add(sp); + feed.Contributors.Add(sp); + + var items = new List(); + var posts = _blogService.GetPosts().Where(p => p.IsPublic).OrderByDescending(p => p.CreationTimeUtc) + .ToList(); + foreach (var blogPost in posts) + { + var item = new SyndicationItem + { + Title = new TextSyndicationContent(blogPost.Title, TextSyndicationContentKind.Plaintext), + Copyright = feed.Copyright, + Id = blogPost.FullUrlWithBaseAddress, + PublishDate = new DateTimeOffset(blogPost.CreationTimeUtc, TimeSpan.Zero), + Summary = new TextSyndicationContent(blogPost.Excerpt, TextSyndicationContentKind.Html), + Content = new TextSyndicationContent(blogPost.HtmlContent, TextSyndicationContentKind.Html), + LastUpdatedTime = new DateTimeOffset(blogPost.LastUpdateTimeUtc, TimeSpan.Zero) + }; + + item.AddPermalink(new Uri(blogPost.FullUrlWithBaseAddress)); + item.Authors.Add(sp); + item.Contributors.Add(sp); + + foreach (var cat in blogPost.CategoryNames) + { + item.Categories.Add(new SyndicationCategory(cat)); + } + + items.Add(item); + } + + feed.Items = items; + + return feed; + } + } +} diff --git a/src/blog/Helpers/HtmlHeaderHelper.cs b/src/blog/Helpers/HtmlHeaderHelper.cs new file mode 100644 index 00000000..05e7c6d6 --- /dev/null +++ b/src/blog/Helpers/HtmlHeaderHelper.cs @@ -0,0 +1,46 @@ +using Laobian.Share.BlogEngine; + +namespace Laobian.Blog.Helpers +{ + public class HtmlHeaderHelper + { + public static string BuildMetaDescription(dynamic desc) + { + string str = desc?.ToString(); + if (string.IsNullOrEmpty(str)) + { + str = string.Empty; + } + else + { + str += "... "; + } + + str += BlogConstant.BlogDescription; + + return str.Length >= 150 ? str.Substring(0, 150) : str; + } + + public static string BuildTitle(dynamic title) + { + string t = title?.ToString(); + return string.IsNullOrEmpty(t) ? BlogConstant.BlogName : $"{title} - {BlogConstant.BlogName}"; + } + + public static string BuildMetaRobots(dynamic robots) + { + string r = robots?.ToString(); + if (!string.IsNullOrEmpty(r)) + { + return r; + } + + if (!BlogState.IsProdEnvironment) + { + return "noindex, nofollow"; + } + + return "index, follow, archive"; + } + } +} diff --git a/src/blog/Helpers/StartupHelper.cs b/src/blog/Helpers/StartupHelper.cs new file mode 100644 index 00000000..c9e3df91 --- /dev/null +++ b/src/blog/Helpers/StartupHelper.cs @@ -0,0 +1,47 @@ +using Laobian.Blog.HostedService; +using Laobian.Share.BlogEngine; +using Laobian.Share.Config; +using Laobian.Share.Infrastructure.Cache; +using Laobian.Share.Infrastructure.Command; +using Laobian.Share.Infrastructure.Email; +using Laobian.Share.Infrastructure.Git; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Laobian.Blog.Helpers +{ + public class StartupHelper + { + public static void RegisterService(IServiceCollection services, IConfiguration config) + { + + services.Configure(ac => MapConfig(config, ac)); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddHostedService(); + } + + private static void MapConfig(IConfiguration config, AppConfig ac) + { + ac.SendGridApiKey = config.GetValue("SEND_GRID_API_KEY"); + ac.AssetGitHubRepoApiToken = config.GetValue("ASSET_GITHUB_REPO_API_TOKEN"); + ac.AssetGitHubRepoBranch = config.GetValue("ASSET_GITHUB_REPO_BRANCH"); + ac.AssetGitHubRepoOwner = config.GetValue("ASSET_GITHUB_REPO_OWNER"); + ac.AssetGitHubRepoName = config.GetValue("ASSET_GITHUB_REPO_NAME"); + ac.AssetRepoLocalDir = config.GetValue("ASSET_REPO_LOCAL_DIR"); + ac.CloneAssetsDuringStartup = config.GetValue("STARTUP_CLONE_ASSETS", true); + ac.AssetGitCommitUser = config.GetValue("ASSET_LOCAL_COMMIT_USER_NAME", "bot"); + ac.AssetGitCommitEmail = config.GetValue("ASSET_LOCAL_COMMIT_USER_EMAIL", "bot@laobian.me"); + ac.BlogAddress = config.GetValue("BLOG_ADDRESS", "https://blog.laobian.me/"); + ac.AssetGitHubHookSecret = config.GetValue("ASSET_GITHUB_HOOK_SECRET", "test"); + ac.PostUpdateScheduled = config.GetValue("POST_UPDATE_SCHEDULED", false); + ac.PostUpdateAtHour = config.GetValue("POST_UPDATE_AT_HOUR", 3); + ac.PostUpdateEverySeconds = config.GetValue("POST_UPDATE_EVERY_SECONDS", 5 * 60); + } + } +} diff --git a/src/blog/HostedService/BlogAssetHostedService.cs b/src/blog/HostedService/BlogAssetHostedService.cs deleted file mode 100644 index 5719bf47..00000000 --- a/src/blog/HostedService/BlogAssetHostedService.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Laobian.Share.BlogEngine; -using Laobian.Share.Config; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; - -namespace Laobian.Blog.HostedService -{ - public class BlogAssetHostedService : BackgroundService - { - private readonly AppConfig _appConfig; - private readonly IBlogService _blogService; - - public BlogAssetHostedService(IBlogService blogService, IOptions appConfig) - { - _blogService = blogService; - _appConfig = appConfig.Value; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) - { - await Task.Delay(TimeSpan.FromDays(1), stoppingToken); - try - { - await _blogService.UpdateCloudAssetsAsync(); - } - catch (Exception ex) - { - - } - } - } - - public override async Task StartAsync(CancellationToken cancellationToken) - { - await _blogService.UpdateLocalAssetsAsync(); - await base.StartAsync(cancellationToken); - } - - public override async Task StopAsync(CancellationToken cancellationToken) - { - await _blogService.UpdateCloudAssetsAsync(); - await base.StopAsync(cancellationToken); - } - } -} diff --git a/src/blog/HostedService/PostHostedService.cs b/src/blog/HostedService/PostHostedService.cs new file mode 100644 index 00000000..b7aec16c --- /dev/null +++ b/src/blog/HostedService/PostHostedService.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Laobian.Share.BlogEngine; +using Laobian.Share.Config; +using Laobian.Share.Extension; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Laobian.Blog.HostedService +{ + public class PostHostedService : BackgroundService + { + private DateTime _lastExecuted; + private readonly AppConfig _appConfig; + private readonly IBlogService _blogService; + private readonly ILogger _logger; + + public PostHostedService(IBlogService blogService, IOptions appConfig, ILogger logger) + { + _logger = logger; + _blogService = blogService; + _appConfig = appConfig.Value; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + if (_appConfig.PostUpdateScheduled) + { + var chinaTime = DateTime.UtcNow.ToChinaTime(); + if (chinaTime.Hour == _appConfig.PostUpdateAtHour && _lastExecuted.Date < chinaTime.Date) + { + _lastExecuted = chinaTime; + await _blogService.UpdateCloudAssetsAsync(); + _logger.LogInformation( + "Post hosted service scheduled executed completely, last executed at = {LastExecutedAt}, scheduled at hour = {Hour}", + _lastExecuted.ToDateAndTime(), + _appConfig.PostUpdateAtHour); + } + + await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); + } + else + { + await Task.Delay(TimeSpan.FromSeconds(_appConfig.PostUpdateEverySeconds), stoppingToken); + await _blogService.UpdateCloudAssetsAsync(); + _logger.LogInformation("Post hosted service interval executed completely, interval = {Interval}.", _appConfig.PostUpdateEverySeconds); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Post hosted service execution failed"); + } + } + } + + public override async Task StartAsync(CancellationToken cancellationToken) + { + await _blogService.UpdateMemoryAssetsAsync(_appConfig.CloneAssetsDuringStartup); + await base.StartAsync(cancellationToken); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + await _blogService.UpdateCloudAssetsAsync(); + await base.StopAsync(cancellationToken); + _logger.LogInformation("Post hosted service stopped."); + } + } +} diff --git a/src/blog/Laobian.Blog.csproj b/src/blog/Laobian.Blog.csproj index 32d0f447..199c5b64 100644 --- a/src/blog/Laobian.Blog.csproj +++ b/src/blog/Laobian.Blog.csproj @@ -3,27 +3,14 @@ netcoreapp3.0 latest - 2.0.0 - 2.0.0 - 2.0.0 - f6e89c8c-637e-4c79-8186-2e3263a7cce4 + Laobian.Blog + Laobian.Blog - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + diff --git a/src/blog/Models/PagedPostViewModel.cs b/src/blog/Models/PagedPostViewModel.cs index 1803c763..64ab6da4 100644 --- a/src/blog/Models/PagedPostViewModel.cs +++ b/src/blog/Models/PagedPostViewModel.cs @@ -1,31 +1,15 @@ using System.Collections.Generic; -using Laobian.Share.BlogEngine.Model; namespace Laobian.Blog.Models { public class PagedPostViewModel { - public Pagination Pagination { get; set; } + public int TotalPages { get; set; } + + public int CurrentPage { get; set; } public IEnumerable Posts { get; set; } public string Url { get; set; } } - - public class Pagination - { - public Pagination(int currentPage, int totalPages) - { - CurrentPage = currentPage; - if (CurrentPage <= 0) CurrentPage = 1; - - if (CurrentPage > totalPages) CurrentPage = totalPages; - - TotalPages = totalPages; - } - - public int TotalPages { get; } - - public int CurrentPage { get; } - } } diff --git a/src/blog/Models/PostViewModel.cs b/src/blog/Models/PostViewModel.cs index 54d8c519..3efb9212 100644 --- a/src/blog/Models/PostViewModel.cs +++ b/src/blog/Models/PostViewModel.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using Humanizer; using Laobian.Share.BlogEngine.Model; namespace Laobian.Blog.Models @@ -21,8 +20,7 @@ public PostViewModel(BlogPost post) public string GetMetadataHtml() { var results = new List(); - results.Add($"发表于 {Post.CreationTimeUtc.Humanize()}"); - results.Add($"{Post.Visits.ToMetric(decimals:1)} 访问"); + results.Add(GetSimpleMetadataHtml()); var catHtml = GetCategoryHtml(); if (!string.IsNullOrEmpty(catHtml)) @@ -39,6 +37,27 @@ public string GetMetadataHtml() return string.Join(" · ", results); } + public string GetSimpleMetadataHtml() + { + var results = new List(); + results.Add($" 发表于 {Post.CreateTimeString}"); + results.Add($" {Post.VisitString} 次访问"); + + return string.Join(" · ", results); + } + + public string GetCategoryAndTagHtml() + { + var categoryHtml = GetCategoryHtml(); + var tagHtml = GetTagHtml(); + if (!string.IsNullOrEmpty(tagHtml)) + { + return categoryHtml + " · " + tagHtml; + } + + return categoryHtml; + } + public string GetCategoryHtml() { var results = new List(); @@ -53,7 +72,7 @@ public string GetCategoryHtml() return string.Empty; } - return $"{string.Join(", ", results)}"; + return $" {string.Join(", ", results)}"; } public string GetTagHtml() @@ -70,7 +89,7 @@ public string GetTagHtml() return string.Empty; } - return $"{string.Join(", ", results)}"; + return $" {string.Join(", ", results)}"; } } } diff --git a/src/blog/Models/Rss.cs b/src/blog/Models/Rss.cs deleted file mode 100644 index 61335d39..00000000 --- a/src/blog/Models/Rss.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Xml.Serialization; - -namespace Laobian.Blog.Models -{ - [XmlRoot("rss")] - public class RssRoot - { - [XmlAttribute("version")] public string Version { get; set; } - - [XmlElement("channel")] public RssChannel Channel { get; set; } - } - - public class RssChannel - { - [XmlElement("title")] public string Title { get; set; } - - [XmlElement("link")] public string Link { get; set; } - - [XmlElement("copyright")] public string Copyright { get; set; } - - [XmlElement("description")] public string Description { get; set; } - - [XmlElement("language")] public string Language { get; set; } - - [XmlIgnore] public DateTime PubDate { get; set; } - - [XmlElement("pubDate")] - public string PubDateString - { - get => PubDate.ToString("r"); - set => PubDate = DateTime.Parse(value); - } - - [XmlIgnore] public DateTime LastBuildDate { get; set; } - - [XmlElement("lastBuildDate")] - public string LastBuildDateString - { - get => LastBuildDate.ToString("r"); - set => LastBuildDate = DateTime.Parse(value); - } - - [XmlElement("docs")] public string Docs { get; set; } - - [XmlElement("generator")] public string Generator { get; set; } - - [XmlElement("managingEditor")] public string ManagingEditor { get; set; } - - [XmlElement("webMaster")] public string WebMaster { get; set; } - - [XmlElement("category")] public List Category { get; set; } - - [XmlElement("ttl")] public int Ttl { get; set; } - - [XmlElement("image")] public ChannelImage Image { get; set; } - - [XmlElement("item")] public List Items { get; set; } = new List(); - } - - public class ChannelItem - { - [XmlElement("title")] public string Title { get; set; } - - [XmlElement("link")] public string Link { get; set; } - - [XmlElement("description")] public string Description { get; set; } - - [XmlIgnore] public DateTime PubDate { get; set; } - - [XmlElement("pubDate")] - public string PubDateString - { - get => PubDate.ToString("r"); - set => PubDate = DateTime.Parse(value); - } - - [XmlElement("guid")] public string Guid { get; set; } - - [XmlElement("author")] public string Author { get; set; } - - [XmlElement("category")] public List Category { get; set; } - } - - public class ChannelImage - { - [XmlElement("url")] public string Url { get; set; } - - [XmlElement("title")] public string Title { get; set; } - - [XmlElement("link")] public string Link { get; set; } - } -} diff --git a/src/blog/Models/Sitemap.cs b/src/blog/Models/Sitemap.cs new file mode 100644 index 00000000..a0a48215 --- /dev/null +++ b/src/blog/Models/Sitemap.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace Laobian.Blog.Models +{ + [XmlRoot("urlset")] + public class SiteMapUrlSet + { + [XmlElement("url")] + public List Urls { get; set; } + } + + public class SiteMapUrl + { + [XmlElement("loc")] + public string Loc { get; set; } + + [XmlElement("lastmod")] + public string LastMod { get; set; } + + [XmlElement("changefreq")] + public string ChangeFreq { get; set; } + + [XmlElement("priority")] + public double Priority { get; set; } + } +} diff --git a/src/blog/Properties/launchSettings.json b/src/blog/Properties/launchSettings.json index c9348a58..d51a412b 100644 --- a/src/blog/Properties/launchSettings.json +++ b/src/blog/Properties/launchSettings.json @@ -1,21 +1,11 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:61286", - "sslPort": 0 - } - }, "profiles": { "Laobian.Blog": { "commandName": "Project", "environmentVariables": { - "AzureStorageConnection": "UseDevelopmentStorage=true", - "SendGridKey": "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", "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "http://localhost:5000" + "applicationUrl": "http://0.0.0.0:5000" } } } \ No newline at end of file diff --git a/src/blog/README.md b/src/blog/README.md deleted file mode 100644 index 9b38626d..00000000 --- a/src/blog/README.md +++ /dev/null @@ -1,34 +0,0 @@ -### Introduction - -Source of my blog website: [Jerry Bian's blog](https://blog.laobian.me). - -Built based on latest ASP.NET Core, running on Ubuntu Linux. - -Database free, and no important data except the source are stored at server machine. - - -### License - -``` -MIT License - -Copyright (c) 2018 Jerry Bian - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` \ No newline at end of file diff --git a/src/blog/Startup.cs b/src/blog/Startup.cs index 8c44bb50..69bc0bf2 100644 --- a/src/blog/Startup.cs +++ b/src/blog/Startup.cs @@ -1,16 +1,23 @@ -using System.Globalization; +using System; +using System.Globalization; using System.IO; +using System.Text.Encodings.Web; +using System.Text.Unicode; +using Laobian.Blog.Helpers; using Laobian.Share.BlogEngine; using Laobian.Share.Config; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; @@ -18,48 +25,99 @@ namespace Laobian.Blog { public class Startup { - public Startup(IConfiguration configuration) + public Startup(IConfiguration configuration, IWebHostEnvironment environment) { Configuration = configuration; + HostEnvironment = environment; } public IConfiguration Configuration { get; } + public IWebHostEnvironment HostEnvironment { get; } + // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - BlogServiceRegister.Register(services, Configuration); - services.AddControllersWithViews(); + services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.BasicLatin, UnicodeRanges.CjkUnifiedIdeographs)); + StartupHelper.RegisterService(services, Configuration); + + if (HostEnvironment.IsDevelopment()) + { + services.AddControllersWithViews(SetCacheProfile).AddRazorRuntimeCompilation(); + } + else + { + services.AddControllersWithViews(SetCacheProfile); + } + services.AddDirectoryBrowser(); } + private static void SetCacheProfile(MvcOptions options) + { + options.CacheProfiles.Add( + "Cache10Sec", + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.Client, + VaryByHeader = "Accept-Encoding" + }); + options.CacheProfiles.Add( + "Cache1Hour", + new CacheProfile + { + Duration = 60 * 60, + Location = ResponseCacheLocation.Client, + VaryByHeader = "Accept-Encoding" + }); + options.CacheProfiles.Add( + "Cache1Day", + new CacheProfile + { + Duration = 60 * 60 * 24, + Location = ResponseCacheLocation.Client, + VaryByHeader = "Accept-Encoding" + }); + } + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime) + public void Configure(IApplicationBuilder app, IHostApplicationLifetime applicationLifetime) { CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("zh-cn"); CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("zh-cn"); var appConfig = app.ApplicationServices.GetService>().Value; + var logger = app.ApplicationServices.GetService>(); - app.UseForwardedHeaders(new ForwardedHeadersOptions + applicationLifetime.ApplicationStarted.Register(() => { - ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto + BlogState.StartAtUtc = DateTime.UtcNow; + BlogState.IsDevEnvironment = HostEnvironment.IsDevelopment(); + BlogState.IsStageEnvironment = HostEnvironment.IsStaging(); + BlogState.IsProdEnvironment = HostEnvironment.IsProduction(); }); - if (env.IsDevelopment()) + app.UseForwardedHeaders(new ForwardedHeadersOptions { - app.UseDeveloperExceptionPage(); - } - else + ForwardedHeaders = ForwardedHeaders.All + }); + + if (HostEnvironment.IsProduction()) { app.UseExceptionHandler(new ExceptionHandlerOptions { ExceptionHandler = async context => { - await context.Response.WriteAsync($"Something was wrong! Please contact ."); + logger.LogWarning(context.Features.Get()?.Error, "Request error occurred."); + await context.Response.WriteAsync($"Something was wrong! Please contact {BlogConstant.AuthorEmail}."); } }); } + else + { + app.UseDeveloperExceptionPage(); + } app.UseStatusCodePages(async context => { @@ -71,26 +129,22 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApp var provider = new FileExtensionContentTypeProvider(); provider.Mappings[".webmanifest"] = "application/manifest+json"; - app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider, - OnPrepareResponse = ctx => - { - const int durationInSeconds = 60 * 60 * 24 * 30; - ctx.Context.Response.Headers[HeaderNames.CacheControl] = - "public,max-age=" + durationInSeconds; - } + OnPrepareResponse = SetStaticFileCache }); var fileDirFullPath = Path.Combine(appConfig.AssetRepoLocalDir, BlogConstant.FileGitHub); Directory.CreateDirectory(fileDirFullPath); - app.UseFileServer(new FileServerOptions + var fileServerOptions = new FileServerOptions { FileProvider = new PhysicalFileProvider(fileDirFullPath), RequestPath = BlogConstant.FileRequestPath, - EnableDirectoryBrowsing = true - }); + EnableDirectoryBrowsing = true, + }; + fileServerOptions.StaticFileOptions.OnPrepareResponse = SetStaticFileCache; + app.UseFileServer(fileServerOptions); app.UseRouting(); app.UseEndpoints(endpoints => @@ -98,5 +152,12 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApp endpoints.MapDefaultControllerRoute(); }); } + + private static void SetStaticFileCache(StaticFileResponseContext ctx) + { + const int durationInSeconds = 60 * 60 * 24 * 30; + ctx.Context.Response.Headers[HeaderNames.CacheControl] = + "public,max-age=" + durationInSeconds; + } } } diff --git a/src/blog/TagHelpers/PaginationTagHelper.cs b/src/blog/TagHelpers/PaginationTagHelper.cs index df3edb0d..d37062f5 100644 --- a/src/blog/TagHelpers/PaginationTagHelper.cs +++ b/src/blog/TagHelpers/PaginationTagHelper.cs @@ -1,6 +1,5 @@ using System; using System.Text; -using Laobian.Blog.Models; using Microsoft.AspNetCore.Razor.TagHelpers; namespace Laobian.Blog.TagHelpers @@ -11,7 +10,9 @@ public class PaginationTagHelper : TagHelper private const string PreviousLabel = "←"; private const string NextLabel = "→"; - public Pagination Pagination { get; set; } + public int TotalPages { get; set; } + + public int CurrentPage { get; set; } public string Url { get; set; } @@ -21,29 +22,29 @@ public override void Process(TagHelperContext context, TagHelperOutput output) output.Attributes.SetAttribute("id", "pagination"); // No need to generate HTML markup as we only have one pagination - if (Pagination.TotalPages < 2) + if (TotalPages < 2) { return; } - if (Pagination.CurrentPage < 1 || Pagination.CurrentPage > Pagination.TotalPages) + if (CurrentPage < 1 || CurrentPage > TotalPages) { - throw new ArgumentOutOfRangeException(nameof(Pagination.CurrentPage)); + throw new ArgumentOutOfRangeException(nameof(CurrentPage)); } var html = new StringBuilder(); html.AppendLine("
    "); - // Prev item only be visible if Pagination.CurrentPage is greater than one - if (Pagination.CurrentPage > 1) + // Prev item only be visible if CurrentPage is greater than one + if (CurrentPage > 1) { - html.AppendLine(GetLinkItem($"{PreviousLabel}", GetUrl(Pagination.CurrentPage - 1))); + html.AppendLine(GetLinkItem($"{PreviousLabel}", GetUrl(CurrentPage - 1))); } - for (var i = 1; i <= Pagination.TotalPages; i++) + for (var i = 1; i <= TotalPages; i++) { - // display Pagination.CurrentPage item as active - if (i == Pagination.CurrentPage) + // display CurrentPage item as active + if (i == CurrentPage) { html.AppendLine(GetActiveItem(i)); continue; @@ -52,10 +53,10 @@ public override void Process(TagHelperContext context, TagHelperOutput output) html.AppendLine(GetLinkItem(i, GetUrl(i))); } - // Next item only be visible if Pagination.CurrentPage is less than Pagination.TotalPages - if (Pagination.CurrentPage < Pagination.TotalPages) + // Next item only be visible if CurrentPage is less than TotalPages + if (CurrentPage < TotalPages) { - html.AppendLine(GetLinkItem($"{NextLabel}", GetUrl(Pagination.CurrentPage + 1))); + html.AppendLine(GetLinkItem($"{NextLabel}", GetUrl(CurrentPage + 1))); } html.AppendLine("
"); diff --git a/src/blog/TitleHelper.cs b/src/blog/TitleHelper.cs deleted file mode 100644 index 3c821779..00000000 --- a/src/blog/TitleHelper.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Laobian.Share.BlogEngine; - -namespace Laobian.Blog -{ - public class TitleHelper - { - public static string GetTitle(dynamic title) - { - var t = title?.ToString(); - return string.IsNullOrEmpty(t) ? BlogConstant.BlogName : $"{title} - {BlogConstant.BlogName}"; - } - } -} diff --git a/src/blog/Views/About/Index.cshtml b/src/blog/Views/About/Index.cshtml index e2a10d52..71299f8b 100644 --- a/src/blog/Views/About/Index.cshtml +++ b/src/blog/Views/About/Index.cshtml @@ -1,9 +1,14 @@ @model string -@{ - ViewData["Title"] = "Index"; -} +
+
+ @Html.Raw(Model) +
+
-
- @Html.Raw(Model) -
+@section style{ + +} +@section script{ + +} \ No newline at end of file diff --git a/src/blog/Views/Archive/Index.cshtml b/src/blog/Views/Archive/Index.cshtml index 9fc8b983..298587d3 100644 --- a/src/blog/Views/Archive/Index.cshtml +++ b/src/blog/Views/Archive/Index.cshtml @@ -4,8 +4,8 @@ @@ -13,13 +13,34 @@
@foreach (var item in Model) { - -
    + + @foreach (var post in item.Posts.OrderByDescending(p => p.CreationTimeUtc)) { -
  • @post.Title
  • + + + + + } - +
    + + @post.Title + +
    }
- \ No newline at end of file + + +@section style{ + +} + +@section script{ + +} \ No newline at end of file diff --git a/src/blog/Views/Home/About.cshtml b/src/blog/Views/Home/About.cshtml deleted file mode 100644 index d8fc6d09..00000000 --- a/src/blog/Views/Home/About.cshtml +++ /dev/null @@ -1,40 +0,0 @@ -@* -
-

Hi there,

- -

My name is 卞良忠, and use Jerry Bian as my English name.

- -

I am a software engineer, working at software department of a bank at Hangzhou, China.

- -

My interested areas including: .NET/C#, Python, Go, as well as architect and beautiful codes with high performance. Writing codes are one of my main hobbies and it lets me feel better once something bothering happened. However, I have no interests on comparing one language to another or this technology to the other one. I believe the best choice always should be made under certain scenarios.

- -

I am also a father, with a cute boy born at 2017. Making him laugh and taking part in his growth is another best thing of my life. By the way, maybe someday when he is well prepared, I can tech him programming.

- -

This website is basically my personal blogs and notes, they are not representing anything else besides myself. All posts cannot be copied or commercial usage until you got my approval.

- -

Usually, I wrote programming and software related posts. But anything else can be put here too, as long as I think I should.

- -

I can be reached at:

- - - -

Life is short, letters will be last forever.

-
- - @section sidebar{ - - } -
*@ diff --git a/src/blog/Views/Home/Index.cshtml b/src/blog/Views/Home/Index.cshtml index f50ff575..69824aa5 100644 --- a/src/blog/Views/Home/Index.cshtml +++ b/src/blog/Views/Home/Index.cshtml @@ -1,30 +1,36 @@ @model PagedPostViewModel -
+
@foreach (var item in Model.Posts) { -
+
-

+
+ @item.Post.Title -

+
- @{ -
@Html.Raw(item.Post.Excerpt)
- } - +
@Html.Raw(item.Post.Excerpt)
- @Html.Raw(item.GetMetadataHtml()) +
@Html.Raw(item.GetMetadataHtml())
+
@Html.Raw(item.GetSimpleMetadataHtml())
}
- - + +
+@section style{ + +} + +@section script{ + +} \ No newline at end of file diff --git a/src/blog/Views/Home/Post.cshtml b/src/blog/Views/Home/Post.cshtml deleted file mode 100644 index eaa59ec8..00000000 --- a/src/blog/Views/Home/Post.cshtml +++ /dev/null @@ -1,54 +0,0 @@ -@model Laobian.Share.BlogEngine.Model.BlogPost - -
-

- @Model.Title -

-
@Model.CreationTimeUtc.ToRelativeTime() · @Model.Visits.ToThousandsPlace() 次访问
-
- @Html.Raw(Model.HtmlContent) -
-
- -@section stickySidebar{ -
-
-
- - - - - - - - - - - - - -
访问@Model.Visits.ToThousandsPlace() 次
创建@Model.CreationTimeUtc.ToRelativeTime()
更新@Model.LastUpdateTimeUtc.ToRelativeTime()
-
-
-
- - -} - -@section Scripts{ - -} \ No newline at end of file diff --git a/src/blog/Views/Post/Index.cshtml b/src/blog/Views/Post/Index.cshtml new file mode 100644 index 00000000..3d4cae6b --- /dev/null +++ b/src/blog/Views/Post/Index.cshtml @@ -0,0 +1,45 @@ +@model PostViewModel +@inject IOptions Config + +
+
+ + @Model.Post.Title +
+ +
+ @Html.Raw(Model.Post.HtmlContent) + +

(本文完)

+
+
+ + + +@section stickySidebar{ + +} + +@section script{ + +} + +@section style{ + +} \ No newline at end of file diff --git a/src/blog/Views/Shared/_AsideAbout.cshtml b/src/blog/Views/Shared/_AsideAbout.cshtml new file mode 100644 index 00000000..37974783 --- /dev/null +++ b/src/blog/Views/Shared/_AsideAbout.cshtml @@ -0,0 +1,7 @@ +
+
关于
+
+

@BlogConstant.AuthorChineseName,英文名: @BlogConstant.AuthorEnglishName。软件工程师,热爱技术与写作。该博客记录技术心得以及所思所感。

+

所有言论均代表个人,除非特殊声明,均与我的雇主或者其他任何团体无关。

+
+
\ No newline at end of file diff --git a/src/blog/Views/Shared/_AsideCopyright.cshtml b/src/blog/Views/Shared/_AsideCopyright.cshtml new file mode 100644 index 00000000..677fc486 --- /dev/null +++ b/src/blog/Views/Shared/_AsideCopyright.cshtml @@ -0,0 +1,8 @@ +
+
版权
+
+

+ 本博客所有文章如无特殊声明,均遵守“署名-非商业性使用-禁止演绎 4.0 国际(CC BY-NC-ND 4.0)协议”。 +

+
+
\ No newline at end of file diff --git a/src/blog/Views/Shared/_AsidePostInfo.cshtml b/src/blog/Views/Shared/_AsidePostInfo.cshtml new file mode 100644 index 00000000..82d70821 --- /dev/null +++ b/src/blog/Views/Shared/_AsidePostInfo.cshtml @@ -0,0 +1,24 @@ +@model PostViewModel + +
+
+
+ + + + + + + + + + + + + +
访问@Model.Post.VisitString 次
创建 + @Model.Post.CreateTimeString +
更新@Model.Post.LastUpdateTimeString
+
+
+
\ No newline at end of file diff --git a/src/blog/Views/Shared/_Layout.cshtml b/src/blog/Views/Shared/_Layout.cshtml index 1c729b9c..b6e61822 100644 --- a/src/blog/Views/Shared/_Layout.cshtml +++ b/src/blog/Views/Shared/_Layout.cshtml @@ -1,76 +1,37 @@ -@using Laobian.Share.BlogEngine - + - - - @**@ + + - - - @TitleHelper.GetTitle(ViewData["Title"]) - - + + + + + + + + + @RenderSection("style", false) + + + + + + + + @**@ - - - - - - - - - - - - @* - - - - - - - *@ - - - - - - - - - - @**@ - @* - - *@ + - - + @RenderSection("script", false) - - -
+
@RenderBody()
-