Repository: mkdocs/mkdocs

Issue:
Feature Request: `on_cleanup` event type
Hello, 

I'm working on a plugin that creates temporary files that should be cleaned up at the end. 
i'm having an issue caused by `on_post_build` firing multiple times (as it should) during `mkdocs serve`. 
I'd like to propose an `on_cleanup` event type that runs once and only once at the end of either `mkdocs build` or `mkdocs serve`.

i'm happy to take a shot at implementing this first but would like some feedback on the idea before i try. 

<details><summary>alternatives considered</summary>

If i was able to determine the cli command that was executed (i.e., is this a `build` or a `serve`) within my implementation of `BasePlugin` then i'd have a suitable workaround - but i'm not seeing a way to cleanly determine this information. 

</details>

Discussion:

Patch:
diff --git a/docs/dev-guide/plugins.md b/docs/dev-guide/plugins.md
index 2d692fc6..f71f0dc7 100644
--- a/docs/dev-guide/plugins.md
+++ b/docs/dev-guide/plugins.md
@@ -157,11 +157,23 @@ There are three kinds of events: [Global Events], [Page Events] and
 </details>
 <br>
 
-#### Global Events
+#### One-time Events
 
-Global events are called once per build at either the beginning or end of the
-build process. Any changes made in these events will have a global effect on the
-entire site.
+One-time events run once per `mkdocs` invocation. The only case where these tangibly differ from [global events](#global-events) is for `mkdocs serve`: global events, unlike these, will run multiple times -- once per *build*.
+
+##### on_startup
+
+::: mkdocs.plugins.BasePlugin.on_startup
+    options:
+        show_root_heading: false
+        show_root_toc_entry: false
+
+##### on_shutdown
+
+::: mkdocs.plugins.BasePlugin.on_shutdown
+    options:
+        show_root_heading: false
+        show_root_toc_entry: false
 
 ##### on_serve
 
@@ -170,6 +182,12 @@ entire site.
         show_root_heading: false
         show_root_toc_entry: false
 
+#### Global Events
+
+Global events are called once per build at either the beginning or end of the
+build process. Any changes made in these events will have a global effect on the
+entire site.
+
 ##### on_config
 
 ::: mkdocs.plugins.BasePlugin.on_config
diff --git a/docs/img/plugin-events.py b/docs/img/plugin-events.py
index 86fc1484..0ad1f982 100644
--- a/docs/img/plugin-events.py
+++ b/docs/img/plugin-events.py
@@ -7,11 +7,11 @@ import re
 
 from graphviz import Digraph
 
-g = Digraph("MkDocs", format="svg")
-g.attr(compound="true", bgcolor="transparent")
-g.graph_attr.update(fontname="inherit", tooltip=" ")
-g.node_attr.update(fontname="inherit", tooltip=" ", style="filled")
-g.edge_attr.update(fontname="inherit", tooltip=" ")
+graph = Digraph("MkDocs", format="svg")
+graph.attr(compound="true", bgcolor="transparent")
+graph.graph_attr.update(fontname="inherit", tooltip=" ")
+graph.node_attr.update(fontname="inherit", tooltip=" ", style="filled")
+graph.edge_attr.update(fontname="inherit", tooltip=" ")
 
 
 def strip_suffix(name):
@@ -60,7 +60,7 @@ def edge(g, a, b, dashed=False, **kwargs):
 
 
 def ensure_order(a, b):
-    edge(g, a, b, style="invis")
+    edge(graph, a, b, style="invis")
 
 
 @contextlib.contextmanager
@@ -79,7 +79,7 @@ def event(g, name, parameters):
         g, f"cluster_{name}", href=f"#{name}", bgcolor="#ffff3388", pencolor="#00000088"
     ) as c:
         label = "|".join(f"<{p}>{p}" for p in parameters.split())
-        node(c, name, shape="record", label=label, fillcolor="#ffffff55")
+        node(c, name, shape="record" if parameters else "point", label=label, fillcolor="#ffffff55")
 
 
 def placeholder_cluster(g, name):
@@ -87,72 +87,74 @@ def placeholder_cluster(g, name):
         node(c, f"placeholder_{name}", label="...", fillcolor="transparent", color="transparent")
 
 
-event(g, "on_config", "config")
-event(g, "on_pre_build", "config")
-event(g, "on_files", "files config")
-event(g, "on_nav", "nav config files")
+event(graph, "on_startup", "command dirty")
 
-edge(g, "load_config", "on_config:config")
-edge(g, "on_config:config", "on_pre_build:config")
-edge(g, "on_config:config", "get_files")
-edge(g, "get_files", "on_files:files")
-edge(g, "on_files:files", "get_nav")
-edge(g, "get_nav", "on_nav:nav")
-edge(g, "on_files:files", "on_nav:files")
+with cluster(graph, "cluster_build", bgcolor="#dddddd11") as g:
+    event(g, "on_config", "config")
+    event(g, "on_pre_build", "config")
+    event(g, "on_files", "files config")
+    event(g, "on_nav", "nav config files")
 
-with cluster(g, "cluster_populate_page") as c:
-    event(c, "on_pre_page", "page config files")
-    event(c, "on_page_read_source", "page config")
-    event(c, "on_page_markdown", "markdown page config files")
-    event(c, "on_page_content", "html page config files")
+    edge(g, "load_config", "on_config:config")
+    edge(g, "on_config:config", "on_pre_build:config")
+    edge(g, "on_config:config", "get_files")
+    edge(g, "get_files", "on_files:files")
+    edge(g, "on_files:files", "get_nav")
+    edge(g, "get_nav", "on_nav:nav")
+    edge(g, "on_files:files", "on_nav:files")
 
-    edge(c, "on_pre_page:page", "on_page_read_source:page", style="dashed")
-    edge(c, "cluster_on_page_read_source", "on_page_markdown:markdown", style="dashed")
-    edge(c, "on_page_markdown:markdown", "render_p", style="dashed")
-    edge(c, "render_p", "on_page_content:html", style="dashed")
+    with cluster(g, "cluster_populate_page") as c:
+        event(c, "on_pre_page", "page config files")
+        event(c, "on_page_read_source", "page config")
+        event(c, "on_page_markdown", "markdown page config files")
+        event(c, "on_page_content", "html page config files")
 
-edge(g, "on_nav:files", "pages_point_a", arrowhead="none")
-edge(g, "pages_point_a", "on_pre_page:page", style="dashed")
-edge(g, "pages_point_a", "cluster_populate_page")
+        edge(c, "on_pre_page:page", "on_page_read_source:page", style="dashed")
+        edge(c, "cluster_on_page_read_source", "on_page_markdown:markdown", style="dashed")
+        edge(c, "on_page_markdown:markdown", "render_p", style="dashed")
+        edge(c, "render_p", "on_page_content:html", style="dashed")
 
-for i in 2, 3:
-    placeholder_cluster(g, f"cluster_populate_page_{i}")
-    edge(g, "pages_point_a", f"cluster_populate_page_{i}", style="dashed")
-    edge(g, f"cluster_populate_page_{i}", "pages_point_b", style="dashed")
+    edge(g, "on_nav:files", "pages_point_a", arrowhead="none")
+    edge(g, "pages_point_a", "on_pre_page:page", style="dashed")
+    edge(g, "pages_point_a", "cluster_populate_page")
 
+    for i in 2, 3:
+        placeholder_cluster(g, f"cluster_populate_page_{i}")
+        edge(g, "pages_point_a", f"cluster_populate_page_{i}", style="dashed")
+        edge(g, f"cluster_populate_page_{i}", "pages_point_b", style="dashed")
 
-event(g, "on_env", "env config files")
+    event(g, "on_env", "env config files")
 
-edge(g, "on_page_content:html", "pages_point_b", style="dashed")
-edge(g, "pages_point_b", "on_env:files")
+    edge(g, "on_page_content:html", "pages_point_b", style="dashed")
+    edge(g, "pages_point_b", "on_env:files")
 
+    edge(g, "pages_point_b", "pages_point_c", arrowhead="none")
+    edge(g, "pages_point_c", "on_page_context:page", style="dashed")
 
-edge(g, "pages_point_b", "pages_point_c", arrowhead="none")
-edge(g, "pages_point_c", "on_page_context:page", style="dashed")
+    with cluster(g, "cluster_build_page") as c:
+        event(c, "on_page_context", "context page config nav")
+        event(c, "on_post_page", "output page config")
 
-with cluster(g, "cluster_build_page") as c:
-    event(c, "on_page_context", "context page config nav")
-    event(c, "on_post_page", "output page config")
+        edge(c, "get_context", "on_page_context:context")
+        edge(c, "on_page_context:context", "render")
+        edge(c, "get_template", "render")
+        edge(c, "render", "on_post_page:output")
+        edge(c, "on_post_page:output", "write_file")
 
-    edge(c, "get_context", "on_page_context:context")
-    edge(c, "on_page_context:context", "render")
-    edge(c, "get_template", "render")
-    edge(c, "render", "on_post_page:output")
-    edge(c, "on_post_page:output", "write_file")
+    edge(g, "on_nav:nav", "cluster_build_page")
+    edge(g, "on_env:env", "cluster_build_page")
 
+    for i in 2, 3:
+        placeholder_cluster(g, f"cluster_build_page_{i}")
+        edge(g, "pages_point_c", f"cluster_build_page_{i}", style="dashed")
 
-edge(g, "on_nav:nav", "cluster_build_page")
-edge(g, "on_env:env", "cluster_build_page")
+    event(g, "on_post_build", "config")
 
-for i in 2, 3:
-    placeholder_cluster(g, f"cluster_build_page_{i}")
-    edge(g, "pages_point_c", f"cluster_build_page_{i}", style="dashed")
-
-
-event(g, "on_post_build", "config")
-event(g, "on_serve", "server config")
+event(graph, "on_serve", "server config")
+event(graph, "on_shutdown", "")
 
 
+ensure_order("on_startup", "cluster_build")
 ensure_order("on_pre_build", "on_files")
 ensure_order("on_nav", "cluster_populate_page")
 ensure_order("cluster_populate_page_2", "cluster_populate_page_3")
@@ -161,8 +163,9 @@ ensure_order("pages_point_c", "cluster_build_page")
 ensure_order("cluster_build_page_2", "cluster_build_page_3")
 ensure_order("cluster_build_page", "on_post_build")
 ensure_order("on_post_build", "on_serve")
+ensure_order("on_serve", "on_shutdown")
 
 
-data = g.pipe()
+data = graph.pipe()
 data = data[data.index(b"<svg ") :]
 pathlib.Path(__file__).with_suffix(".svg").write_bytes(data)
diff --git a/docs/img/plugin-events.svg b/docs/img/plugin-events.svg
index 37bc013a..6fc4702b 100644
--- a/docs/img/plugin-events.svg
+++ b/docs/img/plugin-events.svg
@@ -1,177 +1,222 @@
-<svg width="619pt" height="1585pt"
- viewBox="0.00 0.00 619.17 1585.20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1581.2)">
+<svg width="527pt" height="1786pt"
+ viewBox="0.00 0.00 527.00 1785.80" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1781.8)">
 <title>MkDocs</title>
 <g id="a_graph0"><a xlink:title=" ">
 </a>
 </g>
 <g id="clust1" class="cluster">
-<title>cluster_on_config</title>
-<g id="a_clust1"><a xlink:href="#on_config" xlink:title=" ">
-<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="222.17,-1457.2 222.17,-1533.2 302.17,-1533.2 302.17,-1457.2 222.17,-1457.2"/>
-<text text-anchor="middle" x="262.17" y="-1518" font-family="inherit" font-size="14.00">on_config</text>
+<title>cluster_on_startup</title>
+<g id="a_clust1"><a xlink:href="#on_startup" xlink:title=" ">
+<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="199,-1693.8 199,-1769.8 341,-1769.8 341,-1693.8 199,-1693.8"/>
+<text text-anchor="middle" x="270" y="-1754.6" font-family="inherit" font-size="14.00">on_startup</text>
 </a>
 </g>
 </g>
 <g id="clust2" class="cluster">
