Skip to content
Browse files

Merge pull request #1 from granicz/add-hosted-support

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 2650b6bb6a635e9c5f0019aa2d794a252707e9a1
@@ -5,7 +5,13 @@
@@ -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}"
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}"
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Website", "src\Website\Website.fsproj", "{30CC8824-A610-4C33-962F-ED88339B25B3}"
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1,10 +1,75 @@
# How to build this project
# BlogEngine - your F# static site generator

1) Run `.\`
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 `.\` - 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 ``. 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:

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:

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 -

@@ -3,7 +3,7 @@ param(

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

@@ -1,23 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">


<Compile Include="Client.fs" />

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

<PackageReference Include="WebSharper" Version="" />
<PackageReference Include="WebSharper.FSharp" Version="" />
<PackageReference Include="WebSharper.HighlightJS" Version="" />
<PackageReference Include="WebSharper.UI" Version="" />
<PackageReference Include="WebSharper" Version="" />
<PackageReference Include="WebSharper.FSharp" Version="" />
<PackageReference Include="WebSharper.HighlightJS" Version="" />
<PackageReference Include="WebSharper.UI" Version="" />

@@ -1,5 +1,5 @@
"$schema": "",
"project": "bundle",
"outputDir": "../Website/js"
"outputDir": "../Hosted/js"
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk.Web">


<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" />

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

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

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

@@ -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
@@ -26,8 +27,8 @@ module Markdown =

let Convert content = Markdown.ToHtml(content, pipeline)
@@ -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 =
@@ -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
@@ -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()
#if !DEBUG
@@ -217,13 +219,15 @@ module Site =
|> 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 ->
Doc.Concat [
for (_, article) in Map.toList articles ->
for (_, article) in Map.toList !articles ->
.Author("My name")
@@ -233,20 +237,35 @@ module Site =
|> 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)
if (!articles).ContainsKey page then
ArticlePage !articles (!articles).[page]
Map.toList !articles
|> 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."

type Website() =
let articles = Site.Articles ()
let articles = ref <| Site.Articles()

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

@@ -0,0 +1,20 @@
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:50354/",
"sslPort": 44389
"profiles": {
"Hosted": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"applicationUrl": "http://localhost:5000"

0 comments on commit 2650b6b

Please sign in to comment.
You can’t perform that action at this time.