Full F# Blog
this post is part of the F# Advent Calendar 2018
Updated 2020-09-23 to use official FSharp.Formatting release
Christmas arrived early this year: my pull request to update FSharp.Formatting to netstandard2.0 was accepted on Oct 22 .
This, combined with a discussion at Open FSharp with Alfonso - famous for creating Fable - led me to use F#, and only F# to publish my blog.
Why not just using wordpress ?
This blog was previously hosted by Gandi on dotclear, and it was ok for years.
Pros:
- Free
- Managed
- No ads
Cons:
- Hard to style
- No extensibility
- Low ownership
- Painful editing
The editing was really painful in the browser, especialy since Live Writer was not supported anymore. I managed to use FSharp.Formatting for some posts. But when pasting the code in the editor, most of the formatting was rearranged due to platform limitations. I took infinite time to gets the tips working.
But it was free.
I could have found another blogging platform, but most of them are free because ads. Or I could pay. But those plateforms are not necessary better...
And there was anyway an extra challenge: I didn't want to break existing links.
I have some traffic on my blog. External posts, Stack Overflow answers, tweets pointing to my posts. It would be a waste to lose all this.
So it took some time and I finally decided to host it myself.
My stack
The constraint of not breaking existing structure forced me to actually develop my own solution. And since I'm quite fluent in F#... everything would eventually be in F#.
-
F# scripts for building
- FSharp.Literate to convert md and fsx files to HTML.
- Fable.React server side rendering for a static full F# template engine
- FSharp.Data for RSS generation
-
F# script for testing
- Giraffe to host a local web server to view the result
- F# script for publishing
The other constraint was price. And since previous solution was free, I took it has a chanlenge to try to make it as cheap as possible. There are lots of free options, but never with custom domain (needed to not break links), and never with https (mandatory since google is showing HTTP sites as unsecure).
I choosed Azure Functions, but with no code. I get:
- Custom domain for free
- Free SSL certificate thanks to letsencrypt
- KeyVault for secret management
- Staging for deployment
- Full ownership
- Almost free (currently around 0.01€/month)
I'll detail the F# part in this post. The azure hosting will be for part 2, and letsencrypt for part 3.
Fake 5
Fake 5 is the tool to write build scripts or simply scripts in F# (thx forki).
To install it, simply type in a command line
|
and you're done.
The strength of Fake is that it can reference and load nuget packages using paket (thx again forki) directly in the fsx script:
|
Fake will then dowload and reference specified packages.
To help with completion at design time you can reference an autogenerated fsx like this:
|
here I use .. because this blog post is in a subfolder in my project
Packages can then be used:
open FSharp.Data
FSharp.Literate
FSharp.Formatting is an awsome project to convert F# and MarkDown to HTML.
Conversion to netstandard has been stuck for some time due to its dependency on Razor for HTML templating.
Razor has changed a lot in AspNetCore, and porting existing code was a real nightmare.
To speed up things, I proposed to only port FSharp.Literate and the rest of the project but to get rid of formatting and this dependency on Razor. There is now a beta nuget package deployed on appveyor at https://ci.appveyor.com/nuget/fsharp-formatting : so for my build script I use the following references:
|
open FSharp.Formatting.Literate
Markdown
The simplest usage of FSharp.Literate is for posts with no code. In this case, I write it as MarkDown file and convert them using the TransformHtml function:
let md = """# Markdown is cool
especially with *FSharp.Formatting* ! """
|> FSharp.Formatting.Markdown.Markdown.ToHtml
which returns:
|
Fsx
We can also take a snipet of F# and convert it to HTML:
open FSharp.Formatting.Literate.Evaluation
let snipet =
"""
(** # *F# literate* in action *)
printfn "Hello"
"""
let parse source =
let fsharpCoreDir = "-I:" + __SOURCE_DIRECTORY__ + @"\..\packages\full\FSharp.Core\lib\netstandard2.0\"
let fcsDir = "-I:" + __SOURCE_DIRECTORY__ + @"\..\packages\full\FSharp.Compiler.Service\lib\netstandard2.0\"
let fcs = "-r:" + __SOURCE_DIRECTORY__ + @"\..\packages\full\FSharp.Compiler.Service\lib\netstandard2.0\FSharp.Compiler.Service.dll"
Literate.ParseScriptString(
source,
fscOptions = String.concat " " [ fsharpCoreDir; fcsDir; fcs ],
fsiEvaluator = FsiEvaluator([|fsharpCoreDir; fcsDir; fcs|]))
let fs =
let doc =
snipet
|> parse
Literate.ToHtml(doc, "", true, true)
The fsharpCoreDir and the -I options are necessary to help FSharp.Literate resolve the path to FSharp.Core. System.Runtime must also be referenced to get tooltips working fine with netstandard assemblies. FSharp interactive is not totally ready for production due to this problem, but with some helps, it works for our need.
Running this code we get:
|
As you can see, the code contains a reference to a javascript functions. You can find an implementation on github. It displays type information tool tips generated by the compiler. All the type information is generated during parsing phase:
let tips =
let doc = parse snipet
doc.FormattedTips
|
this way readers get full type inference information in the browser !
But it's even better than that. You can also get the value of some bindings in your ouput:
let values =
"""
(** # code execution *)
let square x = x * x
let v = square 3
(** the value is: *)
(*** include-value: v ***)"""
|> parse
|> Literate.ToHtml
and the result is:
|
You can see that the value of v - 9 - has been computed in the output HTML !
As you can gess, I just used this feature to print the HTML output! Inception !
It also works for printf:
let output =
"""
(** # printing *)
let square x = x * x
(*** define-output: result ***)
printfn "result: %d" (square 3)
(** the value is: *)
(*** include-output: result ***)"""
|> parse
|> Literate.ToHtml
Notice the presence of the printf output on the last line:
|
Templating
Now that we can convert the content to HTML, we need to add the surrounding layout.
I use Fable.React for this, but just the server side rendering. So there is no need for the JS tools, only the .net nuget.
After adding the nuget Fable.React
in the paket includes, we can open it and
start a HTML template:
open Fable.React
open Fable.React.Props
open FSharp.Formatting.Markdown
type Post = {
title: string
content: string
}
let template post =
html [Lang "en"] [
head [] [
title [] [ str ("My blog / " + post.title) ]
]
body [] [
RawText post.content
]
]
to convert it too string, we simply add the doctype to make it HTML5 compatible and use renderToString
let render html =
fragment [] [
RawText "<!doctype html>"
RawText "\n"
html ]
|> Fable.ReactServer.renderToString
let's use it :
let myblog =
{ title = "super post"
content = Markdown.ToHtml "# **interesting** things" }
|> template
|> render
now we get the final page:
|
RSS
Rss has lost attraction lately, but I still have requests every day on the atom feed.
Using Fsharp data, generating the RSS feed is straight forward:
#I @"..\packages\full\FSharp.Data\lib\netstandard2.0\"
#r "System.Xml.Linq"
#r "FSharp.Data"
open System
open FSharp.Data
open System.Security.Cryptography
[<Literal>]
let feedXml = """<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
xml:lang="en">
<title type="html">Think Before Coding</title>
<link href="http://thinkbeforecoding.com:82/feed/atom" rel="self" type="application/atom+xml"/>
<link href="http://thinkbeforecoding.com/" rel="alternate" type="text/html"
title=""/>
<updated>2017-12-09T01:20:21+01:00</updated>
<author>
<name>Jérémie Chassaing</name>
</author>
<id>urn:md5:18477</id>
<generator uri="http://www.dotclear.net/">Dotclear</generator>
<entry>
<title>fck: Fake Construction Kit</title>
<link href="http://thinkbeforecoding.com/post/2016/12/04/fck%3A-Fake-Construction-Kit" rel="alternate" type="text/html"
title="fck: Fake Construction Kit" />
<id>urn:md5:d78962772329a428a89ca9d77ae1a56b</id>
<updated>2016-12-04T10:34:00+01:00</updated>
<author><name>Jérémie Chassaing</name></author>
<dc:subject>f</dc:subject><dc:subject>FsAdvent</dc:subject>
<content type="html"> <p>Yeah it's christmas time again, and santa's elves are quite busy.</p>
ll name: Microsoft.FSharp.Core.Operators.not</div></content>
</entry>
<entry>
<title>Ukulele Fun for XMas !</title>
<link href="http://thinkbeforecoding.com/post/2015/12/17/Ukulele-Fun-for-XMas-%21" rel="alternate" type="text/html"
title="Ukulele Fun for XMas !" />
<id>urn:md5:5919e73c387df2af043bd531ea6edf47</id>
<updated>2015-12-17T10:44:00+01:00</updated>
<author><name>Jérémie Chassaing</name></author>
<dc:subject>F#</dc:subject>
<content type="html"> <div style="margin-top:30px" class="container row">
lt;/div></content>
</entry>
</feed>"""
type Rss = XmlProvider<feedXml>
let links: Rss.Link[] = [|
Rss.Link("https://thinkbeforecoding.com/feed/atom","self", "application/atom+xml", null)
Rss.Link("https://thinkbeforecoding.com/","alternate", "text/html", "thinkbeforecoding")
|]
let entry title link date content =
let md5Csp = MD5.Create()
let md5 =
md5Csp.ComputeHash(Text.Encoding.UTF8.GetBytes(content: string))
|> Array.map (sprintf "%2x")
|> String.concat ""
|> (+) "urn:md5:"
Rss.Entry(
title,
Rss.Link2(link, "alternate", "text/html", title),
md5,
DateTimeOffset.op_Implicit date,
Rss.Author2("Jérémie Chassaing"),
[||],
Rss.Content("html", content)
)
let feed entries =
Rss.Feed("en",
Rss.Title("html","thinkbeforecoding"),
links,DateTimeOffset.UtcNow,
Rss.Author("Jérémie Chassaing"),
"urn:md5:18477",
Rss.Generator("https://fsharp.org","F# script"),
List.toArray entries
)
just pass all posts to the feed function, and you get a full RSS feed.
Migration
To migrate from my previous blog, I exported all data to csv, and used the CSV type provider to parse it.
I extracted the HTML and put it in files, and generated a fsx file containing a list of posts with metadata:
- title
- date
- url
- category
Once done, I just have to map the post list using conversion and templates, and I have my new blog.
Wrapping it up
Using F# tools, I get easily a full control on my blog. And all this in my favorite language!
See you in next part about hosting in Azure.
Happy Christmas!
namespace FSharp
--------------------
namespace Microsoft.FSharp
namespace FSharp.Data
--------------------
namespace Microsoft.FSharp.Data
<summary> Static class that provides methods for formatting and transforming Markdown documents. </summary>
static member FSharp.Formatting.Markdown.Markdown.ToHtml: doc: FSharp.Formatting.Markdown.MarkdownDocument * ?newline: string * ?substitutions: (FSharp.Formatting.Templating.ParamKey * string) list * ?crefResolver: (string -> (string * string) option) * ?mdlinkResolver: (string -> string option) -> string
<summary> This type provides three simple methods for calling the literate programming tool. The <c>ConvertMarkdownFile</c> and <c>ConvertScriptFile</c> methods process a single Markdown document and F# script, respectively. The <c>ConvertDirectory</c> method handles an entire directory tree (looking for <c>*.fsx</c> and <c>*.md</c> files). </summary>
<namespacedoc><summary>Functionality to support literate programming for F# scripts</summary></namespacedoc>
<summary>Functional programming operators for string processing. Further string operations are available via the member functions on strings and other functionality in <a href="http://msdn2.microsoft.com/en-us/library/system.string.aspx">System.String</a> and <a href="http://msdn2.microsoft.com/library/system.text.regularexpressions.aspx">System.Text.RegularExpressions</a> types. </summary>
<category>Strings and Text</category>
<summary>Returns a new string made by concatenating the given strings with separator <c>sep</c>, that is <c>a1 + sep + ... + sep + aN</c>.</summary>
<param name="sep">The separator string to be inserted between the strings of the input sequence.</param>
<param name="strings">The sequence of strings to be concatenated.</param>
<returns>A new string consisting of the concatenated strings separated by the separation string.</returns>
<exception cref="T:System.ArgumentNullException">Thrown when <c>strings</c> is null.</exception>
type FsiEvaluator = interface IFsiEvaluator new: ?options: string[] * ?fsiObj: obj * ?addHtmlPrinter: bool * ?discardStdOut: bool * ?disableFsiObj: bool * ?onError: (string -> unit) -> FsiEvaluator member RegisterTransformation: f: (obj * Type * int -> MarkdownParagraph list option) -> unit member EvaluationFailed: IEvent<FsiEvaluationFailedInfo>
<summary> A wrapper for F# interactive service that is used to evaluate inline snippets </summary>
--------------------
new: ?options: string[] * ?fsiObj: obj * ?addHtmlPrinter: bool * ?discardStdOut: bool * ?disableFsiObj: bool * ?onError: (string -> unit) -> FsiEvaluator
<summary> Formatted tool tips </summary>
val string: value: 'T -> string
<summary>Converts the argument to a string using <c>ToString</c>.</summary>
<remarks>For standard integer and floating point values the and any type that implements <c>IFormattable</c><c>ToString</c> conversion uses <c>CultureInfo.InvariantCulture</c>. </remarks>
<param name="value">The input value.</param>
<returns>The converted string.</returns>
--------------------
type string = System.String
<summary>An abbreviation for the CLI type <see cref="T:System.String" />.</summary>
<category>Basic Types</category>
<summary> Alias of `ofString` </summary>
<summary> Instantiate a React fragment </summary>
static member Markdown.ToHtml: doc: MarkdownDocument * ?newline: string * ?substitutions: (FSharp.Formatting.Templating.ParamKey * string) list * ?crefResolver: (string -> (string * string) option) * ?mdlinkResolver: (string -> string option) -> string
union case MarkdownSpan.Literal: text: string * range: MarkdownRange option -> MarkdownSpan
--------------------
type LiteralAttribute = inherit Attribute new: unit -> LiteralAttribute
<summary>Adding this attribute to a value causes it to be compiled as a CLI constant literal.</summary>
<category>Attributes</category>
--------------------
new: unit -> LiteralAttribute
<summary>Typed representation of a XML file.</summary> <param name='Sample'>Location of a XML sample file or a string containing a sample XML document.</param> <param name='SampleIsList'>If true, the children of the root in the sample document represent individual samples for the inference.</param> <param name='Global'>If true, the inference unifies all XML elements with the same name.</param> <param name='Culture'>The culture used for parsing numbers and dates. Defaults to the invariant culture.</param> <param name='Encoding'>The encoding used to read the sample. You can specify either the character set name or the codepage number. Defaults to UTF8 for files, and to ISO-8859-1 the for HTTP requests, unless <c>charset</c> is specified in the <c>Content-Type</c> response header.</param> <param name='ResolutionFolder'>A directory that is used when resolving relative file references (at design time and in hosted execution).</param> <param name='EmbeddedResource'>When specified, the type provider first attempts to load the sample from the specified resource (e.g. 'MyCompany.MyAssembly, resource_name.xml'). This is useful when exposing types generated by the type provider.</param> <param name='InferTypesFromValues'>If true, turns on additional type inference from values. (e.g. type inference infers string values such as "123" as ints and values constrained to 0 and 1 as booleans. The XmlProvider also infers string values as JSON.)</param> <param name='Schema'>Location of a schema file or a string containing xsd.</param>
<summary>Represents the abstract class from which all implementations of the <see cref="T:System.Security.Cryptography.MD5" /> hash algorithm inherit.</summary>
MD5.Create(algName: string) : MD5
HashAlgorithm.ComputeHash(buffer: byte[]) : byte[]
HashAlgorithm.ComputeHash(buffer: byte[], offset: int, count: int) : byte[]
union case HttpResponseBody.Text: string -> HttpResponseBody
--------------------
namespace System.Text
<summary>Represents a character encoding.</summary>
<summary>Gets an encoding for the UTF-8 format.</summary>
<returns>An encoding for the UTF-8 format.</returns>
Text.Encoding.GetBytes(chars: char[]) : byte[]
Text.Encoding.GetBytes(chars: ReadOnlySpan<char>, bytes: Span<byte>) : int
Text.Encoding.GetBytes(s: string, index: int, count: int) : byte[]
Text.Encoding.GetBytes(chars: char[], index: int, count: int) : byte[]
Text.Encoding.GetBytes(chars: nativeptr<char>, charCount: int, bytes: nativeptr<byte>, byteCount: int) : int
Text.Encoding.GetBytes(s: string, charIndex: int, charCount: int, bytes: byte[], byteIndex: int) : int
Text.Encoding.GetBytes(chars: char[], charIndex: int, charCount: int, bytes: byte[], byteIndex: int) : int
val string: value: 'T -> string
<summary>Converts the argument to a string using <c>ToString</c>.</summary>
<remarks>For standard integer and floating point values the and any type that implements <c>IFormattable</c><c>ToString</c> conversion uses <c>CultureInfo.InvariantCulture</c>. </remarks>
<param name="value">The input value.</param>
<returns>The converted string.</returns>
--------------------
type string = String
<summary>An abbreviation for the CLI type <see cref="T:System.String" />.</summary>
<category>Basic Types</category>
<summary>Provides methods for creating, manipulating, searching, and sorting arrays, thereby serving as the base class for all arrays in the common language runtime.</summary>
<summary>Builds a new array whose elements are the results of applying the given function to each of the elements of the array.</summary>
<param name="mapping">The function to transform elements of the array.</param>
<param name="array">The input array.</param>
<returns>The array of transformed elements.</returns>
<exception cref="T:System.ArgumentNullException">Thrown when the input array is null.</exception>
<summary>Print to a string using the given format.</summary>
<param name="format">The formatter.</param>
<returns>The formatted result.</returns>
type String = interface IEnumerable<char> interface IEnumerable interface ICloneable interface IComparable interface IComparable<string> interface IConvertible interface IEquatable<string> new: value: nativeptr<char> -> unit + 8 overloads member Clone: unit -> obj member CompareTo: value: obj -> int + 1 overload ...
<summary>Represents text as a sequence of UTF-16 code units.</summary>
--------------------
String(value: nativeptr<char>) : String
String(value: char[]) : String
String(value: ReadOnlySpan<char>) : String
String(value: nativeptr<sbyte>) : String
String(c: char, count: int) : String
String(value: nativeptr<char>, startIndex: int, length: int) : String
String(value: char[], startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: Text.Encoding) : String
[<Struct>] type DateTimeOffset = new: dateTime: DateTime -> unit + 5 overloads member Add: timeSpan: TimeSpan -> DateTimeOffset member AddDays: days: float -> DateTimeOffset member AddHours: hours: float -> DateTimeOffset member AddMilliseconds: milliseconds: float -> DateTimeOffset member AddMinutes: minutes: float -> DateTimeOffset member AddMonths: months: int -> DateTimeOffset member AddSeconds: seconds: float -> DateTimeOffset member AddTicks: ticks: int64 -> DateTimeOffset member AddYears: years: int -> DateTimeOffset ...
<summary>Represents a point in time, typically expressed as a date and time of day, relative to Coordinated Universal Time (UTC).</summary>
--------------------
DateTimeOffset ()
DateTimeOffset(dateTime: DateTime) : DateTimeOffset
DateTimeOffset(dateTime: DateTime, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(ticks: int64, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, calendar: Globalization.Calendar, offset: TimeSpan) : DateTimeOffset
<summary>Gets a <see cref="T:System.DateTimeOffset" /> object whose date and time are set to the current Coordinated Universal Time (UTC) date and time and whose offset is <see cref="F:System.TimeSpan.Zero" />.</summary>
<returns>An object whose date and time is the current Coordinated Universal Time (UTC) and whose offset is <see cref="F:System.TimeSpan.Zero" />.</returns>
union case HTMLAttr.List: string -> HTMLAttr
--------------------
module List from Microsoft.FSharp.Collections
<summary>Contains operations for working with values of type <see cref="T:Microsoft.FSharp.Collections.list`1" />.</summary>
<namespacedoc><summary>Operations for collections such as lists, arrays, sets, maps and sequences. See also <a href="https://docs.microsoft.com/dotnet/fsharp/language-reference/fsharp-collection-types">F# Collection Types</a> in the F# Language Guide. </summary></namespacedoc>
--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T member IsEmpty: bool member Item: index: int -> 'T with get ...
<summary>The type of immutable singly-linked lists.</summary>
<remarks>Use the constructors <c>[]</c> and <c>::</c> (infix) to create values of this type, or the notation <c>[1;2;3]</c>. Use the values in the <c>List</c> module to manipulate values of this type, or pattern match against the values directly. </remarks>
<exclude />
<summary>Builds an array from the given list.</summary>
<param name="list">The input list.</param>
<returns>The array containing the elements of the list.</returns>