Repository: tlienart/Franklin.jl
Pull request #356 (2020-02-07 18:38:49)

Folderstructure
Addresses  #335 (this is far from finished)

* [x] complete the tests for FS2 (ongoing)
* [x] migrate franklindocs (almost done)
* [x] fix publish + setup gh-pages
* [x] deploy franklintemplates

## Notes

* `@def folder_structure = v"0.1"` to stay with old mode.


Patch:
diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml
new file mode 100644
index 00000000..d77d3a0c
--- /dev/null
+++ b/.github/workflows/TagBot.yml
@@ -0,0 +1,11 @@
+name: TagBot
+on:
+  schedule:
+    - cron: 0 * * * *
+jobs:
+  TagBot:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: JuliaRegistries/TagBot@v1
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/NEWS.md b/NEWS.md
new file mode 100644
index 00000000..282951fe
--- /dev/null
+++ b/NEWS.md
@@ -0,0 +1,88 @@
+# NEWS
+
+This document keeps track of breaking changes and what you can do if you update and things don't work anymore.
+
+Notes are in reverse chronological order.
+
+### v0.6
+
+The most important change is a complete re-vamp of the folder structure resulting from issue #335.
+
+TL;DR: _I just want to keep stuff as they were..._, just add a single line to your `config.md`:
+
+```markdown
+@def folder_structure = v"0.1"
+```
+
+things should _just work_, and if they don't please open an issue.
+
+---
+
+Now if you want to migrate your site: the "old" folder structure was essentially:
+
+```
+.
+├── assets
+├── css
+├── libs
+├── pub
+└── src
+    ├── _css
+    ├── _html_parts
+    ├── config.md
+    ├── index.md
+    ├── pages
+    └── search.md
+```
+
+where the "new" folder structure is now:
+
+```
+.
+├── __site
+├── _assets
+├── _css
+├── _layout
+├── _libs
+├── config.md
+├── index.md
+├── pages
+└── search.md
+```
+
+where `__site` will contain the full generated site. To migrate from old to new:
+
+1. move `assets` to `_assets`
+1. move `src/_css` to `_css`
+1. move `src/_html_parts` to `_layout`
+1. move `libs` to `_libs`
+1. move the rest of the content of `src/` up one level
+1. optionally move the content of `pages/` up one level (1)
+1. remove `pub/` and `css/`
+
+(1): since the `pub` disappears, there's also no need for the `pages/` folder but you can keep it if you like.
+
+Then you may have to:
+
+- fix links in `_layout/head.html` removing `/pub/` and `.html` so for instance `/pub/menu1.html` becomes `/menu1/`,
+- fix a few of your internal links (use `verify_links()` to help you with that),
+- set things up so that the content of  `__site` is what's served on GitHub or elsewhere.
+- **Note**: links to stylesheets should still be ok, even though the source is now in `_css`, this gets mapped to `__site/_css/` and so `/css/somesheet.css` will still work fine.
+
+In terms of understanding how the generated paths look, consider the following mappings:
+
+```
+`/index.md`         -->     `__site/index.html`
+`/index.html`       -->     `__site/index.html`
+`/menu1.md`         -->     `__site/menu1/index.html`
+`/folder/index.md`  -->     `__site/folder/index.html`
+`/folder/page.md`   -->     `__site/folder/page/index.html`
+```
+
+This allows to have urls like `www.site.ext/menu1/`.
+
+**If you encounter issues during the process, open an issue on  GitHub, I'll try to help and improve these instructions at the same time, thanks!**
+
+### v0.5 (JuDoc -> Frankin)
+
+See [the corresponding release notes](https://github.com/tlienart/Franklin.jl/releases/tag/v0.5.0) which include a step-by-step of how to migrate from JuDoc to Franklin.s
diff --git a/Project.toml b/Project.toml
index e115e415..7600c8d4 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,7 +1,7 @@
 name = "Franklin"
 uuid = "713c75ef-9fc9-4b05-94a9-213340da978e"
 authors = ["Thibaut Lienart <tlienart@me.com>"]
-version = "0.5.0"
+version = "0.6.0"
 
 [deps]
 Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
@@ -20,9 +20,9 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
 
 [compat]
 DocStringExtensions = "^0.8"
-FranklinTemplates = "^0.4.2"
+FranklinTemplates = "^0.5"
 HTTP = "^0.8"
-Literate = "^2.0"
+Literate = "^2.2"
 LiveServer = "^0.3"
 NodeJS = "^0.6, ^1"
 OrderedCollections = "^1.1"
diff --git a/src/Franklin.jl b/src/Franklin.jl
index 9909e95c..e71b3618 100644
--- a/src/Franklin.jl
+++ b/src/Franklin.jl
@@ -36,6 +36,15 @@ export jd2html # = fd2html
 # CONSTANTS
 #
 
+# Obtained via `dig www...`; may change over time; see check_ping
+# we check in sequence, one should work... this may need to be updated
+# over time.
+const IP_CHECK = (
+    "172.217.21.132" => "Google", # google
+    "140.82.118.4"   => "GitHub",   # github
+    "103.235.46.39"  => "Baidu",  # baidu
+    )
+
 """Big number when we want things to be far."""
 const BIG_INT = typemax(Int)
 
@@ -46,7 +55,8 @@ const FD_ENV = LittleDict(
     :SUPPRESS_ERR  => false,
     :SILENT_MODE   => false,
     :OFFSET_LXDEFS => -BIG_INT,
-    :CUR_PATH      => "")
+    :CUR_PATH      => "",
+    :STRUCTURE     => v"0.2")
 
 """Dict to keep track of languages and how comments are indicated and their extensions. This is relevant to allow hiding lines of code. """
 const CODE_LANG = LittleDict{String,NTuple{2,String}}(
@@ -101,7 +111,6 @@ include("utils/vars.jl")
 include("utils/misc.jl")
 include("utils/html.jl")
 include("utils/errors.jl")
-include("utils/easyr.jl")
 
 # PARSING
 include("parser/tokens.jl")
diff --git a/src/converter/html/functions.jl b/src/converter/html/functions.jl
index b3752581..193c01f5 100644
--- a/src/converter/html/functions.jl
+++ b/src/converter/html/functions.jl
@@ -46,8 +46,9 @@ end
 $(SIGNATURES)
 
 H-Function of the form `{{ insert fpath }}` to plug in the content of a file at
-`fpath`. Note that the base path is assumed to be `PATHS[:src_html]` so paths
-have to be expressed relative to that.
+`fpath`. Note that the base path is assumed to be `PATHS[:src_html]`
+(`< v"0.2"`) and `PATHS[:layout]` otherwise and so paths have to be expressed
+relative to that.
 """
 function hfun_insert(params::Vector{String})::String
     # check params
@@ -55,8 +56,9 @@ function hfun_insert(params::Vector{String})::String
         throw(HTMLFunctionError("I found a {{insert ...}} with more than one parameter. Verify."))
     end
     # apply
-    repl  = ""
-    fpath = joinpath(PATHS[:src_html], split(params[1], "/")...)
+    repl   = ""
+    layout = path(ifelse(FD_ENV[:STRUCTURE] < v"0.2", :src_html, :layout))
+    fpath  = joinpath(layout, split(params[1], "/")...)
     if isfile(fpath)
         repl = convert_html(read(fpath, String))
     else
diff --git a/src/converter/html/html.jl b/src/converter/html/html.jl
index a75a4527..fe2d20f7 100644
--- a/src/converter/html/html.jl
+++ b/src/converter/html/html.jl
@@ -91,13 +91,19 @@ function process_html_qblocks(hs::AS, qblocks::Vector{AbstractBlock},
     return String(take!(htmls))
 end
 
+"""
+    match_url(base, cand)
 
+Try to match two url indicators.
+"""
 function match_url(base::AS, cand::AS)
-    sbase = base[1] == "/" ? base[2:end] : base
-    scand = cand[1] == "/" ? cand[2:end] : cand
+    sbase = base[1] == '/' ? base[2:end] : base
+    scand = cand[1] == '/' ? cand[2:end] : cand
     # joker-style syntax
-    if endswith(cand, "/*")
-        return startswith(sbase, scand[1:prevind(scand, lastindex(scand))])
+    if endswith(scand, "/*")
+        return startswith(sbase, scand[1:prevind(scand, lastindex(scand), 2)])
+    elseif endswith(scand, "/")
+        scand = scand[1:prevind(scand, lastindex(scand))]
     end
     return splitext(scand)[1] == sbase
 end
@@ -170,13 +176,14 @@ function process_html_cond(hs::AS, qblocks::Vector{AbstractBlock},
             k = Int(!haskey(LOCAL_VARS, βi.vname))
         else
             # HIsPage//HIsNotPage
-            #
-            # current path is relative to /src/ for instance
-            # /src/pages/blah.md -> pages/blah
-            # if starts with `pages/`, replaces by `pub/`:
-            # pages/blah => pub/blah
             rpath = splitext(unixify(locvar("fd_rpath")))[1]
-            rpath = replace(rpath, Regex("^pages") => "pub")
+            if FD_ENV[:STRUCTURE] < v"0.2"
+                # current path is relative to /src/ for instance
+                # /src/pages/blah.md -> pages/blah
+                # if starts with `pages/`, replaces by `pub/`:
+                # pages/blah => pub/blah
+                rpath = replace(rpath, Regex("^pages") => "pub")
+            end
             # compare with β.pages
             inpage = any(p -> match_url(rpath, p), βi.pages)
 
diff --git a/src/converter/html/link_fixer.jl b/src/converter/html/link_fixer.jl
index 550eaea7..83401b89 100644
--- a/src/converter/html/link_fixer.jl
+++ b/src/converter/html/link_fixer.jl
@@ -71,8 +71,9 @@ end
 """
 $(SIGNATURES)
 
-for a project website, for instance `username.github.io/project/` all paths should eventually
-be pre-prended with `/project/`. This would happen just before you publish the website.
+for a project website, for instance `username.github.io/project/` all paths
+should eventually be pre-prended with `/project/`. This would happen just
+before you publish the website.
 """
 function fix_links(pg::String)::String
     pp = strip(GLOBAL_VARS["prepath"].first, '/')
diff --git a/src/converter/latex/io.jl b/src/converter/latex/io.jl
index ab27fe8c..4be47740 100644
--- a/src/converter/latex/io.jl
+++ b/src/converter/latex/io.jl
@@ -60,7 +60,11 @@ function _lx_input_plot(rpath::AS, id::AS="")
             lc_e = lowercase(e)
             if f == pname && lc_e ∈ (".gif", ".jpg", ".jpeg", ".png", ".svg")
                 # construct a relative path to the plot
-                reldir = odir[length(PATHS[:folder])+1:end]
+                if FD_ENV[:STRUCTURE] < v"0.2"
+                    reldir = odir[length(PATHS[:folder])+1:end]
+                else
+                    reldir = odir[length(PATHS[:site])+1:end]
+                end
                 ppath  = unixify(joinpath(reldir, pname * lc_e))
                 return html_img(ppath)
             end
@@ -148,7 +152,11 @@ function lx_figalt(lxc::LxCom, _)
                      (".png", ".jpeg", ".jpg", ".svg", ".gif"), (fext,))
     for ext ∈ candext
         candpath = fdir * ext
-        syspath  = joinpath(PATHS[:folder], split(candpath, '/')...)
+        if FD_ENV[:STRUCTURE] < v"0.2"
+            syspath  = joinpath(PATHS[:folder], split(candpath, '/')...)
+        else
+            syspath  = joinpath(PATHS[:site], split(candpath, '/')...)
+        end
         isfile(syspath) && return html_img(candpath, alt)
     end
     # now try in the output dir just in case (provided we weren't already
@@ -157,7 +165,11 @@ function lx_figalt(lxc::LxCom, _)
     if splitdir(p1)[2] != "output"
         for ext ∈ candext
             candpath = joinpath(p1, "output", p2 * ext)
-            syspath  = joinpath(PATHS[:folder], split(candpath, '/')...)
+            if FD_ENV[:STRUCTURE] < v"0.2"
+                syspath  = joinpath(PATHS[:folder], split(candpath, '/')...)
+            else
+                syspath  = joinpath(PATHS[:site], split(candpath, '/')...)
+            end
             isfile(syspath) && return html_img(candpath, alt)
         end
     end
@@ -177,14 +189,22 @@ function lx_tableinput(lxc::LxCom, _)
     # there can be a relative path set but the user may mean
     # that it's in the subfolder /output/ (if generated by code) so should look
     # both in the relpath and if not found and if /output/ not already the last subdir
-    syspath = joinpath(PATHS[:folder], split(path, '/')...)
+    if FD_ENV[:STRUCTURE] < v"0.2"
+        syspath = joinpath(PATHS[:folder], split(path, '/')...)
+    else
+        syspath = joinpath(PATHS[:site], split(path, '/')...)
+    end
     isfile(syspath) && return csv2html(syspath, header)
     # now try in the output dir just in case (provided we weren't already
     # looking there)
     p1, p2 = splitdir(fdir)
     if splitdir(p1)[2] != "output"
         candpath = joinpath(p1, "output", p2 * fext)
-        syspath  = joinpath(PATHS[:folder], split(candpath, '/')...)
+        if FD_ENV[:STRUCTURE] < v"0.2"
+            syspath = joinpath(PATHS[:folder], split(candpath, '/')...)
+        else
+            syspath = joinpath(PATHS[:site], split(candpath, '/')...)
+        end
         isfile(syspath) && return csv2html(syspath, header)
     end
     return html_err("Table matching '$path' not found.")
diff --git a/src/eval/codeblock.jl b/src/eval/codeblock.jl
index a75c82f4..bd65c9b2 100644
--- a/src/eval/codeblock.jl
+++ b/src/eval/codeblock.jl
@@ -50,6 +50,12 @@ function should_eval(code::AS, rpath::AS)
     # 4. if the code has changed reeval
     cp = form_codepaths(rpath)
     # >> does the script exist?
+
+    # @show isfile(cp.script_path)
+    # @show MESSAGE_FILE_GEN_FMD * code == read(cp.script_path, String)
+    # @show isdir(cp.out_dir)
+    # @show all(isfile, (cp.out_path, cp.res_path))
+
     isfile(cp.script_path) || return true
     # >> does the script match the code?
     MESSAGE_FILE_GEN_FMD * code == read(cp.script_path, String) || return true
diff --git a/src/eval/io.jl b/src/eval/io.jl
index 73983178..61d0d5df 100644
--- a/src/eval/io.jl
+++ b/src/eval/io.jl
@@ -43,13 +43,13 @@ see [`resolve_rpath`](@ref).
 
 * `/[path]`:  this is a path relative to the website root folder, e.g.:
               `/foo/figs/img1.png`.
+* `[path]`:   this is a path relative to the site assets folder, e.g.:
+              `/assets/foo`.
 * `./[path]`: this is a path relative to the assets folder with the same path
-              as the calling page.
-              For instance, `./im1.png` in `src/pages/pg1.md` will point to
-              `/assets/pages/pg1/im1.png`
+              as the calling page. For instance, `./im1.png` in
+              `src/pages/pg1.md` will point to `/assets/pages/pg1/im1.png`.
 
-Otherwise, if in `code` mode, recurse with `./code/path`, otherwise consider
-the given path as a full path relative to the `/assets/` folder.
+Otherwise, if in `code` mode, recurse with `./code/path`.
 
 ## Argument
 
@@ -63,36 +63,74 @@ the given path as a full path relative to the `/assets/` folder.
                      additional shortcut form is allowed).
 """
 function parse_rpath(rpath::AS; canonical::Bool=false, code::Bool=false)::AS
+
+    FS2 = FD_ENV[:STRUCTURE] >= v"0.2"
+
     # path from the website root folder
+    # examples with FS2:
+    # /foo.bar          ==> { /foo.bar        ; [__site]/foo.bar        }
+    # /_assets/foo.bar  ==> { /assets/foo.bar ; [__site]/assets/foo.bar }
+    #
     if startswith(rpath, "/")
-        length(rpath) > 1 || throw(RelativePathError("Relative path `$rpath` doesn't look right."))
+        if length(rpath) <= 1
+            throw(RelativePathError("Relative path `$rpath` doesn't " *
+                                    "look right."))
+        end
+        if FS2
+            rpath = replace(rpath, "/_assets" => "/assets")
+        end
         canonical || return rpath
-        full_path = joinpath(PATHS[:folder], join_rpath(rpath[2:end]))
+        if FS2
+            full_path = joinpath(PATHS[:site], join_rpath(rpath[2:end]))
+        else
+            full_path = joinpath(PATHS[:folder], join_rpath(rpath[2:end]))
+        end
         return normpath(full_path)
+
     # path relative to the assets folder with the same path as the parent file
+    # example with FS2 assuming we're on page /page/
+    # ./foo.bar ==> { /assets/page/foo.bar  ; [__site]/assets/page/foo.bar}
     elseif startswith(rpath, "./")
-        length(rpath) > 2 || throw(RelativePathError("Relative path `$rpath` doesn't look right."))
-        if canonical
-            full_path = joinpath(PATHS[:assets],
-                                 splitext(locvar("fd_rpath"))[1],
-                                 join_rpath(rpath[3:end]))
-            return normpath(full_path)
-        else
+        if length(rpath) <= 2
+            throw(RelativePathError("Relative path `$rpath` doesn't " *
+                                    "look right."))
+        end
+        if !canonical
             # here we want to remain unix-style, so we don't use joinpath
             # note that rpath never starts with "/" so there's
             # no doubling of "//"
             rpath = unixify(splitext(locvar("fd_rpath"))[1]) * rpath[3:end]
             return "/assets/" * rpath
+        else
+            if FS2
+                full_path = joinpath(PATHS[:site], "assets",
+                                     splitext(locvar("fd_rpath"))[1],
+                                     join_rpath(rpath[3:end]))
+            else
+                full_path = joinpath(PATHS[:assets],
+                                     splitext(locvar("fd_rpath"))[1],
+                                     join_rpath(rpath[3:end]))
+            end
+            return normpath(full_path)
         end
     end
+
     # if the rpath didn't start with "/" or "./", check if it's not empty
     # and if we're in a code context
-    length(rpath) > 0 || throw(RelativePathError("Relative path `$rpath` (empty)  isn't right."))
-    code && return parse_rpath("./code/" * rpath; canonical=canonical)
+    if iszero(length(rpath))
+        throw(RelativePathError("Relative path `$rpath` (empty) isn't right."))
+    end
+    if code # recurse adding "./"
+        return parse_rpath("./code/" * rpath; canonical=canonical)
+    end
     # otherwise, we consider the path as a full path relative to the
     # /assets/ folder. For instance `blah/img1.png` => `/assets/blah/img1.png`
     canonical || return "/assets/" * rpath # no doubling bc rpath[1] != '/'
-    full_path = joinpath(PATHS[:assets], join_rpath(rpath))
+    if FS2
+        full_path = joinpath(PATHS[:site], "assets", join_rpath(rpath))
+    else
+        full_path = joinpath(PATHS[:assets], join_rpath(rpath))
+    end
     return normpath(full_path)
 end
 
@@ -106,13 +144,14 @@ given a relative path (in unix format) and return a tuple with:
 * the full system path to the directory containing the asset
 * the name of the asset without its extension
 
-If an extension is not provided, JuDoc will try to find something with an
+If an extension is not provided, Franklin will try to find something with an
 adequate extension or, failing that, will try to find anything that matches
 the file name.
 """
 function resolve_rpath(rpath::AS, lang::AS="")::NTuple{3,String}
     # parse the relative path
     fpath = parse_rpath(rpath; canonical=true, code=!isempty(lang))
+
     # check if an extension is given, if not, consider it's
     # `.xx` with language `nothing`
     fp, ext = splitext(fpath)
diff --git a/src/eval/literate.jl b/src/eval/literate.jl
index c63388ae..d0bdd796 100644
--- a/src/eval/literate.jl
+++ b/src/eval/literate.jl
@@ -33,8 +33,8 @@ $SIGNATURES
 Take a Literate.jl script and transform it into a Franklin-Markdown file.
 """
 function literate_to_franklin(rpath::AS)::Tuple{String,Bool}
-    startswith(rpath, "/") || throw(LiterateRelativePathError(
-                                "Literate expects a path starting with '/'"))
+    startswith(rpath, "/") || throw(
+        LiterateRelativePathError("Literate expects a path starting with '/'"))
     # rpath is of the form "/scripts/[path/]tutorial[.jl]"
     # split it so that when recombining it will lead to valid path inc windows
     srpath = split(rpath, '/')[2:end] # discard empty [1] since starts with "/"
@@ -45,7 +45,13 @@ function literate_to_franklin(rpath::AS)::Tuple{String,Bool}
         @warn "File not found when trying to convert a literate file ($fpath)."
         return "", true
     end
-    outpath = joinpath(PATHS[:assets], "literate", srpath[2:end-1]...)
+    if FD_ENV[:STRUCTURE] < v"0.2"
+        outpath = joinpath(PATHS[:assets], "literate",
+                            srpath[2:end-1]...)
+    else
+        outpath = joinpath(PATHS[:site], "assets", "literate",
+                            srpath[2:end-1]...)
+    end
     isdir(outpath) || mkpath(outpath)
     # retrieve the file name
     fname = splitext(splitdir(fpath)[2])[1]
diff --git a/src/manager/dir_utils.jl b/src/manager/dir_utils.jl
index 5c9c56f6..8c32322e 100644
--- a/src/manager/dir_utils.jl
+++ b/src/manager/dir_utils.jl
@@ -15,25 +15,57 @@ Prepare the output directory `PATHS[:pub]`.
                 start from a blank slate
 """
 function prepare_output_dir(clear::Bool=true)::Nothing
-    # if required to start from a blank slate -> remove the output dir
-    (clear & isdir(PATHS[:pub])) && rm(PATHS[:pub], recursive=true)
-    # create the output dir and the css dir if necessary
-    !isdir(PATHS[:pub]) && mkdir(PATHS[:pub])
-    !isdir(PATHS[:css]) && mkdir(PATHS[:css])
+    if FD_ENV[:STRUCTURE] < v"0.2"
+        # if required to start from a blank slate -> remove the output dir
+        (clear & isdir(PATHS[:pub])) && rm(PATHS[:pub], recursive=true)
+        # create the output dir and the css dir if necessary
+        !isdir(path(:pub)) && mkdir(path(:pub))
+        !isdir(path(:css)) && mkdir(path(:css))
+    else
+        (clear & isdir(path(:site))) && rm(path(:site), recursive=true)
+        !isdir(path(:site)) && mkdir(path(:site))
+    end
     return nothing
 end
 
-
 """
 $(SIGNATURES)
 
-Take a `root` path to an input file and convert to output path. If the output
-path does not exist, create it.
+Given a file path split in `(base, file)`, form the output path (where the
+output file will be written/copied).
 """
-function out_path(root::String)::String
+function form_output_path(base::AS, file::AS, case::Symbol)
+    # .md -> .html for md pages:
+    case == :md && (file = change_ext(file))
+    if FD_ENV[:STRUCTURE] < v"0.2"
+        outbase = _out_path(base)
+    else
+        outbase = _out_path2(base)
+        if case in (:md, :html)
+            # file is index.html --> keep the path
+            # file is page.html  --> .../page/index.html
+            fname = splitext(file)[1]
+            if fname != "index"
+                file = joinpath(fname, "index.html")
+            end
+        end
+    end
+    outpath = joinpath(outbase, file)
+    outdir  = splitdir(outpath)[1]
+    isdir(outdir) || mkpath(outdir)
+    return outpath
+end
+
+# NOTE: LEGACY way of getting the target path
+function _out_path(base::String)::String
+    if startswith(base, PATHS[:src_css])
+        f_out_path = replace(base, PATHS[:src_css] => PATHS[:css])
+        !ispath(f_out_path) && mkpath(f_out_path)
+        return f_out_path
+    end
     len_in = lastindex(joinpath(PATHS[:src], ""))
-    length(root) <= len_in && return PATHS[:folder]
-    dpath = root[nextind(root, len_in):end]
+    length(base) <= len_in && return PATHS[:folder]
+    dpath = base[nextind(base, len_in):end]
     # construct the out path
     f_out_path = joinpath(PATHS[:folder], dpath)
     f_out_path = replace(f_out_path, r"([^a-zA-Z\d\s_:])pages" => s"\1pub")
@@ -42,6 +74,26 @@ function out_path(root::String)::String
     return f_out_path
 end
 
+function _out_path2(base::String)::String
+    if startswith(base, path(:assets)) ||
+       startswith(base, path(:css))    ||
+       startswith(base, path(:layout)) ||
+       startswith(base, path(:libs))   ||
+       startswith(base, path(:literate))
+
+       # add a closing separator to folder path
+       rbase   = joinpath(path(:folder), "")
+       outpath = replace(base, Regex("$(rbase)_([a-z]+)") => s"\1")
+       outpath = joinpath(path(:site), outpath)
+   else
+       # path is not a 'special folder'
+       outpath = replace(base, path(:folder) => path(:site))
+   end
+    # if it doesn't exist, make the path
+    !ispath(outpath) && mkpath(outpath)
+    return outpath
+end
+
 
 """
 $(SIGNATURES)
@@ -49,9 +101,19 @@ $(SIGNATURES)
 Update the dictionaries referring to input files and their time of last change.
 The variable `verb` propagates verbosity.
 """
-function scan_input_dir!(md_files::TrackedFiles, html_files::TrackedFiles,
-                         other_files::TrackedFiles, infra_files::TrackedFiles,
-                         literate_files::TrackedFiles, verb::Bool=false)::Nothing
+function scan_input_dir!(args...; kw...)
+    if FD_ENV[:STRUCTURE] < v"0.2"
+        return _scan_input_dir!(args...)
+    end
+    return _scan_input_dir2!(args...; kw...)
+end
+
+function _scan_input_dir!(other_files::TrackedFiles,
+                          infra_files::TrackedFiles,
+                          md_files::TrackedFiles,
+                          html_files::TrackedFiles,
+                          literate_files::TrackedFiles,
+                          verb::Bool=false)::Nothing
     # top level files (src/*)
     for file ∈ readdir(PATHS[:src])
         isfile(joinpath(PATHS[:src], file)) || continue
@@ -109,15 +171,72 @@ function scan_input_dir!(md_files::TrackedFiles, html_files::TrackedFiles,
     return nothing
 end
 
+function _scan_input_dir2!(other_files::TrackedFiles,
+                           infra_files::TrackedFiles,
+                           md_pages::TrackedFiles,
+                           html_pages::TrackedFiles,
+                           literate_scripts::TrackedFiles,
+                           verb::Bool=false;
+                           in_loop::Bool=false)::Nothing
+    # go over all files in the website folder
+    for (root, _, files) ∈ walkdir(path(:folder))
+        for file in files
+            # assemble full path (root is an absolute path)
+            fpath = joinpath(root, file)
+            fpair = root => file
+            fext  = splitext(file)[2]
+
+            opts = (fpair, verb, in_loop)
+
+            # early skips
+            (!isfile(fpath) || file ∈ IGNORE_FILES) && continue
+            # skip over `__site` folder, `.git` and `.github` folder
+            startswith(fpath, path(:site)) && continue
+            startswith(fpath, joinpath(path(:folder), ".git")) && continue
+            # skip over toml files
+            fext == ".toml" && continue
+
+            # assets file --> other
+            if startswith(fpath, path(:assets))
+                add_if_new_file!(other_files, opts...)
+            # infra_files
+            elseif startswith(fpath, path(:css))    ||
+                   startswith(fpath, path(:layout)) ||
+                   startswith(fpath, path(:libs))
+                add_if_new_file!(infra_files, opts...)
+            # literate_files
+            elseif startswith(fpath, path(:literate))
+                # ignore files that are not script files
+                fext == ".jl" || continue
+                add_if_new_file!(literate_scripts, opts...)
+            else
+                if file == "config.md"
+                    add_if_new_file!(infra_files, opts...)
+                elseif fext == ".md"
+                    add_if_new_file!(md_pages, opts...)
+                elseif fext ∈ (".html", ".htm")
+                    add_if_new_file!(html_pages, opts...)
+                else
+                    add_if_new_file!(other_files, opts...)
+                end
+            end
+        end
+    end
+    return nothing
+end
+
 
 """
 Helper function, if `fpair` is not referenced in the dictionary (new file) add
 the entry to the dictionary with the time of last modification as val.
 """
-function add_if_new_file!(dict::TrackedFiles, fpair::Pair{String,String}, verb::Bool)::Nothing
+function add_if_new_file!(dict::TrackedFiles, fpair::Pair{String,String},
+                          verb::Bool, in_loop::Bool=false)::Nothing
     haskey(dict, fpair) && return nothing
     # it's a new file
     verb && println("tracking new file '$(fpair.second)'.")
-    dict[fpair] = mtime(joinpath(fpair...))
+    # save it's modification time, set to zero if it's a new file in a loop
+    # to force its processing in FS2
+    dict[fpair] = ifelse(in_loop, 0, mtime(joinpath(fpair...)))
     return nothing
 end
diff --git a/src/manager/extras.jl b/src/manager/extras.jl
index 953fcf2e..92a36620 100644
--- a/src/manager/extras.jl
+++ b/src/manager/extras.jl
@@ -9,12 +9,16 @@ function lunr()::Nothing
         (@warn "No `lunr` folder found in the `/libs/` folder."; return)
     # are there the relevant files in /libs/lunr/
     buildindex = joinpath(lunr, "build_index.js")
-    isfile(buildindex) ||
-        (@warn "No `build_index.js` file found in the `/libs/lunr/` folder."; return)
+    if !isfile(buildindex)
+		@warn "No `build_index.js` file found in the `/libs/lunr/` folder."
+		return nothing
+	end
     # overwrite PATH_PREPEND = "..";
     if !isempty(prepath)
         f = String(read(buildindex))
-        f = replace(f, r"const\s+PATH_PREPEND\s*?=\s*?\".*?\"\;" => "const PATH_PREPEND = \"$(prepath)\";"; count=1)
+        f = replace(f,
+					r"const\s+PATH_PREPEND\s*?=\s*?\".*?\"\;" =>
+					 "const PATH_PREPEND = \"$(prepath)\";"; count=1)
         buildindex = replace(buildindex, r".js$" => ".tmp.js")
         write(buildindex, f)
     end
diff --git a/src/manager/file_utils.jl b/src/manager/file_utils.jl
index 34454b0b..0206098b 100644
--- a/src/manager/file_utils.jl
+++ b/src/manager/file_utils.jl
@@ -4,14 +4,32 @@ $(SIGNATURES)
 Checks for a `config.md` file in `PATHS[:src]` and uses it to set the global
 variables referenced in `GLOBAL_VARS` it also sets the global latex commands
 via `GLOBAL_LXDEFS`. If the configuration file is not found a warning is shown.
+The keyword `init` is used internally to distinguish between the first call
+where only structural variables are considered (e.g. controlling folder
+structure).
 """
-function process_config()::Nothing
-    # read the config.md file if it is present
-    config_path = joinpath(PATHS[:src], "config.md")
-    if isfile(config_path)
-        convert_md(read(config_path, String); isconfig=true)
+function process_config(; init::Bool=false)::Nothing
+    if init
+        # initially the paths variable aren't set, try to find a config.md
+        # and read definitions in it; in particular folder_structure.
+        config_path_v1 = joinpath(FOLDER_PATH[], "src", "config.md")
+        config_path_v2 = joinpath(FOLDER_PATH[], "config.md")
+        if isfile(config_path_v2)
+            convert_md(read(config_path_v2, String); isconfig=true)
+        elseif isfile(config_path_v1)
+            convert_md(read(config_path_v1, String); isconfig=true)
+        else
+            @warn "I didn't find a config file. Ignoring."
+        end
     else
-        @warn "I didn't find a config file. Ignoring."
+        key = ifelse(FD_ENV[:STRUCTURE] < v"0.2", :src, :folder)
+        dir = path(key)
+        config_path = joinpath(dir, "config.md")
+        if isfile(config_path)
+            convert_md(read(config_path, String); isconfig=true)
+        else
+            @warn "I didn't find a config file. Ignoring."
+        end
     end
     return nothing
 end
@@ -25,9 +43,9 @@ the appropriate HTML page (inserting `head`, `pg_foot` and `foot`) and finally
 write it at the appropriate place.
 """
 function write_page(root::String, file::String, head::String,
-                    pg_foot::String, foot::String;
+                    pg_foot::String, foot::String, out_path::String;
                     prerender::Bool=false, isoptim::Bool=false,
-                    on_write::Function=(_, _) -> nothing)::Nothing
+                    on_write::Function=(_,_)->nothing)::Nothing
     # 1. read the markdown into string, convert it and extract definitions
     # 2. eval the definitions and update the variable dictionary, also retrieve
     # document variables (time of creation, time of last modif) and add those
@@ -36,7 +54,8 @@ function write_page(root::String, file::String, head::String,
     # The curpath is the relative path starting after /src/ so for instance:
     # f1/blah/page1.md or index.md etc... this is useful in the code evaluation and management
     # of paths
-    cur_rpath = fpath[lastindex(PATHS[:src])+length(PATH_SEP)+1:end]
+    root      = path(ifelse(FD_ENV[:STRUCTURE] < v"0.2", :src, :folder))
+    cur_rpath = fpath[lastindex(root)+length(PATH_SEP)+1:end]
     FD_ENV[:CUR_PATH] = cur_rpath
 
     content = convert_md(read(fpath, String))
@@ -85,8 +104,9 @@ function write_page(root::String, file::String, head::String,
     end
 
     # 5. write the html file where appropriate
-    write(joinpath(out_path(root), change_ext(file)), pg)
+    write(out_path, pg)
 
+    # 6. possible post-processing via the "on-write" function.
     on_write(pg, LOCAL_VARS)
     return nothing
 end
@@ -97,16 +117,17 @@ $(SIGNATURES)
 
 See [`process_file_err`](@ref).
 """
-function process_file(case::Symbol, fpair::Pair{String,String}, args...; kwargs...)::Int
+function process_file(case::Symbol, fpair::Pair{String,String}, args...;
+                      kwargs...)::Int
     try
         process_file_err(case, fpair, args...; kwargs...)
     catch err
         FD_ENV[:DEBUG_MODE] && throw(err)
         rp = fpair.first
         rp = rp[end-min(20, length(rp))+1 : end]
-        println("\n... encountered an issue processing '$(fpair.second)' in ...$rp.")
-        println("Verify, then start franklin again...\n")
-        FD_ENV[:SUPPRESS_ERR] || @show err
+        println("\n... encountered an issue processing '$(fpair.second)' " *
+                "in ...$rp. Verify, then start franklin again...\n")
+        FD_ENV[:SUPPRESS_ERR] || throw(err)
         return -1
     end
     return 0
@@ -122,31 +143,40 @@ processes it by converting it and adding appropriate header and footer and
 writes it to the appropriate place. It can throw an error which will be
 caught in `process_file(args...)`.
 """
-function process_file_err(case::Symbol, fpair::Pair{String, String}, head::AS="",
-                          pg_foot::AS="", foot::AS="", t::Float64=0.;
-                          clear::Bool=false, prerender::Bool=false, isoptim::Bool=false,
-                          on_write::Function=(_, _) -> nothing)::Nothing
+function process_file_err(
+            case::Symbol, fpair::Pair{String, String},
+            head::AS="", pgfoot::AS="", foot::AS="", t::Float64=0.;
+            clear::Bool=false, prerender::Bool=false, isoptim::Bool=false,
+            on_write::Function=(_,_)->nothing)::Nothing
+    # depending on the file extension, either full process (.md), partial
+    # process (.html) or no process (everything else)
+    inp  = joinpath(fpair...)
+    outp = form_output_path(fpair.first, fpair.second, case)
     if case == :md
-        write_page(fpair..., head, pg_foot, foot; prerender=prerender, isoptim=isoptim, on_write=on_write)
+        write_page(fpair..., head, pgfoot, foot, outp;
+                   prerender=prerender, isoptim=isoptim, on_write=on_write)
     elseif case == :html
-        fpath = joinpath(fpair...)
-        raw_html = read(fpath, String)
+        raw_html  = read(inp, String)
         proc_html = convert_html(raw_html; isoptim=isoptim)
-        write(joinpath(out_path(fpair.first), fpair.second), proc_html)
-    elseif case == :other
-        opath = joinpath(out_path(fpair.first), fpair.second)
-        # only copy it again if necessary (particularly relevant when the asset
-        # files take quite a bit of space.
-        if clear || !isfile(opath) || mtime(opath) < t
-            cp(joinpath(fpair...), opath, force=true)
+        write(outp, proc_html)
+    else # case in (:other, :infra)
+        if FD_ENV[:STRUCTURE] >= v"0.2"
+            # there's a bunch of thing we don't want to copy over
+            if startswith(inp, path(:layout))   ||
+                startswith(inp, path(:literate)) ||
+                endswith(inp, "config.md") ||
+                endswith(inp, "search.md")
+                # skip
+                @goto end_copyblock
+            end
         end
-    else # case == :infra
-        # copy over css files
-        # NOTE some processing may be further added here later on.
-        if splitext(fpair.second)[2] == ".css"
-            cp(joinpath(fpair...), joinpath(PATHS[:css], fpair.second),
-                force=true)
+        # NOTE: some processing may be further added here later on (e.g.
+        # parsing) of CSS files)
+        # only copy again if necessary (file is not there or has changed)
+        if !isfile(outp) || (mtime(outp) < t && !filecmp(inp, outp))
+            cp(inp, outp, force=true)
         end
+        @label end_copyblock
     end
     FD_ENV[:FULL_PASS] || FD_ENV[:SILENT_MODE] || rprint("→ page updated [✓]")
     return nothing
@@ -166,5 +196,5 @@ $(SIGNATURES)
 
 Convenience function to assemble the html out of its parts.
 """
-build_page(head::String, content::String, pg_foot::String, foot::String)::String =
-    "$head\n<div class=\"franklin-content\">\n$content\n$pg_foot\n</div>\n$foot"
+build_page(head::String, content::String, pgfoot::String, foot::String) =
+    "$head\n<div class=\"franklin-content\">\n$content\n$pgfoot\n</div>\n$foot"
diff --git a/src/manager/franklin.jl b/src/manager/franklin.jl
index 85532a79..390d7521 100644
--- a/src/manager/franklin.jl
+++ b/src/manager/franklin.jl
@@ -27,7 +27,7 @@ Keyword arguments:
 * `on_write(pg, fd_vars)`: callback function after the page is rendered, passing as arguments
                      the rendered page and the page variables
 """
-function serve(; clear::Bool=true,
+function serve(; clear::Bool=false,
                  verb::Bool=false,
                  port::Int=8000,
                  single::Bool=false,
@@ -40,17 +40,41 @@ function serve(; clear::Bool=true,
                  cleanup::Bool=true,
                  on_write::Function=(_, _) -> nothing)::Union{Nothing,Int}
     # set the global path
-    FOLDER_PATH[]  = pwd()
-
+    FOLDER_PATH[] = pwd()
     # silent mode?
     silent && (FD_ENV[:SILENT_MODE] = true; verb = false)
 
-    # brief check to see if we're in a folder that looks promising, otherwise stop
-    # and tell the user to check (#155)
-    if !isdir(joinpath(FOLDER_PATH[], "src"))
-        throw(ArgumentError("The current directory doesn't have a src/ " *
-                            "folder. Please change directory to a valid " *
-                            "Franklin folder."))
+    # in case of optim, there may be a prepath given which should be
+    # kept
+    prepath = get(GLOBAL_VARS, "prepath", nothing)
+    def_GLOBAL_VARS!()
+    isnothing(prepath) || set_var!(GLOBAL_VARS, "prepath", prepath.first)
+
+    # check if there's a config file, if there is, check the variable
+    # definitions looking at the ones that would affect overall structure etc.
+    process_config(init=true)
+    # in order to avoid the user incidentally causing troubles by redefining
+    # folder_structure along the way, we store it in FD_ENV.
+    fsv = GLOBAL_VARS["folder_structure"].first
+    FD_ENV[:STRUCTURE] = fsv
+
+    if FD_ENV[:STRUCTURE] < v"0.2"
+        # check to see if we're in a folder that has the right structure,
+        # otherwise stop and tell the user to check (#155)
+        if !isdir(joinpath(FOLDER_PATH[], "src"))
+            throw(ArgumentError(
+                "The current directory doesn't have a `src/` folder. " *
+                "Please change directory to a valid Franklin folder."))
+        end
+    else
+        if !all(isdir, (joinpath(FOLDER_PATH[], "_layout"),
+                        joinpath(FOLDER_PATH[], "_css")))
+            throw(ArgumentError(
+                "The current directory doens't  have a `_layout` or `_css` " *
+                "folder, if you are using the old folder structure, please " *
+                "add `@def folder_structure = v\"0.1\"` in your config.md; " *
+                "otherwise, change directory to a valid Franklin folder."))
+        end
     end
 
     # check if a Project.toml file is available, if so activate the folder
@@ -63,6 +87,7 @@ function serve(; clear::Bool=true,
     # construct the set of files to watch
     watched_files = fd_setup(clear=clear)
 
+    # set a verbosity var that we'll use in the rest of the function
     nomess && (verb = false)
 
     # do a first full pass
@@ -85,13 +110,15 @@ function serve(; clear::Bool=true,
                                             clear=clear, verb=verb,
                                             on_write=on_write)
         # start the liveserver in the current directory
+        live_server_dir = ifelse(FD_ENV[:STRUCTURE] < v"0.2", "", "__site")
         LiveServer.setverbose(verb)
-        LiveServer.serve(port=port, coreloopfun=coreloopfun)
+        LiveServer.serve(port=port, coreloopfun=coreloopfun,
+                         dir=live_server_dir)
     end
-    flag_env && rprint("→ Use Pkg.activate() to go back to your main environment.")
+    flag_env &&
+        rprint("→ Use Pkg.activate() to go back to your main environment.")
 
     cleanup && clear_dicts()
-
     return nothing
 end
 
@@ -118,20 +145,23 @@ function fd_setup(; clear::Bool=true)::NamedTuple
     # . recovering the list of files in the input dir we care about
     # -- these are stored in dictionaries, the key is the full path and the value is the time of
     # last change (useful for continuous monitoring)
-    md_files       = TrackedFiles()
-    html_files     = TrackedFiles()
-    other_files    = TrackedFiles()
-    infra_files    = TrackedFiles()
-    literate_files = TrackedFiles()
+    md_pages         = TrackedFiles()
+    html_pages       = TrackedFiles()
+    other_files      = TrackedFiles()
+    infra_files      = TrackedFiles()
+    literate_scripts = TrackedFiles()
     # named tuples of all the watched files
-    watched_files = (md=md_files, html=html_files, other=other_files,
-                     infra=infra_files, literate=literate_files)
+    # NOTE: with FS2 the ordering now matters, i.p. other should be first
+    watched_files = (other = other_files,
+                     infra = infra_files,
+                     md    = md_pages,
+                     html  = html_pages,
+                     literate = literate_scripts)
     # fill the dictionaries
     scan_input_dir!(watched_files...)
     return watched_files
 end
 
-
 """
 $(SIGNATURES)
 
@@ -153,10 +183,6 @@ function fd_fullpass(watched_files::NamedTuple; clear::Bool=false,
                      no_fail_prerender::Bool=true,
                      on_write::Function=(_, _) -> nothing)::Int
     FD_ENV[:FULL_PASS] = true
-    # initiate page segments
-    head    = read(joinpath(PATHS[:src_html], "head.html"), String)
-    pg_foot = read(joinpath(PATHS[:src_html], "page_foot.html"), String)
-    foot    = read(joinpath(PATHS[:src_html], "foot.html"), String)
 
     # reset global page variables and latex definitions
     # NOTE: need to keep track of pre-path if specified, see optimize
@@ -170,44 +196,43 @@ function fd_fullpass(watched_files::NamedTuple; clear::Bool=false,
     # process configuration file (see also `process_mddefs!`)
     process_config()
 
-    # looking for an index file to process
-    indexmd   = PATHS[:src] => "index.md"
-    indexhtml = PATHS[:src] => "index.html"
+    # form page segments
+    root_key   = ifelse(FD_ENV[:STRUCTURE] < v"0.2", :src, :folder)
+    layout_key = ifelse(FD_ENV[:STRUCTURE] < v"0.2", :src_html, :layout)
+    root       = path(root_key)
+    layout     = path(layout_key)
+
+    head    = read(joinpath(layout, "head.html"),      String)
+    pg_foot = read(joinpath(layout, "page_foot.html"), String)
+    foot    = read(joinpath(layout, "foot.html"),      String)
+
+    # look for an index file to process
+    hasindexmd   = isfile(joinpath(root, "index.md"))
+    hasindexhtml = isfile(joinpath(root, "index.html"))
 
-    # rest of the pages
+    if !hasindexmd && !hasindexhtml
+        @warn "No /index.md or /index.html found, there should be one. " *
+              "Ignoring..."
+    end
+
+    # go over all pages note that the html files are processed AFTER the
+    # markdown files and so if you both have an `index.md` and an `index.html`
+    # with otherwise the same path, it's the latter that will be considered.
     s = 0
-    begin
-        if isfile(joinpath(indexmd...))
-            a = process_file(:md, indexmd, head, pg_foot, foot; clear=clear,
-                             prerender=prerender, isoptim=isoptim, on_write=on_write)
-            if a < 0 && prerender && no_fail_prerender
-                process_file(:md, indexmd, head, pg_foot, foot; clear=clear,
-                             prerender=false, isoptim=isoptim, on_write=on_write)
-            end
-            s += a
-        elseif isfile(joinpath(indexhtml...))
-            a = process_file(:html, indexhtml, head, pg_foot, foot; clear=clear,
-                             prerender=prerender, isoptim=isoptim, on_write=on_write)
-            if a < 0 && prerender && no_fail_prerender
-                process_file(:html, indexhtml, head, pg_foot, foot; clear=clear,
-                             prerender=false, isoptim=isoptim, on_write=on_write)
-            end
-            s += a
-        else
-            @warn "I didn't find an index.[md|html], there should be one. Ignoring."
-        end
-        # process rest of the files
-        for (case, dict) ∈ pairs(watched_files), (fpair, t) ∈ dict
-            occursin("index.", fpair.second) && continue
-            a = process_file(case, fpair, head, pg_foot, foot, t; clear=clear,
-                             prerender=prerender, isoptim=isoptim, on_write=on_write)
-            if a < 0 && prerender && no_fail_prerender
-                process_file(case, fpair, head, pg_foot, foot, t; clear=clear,
-                             prerender=false, isoptim=isoptim, on_write=on_write)
-            end
-            s += a
+    for (case, dict) ∈ pairs(watched_files), (fpair, t) ∈ dict
+        a = process_file(case, fpair, head, pg_foot, foot, t;
+                         clear=clear, prerender=prerender,
+                         isoptim=isoptim, on_write=on_write)
+        # in case of failure of prerendering, if no_fail_prerender, we force
+        # prerender=false
+        if a < 0 && prerender && no_fail_prerender
+            process_file(case, fpair, head, pg_foot, foot, t;
+                         clear=clear, prerender=false,
+                         isoptim=isoptim, on_write=on_write)
         end
+        s += a
     end
+
     # generate RSS if appropriate
     globvar("generate_rss") && rss_generator()
     FD_ENV[:FULL_PASS] = false
@@ -231,8 +256,8 @@ function fd_loop(cycle_counter::Int, ::LiveServer.FileWatcher,
                  watched_files::NamedTuple;
                  clear::Bool=false, verb::Bool=false,
                  on_write::Function=(_, _) -> nothing)::Nothing
-    # every 30 cycles (3 seconds), scan directory to check for new or deleted files and
-    # update dicts accordingly
+    # every 30 cycles (3 seconds), scan directory to check for new or deleted
+    # files and update dicts accordingly
     if mod(cycle_counter, 30) == 0
         # 1) check if some files have been deleted; note that we don't do
         # anything, we just remove the file reference from the corresponding
@@ -242,8 +267,10 @@ function fd_loop(cycle_counter::Int, ::LiveServer.FileWatcher,
         end
         # 2) scan the input folder, if new files have been added then this will
         # update the dictionaries
-        scan_input_dir!(watched_files..., verb)
+        scan_input_dir!(watched_files..., verb; in_loop=true)
     else
+        layout_key = ifelse(FD_ENV[:STRUCTURE] < v"0.2", :src_html, :layout)
+        layout     = path(layout_key)
         # do a pass over the files, check if one has changed and if so trigger
         # the appropriate file processing mechanism
         for (case, dict) ∈ pairs(watched_files), (fpair, t) ∈ dict
@@ -256,50 +283,62 @@ function fd_loop(cycle_counter::Int, ::LiveServer.FileWatcher,
             fmsg = rpad("→ file $(fpath[length(FOLDER_PATH[])+1:end]) was modified ", 30)
             verb && print(fmsg)
             dict[fpair] = cur_t
-            # if it's an infra_file
+
+            # if it's an infra_file trigger a fullpass as potentially
+            # the whole website depends upon it (e.g. CSS)
             if haskey(watched_files[:infra], fpair)
                 verb && println("→ full pass...")
                 start = time()
-                fd_fullpass(watched_files; clear=false, verb=false, prerender=false, on_write=on_write)
-                verb && (print_final(rpad("✔ full pass...", 15), start); println(""))
-            # if it's a literate file
+                fd_fullpass(watched_files;
+                            clear=false, verb=false, prerender=false,
+                            on_write=on_write)
+                if verb
+                    print_final(rpad("✔ full pass...", 15), start)
+                    println("")
+                end
+
+            # if it's a literate_file, check that it's included on some page
+            # and if it is, then trigger build of that (or those) page(s)
             elseif haskey(watched_files[:literate], fpair)
                 fmsg = fmsg * rpad("→ updating... ", 15)
                 verb && print("\r" * fmsg)
                 start = time()
                 #
                 literate_path = splitext(unixify(joinpath(fpair...)))[1]
-                #
-                head    = read(joinpath(PATHS[:src_html], "head.html"), String)
-                pg_foot = read(joinpath(PATHS[:src_html], "page_foot.html"), String)
-                foot    = read(joinpath(PATHS[:src_html], "foot.html"), String)
-                # process all md files that have `\literate` with something that matches
-
+                # retrieve head, foot etc
+                head   = read(joinpath(layout, "head.html"),      String)
+                pgfoot = read(joinpath(layout, "page_foot.html"), String)
+                foot   = read(joinpath(layout, "foot.html"),      String)
+                # process all md files that have `\literate` with something
+                # that matches
                 for mdfpair in keys(watched_files.md)
-                    mdfpath = joinpath(mdfpair...)
                     # read the content and look for `{...}` that may refer
                     # to that script.
-                    content = read(mdfpath, String)
+                    content = read(joinpath(mdfpair...), String)
                     for m in eachmatch(r"\{(.*?)(\.jl)?\}", content)
                         if endswith(literate_path, m.captures[1])
-                            process_file(:md, mdfpair, head, pg_foot, foot, cur_t;
-                                         clear=false, prerender=false, on_write=on_write)
+                            process_file(:md, mdfpair, head, pgfoot, foot,
+                                         cur_t; clear=false, prerender=false,
+                                         on_write=on_write)
+                            # no need to look at further matches on that page
+                            # since we've already triggered the page build
                             break
                         end
                     end
                 end
                 verb && print_final(fmsg, start)
+
+            # for any other file, just call `process_file`
             else
                 fmsg = fmsg * rpad("→ updating... ", 15)
                 verb && print("\r" * fmsg)
                 start = time()
-                #
-                head    = read(joinpath(PATHS[:src_html], "head.html"), String)
-                pg_foot = read(joinpath(PATHS[:src_html], "page_foot.html"), String)
-                foot    = read(joinpath(PATHS[:src_html], "foot.html"), String)
-                # if it's the first time we modify this file then all blocks need
-                # to be evaluated so that everything is in scope
-                process_file(case, fpair, head, pg_foot, foot, cur_t;
+                # retrieve head, foot etc
+                head   = read(joinpath(layout, "head.html"),      String)
+                pgfoot = read(joinpath(layout, "page_foot.html"), String)
+                foot   = read(joinpath(layout, "foot.html"),      String)
+                # then process
+                process_file(case, fpair, head, pgfoot, foot, cur_t;
                              clear=false, prerender=false, on_write=on_write)
                 verb && print_final(fmsg, start)
             end
diff --git a/src/manager/post_processing.jl b/src/manager/post_processing.jl
index 90cc2bad..3ef1df69 100644
--- a/src/manager/post_processing.jl
+++ b/src/manager/post_processing.jl
@@ -3,20 +3,39 @@ Take a page (in HTML) and check all `href` on it to see if they lead somewhere.
 """
 function verify_links_page(path::AS, online::Bool)
     shortpath = replace(path, PATHS[:folder] => "")
-    mdpath    = replace(shortpath, Regex("^pub$(escape_string(PATH_SEP))")=>"pages$(PATH_SEP)")
-    mdpath    = splitext(mdpath)[1] * ".md"
-    shortpath = replace(shortpath, r"^\/"=>"")
-    mdpath    = replace(mdpath, r"^\/"=>"")
-    allok     = true
-    page      = read(path, String)
+    if FD_ENV[:STRUCTURE] < v"0.2"
+        mdpath    = replace(shortpath,
+                        Regex(joinpath("^pub", "")=>joinpath("pages", "")))
+        mdpath    = splitext(mdpath)[1] * ".md"
+        shortpath = replace(shortpath, r"^\/"=>"")
+        mdpath    = replace(mdpath, r"^\/"=>"")
+    else
+        mdpath = replace(path, PATHS[:site] => "")
+        mdpath = splitext(mdpath)[1] * ".md"
+        dir, fn = splitdir(mdpath) # fn will be index.md
+        if dir == "/"
+            mdpath = dir * "index.md"
+        else
+            mdpath = dir * "[" * PATH_SEP *  "index].md"
+        end
+    end
+    allok = true
+    page  = read(path, String)
     for m in eachmatch(r"\shref=\"(https?://)?(.*?)(?:#(.*?))?\"", page)
         if m.captures[1] === nothing
             # internal link, remove the front / otherwise it fails with joinpath
-            link   = replace(m.captures[2], r"^\/"=>"")
+            link = replace(m.captures[2], r"^\/"=>"")
             # if it's empty it's `href="/"` which is always ok
             isempty(link) && continue
             anchor = m.captures[3]
-            full_link = joinpath(PATHS[:folder], link)
+            if FD_ENV[:STRUCTURE] < v"0.2"
+                full_link = joinpath(PATHS[:folder], link)
+            else
+                full_link = joinpath(PATHS[:site], link)
+                if endswith(full_link, "/")
+                    full_link *= "index.html"
+                end
+            end
             if !isfile(full_link)
                 allok && println("")
                 println("- internal link issue on page $mdpath: $link.")
@@ -40,9 +59,12 @@ function verify_links_page(path::AS, online::Bool)
             online || continue
             # external link
             link = m.captures[1] * m.captures[2]
+            ok = false
             try
-                HTTP.request("HEAD", link).status == 200 || throw()
-            catch
+                ok = HTTP.request("HEAD", link, timeout=3).status == 200
+            catch e
+            end
+            if !ok
                 allok && println("")
                 println("- external link issue on page $mdpath: $link")
                 allok = false
@@ -54,14 +76,14 @@ end
 
 """Verify all links in generated HTML."""
 function verify_links()::Nothing
+    # if this is done before `serve` is called
+    if isempty(PATHS)
+        FOLDER_PATH[] = pwd()
+        set_paths!()
+    end
     # check that the user is online (otherwise only verify internal links)
-    online =
-        try
-            HTTP.request("HEAD", "https://discourse.julialang.org/", readtimeout=10).status == 200
-        catch
-            # might fail with DNSError
-            false
-        end
+    # this is fast as it uses `ping` and does not resolve a request
+    online = findfirst(check_ping, first.(IP_CHECK)) !== nothing
 
     print("Verifying links...")
     if online
@@ -70,15 +92,35 @@ function verify_links()::Nothing
         print(" [you don't seem online ✗]")
     end
 
-    # go over `index.html` then everything in `pub/`
-    overallok = verify_links_page(joinpath(PATHS[:folder], "index.html"), online)
+    if FD_ENV[:STRUCTURE] < v"0.2"
+        # go over `index.html` then everything in `pub/`
+        overallok = verify_links_page(joinpath(PATHS[:folder], "index.html"), online)
 
-    for (root, _, files) ∈ walkdir(PATHS[:pub])
-        for file ∈ files
-            splitext(file)[2] == ".html" && continue
-            path = joinpath(root, file)
-            allok = verify_links_page(path)
-            overallok = overallok && allok
+        for (root, _, files) ∈ walkdir(PATHS[:pub])
+            for file ∈ files
+                splitext(file)[2] == ".html" || continue
+                path = joinpath(root, file)
+                allok = verify_links_page(path, online)
+                overallok = overallok && allok
+            end
+        end
+    else
+        overallok = true
+        for (root, _, files) ∈ walkdir(PATHS[:folder])
+            for file ∈ files
+                splitext(file)[2] == ".html" || continue
+                fpath = joinpath(root, file)
+                if startswith(fpath, path(:assets)) ||
+                   startswith(fpath, path(:css))    ||
+                   startswith(fpath, path(:layout)) ||
+                   startswith(fpath, path(:libs))   ||
+                   startswith(fpath, path(:literate))
+                   startswith(fpath, joinpath(path(:folder), ".git"))
+                   continue
+                end
+                allok = verify_links_page(fpath, online)
+                overallok = overallok && allok
+            end
         end
     end
     overallok && println("\rAll internal $(ifelse(online,"and external ",""))links verified ✓.      ")
@@ -128,6 +170,7 @@ function optimize(; prerender::Bool=true, minify::Bool=true, sig::Bool=false,
     if !isempty(prepath)
         GLOBAL_VARS["prepath"] = prepath => (String,)
     end
+
     # re-do a (silent) full pass
     start = time()
     fmsg = "\r→ Full pass"
@@ -149,7 +192,12 @@ function optimize(; prerender::Bool=true, minify::Bool=true, sig::Bool=false,
             mmsg = rpad("→ Minifying *.[html|css] files...", 35)
             print(mmsg)
             # copy the script to the current dir
-            cp(joinpath(dirname(pathof(Franklin)), "scripts", "minify.py"), FD_PY_MIN_NAME; force=true)
+            path_to = joinpath(dirname(pathof(Franklin)),
+                                "scripts", "minify.py")
+            py_script = read(path_to, String)
+            oldfs     = ifelse(FD_ENV[:STRUCTURE] < v"0.2",  "True", "False")
+            py_script = "old_folder_structure = $oldfs\n" * py_script
+            write(FD_PY_MIN_NAME, py_script)
             # run it
             succ = success(`$([e for e in split(PY)]) $FD_PY_MIN_NAME`)
             # remove the script file
@@ -168,9 +216,10 @@ end
 """
 $(SIGNATURES)
 
-This is a simple wrapper doing a git commit and git push without much fanciness. It assumes the
-current directory is a git folder.
-It also fixes all links if you specify `prepath` (or if it's set in `config.md`).
+This is a simple wrapper doing a git commit and git push without much
+fanciness. It assumes the current directory is a git folder.
+It also fixes all links if you specify `prepath` (or if it's set in
+`config.md`).
 
 **Keyword arguments**
 
@@ -214,11 +263,13 @@ function publish(; prerender::Bool=true, minify::Bool=true, nopass::Bool=false,
             run(`git push --quiet`)
             print_final(pubmsg, start)
         catch e
-            println("✘ Could not push updates, verify your connection and try manually.\n")
+            println("✘ Could not push updates, verify your connection " *
+                    "and/or try manually.\n")
             @show e
         end
     else
-        println("✘ Something went wrong in the optimisation step. Not pushing updates.")
+        println("✘ Something went wrong in the optimisation step. " *
+                "Not pushing updates.")
     end
     return nothing
 end
diff --git a/src/manager/rss_generator.jl b/src/manager/rss_generator.jl
index 15fd12b2..04eda58b 100644
--- a/src/manager/rss_generator.jl
+++ b/src/manager/rss_generator.jl
@@ -30,17 +30,26 @@ end
 const RSS_DICT = LittleDict{String,RSSItem}()
 
 
-"""Convenience function for fallback fields"""
+"""
+    jor(a, b)
+
+Convenience function for fallback fields.
+"""
 jor(a::String, b::String) = ifelse(isempty(locvar(a)), locvar(b), locvar(a))
 
-"""Convenience function to remove <p> and </p> in RSS description (not supposed to happen)"""
+"""
+    remove_html_ps
+
+Convenience function to remove <p> and </p> in RSS description (not supposed to
+happen).
+"""
 remove_html_ps(s::String)::String = replace(s, r"</?p>" => "")
 
 """
 $SIGNATURES
 
-RSS should not contain relative links so this finds relative links and prepends them with the
-canonical link.
+RSS should not contain relative links so this finds relative links and prepends
+them with the canonical link.
 """
 fix_relative_links(s::String, link::String) =
     replace(s, r"(href|src)\s*?=\s*?\"\/" => SubstitutionString("\\1=\"$link"))
@@ -107,7 +116,11 @@ function rss_generator()::Nothing
     rss_descr = fd2html(rss_descr; internal=true) |> remove_html_ps
 
     # is there an RSS file already? if so remove it
-    rss_path = joinpath(PATHS[:folder], "feed.xml")
+    if FD_ENV[:STRUCTURE] < v"0.2"
+        rss_path = joinpath(PATHS[:folder], "feed.xml")
+    else
+        rss_path = joinpath(PATHS[:site], "feed.xml")
+    end
     isfile(rss_path) && rm(rss_path)
 
     # create a buffer which will correspond to the output
diff --git a/src/scripts/minify.py b/src/scripts/minify.py
index 72c43457..5626752d 100644
--- a/src/scripts/minify.py
+++ b/src/scripts/minify.py
@@ -8,21 +8,29 @@ from multiprocessing import Pool, cpu_count
 from functools import partial
 
 # modify those if you're not using the standard output paths.
-CSS, PUB = "css", "pub"
-
-html_files = ["index.html"]
-
-for root, dirs, files in os.walk(PUB):
-    for fname in files:
-        if fname.endswith(".html"):
-            html_files.append(os.path.join(root, fname))
-
-css_files = []
-
-for root, dirs, files in os.walk(CSS):
-    for fname in files:
-        if fname.endswith(".css"):
-            css_files.append(os.path.join(root, fname))
+if old_folder_structure:
+    CSS, PUB   = "css", "pub"
+    html_files = ["index.html"]
+    for root, dirs, files in os.walk(PUB):
+        for fname in files:
+            if fname.endswith(".html"):
+                html_files.append(os.path.join(root, fname))
+
+    css_files = []
+
+    for root, dirs, files in os.walk(CSS):
+        for fname in files:
+            if fname.endswith(".css"):
+                css_files.append(os.path.join(root, fname))
+else:
+    html_files = []
+    css_files  = []
+    for root, dirs, files in os.walk("__site"):
+        for fname in files:
+            if fname.endswith(".html"):
+                html_files.append(os.path.join(root, fname))
+            if fname.endswith(".css"):
+                css_files.append(os.path.join(root, fname))
 
 if os.name == 'nt':
     # multiprocessing doesn't seem to go well with windows...
diff --git a/src/utils/easyr.jl b/src/utils/easyr.jl
deleted file mode 100644
index ca3fa3f8..00000000
--- a/src/utils/easyr.jl
+++ /dev/null
@@ -1,35 +0,0 @@
-function rprint(s::AS)::Nothing
-    dwidth  = displaysize(stdout)[2]
-    lstring = lastindex(s)
-    lstring < dwidth && return rprint_under(s, dwidth)
-    return rprint_over(s, lstring, dwidth)
-end
-
-function rprint_under(s::AS, dw::Int)::Nothing
-    # print the string, padded to the limit
-    print(rpad("\r$s", dw))
-    return nothing
-end
-
-
-# - Make the (...) travel around so that get an idea of where we are
-# - corner case
-
-function rprint_over(s::AS, ls::Int, dw::Int)::Nothing
-    # unreasonable case
-    dw < 20 && return print("\r$s")
-    # standard case
-    fend = prevind(s, ls-7)
-    eos  = s[fend:end]
-
-    mos  = " (...) "
-
-    fbeg = prevind(s, dw - lastindex(fend) - lastindex(mos) - 8)
-    bos  = s[1:fbeg]
-
-    # construct
-    # bos (...) end
-    proxy_string = bos * " (...) " * eos * " "
-
-    return rprint_under(proxy_string, dw)
-end
diff --git a/src/utils/html.jl b/src/utils/html.jl
index 20ba722e..8e82c2a4 100644
--- a/src/utils/html.jl
+++ b/src/utils/html.jl
@@ -92,6 +92,13 @@ html_err(mess::String="") =
 
 """Helper function to get the relative url of the current page."""
 function url_curpage()
+    if FD_ENV[:STRUCTURE] < v"0.2"
+        return _url_curpage()
+    end
+    return _url_curpage2()
+end
+
+function _url_curpage()
     # go from /pages/.../something.md to /pub/.../something.html note that if
     # on windows then it would be \\ whence the PATH_SEP
     rp = replace(locvar("fd_rpath"),
@@ -102,6 +109,22 @@ function url_curpage()
     return rp
 end
 
+function _url_curpage2()
+    # get the relative path to current page and split extension (.md)
+    fn = splitext(locvar("fd_rpath"))[1]
+    # if it's not `index` then add `index`:
+    if splitdir(fn)[2] != "index"
+        fn = joinpath(fn, "index")
+    end
+    fn *= ".html"
+    # unixify
+    fn = unixify(fn)
+    # if it does not start with "/", add a "/" in front
+    startswith(fn, "/") || (fn = "/" * fn)
+    # append .html and return
+    return fn
+end
+
 # Copied from https://github.com/JuliaLang/julia/blob/acb7bd93fb2d5adbbabeaed9f39ab3c85495b02f/stdlib/Markdown/src/render/html.jl#L25-L31
 const _htmlescape_chars = LittleDict(
             '<' => "&lt;",
diff --git a/src/utils/misc.jl b/src/utils/misc.jl
index 1b040768..60f0ab4e 100644
--- a/src/utils/misc.jl
+++ b/src/utils/misc.jl
@@ -1,6 +1,4 @@
 """
-$SIGNATURES
-
 Specify the folder for the Literate scripts, by default this is `scripts/`.
 """
 function literate_folder(rp::String="")
@@ -16,7 +14,7 @@ end
 #
 
 """
-$(SIGNATURES)
+    str(s)
 
 Returns the string corresponding to `s`: `s` itself if it is a string, or the
 parent string if `s` is a substring. Do not confuse with `String(s::SubString)`
@@ -162,7 +160,7 @@ function refstring(s::AS)::String
 end
 
 """
-context(parent, position)
+    context(parent, position)
 
 Return an informative message of the context of a position and where the
 position is, this is useful when throwing error messages.
@@ -207,3 +205,58 @@ function context(par::AS, pos::Int)
     """
     return mess
 end
+
+"""
+    rprint(s)
+
+Print an overwriting line being aware of the display width.
+"""
+function rprint(s::AS)::Nothing
+    dwidth  = displaysize(stdout)[2]
+
+    padded_s = rpad(s, dwidth)
+    trunc_s  = padded_s[1:prevind(padded_s, min(dwidth, lastindex(padded_s)))]
+    padded_s = rpad(trunc_s, dwidth)
+
+    print("\r$padded_s")
+    return nothing
+end
+
+"""
+    filecmp(path1, path2)
+
+Takes 2 absolute paths and check if the files are different.
+This code was suggested by Steven J. Johnson on discourse:
+https://discourse.julialang.org/t/how-to-obtain-the-result-of-a-diff-between-2-files-in-a-loop/23784/4
+"""
+function filecmp(path1::AbstractString, path2::AbstractString)
+    stat1, stat2 = stat(path1), stat(path2)
+    if !(isfile(stat1) && isfile(stat2)) || filesize(stat1) != filesize(stat2)
+        return false
+    end
+    stat1 == stat2 && return true # same file
+    open(path1, "r") do file1
+        open(path2, "r") do file2
+            buf1 = Vector{UInt8}(undef, 32768)
+            buf2 = similar(buf1)
+            while !eof(file1) && !eof(file2)
+                n1 = readbytes!(file1, buf1)
+                n2 = readbytes!(file2, buf2)
+                n1 != n2 && return false
+                0 != Base._memcmp(buf1, buf2, n1) && return false
+            end
+            return eof(file1) == eof(file2)
+        end
+    end
+end
+
+"""
+    check_ping(ipaddr)
+
+Try a single ping to an address `ipaddr`. There's a timeout of 1s.
+"""
+function check_ping(ipaddr)
+    # portable count (creds https://docdave.science/writing-ping-in-julia/)
+    opt = ifelse(Sys.iswindows(), "-n", "-c")
+    return success(`ping $opt 1 -t 1 $ipaddr`)
+end
diff --git a/src/utils/paths.jl b/src/utils/paths.jl
index bf652255..8da86ff3 100644
--- a/src/utils/paths.jl
+++ b/src/utils/paths.jl
@@ -11,7 +11,7 @@ IGNORE_FILES
 
 Collection of file names that will be ignored at compile time.
 """
-const IGNORE_FILES = [".DS_Store"]
+const IGNORE_FILES = [".DS_Store", ".gitignore", "LICENSE.md", "README.md"]
 
 
 """
@@ -40,26 +40,44 @@ which is assigned at runtime.
 """
 function set_paths!()::LittleDict{Symbol,String}
     @assert isassigned(FOLDER_PATH) "FOLDER_PATH undefined"
-    @assert isdir(FOLDER_PATH[]) "FOLDER_PATH is not a valid path"
+    @assert isdir(FOLDER_PATH[])    "FOLDER_PATH is not a valid path"
 
     # NOTE it is recommended not to change the names of those paths.
     # Particularly for the output dir. If you do, check for example that
     # functions such as Franklin.publish point to the right dirs/files.
 
-    PATHS[:folder]    = normpath(FOLDER_PATH[])
-    PATHS[:src]       = joinpath(PATHS[:folder],  "src")
-    PATHS[:src_pages] = joinpath(PATHS[:src],     "pages")
-    PATHS[:src_css]   = joinpath(PATHS[:src],     "_css")
-    PATHS[:src_html]  = joinpath(PATHS[:src],     "_html_parts")
-    PATHS[:pub]       = joinpath(PATHS[:folder],  "pub")
-    PATHS[:css]       = joinpath(PATHS[:folder],  "css")
-    PATHS[:libs]      = joinpath(PATHS[:folder],  "libs")
-    PATHS[:assets]    = joinpath(PATHS[:folder],  "assets")
-    PATHS[:literate]  = joinpath(PATHS[:folder],  "scripts")
+    if FD_ENV[:STRUCTURE] < v"0.2"
+        PATHS[:folder]    = normpath(FOLDER_PATH[])
+        PATHS[:src]       = joinpath(PATHS[:folder], "src")
+        PATHS[:src_pages] = joinpath(PATHS[:src],    "pages")
+        PATHS[:src_css]   = joinpath(PATHS[:src],    "_css")
+        PATHS[:src_html]  = joinpath(PATHS[:src],    "_html_parts")
+        PATHS[:pub]       = joinpath(PATHS[:folder], "pub")
+        PATHS[:css]       = joinpath(PATHS[:folder], "css")
+        PATHS[:libs]      = joinpath(PATHS[:folder], "libs")
+        PATHS[:assets]    = joinpath(PATHS[:folder], "assets")
+        PATHS[:literate]  = joinpath(PATHS[:folder], "scripts")
+    else
+        PATHS[:folder]   = normpath(FOLDER_PATH[])
+        PATHS[:site]     = joinpath(PATHS[:folder], "__site")    # mandatory
+        PATHS[:assets]   = joinpath(PATHS[:folder], "_assets")   # mandatory
+        PATHS[:css]      = joinpath(PATHS[:folder], "_css")      # mandatory
+        PATHS[:layout]   = joinpath(PATHS[:folder], "_layout")   # mandatory
+        PATHS[:libs]     = joinpath(PATHS[:folder], "_libs")     # mandatory
+        PATHS[:literate] = joinpath(PATHS[:folder], "_literate") # optional
+    end
 
     return PATHS
 end
 
+"""
+    path(s)
+
+Return the paths corresponding to `s` e.g. `path(:folder)`.
+"""
+path(s) = PATHS[Symbol(s)]
+
+
 """
 Pointer to the `/output/` folder associated with an eval block (see also
 [`@OUTPUT`](@ref)).
diff --git a/src/utils/vars.jl b/src/utils/vars.jl
index adb94c30..3befcc77 100644
--- a/src/utils/vars.jl
+++ b/src/utils/vars.jl
@@ -9,15 +9,17 @@ for that value. (e.g.: "THE AUTHOR" => (String, Nothing))
 const GLOBAL_VARS = PageVars()
 
 const GLOBAL_VARS_DEFAULT = [
+    # Folder org
+    "folder_structure" => Pair(FD_ENV[:STRUCTURE], (VersionNumber,)),
     # General
-    "author"        => Pair("THE AUTHOR",   (String, Nothing)),
-    "date_format"   => Pair("U dd, yyyy",   (String,)),
-    "prepath"       => Pair("",             (String,)),
+    "author"           => Pair("THE AUTHOR",   (String, Nothing)),
+    "date_format"      => Pair("U dd, yyyy",   (String,)),
+    "prepath"          => Pair("",             (String,)),
     # RSS
-    "website_title" => Pair("",             (String,)),
-    "website_descr" => Pair("",             (String,)),
-    "website_url"   => Pair("",             (String,)),
-    "generate_rss"  => Pair(true,           (Bool,)),
+    "website_title"    => Pair("",             (String,)),
+    "website_descr"    => Pair("",             (String,)),
+    "website_url"      => Pair("",             (String,)),
+    "generate_rss"     => Pair(true,           (Bool,)),
     ]
 
 """


Test patch:
diff --git a/test/converter/html.jl b/test/converter/html.jl
index 887fb139..3fd5baff 100644
--- a/test/converter/html.jl
+++ b/test/converter/html.jl
@@ -27,6 +27,7 @@ end
 
 
 @testset "h-insert" begin
+    fs1()
     temp_rnd = joinpath(F.PATHS[:src_html], "temp.rnd")
     write(temp_rnd, "some random text to insert")
     hs = raw"""
@@ -38,6 +39,19 @@ end
     @test (@test_logs (:warn, "I found an {{insert ...}} block and tried to insert '$(joinpath(F.PATHS[:src_html], "nope.rnd"))' but I couldn't find the file. Ignoring.") F.convert_html(hs)) == "Trying to insert:  and see."
 end
 
+@testset "h-insert-fs2" begin
+    fs2()
+    temp_rnd = joinpath(F.PATHS[:layout], "temp.rnd")
+    write(temp_rnd, "some random text to insert")
+    hs = raw"""
+        Trying to insert: {{ insert temp.rnd }} and see.
+        """
+    @test F.convert_html(hs) == "Trying to insert: some random text to insert and see.\n"
+
+    hs = raw"""Trying to insert: {{ insert nope.rnd }} and see."""
+    @test (@test_logs (:warn, "I found an {{insert ...}} block and tried to insert '$(joinpath(F.PATHS[:layout], "nope.rnd"))' but I couldn't find the file. Ignoring.") F.convert_html(hs)) == "Trying to insert:  and see."
+end
+
 
 @testset "cond-insert" begin
     F.set_vars!(F.LOCAL_VARS, [
diff --git a/test/converter/lx_input.jl b/test/converter/lx_input.jl
index 65096bae..d242ccc6 100644
--- a/test/converter/lx_input.jl
+++ b/test/converter/lx_input.jl
@@ -1,5 +1,8 @@
+fs1()
+
 @testset "LX input" begin
     set_curpath("index.md")
+    mkpath(joinpath(F.PATHS[:assets], "index", "code", "output"))
     write(joinpath(F.PATHS[:assets], "index", "code", "s1.jl"), "println(1+1)")
     write(joinpath(F.PATHS[:assets], "index", "code", "output", "s1a.png"), "blah")
     write(joinpath(F.PATHS[:assets], "index", "code", "output", "s1.out"), "blih")
@@ -34,3 +37,44 @@ end
         """
     @test isapproxstr(st |> conv, "<p>Some string blah <strong>blih</strong></p>")
 end
+
+
+fs2()
+
+@testset "LX input" begin
+    set_curpath("index.md")
+    mkpath(joinpath(F.PATHS[:site], "assets", "index", "code", "output"))
+    write(joinpath(F.PATHS[:site], "assets", "index", "code", "s1.jl"), "println(1+1)")
+    write(joinpath(F.PATHS[:site], "assets", "index", "code", "output", "s1a.png"), "blah")
+    write(joinpath(F.PATHS[:site], "assets", "index", "code", "output", "s1.out"), "blih")
+    st = raw"""
+        Some string
+        \input{julia}{s1.jl}
+        Then maybe
+        \output{s1.jl}
+        Finally img:
+        \input{plot:a}{s1.jl}
+        done.
+        """;
+
+    F.def_GLOBAL_VARS!()
+    F.def_GLOBAL_LXDEFS!()
+
+    m = F.convert_md(st)
+    h = F.convert_html(m)
+
+    @test occursin("<p>Some string <pre><code class=\"language-julia\">$(read(joinpath(F.PATHS[:site], "assets", "index", "code", "s1.jl"), String))</code></pre>", h)
+    @test occursin("Then maybe <pre><code class=\"plaintext\">$(read(joinpath(F.PATHS[:site], "assets", "index", "code",  "output", "s1.out"), String))</code></pre>", h)
+    @test occursin("Finally img: <img src=\"/assets/index/code/output/s1a.png\" alt=\"\"> done.", h)
+end
+
+@testset  "Input MD" begin
+    mkpath(joinpath(F.PATHS[:site], "assets", "ccc"))
+    fp = joinpath(F.PATHS[:site], "assets", "ccc", "asset1.md")
+    write(fp, "blah **blih**")
+    st = raw"""
+        Some string
+        \textinput{ccc/asset1}
+        """
+    @test isapproxstr(st |> conv, "<p>Some string blah <strong>blih</strong></p>")
+end
diff --git a/test/converter/lx_simple.jl b/test/converter/lx_simple.jl
index 3431bc47..fdfbaa99 100644
--- a/test/converter/lx_simple.jl
+++ b/test/converter/lx_simple.jl
@@ -1,3 +1,5 @@
+fs1()
+
 @testset "figalt, fig" begin
     write(joinpath(F.PATHS[:assets], "testimg.png"), "png code")
     h = raw"""
diff --git a/test/converter/lx_simple_fs2.jl b/test/converter/lx_simple_fs2.jl
new file mode 100644
index 00000000..55388241
--- /dev/null
+++ b/test/converter/lx_simple_fs2.jl
@@ -0,0 +1,178 @@
+fs2()
+
+@testset "figalt, fig" begin
+    mkpath(joinpath(F.PATHS[:site], "assets"))
+    write(joinpath(F.PATHS[:site], "assets", "testimg.png"), "png code")
+    h = raw"""
+        A figure:
+        \figalt{fig 1}{/assets/testimg.png}
+        \fig{/assets/testimg.png}
+        \fig{/assets/testimg}
+        Done.
+        """ |> seval
+    @test isapproxstr(h, """
+            <p>A figure:
+            <img src=\"/assets/testimg.png\" alt=\"fig 1\">
+            <img src=\"/assets/testimg.png\" alt=\"\">
+            <img src=\"/assets/testimg.png\" alt=\"\">
+            Done.</p>
+            """)
+    p = mkpath(joinpath(F.PATHS[:site], "assets", "output"))
+    write(joinpath(p, "testimg_2.png"), "png code")
+    h = raw"""
+        Another figure:
+        \figalt{fig blah "hello!"}{/assets/testimg_2.png}
+        """ |> seval
+    @test isapproxstr(h, """
+            <p>Another figure:
+            <img src=\"/assets/output/testimg_2.png\" alt=\"fig blah $(Markdown.htmlesc("\"hello!\""))\">
+            </p>
+            """)
+    h = raw"""
+        No fig:
+        \fig{/assets/testimg_3.png}
+        """ |> seval
+    @test isapproxstr(h, """
+            <p>No fig: $(F.html_err("Image matching '/assets/testimg_3.png' not found."))</p>
+            """)
+end
+
+@testset "table" begin
+    #
+    # has header in source
+    #
+    testcsv = "h1,h2,h3\nstring1, 1.567, 0\n,,\n l i n e ,.158,99999999"
+    write(joinpath(F.PATHS[:site], "assets", "testcsv.csv"), testcsv)
+    # no header specified
+    h = raw"""
+        A table:
+        \tableinput{}{/assets/testcsv.csv}
+        Done.
+        """ |> seval
+
+    # NOTE: in VERSION > 1.4, Markdown has alignment for tables:
+    # -- https://github.com/JuliaLang/julia/pull/33849
+    if VERSION >= v"1.4.0-"
+        shouldbe = """
+            <p>A table:
+            <table>
+              <tr><th align="right">h1</th><th align="right">h2</th><th align="right">h3</th></tr>
+              <tr><td align="right">string1</td><td align="right">1.567</td><td align="right">0</td></tr>
+              <tr><td align="right"></td><td align="right"></td><td align="right"></td></tr>
+              <tr><td align="right">l i n e</td><td align="right">.158</td><td align="right">99999999</td></tr>
+            </table>
+            Done.</p>"""
+    else
+        shouldbe = """
+            <p>A table:
+            <table>
+              <tr><th>h1</th><th>h2</th><th>h3</th></tr>
+              <tr><td>string1</td><td>1.567</td><td>0</td></tr>
+              <tr><td></td><td></td><td></td></tr>
+              <tr><td>l i n e</td><td>.158</td><td>99999999</td></tr>
+            </table>
+            Done.</p>"""
+    end
+    @test isapproxstr(h, shouldbe)
+    # header specified
+    h = raw"""
+        A table:
+        \tableinput{A,B,C}{/assets/testcsv.csv}
+        Done.
+        """ |> seval
+    if VERSION >= v"1.4.0-"
+        shouldbe = """
+            <p>A table:
+            <table>
+              <tr><th align="right">A</th><th align="right">B</th><th align="right">C</th></tr>
+              <tr><td align="right">h1</td><td align="right">h2</td><td align="right">h3</td></tr>
+              <tr><td align="right">string1</td><td align="right">1.567</td><td align="right">0</td></tr>
+              <tr><td align="right"></td><td align="right"></td><td align="right"></td></tr>
+              <tr><td align="right">l i n e</td><td align="right">.158</td><td align="right">99999999</td></tr>
+            </table>
+            Done.</p>"""
+    else
+        shouldbe = """
+            <p>A table:
+            <table>
+              <tr><th>A</th><th>B</th><th>C</th></tr>
+              <tr><td>h1</td><td>h2</td><td>h3</td></tr>
+              <tr><td>string1</td><td>1.567</td><td>0</td></tr>
+              <tr><td></td><td></td><td></td></tr>
+              <tr><td>l i n e</td><td>.158</td><td>99999999</td></tr>
+            </table>
+            Done.</p>"""
+    end
+    @test isapproxstr(h, shouldbe)
+    # wrong header
+    h = raw"""
+        A table:
+        \tableinput{,}{/assets/testcsv.csv}
+        Done.
+        """ |> seval
+    shouldbe = """<p>A table: <p><span style=\"color:red;\">// In `\\tableinput`: header size (2) and number of columns (3) do not match. //</span></p>
+            Done.</p>"""
+    @test isapproxstr(h, shouldbe)
+
+    #
+    # does not have header in source
+    #
+
+    testcsv = "string1, 1.567, 0\n,,\n l i n e ,.158,99999999"
+    write(joinpath(F.PATHS[:site], "assets", "testcsv.csv"), testcsv)
+    # no header specified
+    h = raw"""
+        A table:
+        \tableinput{}{/assets/testcsv.csv}
+        Done.
+        """ |> seval
+    if VERSION >= v"1.4.0-"
+        shouldbe = """
+            <p>A table:
+            <table>
+              <tr><th align="right">string1</th><th align="right">1.567</th><th align="right">0</th></tr>
+              <tr><td align="right"></td><td align="right"></td><td align="right"></td></tr>
+              <tr><td align="right">l i n e</td><td align="right">.158</td><td align="right">99999999</td></tr>
+            </table>
+            Done.</p>"""
+    else
+        shouldbe = """
+            <p>A table:
+            <table>
+              <tr><th>string1</th><th>1.567</th><th>0</th></tr>
+              <tr><td></td><td></td><td></td></tr>
+              <tr><td>l i n e</td><td>.158</td><td>99999999</td></tr>
+            </table>
+            Done.</p>"""
+    end
+    @test isapproxstr(h, shouldbe)
+    # header specified
+    h = raw"""
+        A table:
+        \tableinput{A,B,C}{/assets/testcsv.csv}
+        Done.
+        """ |> seval
+
+    if VERSION >= v"1.4.0-"
+        shouldbe = """
+            <p>A table: <table><tr><th align="right">A</th><th align="right">B</th><th align="right">C</th></tr><tr><td align="right">string1</td><td align="right">1.567</td><td align="right">0</td></tr><tr><td align="right"></td><td align="right"></td><td align="right"></td></tr><tr><td align="right">l i n e</td><td align="right">.158</td><td align="right">99999999</td></tr></table>
+            Done.</p>"""
+    else
+        shouldbe = """<p>A table: <table><tr><th>A</th><th>B</th><th>C</th></tr>
+                <tr><td>string1</td><td>1.567</td><td>0</td></tr>
+                <tr><td></td><td></td><td></td></tr>
+                <tr><td>l i n e</td><td>.158</td><td>99999999</td></tr></table>
+                Done.</p>"""
+    end
+    @test isapproxstr(h, shouldbe)
+    # wrong header
+    h = raw"""
+        A table:
+        \tableinput{A,B}{/assets/testcsv.csv}
+        Done.
+        """ |> seval
+
+    shouldbe = """<p>A table: <p><span style=\"color:red;\">// In `\\tableinput`: header size (2) and number of columns (3) do not match. //</span></p>
+            Done.</p>"""
+    @test isapproxstr(h, shouldbe)
+end
diff --git a/test/converter/markdown2.jl b/test/converter/markdown2.jl
index 929117f9..0985d163 100644
--- a/test/converter/markdown2.jl
+++ b/test/converter/markdown2.jl
@@ -29,6 +29,7 @@ end
 
 
 @testset "TOC"  begin
+    fs1()
     h = raw"""
         @def fd_rpath = "pages/ff/aa.md"
         \toc
@@ -59,6 +60,7 @@ end
 end
 
 @testset "TOC"  begin
+    fs1()
     s = raw"""
         @def fd_rpath = "pages/ff/aa.md"
         @def mintoclevel = 2
@@ -95,3 +97,73 @@ end
         <h3 id="f"><a href="/pub/ff/aa.html#f">F</a></h3> done.
         """)
 end
+
+@testset "TOC-fs2"  begin
+    fs2()
+    h = raw"""
+        @def fd_rpath = "pages/ff/aa.md"
+        \toc
+        ## Hello `fd`
+        #### weirdly nested
+        ### Goodbye!
+        ## Done
+        done.
+        """ |> seval
+    @test isapproxstr(h, raw"""
+        <div class="franklin-toc">
+          <ol>
+            <li>
+              <a href="/pages/ff/aa/index.html#hello_fd">Hello <code>fd</code></a>
+              <ol>
+                <li><ol><li><a href="/pages/ff/aa/index.html#weirdly_nested">weirdly nested</a></li></ol></li>
+                <li><a href="/pages/ff/aa/index.html#goodbye">Goodbye&#33;</a></li>
+              </ol>
+            </li>
+            <li><a href="/pages/ff/aa/index.html#done">Done</a></li>
+          </ol>
+        </div>
+        <h2 id="hello_fd"><a href="/pages/ff/aa/index.html#hello_fd">Hello <code>fd</code></a></h2>
+        <h4 id="weirdly_nested"><a href="/pages/ff/aa/index.html#weirdly_nested">weirdly nested</a></h4>
+        <h3 id="goodbye"><a href="/pages/ff/aa/index.html#goodbye">Goodbye&#33;</a></h3>
+        <h2 id="done"><a href="/pages/ff/aa/index.html#done">Done</a></h2>done.
+        """)
+end
+
+@testset "TOC-fs2"  begin
+    fs2()
+    s = raw"""
+        @def fd_rpath = "pages/ff/aa.md"
+        @def mintoclevel = 2
+        @def maxtoclevel = 3
+        \toc
+        # A
+        ## B
+        #### C
+        ### D
+        ## E
+        ### F
+        done.
+        """ |> seval
+    @test isapproxstr(s, raw"""
+        <div class="franklin-toc">
+            <ol>
+                <li><a href="/pages/ff/aa/index.html#b">B</a>
+                    <ol>
+                        <li><a href="/pages/ff/aa/index.html#d">D</a></li>
+                    </ol>
+                </li>
+                <li><a href="/pages/ff/aa/index.html#e">E</a>
+                    <ol>
+                        <li><a href="/pages/ff/aa/index.html#f">F</a></li>
+                    </ol>
+                </li>
+            </ol>
+        </div>
+        <h1 id="a"><a href="/pages/ff/aa/index.html#a">A</a></h1>
+        <h2 id="b"><a href="/pages/ff/aa/index.html#b">B</a></h2>
+        <h4 id="c"><a href="/pages/ff/aa/index.html#c">C</a></h4>
+        <h3 id="d"><a href="/pages/ff/aa/index.html#d">D</a></h3>
+        <h2 id="e"><a href="/pages/ff/aa/index.html#e">E</a></h2>
+        <h3 id="f"><a href="/pages/ff/aa/index.html#f">F</a></h3> done.
+        """)
+end
diff --git a/test/coverage/extras1.jl b/test/coverage/extras1.jl
index 54dc3d0f..a9373bbe 100644
--- a/test/coverage/extras1.jl
+++ b/test/coverage/extras1.jl
@@ -29,7 +29,7 @@ end
 end
 
 @testset "Franklin" begin
-    cd(td); mkpath("foo"); cd("foo");
+    cd(td); mkpath("foo"); cd("foo"); write("config.md","")
     @test_throws ArgumentError serve(single=true)
     cd(td)
 end
diff --git a/test/eval/eval.jl b/test/eval/eval.jl
index 899393be..01f80ab0 100644
--- a/test/eval/eval.jl
+++ b/test/eval/eval.jl
@@ -1,3 +1,5 @@
+fs1()
+
 set_curpath("index.md")
 
 @testset "Evalcode" begin
diff --git a/test/eval/eval_fs2.jl b/test/eval/eval_fs2.jl
new file mode 100644
index 00000000..0a759edc
--- /dev/null
+++ b/test/eval/eval_fs2.jl
@@ -0,0 +1,249 @@
+fs2()
+
+set_curpath("index.md")
+
+@testset "Evalcode" begin
+    h = raw"""
+        Simple code:
+        ```julia:./code/exca1
+        a = 5
+        print(a^2)
+        ```
+        then:
+        \output{./code/exca1}
+        done.
+        """ |> seval
+
+    h2 = raw"""
+        Simple code:
+        ```julia:excb1
+        a = 5
+        print(a^2)
+        ```
+        then:
+        \output{excb1}
+        done.
+        """ |> seval
+
+    @test h == h2
+
+    spatha = joinpath(F.PATHS[:site], "assets", "index", "code", "exca1.jl")
+    spathb = joinpath(F.PATHS[:site], "assets", "index", "code", "excb1.jl")
+    @test isfile(spatha)
+    @test isfile(spathb)
+    @test isapproxstr(read(spatha, String), """
+        $(F.MESSAGE_FILE_GEN_FMD)
+        a = 5\nprint(a^2)""")
+
+    opath = joinpath(F.PATHS[:site], "assets", "index", "code", "output", "exca1.out")
+    @test isfile(opath)
+    @test read(opath, String) == "25"
+
+    @test isapproxstr(h, raw"""
+            <p>Simple code:
+            <pre><code class="language-julia">a = 5
+            print(a^2)</code></pre>
+            then:
+            <pre><code class="plaintext">25</code></pre>
+            done.</p>""")
+end
+
+@testset "Eval (errs)" begin
+    s = raw"""
+        Simple code:
+        ```python:./scripts/testpy
+        a = 5
+        print(a**2)
+        ```
+        done.
+        """
+    h = ""
+    @test_logs (:warn, "Evaluation of non-Julia code blocks is not yet supported.") (h = s |> seval)
+
+    @test isapproxstr(h, raw"""
+            <p>Simple code:
+            <pre><code class="language-python">a = 5
+            print(a**2)</code></pre>
+            done.</p>""")
+end
+
+@testset "Eval (rinput)" begin
+    h = raw"""
+        Simple code:
+        ```julia:/assets/scripts/test2
+        a = 5
+        print(a^2)
+        ```
+        then:
+        \output{/assets/scripts/test2}
+        done.
+        """ |> seval
+
+    spath = joinpath(F.PATHS[:site], "assets", "scripts", "test2.jl")
+    @test isfile(spath)
+    @test occursin("a = 5\nprint(a^2)", read(spath, String))
+
+    opath = joinpath(F.PATHS[:site], "assets", "scripts", "output", "test2.out")
+    @test isfile(opath)
+    @test read(opath, String) == "25"
+
+    @test isapproxstr(h, """
+            <p>Simple code:
+            <pre><code class="language-julia">a = 5
+            print(a^2)</code></pre>
+            then:
+            <pre><code class="plaintext">25</code></pre>
+            done.</p>""")
+
+    # ------------
+
+    h = raw"""
+        @def fd_rpath = "pages/pg1.md"
+        Simple code:
+        ```julia:./code/abc2
+        a = 5
+        print(a^2)
+        ```
+        then:
+        \output{./code/abc2}
+        done.
+        """ |> seval
+
+    spath = joinpath(F.PATHS[:site], "assets", "pages", "pg1", "code", "abc2.jl")
+    @test isfile(spath)
+    @test occursin("a = 5\nprint(a^2)", read(spath, String))
+
+    opath = joinpath(F.PATHS[:site], "assets", "pages", "pg1", "code", "output" ,"abc2.out")
+    @test isfile(opath)
+    @test read(opath, String) == "25"
+
+    @test isapproxstr(h, """
+            <p>Simple code:
+            <pre><code class="language-julia">a = 5
+            print(a^2)</code></pre>
+            then:
+            <pre><code class="plaintext">25</code></pre>  done.</p>""")
+end
+
+@testset "Eval (module)" begin
+    h = raw"""
+        Simple code:
+        ```julia:scripts/test1
+        using LinearAlgebra
+        a = [5, 2, 3, 4]
+        print(dot(a, a))
+        ```
+        then:
+        \output{scripts/test1}
+        done.
+        """ |> seval
+    # dot(a, a) == 54
+    @test occursin("""then: <pre><code class="plaintext">54</code></pre> done.""", h)
+end
+
+@testset "Eval (img)" begin
+    h = raw"""
+        @def reeval=true
+        Simple code:
+        ```julia:tv2
+        #hideall
+        write(joinpath(@OUTPUT, "tv2.png"), "blah")
+        ```
+        then:
+        \input{plot}{tv2}
+        done.
+        """ |> seval
+    @test occursin("then: <img src=\"/assets/index/code/output/tv2.png\" alt=\"\"> done.", h)
+end
+
+@testset "Eval (throw)" begin
+    s = raw"""
+        Simple code:
+        ```julia:scripts/test1
+        sqrt(-1)
+        ```
+        then:
+        \output{scripts/test1}
+        done.
+        """
+    h = ""
+    @test_logs (:warn, "There was an error of type DomainError running the code.") (h = s |> seval)
+    # errors silently
+    @test occursin("then: <pre><code class=\"plaintext\">DomainError(-1.0, \"sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).\"", h)
+end
+
+@testset "Eval (nojl)" begin
+    h = raw"""
+        Simple code:
+        ```python:scripts/test1
+        sqrt(-1)
+        ```
+        done.
+        """
+
+    @test (@test_logs (:warn, "Evaluation of non-Julia code blocks is not yet supported.") h |> seval) == "<p>Simple code: <pre><code class=\"language-python\">sqrt(-1)</code></pre> done.</p>\n"
+end
+
+# temporary fix for 186: make error appear and also use `abspath` in internal include
+@testset "eval #186" begin
+    h = raw"""
+        Simple code:
+        ```julia:scripts/test186
+        fn = "tempf.jl"
+        write(fn, "a = 1+1")
+        println("Is this a file? $(isfile(fn))")
+        include(abspath(fn))
+        println("Now: $a")
+        rm(fn)
+        ```
+        done.
+        \output{scripts/test186}
+        """ |> seval
+    @test isapproxstr(h, raw"""
+            <p>Simple code: <pre><code class="language-julia">fn = "tempf.jl"
+            write(fn, "a = 1+1")
+            println("Is this a file? $(isfile(fn))")
+            include(abspath(fn))
+            println("Now: $a")
+            rm(fn)</code></pre> done. <pre><code class="plaintext">Is this a file? true
+            Now: 2
+            </code></pre></p>
+            """)
+end
+
+
+@testset "show" begin
+    h = raw"""
+        @def hascode = true
+        @def reeval = true
+        ```julia:ex
+        a = 5
+        a *= 2
+        ```
+        \show{ex}
+        """ |> fd2html_td
+    @test isapproxstr(h, """
+        <pre><code class="language-julia">a = 5
+        a *= 2</code></pre>
+        <pre><code class="plaintext">10</code></pre>
+        """)
+
+    # Show with stdout
+    h = raw"""
+        @def hascode = true
+        @def reeval = true
+        ```julia:ex
+        a = 5
+        println("hello")
+        a *= 2
+        ```
+        \show{ex}
+        """ |> fd2html_td
+    @test isapproxstr(h, """
+        <pre><code class="language-julia">a = 5
+        println("hello")
+        a *= 2</code></pre>
+        <pre><code class="plaintext">hello
+        10</code></pre>
+        """)
+end
diff --git a/test/eval/io.jl b/test/eval/io.jl
index 0128a318..1efeae80 100644
--- a/test/eval/io.jl
+++ b/test/eval/io.jl
@@ -1,3 +1,5 @@
+fs1()
+
 @testset "unixify" begin
     @test F.unixify("")         == "/"
     @test F.unixify("blah.txt") == "blah.txt"
diff --git a/test/eval/io_fs2.jl b/test/eval/io_fs2.jl
new file mode 100644
index 00000000..eb0557d1
--- /dev/null
+++ b/test/eval/io_fs2.jl
@@ -0,0 +1,80 @@
+fs2()
+
+@testset "unixify" begin
+    @test F.unixify("")         == "/"
+    @test F.unixify("blah.txt") == "blah.txt"
+    @test F.unixify("blah/")    == "blah/"
+    @test F.unixify("foo/bar")  == "foo/bar/"
+end
+
+@testset "join_rpath" begin
+    @test F.join_rpath("blah/blah") == joinpath("blah", "blah")
+end
+
+@testset "parse_rpath" begin
+    F.PATHS[:folder] = "fld"
+    # /[path]
+    @test_throws F.RelativePathError F.parse_rpath("/")
+    @test F.parse_rpath("/a") == "/a"
+    @test F.parse_rpath("/a/b") == "/a/b"
+    @test F.parse_rpath("/a/b", canonical=true) == joinpath(F.PATHS[:site], "a", "b")
+    @test F.parse_rpath("/a/../b", canonical=true) == joinpath(F.PATHS[:site], "b")
+
+    # ./[path]
+    set_curpath("pg1.md")
+    @test_throws F.RelativePathError F.parse_rpath("./")
+    @test F.parse_rpath("./a") == "/assets/pg1/a"
+    @test F.parse_rpath("./a/b") == "/assets/pg1/a/b"
+    @test F.parse_rpath("./a/b", canonical=true) == joinpath(F.PATHS[:site], "assets", "pg1", "a", "b")
+
+    # [path]
+    @test_throws F.RelativePathError F.parse_rpath("")
+    @test F.parse_rpath("blah") == "/assets/blah"
+    @test F.parse_rpath("blah", code=true) == "/assets/pg1/code/blah"
+    @test F.parse_rpath("blah", canonical=true) == joinpath(F.PATHS[:site], "assets", "blah")
+end
+
+@testset "resolve_rpath" begin
+    ass = F.PATHS[:assets]
+    write(joinpath(ass, "p1.jl"), "a = 5")
+    write(joinpath(ass, "p2.png"), "gibberish")
+    assout = joinpath(F.PATHS[:site], "assets")
+    isdir(assout) && rm(assout, recursive=true)
+    mkpath(assout)
+    cp(joinpath(ass, "p1.jl"), joinpath(assout, "p1.jl"), force=true)
+    cp(joinpath(ass, "p1.jl"), joinpath(assout, "p2.png"), force=true)
+
+    fp, d, fn = F.resolve_rpath("/assets/p1", "julia")
+    @test fp == joinpath(assout, "p1.jl")
+    @test d == assout
+    @test fn == "p1"
+
+    fp, d, fn = F.resolve_rpath("/assets/p2")
+    @test fp == joinpath(assout, "p2.png")
+    @test d == assout
+    @test fn == "p2"
+
+    @test_throws F.FileNotFoundError F.resolve_rpath("/assets/foo")
+    @test_throws F.FileNotFoundError F.resolve_rpath("foo")
+    @test_throws F.FileNotFoundError F.resolve_rpath("foo", "julia")
+end
+
+@testset "form_cpaths" begin
+    ass = F.PATHS[:assets]
+    assout = joinpath(F.PATHS[:site], "assets")
+    isdir(assout) && rm(assout, recursive=true)
+    mkpath(assout)
+
+    curpath = set_curpath(joinpath("pages", "pg1.md"))
+
+    write(joinpath(ass, "p1.jl"), "a = 5")
+    write(joinpath(ass, "p2.png"), "gibberish")
+
+    sp, sd, sn, od, op, rp = F.form_codepaths("p1")
+    @test sp == joinpath(assout, splitext(curpath)[1], "code", "p1.jl")
+    @test sd == joinpath(assout, splitext(curpath)[1], "code")
+    @test sn == "p1.jl"
+    @test od == joinpath(assout, splitext(curpath)[1], "code", "output")
+    @test op == joinpath(od, "p1.out")
+    @test rp == joinpath(od, "p1.res")
+end
diff --git a/test/global/cases2.jl b/test/global/cases2.jl
index 7d39acf6..e2e8110a 100644
--- a/test/global/cases2.jl
+++ b/test/global/cases2.jl
@@ -61,6 +61,7 @@ end
 
 
 @testset "Table" begin
+    fs1()
     st = """
         @def fd_rpath = "pages/pg1.html"
         A
@@ -105,6 +106,52 @@ end
     end
 end
 
+@testset "Table-fs2" begin
+    fs2()
+    st = """
+        @def fd_rpath = "pages/pg1.html"
+        A
+
+        ### Title
+
+        No. | Graph | Vertices | Edges
+        :---: | :---------: | :------------: | :-----------------:
+        1 | Twitter Social Circles | 81,306 | 1,342,310
+        2 | Astro-Physics Collaboration | 17,903 | 197,031
+        3 | Facebook Social Circles | 4,039 | 88,234
+
+        C
+        """
+
+    if VERSION >= v"1.4.0-"
+        @test isapproxstr(st |> seval, raw"""<p>A</p>
+<h3 id="title"><a href="/pages/pg1/index.html#title">Title</a></h3>
+<table><tr><th align="center">No.</th><th align="center">Graph</th><th align="center">Vertices</th><th align="center">Edges</th></tr><tr><td align="center">1</td><td align="center">Twitter Social Circles</td><td align="center">81,306</td><td align="center">1,342,310</td></tr><tr><td align="center">2</td><td align="center">Astro-Physics Collaboration</td><td align="center">17,903</td><td align="center">197,031</td></tr><tr><td align="center">3</td><td align="center">Facebook Social Circles</td><td align="center">4,039</td><td align="center">88,234</td></tr></table>
+<p>C</p>""")
+    else
+        @test isapproxstr(st |> seval, raw""" <p>A</p>
+            <h3 id="title"><a href="/pages/pg1/index.html#title">Title</a></h3>
+
+            <table>
+              <tr>
+                <th>No.</th><th>Graph</th><th>Vertices</th><th>Edges</th>
+              </tr>
+              <tr>
+                <td>1</td><td>Twitter Social Circles</td><td>81,306</td><td>1,342,310</td>
+              </tr>
+              <tr>
+                <td>2</td><td>Astro-Physics Collaboration</td><td>17,903</td><td>197,031</td>
+              </tr>
+              <tr>
+                <td>3</td><td>Facebook Social Circles</td><td>4,039</td><td>88,234</td>
+              </tr>
+            </table>
+
+            <p>C</p>
+            """)
+    end
+end
+
 @testset "Auto title" begin
     # if no title is set, then the first header is used
     s = raw"""
diff --git a/test/global/postprocess.jl b/test/global/postprocess.jl
index b51236c2..9c8fa42c 100644
--- a/test/global/postprocess.jl
+++ b/test/global/postprocess.jl
@@ -15,19 +15,21 @@
     @test silly_call > 0
 
     # ---------------
-    @test all(isdir, ("assets", "css", "libs", "pub", "src"))
-    @test all(isfile, ("index.html",
-                 map(e->joinpath("pub", "menu$e.html"), 1:3)...,
-                 map(e->joinpath("css", e), ("basic.css", "franklin.css"))...,
-                 )
-              )
+    @test all(isdir, ("_assets", "_css", "_libs", "_layout", "__site"))
+    @test isfile(joinpath("__site", "index.html"))
+    @test isfile(joinpath("__site", "menu1", "index.html"))
+    @test isfile(joinpath("__site", "menu2", "index.html"))
+    @test isfile(joinpath("__site", "menu3", "index.html"))
+    @test isfile(joinpath("__site", "css", "basic.css"))
+    @test isfile(joinpath("__site", "css", "franklin.css"))
+
     # ---------------
     if Franklin.FD_CAN_MINIFY
-        presize1 = stat(joinpath("css", "basic.css")).size
-        presize2 = stat("index.html").size
+        presize1 = stat(joinpath("__site", "css", "basic.css")).size
+        presize2 = stat(joinpath("__site", "index.html")).size
         optimize(prerender=false)
-        @test stat(joinpath("css", "basic.css")).size < presize1
-        @test stat("index.html").size < presize2
+        @test stat(joinpath("__site", "css", "basic.css")).size < presize1
+        @test stat(joinpath("__site", "index.html")).size < presize2
     end
     # ---------------
     # verify all links
@@ -35,14 +37,14 @@
 
     # ---------------
     # change the prepath
-    index = read("index.html", String)
+    index = read(joinpath("__site","index.html"), String)
     @test occursin("=\"/css/basic.css", index)
     @test occursin("=\"/css/franklin.css", index)
     @test occursin("=\"/libs/highlight/github.min.css", index)
     @test occursin("=\"/libs/katex/katex.min.css", index)
 
     optimize(minify=false, prerender=false, prepath="prependme")
-    index = read("index.html", String)
+    index = read(joinpath("__site","index.html"), String)
     @test occursin("=\"/prependme/css/basic.css", index)
     @test occursin("=\"/prependme/css/franklin.css", index)
     @test occursin("=\"/prependme/libs/highlight/github.min.css", index)
diff --git a/test/global/rss.jl b/test/global/rss.jl
index facd742a..06632418 100644
--- a/test/global/rss.jl
+++ b/test/global/rss.jl
@@ -1,6 +1,6 @@
 # simple test that things get created
 @testset "RSS gen" begin
-    f = joinpath(p, "basic", "feed.xml")
+    f = joinpath(p, "basic", "__site", "feed.xml")
     @test isfile(f)
     fc = prod(readlines(f, keep=true))
 
@@ -9,6 +9,6 @@
     @test occursin(raw"""<description><![CDATA[Example website using Franklin
   ]]></description>""", fc)
     @test !occursin(raw"""<author>""", fc)
-    @test occursin(raw"""<link>https://tlienart.github.io/FranklinTemplates.jl/pub/menu1.html</link>""", fc)
+    @test occursin(raw"""<link>https://tlienart.github.io/FranklinTemplates.jl/menu1/index.html</link>""", fc)
     @test occursin(raw"""<description><![CDATA[A short description of the page which would serve as <strong>blurb</strong> in a <code>RSS</code> feed;""", fc)
 end
diff --git a/test/integration/literate.jl b/test/integration/literate.jl
index e634d254..e6390edb 100644
--- a/test/integration/literate.jl
+++ b/test/integration/literate.jl
@@ -1,11 +1,11 @@
-scripts = joinpath(F.PATHS[:folder], "literate-scripts")
-flush_td()
-cd(td); F.set_paths!(); mkpath(scripts)
+fs2()
+
+mkpath(F.path(:literate))
 
 @testset "Literate-0" begin
     @test_throws ErrorException literate_folder("foo/")
-    litpath = literate_folder("literate-scripts/")
-    @test litpath == joinpath(F.PATHS[:folder], "literate-scripts/")
+    litpath = literate_folder("_literate/")
+    @test litpath == literate_folder(F.path(:literate))
 end
 
 @testset "Literate-a" begin
@@ -40,7 +40,6 @@ end
 end
 
 @testset "Literate-b" begin
-    flush_td(); cd(td); F.set_paths!(); mkpath(scripts)
     # Literate to Franklin
     s = raw"""
         # # Rational numbers
@@ -56,10 +55,10 @@ end
 
         z = x + y
         """
-    path = joinpath(scripts, "tutorial.jl")
+    path = joinpath(F.path(:literate), "tutorial.jl")
     write(path, s)
-    opath, = F.literate_to_franklin("/literate-scripts/tutorial")
-    @test endswith(opath, joinpath(F.PATHS[:assets], "literate", "tutorial.md"))
+    opath, = F.literate_to_franklin("/_literate/tutorial")
+    @test endswith(opath, joinpath(F.PATHS[:site], "assets", "literate", "tutorial.md"))
     out = read(opath, String)
     @test out == """
         <!--This file was generated, do not modify it.-->
@@ -88,7 +87,7 @@ end
         @def showall = true
         @def reeval = true
 
-        \literate{/literate-scripts/tutorial.jl}
+        \literate{/_literate/tutorial.jl}
         """ |> fd2html_td
     @test isapproxstr(h, """
         <h1 id="rational_numbers"><a href="/index.html#rational_numbers">Rational numbers</a></h1>
diff --git a/test/integration/literate_fs2.jl b/test/integration/literate_fs2.jl
new file mode 100644
index 00000000..5462fff3
--- /dev/null
+++ b/test/integration/literate_fs2.jl
@@ -0,0 +1,115 @@
+fs1()
+
+scripts = joinpath(F.PATHS[:folder], "literate-scripts")
+mkpath(scripts)
+
+@testset "Literate-0" begin
+    @test_throws ErrorException literate_folder("foo/")
+    litpath = literate_folder("literate-scripts/")
+    @test litpath == joinpath(F.PATHS[:folder], "literate-scripts/")
+end
+
+@testset "Literate-a" begin
+    # Post processing: numbering of julia blocks
+    s = raw"""
+        A
+
+        ```julia
+        B
+        ```
+
+        C
+
+        ```julia
+        D
+        ```
+        """
+    @test F.literate_post_process(s) == """
+        <!--This file was generated, do not modify it.-->
+        A
+
+        ```julia:ex1
+        B
+        ```
+
+        C
+
+        ```julia:ex2
+        D
+        ```
+        """
+end
+
+@testset "Literate-b" begin
+    # Literate to Franklin
+    s = raw"""
+        # # Rational numbers
+        #
+        # In julia rational numbers can be constructed with the `//` operator.
+        # Lets define two rational numbers, `x` and `y`:
+
+        ## Define variable x and y
+        x = 1//3
+        y = 2//5
+
+        # When adding `x` and `y` together we obtain a new rational number:
+
+        z = x + y
+        """
+    path = joinpath(scripts, "tutorial.jl")
+    write(path, s)
+    opath, = F.literate_to_franklin("/literate-scripts/tutorial")
+    @test endswith(opath, joinpath(F.PATHS[:assets], "literate", "tutorial.md"))
+    out = read(opath, String)
+    @test out == """
+        <!--This file was generated, do not modify it.-->
+        # Rational numbers
+
+        In julia rational numbers can be constructed with the `//` operator.
+        Lets define two rational numbers, `x` and `y`:
+
+        ```julia:ex1
+        # Define variable x and y
+        x = 1//3
+        y = 2//5
+        ```
+
+        When adding `x` and `y` together we obtain a new rational number:
+
+        ```julia:ex2
+        z = x + y
+        ```
+
+        """
+
+    # Use of `\literate` command
+    h = raw"""
+        @def hascode = true
+        @def showall = true
+        @def reeval = true
+
+        \literate{/literate-scripts/tutorial.jl}
+        """ |> fd2html_td
+    @test isapproxstr(h, """
+        <h1 id="rational_numbers"><a href="/index.html#rational_numbers">Rational numbers</a></h1>
+        <p>In julia rational numbers can be constructed with the <code>//</code> operator. Lets define two rational numbers, <code>x</code> and <code>y</code>:</p>
+        <pre><code class="language-julia"># Define variable x and y
+        x = 1//3
+        y = 2//5</code></pre>
+        <pre><code class=\"plaintext\">2//5</code></pre>
+        <p>When adding <code>x</code> and <code>y</code> together we obtain a new rational number:</p>
+        <pre><code class="language-julia">z = x + y</code></pre>
+        <pre><code class=\"plaintext\">11//15</code></pre>
+        """)
+end
+
+@testset "Literate-c" begin
+    s = raw"""
+        \literate{foo}
+        """
+    @test_throws F.LiterateRelativePathError (s |> fd2html_td)
+    s = raw"""
+        \literate{/foo}
+        """
+    @test @test_logs (:warn, "File not found when trying to convert a literate file ($(joinpath(F.PATHS[:folder], "foo.jl"))).") (s |> fd2html_td) == """<p><span style="color:red;">// Literate file matching '/foo' not found. //</span></p></p>\n"""
+end
diff --git a/test/manager/config.jl b/test/manager/config.jl
index 018d896d..f2a83fac 100644
--- a/test/manager/config.jl
+++ b/test/manager/config.jl
@@ -1,4 +1,5 @@
 # additional tests for config
+fs1()
 
 foofig(p, s) = (write(joinpath(p, "src", "config.md"), s); F.process_config())
 
diff --git a/test/manager/config_fs2.jl b/test/manager/config_fs2.jl
new file mode 100644
index 00000000..6ae25de2
--- /dev/null
+++ b/test/manager/config_fs2.jl
@@ -0,0 +1,34 @@
+# additional tests for config
+fs2()
+
+foofig(s) = (write(joinpath(td, "config.md"), s); F.process_config())
+
+@testset "config" begin
+    # ================================
+    # asssignments go to GLOBAL
+    foofig(raw"""
+        @def var = 5
+        """)
+    @test haskey(F.GLOBAL_VARS, "var")
+    @test F.GLOBAL_VARS["var"][1] == 5
+
+    # lxdefs go to GLOBAL
+    foofig(raw"""
+        \newcommand{\hello}{goodbye}
+        """)
+    @test haskey(F.GLOBAL_LXDEFS, "\\hello")
+    @test F.GLOBAL_LXDEFS["\\hello"].def == "goodbye"
+
+    # combination of lxdefs
+    foofig(raw"""
+        \newcommand{\hello}{goodbye}
+        \newcommand{\hellob}{\hello}
+        \newcommand{\helloc}{\hellob}
+        """)
+
+    @test F.GLOBAL_LXDEFS["\\hello"].from < F.GLOBAL_LXDEFS["\\hello"].to <
+          F.GLOBAL_LXDEFS["\\hellob"].from < F.GLOBAL_LXDEFS["\\hellob"].to <
+          F.GLOBAL_LXDEFS["\\helloc"].from < F.GLOBAL_LXDEFS["\\helloc"].to
+
+    @test fd2html(raw"""\helloc"""; dir=td, internal=true) == "goodbye"
+end
diff --git a/test/manager/rss.jl b/test/manager/rss.jl
index 60df3070..e7126f06 100644
--- a/test/manager/rss.jl
+++ b/test/manager/rss.jl
@@ -13,6 +13,7 @@
 end
 
 @testset "RSSbasics" begin
+    fs1()
     empty!(F.RSS_DICT)
     F.def_GLOBAL_VARS!()
     F.set_var!(F.GLOBAL_VARS, "website_title", "Website title")
@@ -44,3 +45,37 @@ end
     fc = prod(readlines(feed, keep=true))
     @test occursin("<description><![CDATA[A <strong>description</strong> done.", fc)
 end
+
+@testset "RSSbasics" begin
+    fs2()
+    empty!(F.RSS_DICT)
+    F.def_GLOBAL_VARS!()
+    F.set_var!(F.GLOBAL_VARS, "website_title", "Website title")
+    F.set_var!(F.GLOBAL_VARS, "website_descr", "Website descr")
+    F.set_var!(F.GLOBAL_VARS, "website_url", "https://github.com/tlienart/Franklin.jl/")
+    F.def_LOCAL_VARS!()
+    set_curpath("hey/ho.md")
+    F.set_var!(F.LOCAL_VARS, "rss_title", "title")
+    F.set_var!(F.LOCAL_VARS, "rss", "A **description** done.")
+    F.set_var!(F.LOCAL_VARS, "rss_author", "chuck@norris.com")
+
+    item = F.add_rss_item()
+    @test item.title == "title"
+    @test item.description == "A <strong>description</strong> done.\n"
+    @test item.author == "chuck@norris.com"
+    # unchanged bc all three fallbacks lead to Data(1)
+    @test item.pubDate == Date(1)
+
+    F.set_var!(F.LOCAL_VARS, "rss_title", "")
+    @test @test_logs (:warn, "Found an RSS description but no title for page /hey/ho/index.html.") F.add_rss_item().title == ""
+
+    @test F.RSS_DICT["/hey/ho/index.html"].description == item.description
+
+    # Generation
+    F.PATHS[:folder] = td
+    F.rss_generator()
+    feed = joinpath(F.PATHS[:site], "feed.xml")
+    @test isfile(feed)
+    fc = prod(readlines(feed, keep=true))
+    @test occursin("<description><![CDATA[A <strong>description</strong> done.", fc)
+end
diff --git a/test/manager/utils.jl b/test/manager/utils.jl
index 498faec2..f4ef6afc 100644
--- a/test/manager/utils.jl
+++ b/test/manager/utils.jl
@@ -1,3 +1,5 @@
+fs1()
+
 temp_config = joinpath(F.PATHS[:src], "config.md")
 write(temp_config, "@def author = \"Stefan Zweig\"\n")
 temp_index = joinpath(F.PATHS[:src], "index.md")
@@ -37,8 +39,8 @@ end
     other_files = empty(md_files)
     infra_files = empty(md_files)
     literate_files = empty(md_files)
-    watched_files = [md_files, html_files, other_files, infra_files, literate_files]
-    F.scan_input_dir!(md_files, html_files, other_files, infra_files, literate_files, true)
+    watched_files = [other_files, infra_files, md_files, html_files, literate_files]
+    F.scan_input_dir!(other_files, infra_files, md_files, html_files, literate_files, true)
     @test haskey(md_files, F.PATHS[:src_pages]=>"blah.md")
     @test md_files[F.PATHS[:src_pages]=>"blah.md"] == mtime(temp_blah) == stat(temp_blah).mtime
     @test html_files[F.PATHS[:src_pages]=>"temp.html"] == mtime(temp_html)
@@ -55,8 +57,8 @@ end
     pg_foot = "\npage_foot"
     foot = "foot {{if hasmath}} {{fill author}}{{end}}"
 
-    F.write_page(F.PATHS[:src], "index.md", head, pg_foot, foot)
-    out_file = joinpath(F.out_path(F.PATHS[:folder]), "index.html")
+    out_file = F.form_output_path(F.PATHS[:folder], "index.html", :html)
+    F.write_page(F.PATHS[:src], "index.md", head, pg_foot, foot, out_file)
 
     @test isfile(out_file)
     @test read(out_file, String) == "head\n<div class=\"franklin-content\">\n<p>blah blah</p>\n\n\npage_foot\n</div>\nfoot  Stefan Zweig"
diff --git a/test/manager/utils_fs2.jl b/test/manager/utils_fs2.jl
new file mode 100644
index 00000000..a0d77434
--- /dev/null
+++ b/test/manager/utils_fs2.jl
@@ -0,0 +1,109 @@
+fs2()
+
+temp_config = joinpath(F.PATHS[:folder], "config.md")
+write(temp_config, "@def author = \"Stefan Zweig\"\n")
+temp_index = joinpath(F.PATHS[:folder], "index.md")
+write(temp_index, "blah blah")
+temp_index2 = joinpath(F.PATHS[:folder], "index.html")
+write(temp_index2, "blah blih")
+temp_blah = joinpath(F.PATHS[:folder], "blah.md")
+write(temp_blah, "blah blah")
+temp_html = joinpath(F.PATHS[:folder], "temp.html")
+write(temp_html, "some html")
+temp_rnd = joinpath(F.PATHS[:folder], "temp.rnd")
+write(temp_rnd, "some random")
+temp_css = joinpath(F.PATHS[:css], "temp.css")
+write(temp_css, "some css")
+temp_js = joinpath(F.PATHS[:libs], "foo.js")
+write(temp_js, "some js")
+
+F.process_config()
+
+@testset "Prep outdir" begin
+    F.prepare_output_dir()
+    @test isdir(F.PATHS[:site])
+    temp_out = joinpath(F.PATHS[:site], "tmp.html")
+    write(temp_out, "This is a test page.\n")
+    # clear is false => file should remain
+    F.prepare_output_dir(false)
+    @test isfile(temp_out)
+    # clear is true => file should go
+    F.prepare_output_dir(true)
+    @test !isfile(temp_out)
+end
+
+@testset "Scan dir" begin
+    println("🐝 Testing file tracking...:")
+    # it also tests add_if_new_file and last
+    md_files       = Dict{Pair{String, String}, Float64}()
+    html_files     = empty(md_files)
+    other_files    = empty(md_files)
+    infra_files    = empty(md_files)
+    literate_files = empty(md_files)
+    watched_files  = [other_files, infra_files, md_files, html_files, literate_files]
+    F.scan_input_dir!(other_files, infra_files, md_files, html_files, literate_files, true)
+    @test haskey(md_files, F.PATHS[:folder]=>"blah.md")
+    @test md_files[F.PATHS[:folder]=>"blah.md"] == mtime(temp_blah) == stat(temp_blah).mtime
+    @test html_files[F.PATHS[:folder]=>"temp.html"] == mtime(temp_html)
+    @test other_files[F.PATHS[:folder]=>"temp.rnd"] == mtime(temp_rnd)
+end
+
+@testset "Config+write" begin
+    F.process_config()
+    @test F.GLOBAL_VARS["author"].first == "Stefan Zweig"
+    rm(temp_config)
+    @test_logs (:warn, "I didn't find a config file. Ignoring.") F.process_config()
+    # testing write
+    head = "head"
+    pg_foot = "\npage_foot"
+    foot = "foot {{if hasmath}} {{fill author}}{{end}}"
+
+    out_file = F.form_output_path(F.PATHS[:site], "index.html", :html)
+    F.write_page(F.PATHS[:folder], "index.md", head, pg_foot, foot, out_file)
+
+    @test isfile(out_file)
+    @test read(out_file, String) == "head\n<div class=\"franklin-content\">\n<p>blah blah</p>\n\n\npage_foot\n</div>\nfoot  Stefan Zweig"
+end
+
+temp_config = joinpath(F.PATHS[:folder], "config.md")
+write(temp_config, "@def author = \"Stefan Zweig\"\n")
+rm(temp_index2)
+
+@testset "Part convert" begin
+    write(joinpath(F.PATHS[:layout], "head.html"), raw"""
+        <!doctype html>
+        <html lang="en-UK">
+            <head>
+                <meta charset="UTF-8">
+                <link rel="stylesheet" href="/css/main.css">
+            </head>
+        <body>""")
+    write(joinpath(F.PATHS[:layout], "page_foot.html"), raw"""
+        <div class="page-foot">
+                <div class="copyright">
+                        &copy; All rights reserved.
+                </div>
+        </div>""")
+    write(joinpath(F.PATHS[:layout], "foot.html"), raw"""
+            </body>
+        </html>""")
+
+    clear = true
+
+    watched_files = F.fd_setup(; clear=clear)
+
+    F.fd_fullpass(watched_files; clear=clear)
+
+    @test issubset(["css", "libs", "index.html"], readdir(F.PATHS[:site]))
+    @test issubset(["temp", "temp.rnd"], readdir(F.PATHS[:site]))
+    @test issubset(["index.html"], readdir(joinpath(F.PATHS[:site], "temp")))
+    @test issubset(["index.html"], readdir(joinpath(F.PATHS[:site], "blah")))
+
+    @test all(split(read(joinpath(F.PATHS[:site], "index.html"), String)) .== split("<!doctype html>\n<html lang=\"en-UK\">\n\t<head>\n\t\t<meta charset=\"UTF-8\">\n\t\t<link rel=\"stylesheet\" href=\"/css/main.css\">\n\t</head>\n<body>\n<div class=\"franklin-content\">\n<p>blah blah</p>\n\n<div class=\"page-foot\">\n\t\t<div class=\"copyright\">\n\t\t\t\t&copy; All rights reserved.\n\t\t</div>\n</div>\n</div>\n    </body>\n</html>"))
+end
+
+@testset "Err procfile" begin
+    write(temp_index, "blah blah { blih etc")
+    println("🐝 Testing error message...:")
+    @test_throws F.OCBlockError F.process_file_err(:md, F.PATHS[:folder] => "index.md"; clear=false)
+end
diff --git a/test/runtests.jl b/test/runtests.jl
index 78265874..dea417d1 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -4,9 +4,11 @@ const R = @__DIR__
 const D = joinpath(dirname(dirname(pathof(Franklin))), "test", "_dummies")
 
 F.FD_ENV[:SILENT_MODE] = true
+F.FD_ENV[:STRUCTURE] = v"0.1" # legacy, it's switched up in the tests.
 
 # UTILS
 println("UTILS")
+include("utils/folder_structure.jl")
 include("utils/paths_vars.jl"); include("test_utils.jl")
 include("utils/misc.jl")
 include("utils/errors.jl")
@@ -16,8 +18,10 @@ println("🍺")
 # MANAGER folder
 println("MANAGER")
 include("manager/utils.jl")
+include("manager/utils_fs2.jl")
 include("manager/rss.jl")
 include("manager/config.jl")
+include("manager/config_fs2.jl")
 println("🍺")
 
 # PARSER folder
@@ -34,8 +38,10 @@ println("EVAL")
 include("eval/module.jl")
 include("eval/run.jl")
 include("eval/io.jl")
+include("eval/io_fs2.jl")
 include("eval/codeblock.jl")
 include("eval/eval.jl")
+include("eval/eval_fs2.jl")
 include("eval/integration.jl")
 
 # CONVERTER folder
@@ -53,8 +59,11 @@ println("🍺")
 println("CONVERTER/LX")
 include("converter/lx_input.jl")
 include("converter/lx_simple.jl")
+include("converter/lx_simple_fs2.jl")
 println("🍺")
 
+fs2()
+
 println("GLOBAL")
 include("global/cases1.jl")
 include("global/cases2.jl")
@@ -83,6 +92,7 @@ cd(dirname(dirname(pathof(Franklin))))
 
 println("INTEGRATION")
 include("integration/literate.jl")
+include("integration/literate_fs2.jl")
 
 flush_td()
 cd(joinpath(dirname(dirname(pathof(Franklin)))))
@@ -91,3 +101,9 @@ println("COVERAGE")
 include("coverage/extras1.jl")
 
 println("😅 😅 😅 😅")
+
+# check quickly if the IPs in IP_CHECK are still ok
+println("Verifying ip addresses, if online these should succeed.")
+for (addr, name) in F.IP_CHECK
+    println(rpad("Ping $name:", 13), ifelse(F.check_ping(addr), "✓", "✗"), ".")
+end
diff --git a/test/test_utils.jl b/test/test_utils.jl
index 352b72fa..74be6a74 100644
--- a/test/test_utils.jl
+++ b/test/test_utils.jl
@@ -123,3 +123,31 @@ function explore_h_steps(hs, allvars=F.PageVars())
 
     return steps
 end
+
+gotd() = (flush_td(); cd(td); F.FOLDER_PATH[] = td)
+
+function fs1()
+    F.FD_ENV[:STRUCTURE] = v"0.1"
+    gotd()
+    empty!(F.PATHS)
+    F.set_paths!()
+    mkdir(F.PATHS[:src])
+    mkdir(F.PATHS[:src_pages])
+    mkdir(F.PATHS[:libs])
+    mkdir(F.PATHS[:src_css])
+    mkdir(F.PATHS[:src_html])
+    mkdir(F.PATHS[:assets])
+end
+
+function fs2()
+    F.FD_ENV[:STRUCTURE] = v"0.2"
+    gotd()
+    empty!(F.PATHS)
+    F.set_paths!()
+    mkdir(F.PATHS[:site])
+    mkdir(F.PATHS[:assets])
+    mkdir(F.PATHS[:css])
+    mkdir(F.PATHS[:layout])
+    mkdir(F.PATHS[:libs])
+    mkdir(F.PATHS[:literate])
+end
diff --git a/test/utils/folder_structure.jl b/test/utils/folder_structure.jl
new file mode 100644
index 00000000..c8ff551f
--- /dev/null
+++ b/test/utils/folder_structure.jl
@@ -0,0 +1,87 @@
+@testset "set_paths!" begin
+    root = F.FOLDER_PATH[] = mktempdir()
+    empty!(F.PATHS); F.FD_ENV[:STRUCTURE] = v"0.1"; F.set_paths!()
+    @test Set(keys(F.PATHS)) == Set([:folder, :src, :src_pages, :src_css, :src_html, :pub, :css, :libs, :assets, :literate])
+    @test F.PATHS[:folder] == root
+    @test F.PATHS[:src_pages] == joinpath(F.PATHS[:src], "pages")
+
+    # ================================================
+    empty!(F.PATHS); F.FD_ENV[:STRUCTURE] = v"0.2"; F.set_paths!()
+    @test Set(keys(F.PATHS)) == Set([:folder, :assets, :css, :layout, :libs, :literate, :site])
+    @test F.PATHS[:folder]   == root
+    @test F.PATHS[:site]     == joinpath(root, "__site")
+    @test F.PATHS[:assets]   == joinpath(root, "_assets")
+    @test F.PATHS[:css]      == joinpath(root, "_css")
+    @test F.PATHS[:layout]   == joinpath(root, "_layout")
+    @test F.PATHS[:libs]     == joinpath(root, "_libs")
+    @test F.PATHS[:literate] == joinpath(root, "_literate")
+
+    # ================================================
+    # reset the structure to legacy for further tests
+    empty!(F.PATHS); F.FD_ENV[:STRUCTURE] = v"0.1"; F.set_paths!()
+end
+
+@testset "outp_path" begin
+    empty!(F.PATHS); F.FD_ENV[:STRUCTURE] = v"0.1"; F.set_paths!()
+    # MD_PAGES
+    out = F.form_output_path(F.PATHS[:src], "index.md", :md)
+    @test out == joinpath(F.PATHS[:folder], "index.html")
+
+    out = F.form_output_path(F.PATHS[:src_pages], "foo.md", :md)
+    @test out == joinpath(F.PATHS[:folder], "pub", "foo.html")
+
+    # CSS
+    out = F.form_output_path(F.PATHS[:src_css], "foo.css", :infra)
+    @test out == joinpath(F.PATHS[:css], "foo.css")
+
+    # (note: assets, libs are not tracked and not considered in v1)
+
+    # ================================================
+
+    empty!(F.PATHS); F.FD_ENV[:STRUCTURE] = v"0.2"; F.set_paths!()
+
+    # MD_PAGES
+    base = F.PATHS[:folder]
+    file = "index.md"
+    out = F.form_output_path(base, file, :md)
+    @test out == joinpath(F.PATHS[:site], "index.html")
+
+    file = "index.html"
+    out = F.form_output_path(base, file, :html)
+    @test out == joinpath(F.PATHS[:site], "index.html")
+
+    file = "page.md"
+    out = F.form_output_path(base, file, :md)
+    @test out == joinpath(F.PATHS[:site], "page", "index.html")
+
+    file = "page.html"
+    out = F.form_output_path(base, file, :html)
+    @test out == joinpath(F.PATHS[:site], "page", "index.html")
+
+    file = "index.md"
+    base = joinpath(F.PATHS[:folder], "menu")
+    out = F.form_output_path(base, file, :md)
+    @test out == joinpath(F.PATHS[:site], "menu", "index.html")
+
+    file = "index.html"
+    out = F.form_output_path(base, file, :html)
+    @test out == joinpath(F.PATHS[:site], "menu", "index.html")
+
+    file = "page.md"
+    out = F.form_output_path(base, file, :md)
+    @test out == joinpath(F.PATHS[:site], "menu", "page", "index.html")
+
+    # OTHER STUFF
+    file = "foo.css"
+    base = F.PATHS[:css]
+    out = F.form_output_path(base, file, :infra)
+    @test out == joinpath(F.PATHS[:site], "css", "foo.css")
+    file = "lib.js"
+    base = F.PATHS[:libs]
+    out = F.form_output_path(base, file, :infra)
+    @test out == joinpath(F.PATHS[:site], "libs", "lib.js")
+
+    # ================================================
+
+    empty!(F.PATHS); F.FD_ENV[:STRUCTURE] = v"0.1"; F.set_paths!()
+end
diff --git a/test/utils/html.jl b/test/utils/html.jl
index e2eb23fa..c94b57a9 100644
--- a/test/utils/html.jl
+++ b/test/utils/html.jl
@@ -1,14 +1,24 @@
 @testset "misc-html" begin
+    fs1()
     λ = "blah/blah.ext"
     set_curpath("pages/cpB/blah.md")
-    @test F.html_ahref(λ, 1) == "<a href=\"$λ\">1</a>"
-    @test F.html_ahref(λ, "bb") == "<a href=\"$λ\">bb</a>"
+    @test F.html_ahref(λ, 1)           == "<a href=\"$λ\">1</a>"
+    @test F.html_ahref(λ, "bb")        == "<a href=\"$λ\">bb</a>"
     @test F.html_ahref_key("cc", "dd") == "<a href=\"/pub/cpB/blah.html#cc\">dd</a>"
     @test F.html_div("dn","ct") == "<div class=\"dn\">ct</div>"
     @test F.html_img("src", "alt") == "<img src=\"src\" alt=\"alt\">"
     @test F.html_code("code") == "<pre><code class=\"plaintext\">code</code></pre>"
     @test F.html_code("code", "lang") == "<pre><code class=\"language-lang\">code</code></pre>"
     @test F.html_err("blah") == "<p><span style=\"color:red;\">// blah //</span></p>"
+
+    fs2()
+    λ = "blah/blah.ext"
+    set_curpath("cpB/blah.md")
+    @test F.html_ahref(λ, 1)           == "<a href=\"$λ\">1</a>"
+    @test F.html_ahref(λ, "bb")        == "<a href=\"$λ\">bb</a>"
+    @test F.html_ahref_key("cc", "dd") == "<a href=\"/cpB/blah/index.html#cc\">dd</a>"
+
+    fs1()
 end
 
 @testset "misc-html 2" begin
diff --git a/test/utils/misc.jl b/test/utils/misc.jl
index 9633b71e..dee78af3 100644
--- a/test/utils/misc.jl
+++ b/test/utils/misc.jl
@@ -104,3 +104,14 @@ end
    @test F.is_html_escaped(he)
    @test F.html_code(h, "html") == F.html_code(h, "html")
 end
+
+@testset "match_url" begin
+    @test F.match_url("index", "/index.html")
+    @test F.match_url("index", "/index.html")
+    @test F.match_url("index", "index.html")
+    @test F.match_url("/index", "index.html")
+    @test F.match_url("menu1", "/menu1/")
+    @test F.match_url("menu1", "menu1/")
+    @test F.match_url("menu1", "menu1")
+    @test F.match_url("menu1", "/menu1/*")
+end