-<title>cluster_on_pre_build</title>
-<g id="a_clust2"><a xlink:href="#on_pre_build" xlink:title=" ">
-<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="154.17,-1373.2 154.17,-1449.2 254.17,-1449.2 254.17,-1373.2 154.17,-1373.2"/>
-<text text-anchor="middle" x="204.17" y="-1434" font-family="inherit" font-size="14.00">on_pre_build</text>
+<title>cluster_build</title>
+<g id="a_clust2"><a xlink:title=" ">
+<polygon fill="#dddddd" fill-opacity="0.066667" stroke="#000000" stroke-opacity="0.400000" points="8,-142.6 8,-1685.8 511,-1685.8 511,-142.6 8,-142.6"/>
+<text text-anchor="middle" x="259.5" y="-1670.6" font-family="inherit" font-size="14.00">build</text>
 </a>
 </g>
 </g>
 <g id="clust3" class="cluster">
-<title>cluster_on_files</title>
-<g id="a_clust3"><a xlink:href="#on_files" xlink:title=" ">
-<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="218.17,-1289.2 218.17,-1365.2 334.17,-1365.2 334.17,-1289.2 218.17,-1289.2"/>
-<text text-anchor="middle" x="276.17" y="-1350" font-family="inherit" font-size="14.00">on_files</text>
+<title>cluster_on_config</title>
+<g id="a_clust3"><a xlink:href="#on_config" xlink:title=" ">
+<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="230,-1534.8 230,-1610.8 310,-1610.8 310,-1534.8 230,-1534.8"/>
+<text text-anchor="middle" x="270" y="-1595.6" font-family="inherit" font-size="14.00">on_config</text>
 </a>
 </g>
 </g>
 <g id="clust4" class="cluster">
-<title>cluster_on_nav</title>
-<g id="a_clust4"><a xlink:href="#on_nav" xlink:title=" ">
-<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="173.17,-1141.2 173.17,-1217.2 327.17,-1217.2 327.17,-1141.2 173.17,-1141.2"/>
-<text text-anchor="middle" x="250.17" y="-1202" font-family="inherit" font-size="14.00">on_nav</text>
+<title>cluster_on_pre_build</title>
+<g id="a_clust4"><a xlink:href="#on_pre_build" xlink:title=" ">
+<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="161,-1450.8 161,-1526.8 261,-1526.8 261,-1450.8 161,-1450.8"/>
+<text text-anchor="middle" x="211" y="-1511.6" font-family="inherit" font-size="14.00">on_pre_build</text>
 </a>
 </g>
 </g>
 <g id="clust5" class="cluster">
-<title>cluster_populate_page</title>
-<g id="a_clust5"><a xlink:title=" ">
-<polygon fill="#dddddd" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.400000" points="15.17,-670.6 15.17,-1101.6 277.17,-1101.6 277.17,-670.6 15.17,-670.6"/>
-<text text-anchor="middle" x="146.17" y="-1086.4" font-family="inherit" font-size="14.00">populate_page</text>
+<title>cluster_on_files</title>
+<g id="a_clust5"><a xlink:href="#on_files" xlink:title=" ">
+<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="226,-1366.8 226,-1442.8 342,-1442.8 342,-1366.8 226,-1366.8"/>
+<text text-anchor="middle" x="284" y="-1427.6" font-family="inherit" font-size="14.00">on_files</text>
 </a>
 </g>
 </g>
 <g id="clust6" class="cluster">
-<title>cluster_on_pre_page</title>
-<g id="a_clust6"><a xlink:href="#on_pre_page" xlink:title=" ">
-<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="40.17,-994.6 40.17,-1070.6 204.17,-1070.6 204.17,-994.6 40.17,-994.6"/>
-<text text-anchor="middle" x="122.17" y="-1055.4" font-family="inherit" font-size="14.00">on_pre_page</text>
+<title>cluster_on_nav</title>
+<g id="a_clust6"><a xlink:href="#on_nav" xlink:title=" ">
+<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="181,-1218.8 181,-1294.8 335,-1294.8 335,-1218.8 181,-1218.8"/>
+<text text-anchor="middle" x="258" y="-1279.6" font-family="inherit" font-size="14.00">on_nav</text>
 </a>
 </g>
 </g>
 <g id="clust7" class="cluster">
-<title>cluster_on_page_read_source</title>
-<g id="a_clust7"><a xlink:href="#on_page_read_source" xlink:title=" ">
-<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="23.17,-910.6 23.17,-986.6 178.17,-986.6 178.17,-910.6 23.17,-910.6"/>
-<text text-anchor="middle" x="100.67" y="-971.4" font-family="inherit" font-size="14.00">on_page_read_source</text>
+<title>cluster_populate_page</title>
+<g id="a_clust7"><a xlink:title=" ">
+<polygon fill="#dddddd" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.400000" points="22,-729.2 22,-1179.2 284,-1179.2 284,-729.2 22,-729.2"/>
+<text text-anchor="middle" x="153" y="-1164" font-family="inherit" font-size="14.00">populate_page</text>
 </a>
 </g>
 </g>
 <g id="clust8" class="cluster">
-<title>cluster_on_page_markdown</title>
-<g id="a_clust8"><a xlink:href="#on_page_markdown" xlink:title=" ">
-<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="23.17,-826.6 23.17,-902.6 269.17,-902.6 269.17,-826.6 23.17,-826.6"/>
-<text text-anchor="middle" x="146.17" y="-887.4" font-family="inherit" font-size="14.00">on_page_markdown</text>
+<title>cluster_on_pre_page</title>
+<g id="a_clust8"><a xlink:href="#on_pre_page" xlink:title=" ">
+<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="80,-1072.2 80,-1148.2 244,-1148.2 244,-1072.2 80,-1072.2"/>
+<text text-anchor="middle" x="162" y="-1133" font-family="inherit" font-size="14.00">on_pre_page</text>
 </a>
 </g>
 </g>
 <g id="clust9" class="cluster">
+<title>cluster_on_page_read_source</title>
+<g id="a_clust9"><a xlink:href="#on_page_read_source" xlink:title=" ">
+<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="46,-988.2 46,-1064.2 201,-1064.2 201,-988.2 46,-988.2"/>
+<text text-anchor="middle" x="123.5" y="-1049" font-family="inherit" font-size="14.00">on_page_read_source</text>
+</a>
+</g>
+</g>
+<g id="clust10" class="cluster">
+<title>cluster_on_page_markdown</title>
+<g id="a_clust10"><a xlink:href="#on_page_markdown" xlink:title=" ">
+<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="30,-904.2 30,-980.2 276,-980.2 276,-904.2 30,-904.2"/>
+<text text-anchor="middle" x="153" y="-965" font-family="inherit" font-size="14.00">on_page_markdown</text>
+</a>
+</g>
+</g>
+<g id="clust11" class="cluster">
 <title>cluster_on_page_content</title>
-<g id="a_clust9"><a xlink:href="#on_page_content" xlink:title=" ">
-<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="59.17,-678.6 59.17,-754.6 269.17,-754.6 269.17,-678.6 59.17,-678.6"/>
-<text text-anchor="middle" x="164.17" y="-739.4" font-family="inherit" font-size="14.00">on_page_content</text>
+<g id="a_clust11"><a xlink:href="#on_page_content" xlink:title=" ">
+<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="66,-737.2 66,-813.2 276,-813.2 276,-737.2 66,-737.2"/>
+<text text-anchor="middle" x="171" y="-798" font-family="inherit" font-size="14.00">on_page_content</text>
 </a>
 </g>
 </g>
-<g id="clust12" class="cluster">
+<g id="clust14" class="cluster">
 <title>cluster_on_env</title>
-<g id="a_clust12"><a xlink:href="#on_env" xlink:title=" ">
-<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="273.17,-563 273.17,-639 427.17,-639 427.17,-563 273.17,-563"/>
-<text text-anchor="middle" x="350.17" y="-623.8" font-family="inherit" font-size="14.00">on_env</text>
+<g id="a_clust14"><a xlink:href="#on_env" xlink:title=" ">
+<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="202,-621.6 202,-697.6 356,-697.6 356,-621.6 202,-621.6"/>
+<text text-anchor="middle" x="279" y="-682.4" font-family="inherit" font-size="14.00">on_env</text>
 </a>
 </g>
 </g>
-<g id="clust10" class="cluster">
+<g id="clust12" class="cluster">
 <title>cluster_populate_page_2</title>
-<g id="a_clust10"><a xlink:title=" ">
-<polygon fill="#dddddd" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.400000" points="334.17,-995.1 334.17,-1070.1 446.17,-1070.1 446.17,-995.1 334.17,-995.1"/>
-<text text-anchor="middle" x="390.17" y="-1054.9" font-family="inherit" font-size="14.00">populate_page</text>
+<g id="a_clust12"><a xlink:title=" ">
+<polygon fill="#dddddd" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.400000" points="292,-821.2 292,-896.2 404,-896.2 404,-821.2 292,-821.2"/>
+<text text-anchor="middle" x="348" y="-881" font-family="inherit" font-size="14.00">populate_page</text>
 </a>
 </g>
 </g>
-<g id="clust11" class="cluster">
+<g id="clust13" class="cluster">
 <title>cluster_populate_page_3</title>
-<g id="a_clust11"><a xlink:title=" ">
-<polygon fill="#dddddd" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.400000" points="296.17,-911.1 296.17,-986.1 408.17,-986.1 408.17,-911.1 296.17,-911.1"/>
-<text text-anchor="middle" x="352.17" y="-970.9" font-family="inherit" font-size="14.00">populate_page</text>
+<g id="a_clust13"><a xlink:title=" ">
+<polygon fill="#dddddd" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.400000" points="312,-737.7 312,-812.7 424,-812.7 424,-737.7 312,-737.7"/>
+<text text-anchor="middle" x="368" y="-797.5" font-family="inherit" font-size="14.00">populate_page</text>
 </a>
 </g>
 </g>
-<g id="clust13" class="cluster">
+<g id="clust15" class="cluster">
 <title>cluster_build_page</title>
-<g id="a_clust13"><a xlink:title=" ">
-<polygon fill="#dddddd" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.400000" points="97.17,-176 97.17,-555 479.17,-555 479.17,-176 97.17,-176"/>
-<text text-anchor="middle" x="288.17" y="-539.8" font-family="inherit" font-size="14.00">build_page</text>
+<g id="a_clust15"><a xlink:title=" ">
+<polygon fill="#dddddd" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.400000" points="16,-234.6 16,-613.6 398,-613.6 398,-234.6 16,-234.6"/>
+<text text-anchor="middle" x="207" y="-598.4" font-family="inherit" font-size="14.00">build_page</text>
 </a>
 </g>
 </g>
-<g id="clust14" class="cluster">
+<g id="clust16" class="cluster">
 <title>cluster_on_page_context</title>
-<g id="a_clust14"><a xlink:href="#on_page_context" xlink:title=" ">
-<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="247.17,-396 247.17,-472 471.17,-472 471.17,-396 247.17,-396"/>
-<text text-anchor="middle" x="359.17" y="-456.8" font-family="inherit" font-size="14.00">on_page_context</text>
+<g id="a_clust16"><a xlink:href="#on_page_context" xlink:title=" ">
+<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="166,-454.6 166,-530.6 390,-530.6 390,-454.6 166,-454.6"/>
+<text text-anchor="middle" x="278" y="-515.4" font-family="inherit" font-size="14.00">on_page_context</text>
 </a>
 </g>
 </g>
