Skip to content

Commit

Permalink
Merge pull request #1 from granicz/add-hosted-support
Browse files Browse the repository at this point in the history
Add hosted support - making layout/template/content changes without recompilation to streamline customizing your blog. Then build static site and deploy.
  • Loading branch information
granicz committed Jan 7, 2020
2 parents a15f484 + 7f49cc8 commit 2650b6b
Show file tree
Hide file tree
Showing 28 changed files with 292 additions and 70 deletions.
12 changes: 9 additions & 3 deletions .gitignore
Expand Up @@ -5,7 +5,13 @@
/src/Website/.vs
/src/Website/bin
/src/Website/obj
/src/Website/js
/src/Website/css
/src/Website/node_modules
/src/Hosted/css
/src/Hosted/bin
/src/Hosted/obj
/src/Hosted/js
/src/Hosted/css
/src/Hosted/scripts
/src/Hosted/node_modules
/build
*.user
/.ionide
26 changes: 16 additions & 10 deletions BlogEngine.sln
Expand Up @@ -3,24 +3,30 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29505.145
MinimumVisualStudioVersion = 10.0.40219.1
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Website", "src\Website\Website.fsproj", "{45D38E07-BBDB-4DBF-A612-D690F18D2FB8}"
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Client", "src\Client\Client.fsproj", "{6C91BEBD-6C09-4A58-9BFC-973372BAD4FE}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Client", "src\Client\Client.fsproj", "{2CA71D09-93EB-40D6-80F3-25F74F852583}"
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Hosted", "src\Hosted\Hosted.fsproj", "{C4BC7A61-3EB6-48D3-92DF-28CAF4997B92}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Website", "src\Website\Website.fsproj", "{30CC8824-A610-4C33-962F-ED88339B25B3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{45D38E07-BBDB-4DBF-A612-D690F18D2FB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{45D38E07-BBDB-4DBF-A612-D690F18D2FB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{45D38E07-BBDB-4DBF-A612-D690F18D2FB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{45D38E07-BBDB-4DBF-A612-D690F18D2FB8}.Release|Any CPU.Build.0 = Release|Any CPU
{2CA71D09-93EB-40D6-80F3-25F74F852583}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2CA71D09-93EB-40D6-80F3-25F74F852583}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2CA71D09-93EB-40D6-80F3-25F74F852583}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2CA71D09-93EB-40D6-80F3-25F74F852583}.Release|Any CPU.Build.0 = Release|Any CPU
{6C91BEBD-6C09-4A58-9BFC-973372BAD4FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6C91BEBD-6C09-4A58-9BFC-973372BAD4FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6C91BEBD-6C09-4A58-9BFC-973372BAD4FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6C91BEBD-6C09-4A58-9BFC-973372BAD4FE}.Release|Any CPU.Build.0 = Release|Any CPU
{C4BC7A61-3EB6-48D3-92DF-28CAF4997B92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C4BC7A61-3EB6-48D3-92DF-28CAF4997B92}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C4BC7A61-3EB6-48D3-92DF-28CAF4997B92}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C4BC7A61-3EB6-48D3-92DF-28CAF4997B92}.Release|Any CPU.Build.0 = Release|Any CPU
{30CC8824-A610-4C33-962F-ED88339B25B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{30CC8824-A610-4C33-962F-ED88339B25B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{30CC8824-A610-4C33-962F-ED88339B25B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{30CC8824-A610-4C33-962F-ED88339B25B3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
75 changes: 70 additions & 5 deletions README.md
@@ -1,10 +1,75 @@
# How to build this project
# BlogEngine - your F# static site generator

1) Run `.\install.ps`
2) In the root folder, run `dotnet build`
3) Run `.\start.cmd`
BlogEngine is a simple and highly configurable static site generator for F#. It uses WebSharper to build the pages of your website and generate HTML files for them.

Image credit
There are three projects in here that you should be aware of:

* `src\Client` - this contains client-side functionality (to be run as JavaScript code) that you intend to embed in all (or some) of the output HTML pages. `Client` is a WebSharper+F# project and uses WebSharper to generate transpiled JavaScript code. Currently, this project consists of a single `Main.fs` file that enables F# syntax highlighting for markdown code blocks, and hides/shows the responsive drawer menu in the generated pages on mobile devices.

* `src\Hosted` - this is a WebSharper client-server and an ASP.NET Core application. You can run it to self-host your blog and work with template/style/layout changes much more effectively without having to recompile on each update. Simply deploy, make changes to `index.html` in the root of the project, and refresh your page in your browser. You can also author your blog articles and see them in their rendered form by triggering a runtime update URL (see below.)

* `src\Website` - this is a dummy WebSharper offline sitelet project that uses the code from `src\Hosted` and generates HTML pages in the `\output` root folder. It also copies all related artifacts (CSS, JS, images, etc.) into this folder, making it self-contained and ready to deploy in GitHub Pages or any other HTML server.

The repository provides a general blueprint to structure your static blog application, and without any changes is able to generate a static blog from a list of blog articles given as a folder of markdown files.

# How to build and run your blog

1) Run `.\install.ps` - do this the first time you start working with this repository. This script installs the required JS/CSS resources (only Bulma at the moment) and a convenient HTML server, `dotnet-serve` to serve the output.

