Repository: tlienart/Franklin.jl
Pull request #799 (2021-03-16 18:07:26)

Fixing RSS properly
This (finally) fixes the RSS problems (e.g. #805). 

* [x] add `_rss` folder to FranklinTemplates
* [x] change logic of `rss_generator`
* [x] help deployment at JuliaGPU.org
* [x] add tests
* [x] add docs

## How to use this before the release

In your `deploy.yml` script put the package spec: 

```
- run: julia --project -e '
            using Pkg; Pkg.add(PackageSpec(name="Franklin", rev="rssfix"));
            using Franklin;
            optimize();'
```

## What changes on the user side

If you had at some point a working RSS feed, **this should not require you to do anything special**, things should just work (and if they don't, please say so). 

One thing is that it is now *recommended* to explicitly set

```
generate_rss = true
```

in your `config.md` along with the mandatory definition of `website_url`, `website_description` (or `website_descr`) for instance:

```
generate_rss = true
website_title = "JuliaGPU"
website_descr = "High-performance GPU programming in a high-level language."
website_url = "https://juliagpu.org"
```

## What the changes allow / how to benefit from them

The RSS feed is now fully customisable; it is created by doing:

```
head * prod(items) * "</channel></rss>"
```

where `head` comes from `_rss/head.xml` a file that you control just like a `_layout/*` file and which, if you don't have it, is copied from [head.xml](https://github.com/tlienart/FranklinTemplates.jl/blob/master/src/templates/common/_rss/head.xml); likewise the items are generated from `_rss/item.xml` a file that you control as well, see the default [item.xml](https://github.com/tlienart/FranklinTemplates.jl/blob/master/src/templates/common/_rss/item.xml)

* **head**: the head has access to what's defined in your `config.md` file, for instance the variables `website_description`, `website_url` etc. 
* **item**: the item has access to the scope of the corresponding page if it has a `rss` variable. 

That's about it. You can see an example for [JuliaGPU](https://github.com/JuliaGPU/juliagpu.org/blob/master/config.md):

* [config.md](https://raw.githubusercontent.com/JuliaGPU/juliagpu.org/master/config.md) note the few lines defining the general RSS variables
* [_rss folder](https://github.com/JuliaGPU/juliagpu.org/tree/master/_rss) the slightly adjusted RSS template

They use a `abstract` function which basically sets the `rss` variable see

* [a post](https://raw.githubusercontent.com/JuliaGPU/juliagpu.org/master/post/2020-03-25-cudanative_3.0-cuarrays_2.0.md)
* [the definition of abstract](https://github.com/JuliaGPU/juliagpu.org/blob/8c4f13fb2bb89625d99548ee751bf369237c82f6/utils.jl#L109-L114) observe that it sets the variable `rss_description`; another, easier way would have been to define that variable explicitly and called `{{rss}}` in the text 😄 

### Adding full content to RSS item

There are two fields per RSS item: one is the `description` which should be a short description of what's on the page and another one is `content:encoded` which is basically the full content of the page. You are allowed to:

* only set description (and it could be the full content)
* set description + content:encoded

you cannot just set `content:encoded`. 

By default only the `description` is set, but you can add the full page HTML by adding `rss_full_content=true` in your `config.md` ([example](https://raw.githubusercontent.com/JuliaGPU/juliagpu.org/master/config.md)).

## Validating your feed

- https://validator.w3.org/feed/docs/rss2.html 
- https://www.rssboard.org/rss-profile (for something that's supposed to be _really simple_ there's a lot of best practices...)


Patch:
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 375ea174..65ab46b0 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -26,10 +26,16 @@ jobs:
       with:
         version: 1.5
     - run: julia -e '
-            using Pkg; Pkg.add(Pkg.PackageSpec(name="Franklin", rev="master"));
+            using Pkg;
+            Pkg.add(Pkg.PackageSpec(name="NodeJS"));
+            using NodeJS;
+            run(`$(npm_cmd()) install lunr`);
+            run(`$(npm_cmd()) install cheerio`);
+            Pkg.add(Pkg.PackageSpec(name="Franklin", rev="master"));
             using Franklin;
             cd("docs");
             Pkg.activate("."); Pkg.instantiate();
+            lunr();
             optimize(minify=false, prerender=false);
             cd("../demos");
             Pkg.activate("."); Pkg.instantiate();
diff --git a/Project.toml b/Project.toml
index 7698fedb..f23edcf7 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.10.33"
+version = "0.10.35"
 
 [deps]
 Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
diff --git a/docs/_layout/head.html b/docs/_layout/head.html
index 95defae2..738c4374 100644
--- a/docs/_layout/head.html
+++ b/docs/_layout/head.html
@@ -60,6 +60,7 @@
               <li class="menu-list-item {{ispage syntax/markdown}}active{{end}}"><a href="/syntax/markdown/" class="menu-list-link">Markdown</a>
               <li class="menu-list-item {{ispage syntax/divs-commands}}active{{end}}"><a href="/syntax/divs-commands/" class="menu-list-link">Divs and Commands</a>
               <li class="menu-list-item {{ispage syntax/page-variables}}active{{end}}"><a href="/syntax/page-variables/" class="menu-list-link">Page Variables</a>
+							<li class="menu-list-item {{ispage syntax/rss}}active{{end}}"><a href="/syntax/rss/" class="menu-list-link">RSS</a>
 			  <li class="menu-list-item {{ispage syntax/utils}}active{{end}}"><a href="/syntax/utils/" class="menu-list-link">Utils</a>
             </ul>
 		  <!-- STYLING -->
diff --git a/docs/syntax/page-variables.md b/docs/syntax/page-variables.md
index 17b76261..d2758af4 100644
--- a/docs/syntax/page-variables.md
+++ b/docs/syntax/page-variables.md
@@ -1,7 +1,7 @@
 # Page variables
 
 <!--
-reviewed: Oct 18, 2020
+reviewed: April 10, 2021
 -->
 
 \blurb{Page variables offer a straightforward way to interact with the HTML templating from the markdown and much more.}
@@ -26,7 +26,8 @@ where you could set the variable to a string, a number, a date,... anything. Def
     but lines must be indented"""
 ```
 
-You can also define them in blocks surrounded by `+++...+++`, the entire content of which will be evaluated as if it was Julia code but all assigned variables will be considered as page variables:
+You can also define them in blocks surrounded by `+++...+++`, the entire content of which will be evaluated as if it was Julia code but all assigned variables will be considered as page variables.
+For instance, consider:
 
 ```julia
 +++
@@ -36,6 +37,8 @@ var2 = "Hello goodbye"
 +++
 ```
 
+this block will be executed as normal Julia code and assigns `var1` and `var2` which then become available as page variable.
+
 These variables can serve multiple purposes but, primarily, they can be accessed from the HTML template blocks e.g.:
 
 ```html
@@ -52,11 +55,46 @@ The syntax `{{ ... }}` indicates a HTML _function_, `fill` is the function name
   Argument-less calls such as `{{ name }}` will be interpreted in either one of two way: first it will check whether there is a html function `name` and if so will call it, otherwise it will check whether there is a page variable `name` and will just insert it -- it will be treated as a shortcut for `{{fill name}}`.
 }
 
-_Local_ page variables denote variables that are defined on a single page and accessible on that page only by contrast to _global_ page variables which are set globally (in the `config.md` file) and accessible on all pages.
+_Local_ page variables denote variables that are defined on one page and directly accessible on that page. _Global_ page variables, by contrast, are directly available on _all_ pages. Any variable defined in your `config.md` file is global.
 
 In both cases there are _default_ page variables set by Franklin with default values which you can both change and use.
 You can of course also define your own variables, both global and local.
 
+### Defining and accessing page variables
+
+Let us say that in `config.md`, we have
+
+```
+@def global_var_1 = "foo"
+```
+
+and that in `index.md`, we have
+
+```
+@def local_var_1 = "bar"
+```
+
+On `index.md`, you can access both variables so this:
+
+```
+{{global_var_1}} - {{local_var_1}}
+```
+
+will insert
+
+```
+foo - bar
+```
+
+On another page `foo.md`, only `global_var_1` is directly accessible but you can still access `local_var_1` except you have to specify where it's defined (and you **must** use `fill`):
+
+```
+{{global_var_1}} - {{fill local_var_1 index.md}}
+```
+
+The second bit essentially says: "get `local_var_1` from the scope of the page `index.md`".
+
+
 ### Using page variables
 
 Both local and global page variables are meant for essentially two purposes:
@@ -76,12 +114,12 @@ In the first case, you can access variables in your HTML template via one of the
 
 ## HTML functions
 
-HTML functions can be used in any one of your `*.html` file and in particular in any of the `_layout/*.html` files such as `head.html`.
+HTML functions can be used in any one of your `*.html` **and** `.md` files; in particular, they can be used in any of the `_layout/*.html` files such as `head.html`.
 They are always called with the syntax `{{fname pv1 pv2 ...}}` where `pv1 pv2 ...` are page variable names.
 
 ### Basic functions
 
-A few functions are available with the `{{fill ...}}` arguably the most likely to be of use.
+A few functions are available with the `{{fill ...}}` arguably the most likely to be of use:
 
 @@lalign
 | Format | Role |
@@ -253,8 +291,7 @@ For more informations on these, see the section on [inserting and evaluating cod
 ### RSS
 
 These are variables related to [RSS 2.0 specifications](https://cyber.harvard.edu/rss/rss.html)  and must match the format indicated there.
-If you want proper RSS to be generated, you **must** define at least the `rss_description` or `rss` (which is an alias for `rss_description`).
-All these variables expect a `String`.
+For more information about generating an RSS feed, see [the dedicated RSS page](/syntax/rss/).
 
 @@lalign
 | Name | Default value |
diff --git a/docs/syntax/rss.md b/docs/syntax/rss.md
new file mode 100644
index 00000000..b83bcc12
--- /dev/null
+++ b/docs/syntax/rss.md
@@ -0,0 +1,1 @@
+# RSS
diff --git a/src/Franklin.jl b/src/Franklin.jl
index af309281..791fc8ea 100644
--- a/src/Franklin.jl
+++ b/src/Franklin.jl
@@ -21,7 +21,7 @@ import Random
 import ExprTools: splitdef, combinedef
 import REPL.REPLCompletions: emoji_symbols
 
-export serve, publish, cleanpull, newsite, optimize, fd2html,
+export serve, publish, cleanpull, newsite, optimize, fd2html, fd2text,
        literate_folder, verify_links, @OUTPUT, get_url
 
 # Extra functions
@@ -55,9 +55,9 @@ const POINTER_WORKFLOW =
 # obtained via `dig www...`; they may change over time; see `check_ping`
 # we check in sequence, one should work if the user is online...
 const IP_CHECK = (
-    "172.217.21.132" => "Google", # google
-    "140.82.118.4"   => "GitHub",   # github
-    "103.235.46.39"  => "Baidu",  # baidu
+    "172.217.21.132"  => "Google", # google
+    "185.199.109.153" => "GitHub", # github
+    "103.235.46.39"   => "Baidu",  # baidu
     )
 
 """Big number when we want things to be far."""
@@ -88,7 +88,7 @@ const FD_ENV = LittleDict(
     :UTILS_HASH    => nothing   # hash of the utils
     )
 
-utils_name()   = "Utils_$(FD_ENV[:UTILS_COUNTER])"
+utils_name()   = "Utils_$(FD_ENV[:UTILS_COUNTER]::Int)"
 utils_symb()   = Symbol(utils_name())
 utils_module() = getproperty(Main, utils_symb())
 utils_hash()   = nothing
diff --git a/src/converter/html/blocks.jl b/src/converter/html/blocks.jl
index 8f3a925c..a53b38a6 100644
--- a/src/converter/html/blocks.jl
+++ b/src/converter/html/blocks.jl
@@ -68,7 +68,7 @@ function process_html_cond(hs::AS, qblocks::Vector{AbstractBlock},
                 k ⊻= βi isa HIsNotDef
             elseif βi isa Union{HIsEmpty, HIsNotEmpty}
                 v = locvar(βi.vname)
-                e = isempty(v)
+                e = isnothing(v) || isempty(v)
                 k = ifelse(βi isa HIsEmpty, e, !e)
             end
         else
@@ -200,8 +200,8 @@ function process_html_for(hs::AS, qblocks::Vector{AbstractBlock},
 
     # content of the for block
     inner = subs(hs, nextind(hs, to(β_open)), prevind(hs, from(β_close)))
-    isempty(strip(inner)) && @goto final_step
     content = ""
+    isempty(strip(inner)) && @goto final_step
     if length(vnames) == 1
         rx1 = Regex("{{\\s*(?:fill\\s)?\\s*$vname\\s*}}")           # {{ fill v}} or {{v}}
         rx2 = Regex("{{\\s*(?:fill\\s)?\\s*(\\S+)\\s+$vname\\s*}}") # {{ fill x v}}
diff --git a/src/converter/html/functions.jl b/src/converter/html/functions.jl
index 537b5d78..dbed994c 100644
--- a/src/converter/html/functions.jl
+++ b/src/converter/html/functions.jl
@@ -262,7 +262,9 @@ function hfun_redirect(params::Vector{String})::String
     dst = joinpath(path(:site), addr)
     isfile(dst) && return ""
     mkpath(splitdir(dst)[1])
-    pp = ifelse(FD_ENV[:FINAL_PASS]::Bool, "/$(globvar(:prepath)::String)", "")
+    pp = globvar(:prepath)::String
+    pp = ifelse(isempty(pp), pp, "/$pp")
+    pp = ifelse(FD_ENV[:FINAL_PASS]::Bool, pp, "")
     write(dst, """
     <!-- Generated Redirect -->
     <!doctype html>
@@ -376,3 +378,44 @@ function hfun_sitemap_opts(params::Vector{String})::String
     SITEMAP_DICT[key] = SMOpts(lastmod, changefreq, priority)
     return ""
 end
+
+
+"""
+    hfun_fd2rss
+
+Take a fd-markdown string, convert it to HTML and fix relative links.
+"""
+function hfun_fd2rss(params::Vector{String})::String
+    src = locvar(params[1])::String
+    src_html = fd2html(src; internal=true, nop=true)
+    return _fix_relative_links(src_html)
+end
+
+
+"""
+    hfun_fix_relative_links
+
+Makes relative links into full links, typically in the context of RSS.
+"""
+function hfun_fix_relative_links(params::Vector{String})::String
+    src = locvar(params[1])::String
+    return _fix_relative_links(src)
+end
+
+function _fix_relative_links(src::String)
+    base_link = globvar(:website_url)::String
+    isempty(strip(base_link, '/')) && return src
+    endswith(base_link, '/') || (base_link *= '/')
+    return replace(src, PREPATH_FIX_PAT => SubstitutionString("\\1=\\2$base_link"))
+end
+
+
+"""
+    hfun_RFC822
+
+Print a date with the format 822/1123 (RSS).
+"""
+function hfun_rfc822(params::Vector{String})
+    dt = DateTime(locvar(params[1])::Date)
+    return Dates.format(dt, Dates.RFC1123Format) * " +0000"
+end
diff --git a/src/converter/html/html.jl b/src/converter/html/html.jl
index f04e4fa9..3816c35c 100644
--- a/src/converter/html/html.jl
+++ b/src/converter/html/html.jl
@@ -83,8 +83,14 @@ Take a Markdown string and return the HTML that Franklin would produce for it.
 """
 fd2html(s; kw...)::String = fd2html_v(s; kw...)[1]
 
-# legacy JuDoc
-jd2html = fd2html
+"""
+    fd2text(s; kw)
+
+Take a Markdown string and return a version with no markup (used in RSS circumstances).
+This is bruteforce to say the least, it just discards all HTML tags. It should be safe
+in that '<' and '>' should have been escaped by the fd2html pass.
+"""
+fd2text(s; kw...)::String = replace(fd2html(s; kw...), r"\<.*?\>" => "")
 
 """
 $SIGNATURES
diff --git a/src/converter/markdown/blocks.jl b/src/converter/markdown/blocks.jl
index f595f0ba..77bac6f1 100644
--- a/src/converter/markdown/blocks.jl
+++ b/src/converter/markdown/blocks.jl
@@ -134,9 +134,13 @@ function convert_header(β::OCBlock, lxdefs::Vector{LxDef})::String
         PAGE_HEADERS[rstitle] = (title, 1, parse(Int, hk[2]))
     end
     # return the title
-    return html_hk(hk, html_ahref_key(rstitle, title;
+    if globvar(:title_links)::Bool
+        return html_hk(hk,
+                       html_ahref_key(rstitle, title;
                                       class=locvar(:header_anchor_class)::String),
-                   id=rstitle)
+                       id=rstitle)
+    end
+    return html_hk(hk, title; id=rstitle)
 end
 
 """
diff --git a/src/converter/markdown/md.jl b/src/converter/markdown/md.jl
index d88184c3..a6e03899 100644
--- a/src/converter/markdown/md.jl
+++ b/src/converter/markdown/md.jl
@@ -88,10 +88,10 @@ function convert_md(mds::String,
     #>> e. keep track of literal content of possible link definitions to use
     validate_and_store_link_defs!(blocks)
 
-    if globvar("autocode") && any(b -> b.name in CODE_BLOCKS_NAMES, blocks)
+    if globvar(:autocode)::Bool && any(b -> b.name in CODE_BLOCKS_NAMES, blocks)
         set_var!(LOCAL_VARS, "hascode", true)
     end
-    if globvar("automath") && any(b -> b.name in MATH_BLOCKS_NAMES, blocks)
+    if globvar(:automath)::Bool && any(b -> b.name in MATH_BLOCKS_NAMES, blocks)
         set_var!(LOCAL_VARS, "hasmath", true)
     end
 
diff --git a/src/converter/markdown/mddefs.jl b/src/converter/markdown/mddefs.jl
index b3aa7e02..ed427f6b 100644
--- a/src/converter/markdown/mddefs.jl
+++ b/src/converter/markdown/mddefs.jl
@@ -33,17 +33,7 @@ function process_mddefs(blocks::Vector{OCBlock}, isconfig::Bool,
         for vname in vnames
             key    = String(vname)
             value  = getproperty(mdl, vname)
-            # is it already in curdict? if so check type
-            if haskey(curdict, key)
-                acc_types = curdict[key].second
-                if check_type(typeof(value), acc_types)
-                    curdict[key] = Pair(value, acc_types)
-                else
-                    mddef_warn(key, value, acc_types)
-                end
-            else
-                set_var!(curdict, key, value)
-            end
+            set_var!(curdict, key, value; isglobal=isconfig)
         end
     end
 
@@ -110,7 +100,7 @@ function process_mddefs(blocks::Vector{OCBlock}, isconfig::Bool,
     PAGE_TAGS = globvar("fd_page_tags")
     if isnothing(PAGE_TAGS)
         isempty(tags) && return nothing
-        set_var!(GLOBAL_VARS, "fd_page_tags", DTAG((rpath => tags,)))
+        set_var!(GLOBAL_VARS, "fd_page_tags", DTAG((rpath => tags,)); check=false)
     elseif !haskey(PAGE_TAGS, rpath)
         isempty(tags) && return nothing
         PAGE_TAGS[rpath] = tags
diff --git a/src/converter/markdown/tags.jl b/src/converter/markdown/tags.jl
index acc1545c..3c86b158 100644
--- a/src/converter/markdown/tags.jl
+++ b/src/converter/markdown/tags.jl
@@ -27,7 +27,7 @@ function generate_tag_pages(refresh_tags=Set{String}())::Nothing
     # Get the inverse dictionary {tag -> [rp1, rp2...]}
     TAG_PAGES = invert_dict(PAGE_TAGS)
     # store it in globvar
-    set_var!(GLOBAL_VARS, "fd_tag_pages", TAG_PAGES)
+    set_var!(GLOBAL_VARS, "fd_tag_pages", TAG_PAGES; check=false)
     all_tags = collect(keys(TAG_PAGES))
 
     # some tags may have been given to refresh which don't have any
@@ -111,8 +111,8 @@ that the user has to modify it. This will help users transition when they may
 have used a template that did not define `_layout/tag.html`.
 """
 function write_default_tag_layout()::Nothing
-    dc = globvar("div_content")
-    dc = ifelse(isempty(dc), globvar("content_class"), dc)
+    dc = globvar("div_content")::String
+    dc = ifelse(isempty(dc), globvar("content_class")::String, dc)
     html = """
         <!doctype html>
         <html lang="en">
diff --git a/src/eval/run.jl b/src/eval/run.jl
index f3b48c08..bb39c81f 100644
--- a/src/eval/run.jl
+++ b/src/eval/run.jl
@@ -67,7 +67,7 @@ function run_code(mod::Module, code::AS, out_path::AS;
     stacktrace = nothing
     ispath(out_path) || mkpath(dirname(out_path))
     open(out_path, "w") do outf
-        if !FD_ENV[:SILENT_MODE]
+        if !FD_ENV[:SILENT_MODE]::Bool
             rprint("→ evaluating code [$(out_path |> basename |> splitext |> first)] in ($(locvar("fd_rpath")))")
         end
         redirect_stdout(outf) do
diff --git a/src/manager/dir_utils.jl b/src/manager/dir_utils.jl
index 08b671b2..9c7b1dd2 100644
--- a/src/manager/dir_utils.jl
+++ b/src/manager/dir_utils.jl
@@ -93,7 +93,8 @@ function form_custom_output_path(slug::String)
 end
 
 
-_access(p)   = isa(p, Regex) ? p.pattern : p
+_access(p) = p
+_access(p::Regex) = p.pattern
 _isempty(p)  = isempty(_access(p))
 _endswith(p) = endswith(_access(p), '/')
 
diff --git a/src/manager/file_utils.jl b/src/manager/file_utils.jl
index f1d60d1d..1eb6a5cb 100644
--- a/src/manager/file_utils.jl
+++ b/src/manager/file_utils.jl
@@ -16,6 +16,12 @@ function process_config()::Nothing
     else
         config_warn()
     end
+    rss_url = joinpath(
+        globvar(:website_url)::String,
+        globvar(:rss_file)::String
+    )
+    rss_url *= ".xml"
+    set_var!(GLOBAL_VARS, "fd_rss_feed_url", rss_url)
     return nothing
 end
 
@@ -89,7 +95,7 @@ $(SIGNATURES)
 See [`process_file_err`](@ref).
 """
 function process_file(case::Symbol, fpair::Pair{String,String}, args...)::Int
-    if FD_ENV[:DEBUG_MODE]
+    if FD_ENV[:DEBUG_MODE]::Bool
         process_file_err(case, fpair, args...)
         return 0
     end
@@ -99,7 +105,7 @@ function process_file(case::Symbol, fpair::Pair{String,String}, args...)::Int
     catch err
         rp = fpair.first
         rp = rp[end-min(20, length(rp))+1 : end]
-        if !FD_ENV[:QUIET_TEST]
+        if !FD_ENV[:QUIET_TEST]::Bool
             FD_ENV[:SOURCE] = fpair.second
             print_warning("""
                 Encountered an issue processing '$(fpair.second)' in $rp.
@@ -130,6 +136,18 @@ function process_file_err(case::Symbol, fpair::Pair{String, String},
     # depending on the file extension, either full process (.md), partial
     # process (.html) or no process (everything else)
     inp  = joinpath(fpair...)
+
+    # there's a bunch of things we don't want to copy over or process
+    if startswith(inp, path(:layout)) ||
+       startswith(inp, path(:literate)) ||
+       startswith(inp, path(:rss)) ||
+       endswith(inp, "config.md") ||
+       endswith(inp, "search.md") ||
+       endswith(inp, "utils.jl")
+
+       @goto end_copyblock
+   end
+
     outp = form_output_path(fpair.first, fpair.second, case)
     if case == :md
         FD_ENV[:SOURCE] = get_rpath(inp)
@@ -141,19 +159,11 @@ function process_file_err(case::Symbol, fpair::Pair{String, String},
         raw_html  = read(inp, String)
         # add the item *before* the conversion so that the conversion
         # can affect the page itself with {{...}}
-        cond_add = globvar(:generate_sitemap) && FD_ENV[:FULL_PASS]
+        cond_add = globvar(:generate_sitemap)::Bool && FD_ENV[:FULL_PASS]::Bool
         cond_add && add_sitemap_item(html=true)
         proc_html = convert_html(raw_html) |> postprocess_page
         write(outp, proc_html)
     else # case in (:other, :infra)
-        # 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
         # NOTE: some processing may be added here later on (e.g. parsing of
         # CSS files). Only copy again if necessary (file is not there or
         # has changed)
diff --git a/src/manager/franklin.jl b/src/manager/franklin.jl
index f52b1b56..9a3cf02f 100644
--- a/src/manager/franklin.jl
+++ b/src/manager/franklin.jl
@@ -46,23 +46,24 @@ Keyword arguments:
 * `show_warnings=true`: whether to show franklin  warnings
 * `launch=!single`:   whether to launch the browser when serving
 """
-function serve(; clear::Bool=false,
-                 verb::Bool=false,
-                 port::Int=8000,
-                 single::Bool=false,
-                 prerender::Bool=false,
-                 nomess::Bool=false,
-                 is_final_pass::Bool=false,
-                 no_fail_prerender::Bool=true,
-                 eval_all::Bool=false,
-                 silent::Bool=false,
-                 cleanup::Bool=true,
-                 on_write::Function=(_, _) -> nothing,
-                 log::Bool=false,
-                 host::String="127.0.0.1",
-                 show_warnings::Bool=true,
-                 launch::Bool=!single,
+function serve(; clear::Bool             = false,
+                 verb::Bool              = false,
+                 port::Int               = 8000,
+                 single::Bool            = false,
+                 prerender::Bool         = false,
+                 nomess::Bool            = false,
+                 is_final_pass::Bool     = false,
+                 no_fail_prerender::Bool = true,
+                 eval_all::Bool          = false,
+                 silent::Bool            = false,
+                 cleanup::Bool           = true,
+                 on_write::Function      = (_, _) -> nothing,
+                 log::Bool               = false,
+                 host::String            = "127.0.0.1",
+                 show_warnings::Bool     = true,
+                 launch::Bool            = !single,
                  )::Union{Nothing,Int}
+
     LOGGING[] = log
     # set the global path
     FOLDER_PATH[] = pwd()
@@ -200,7 +201,7 @@ function fd_fullpass(watched_files::NamedTuple)::Int
     prepath = get(GLOBAL_VARS, "prepath", "")
     def_GLOBAL_VARS!()
     def_GLOBAL_LXDEFS!()
-    empty!.((RSS_DICT, SITEMAP_DICT))
+    empty!.((RSS_ITEMS, SITEMAP_DICT))
     # reinsert prepath if specified
     isempty(prepath) || (GLOBAL_VARS["prepath"] = prepath)
 
@@ -210,9 +211,8 @@ function fd_fullpass(watched_files::NamedTuple)::Int
     process_config()
 
     # form page segments
-    root       = path(:folder)
-    layout     = path(:layout)
-
+    root    = path(:folder)
+    layout  = path(:layout)
     head    = read(joinpath(layout, "head.html"),      String)
     pg_foot = read(joinpath(layout, "page_foot.html"), String)
     foot    = read(joinpath(layout, "foot.html"),      String)
@@ -229,6 +229,10 @@ function fd_fullpass(watched_files::NamedTuple)::Int
             """)
     end
 
+    # if RSS is to be generated, check the template and the variables
+    # the function will not do much if generate_rss is set to false
+    prepare_for_rss()
+
     # 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.
@@ -264,14 +268,16 @@ function fd_fullpass(watched_files::NamedTuple)::Int
             s += a
         end
     end
-    # generate RSS if appropriate
-    globvar("generate_rss") && rss_generator()
-    # generate tags if appropriate
+
+    # Finalize
+    # > generate RSS if appropriate
+    globvar("generate_rss")::Bool && rss_generator()
+    # > generate tags if appropriate
     generate_tag_pages()
-    # generate sitemap if appropriate
-    globvar("generate_sitemap") && sitemap_generator()
-    # generate robots if appropriate
-    globvar("generate_robots") && robots_generator()
+    # > generate sitemap if appropriate
+    globvar("generate_sitemap")::Bool && sitemap_generator()
+    # > generate robots if appropriate
+    globvar("generate_robots")::Bool && robots_generator()
     # done
     FD_ENV[:FULL_PASS] = false
     # return -1 if any page failed to build, 0 otherwise
diff --git a/src/manager/robots_generator.jl b/src/manager/robots_generator.jl
index f44285d9..cbdc49a0 100644
--- a/src/manager/robots_generator.jl
+++ b/src/manager/robots_generator.jl
@@ -13,8 +13,7 @@ Add an entry to `DISALLOW`.
 """
 function add_disallow_item()
     loc = url_curpage()
-    loc in DISALLOW && return nothing
-    push!(DISALLOW, loc)
+    loc in DISALLOW || push!(DISALLOW, loc)
     return nothing
 end
 
@@ -27,19 +26,19 @@ function robots_generator()
     dst = joinpath(path(:site), "robots.txt")
     isfile(dst) && rm(dst)
     io = IOBuffer()
-    globvar(:generate_sitemap) && println(io, """
-        Sitemap: $(joinpath(globvar(:website_url), "sitemap.xml"))
+    globvar(:generate_sitemap)::Bool && println(io, """
+        Sitemap: $(joinpath(globvar(:website_url)::String, "sitemap.xml"))
         """)
     print(io, """
         User-agent: *
         """)
-    if !(all(isempty, (DISALLOW, globvar(:robots_disallow))))
+    if !(all(isempty, (DISALLOW, globvar(:robots_disallow)::Vector{String})))
         for page in DISALLOW
             print(io, """
                 Disallow: $page
                 """)
         end
-        for dir in globvar(:robots_disallow)
+        for dir in globvar(:robots_disallow)::Vector{String}
             if dir == "/"
                 dir = ""
             end
diff --git a/src/manager/rss_generator.jl b/src/manager/rss_generator.jl
index 920ccebd..b63ef05a 100644
--- a/src/manager/rss_generator.jl
+++ b/src/manager/rss_generator.jl
@@ -1,221 +1,140 @@
-# TODO: could also expose the channel options if someone wanted
-# to define those; can probably leave for later until feedback has
-# been received.
-
-# Specifications: RSS 2.0 -- https://cyber.harvard.edu/rss/rss.html#sampleFiles
-# steps:
-# 0. check if the relevant variables are defined otherwise don't generate the RSS
-# 1. is there an RSS file?
-#  --> remove it and create a new one (bc items may have been updated)
-# 2. go over all pages
-#  --> is there a rss var?
-#  NO  --> skip
-#  YES --> recuperate the `fd_ctime` and `fd_mtime` and add a RSS channel object
-# 3. save the file
-
+# Items are assembled by the rss_generator in a global feed and sub-feeds
+# for each of the tag. So each item is a tuple with the string of the item
+# and
 struct RSSItem
-    # -- required fields
-    title::String
-    link::String
-    description::String  # note: should not contain <p>
-    # -- optional fields
-    content::String
-    author::String       # note: should be a valid email
-    category::String
-    comments::String     # note: should be a valid URL
-    enclosure::String
-    # guid == link
-    pubDate::Date        # note: should respect RFC822 (https://www.w3.org/Protocols/rfc822/)
+    item::String
+    date::Date
+    tags::Vector{String}
 end
 
-# page_url => (RSSItem, tag vector)
-const RSS_DICT = LittleDict{String,Tuple{RSSItem,Vector{String}}}()
-
-
-"""
-    jor(a, b)
-
-Convenience function for fallback fields.
-"""
-jor(a::String, b::String) = ifelse(isempty(locvar(a)), locvar(b), locvar(a))
-
-"""
-    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>" => "")
+const RSS_ITEMS = Vector{RSSItem}()
 
-"""
-$SIGNATURES
-
-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"))
 
 """
 $SIGNATURES
 
-Create an `RSSItem` out of the provided fields defined in the page vars.
+If there's an RSS feed to generate, check that the template files are there, if not
+then get them from FranklinTemplates. Also check that a few key variables
+are defined.
 """
-function add_rss_item()
-    link  = url_curpage()
-    title = jor("rss_title", "title")
-    descr = jor("rss", "rss_description")
-
-    descr = fd2html(descr; internal=true) |> remove_html_ps
-
-    content = ""
-    if globvar(:rss_full_content)::Bool
-        raw = read(locvar(:fd_rpath)::String, String)
-        m = convert_md(raw; isinternal=true)
-        # remove all `{{}}` functions
-        m = replace(m, r"{{.*?}}" => "")
-        content = convert_html(m)
+function prepare_for_rss()::Nothing
+    # check for key variables
+    key_vars = (:website_title, :website_url, :website_description)
+    flag = any(isempty, globvar(key_var)::String for key_var in key_vars)
+
+    # XXX [April 2020, 0.10.35] if the flag is false and generate_rss is false
+    # switch it to true for backward compatibility; otherwise return
+    gen_rss = globvar(:generate_rss)::Bool
+    if flag
+        gen_rss || return nothing
+        print_warning("""
+            RSS is set to be generated but the RSS feed is improperly described:
+            at least one of the following variables have not been defined in
+            your 'config.md': 'website_title', 'website_url', 'website_description'.
+            The feed will not be (re)generated.
+            \nRelevant pointer:
+            $POINTER_PV
+            """)
+        return nothing
+    elseif !gen_rss
+        set_var!(GLOBAL_VARS, "generate_rss", true)
     end
-
-    author    = locvar(:rss_author)::String
-    category  = locvar(:rss_category)::String
-    comments  = locvar(:rss_comments)::String
-    enclosure = locvar(:rss_enclosure)::String
-
-    # Keep track of tags for tag specific feeds
-    tags = locvar(:tags)::Vector{String}
-
-    pubDate = locvar(:rss_pubdate)::Date
-    if pubDate == Date(1)
-        pubDate = locvar(:date)
-        if !isa(pubDate, Date) || pubDate == Date(1)
-            pubDate = locvar(:fd_mtime_raw)::Date
+    # check if there's an _rss folder, if there isn't generate one from
+    # template (see FranklinTemplates)
+    isdir(path(:rss)) || mkdir(path(:rss))
+    for template in ("head.xml", "item.xml")
+        dst = joinpath(path(:rss), template)
+        if !isfile(dst)
+            src = joinpath(
+                dirname(pathof(FranklinTemplates)),
+                "templates", "common", "_rss", template
+            )
+            cp(src, dst)
         end
     end
-
-    # warning for title which should really be defined
-    isnothing(title) && (title = "")
-    isempty(title)   && print_warning("""
-        An RSS description was found but without title for page '$link'.
-        """)
-
-    rss = RSSItem(title, link, descr, content, author, category, comments, enclosure, pubDate)
-
-    res = RSS_DICT[link] = (rss, tags)
-    return res
+    return nothing
 end
 
 
 """
 $SIGNATURES
 
-Extract the entries from RSS_DICT and assemble the RSS. If the dictionary is empty, nothing
-is generated.
+Extract the entries from RSS_ITEMS and assemble the RSS. If RSS_ITEMS is empty, nothing
+is generated. If tags are associated with items, also create one feed per tag.
+
+Note: if a file `head_rtag.xml` is available, where `rtag` is the refstring of a tag, it
+will take precedence for the corresponding tag feed; otherwise `head.xml` will be used.
 """
 function rss_generator()::Nothing
     # is there anything to go in the RSS feed?
-    isempty(RSS_DICT) && return nothing
-
-    # are the basic defs there? otherwise warn and break
-    rss_title = globvar("website_title")
-    rss_descr = globvar("website_descr")
-    rss_link  = globvar("website_url")
-
-    if any(isempty, (rss_title, rss_descr, rss_link))
-        print_warning("""
-            RSS items were found but the RSS feed is improperly described:
-            at least one of the following variables have not been defined in
-            your 'config.md': 'website_title', 'website_descr', 'website_url'.
-            The feed will not be (re)generated.
-            \nRelevant pointer:
-            $POINTER_PV
-            """)
-        return nothing
+    isempty(RSS_ITEMS) && return nothing
+
+    # sort items by reverse chronological order
+    sorted_items = sort(RSS_ITEMS, rev=true, by=x->x.date)
+
+    feed_name = globvar(:rss_file)::String * ".xml"
+    global_feed_path = joinpath(PATHS[:site], feed_name)
+    global_feed_head = replace(
+        read(joinpath(path(:rss), "head.xml"), String),
+        r"<!--(.|\n)*?-->" => ""
+    )
+    open(global_feed_path, "w") do io
+        write(io, convert_html(global_feed_head))
+        for item in sorted_items
+            write(io, item.item)
+        end
+        write(io, "</channel></rss>")
     end
 
-    endswith(rss_link, "/") || (rss_link *= "/")
-    rss_descr = fd2html(rss_descr; internal=true) |> remove_html_ps
-
-    # sort items by pubDate
-    RSS_DICT_SORTED = sort(OrderedDict(RSS_DICT), rev = true, byvalue = true, by = x -> x[1].pubDate)
-
-    # Global feed; include all items
-    rss_path = joinpath(PATHS[:site], "feed.xml")
-    ## Remove tags vector
-    rss_items = OrderedDict{String,RSSItem}(k => v[1] for (k, v) in RSS_DICT_SORTED)
-    ## Write the file
-    write_rss_xml(rss_path, rss_title, rss_descr, rss_link, rss_items)
-
     # Tag specific feed; filter items by tag
-    ## Collect all tags
+    # > Collect all raw tags
     tags = Set{String}()
-    foreach(x -> union!(tags, x[2]), values(RSS_DICT))
+    foreach(x -> union!(tags, x.tags), RSS_ITEMS)
+    # > write a feed per tag, use head_tag if it exists
     for tag in tags
-        rss_path = joinpath(path(:tag), refstring(tag), "feed.xml")
-        ## Filter items containing this tag only
-        rss_items = OrderedDict{String,RSSItem}(k => v[1] for (k, v) in RSS_DICT_SORTED if tag ∈ v[2])
-        ## Find the relative path for the tag-feeds
-        rss_rel = strip(globvar("tag_page_path"), '/') * "/" * refstring(tag) * "/"
-        ## Write the file
-        write_rss_xml(rss_path, rss_title, rss_descr, rss_link, rss_items, rss_rel)
+        rtag = refstring(tag)
+        tag_dir = joinpath(path(:tag), refstring(tag))
+        isdir(tag_dir) || mkpath(tag_dir)
+        tag_feed_path = joinpath(tag_dir, feed_name)
+        open(tag_feed_path, "w") do io
+            tag_feed_head = global_feed_head
+            tag_feed_head_cand = joinpath(path(:rss), "head_$rtag.xml")
+            if isfile(tag_feed_head_cand)
+                tag_feed_head = read(tag_feed_head_cand)
+            end
+            write(io, tag_feed_head)
+            for item in sorted_items
+                tag ∈ item.tags || continue
+                write(io, item.item)
+            end
+            write(io, "</channel></rss>")
+        end
     end
-
     return nothing
 end
 
-function write_rss_xml(rss_path, rss_title, rss_descr, rss_link, rss_items, rss_rel="")
-    # is there an RSS file already? if so remove it
-    isfile(rss_path) && rm(rss_path)
-    # make sure the directory exists
-    mkpath(dirname(rss_path))
-
-    # create a buffer which will correspond to the output
-    rss_buff = IOBuffer()
-    write(rss_buff,
-        """
-        <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
-        <channel>
-          <title><![CDATA[$rss_title]]></title>
-          <description><![CDATA[$(fix_relative_links(rss_descr, rss_link))]]></description>
-          <link>$rss_link</link>
-          <atom:link href="$(rss_link)$(rss_rel)feed.xml" rel="self" type="application/rss+xml" />
-        """)
-
-
-    # loop over items
-    for (k, v) in rss_items
-        full_link = rss_link * lstrip(v.link, '/')
-        full_link = replace(full_link, r"index\.html$" => "")
-        write(rss_buff,
-          """
-            <item>
-              <title><![CDATA[$(v.title)]]></title>
-              <link>$(full_link)</link>
-              <description><![CDATA[$(fix_relative_links(v.description, rss_link))<br><a href=\"$(full_link)\">Read more</a>]]></description>
-              $(ifelse(isempty(v.content),
-                "", "<content:encoded><![CDATA[$(fix_relative_links(v.content, rss_link))]]></content:encoded>"))
-          """)
-        for elem in (:author, :category, :comments, :enclosure)
-            e = getproperty(v, elem)
-            isempty(e) || write(rss_buff,
-              """
-                  <$elem>$e</$elem>
-              """)
-        end
-        write(rss_buff,
-          """
-              <guid>$(full_link)</guid>
-              <pubDate>$(Dates.format(v.pubDate, "e, d u Y")) 00:00:00 UT</pubDate>
-            </item>
-          """)
-    end
-    # finalize
-    write(rss_buff,
-        """
-        </channel>
-        </rss>
-        """)
-    write(rss_path, take!(rss_buff))
 
-    return nothing
+"""
+$SIGNATURES
+
+This is called in `convert_and_write` to add an RSS item.
+"""
+function add_rss_item()
+    item_template = replace(
+        read(joinpath(path(:rss), "item.xml"), String),
+        r"<!--(.|\n)*?-->" => ""
+    )
+    # if the rss_title is not given, infer from title
+    if isempty(locvar(:rss_title)::String)
+        set_var!(LOCAL_VARS, "rss_title", locvar(:title)::String)
+    end
+    item = replace(convert_html(item_template), r"\n\n" => "")
+    item = replace(item, Regex(raw"""<a\shref=(?:"|')?\#.*?>(.*?)</a>""") => s"\1")
+
+    rss_item = RSSItem(
+        item,
+        locvar(:rss_pubdate)::Date,
+        locvar(:tags)::Vector{String}
+    )
+    push!(RSS_ITEMS, rss_item)
 end
diff --git a/src/manager/write_page.jl b/src/manager/write_page.jl
index effba265..effea774 100644
--- a/src/manager/write_page.jl
+++ b/src/manager/write_page.jl
@@ -40,7 +40,7 @@ appropriate.
 function postprocess_page(pg)
     # Prerender if required (using JS tools)
     # order in which things are done matter a bit.
-    if FD_ENV[:PRERENDER]
+    if FD_ENV[:PRERENDER]::Bool
         # Maths (KATEX)
         if locvar(:hasmath)::Bool
             pg = js_prerender_katex(pg)
@@ -57,7 +57,7 @@ function postprocess_page(pg)
         end
     end
     # append pre-path to links if required (see optimize)
-    if FD_ENV[:FINAL_PASS]
+    if FD_ENV[:FINAL_PASS]::Bool
         pg = fix_links(pg)
     end
     return pg
@@ -77,8 +77,15 @@ function write_page(output_path::AS, content::AS;
                     )::String where T <: Union{Nothing,AS}
     slug = locvar(:slug)
     if !isempty(slug)
+        # this sets fd_url
         output_path = form_custom_output_path(slug)
     end
+    full_url = joinpath(
+        locvar(:website_url)::String,
+        strip(locvar(:fd_url)::String, '/')
+    )
+    set_var!(LOCAL_VARS, "fd_full_url", full_url)
+
     # NOTE
     #   - output_path is assumed to exist // see form_output_path
     #   - head/pgfoot/foot === nothing --> read (see franklin.jl)
@@ -94,6 +101,7 @@ function write_page(output_path::AS, content::AS;
     end
     # convert the pieces
     head, ctt, pgfoot, foot = map(convert_html, (head, content, pgfoot, foot))
+    set_var!(LOCAL_VARS, "fd_page_html", ctt)
 
     # remove dirs from past pagination attempts (so we limit risk of stale
     # pagination folder with stale files)
@@ -174,30 +182,40 @@ function convert_and_write(root::String, file::String, head::String,
     s = stat(fpath)
     set_var!(LOCAL_VARS, "fd_ctime", fd_date(unix2datetime(s.ctime)))
     mtime = unix2datetime(s.mtime)
-    set_var!(LOCAL_VARS, "fd_mtime_raw", Date(mtime))
+    mtime_date = Date(mtime)
+    set_var!(LOCAL_VARS, "fd_mtime_raw", mtime_date)
     set_var!(LOCAL_VARS, "fd_mtime", fd_date(mtime))
 
+    if locvar(:rss_pubdate)::Date == Date(1)
+        pubdate = locvar(:date)
+        if !isa(pubdate, Date) || pubdate == Date(1)
+            pubdate = mtime_date
+        end
+        set_var!(LOCAL_VARS, "rss_pubdate", pubdate)
+    end
+
+    pg = write_page(output_path, content;
+                    head=head, pgfoot=pgfoot, foot=foot)
+
     # Check if should add item
     #   should we generate ? otherwise no
     #   are we in the full pass ? otherwise no
     #   is there a `rss` or `rss_description` ? otherwise no
-    cond_add = globvar(:generate_rss) &&   # should we generate?
-                    FD_ENV[:FULL_PASS] &&  # are we in the full pass?
-                    !all(e -> isempty(locvar(e)), ("rss", "rss_description"))
+    cond_add = globvar(:generate_rss)::Bool &&  # should we generate?
+                 FD_ENV[:FULL_PASS]::Bool   &&  # are we in the full pass?
+                   !isempty(locvar(:rss_description)::String)
     # otherwise yes
     cond_add && add_rss_item()
 
     # Same for the sitemap
-    cond_add = globvar(:generate_sitemap) && FD_ENV[:FULL_PASS]
+    cond_add = globvar(:generate_sitemap) && FD_ENV[:FULL_PASS]::Bool
     cond_add && add_sitemap_item()
 
     # Same for disallow robots locally
-    cond_add = locvar(:robots_disallow_this_page)
+    cond_add = locvar(:robots_disallow_this_page)::Bool
     cond_add && add_disallow_item()
 
-    pg = write_page(output_path, content; head=head, pgfoot=pgfoot, foot=foot)
-
     # 6. possible post-processing via the "on-write" function.
-    FD_ENV[:ON_WRITE](pg, LOCAL_VARS)
+    (FD_ENV[:ON_WRITE]::Function)(pg, LOCAL_VARS)
     return nothing
 end
diff --git a/src/parser/html/blocks.jl b/src/parser/html/blocks.jl
index 550d7b0c..667792e2 100644
--- a/src/parser/html/blocks.jl
+++ b/src/parser/html/blocks.jl
@@ -79,16 +79,19 @@ const HTML_OPEN_COND_SP =
 """Blocks that can open a conditional block."""
 const HTML_OPEN_COND = Union{HIf, HTML_OPEN_COND_SP}
 
+"""Blocks that can open another block."""
+const HTML_OPEN_OTHER = HFor
+
 """
 $SIGNATURES
 
 Internal function to balance conditional blocks. See [`process_html_qblocks`](@ref).
 """
-hbalance(::HTML_OPEN_COND) = 1
+hbalance(::Union{HTML_OPEN_COND, HTML_OPEN_OTHER}) = 1
 hbalance(::HEnd) = -1
 hbalance(::AbstractBlock) = 0
 
 function split_hfun_parameters(s)
     matches = [e.match for e in eachmatch(HFUN_PARAMS_PAT, s; overlap=false)]
     return strip.(matches, '\"')
-end
\ No newline at end of file
+end
diff --git a/src/utils/paths.jl b/src/utils/paths.jl
index bf10f4e2..753ef605 100644
--- a/src/utils/paths.jl
+++ b/src/utils/paths.jl
@@ -53,6 +53,7 @@ function set_paths!()::LittleDict{Symbol,String}
     PATHS[:css]      = joinpath(PATHS[:folder], "_css")      # mandatory
     PATHS[:layout]   = joinpath(PATHS[:folder], "_layout")   # mandatory
     PATHS[:libs]     = joinpath(PATHS[:folder], "_libs")     # mandatory
+    PATHS[:rss]      = joinpath(PATHS[:folder], "_rss")      # optional/generated
     PATHS[:literate] = joinpath(PATHS[:folder], "_literate") # optional
     tpp = globvar("tag_page_path")
     if !isnothing(tpp)
diff --git a/src/utils/vars.jl b/src/utils/vars.jl
index f0ffbc17..3fb718e8 100644
--- a/src/utils/vars.jl
+++ b/src/utils/vars.jl
@@ -30,6 +30,7 @@ const GLOBAL_VARS_DEFAULT = [
     "date_shortdays"   => dpair(String[]),
     "prepath"          => dpair(""),
     "tag_page_path"    => dpair("tag"),
+    "title_links"      => dpair(true),
     # will be added to IGNORE_FILES
     "ignore"           => Pair(String[], (Vector{Any},)),
     # don't insert `index.html` at the end of the path for these files
@@ -37,13 +38,15 @@ const GLOBAL_VARS_DEFAULT = [
     # for robots.txt
     "robots_disallow"  => Pair(String[], (Vector{String},)),
     "generate_robots"  => dpair(true),
-    # RSS + sitemap
-    "website_title"    => dpair(""),
-    "website_descr"    => dpair(""),
-    "website_url"      => dpair(""),
-    "generate_rss"     => dpair(true),
+    # RSS
+    "generate_rss"        => dpair(false),
+    "website_title"       => dpair(""),
+    "website_url"         => dpair(""),
+    "website_description" => dpair(""),
+    "rss_file"            => dpair("feed"),
+    "rss_full_content"    => dpair(false),
+    # Sitemap
     "generate_sitemap" => dpair(true),
-    "rss_full_content" => dpair(false),
     # div names
     "content_tag"      => dpair("div"),
     "content_class"    => dpair("franklin-content"),
@@ -54,15 +57,20 @@ const GLOBAL_VARS_DEFAULT = [
     # keep track page=>tags and tag=>pages
     "fd_page_tags"     => Pair(nothing, (DTAG,  Nothing)),
     "fd_tag_pages"     => Pair(nothing, (DTAGI, Nothing)),
+    "fd_rss_feed_url"  => dpair(""),
     # -----------------------------------------------------
     # LEGACY
     "div_content" => dpair(""), # see build_page
     ]
 
 const GLOBAL_VARS_ALIASES = LittleDict(
-    "prefix"    => "prepath",
-    "base_path" => "prepath",
-    "base_url"  => "website_url",
+    "prefix"            => "prepath",
+    "base_path"         => "prepath",
+    "base_url"          => "website_url",
+    "rss_website_title" => "website_title",
+    "rss_website_url"   => "website_url",
+    "rss_website_descr" => "website_description",
+    "website_descr"     => "website_description",
     )
 
 """
@@ -108,7 +116,6 @@ const LOCAL_VARS_DEFAULT = [
     "header_anchor_class" => dpair("header-anchor"),
     # ------------------
     # RSS 2.0 specs [^2]
-    "rss"             => dpair(""),
     "rss_description" => dpair(""),
     "rss_title"       => dpair(""),
     "rss_author"      => dpair(""),
@@ -129,10 +136,12 @@ const LOCAL_VARS_DEFAULT = [
     "fd_mtime_raw" => dpair(Date(1)),
     "fd_ctime"     => dpair("0001-01-01"),  # time of creation
     "fd_mtime"     => dpair("0001-01-01"),  # time of last modification
-    "fd_rpath"     => dpair(""),            # rpath to current page [1]
-    "fd_url"       => dpair(""),            # url to current page [2]
+    "fd_rpath"     => dpair(""),            # relative path to current page [1]
+    "fd_url"       => dpair(""),            # relative url to current page [2]
+    "fd_full_url"  => dpair(""),            # full url to current page [3]
     "fd_tag"       => dpair(""),            # (generated) current tag
     "fd_evalc"     => dpair(1),             # counter for direct evaluation cells (3! blocks)
+    "fd_page_html" => dpair(""),            # the generated html for the page
     ]
 #=
 NOTE:
@@ -149,9 +158,16 @@ NOTE:
     (*) source      -- [unsupported assumes for now there's only one channel]
 
 [1] e.g.: blog/kaggle.md
-[2] e.g.: blog/kaggle/index.html
+[2] e.g.: /blog/kaggle/index.html
+[3] e.g.: https://username.github.io/project/blog/kaggle/index.html
 =#
 
+const LOCAL_VARS_ALIASES = LittleDict(
+    "rss_descr" => "rss_description",
+    "rss"       => "rss_description"
+    )
+
+
 """
 Re-initialise the local page vars dictionary. (This is done for every page).
 Note that a global default overrides a local default. So for instance if
@@ -375,12 +391,33 @@ Take a var dictionary `dict` and update the corresponding pair. This should
 only be used internally as it does not check the validity of `val`. See
 [`convert_and_write`](@ref) where it is used to store a file's creation and last
 modification time.
+
+Note: `check` can be false for DTAG, DTAGI which are considered as UnionAll types.
 """
-function set_var!(d::PageVars, k::K, v) where K
-    if k in keys(d)
-        d[k] = Pair(v, d[k].second)
+function set_var!(vars::PageVars, key::String, value::T;
+                  isglobal=false, check=true) where T
+    exists = haskey(vars, key)
+    # aliases are allowed for some global variables
+    if !exists
+        if isglobal && haskey(GLOBAL_VARS_ALIASES, key)
+            exists = true
+            key = GLOBAL_VARS_ALIASES[key]
+        elseif !isglobal && haskey(LOCAL_VARS_ALIASES, key)
+            exists = true
+            key = LOCAL_VARS_ALIASES[key]
+        end
+    end
+    if exists && check
+        # if the retrieved value has the right type, assign it to the corresponding key
+        acc_types = vars[key].second
+        if check_type(T, acc_types)
+            vars[key] = Pair(value, acc_types)
+        else
+            mddef_warn(key, value, acc_types)
+        end
     else
-        d[k] = Pair(v, (typeof(v), ))
+        # there is no key, so directly assign, the type is not checked
+        vars[key] = Pair(value, (T,))
     end
     return
 end
@@ -412,34 +449,16 @@ function set_vars!(vars::PageVars, assignments::Vector{Pair{String,String}};
         # this in a string it would fail (but come on...)
         idx = findfirst("<!--", assign)
         !isnothing(idx) && (assign = assign[1:prevind(assign, idx[1])])
-        tmp, = Meta.parse(assign, 1)
+        value, = Meta.parse(assign, 1)
         # try to evaluate the parsed assignment
         try
-            tmp = eval(tmp)
+            value = eval(value)
         catch err
             throw(PageVariableError(
                 "An error (of type '$(typeof(err))') occurred when trying " *
-                "to evaluate '$tmp' in a page variable assignment."))
-        end
-        exists = haskey(vars, key)
-        # aliases are allowed for some global variables
-        if !exists && isglobal && haskey(GLOBAL_VARS_ALIASES, key)
-            exists = true
-            key = GLOBAL_VARS_ALIASES[key]
-        end
-        if exists
-            # if the retrieved value has the right type, assign it to the corresponding key
-            type_tmp  = typeof(tmp)
-            acc_types = vars[key].second
-            if check_type(type_tmp, acc_types)
-                vars[key] = Pair(tmp, acc_types)
-            else
-                mddef_warn(key, tmp, acc_types)
-            end
-        else
-            # there is no key, so directly assign, the type is not checked
-            vars[key] = Pair(tmp, (typeof(tmp), ))
+                "to evaluate '$value' in a page variable assignment."))
         end
+        set_var!(vars, key, value; isglobal=isglobal)
     end
     return vars
 end


Test patch:
diff --git a/test/converter/html/html_for.jl b/test/converter/html/html_for.jl
index d9291d46..130582e0 100644
--- a/test/converter/html/html_for.jl
+++ b/test/converter/html/html_for.jl
@@ -83,6 +83,45 @@ end
         """)
 end
 
+@testset "for nested" begin
+    s = """
+        @def iter1 = ("a"=>0, "b"=>1)
+        @def iter2 = ("c"=>2, "d"=>3)
+        """ |> fd2html_td
+    h = raw"""
+        A
+        {{for (n1, v1) in iter1}}
+            {{for (n2, v2) in iter2}}
+            {{end}}
+        {{end}}
+        """ |> F.convert_html
+    @test isapproxstr(h, "A")
+    h = raw"""
+        A
+        {{for (n1, v1) in iter1}}
+            {{for (n2, v2) in iter2}}
+            B
+            {{end}}
+        {{end}}
+        """ |> F.convert_html
+    @test isapproxstr(h, "ABBBB")
+    h = raw"""
+        A
+        {{for (n1, v1) in iter1}}
+            {{for (n2, v2) in iter2}}
+            {{n1}},{{v1}},{{n2}},{{v2}}
+            {{end}}
+        {{end}}
+        """ |> F.convert_html
+    @test isapproxstr(h, """
+        A
+        a,0,c,2
+        a,0,d,3
+        b,1,c,2
+        b,1,d,3
+        """)
+end
+
 
 # read from file, see FranklinFAQ-001
 @testset "for-file" begin
diff --git a/test/converter/html/html_functions.jl b/test/converter/html/html_functions.jl
new file mode 100644
index 00000000..6c8cc597
--- /dev/null
+++ b/test/converter/html/html_functions.jl
@@ -0,0 +1,20 @@
+@testset "fill" begin
+    s = """
+        @def x = 5
+        {{fill x}}
+        """ |> fd2html_td
+    @test isapproxstr(s, "5")
+    @test_throws F.HTMLFunctionError ("{{fill x y z}}" |> fd2html)
+end
+
+@testset "href" begin
+    s = "{{href EQR foo}}" |> fd2html_td
+    @test isapproxstr(s, "<b>??</b>")
+end
+
+@testset "redirect" begin
+    # only one arg
+    @test_throws F.HTMLFunctionError ("{{redirect foo bar}}" |> fd2html)
+    # must end with html
+    @test_throws F.HTMLFunctionError ("{{redirect foo}}" |> fd2html)
+end
diff --git a/test/converter/md/tags.jl b/test/converter/md/tags.jl
index 21596f9a..24bf23ea 100644
--- a/test/converter/md/tags.jl
+++ b/test/converter/md/tags.jl
@@ -11,7 +11,7 @@
     F.def_LOCAL_VARS!()
     write("pg1.md", "@def title = \"Page1\"")
     write("pg2.md", "@def title = \"Page2\"")
-    F.set_var!(F.GLOBAL_VARS, "fd_page_tags", F.DTAG(("pg1" => Set(["aa", "bb"]),)))
+    F.set_var!(F.GLOBAL_VARS, "fd_page_tags", F.DTAG(("pg1" => Set(["aa", "bb"]),)), check=false)
     F.globvar("fd_page_tags")["pg2"] = Set(["bb", "cc"])
 
     @test Set(keys(F.globvar("fd_page_tags"))) == Set(["pg1", "pg2"])
diff --git a/test/coverage/extras1.jl b/test/coverage/extras1.jl
index c468cfaf..a4c03bb6 100644
--- a/test/coverage/extras1.jl
+++ b/test/coverage/extras1.jl
@@ -45,17 +45,16 @@ end
     cd(td)
 end
 
-@testset "RSS" begin
-    F.set_var!(F.GLOBAL_VARS, "website_descr", "")
-    F.RSS_DICT["hello"] = (F.RSSItem("","","","","","","","",Date(1)), String[])
-    global r = ""; s = @capture_out begin
-        global r
-        r = F.rss_generator()
-    end
-    @test r === nothing
-    @test occursin("RSS items were found but", s)
-end
-
+# @testset "RSS" begin
+#     F.set_var!(F.GLOBAL_VARS, "website_descr", "")
+#     F.RSS_DICT["hello"] = (F.RSSItem("","","","","","","","",Date(1)), String[])
+#     global r = ""; s = @capture_out begin
+#         global r
+#         r = F.rss_generator()
+#     end
+#     @test r === nothing
+#     @test occursin("RSS items were found but", s)
+# end
 
 @testset "parser-lx" begin
     s = raw"""
diff --git a/test/global/rss_sitemap.jl b/test/global/sitemap.jl
similarity index 51%
rename from test/global/rss_sitemap.jl
rename to test/global/sitemap.jl
index 8edc4e42..54b828f8 100644
--- a/test/global/rss_sitemap.jl
+++ b/test/global/sitemap.jl
@@ -1,18 +1,3 @@
-# simple test that things get created
-@testset "RSS gen" begin
-    f = joinpath(p, "basic", "__site", "feed.xml")
-    @test isfile(f)
-    fc = prod(readlines(f, keep=true))
-
-    @test occursin(raw"""<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">""", fc)
-    @test occursin(raw"""<title><![CDATA[Franklin Template]]></title>""", fc)
-    @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/menu1/</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
-
 @testset "Sitemap gen" begin
     f = joinpath(p, "basic", "__site", "sitemap.xml")
     @test isfile(f)
diff --git a/test/manager/dir_utils.jl b/test/manager/dir_utils.jl
index 89b1db08..42188f19 100644
--- a/test/manager/dir_utils.jl
+++ b/test/manager/dir_utils.jl
@@ -22,3 +22,23 @@ fs()
     @test length(watched.md) == 1
     @test first(watched.md).first.second == "index.md"
 end
+
+@testset "coverage" begin
+    # form custom output path
+    F.def_LOCAL_VARS!()
+    path = F.form_custom_output_path("aa/bb")
+    @test endswith(F.locvar(:fd_url), "aa/bb/index.html")
+    @test isdir(dirname(path))
+
+    # helper functions around regexes
+    r1 = r"foo/bar"
+    r2 = "foo/bar"
+    @test F._access(r1) == r1.pattern
+    @test !F._isempty(r1)
+    @test !F._isempty(r2)
+    @test F._isempty(r"")
+    @test F._isempty("")
+    @test !F._endswith(r1)
+    @test F._endswith("foo/bar/")
+    @test F._endswith(r"foo/bar/")
+end
diff --git a/test/manager/robots_generator.jl b/test/manager/robots_generator.jl
new file mode 100644
index 00000000..8afa6c6d
--- /dev/null
+++ b/test/manager/robots_generator.jl
@@ -0,0 +1,5 @@
+@testset "robots" begin
+    F.def_LOCAL_VARS!()
+    F.add_disallow_item()
+    @test F.url_curpage() in F.DISALLOW
+end
diff --git a/test/manager/rss.jl b/test/manager/rss.jl
deleted file mode 100644
index fc2ffe24..00000000
--- a/test/manager/rss.jl
+++ /dev/null
@@ -1,110 +0,0 @@
-@testset "RSSItem" begin
-    rss = F.RSSItem(
-        "title", "www.link.com", "description", "", "author@author.com", "category",
-        "www.comments.com", "enclosure", Date(2012,12,12))
-    @test rss.title == "title"
-    @test rss.link == "www.link.com"
-    @test rss.description == "description"
-    @test rss.author == "author@author.com"
-    @test rss.category == "category"
-    @test rss.comments == "www.comments.com"
-    @test rss.enclosure == "enclosure"
-    @test rss.pubDate == Date(2012,12,12)
-end
-
-@testset "RSSbasics" begin
-    fs()
-    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/")
-
-    # Page 1 with tags = ["foo", "t.a g"]
-    F.def_LOCAL_VARS!()
-    set_curpath("hey/hello.md")
-    F.set_var!(F.LOCAL_VARS, "rss_title", "title 1")
-    F.set_var!(F.LOCAL_VARS, "rss", "Page with tag foo.")
-    F.set_var!(F.LOCAL_VARS, "rss_pubdate", Date(2020, 10, 27))
-    F.set_var!(F.LOCAL_VARS, "tags", ["foo", "t.a g"])
-
-    item, tags = F.add_rss_item()
-    @test item.title == "title 1"
-    @test item.description == "Page with tag foo.\n"
-    @test item.author == ""
-    @test item.pubDate == Date(2020, 10, 27)
-    @test tags == ["foo", "t.a g"]
-    @test F.RSS_DICT["/hey/hello/index.html"][1].description == item.description
-    @test F.RSS_DICT["/hey/hello/index.html"][2] == ["foo", "t.a g"]
-
-    # Page 2 with tags = ["foo", "bar"]
-    F.def_LOCAL_VARS!()
-    set_curpath("hey/ho.md")
-    F.set_var!(F.LOCAL_VARS, "rss_title", "title 2")
-    F.set_var!(F.LOCAL_VARS, "rss", "A **description** done. Page with tags foo and bar.")
-    F.set_var!(F.LOCAL_VARS, "rss_pubdate", Date(2020, 10, 30))
-    F.set_var!(F.LOCAL_VARS, "rss_author", "chuck@norris.com")
-    F.set_var!(F.LOCAL_VARS, "tags", ["foo", "bar"])
-
-    item, tags = F.add_rss_item()
-    @test item.title == "title 2"
-    @test item.description == "A <strong>description</strong> done. Page with tags foo and bar.\n"
-    @test item.author == "chuck@norris.com"
-    @test item.pubDate == Date(2020, 10, 30)
-    @test tags == ["foo", "bar"]
-    @test F.RSS_DICT["/hey/ho/index.html"][1].description == item.description
-    @test F.RSS_DICT["/hey/ho/index.html"][2] == ["foo", "bar"]
-
-    # Generation
-    F.PATHS[:folder] = td
-    F.rss_generator()
-    ## Global feed
-    feed = joinpath(F.PATHS[:site], "feed.xml")
-    @test isfile(feed)
-    fc = read(feed, String)
-    @test occursin("<title><![CDATA[title 2]]></title>", fc)
-    @test occursin("<title><![CDATA[title 1]]></title>", fc)
-    @test occursin("<description><![CDATA[A <strong>description</strong> done. Page with tags foo and bar.", fc)
-    @test occursin("<description><![CDATA[Page with tag foo.", fc)
-    @test occursin("<link>https://github.com/tlienart/Franklin.jl/hey/ho/</link>", fc)
-    @test occursin("<link>https://github.com/tlienart/Franklin.jl/hey/hello/</link>", fc)
-    @test occursin("<guid>https://github.com/tlienart/Franklin.jl/hey/ho/</guid>", fc)
-    @test occursin("<guid>https://github.com/tlienart/Franklin.jl/hey/hello/</guid>", fc)
-    @test occursin("<br><a href=\"https://github.com/tlienart/Franklin.jl/hey/ho/\">Read more</a>", fc)
-    @test occursin("<br><a href=\"https://github.com/tlienart/Franklin.jl/hey/hello/\">Read more</a>", fc)
-    @test occursin("<pubDate>Fri, 30 Oct 2020 00:00:00 UT</pubDate>", fc)
-    @test occursin("<pubDate>Tue, 27 Oct 2020 00:00:00 UT</pubDate>", fc)
-    @test occursin("<atom:link href=\"https://github.com/tlienart/Franklin.jl/feed.xml\" rel=\"self\" type=\"application/rss+xml\" />", fc)
-    @test findfirst("Fri, 30 Oct 2020", fc) < findfirst("27 Oct 2020", fc) # ordered by pubDate
-
-    ## Tag filtered feeds
-    ### foo tag
-    feed = joinpath(F.PATHS[:tag], "foo", "feed.xml")
-    @test isfile(feed)
-    foo_feed = read(feed, String)
-    @test occursin("<title><![CDATA[title 2]]></title>", foo_feed)
-    @test occursin("<title><![CDATA[title 1]]></title>", foo_feed)
-    @test occursin("<description><![CDATA[A <strong>description</strong> done. Page with tags foo and bar.", foo_feed)
-    @test occursin("<description><![CDATA[Page with tag foo.", foo_feed)
-    @test occursin("<pubDate>Fri, 30 Oct 2020 00:00:00 UT</pubDate>", foo_feed)
-    @test occursin("<pubDate>Tue, 27 Oct 2020 00:00:00 UT</pubDate>", foo_feed)
-    @test occursin("<atom:link href=\"https://github.com/tlienart/Franklin.jl/tag/foo/feed.xml\" rel=\"self\" type=\"application/rss+xml\" />", foo_feed)
-    @test findfirst("Fri, 30 Oct 2020", foo_feed) < findfirst("27 Oct 2020", foo_feed) # ordered by pubDate
-    ### bar tag
-    feed = joinpath(F.PATHS[:tag], "bar", "feed.xml")
-    @test isfile(feed)
-    bar_feed = read(feed, String)
-    @test occursin("<title><![CDATA[title 2]]></title>", bar_feed)
-    @test !occursin("<title><![CDATA[title 1]]></title>", bar_feed)
-    @test occursin("<description><![CDATA[A <strong>description</strong> done. Page with tags foo and bar.", bar_feed)
-    @test !occursin("<description><![CDATA[Page with tag foo.", bar_feed)
-    @test occursin("<pubDate>Fri, 30 Oct 2020 00:00:00 UT</pubDate>", bar_feed)
-    @test !occursin("<pubDate>Tue, 27 Oct 2020 00:00:00 UT</pubDate>", bar_feed)
-    @test occursin("<atom:link href=\"https://github.com/tlienart/Franklin.jl/tag/bar/feed.xml\" rel=\"self\" type=\"application/rss+xml\" />", bar_feed)
-
-    ### t.a g tag (check handling of special characters)
-    feed = joinpath(F.PATHS[:tag], "ta_g", "feed.xml")
-    @test isfile(feed)
-    tag_feed = read(feed, String)
-    @test occursin("<atom:link href=\"https://github.com/tlienart/Franklin.jl/tag/ta_g/feed.xml\" rel=\"self\" type=\"application/rss+xml\" />", tag_feed)
-end
diff --git a/test/manager/rss_generator.jl b/test/manager/rss_generator.jl
new file mode 100644
index 00000000..7b117e37
--- /dev/null
+++ b/test/manager/rss_generator.jl
@@ -0,0 +1,13 @@
+@testset "rss_prep" begin
+    F.def_GLOBAL_VARS!()
+    F.set_var!(F.GLOBAL_VARS, "generate_rss", false)
+    F.set_var!(F.GLOBAL_VARS, "website_description", "Description")
+    F.set_var!(F.GLOBAL_VARS, "website_url", "URL")
+    F.set_var!(F.GLOBAL_VARS, "website_title", "Title")
+    F.prepare_for_rss()
+    # because things are defined, switch to true
+    @test F.globvar(:generate_rss)
+    @test isdir(F.path(:rss))
+    @test isfile(joinpath(F.path(:rss), "head.xml"))
+    @test isfile(joinpath(F.path(:rss), "item.xml"))
+end
diff --git a/test/runtests.jl b/test/runtests.jl
index 8d83bdc2..58c28715 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -1,9 +1,11 @@
-using Franklin, Test, Markdown, Dates, Random
+using Franklin, Test, Markdown, Dates, Random, Pkg
 using Literate, DelimitedFiles, Suppressor
 const F = Franklin
 const R = @__DIR__
 const D = joinpath(dirname(dirname(pathof(Franklin))), "test", "_dummies")
 
+INIT_DIR = pwd()
+
 F.FD_ENV[:SILENT_MODE] = true
 # F.FD_ENV[:DEBUG_MODE] = true
 
@@ -25,11 +27,12 @@ println("🍺")
 # MANAGER folder
 println("MANAGER")
 include("manager/utils.jl")
-include("manager/rss.jl")
 include("manager/config.jl")
 include("manager/dir_utils.jl")
 include("manager/page_vars_html.jl")
 include("manager/paginate.jl")
+include("manager/robots_generator.jl")
+include("manager/rss_generator.jl")
 println("🍺")
 
 # PARSER folder
@@ -77,6 +80,7 @@ println("CONVERTER/HTML")
 include("converter/html/html.jl")
 include("converter/html/html2.jl")
 include("converter/html/html_for.jl")
+include("converter/html/html_functions.jl")
 println("🍺")
 println("CONVERTER/LX")
 include("converter/lx/input.jl")
@@ -101,8 +105,8 @@ begin
     # make dir, go in it, do the tests, then get completely out (otherwise
     # windows can't delete the folder)
     mkdir(p); cd(p);
-    include("global/postprocess.jl");
-    include("global/rss_sitemap.jl")
+    include("global/postprocess.jl")
+    include("global/sitemap.jl")
     cd(p)
     include("global/eval.jl")
     cd(joinpath(D, ".."))
@@ -142,3 +146,7 @@ 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
+
+
+Pkg.activate()
+cd(INIT_DIR)
diff --git a/test/test_utils.jl b/test/test_utils.jl
index c610208d..630df831 100644
--- a/test/test_utils.jl
+++ b/test/test_utils.jl
@@ -51,7 +51,7 @@ end
 function explore_md_steps(mds)
     F.def_GLOBAL_VARS!()
     F.def_GLOBAL_LXDEFS!()
-    empty!(F.RSS_DICT)
+    empty!(F.RSS_ITEMS)
 
     F.def_LOCAL_VARS!()
     F.def_PAGE_HEADERS!()
diff --git a/test/utils/folder_structure.jl b/test/utils/folder_structure.jl
index e2015146..04b95b62 100644
--- a/test/utils/folder_structure.jl
+++ b/test/utils/folder_structure.jl
@@ -2,7 +2,7 @@
     F.def_GLOBAL_VARS!()
     root = F.FOLDER_PATH[] = mktempdir()
     empty!(F.PATHS); F.set_paths!()
-    @test Set(keys(F.PATHS)) == Set([:folder, :assets, :css, :layout, :libs, :literate, :site, :tag])
+    @test Set(keys(F.PATHS)) == Set([:folder, :assets, :css, :layout, :libs, :literate, :site, :tag, :rss])
     @test F.PATHS[:folder]   == root
     @test F.PATHS[:site]     == joinpath(root, "__site")
     @test F.PATHS[:assets]   == joinpath(root, "_assets")
@@ -10,6 +10,7 @@
     @test F.PATHS[:layout]   == joinpath(root, "_layout")
     @test F.PATHS[:libs]     == joinpath(root, "_libs")
     @test F.PATHS[:literate] == joinpath(root, "_literate")
+    @test F.PATHS[:rss]      == joinpath(root, "_rss")
     @test F.PATHS[:tag]      == joinpath(root, "__site", "tag")
 end
 
diff --git a/test/utils/html.jl b/test/utils/html.jl
index fd5e6960..ae2a87b6 100644
--- a/test/utils/html.jl
+++ b/test/utils/html.jl
@@ -52,3 +52,17 @@ end
     h = F.html_content("div", "foo bar"; class="container", id="body")
     @test h == "<div class=\"container\" id=\"body\">foo bar</div>"
 end
+
+@testset "fd2text" begin
+    h = F.fd2text(
+        """
+        A **B** `C` [D](https://julialang.org)
+        - E
+        - F
+        G < H
+        """
+    )
+    @test isapproxstr(h, """
+        A B C D E F G &lt; H
+        """)
+end
diff --git a/test/utils/warnings.jl b/test/utils/warnings.jl
index 3c3796b5..818a9257 100644
--- a/test/utils/warnings.jl
+++ b/test/utils/warnings.jl
@@ -16,21 +16,6 @@ end
     @test occursin("No 'config.md' file found", s)
 end
 
-@testset "warn-rss" begin
-    resource(); gotd()
-    empty!(F.RSS_DICT)
-    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", "")
-    s = @capture_out F.add_rss_item()
-    @test occursin("Warning: in <unknown>", s)
-    @test occursin("An RSS description was found but without title", s)
-    @test occursin("for page '/hey/ho/index.html'.", s)
-end
-
 @testset "warn-paginate" begin
     resource(); gotd()
     write(joinpath(td, "config.md"), "@def aa = 5")