-<g id="clust15" class="cluster">
+<g id="clust17" class="cluster">
 <title>cluster_on_post_page</title>
-<g id="a_clust15"><a xlink:href="#on_post_page" xlink:title=" ">
-<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="190.17,-248 190.17,-324 372.17,-324 372.17,-248 190.17,-248"/>
-<text text-anchor="middle" x="281.17" y="-308.8" font-family="inherit" font-size="14.00">on_post_page</text>
+<g id="a_clust17"><a xlink:href="#on_post_page" xlink:title=" ">
+<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="109,-306.6 109,-382.6 291,-382.6 291,-306.6 109,-306.6"/>
+<text text-anchor="middle" x="200" y="-367.4" font-family="inherit" font-size="14.00">on_post_page</text>
 </a>
 </g>
 </g>
-<g id="clust16" class="cluster">
+<g id="clust18" class="cluster">
 <title>cluster_build_page_2</title>
-<g id="a_clust16"><a xlink:title=" ">
-<polygon fill="#dddddd" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.400000" points="516.17,-480 516.17,-555 603.17,-555 603.17,-480 516.17,-480"/>
-<text text-anchor="middle" x="559.67" y="-539.8" font-family="inherit" font-size="14.00">build_page</text>
+<g id="a_clust18"><a xlink:title=" ">
+<polygon fill="#dddddd" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.400000" points="406,-538.6 406,-613.6 493,-613.6 493,-538.6 406,-538.6"/>
+<text text-anchor="middle" x="449.5" y="-598.4" font-family="inherit" font-size="14.00">build_page</text>
 </a>
 </g>
 </g>
-<g id="clust17" class="cluster">
+<g id="clust19" class="cluster">
 <title>cluster_build_page_3</title>
-<g id="a_clust17"><a xlink:title=" ">
-<polygon fill="#dddddd" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.400000" points="501.17,-396.5 501.17,-471.5 588.17,-471.5 588.17,-396.5 501.17,-396.5"/>
-<text text-anchor="middle" x="544.67" y="-456.3" font-family="inherit" font-size="14.00">build_page</text>
+<g id="a_clust19"><a xlink:title=" ">
+<polygon fill="#dddddd" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.400000" points="411,-455.1 411,-530.1 498,-530.1 498,-455.1 411,-455.1"/>
+<text text-anchor="middle" x="454.5" y="-514.9" font-family="inherit" font-size="14.00">build_page</text>
 </a>
 </g>
 </g>
-<g id="clust18" class="cluster">
+<g id="clust20" class="cluster">
 <title>cluster_on_post_build</title>
-<g id="a_clust18"><a xlink:href="#on_post_build" xlink:title=" ">
-<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="176.17,-92 176.17,-168 283.17,-168 283.17,-92 176.17,-92"/>
-<text text-anchor="middle" x="229.67" y="-152.8" font-family="inherit" font-size="14.00">on_post_build</text>
+<g id="a_clust20"><a xlink:href="#on_post_build" xlink:title=" ">
+<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="95,-150.6 95,-226.6 202,-226.6 202,-150.6 95,-150.6"/>
+<text text-anchor="middle" x="148.5" y="-211.4" font-family="inherit" font-size="14.00">on_post_build</text>
 </a>
 </g>
 </g>
-<g id="clust19" class="cluster">
+<g id="clust21" class="cluster">
 <title>cluster_on_serve</title>
-<g id="a_clust19"><a xlink:href="#on_serve" xlink:title=" ">
-<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="165.17,-8 165.17,-84 293.17,-84 293.17,-8 165.17,-8"/>
-<text text-anchor="middle" x="229.17" y="-68.8" font-family="inherit" font-size="14.00">on_serve</text>
+<g id="a_clust21"><a xlink:href="#on_serve" xlink:title=" ">
+<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="84,-58.6 84,-134.6 212,-134.6 212,-58.6 84,-58.6"/>
+<text text-anchor="middle" x="148" y="-119.4" font-family="inherit" font-size="14.00">on_serve</text>
 </a>
 </g>
 </g>
-<!-- on_config -->
+<g id="clust22" class="cluster">
+<title>cluster_on_shutdown</title>
+<g id="a_clust22"><a xlink:href="#on_shutdown" xlink:title=" ">
+<polygon fill="#ffff33" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.533333" points="102,-8 102,-50.6 206,-50.6 206,-8 102,-8"/>
+<text text-anchor="middle" x="154" y="-35.4" font-family="inherit" font-size="14.00">on_shutdown</text>
+</a>
+</g>
+</g>
+<!-- on_startup -->
 <g id="node1" class="node">
-<title>on_config</title>
+<title>on_startup</title>
 <g id="a_node1"><a xlink:title=" ">
-<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="234.17,-1465.7 234.17,-1501.7 290.17,-1501.7 290.17,-1465.7 234.17,-1465.7"/>
-<text text-anchor="middle" x="262.17" y="-1480" font-family="inherit" font-size="14.00">config</text>
+<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="207,-1702.3 207,-1738.3 333,-1738.3 333,-1702.3 207,-1702.3"/>
+<text text-anchor="middle" x="246.5" y="-1716.6" font-family="inherit" font-size="14.00">command</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="286,-1702.3 286,-1738.3"/>
+<text text-anchor="middle" x="309.5" y="-1716.6" font-family="inherit" font-size="14.00">dirty</text>
 </a>
 </g>
 </g>
-<!-- on_pre_build -->
+<!-- load_config -->
+<g id="node6" class="node">
+<title>load_config</title>
+<g id="a_node6"><a xlink:title=" ">
+<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="270" cy="-1636.8" rx="59.29" ry="18"/>
+<text text-anchor="middle" x="270" y="-1633.1" font-family="inherit" font-size="14.00">load_config</text>
+</a>
+</g>
+</g>
+<!-- on_startup&#45;&gt;load_config -->
+<!-- on_config -->
 <g id="node2" class="node">
-<title>on_pre_build</title>
+<title>on_config</title>
 <g id="a_node2"><a xlink:title=" ">
-<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="188.17,-1381.7 188.17,-1417.7 244.17,-1417.7 244.17,-1381.7 188.17,-1381.7"/>
-<text text-anchor="middle" x="216.17" y="-1396" font-family="inherit" font-size="14.00">config</text>
+<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="242,-1543.3 242,-1579.3 298,-1579.3 298,-1543.3 242,-1543.3"/>
+<text text-anchor="middle" x="270" y="-1557.6" font-family="inherit" font-size="14.00">config</text>
+</a>
+</g>
+</g>
+<!-- on_pre_build -->
+<g id="node3" class="node">
+<title>on_pre_build</title>
+<g id="a_node3"><a xlink:title=" ">
+<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="196,-1459.3 196,-1495.3 252,-1495.3 252,-1459.3 196,-1459.3"/>
+<text text-anchor="middle" x="224" y="-1473.6" font-family="inherit" font-size="14.00">config</text>
 </a>
 </g>
 </g>
@@ -179,17 +224,17 @@
 <g id="edge2" class="edge">
 <title>on_config:s&#45;&gt;on_pre_build:n</title>
 <g id="a_edge2"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M262.17,-1464.7C262.17,-1453.18 232.96,-1439.99 221.09,-1427.79"/>
-<polygon fill="black" stroke="black" points="224.01,-1425.83 216.17,-1418.7 217.85,-1429.16 224.01,-1425.83"/>
+<path fill="none" stroke="black" d="M270,-1542.3C270,-1536.71 242.48,-1516.57 230.01,-1504.39"/>
+<polygon fill="black" stroke="black" points="232.77,-1502.24 224,-1496.3 227.16,-1506.41 232.77,-1502.24"/>
 </a>
 </g>
 </g>
 <!-- get_files -->
-<g id="node6" class="node">
+<g id="node7" class="node">
 <title>get_files</title>
-<g id="a_node6"><a xlink:title=" ">
-<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="308.17" cy="-1399.7" rx="46.29" ry="18"/>
-<text text-anchor="middle" x="308.17" y="-1396" font-family="inherit" font-size="14.00">get_files</text>
+<g id="a_node7"><a xlink:title=" ">
+<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="316" cy="-1477.3" rx="46.29" ry="18"/>
+<text text-anchor="middle" x="316" y="-1473.6" font-family="inherit" font-size="14.00">get_files</text>
 </a>
 </g>
 </g>
@@ -197,33 +242,33 @@
 <g id="edge3" class="edge">
 <title>on_config:s&#45;&gt;get_files</title>
 <g id="a_edge3"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M262.17,-1464.7C262.17,-1449.02 271.38,-1434.56 281.56,-1423.37"/>
-<polygon fill="black" stroke="black" points="284.13,-1425.74 288.66,-1416.16 279.15,-1420.83 284.13,-1425.74"/>
+<path fill="none" stroke="black" d="M270,-1542.3C270,-1526.62 279.21,-1512.16 289.39,-1500.97"/>
+<polygon fill="black" stroke="black" points="291.96,-1503.34 296.49,-1493.76 286.98,-1498.43 291.96,-1503.34"/>
 </a>
 </g>
 </g>
 <!-- on_files -->
-<g id="node3" class="node">
+<g id="node4" class="node">
 <title>on_files</title>
-<g id="a_node3"><a xlink:title=" ">
-<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="226.67,-1297.7 226.67,-1333.7 325.67,-1333.7 325.67,-1297.7 226.67,-1297.7"/>
-<text text-anchor="middle" x="248.17" y="-1312" font-family="inherit" font-size="14.00">files</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="269.67,-1297.7 269.67,-1333.7 "/>
-<text text-anchor="middle" x="297.67" y="-1312" font-family="inherit" font-size="14.00">config</text>
+<g id="a_node4"><a xlink:title=" ">
+<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="234.5,-1375.3 234.5,-1411.3 333.5,-1411.3 333.5,-1375.3 234.5,-1375.3"/>
+<text text-anchor="middle" x="256" y="-1389.6" font-family="inherit" font-size="14.00">files</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="277.5,-1375.3 277.5,-1411.3"/>
+<text text-anchor="middle" x="305.5" y="-1389.6" font-family="inherit" font-size="14.00">config</text>
 </a>
 </g>
 </g>
 <!-- on_pre_build&#45;&gt;on_files -->
 <!-- on_nav -->
-<g id="node4" class="node">
+<g id="node5" class="node">
 <title>on_nav</title>
-<g id="a_node4"><a xlink:title=" ">
-<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="181.67,-1149.7 181.67,-1185.7 318.67,-1185.7 318.67,-1149.7 181.67,-1149.7"/>
-<text text-anchor="middle" x="200.67" y="-1164" font-family="inherit" font-size="14.00">nav</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="219.67,-1149.7 219.67,-1185.7 "/>
-<text text-anchor="middle" x="247.67" y="-1164" font-family="inherit" font-size="14.00">config</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="275.67,-1149.7 275.67,-1185.7 "/>
-<text text-anchor="middle" x="297.17" y="-1164" font-family="inherit" font-size="14.00">files</text>
+<g id="a_node5"><a xlink:title=" ">
+<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="189.5,-1227.3 189.5,-1263.3 326.5,-1263.3 326.5,-1227.3 189.5,-1227.3"/>
+<text text-anchor="middle" x="208.5" y="-1241.6" font-family="inherit" font-size="14.00">nav</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="227.5,-1227.3 227.5,-1263.3"/>
+<text text-anchor="middle" x="255.5" y="-1241.6" font-family="inherit" font-size="14.00">config</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="283.5,-1227.3 283.5,-1263.3"/>
+<text text-anchor="middle" x="305" y="-1241.6" font-family="inherit" font-size="14.00">files</text>
 </a>
 </g>
 </g>