2) In the root folder, run `dotnet build` - this builds and runs `src\Website` and its dependencies, and generates your HTML files in `\output`.

3) Run `.\start.cmd` - this invokes `dotnet-serve` on the output folder so you can view your blog articles in the browser (by default at `http://localhost:56001`). You can change the port, if needed in the script.

# Writing your blog articles

The input markdown files for your blog articles are in `src\Hosted\posts`. Add your `.md` files here with the naming convention `YYYY-MM-DD-YourArticleTitle.md`. Give at least the title in the YAML header, as follows:

```
---
title: A wonderful F# journey
subtitle: The best path to getting my F# blog up and running
---
```

Remember to rebuild this project after each change and/or new article to get the matching HTML output. Or alternatively, use the `src/Hosted` project to enable near-live edits - see below for details.

# Extending your blog website

The default blog is represented in `\src\Hosted\Main.fs` as follows:

```fsharp
type EndPoint =
| [<EndPoint "GET /">] Home
| [<EndPoint "GET /blog">] Article of slug:string
```

If you need other pages, such as an About page or a set of documentation pages, you can add further shapes to this type and enhance `Site.Main` accordingly.

# Making template changes

I recommend you run the `src\Hosted` project if you intend to make template/layout/style changes. By default, the master template (`index.html`) is used in such a way that updates to this file are reflected runtime, i.e. without requiring recompilation (unless you change the bindings/placeholders, in which case you need to recompile and adapt your `Main.fs` accordingly), significantly speeding up your development workflow.

You can run the hosted project as opposed to `src/Website` in Visual Studio by making it your default project and running it, and in Visual Studio Code (or in any terminal) by running (from the root folder):

```
dotnet run --project src\Hosted\Hosted.fsproj
```

By default, the hosted application is deployed to `localhost:5000`, note the different port here.

When you change existing blog articles or add new ones, you need to reload their markdown files. I have added a sitelet endpoint to trigger this:

```fsharp
type EndPoint =
...
| [<EndPoint "GET /refresh">] Refresh
```

You can simply go to `http://localhost:5000/refresh`, and reload your article to reflect any changes you made to it while the hosted blog has been running.

Have fun writing your blog with BlogEngine!

# Appendix - Image credit

* src\Website\img\Banner.jpg by Plush Design Studio on Unsplash - https://unsplash.com/photos/UHqfUTDmdC4

2 changes: 1 addition & 1 deletion install.ps1
Expand Up @@ -3,7 +3,7 @@ param(
)

# Install npm packages
pushd src\Website
pushd src\Hosted
npm install
popd

Expand Down
20 changes: 10 additions & 10 deletions src/Client/Client.fsproj
@@ -1,23 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<WebSharperProject>Html</WebSharperProject>
<WebSharperHtmlDirectory>$(MSBuildThisFileDirectory)bin\html</WebSharperHtmlDirectory>
<StartAction>Program</StartAction>
<StartProgram>$([System.Environment]::GetEnvironmentVariable(`WinDir`))\explorer.exe</StartProgram>
<StartArguments>$(WebSharperHtmlDirectory)</StartArguments>
<WebSharperHtmlDirectory>../Hosted/js/</WebSharperHtmlDirectory>
</PropertyGroup>

<ItemGroup>
<Compile Include="Client.fs" />
</ItemGroup>

<Target Name="CleanGeneratedFiles" AfterTargets="Clean">
<RemoveDir Directories="$(WebSharperHtmlDirectory)" />
</Target>

<ItemGroup>
<PackageReference Include="WebSharper" Version="4.5.19.349" />
<PackageReference Include="WebSharper.FSharp" Version="4.5.19.349" />
<PackageReference Include="WebSharper.HighlightJS" Version="4.5.2.168" />
<PackageReference Include="WebSharper.UI" Version="4.5.13.180" />
<PackageReference Include="WebSharper" Version="4.6.0.361" />
<PackageReference Include="WebSharper.FSharp" Version="4.6.0.361" />
<PackageReference Include="WebSharper.HighlightJS" Version="4.6.0.175" />
<PackageReference Include="WebSharper.UI" Version="4.6.0.190" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/Client/wsconfig.json
@@ -1,5 +1,5 @@
{
"$schema": "https://websharper.com/wsconfig.schema.json",
"project": "bundle",
"outputDir": "../Website/js"
"outputDir": "../Hosted/js"
}
37 changes: 37 additions & 0 deletions src/Hosted/Hosted.fsproj
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Content Include="posts/**/*.*" />
<Content Include="scss/**/*.*" />
<Content Include="Properties/launchSettings.json" />
<Compile Include="Main.fs" />
<Compile Include="Startup.fs" />
<Content Include="index.html" CopyToPublishDirectory="Always" />
<None Include="wsconfig.json" />
</ItemGroup>

