diff --git a/README.md b/README.md
index 36393b4..a52375d 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1,189 @@
# PineBlog
-A blogging engine based on ASP.NET Core MVC Razor Pages and Entity Framework Core.
[![Build Status](https://dev.azure.com/ofpinewood/Of%20Pine%20Wood/_apis/build/status/ofpinewood.pineblog?branchName=master)](https://dev.azure.com/ofpinewood/Of%20Pine%20Wood/_build/latest?definitionId=7&branchName=master)
[![NuGet Badge](https://img.shields.io/nuget/v/Opw.PineBlog.svg)](https://www.nuget.org/packages/Opw.PineBlog/)
[![License: MIT](https://img.shields.io/github/license/ofpinewood/pineblog.svg)](https://github.com/ofpinewood/pineblog/blob/master/LICENSE)
-## Opw.PineBlog metapackage
-The Opw.PineBlog metapackage includes the following packages.
+PineBlog is a light-weight open source blogging engine written in ASP.NET Core MVC Razor Pages, using Entity Framework Core. It is highly extendable, customizable and easy to integrate in an existing web application.
-### Opw.PineBlog.EntityFrameworkCore
-The PineBlog data provider that uses Entity Framework Core.
+### Features
-[![NuGet Badge](https://img.shields.io/nuget/v/Opw.PineBlog.EntityFrameworkCore.svg)](https://www.nuget.org/packages/Opw.PineBlog.EntityFrameworkCore/)
+- Markdown post editor
+- File management
+- Light-weight using Razor Pages
+- SEO optimized
+- Open Graph protocol
+- Clean Architecture (youtube: [Clean Architecture with ASP.NET Core](https://youtu.be/_lwCVE_XgqI))
+- Entity Framework Core, SQL database
+- Azure Blob Storage, for file storage
+- ..only a blogging engine, nothing else..
-### Opw.PineBlog.RazorPages
-The PineBlog UI using ASP.NET Core MVC Razor Pages.
+### What is not included
+Because PineBlog is very light-weight it is not a complete website, it needs to be integrated in an existing web application of you need to create a basic web application for it. There are a few things PineBlog depends on, but that it does not provide.
-[![NuGet Badge](https://img.shields.io/nuget/v/Opw.PineBlog.RazorPages.svg)](https://www.nuget.org/packages/Opw.PineBlog.RazorPages/)
+- Authentication and authorization
-### Opw.PineBlog.Core
-The PineBlog core package. This package is a dependency for `Opw.PineBlog.RazorPages` and `Opw.PineBlog.EntityFrameworkCore`.
-
-[![NuGet Badge](https://img.shields.io/nuget/v/Opw.PineBlog.Core.svg)](https://www.nuget.org/packages/Opw.PineBlog.Core/)
+> **Note:** The admin pages require that authentication/authorization has been setup in your website, the admin area has a `AuthorizeFilter` with the default policy set to all pages in that area folder.
## Where can I get it?
-You can install [Opw.PineBlog](https://www.nuget.org/packages/Opw.PineBlog/) from the NuGet package manager console:
+You can install the [Opw.PineBlog](https://www.nuget.org/packages/Opw.PineBlog/) metapackage from the console.
-``` ps
-PM> Install-Package Opw.PineBlog
+``` cmd
+> dotnet add package Opw.PineBlog
```
+
+The Opw.PineBlog metapackage includes the following packages.
+
+- **Opw.PineBlog.EntityFrameworkCore package**
+The PineBlog data provider that uses Entity Framework Core.
+[![NuGet Badge](https://img.shields.io/nuget/v/Opw.PineBlog.EntityFrameworkCore.svg)](https://www.nuget.org/packages/Opw.PineBlog.EntityFrameworkCore/)
+
+- **Opw.PineBlog.RazorPages package**
+The PineBlog UI using ASP.NET Core MVC Razor Pages.
+[![NuGet Badge](https://img.shields.io/nuget/v/Opw.PineBlog.RazorPages.svg)](https://www.nuget.org/packages/Opw.PineBlog.RazorPages/)
+
+- **Opw.PineBlog.Core package**
+The PineBlog core package. This package is a dependency for `Opw.PineBlog.RazorPages` and `Opw.PineBlog.EntityFrameworkCore`.
+[![NuGet Badge](https://img.shields.io/nuget/v/Opw.PineBlog.Core.svg)](https://www.nuget.org/packages/Opw.PineBlog.Core/)
+
## Getting started
-[TODO]
+Add the PineBlog services and the Razor Pages UI in the Startup.cs of your application.
+
+``` csharp
+public void ConfigureServices(IServiceCollection services)
+{
+ ...
+ services.AddPineBlog(Configuration);
+
+ services.AddMvc().AddPineBlogRazorPages();
+ // or services.AddMvcCore().AddPineBlogRazorPages();
+ ...
+}
+```
+
+### Configuration
+A few properties need to be configured before you can run your web application with PineBlog.
+
+**Title** the title of your blog/website.
+**CoverUrl** the URL for the cover image of your blog, this can be a relative or absolute URL.
+**ConnectionStringName** this is the name to the connection string used in your application.
+**CreateAndSeedDatabases** to automatically create and seed the tables for the blog set this property to `true`, if you want to create and seed your database in any other way set this property to `false`.
+**AzureStorageConnectionString** your Azure Blog Storage connection string.
+**AzureStorageBlobContainerName** the name of the blob container to use for file storage.
+**FileBaseUrl** The base URL for the files, this should be the URL for your Azure Blob Storage, e.g. `https://.blob.core.windows.net`.
+
+
+The rest of the properties are optional and will be set with default values if you don't specify them.
+
+``` json
+{
+ "ConnectionStrings": {
+ "DefaultConnection": "Server=inMemory; Database=pineblog-db;"
+ },
+ "PineBlogOptions": {
+ "Title": "PineBlog",
+ "Description": "A blogging engine based on ASP.NET Core MVC Razor Pages and Entity Framework Core",
+ "CoverUrl": "/images/woods.gif",
+ "CoverCaption": "Battle background for the Misty Woods in the game Shadows of Adam by Tim Wendorf",
+ "CoverLink": "http://pixeljoint.com/pixelart/94359.htm",
+ "ItemsPerPage": 5,
+ "CreateAndSeedDatabases": true,
+ "ConnectionStringName": "DefaultConnection",
+ "AzureStorageConnectionString": "UseDevelopmentStorage=true",
+ "AzureStorageBlobContainerName": "pineblog",
+ "FileBaseUrl": "http://127.0.0.1:10000/devstoreaccount1"
+ }
+}
+```
+
+## Blog layout page
+For the **Blog** area you need to override the `_Layout.cshtml` for the pages, to do this create a new `_Layout.cshtml` page in the `Areas/Blog/Shared` folder. This will make the blog pages use that layout page instead of the one included in the `Opw.PineBlog.RazorPages` package.
+In the new page you can set the layout page of your website. Make sure to add the `head` and `script` sections.
+
+``` csharp
+@{
+ Layout = "~/Pages/Shared/_Layout.cshtml";
+}
+@section head {
+ @RenderSection("head", required: false)
+}
+@section scripts {
+ @RenderSection("scripts", required: false)
+}
+@RenderBody()
+```
+
+### Your layout page
+PineBlog is dependent on [Bootstrap 4.3](https://getbootstrap.com/) and [Font Awesome 4.7](https://fontawesome.com/v4.7.0/), so make sure to include them in your layout page and add the necessary files to the `wwwroot` of your project (see the sample project for an example).
+
+``` html
+
+
+ ...
+
+
+
+
+
+
+
+
+
+
+ ...
+
+
+ ...
+
+
+
+
+
+
+
+
+
+
+
+
+```
### Open Graph protocol
-The blog pages include Open Graph `` tags in the `` of the page. Make sure you add the prefix to the `` tag in your pagers.
+The blog pages include Open Graph `` tags in the `` of the page. These require that you add the `og: http://ogp.me/ns#` prefix to the `` tag in your pages.
``` html
```
+### Overriding the UI
+You can override any other Razor view you like by following the same steps as described above for the layout page. For an example have a look at the sample project where we override the footer ([_Footer.cshtml](https://github.com/ofpinewood/pineblog/blob/master/samples/Opw.PineBlog.Sample/Areas/Blog/Pages/Shared/_Footer_.cshtml)).
+
+## Admin layout page
+For the **Admin** area layout page do the same as you did for the **Blog** area.
+
## Samples
-See [Opw.PineBlog.Sample](/docs/Opw.PineBlog.Sample.md).
+The [sample project](https://github.com/ofpinewood/pineblog/tree/master/samples/Opw.PineBlog.Sample) contains an example web application with PineBlog.
+
+**Please see the code** :nerd_face
## Usage
PineBlog is used on the following website:
diff --git a/docs/Opw.PineBlog.Sample.md b/docs/Opw.PineBlog.Sample.md
deleted file mode 100644
index 192727f..0000000
--- a/docs/Opw.PineBlog.Sample.md
+++ /dev/null
@@ -1,4 +0,0 @@
-# Sample project using PineBlog
-This sample project contains an example integration of PineBlog.
-
-**Please see the code** :nerd_face
\ No newline at end of file
diff --git a/samples/Opw.PineBlog.Sample/Areas/Blog/Pages/Shared/_Footer.cshtml b/samples/Opw.PineBlog.Sample/Areas/Blog/Pages/Shared/_Footer.cshtml
new file mode 100644
index 0000000..d66d3f4
--- /dev/null
+++ b/samples/Opw.PineBlog.Sample/Areas/Blog/Pages/Shared/_Footer.cshtml
@@ -0,0 +1,24 @@
+
+
diff --git a/src/Opw.PineBlog.RazorPages/Areas/Blog/Pages/Index.cshtml.cs b/src/Opw.PineBlog.RazorPages/Areas/Blog/Pages/Index.cshtml.cs
index be4a26a..e4fd931 100644
--- a/src/Opw.PineBlog.RazorPages/Areas/Blog/Pages/Index.cshtml.cs
+++ b/src/Opw.PineBlog.RazorPages/Areas/Blog/Pages/Index.cshtml.cs
@@ -50,10 +50,12 @@ public async Task OnGetAsync(CancellationToken cancellationToken,
PageCover = new Models.PageCoverModel
{
+ PostListType = PostList.PostListType,
+ Category = PostList.Category,
Title = PostList.Blog.Title,
CoverUrl = PostList.Blog.CoverUrl,
CoverCaption = PostList.Blog.CoverCaption,
- CoverLink = PostList.Blog.CoverLink
+ CoverLink = PostList.Blog.CoverLink,
};
return Page();
diff --git a/src/Opw.PineBlog.RazorPages/Areas/Blog/Pages/Post.cshtml.cs b/src/Opw.PineBlog.RazorPages/Areas/Blog/Pages/Post.cshtml.cs
index 87b70bc..d1e6cf2 100644
--- a/src/Opw.PineBlog.RazorPages/Areas/Blog/Pages/Post.cshtml.cs
+++ b/src/Opw.PineBlog.RazorPages/Areas/Blog/Pages/Post.cshtml.cs
@@ -4,6 +4,7 @@
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
+using Opw.PineBlog.Models;
using Opw.PineBlog.Posts;
namespace Opw.PineBlog.RazorPages.Areas.Blog.Pages
diff --git a/tests/Opw.PineBlog.Core.Tests/Posts/GetPagedPostListQueryTests.cs b/tests/Opw.PineBlog.Core.Tests/Posts/GetPagedPostListQueryTests.cs
index cbd1d89..e45442e 100644
--- a/tests/Opw.PineBlog.Core.Tests/Posts/GetPagedPostListQueryTests.cs
+++ b/tests/Opw.PineBlog.Core.Tests/Posts/GetPagedPostListQueryTests.cs
@@ -1,6 +1,7 @@
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Opw.PineBlog.Entities;
+using Opw.PineBlog.Models;
using System;
using System.Linq;
using System.Threading.Tasks;
@@ -51,6 +52,24 @@ public async Task Handler_Should_ReturnPostListModel_With6Posts_WhenIncludingUnp
result.Value.Posts.Should().HaveCount(6);
}
+ [Fact]
+ public async Task Handler_Should_ReturnPostListModel_WithPostListTypeBlog()
+ {
+ var result = await Mediator.Send(new GetPagedPostListQuery());
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.PostListType.Should().Be(PostListType.Blog);
+ }
+
+ [Fact]
+ public async Task Handler_Should_ReturnPostListModel_WhenFilterOnCategory_WithPostListTypeCategory()
+ {
+ var result = await Mediator.Send(new GetPagedPostListQuery { Category = "category" });
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.PostListType.Should().Be(PostListType.Category);
+ }
+
[Fact]
public async Task Handler_Should_ReturnPostListModel_WithBlogInfo()
{
diff --git a/tests/Opw.PineBlog.RazorPages.Tests/Areas/Blog/Pages/IndexModelTests.cs b/tests/Opw.PineBlog.RazorPages.Tests/Areas/Blog/Pages/IndexModelTests.cs
index eb1ed42..0adf078 100644
--- a/tests/Opw.PineBlog.RazorPages.Tests/Areas/Blog/Pages/IndexModelTests.cs
+++ b/tests/Opw.PineBlog.RazorPages.Tests/Areas/Blog/Pages/IndexModelTests.cs
@@ -41,17 +41,93 @@ public async Task OnGetAsync_Should_SetPostListModel()
pageModel.PostList.Blog.Should().NotBeNull();
pageModel.PostList.Pager.Should().NotBeNull();
pageModel.PostList.Posts.Should().NotBeNull();
- pageModel.Title.Should().NotBeNull();
pageModel.PostList.Blog.Title.Should().Be("Blog title");
pageModel.PostList.Pager.Should().NotBeNull();
pageModel.PostList.Posts.Should().NotBeNull();
pageModel.Title.Should().Be("Blog title");
+ }
+
+ [Fact]
+ public async Task OnGetAsync_Should_SetMetadataModel()
+ {
+ var loggerMock = new Mock>();
+ var mediaterMock = new Mock();
+ mediaterMock.Setup(m => m.Send(It.IsAny>>(), It.IsAny()))
+ .ReturnsAsync(Result.Success(GetPostListModel()));
+
+ var httpContext = new DefaultHttpContext();
+ var pageContext = GetPageContext(httpContext);
+
+ var pageModel = new IndexModel(mediaterMock.Object, loggerMock.Object)
+ {
+ PageContext = pageContext.Item1,
+ TempData = GetTempDataDictionary(httpContext),
+ Url = new UrlHelper(pageContext.Item2)
+ };
+
+ var result = await pageModel.OnGetAsync(default, 0);
+
+ result.Should().BeOfType();
+ pageModel.Metadata.Title.Should().Be("Blog title");
pageModel.Metadata.Description.Should().Be("Blog description");
- pageModel.PageCover.Title.Should().Be("Blog title");
}
[Fact]
- public async Task OnGetAsync_Should_SetPostListModel_WithAbsoluteCoverUrl()
+ public async Task OnGetAsync_Should_SetPageCoverModel_WhenNoFilters()
+ {
+ var loggerMock = new Mock>();
+ var mediaterMock = new Mock();
+ mediaterMock.Setup(m => m.Send(It.IsAny>>(), It.IsAny()))
+ .ReturnsAsync(Result.Success(GetPostListModel()));
+
+ var httpContext = new DefaultHttpContext();
+ var pageContext = GetPageContext(httpContext);
+
+ var pageModel = new IndexModel(mediaterMock.Object, loggerMock.Object)
+ {
+ PageContext = pageContext.Item1,
+ TempData = GetTempDataDictionary(httpContext),
+ Url = new UrlHelper(pageContext.Item2)
+ };
+
+ var result = await pageModel.OnGetAsync(default, 0);
+
+ result.Should().BeOfType();
+ pageModel.PageCover.PostListType.Should().Be(PostListType.Blog);
+ pageModel.PageCover.Category.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task OnGetAsync_Should_SetPageCoverModel_WhenFilterOnCategory()
+ {
+ var loggerMock = new Mock>();
+ var postListModel = GetPostListModel();
+ postListModel.PostListType = PostListType.Category;
+ postListModel.Category = "category";
+
+ var mediaterMock = new Mock();
+ mediaterMock.Setup(m => m.Send(It.IsAny>>(), It.IsAny()))
+ .ReturnsAsync(Result.Success(postListModel));
+
+ var httpContext = new DefaultHttpContext();
+ var pageContext = GetPageContext(httpContext);
+
+ var pageModel = new IndexModel(mediaterMock.Object, loggerMock.Object)
+ {
+ PageContext = pageContext.Item1,
+ TempData = GetTempDataDictionary(httpContext),
+ Url = new UrlHelper(pageContext.Item2)
+ };
+
+ var result = await pageModel.OnGetAsync(default, 0);
+
+ result.Should().BeOfType();
+ pageModel.PageCover.PostListType.Should().Be(PostListType.Category);
+ pageModel.PageCover.Category.Should().Be("category");
+ }
+
+ [Fact]
+ public async Task OnGetAsync_Should_SetAbsoluteCoverUrl()
{
var loggerMock = new Mock>();
@@ -76,11 +152,12 @@ public async Task OnGetAsync_Should_SetPostListModel_WithAbsoluteCoverUrl()
result.Should().BeOfType();
pageModel.PostList.Blog.CoverUrl.Should().Be("http://www.example.com/images.jpg");
+ pageModel.PageCover.CoverUrl.Should().Be("http://www.example.com/images.jpg");
pageModel.Metadata.Image.Should().Be("http://www.example.com/images.jpg");
}
[Fact]
- public async Task OnGetAsync_Should_SetPostListModel_WithRelativeCoverUrl()
+ public async Task OnGetAsync_Should_SetRelativeCoverUrl()
{
var loggerMock = new Mock>();
@@ -107,6 +184,7 @@ public async Task OnGetAsync_Should_SetPostListModel_WithRelativeCoverUrl()
result.Should().BeOfType();
pageModel.PostList.Blog.CoverUrl.Should().Be("/images.jpg");
+ pageModel.PageCover.CoverUrl.Should().Be("/images.jpg");
pageModel.Metadata.Image.Should().Be("http://localhost:5001/images.jpg");
}
@@ -114,6 +192,7 @@ private PostListModel GetPostListModel()
{
return new PostListModel
{
+ PostListType = PostListType.Blog,
Blog = new BlogModel(new PineBlogOptions { Title = "Blog title", Description = "Blog description" }),
Pager = new Pager(0),
Posts = new List()