@@ -231,17 +276,17 @@
 <g id="edge7" class="edge">
 <title>on_files:s&#45;&gt;on_nav:n</title>
 <g id="a_edge7"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M248.17,-1296.7C248.17,-1279.21 257.94,-1277.13 265.17,-1261.2 278.55,-1231.72 293.72,-1224.16 296.66,-1196.8"/>
-<polygon fill="black" stroke="black" points="300.16,-1196.86 297.17,-1186.7 293.17,-1196.51 300.16,-1196.86"/>
+<path fill="none" stroke="black" d="M256,-1374.3C256,-1352.14 280.73,-1358.43 291,-1338.8 304.91,-1312.21 305.15,-1300.61 305.03,-1274.59"/>
+<polygon fill="black" stroke="black" points="308.53,-1274.29 305,-1264.3 301.53,-1274.31 308.53,-1274.29"/>
 </a>
 </g>
 </g>
 <!-- get_nav -->
-<g id="node7" class="node">
+<g id="node8" class="node">
 <title>get_nav</title>
-<g id="a_node7"><a xlink:title=" ">
-<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="212.17" cy="-1243.2" rx="43.59" ry="18"/>
-<text text-anchor="middle" x="212.17" y="-1239.5" font-family="inherit" font-size="14.00">get_nav</text>
+<g id="a_node8"><a xlink:title=" ">
+<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="216" cy="-1320.8" rx="43.59" ry="18"/>
+<text text-anchor="middle" x="216" y="-1317.1" font-family="inherit" font-size="14.00">get_nav</text>
 </a>
 </g>
 </g>
@@ -249,26 +294,26 @@
 <g id="edge5" class="edge">
 <title>on_files:s&#45;&gt;get_nav</title>
 <g id="a_edge5"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M248.17,-1296.7C248.17,-1286.09 243.11,-1276.06 236.78,-1267.66"/>
-<polygon fill="black" stroke="black" points="239.3,-1265.22 230.17,-1259.84 233.95,-1269.74 239.3,-1265.22"/>
+<path fill="none" stroke="black" d="M256,-1374.3C256,-1363.19 250.26,-1352.96 243.12,-1344.54"/>
+<polygon fill="black" stroke="black" points="245.43,-1341.88 236,-1337.05 240.36,-1346.71 245.43,-1341.88"/>
 </a>
 </g>
 </g>
 <!-- render_p -->
-<g id="node12" class="node">
+<g id="node13" class="node">
 <title>render_p</title>
-<g id="a_node12"><a xlink:title=" ">
-<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="81.17" cy="-780.6" rx="38.19" ry="18"/>
-<text text-anchor="middle" x="81.17" y="-776.9" font-family="inherit" font-size="14.00">render</text>
+<g id="a_node13"><a xlink:title=" ">
+<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="88" cy="-847.2" rx="38.19" ry="18"/>
+<text text-anchor="middle" x="88" y="-843.5" font-family="inherit" font-size="14.00">render</text>
 </a>
 </g>
 </g>
 <!-- on_nav&#45;&gt;render_p -->
 <!-- pages_point_a -->
-<g id="node13" class="node">
+<g id="node14" class="node">
 <title>pages_point_a</title>
-<g id="a_node13"><a xlink:title=" ">
-<ellipse fill="black" stroke="black" cx="297.17" cy="-1111.4" rx="1.8" ry="1.8"/>
+<g id="a_node14"><a xlink:title=" ">
+<ellipse fill="black" stroke="black" cx="305" cy="-1189" rx="1.8" ry="1.8"/>
 </a>
 </g>
 </g>
@@ -276,16 +321,16 @@
 <g id="edge12" class="edge">
 <title>on_nav:s&#45;&gt;pages_point_a</title>
 <g id="a_edge12"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M297.17,-1148.7C297.17,-1134.58 297.17,-1117.38 297.17,-1113.3"/>
+<path fill="none" stroke="black" d="M305,-1226.3C305,-1212.18 305,-1194.98 305,-1190.9"/>
 </a>
 </g>
 </g>
 <!-- get_context -->
-<g id="node21" class="node">
+<g id="node22" class="node">
 <title>get_context</title>
-<g id="a_node21"><a xlink:title=" ">
-<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="321.17" cy="-506" rx="61.19" ry="18"/>
-<text text-anchor="middle" x="321.17" y="-502.3" font-family="inherit" font-size="14.00">get_context</text>
+<g id="a_node22"><a xlink:title=" ">
+<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="229" cy="-564.6" rx="61.19" ry="18"/>
+<text text-anchor="middle" x="229" y="-560.9" font-family="inherit" font-size="14.00">get_context</text>
 </a>
 </g>
 </g>
@@ -293,17 +338,8 @@
 <g id="edge28" class="edge">
 <title>on_nav:s&#45;&gt;get_context</title>
 <g id="a_edge28"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M200.17,-1148.7C200.17,-1092.17 410.43,-1141.81 450.17,-1101.6 475.39,-1076.08 464.17,-1057.98 464.17,-1022.1 464.17,-1022.1 464.17,-1022.1 464.17,-647.8 464.17,-607.36 458.3,-592.99 431.17,-563 430.93,-562.74 430.7,-562.48 430.46,-562.22"/>
-<polygon fill="black" stroke="black" points="432.71,-559.52 423.13,-555 427.79,-564.51 432.71,-559.52"/>
-</a>
-</g>
-</g>
-<!-- load_config -->
-<g id="node5" class="node">
-<title>load_config</title>
-<g id="a_node5"><a xlink:title=" ">
-<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="262.17" cy="-1559.2" rx="59.29" ry="18"/>
-<text text-anchor="middle" x="262.17" y="-1555.5" font-family="inherit" font-size="14.00">load_config</text>
+<path fill="none" stroke="black" d="M208,-1226.3C208,-1176.78 394.87,-1227.61 428,-1190.8 455.13,-1160.66 433,-1140.25 433,-1099.7 433,-1099.7 433,-1099.7 433,-706.4 433,-656.67 403.87,-645.02 360,-621.6 337.19,-609.42 324.79,-620.79 303.65,-616.51"/>
+<polygon fill="black" stroke="black" points="304.58,-613.13 294,-613.6 302.57,-619.84 304.58,-613.13"/>
 </a>
 </g>
 </g>
@@ -311,8 +347,8 @@
 <g id="edge1" class="edge">
 <title>load_config&#45;&gt;on_config:n</title>
 <g id="a_edge1"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M262.17,-1541.2C262.17,-1533.04 262.17,-1522.9 262.17,-1512.93"/>
-<polygon fill="black" stroke="black" points="265.67,-1512.7 262.17,-1502.7 258.67,-1512.7 265.67,-1512.7"/>
+<path fill="none" stroke="black" d="M270,-1618.8C270,-1610.64 270,-1600.5 270,-1590.53"/>
+<polygon fill="black" stroke="black" points="273.5,-1590.3 270,-1580.3 266.5,-1590.3 273.5,-1590.3"/>
 </a>
 </g>
 </g>
@@ -320,8 +356,8 @@
 <g id="edge4" class="edge">
 <title>get_files&#45;&gt;on_files:n</title>
 <g id="a_edge4"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M282.72,-1384.56C269.05,-1375.24 254.09,-1361.67 249.55,-1344.92"/>
-<polygon fill="black" stroke="black" points="252.98,-1344.14 248.17,-1334.7 246.04,-1345.08 252.98,-1344.14"/>
+<path fill="none" stroke="black" d="M290.55,-1462.16C276.88,-1452.84 261.92,-1439.27 257.38,-1422.52"/>
+<polygon fill="black" stroke="black" points="260.81,-1421.74 256,-1412.3 253.87,-1422.68 260.81,-1421.74"/>
 </a>
 </g>
 </g>
@@ -329,32 +365,32 @@
 <g id="edge6" class="edge">
 <title>get_nav&#45;&gt;on_nav:n</title>
 <g id="a_edge6"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M206.45,-1224.91C204.16,-1216.86 201.81,-1206.88 200.75,-1196.95"/>
-<polygon fill="black" stroke="black" points="204.23,-1196.49 200.17,-1186.7 197.24,-1196.88 204.23,-1196.49"/>
+<path fill="none" stroke="black" d="M212.19,-1302.4C210.66,-1294.31 209.09,-1284.32 208.39,-1274.45"/>
+<polygon fill="black" stroke="black" points="211.88,-1274.16 208,-1264.3 204.88,-1274.43 211.88,-1274.16"/>
 </a>
 </g>
 </g>
 <!-- on_pre_page -->
-<g id="node8" class="node">
+<g id="node9" class="node">
 <title>on_pre_page</title>
-<g id="a_node8"><a xlink:title=" ">
-<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="48.67,-1003.1 48.67,-1039.1 195.67,-1039.1 195.67,-1003.1 48.67,-1003.1"/>
-<text text-anchor="middle" x="72.67" y="-1017.4" font-family="inherit" font-size="14.00">page</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="96.67,-1003.1 96.67,-1039.1 "/>
-<text text-anchor="middle" x="124.67" y="-1017.4" font-family="inherit" font-size="14.00">config</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="152.67,-1003.1 152.67,-1039.1 "/>
-<text text-anchor="middle" x="174.17" y="-1017.4" font-family="inherit" font-size="14.00">files</text>
+<g id="a_node9"><a xlink:title=" ">
+<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="88.5,-1080.7 88.5,-1116.7 235.5,-1116.7 235.5,-1080.7 88.5,-1080.7"/>
+<text text-anchor="middle" x="112.5" y="-1095" font-family="inherit" font-size="14.00">page</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="136.5,-1080.7 136.5,-1116.7"/>
+<text text-anchor="middle" x="164.5" y="-1095" font-family="inherit" font-size="14.00">config</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="192.5,-1080.7 192.5,-1116.7"/>
+<text text-anchor="middle" x="214" y="-1095" font-family="inherit" font-size="14.00">files</text>
 </a>
 </g>
 </g>
 <!-- on_page_read_source -->
-<g id="node9" class="node">
+<g id="node10" class="node">
 <title>on_page_read_source</title>
-<g id="a_node9"><a xlink:title=" ">
-<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="48.17,-919.1 48.17,-955.1 152.17,-955.1 152.17,-919.1 48.17,-919.1"/>
-<text text-anchor="middle" x="72.17" y="-933.4" font-family="inherit" font-size="14.00">page</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="96.17,-919.1 96.17,-955.1 "/>
-<text text-anchor="middle" x="124.17" y="-933.4" font-family="inherit" font-size="14.00">config</text>
+<g id="a_node10"><a xlink:title=" ">
+<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="72,-996.7 72,-1032.7 176,-1032.7 176,-996.7 72,-996.7"/>
+<text text-anchor="middle" x="96" y="-1011" font-family="inherit" font-size="14.00">page</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="120,-996.7 120,-1032.7"/>
+<text text-anchor="middle" x="148" y="-1011" font-family="inherit" font-size="14.00">config</text>
 </a>
 </g>
 </g>
@@ -362,23 +398,23 @@
 <g id="edge8" class="edge">
 <title>on_pre_page:s&#45;&gt;on_page_read_source:n</title>
 <g id="a_edge8"><a xlink:title=" ">
-<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M72.17,-1002.1C72.17,-985.49 72.17,-979 72.17,-966.18"/>
-<polygon fill="black" stroke="black" stroke-width="1.5" points="75.67,-966.1 72.17,-956.1 68.67,-966.1 75.67,-966.1"/>
+<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M112,-1079.7C112,-1061.94 101.23,-1056.93 97.36,-1043.75"/>
+<polygon fill="black" stroke="black" stroke-width="1.5" points="100.81,-1043.14 96,-1033.7 93.88,-1044.08 100.81,-1043.14"/>
 </a>
 </g>
 </g>
 <!-- on_page_markdown -->