<Target Name="CleanGeneratedFiles" AfterTargets="Clean">
<RemoveDir Directories="$(MSBuildProjectDirectory)/css" />
<RemoveDir Directories="$(MSBuildProjectDirectory)/Content" />
<RemoveDir Directories="$(MSBuildProjectDirectory)/Scripts" />
</Target>

<ItemGroup>
<PackageReference Include="BuildWebCompiler" Version="1.12.405" />
<PackageReference Include="Markdig" Version="0.18.0" />
<PackageReference Include="WebSharper" Version="4.6.0.361" />
<PackageReference Include="WebSharper.FSharp" Version="4.6.0.361" />
<PackageReference Include="WebSharper.UI" Version="4.6.0.190" />
<PackageReference Include="WebSharper.AspNetCore" Version="4.6.0.120" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Client\Client.fsproj" />
</ItemGroup>

</Project>
41 changes: 30 additions & 11 deletions src/Website/Main.fs → src/Hosted/Main.fs
Expand Up @@ -9,6 +9,7 @@ open WebSharper.UI.Server
type EndPoint =
| [<EndPoint "GET /">] Home
| [<EndPoint "GET /blog">] Article of slug:string
| [<EndPoint "GET /refresh">] Refresh

module Markdown =
open Markdig
Expand All @@ -26,8 +27,8 @@ module Markdown =
.UseCustomContainers()
.UseMathematics()
.UseEmojiAndSmiley()
.UseAdvancedExtensions()
.UseYamlFrontMatter()
.UseAdvancedExtensions()
.Build()

let Convert content = Markdown.ToHtml(content, pipeline)
Expand Down Expand Up @@ -77,7 +78,7 @@ module Site =
open System.IO
open WebSharper.UI.Html

type MainTemplate = Templating.Template<"index.html", serverLoad=Templating.ServerLoad.WhenChanged>
type MainTemplate = Templating.Template<"..\\hosted\\index.html", serverLoad=Templating.ServerLoad.WhenChanged>

type [<CLIMutable>] Article =
{
Expand All @@ -88,8 +89,8 @@ module Site =
date: string
}

let Articles () : Map<string, Article> =
let folder = Path.Combine (__SOURCE_DIRECTORY__, "posts")
let Articles() : Map<string, Article> =
let folder = Path.Combine (__SOURCE_DIRECTORY__, @"..\hosted\posts")
if Directory.Exists folder then
Directory.EnumerateFiles(folder, "*.md", SearchOption.AllDirectories)
|> Seq.toList
Expand Down Expand Up @@ -129,12 +130,13 @@ module Site =
"Latest", "#", latest
]

let private head =
__SOURCE_DIRECTORY__ + "/js/Client.head.html"
let private head() =
__SOURCE_DIRECTORY__ + "/../Hosted/js/Client.head.html"
|> File.ReadAllText
|> Doc.Verbatim

let Page (title: option<string>) hasBanner articles (body: Doc) =
let head = head()
MainTemplate()
#if !DEBUG
.ReleaseMin(".min")
Expand Down Expand Up @@ -217,13 +219,15 @@ module Site =
.Doc()
|> Page (Some article.title) false articles

let articles : Map<string, Article> ref = ref Map.empty

let Main articles =
Application.MultiPage (fun (ctx: Context<_>) -> function
| Home ->
MainTemplate.HomeBody()
.ArticleList(
Doc.Concat [
for (_, article) in Map.toList articles ->
for (_, article) in Map.toList !articles ->
MainTemplate.ArticleCard()
.Author("My name")
.Title(article.title)
Expand All @@ -233,20 +237,35 @@ module Site =
]
)
.Doc()
|> Page None false articles
|> Page None false !articles
| Article p ->
ArticlePage articles articles.[p]
let page =
if p.ToLower().EndsWith(".html") then
p.Substring(0, p.Length-5)
else
p
if (!articles).ContainsKey page then
ArticlePage !articles (!articles).[page]
else
Map.toList !articles
|> List.map fst
|> sprintf "Trying to find page \"%s\" (with key=\"%s\"), but it's not in %A" p page
|> Content.Text
| Refresh ->
// Reload the article cache
articles := Articles()
Content.Text "Articles reloaded."
)

[<Sealed>]
type Website() =
let articles = Site.Articles ()
let articles = ref <| Site.Articles()

interface IWebsite<EndPoint> with
member this.Sitelet = Site.Main articles
member this.Actions = [
Home
for (slug, _) in Map.toList articles do
for (slug, _) in Map.toList !articles do
Article slug
]

Expand Down
20 changes: 20 additions & 0 deletions src/Hosted/Properties/launchSettings.json
@@ -0,0 +1,20 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:50354/",
"sslPort": 44389
}
},
"profiles": {
"Hosted": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5000"
}
}
}

0 comments on commit 2650b6b

Please sign in to comment.