{"id":13686690,"url":"https://github.com/ionide/Fornax","last_synced_at":"2025-05-01T09:32:25.184Z","repository":{"id":40450817,"uuid":"139614272","full_name":"ionide/Fornax","owner":"ionide","description":"Scriptable static site generator using type safe F# DSL to define page templates.","archived":false,"fork":false,"pushed_at":"2023-03-07T15:46:55.000Z","size":1012,"stargazers_count":246,"open_issues_count":22,"forks_count":44,"subscribers_count":16,"default_branch":"master","last_synced_at":"2025-04-28T10:24:16.747Z","etag":null,"topics":["blog","documentation","documentation-generator","dotnet","dsl","fsharp","static-site","static-site-generator"],"latest_commit_sha":null,"homepage":"","language":"F#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ionide.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null},"funding":{"open_collective":"ionide"}},"created_at":"2018-07-03T17:07:00.000Z","updated_at":"2025-04-03T09:04:56.000Z","dependencies_parsed_at":"2023-01-19T19:44:54.590Z","dependency_job_id":"9f635909-a7ad-46f6-adf6-9b5fd3c79239","html_url":"https://github.com/ionide/Fornax","commit_stats":{"total_commits":216,"total_committers":40,"mean_commits":5.4,"dds":0.4537037037037037,"last_synced_commit":"b9a1da015125830f900ea890f11765eb6d657082"},"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ionide%2FFornax","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ionide%2FFornax/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ionide%2FFornax/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ionide%2FFornax/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ionide","download_url":"https://codeload.github.com/ionide/Fornax/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251852885,"owners_count":21654479,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["blog","documentation","documentation-generator","dotnet","dsl","fsharp","static-site","static-site-generator"],"created_at":"2024-08-02T15:00:37.959Z","updated_at":"2025-05-01T09:32:24.761Z","avatar_url":"https://github.com/ionide.png","language":"F#","funding_links":["https://opencollective.com/ionide"],"categories":["F# #"],"sub_categories":[],"readme":"# Fornax\n\n![Logo](https://raw.githubusercontent.com/Ionide/Fornax/master/logo/Fornax.png)\n\nFornax is a **scriptable static site generator** using type safe F# DSL to define page layouts.\n\nFornax is part of the Ionide tooling suite - You can support its development on [Open Collective](https://opencollective.com/ionide).\n\n[![open collective backers](https://img.shields.io/opencollective/backers/ionide.svg?color=blue)](https://opencollective.com/ionide)\n[![open collective sponsors](https://img.shields.io/opencollective/sponsors/ionide.svg?color=blue)](https://opencollective.com/ionide)\n\n[![Open Collective](https://opencollective.com/ionide/donate/button.png?color=blue)](https://opencollective.com/ionide)\n\n\n## Working features\n\n* Creating custom data loaders using `.fsx` files, meaning you can use anything you can imagine as a source of data for your site, not only predefined `.md` or `.yml` files\n* Creating custom generators using `.fsx` files, meaning you can generate any type of output you want\n* Dynamic configuration using `.fsx` file\n* Watch mode that rebuilds your page whenever you change data, or any script file.\n\n\n## Installation\n\nFornax is released as a global .Net Core tool. You can install it with `dotnet tool install fornax -g`\n\n## CLI Application\n\nThe main functionality of Fornax comes from CLI applications that lets user scaffold, and generate webpages.\n\n* `fornax new` - scaffolds new blog in current working directory using a really simple template\n* `fornax build` - builds webpage, puts output to `_public` folder\n* `fornax watch` - starts a small webserver that hosts your generated site, and a background process that recompiles the site whenever any changes are detected. This is the recommended way of working with Fornax.\n* `fornax clean` - removes the output directory and any temp files\n* `fornax version` - prints out the currently-installed version of Fornax\n* `fornax help` - prints out help\n\n## Getting started\n\nEasiest way to get started with `fornax` is running `fornax new` and then `fornax watch` - this will create a fairly minimal blog site template, start `fornax` in watch mode and then start a webserver. Then you can go to `localhost:8080` in your browser to see the page, and edit the scaffolded files in an editor to make changes.\nAdditionally, you can take a look at the `samples` folder in this repository - it has a couple more `loaders` and `generators` that you may use in your website.\n\n## Website definition\n\nFornax is using normal F# code (F# script files) to define any of its core concepts: `loaders`, `generators` and `config`.\n\n### SiteContents\n\n`SiteContents` is a fairly simple type that provides access to any information available to Fornax. The information is provided by using `loaders` and can then be accessed in `generators`.\n\n`SiteContents` has several functions in it's public API:\n\n```fsharp\ntype A = {a: string}\ntype B = {b: int; c: int}\n\nlet sc = SiteContents()\nsc.Add({a = \"test\"})\nsc.Add({a = \"test2\"})\nsc.Add({a = \"test3\"})\n\nsc.Add({b = 1; c = 3}) //You can add objects of different types, `Add` method is generic.\n\nlet as = sc.TryGetValues\u003cA\u003e() //This will return an option of sequence of all added elements for a given type - in this case it will be 3 elements\nlet b = sc.TryGetValue\u003cB\u003e() //This will return an option of element for given type\n```\n\n### Loaders\n\n`Loader` is an F# script responsible for loading external data into generation context. The data typically includes things like content of `.md` files, some global site configuration, etc. But since those are normal F# functions, you can do whatever you need.\nWant to load information from local database, or from the internet? Sure, why not. Want to use the [World Bank type provider](https://fsprojects.github.io/FSharp.Data/library/WorldBank.html) to include some of the World Bank statistics? That's also possible - you can use any dependency in `loader`, just as in a normal F# script.\n\n`Loaders` are normal F# functions that takes as an input `SiteContents` and absolute path to the page root, and returns `SiteContents`:\n\n```fsharp\n#r \"../_lib/Fornax.Core.dll\"\n\ntype Page = {\n    title: string\n    link: string\n}\n\nlet loader (projectRoot: string) (siteContent: SiteContents) =\n    siteContent.Add({title = \"Home\"; link = \"/\"})\n    siteContent.Add({title = \"About\"; link = \"/about.html\"})\n    siteContent.Add({title = \"Contact\"; link = \"/contact.html\"})\n\n    siteContent\n```\n\n**Important note**: You can (and probably should) define multiple loaders - they will all be executed before site generation, and will propagate information into `SiteContents`\n\n### Generators\n\n`Generator` is an F# script responsible for generating output of the Fornax process. This is usually `.html` file, but can be anything else - actually `generator` API just requires to return `string` that will be saved to a file. Generators are, again, plain F# functions that as input takes `SiteContents`, absolute path to the page root, relative path to the file that's currently processed (may be empty for the global generators) and returns `string`:\n\n```fsharp\n#r \"../_lib/Fornax.Core.dll\"\n#if !FORNAX\n#load \"../loaders/postloader.fsx\"\n#endif\n\nopen Html\n\nlet generate' (ctx : SiteContents) (_: string) =\n    let posts = ctx.TryGetValues\u003cPostloader.Post\u003e () |\u003e Option.defaultValue Seq.empty\n\n    let psts =\n        posts\n        |\u003e Seq.toList\n        |\u003e List.map (fun p -\u003e span [] [!! p.link] )\n\n    html [] [\n        div [] psts\n    ]\n\nlet generate (ctx : SiteContents) (projectRoot: string) (page: string) =\n    generate' ctx page\n    |\u003e HtmlElement.ToString\n```\n\n**Important note**: You can (and probably should) define multiple generators - they will generate different kinds of pages and/or content, such as `post`, `index`, `about`, `rss` etc.\n\n### Configuration\n\n`Configuration` is an F# script file that defines when which analyzers need to be run, and how to save its output. A `Config.fsx` file needs to be put in the root of your site project (the place from which you run the `fornax` CLI tool)\n\n```fsharp\n#r \"../_lib/Fornax.Core.dll\"\n\nopen Config\nopen System.IO\n\nlet postPredicate (projectRoot: string, page: string) =\n    let fileName = Path.Combine(projectRoot,page)\n    let ext = Path.GetExtension page\n    if ext = \".md\" then\n        let ctn = File.ReadAllText fileName\n        ctn.Contains(\"layout: post\")\n    else\n        false\n\nlet staticPredicate (projectRoot: string, page: string) =\n    let ext = Path.GetExtension page\n    if page.Contains \"_public\" ||\n       page.Contains \"_bin\" ||\n       page.Contains \"_lib\" ||\n       page.Contains \"_data\" ||\n       page.Contains \"_settings\" ||\n       page.Contains \"_config.yml\" ||\n       page.Contains \".sass-cache\" ||\n       page.Contains \".git\" ||\n       page.Contains \".ionide\" ||\n       ext = \".fsx\"\n    then\n        false\n    else\n        true\n\nlet config = {\n    Generators = [\n        {Script = \"less.fsx\"; Trigger = OnFileExt \".less\"; OutputFile = ChangeExtension \"css\" }\n        {Script = \"sass.fsx\"; Trigger = OnFileExt \".scss\"; OutputFile = ChangeExtension \"css\" }\n        {Script = \"post.fsx\"; Trigger = OnFilePredicate postPredicate; OutputFile = ChangeExtension \"html\" }\n        {Script = \"staticfile.fsx\"; Trigger = OnFilePredicate staticPredicate; OutputFile = SameFileName }\n        {Script = \"index.fsx\"; Trigger = Once; OutputFile = NewFileName \"index.html\" }\n\n    ]\n}\n\n```\n\nPossible Generator Triggers are:\n- `Once` : Runs once, globally.\n- `OnFile filename`: Run once for the given filename.\n- `OnFileExt extension` : Runs once for each file with the given extension.\n- `OnFilePredicate predicate` : Runs once for each file satisfying the predicate (`string -\u003e string`).\n\nPossible Generator Outputs are:\n- `SameFileName` :  Output has the same filename as input file.\n- `ChangeExtension newExtension` : Output has the same filename but with extention change to `newExtension`.\n- `NewFileName newFileName` : Output filename is `newFileName`.\n- `Custom mapper` : Output filename is the result of applying the mapper to the input filename.\n- `MultipleFiles mapper` : Outputs multiple files, the names of which are a result of applying the mapper to the first string output of the generator.\n\n**Note**: For `MultipleFiles` the `generate` function *must* output a `list\u003cstring * string\u003e`.\n\n\n## How to contribute\n\n*Imposter syndrome disclaimer*: I want your help. No really, I do.\n\nThere might be a little voice inside that tells you you're not ready; that you need to do one more tutorial, or learn another framework, or write a few more blog posts before you can help me with this project.\n\nI assure you, that's not the case.\n\nThis project has some clear Contribution Guidelines and expectations that you can [read here](https://github.com/Ionide/Fornax/blob/master/CONTRIBUTING.md).\n\nThe contribution guidelines outline the process that you'll need to follow to get a patch merged. By making expectations and process explicit, I hope it will make it easier for you to contribute.\n\nAnd you don't just have to write code. You can help out by writing documentation, tests, or even by giving feedback about this work. (And yes, that includes giving feedback about the contribution guidelines.)\n\nThank you for contributing!\n\n## Build process\n\n * You need [.NET 6.0 SDK](https://dotnet.microsoft.com/download/dotnet/6.0)\n * Run `dotnet tool restore` to restore the .NET 6 local tools defined at .config/dotnet-tools.json\n * To build the project run `dotnet run` (this will run the `build.fsproj` project that contains the FAKE build pipeline.)\n * To run unit tests run `dotnet run Test`\n\n\n## Contributing and copyright\n\nThe project is hosted on [GitHub](https://github.com/Ionide/Fornax) where you can [report issues](https://github.com/Ionide/Fornax/issues), fork\nthe project and submit pull requests. Please read [Contribution Guide](https://github.com/Ionide/Fornax/blob/master/CONTRIBUTING.md)\n\nThe library is available under [MIT license](https://github.com/Ionide/Fornax/blob/master/LICENSE.md), which allows modification and redistribution for both commercial and non-commercial purposes.\n\nPlease note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fionide%2FFornax","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fionide%2FFornax","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fionide%2FFornax/lists"}