-<g id="node10" class="node">
+<g id="node11" class="node">
 <title>on_page_markdown</title>
-<g id="a_node10"><a xlink:title=" ">
-<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="30.67,-835.1 30.67,-871.1 261.67,-871.1 261.67,-835.1 30.67,-835.1"/>
-<text text-anchor="middle" x="72.67" y="-849.4" font-family="inherit" font-size="14.00">markdown</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="114.67,-835.1 114.67,-871.1 "/>
-<text text-anchor="middle" x="138.67" y="-849.4" font-family="inherit" font-size="14.00">page</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="162.67,-835.1 162.67,-871.1 "/>
-<text text-anchor="middle" x="190.67" y="-849.4" font-family="inherit" font-size="14.00">config</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="218.67,-835.1 218.67,-871.1 "/>
-<text text-anchor="middle" x="240.17" y="-849.4" font-family="inherit" font-size="14.00">files</text>
+<g id="a_node11"><a xlink:title=" ">
+<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="37.5,-912.7 37.5,-948.7 268.5,-948.7 268.5,-912.7 37.5,-912.7"/>
+<text text-anchor="middle" x="79.5" y="-927" font-family="inherit" font-size="14.00">markdown</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="121.5,-912.7 121.5,-948.7"/>
+<text text-anchor="middle" x="145.5" y="-927" font-family="inherit" font-size="14.00">page</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="169.5,-912.7 169.5,-948.7"/>
+<text text-anchor="middle" x="197.5" y="-927" font-family="inherit" font-size="14.00">config</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="225.5,-912.7 225.5,-948.7"/>
+<text text-anchor="middle" x="247" y="-927" font-family="inherit" font-size="14.00">files</text>
 </a>
 </g>
 </g>
@@ -386,8 +422,8 @@
 <g id="edge9" class="edge">
 <title>on_page_read_source:s&#45;&gt;on_page_markdown:n</title>
 <g id="a_edge9"><a xlink:title=" ">
-<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M99.09,-910.6C94.89,-897.29 79.69,-894.12 74.18,-882.02"/>
-<polygon fill="black" stroke="black" stroke-width="1.5" points="77.59,-881.2 72.17,-872.1 70.73,-882.6 77.59,-881.2"/>
+<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M122.73,-988.2C116.55,-971.96 89.67,-974.03 81.42,-959.63"/>
+<polygon fill="black" stroke="black" stroke-width="1.5" points="84.77,-958.59 79,-949.7 77.97,-960.24 84.77,-958.59"/>
 </a>
 </g>
 </g>
@@ -395,31 +431,31 @@
 <g id="edge10" class="edge">
 <title>on_page_markdown:s&#45;&gt;render_p</title>
 <g id="a_edge10"><a xlink:title=" ">
-<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M72.17,-834.1C72.17,-825.65 73.32,-816.54 74.79,-808.32"/>
-<polygon fill="black" stroke="black" stroke-width="1.5" points="78.23,-808.99 76.78,-798.5 71.37,-807.61 78.23,-808.99"/>
+<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M79,-911.7C79,-899.72 80.59,-886.59 82.43,-875.4"/>
+<polygon fill="black" stroke="black" stroke-width="1.5" points="85.91,-875.82 84.24,-865.36 79.02,-874.58 85.91,-875.82"/>
 </a>
 </g>
 </g>
 <!-- on_page_content -->
-<g id="node11" class="node">
+<g id="node12" class="node">
 <title>on_page_content</title>
-<g id="a_node11"><a xlink:title=" ">
-<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="67.17,-687.1 67.17,-723.1 261.17,-723.1 261.17,-687.1 67.17,-687.1"/>
-<text text-anchor="middle" x="90.67" y="-701.4" font-family="inherit" font-size="14.00">html</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="114.17,-687.1 114.17,-723.1 "/>
-<text text-anchor="middle" x="138.17" y="-701.4" font-family="inherit" font-size="14.00">page</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="162.17,-687.1 162.17,-723.1 "/>
-<text text-anchor="middle" x="190.17" y="-701.4" font-family="inherit" font-size="14.00">config</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="218.17,-687.1 218.17,-723.1 "/>
-<text text-anchor="middle" x="239.67" y="-701.4" font-family="inherit" font-size="14.00">files</text>
+<g id="a_node12"><a xlink:title=" ">
+<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="74,-745.7 74,-781.7 268,-781.7 268,-745.7 74,-745.7"/>
+<text text-anchor="middle" x="97.5" y="-760" font-family="inherit" font-size="14.00">html</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="121,-745.7 121,-781.7"/>
+<text text-anchor="middle" x="145" y="-760" font-family="inherit" font-size="14.00">page</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="169,-745.7 169,-781.7"/>
+<text text-anchor="middle" x="197" y="-760" font-family="inherit" font-size="14.00">config</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="225,-745.7 225,-781.7"/>
+<text text-anchor="middle" x="246.5" y="-760" font-family="inherit" font-size="14.00">files</text>
 </a>
 </g>
 </g>
 <!-- pages_point_b -->
-<g id="node15" class="node">
+<g id="node16" class="node">
 <title>pages_point_b</title>
-<g id="a_node15"><a xlink:title=" ">
-<ellipse fill="black" stroke="black" cx="397.17" cy="-648.8" rx="1.8" ry="1.8"/>
+<g id="a_node16"><a xlink:title=" ">
+<ellipse fill="black" stroke="black" cx="326" cy="-707.4" rx="1.8" ry="1.8"/>
 </a>
 </g>
 </g>
@@ -427,21 +463,21 @@
 <g id="edge19" class="edge">
 <title>on_page_content:s&#45;&gt;pages_point_b</title>
 <g id="a_edge19"><a xlink:title=" ">
-<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M90.17,-686.1C90.17,-655.8 329.13,-650.71 385.4,-649.93"/>
-<polygon fill="black" stroke="black" stroke-width="1.5" points="385.56,-653.43 395.52,-649.81 385.48,-646.43 385.56,-653.43"/>
+<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M97,-744.7C97,-722.45 267.86,-711.55 314.41,-709"/>
+<polygon fill="black" stroke="black" stroke-width="1.5" points="314.73,-712.49 324.54,-708.47 314.37,-705.5 314.73,-712.49"/>
 </a>
 </g>
 </g>
 <!-- on_env -->
-<g id="node17" class="node">
+<g id="node18" class="node">
 <title>on_env</title>
-<g id="a_node17"><a xlink:title=" ">
-<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="281.67,-571.5 281.67,-607.5 418.67,-607.5 418.67,-571.5 281.67,-571.5"/>
-<text text-anchor="middle" x="300.67" y="-585.8" font-family="inherit" font-size="14.00">env</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="319.67,-571.5 319.67,-607.5 "/>
-<text text-anchor="middle" x="347.67" y="-585.8" font-family="inherit" font-size="14.00">config</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="375.67,-571.5 375.67,-607.5 "/>
-<text text-anchor="middle" x="397.17" y="-585.8" font-family="inherit" font-size="14.00">files</text>
+<g id="a_node18"><a xlink:title=" ">
+<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="210.5,-630.1 210.5,-666.1 347.5,-666.1 347.5,-630.1 210.5,-630.1"/>
+<text text-anchor="middle" x="229.5" y="-644.4" font-family="inherit" font-size="14.00">env</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="248.5,-630.1 248.5,-666.1"/>
+<text text-anchor="middle" x="276.5" y="-644.4" font-family="inherit" font-size="14.00">config</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="304.5,-630.1 304.5,-666.1"/>
+<text text-anchor="middle" x="326" y="-644.4" font-family="inherit" font-size="14.00">files</text>
 </a>
 </g>
 </g>
@@ -450,8 +486,8 @@
 <g id="edge11" class="edge">
 <title>render_p&#45;&gt;on_page_content:n</title>
 <g id="a_edge11"><a xlink:title=" ">
-<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M85.46,-762.22C87.18,-754.14 88.94,-744.15 89.73,-734.27"/>
-<polygon fill="black" stroke="black" stroke-width="1.5" points="93.24,-734.24 90.17,-724.1 86.25,-733.94 93.24,-734.24"/>
+<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M91.76,-829.04C93.71,-819 95.85,-805.8 96.66,-792.91"/>
+<polygon fill="black" stroke="black" stroke-width="1.5" points="100.16,-792.81 97,-782.7 93.17,-792.58 100.16,-792.81"/>
 </a>
 </g>
 </g>
@@ -459,8 +495,8 @@
 <g id="edge13" class="edge">
 <title>pages_point_a&#45;&gt;on_pre_page:n</title>
 <g id="a_edge13"><a xlink:title=" ">
-<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M295.73,-1110.28C277.88,-1108.76 99.25,-1092.49 74.91,-1049.9"/>
-<polygon fill="black" stroke="black" stroke-width="1.5" points="78.23,-1048.79 72.17,-1040.1 71.49,-1050.67 78.23,-1048.79"/>
+<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M303.35,-1187.97C295.87,-1187.77 264.43,-1186.52 240,-1179.2 183.09,-1162.14 119.89,-1178.63 112.68,-1127.97"/>
+<polygon fill="black" stroke="black" stroke-width="1.5" points="116.15,-1127.45 112,-1117.7 109.17,-1127.91 116.15,-1127.45"/>
 </a>
 </g>
 </g>
@@ -468,17 +504,17 @@
 <g id="edge14" class="edge">
 <title>pages_point_a&#45;&gt;render_p</title>
 <g id="a_edge14"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M296.69,-1109.61C295.82,-1108.11 293.97,-1104.72 293.17,-1101.6 286.59,-1075.78 291.37,-904.53 279.65,-844.98"/>
-<polygon fill="black" stroke="black" points="282.96,-843.8 277.17,-834.93 276.17,-845.48 282.96,-843.8"/>
+<path fill="none" stroke="black" d="M304.65,-1187.19C304,-1185.67 302.63,-1182.23 302,-1179.2 290.63,-1124.31 314.02,-979.63 288.52,-918.91"/>
+<polygon fill="black" stroke="black" points="291.62,-917.28 284,-909.91 285.36,-920.42 291.62,-917.28"/>
 </a>
 </g>
 </g>
 <!-- placeholder_cluster_populate_page_2 -->
-<g id="node14" class="node">
+<g id="node15" class="node">
 <title>placeholder_cluster_populate_page_2</title>
-<g id="a_node14"><a xlink:title=" ">
-<ellipse fill="transparent" stroke="transparent" cx="371.17" cy="-1021.1" rx="27" ry="18"/>
-<text text-anchor="middle" x="371.17" y="-1017.4" font-family="inherit" font-size="14.00">...</text>
+<g id="a_node15"><a xlink:title=" ">
+<ellipse fill="none" stroke="none" cx="327" cy="-847.2" rx="27" ry="18"/>
+<text text-anchor="middle" x="327" y="-843.5" font-family="inherit" font-size="14.00">...</text>
 </a>
 </g>
 </g>
@@ -486,17 +522,17 @@
 <g id="edge15" class="edge">
 <title>pages_point_a&#45;&gt;placeholder_cluster_populate_page_2:n</title>
 <g id="a_edge15"><a xlink:title=" ">
-<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M298.85,-1110.36C304.03,-1110.15 319.95,-1108.92 330.17,-1101.6 342.3,-1092.91 351.87,-1086.2 358.7,-1078.39"/>
-<polygon fill="black" stroke="black" stroke-width="1.5" points="361.66,-1080.27 364.62,-1070.1 355.96,-1076.21 361.66,-1080.27"/>
+<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M305.1,-1187.2C306.53,-1175.36 323.3,-1034.9 326.48,-906.23"/>
+<polygon fill="black" stroke="black" stroke-width="1.5" points="329.98,-906.27 326.7,-896.2 322.98,-906.12 329.98,-906.27"/>
 </a>
 </g>
 </g>
 <!-- placeholder_cluster_populate_page_3 -->
-<g id="node16" class="node">
+<g id="node17" class="node">
 <title>placeholder_cluster_populate_page_3</title>
-<g id="a_node16"><a xlink:title=" ">
-<ellipse fill="transparent" stroke="transparent" cx="371.17" cy="-937.1" rx="27" ry="18"/>
-<text text-anchor="middle" x="371.17" y="-933.4" font-family="inherit" font-size="14.00">...</text>
+<g id="a_node17"><a xlink:title=" ">
+<ellipse fill="none" stroke="none" cx="349" cy="-763.7" rx="27" ry="18"/>
+<text text-anchor="middle" x="349" y="-760" font-family="inherit" font-size="14.00">...</text>
 </a>
 </g>
 </g>
@@ -504,8 +540,8 @@
 <g id="edge17" class="edge">
 <title>pages_point_a&#45;&gt;placeholder_cluster_populate_page_3:n</title>
 <g id="a_edge17"><a xlink:title=" ">
-<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M297.25,-1109.39C298.01,-1100.66 304.28,-1037.09 330.17,-994.6 330.37,-994.27 330.58,-993.94 330.79,-993.62"/>
-<polygon fill="black" stroke="black" stroke-width="1.5" points="333.51,-995.83 337.69,-986.1 328.35,-991.1 333.51,-995.83"/>
+<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M306.14,-1187.29C308.27,-1185.92 312.92,-1182.74 316,-1179.2 371.44,-1115.44 413,-1100.19 413,-1015.7 413,-1015.7 413,-1015.7 413,-929.7 413,-881.43 431.96,-863.11 408,-821.2 407.81,-820.86 407.61,-820.54 407.41,-820.22"/>
+<polygon fill="black" stroke="black" stroke-width="1.5" points="409.89,-817.74 400.58,-812.7 404.71,-822.45 409.89,-817.74"/>
 </a>
 </g>
 </g>
@@ -513,8 +549,8 @@
 <g id="edge16" class="edge">
 <title>placeholder_cluster_populate_page_2:s&#45;&gt;pages_point_b</title>
 <g id="a_edge16"><a xlink:title=" ">
-<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M373.54,-995.1C380.71,-988.29 402.32,-999.36 412.17,-986.6 448.35,-939.72 426.17,-913.32 426.17,-854.1 426.17,-854.1 426.17,-854.1 426.17,-779.6 426.17,-734.14 409.02,-681.83 401.1,-660.1"/>
-<polygon fill="black" stroke="black" stroke-width="1.5" points="404.32,-658.72 397.5,-650.61 397.77,-661.2 404.32,-658.72"/>
+<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M322.03,-821.2C317.43,-819.39 310.89,-819.17 308,-813.2 291.72,-779.6 294.96,-764.18 308,-729.2 309.91,-724.06 313.73,-719.35 317.36,-715.72"/>
+<polygon fill="black" stroke="black" stroke-width="1.5" points="319.79,-718.24 325.08,-709.06 315.22,-712.94 319.79,-718.24"/>
 </a>
 </g>
 </g>
@@ -523,16 +559,16 @@
 <g id="edge20" class="edge">
 <title>pages_point_b&#45;&gt;on_env:n</title>
 <g id="a_edge20"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M397.17,-646.83C397.17,-643.38 397.17,-631.25 397.17,-618.83"/>
-<polygon fill="black" stroke="black" points="400.67,-618.5 397.17,-608.5 393.67,-618.5 400.67,-618.5"/>
+<path fill="none" stroke="black" d="M326,-705.43C326,-701.98 326,-689.85 326,-677.43"/>
+<polygon fill="black" stroke="black" points="329.5,-677.1 326,-667.1 322.5,-677.1 329.5,-677.1"/>
 </a>
 </g>
 </g>
 <!-- pages_point_c -->
-<g id="node18" class="node">
+<g id="node19" class="node">
 <title>pages_point_c</title>
-<g id="a_node18"><a xlink:title=" ">
-<ellipse fill="black" stroke="black" cx="494.17" cy="-589.5" rx="1.8" ry="1.8"/>
+<g id="a_node19"><a xlink:title=" ">
+<ellipse fill="black" stroke="black" cx="454" cy="-648.1" rx="1.8" ry="1.8"/>
 </a>
 </g>
 </g>
@@ -540,7 +576,7 @@
 <g id="edge21" class="edge">
 <title>pages_point_b&#45;&gt;pages_point_c</title>
 <g id="a_edge21"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M398.81,-647.61C403.86,-646.99 419.56,-644.68 431.17,-639 459.94,-624.94 488.31,-596.53 493.37,-591.33"/>
+<path fill="none" stroke="black" d="M327.33,-706.17C331.95,-705.33 347.7,-702.3 360,-697.6 399.8,-682.4 444.66,-654.92 452.73,-649.9"/>
 </a>
 </g>
 </g>
@@ -548,8 +584,8 @@
 <g id="edge18" class="edge">
 <title>placeholder_cluster_populate_page_3:s&#45;&gt;pages_point_b</title>
 <g id="a_edge18"><a xlink:title=" ">
-<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M371.2,-911.1C372.01,-812.21 389.55,-696.4 395.34,-660.78"/>
-<polygon fill="black" stroke="black" stroke-width="1.5" points="398.82,-661.2 397,-650.76 391.91,-660.05 398.82,-661.2"/>
+<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M348.02,-737.7C345.7,-729.17 339.67,-721.46 334.5,-716.09"/>
+<polygon fill="black" stroke="black" stroke-width="1.5" points="336.72,-713.38 326.99,-709.18 331.97,-718.53 336.72,-713.38"/>
 </a>
 </g>
 </g>
@@ -557,23 +593,23 @@
 <g id="edge29" class="edge">
 <title>on_env:s&#45;&gt;get_context</title>
 <g id="a_edge29"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M300.17,-570.5C300.17,-568.74 300.24,-566.97 300.38,-565.19"/>
-<polygon fill="black" stroke="black" points="303.89,-565.39 301.82,-555 296.96,-564.41 303.89,-565.39"/>
+<path fill="none" stroke="black" d="M229,-629.1C229,-627.37 229,-625.62 229,-623.85"/>
+<polygon fill="black" stroke="black" points="232.5,-623.6 229,-613.6 225.5,-623.6 232.5,-623.6"/>
 </a>
 </g>
 </g>
 <!-- on_page_context -->
-<g id="node19" class="node">
+<g id="node20" class="node">
 <title>on_page_context</title>
-<g id="a_node19"><a xlink:title=" ">
-<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="255.67,-404.5 255.67,-440.5 462.67,-440.5 462.67,-404.5 255.67,-404.5"/>
-<text text-anchor="middle" x="288.17" y="-418.8" font-family="inherit" font-size="14.00">context</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="320.67,-404.5 320.67,-440.5 "/>
-<text text-anchor="middle" x="344.67" y="-418.8" font-family="inherit" font-size="14.00">page</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="368.67,-404.5 368.67,-440.5 "/>
-<text text-anchor="middle" x="396.67" y="-418.8" font-family="inherit" font-size="14.00">config</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="424.67,-404.5 424.67,-440.5 "/>
-<text text-anchor="middle" x="443.67" y="-418.8" font-family="inherit" font-size="14.00">nav</text>
+<g id="a_node20"><a xlink:title=" ">
+<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="174.5,-463.1 174.5,-499.1 381.5,-499.1 381.5,-463.1 174.5,-463.1"/>
+<text text-anchor="middle" x="207" y="-477.4" font-family="inherit" font-size="14.00">context</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="239.5,-463.1 239.5,-499.1"/>
+<text text-anchor="middle" x="263.5" y="-477.4" font-family="inherit" font-size="14.00">page</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="287.5,-463.1 287.5,-499.1"/>
+<text text-anchor="middle" x="315.5" y="-477.4" font-family="inherit" font-size="14.00">config</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="343.5,-463.1 343.5,-499.1"/>
+<text text-anchor="middle" x="362.5" y="-477.4" font-family="inherit" font-size="14.00">nav</text>
 </a>
 </g>
 </g>
@@ -581,18 +617,18 @@
 <g id="edge22" class="edge">
 <title>pages_point_c&#45;&gt;on_page_context:n</title>
 <g id="a_edge22"><a xlink:title=" ">
-<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M493.94,-587.69C491.69,-579.69 472.5,-514.64 434.17,-480 404.79,-453.45 354.34,-479.38 345.51,-451.45"/>
-<polygon fill="black" stroke="black" stroke-width="1.5" points="348.97,-450.94 344.17,-441.5 342.03,-451.88 348.97,-450.94"/>
+<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M453.63,-645.9C452.27,-641.79 447.22,-628.11 438,-621.6 424.61,-612.15 415.2,-623.31 402,-613.6 371.05,-590.83 387.99,-563.81 359,-538.6 327.29,-511.03 273.34,-540.1 264.3,-510.2"/>
+<polygon fill="black" stroke="black" stroke-width="1.5" points="267.75,-509.57 263,-500.1 260.81,-510.47 267.75,-509.57"/>
 </a>
 </g>
 </g>
 <!-- pages_point_c&#45;&gt;get_context -->
 <!-- placeholder_cluster_build_page_2 -->
-<g id="node25" class="node">
+<g id="node26" class="node">
 <title>placeholder_cluster_build_page_2</title>
-<g id="a_node25"><a xlink:title=" ">
-<ellipse fill="transparent" stroke="transparent" cx="552.17" cy="-506" rx="27" ry="18"/>
-<text text-anchor="middle" x="552.17" y="-502.3" font-family="inherit" font-size="14.00">...</text>
+<g id="a_node26"><a xlink:title=" ">
+<ellipse fill="none" stroke="none" cx="455" cy="-564.6" rx="27" ry="18"/>
+<text text-anchor="middle" x="455" y="-560.9" font-family="inherit" font-size="14.00">...</text>
 </a>
 </g>
 </g>
@@ -600,17 +636,17 @@
 <g id="edge30" class="edge">
 <title>pages_point_c&#45;&gt;placeholder_cluster_build_page_2:n</title>
 <g id="a_edge30"><a xlink:title=" ">
-<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M495.6,-587.84C500.75,-585.43 518.72,-576.44 533.04,-562.64"/>
-<polygon fill="black" stroke="black" stroke-width="1.5" points="535.89,-564.72 540.11,-555 530.75,-559.96 535.89,-564.72"/>
+<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M454.02,-646.2C454.1,-643.5 454.33,-634.89 454.54,-623.84"/>
+<polygon fill="black" stroke="black" stroke-width="1.5" points="458.05,-623.66 454.72,-613.6 451.05,-623.53 458.05,-623.66"/>
 </a>
 </g>
 </g>
 <!-- placeholder_cluster_build_page_3 -->
-<g id="node26" class="node">
+<g id="node27" class="node">
 <title>placeholder_cluster_build_page_3</title>
-<g id="a_node26"><a xlink:title=" ">
-<ellipse fill="transparent" stroke="transparent" cx="544.17" cy="-422.5" rx="27" ry="18"/>
-<text text-anchor="middle" x="544.17" y="-418.8" font-family="inherit" font-size="14.00">...</text>
+<g id="a_node27"><a xlink:title=" ">
+<ellipse fill="none" stroke="none" cx="459" cy="-481.1" rx="27" ry="18"/>
+<text text-anchor="middle" x="459" y="-477.4" font-family="inherit" font-size="14.00">...</text>
 </a>
 </g>
 </g>
@@ -618,17 +654,17 @@
 <g id="edge31" class="edge">
 <title>pages_point_c&#45;&gt;placeholder_cluster_build_page_3:n</title>
 <g id="a_edge31"><a xlink:title=" ">
-<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M494.14,-587.37C493.94,-578.61 493.43,-520.98 512.17,-480 512.22,-479.89 512.27,-479.78 512.32,-479.68"/>
-<polygon fill="black" stroke="black" stroke-width="1.5" points="515.16,-481.73 517.92,-471.5 509.38,-477.77 515.16,-481.73"/>
+<path fill="none" stroke="black" stroke-width="1.5" stroke-dasharray="5,2" d="M455.41,-646.55C461.75,-644.01 487.69,-632.65 497,-613.6 511.63,-583.65 509.66,-569.44 497,-538.6 496.96,-538.49 496.91,-538.38 496.86,-538.27"/>
+<polygon fill="black" stroke="black" stroke-width="1.5" points="499.88,-536.48 491.42,-530.1 494.05,-540.36 499.88,-536.48"/>
 </a>
 </g>
 </g>
 <!-- render -->
-<g id="node22" class="node">
+<g id="node23" class="node">
 <title>render</title>
-<g id="a_node22"><a xlink:title=" ">
-<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="229.17" cy="-350" rx="38.19" ry="18"/>
-<text text-anchor="middle" x="229.17" y="-346.3" font-family="inherit" font-size="14.00">render</text>
+<g id="a_node23"><a xlink:title=" ">
+<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="148" cy="-408.6" rx="38.19" ry="18"/>
+<text text-anchor="middle" x="148" y="-404.9" font-family="inherit" font-size="14.00">render</text>
 </a>
 </g>
 </g>
@@ -636,30 +672,30 @@
 <g id="edge24" class="edge">
 <title>on_page_context:s&#45;&gt;render</title>
 <g id="a_edge24"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M288.17,-403.5C288.17,-388.57 277.5,-376.85 265.23,-368.3"/>
-<polygon fill="black" stroke="black" points="266.93,-365.23 256.6,-362.89 263.21,-371.16 266.93,-365.23"/>
+<path fill="none" stroke="black" d="M207,-462.1C207,-447.17 196.33,-435.45 184.06,-426.9"/>
+<polygon fill="black" stroke="black" points="185.76,-423.83 175.43,-421.49 182.04,-429.76 185.76,-423.83"/>
 </a>
 </g>
 </g>
 <!-- on_post_page -->
-<g id="node20" class="node">
+<g id="node21" class="node">
 <title>on_post_page</title>
-<g id="a_node20"><a xlink:title=" ">
-<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="198.17,-256.5 198.17,-292.5 364.17,-292.5 364.17,-256.5 198.17,-256.5"/>
-<text text-anchor="middle" x="229.17" y="-270.8" font-family="inherit" font-size="14.00">output</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="260.17,-256.5 260.17,-292.5 "/>
-<text text-anchor="middle" x="284.17" y="-270.8" font-family="inherit" font-size="14.00">page</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="308.17,-256.5 308.17,-292.5 "/>
-<text text-anchor="middle" x="336.17" y="-270.8" font-family="inherit" font-size="14.00">config</text>
+<g id="a_node21"><a xlink:title=" ">
+<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="117,-315.1 117,-351.1 283,-351.1 283,-315.1 117,-315.1"/>
+<text text-anchor="middle" x="148" y="-329.4" font-family="inherit" font-size="14.00">output</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="179,-315.1 179,-351.1"/>
+<text text-anchor="middle" x="203" y="-329.4" font-family="inherit" font-size="14.00">page</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="227,-315.1 227,-351.1"/>
+<text text-anchor="middle" x="255" y="-329.4" font-family="inherit" font-size="14.00">config</text>
 </a>
 </g>
 </g>
 <!-- write_file -->
-<g id="node24" class="node">
+<g id="node25" class="node">
 <title>write_file</title>
-<g id="a_node24"><a xlink:title=" ">
-<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="229.17" cy="-202" rx="50.09" ry="18"/>
-<text text-anchor="middle" x="229.17" y="-198.3" font-family="inherit" font-size="14.00">write_file</text>
+<g id="a_node25"><a xlink:title=" ">
+<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="148" cy="-260.6" rx="50.09" ry="18"/>
+<text text-anchor="middle" x="148" y="-256.9" font-family="inherit" font-size="14.00">write_file</text>
 </a>
 </g>
 </g>
@@ -667,8 +703,8 @@
 <g id="edge27" class="edge">
 <title>on_post_page:s&#45;&gt;write_file</title>
 <g id="a_edge27"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M229.17,-255.5C229.17,-247.26 229.17,-238.3 229.17,-230.14"/>
-<polygon fill="black" stroke="black" points="232.67,-230.02 229.17,-220.02 225.67,-230.02 232.67,-230.02"/>
+<path fill="none" stroke="black" d="M148,-314.1C148,-305.86 148,-296.9 148,-288.74"/>
+<polygon fill="black" stroke="black" points="151.5,-288.62 148,-278.62 144.5,-288.62 151.5,-288.62"/>
 </a>
 </g>
 </g>
@@ -676,8 +712,8 @@
 <g id="edge23" class="edge">
 <title>get_context&#45;&gt;on_page_context:n</title>
 <g id="a_edge23"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M306.98,-488.42C299.79,-478.63 291.99,-465.48 289.22,-451.65"/>
-<polygon fill="black" stroke="black" points="292.68,-451.09 288.17,-441.5 285.72,-451.81 292.68,-451.09"/>
+<path fill="none" stroke="black" d="M219.67,-546.63C214.89,-536.68 209.68,-523.49 207.76,-510.21"/>
+<polygon fill="black" stroke="black" points="211.24,-509.81 207,-500.1 204.26,-510.34 211.24,-509.81"/>
 </a>
 </g>
 </g>
@@ -685,17 +721,17 @@
 <g id="edge26" class="edge">
 <title>render&#45;&gt;on_post_page:n</title>
 <g id="a_edge26"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M229.17,-332C229.17,-323.84 229.17,-313.7 229.17,-303.73"/>
-<polygon fill="black" stroke="black" points="232.67,-303.5 229.17,-293.5 225.67,-303.5 232.67,-303.5"/>
+<path fill="none" stroke="black" d="M148,-390.6C148,-382.44 148,-372.3 148,-362.33"/>
+<polygon fill="black" stroke="black" points="151.5,-362.1 148,-352.1 144.5,-362.1 151.5,-362.1"/>
 </a>
 </g>
 </g>
 <!-- get_template -->
-<g id="node23" class="node">
+<g id="node24" class="node">
 <title>get_template</title>
-<g id="a_node23"><a xlink:title=" ">
-<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="171.17" cy="-422.5" rx="66.09" ry="18"/>
-<text text-anchor="middle" x="171.17" y="-418.8" font-family="inherit" font-size="14.00">get_template</text>
+<g id="a_node24"><a xlink:title=" ">
+<ellipse fill="#77ff77" fill-opacity="0.533333" stroke="#000000" stroke-opacity="0.600000" cx="90" cy="-481.1" rx="66.09" ry="18"/>
+<text text-anchor="middle" x="90" y="-477.4" font-family="inherit" font-size="14.00">get_template</text>
 </a>
 </g>
 </g>
@@ -703,33 +739,42 @@
 <g id="edge25" class="edge">
 <title>get_template&#45;&gt;render</title>
 <g id="a_edge25"><a xlink:title=" ">
-<path fill="none" stroke="black" d="M184.91,-404.79C192.24,-395.89 201.39,-384.77 209.46,-374.96"/>
-<polygon fill="black" stroke="black" points="212.27,-377.05 215.92,-367.1 206.87,-372.6 212.27,-377.05"/>
+<path fill="none" stroke="black" d="M103.75,-463.39C111.07,-454.49 120.22,-443.37 128.29,-433.56"/>
+<polygon fill="black" stroke="black" points="131.1,-435.65 134.75,-425.7 125.7,-431.2 131.1,-435.65"/>
 </a>
 </g>
 </g>
 <!-- on_post_build -->
-<g id="node27" class="node">
+<g id="node28" class="node">
 <title>on_post_build</title>
-<g id="a_node27"><a xlink:title=" ">
-<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="201.17,-100.5 201.17,-136.5 257.17,-136.5 257.17,-100.5 201.17,-100.5"/>
-<text text-anchor="middle" x="229.17" y="-114.8" font-family="inherit" font-size="14.00">config</text>
+<g id="a_node28"><a xlink:title=" ">
+<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="120,-159.1 120,-195.1 176,-195.1 176,-159.1 120,-159.1"/>
+<text text-anchor="middle" x="148" y="-173.4" font-family="inherit" font-size="14.00">config</text>
 </a>
 </g>
 </g>
 <!-- write_file&#45;&gt;on_post_build -->
 <!-- placeholder_cluster_build_page_2&#45;&gt;placeholder_cluster_build_page_3 -->
 <!-- on_serve -->
-<g id="node28" class="node">
+<g id="node29" class="node">
 <title>on_serve</title>
-<g id="a_node28"><a xlink:title=" ">
-<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="173.17,-16.5 173.17,-52.5 285.17,-52.5 285.17,-16.5 173.17,-16.5"/>
-<text text-anchor="middle" x="201.17" y="-30.8" font-family="inherit" font-size="14.00">server</text>
-<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="229.17,-16.5 229.17,-52.5 "/>
-<text text-anchor="middle" x="257.17" y="-30.8" font-family="inherit" font-size="14.00">config</text>
+<g id="a_node29"><a xlink:title=" ">
+<polygon fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" points="92,-67.1 92,-103.1 204,-103.1 204,-67.1 92,-67.1"/>
+<text text-anchor="middle" x="120" y="-81.4" font-family="inherit" font-size="14.00">server</text>
+<polyline fill="none" stroke="#000000" stroke-opacity="0.600000" points="148,-67.1 148,-103.1"/>
+<text text-anchor="middle" x="176" y="-81.4" font-family="inherit" font-size="14.00">config</text>
 </a>
 </g>
 </g>
 <!-- on_post_build&#45;&gt;on_serve -->
+<!-- on_shutdown -->
+<g id="node30" class="node">
+<title>on_shutdown</title>
+<g id="a_node30"><a xlink:title=" ">
+<ellipse fill="#ffffff" fill-opacity="0.333333" stroke="#000000" stroke-opacity="0.600000" cx="148" cy="-17.8" rx="1.8" ry="1.8"/>
+</a>
+</g>
+</g>
+<!-- on_serve&#45;&gt;on_shutdown -->
 </g>
 </svg>
diff --git a/mkdocs/__main__.py b/mkdocs/__main__.py
index bd726cfa..5208da5f 100644
--- a/mkdocs/__main__.py
+++ b/mkdocs/__main__.py
@@ -240,7 +240,12 @@ def build_command(clean, **kwargs):
     from mkdocs.commands import build
 
     _enable_warnings()
-    build.build(config.load_config(**kwargs), dirty=not clean)
+    cfg = config.load_config(**kwargs)
+    cfg['plugins'].run_event('startup', command='build', dirty=not clean)
+    try:
+        build.build(cfg, dirty=not clean)
+    finally:
+        cfg['plugins'].run_event('shutdown')
 
 
 @cli.command(name="gh-deploy")
@@ -263,7 +268,11 @@ def gh_deploy_command(
 
     _enable_warnings()
     cfg = config.load_config(remote_branch=remote_branch, remote_name=remote_name, **kwargs)
-    build.build(cfg, dirty=not clean)
+    cfg['plugins'].run_event('startup', command='gh-deploy', dirty=not clean)
+    try:
+        build.build(cfg, dirty=not clean)
+    finally:
+        cfg['plugins'].run_event('shutdown')
     gh_deploy.gh_deploy(
         cfg,
         message=message,
diff --git a/mkdocs/commands/build.py b/mkdocs/commands/build.py
index e8b76fc4..940624c3 100644
--- a/mkdocs/commands/build.py
+++ b/mkdocs/commands/build.py
@@ -3,6 +3,7 @@ from __future__ import annotations
 import gzip
 import logging
 import os
+import time
 from typing import Any, Dict, Optional, Sequence, Set, Union
 from urllib.parse import urlsplit
 
@@ -265,8 +266,6 @@ def build(config: Config, live_server: bool = False, dirty: bool = False) -> Non
         logging.getLogger('mkdocs').addHandler(warning_counter)
 
     try:
-        import time
-
         start = time.monotonic()
 
         # Run `config` plugin events.
diff --git a/mkdocs/commands/serve.py b/mkdocs/commands/serve.py
index 12412ba6..75f72e54 100644
--- a/mkdocs/commands/serve.py
+++ b/mkdocs/commands/serve.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import functools
 import logging
 import shutil
 import tempfile
@@ -42,17 +43,24 @@ def serve(
     def mount_path(config):
         return urlsplit(config['site_url'] or '/').path
 
-    def builder():
+    get_config = functools.partial(
+        load_config,
+        config_file=config_file,
+        dev_addr=dev_addr,
+        strict=strict,
+        theme=theme,
+        theme_dir=theme_dir,
+        site_dir=site_dir,
+        **kwargs,
+    )
+
+    live_server = livereload in ('dirty', 'livereload')
+    dirty = livereload == 'dirty'
+
+    def builder(config=None):
         log.info("Building documentation...")
-        config = load_config(
-            config_file=config_file,
-            dev_addr=dev_addr,
-            strict=strict,
-            theme=theme,
-            theme_dir=theme_dir,
-            site_dir=site_dir,
-            **kwargs,
-        )
+        if config is None:
+            config = get_config()
 
         # combine CLI watch arguments with config file values
         if config["watch"] is None:
@@ -63,14 +71,14 @@ def serve(
         # Override a few config settings after validation
         config['site_url'] = 'http://{}{}'.format(config['dev_addr'], mount_path(config))
 
-        live_server = livereload in ['dirty', 'livereload']
-        dirty = livereload == 'dirty'
         build(config, live_server=live_server, dirty=dirty)
-        return config
+
+    config = get_config()
+    config['plugins'].run_event('startup', command='serve', dirty=dirty)
 
     try:
         # Perform the initial build
-        config = builder()
+        builder(config)
 
         host, port = config['dev_addr']
         server = LiveReloadServer(
@@ -86,7 +94,7 @@ def serve(
 
         server.error_handler = error_handler
 
-        if livereload in ['livereload', 'dirty']:
+        if live_server:
             # Watch the documentation files, the config file and the theme files.
             server.watch(config['docs_dir'])
             server.watch(config['config_file_path'])
@@ -114,5 +122,6 @@ def serve(
         # Avoid ugly, unhelpful traceback
         raise Abort(f'{type(e).__name__}: {e}')
     finally:
+        config['plugins'].run_event('shutdown')
         if isdir(site_dir):
             shutil.rmtree(site_dir)
diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py
index 0c421866..caeb9989 100644
--- a/mkdocs/config/config_options.py
+++ b/mkdocs/config/config_options.py
@@ -5,7 +5,7 @@ import os
 import sys
 import traceback
 import typing as t
-from typing import NamedTuple
+from typing import Dict, NamedTuple
 from urllib.parse import urlsplit, urlunsplit
 
 import markdown
@@ -705,6 +705,7 @@ class Plugins(OptionallyRequired):
         super().__init__(**kwargs)
         self.installed_plugins = plugins.get_plugins()
         self.config_file_path = None
+        self.plugin_cache: Dict[str, plugins.BasePlugin] = {}
 
     def pre_validation(self, config, key_name):
         self.config_file_path = config.config_file_path
@@ -738,15 +739,22 @@ class Plugins(OptionallyRequired):
         if not isinstance(config, dict):
             raise ValidationError(f"Invalid config options for the '{name}' plugin.")
 
-        Plugin = self.installed_plugins[name].load()
+        try:
+            plugin = self.plugin_cache[name]
+        except KeyError:
+            Plugin = self.installed_plugins[name].load()
 
-        if not issubclass(Plugin, plugins.BasePlugin):
-            raise ValidationError(
-                f'{Plugin.__module__}.{Plugin.__name__} must be a subclass of'
-                f' {plugins.BasePlugin.__module__}.{plugins.BasePlugin.__name__}'
-            )
+            if not issubclass(Plugin, plugins.BasePlugin):
+                raise ValidationError(
+                    f'{Plugin.__module__}.{Plugin.__name__} must be a subclass of'
+                    f' {plugins.BasePlugin.__module__}.{plugins.BasePlugin.__name__}'
+                )
+
+            plugin = Plugin()
+
+            if hasattr(plugin, 'on_startup') or hasattr(plugin, 'on_shutdown'):
+                self.plugin_cache[name] = plugin
 
-        plugin = Plugin()
         errors, warnings = plugin.load_config(config, self.config_file_path)
         self.warnings.extend(warnings)
         errors_message = '\n'.join(f"Plugin '{name}' value: '{x}'. Error: {y}" for x, y in errors)
diff --git a/mkdocs/livereload/__init__.py b/mkdocs/livereload/__init__.py
index e74f4ff4..c0c99bdf 100644
--- a/mkdocs/livereload/__init__.py
+++ b/mkdocs/livereload/__init__.py
@@ -171,7 +171,7 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
                 self._visible_epoch = self._wanted_epoch
                 self._epoch_cond.notify_all()
 
-    def shutdown(self) -> None:
+    def shutdown(self, wait=False) -> None:
         self.observer.stop()
         with self._rebuild_cond:
             self._shutdown = True
@@ -179,6 +179,7 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
 
         if self.serve_thread.is_alive():
             super().shutdown()
+        if wait:
             self.serve_thread.join()
             self.observer.join()
 
diff --git a/mkdocs/plugins.py b/mkdocs/plugins.py
index c7afc758..b38e4ff7 100644
--- a/mkdocs/plugins.py
+++ b/mkdocs/plugins.py
@@ -14,6 +14,11 @@ if sys.version_info >= (3, 10):
 else:
     from importlib_metadata import EntryPoint, entry_points
 
+if sys.version_info >= (3, 8):
+    from typing import Literal
+else:
+    from typing_extensions import Literal
+
 import jinja2.environment
 
 from mkdocs.config.base import Config, ConfigErrors, ConfigWarnings, PlainConfigSchema
@@ -61,16 +66,49 @@ class BasePlugin:
 
         return self.config.validate()
 
-    # (Note that event implementations shouldn't actually be static methods in subclasses)
+    # One-time events
 
-    # Global events
+    def on_startup(self, command: Literal['build', 'gh-deploy', 'serve'], dirty: bool) -> None:
+        """
+        The `startup` event runs once at the very beginning of an `mkdocs` invocation.
+
+        New in MkDocs 1.4.
+
+        The presence of an `on_startup` method (even if empty) migrates the plugin to the new
+        system where the plugin object is kept across builds within one `mkdocs serve`.
+
+        Note that for initializing variables, the `__init__` method is still preferred.
+        For initializing per-build variables (and whenever in doubt), use the `on_config` event.
+
+        Parameters:
+            command: the command that MkDocs was invoked with, e.g. "serve" for `mkdocs serve`.
+            dirty: whether `--dirtyreload` or `--dirty` flags were passed.
+        """
+
+    def on_shutdown(self) -> None:
+        """
+        The `shutdown` event runs once at the very end of an `mkdocs` invocation, before exiting.
+
+        This event is relevant only for support of `mkdocs serve`, otherwise within a
+        single build it's undistinguishable from `on_post_build`.
+
+        New in MkDocs 1.4.
+
+        The presence of an `on_shutdown` method (even if empty) migrates the plugin to the new
+        system where the plugin object is kept across builds within one `mkdocs serve`.
+
+        Note the `on_post_build` method is still preferred for cleanups, when possible, as it has
+        a much higher chance of actually triggering. `on_shutdown` is "best effort" because it
+        relies on detecting a graceful shutdown of MkDocs.
+        """
 
     def on_serve(
         self, server: LiveReloadServer, config: Config, builder: Callable
     ) -> Optional[LiveReloadServer]:
         """
         The `serve` event is only called when the `serve` command is used during
-        development. It is passed the `Server` instance which can be modified before
+        development. It runs only once, after the first build finishes.
+        It is passed the `Server` instance which can be modified before
         it is activated. For example, additional files or directories could be added
         to the list of "watched" files for auto-reloading.
 
@@ -84,6 +122,8 @@ class BasePlugin:
         """
         return server
 
+    # Global events
+
     def on_config(self, config: Config) -> Optional[Config]:
         """
         The `config` event is the first event called on build and is run immediately
@@ -395,7 +435,10 @@ class PluginCollection(OrderedDict):
         be modified by the event method.
         """
         pass_item = item is not None
-        for method in self.events[name]:
+        events = self.events[name]
+        if events:
+            log.debug(f'Running {len(events)} `{name}` events')
+        for method in events:
             if pass_item:
                 result = method(item, **kwargs)
             else:
diff --git a/requirements/project-min.txt b/requirements/project-min.txt
index ce88eeae..8919601d 100644
--- a/requirements/project-min.txt
+++ b/requirements/project-min.txt
@@ -7,6 +7,7 @@ watchdog==2.0.0
 ghp-import==1.0
 pyyaml_env_tag==0.1
 importlib_metadata==4.3; python_version < "3.10"
+typing_extensions==3.10; python_version < "3.8"
 packaging==20.5
 mergedeep==1.3.4
 babel==2.9.0
diff --git a/requirements/project.txt b/requirements/project.txt
index b952b7ba..dac875a9 100644
--- a/requirements/project.txt
+++ b/requirements/project.txt
@@ -7,6 +7,7 @@ watchdog>=2.0.0
 ghp-import>=1.0
 pyyaml_env_tag>=0.1
 importlib_metadata>=4.3; python_version < "3.10"
+typing_extensions>=3.10; python_version < "3.8"
 packaging>=20.5
 mergedeep>=1.3.4
 babel>=2.9.0
diff --git a/setup.py b/setup.py
index e9f5856c..05304b39 100755
--- a/setup.py
+++ b/setup.py
@@ -72,6 +72,7 @@ setup(
         'ghp-import>=1.0',
         'pyyaml_env_tag>=0.1',
         'importlib_metadata>=4.3; python_version < "3.10"',
+        'typing_extensions>=3.10; python_version < "3.8"',
         'packaging>=20.5',
         'mergedeep>=1.3.4'
     ],


Test patch:
diff --git a/mkdocs/tests/plugin_tests.py b/mkdocs/tests/plugin_tests.py
index 7b1f6866..721e82f8 100644
--- a/mkdocs/tests/plugin_tests.py
+++ b/mkdocs/tests/plugin_tests.py
@@ -97,6 +97,8 @@ class TestPluginCollection(unittest.TestCase):
         self.assertEqual(
             collection.events,
             {
+                'startup': [],
+                'shutdown': [],
                 'serve': [],
                 'config': [],
                 'pre_build': [plugin.on_pre_build],