From 2495d3e6bb5720ae23e35caf16888f0c7f37ede0 Mon Sep 17 00:00:00 2001
From: Matt Wang <matt@matthewwang.me>
Date: Sat, 7 Jan 2023 16:08:45 -0800
Subject: [PATCH] refactor: modularize site components (#1058)

Hi everyone, this is a large refactoring PR that looks to **modularize site components** following the discussion in #959. At the top-level, it:

- moves icons, the sidebar, header (navbar, search, aux links), footer, and mermaid components of the `default` layout into their own `_includes`
- creates a new `minimal` layout that does not render the header or sidebar as a proof-of-concept for the composability of components
- documents all existing and new layouts (including vendor code) in the "Customization" section

An important goal of this PR is for it to be **just code motion and flexibility**: there should be **zero impact** on the average end user that only consumes the `default` theme.

The next few sections go in-depth on each of the listed changes.

### new components

The `default` layout contains a "list" of all relevant components. Importantly, some of these components have sub-components:

- the header is split into the search bar, custom code, and aux links
- the icons include imports different icon components, some of which are conditionally imported by feature guards

There are also candidates for future splits and joins:

- the sidebar could be split into navigation, collections, external link, and header/footer code
- the "search footer" could be joined with other search code, which would make it easier to "include search" in one go; *however, this is a markup change*
- @kevinlin1 has pointed out that there is some leakage between the sidebar (which computes parents/grandparents) and the breadcrumbs (which needs them to render). He's graciously added a bandaid fix to `minimal` (which does not render the sidebar). However, in the long term, we should either:
    - calculate this in a parent and pass the information to both components
    - change how this works entirely (which may happen with multi-level navigation)

@pdmosses has done a great job outlining this and more in his [Modular Layouts test site](https://pdmosses.github.io/modular-layouts/docs/main/).

### minimal layout

Based on @kevinlin1's use-case in just-the-class (see: his [Winter 2023 CSE 373 site](https://courses.cs.washington.edu/courses/cse373/23wi/)), we've created a first-class `minimal` layout that does not render the sidebar or header.

In a [comment](https://github.com/just-the-docs/just-the-docs/pull/1058#discussion_r1057015039), Kevin has indicated that we can re-add the search bar in the minimal layout; however, it seems like this would be a code change. I think we should punt this to a future issue/PR.

@pdmosses has also discussed the confusion of `minimal` as a layout and its meaning in inheritance. I've added a note in documentation to clarify the (lack of) inheritance relationship.

### documentation

I've written documentation in the "Customization" page / [Custom layouts and includes](https://deploy-preview-1058--just-the-docs.netlify.app/docs/customization/#custom-layouts-and-includes) section explaining:

- generally, that we use includes/layouts (and pointing to docs)
- the `default` layout and its constituent components (with a warning about name collisions)
- creating alternative layouts with `minimal` as an example
- the inheritance chain of layouts and the vendor layouts that we consume

I've also created (and linked to) a [minimal layout test](https://deploy-preview-1058--just-the-docs.netlify.app/docs/minimal-test/) that is currently a copy of the markdown kitchen sink but with the minimal layout. I think there's room to improve this in the future.

### future work

I think there's a lot we can do. Let me break this into various sections.

Potential follow-ups before `v0.4.0`:

- re-including search in `minimal` (anticipating a minor code change)
- fixing the leakage of parent/grandparent information between the sidebar and breadcrumbs (anticipating no end-user code change, but good to evaluate separately and discuss)
- heavily document this in the migration guide (#1059) and in our RC4 release docs
- improve semantic markup for components (ex `main`, `nav`)

Related work in later minor versions:

- split up components into smaller components
- allow users to easily customize new layouts using frontmatter (see @kevinlin1's [comment in #959](https://github.com/just-the-docs/just-the-docs/issues/959#issuecomment-1249755249))

Related work for `v1.0` (i.e. a major breaking change):

- rename and better categorize existing includes
    - standardizing the "custom" includes
    - moving other components to the `components/` folder (ex `head`, `nav`)
    - potentially: less confusing naming for various components
- potentially separate the search and header as components, so that they are completely independent

Tangentially related work:

- more flexible grid (see @JPrevost's [comment in this PR thread](https://github.com/just-the-docs/just-the-docs/pull/1058#issuecomment-1363314610))
- a formal [feature model](https://en.wikipedia.org/wiki/Feature_model) of JTD, documenting feature dependence (see @pdmosses's [comment in this PR thread](https://github.com/just-the-docs/just-the-docs/pull/1058#issuecomment-1365414023))
- better annotate new features (motivated by writing these docs)
    - we should add "New" to new features :)
    - we should note when a feature was introduced (I think this is a core part of most software documentation)
    - we should annotate things that are "Advanced" in so far as the average Just the Docs user will not use them / they require significant Jekyll knowledge


---

Closes #959.
---
 _includes/components/aux_nav.html       |  15 ++
 _includes/components/breadcrumbs.html   |  15 ++
 _includes/components/children_nav.html  |   9 +
 _includes/components/footer.html        |  34 ++++
 _includes/components/header.html        |  11 ++
 _includes/components/mermaid.html       |   5 +
 _includes/components/search_footer.html |   7 +
 _includes/components/search_header.html |   9 +
 _includes/components/sidebar.html       |  69 ++++++++
 _includes/icons/code_copy.html          |   2 +-
 _includes/icons/document.html           |   6 +
 _includes/icons/expand.html             |   6 +
 _includes/icons/icons.html              |  13 ++
 _includes/icons/link.html               |   6 +
 _includes/icons/menu.html               |   6 +
 _includes/icons/search.html             |   6 +
 _layouts/default.html                   | 217 +-----------------------
 _layouts/minimal.html                   |  60 +++++++
 assets/js/just-the-docs.js              |   4 +-
 docs/customization.md                   | 155 +++++++++++++++++
 docs/minimal-test.md                    |   9 +
 21 files changed, 453 insertions(+), 211 deletions(-)
 create mode 100644 _includes/components/aux_nav.html
 create mode 100644 _includes/components/breadcrumbs.html
 create mode 100644 _includes/components/children_nav.html
 create mode 100644 _includes/components/footer.html
 create mode 100644 _includes/components/header.html
 create mode 100644 _includes/components/mermaid.html
 create mode 100644 _includes/components/search_footer.html
 create mode 100644 _includes/components/search_header.html
 create mode 100644 _includes/components/sidebar.html
 create mode 100644 _includes/icons/document.html
 create mode 100644 _includes/icons/expand.html
 create mode 100644 _includes/icons/icons.html
 create mode 100644 _includes/icons/link.html
 create mode 100644 _includes/icons/menu.html
 create mode 100644 _includes/icons/search.html
 create mode 100644 _layouts/minimal.html
 create mode 100644 docs/minimal-test.md

diff --git a/_includes/components/aux_nav.html b/_includes/components/aux_nav.html
new file mode 100644
index 00000000..f327da68
--- /dev/null
+++ b/_includes/components/aux_nav.html
@@ -0,0 +1,15 @@
+<nav aria-label="Auxiliary" class="aux-nav">
+  <ul class="aux-nav-list">
+    {% for link in site.aux_links %}
+      <li class="aux-nav-list-item">
+        <a href="{{ link.last }}" class="site-button"
+          {% if site.aux_links_new_tab %}
+          target="_blank" rel="noopener noreferrer"
+          {% endif %}
+        >
+          {{ link.first }}
+        </a>
+      </li>
+    {% endfor %}
+  </ul>
+</nav>
diff --git a/_includes/components/breadcrumbs.html b/_includes/components/breadcrumbs.html
new file mode 100644
index 00000000..f1bc4885
--- /dev/null
+++ b/_includes/components/breadcrumbs.html
@@ -0,0 +1,15 @@
+{% unless page.url == "/" %}
+  {% if page.parent %}
+    <nav aria-label="Breadcrumb" class="breadcrumb-nav">
+      <ol class="breadcrumb-nav-list">
+        {% if page.grand_parent %}
+          <li class="breadcrumb-nav-list-item"><a href="{{ first_level_url }}">{{ page.grand_parent }}</a></li>
+          <li class="breadcrumb-nav-list-item"><a href="{{ second_level_url }}">{{ page.parent }}</a></li>
+        {% else %}
+          <li class="breadcrumb-nav-list-item"><a href="{{ first_level_url }}">{{ page.parent }}</a></li>
+        {% endif %}
+        <li class="breadcrumb-nav-list-item"><span>{{ page.title }}</span></li>
+      </ol>
+    </nav>
+  {% endif %}
+{% endunless %}
diff --git a/_includes/components/children_nav.html b/_includes/components/children_nav.html
new file mode 100644
index 00000000..e76f98d1
--- /dev/null
+++ b/_includes/components/children_nav.html
@@ -0,0 +1,9 @@
+<hr>
+{% include toc_heading_custom.html %}
+<ul>
+  {% for child in include.toc_list %}
+    <li>
+      <a href="{{ child.url | relative_url }}">{{ child.title }}</a>{% if child.summary %} - {{ child.summary }}{% endif %}
+    </li>
+  {% endfor %}
+</ul>
diff --git a/_includes/components/footer.html b/_includes/components/footer.html
new file mode 100644
index 00000000..01b2c235
--- /dev/null
+++ b/_includes/components/footer.html
@@ -0,0 +1,34 @@
+{% capture footer_custom %}
+  {%- include footer_custom.html -%}
+{% endcapture %}
+{% if footer_custom != "" or site.last_edit_timestamp or site.gh_edit_link %}
+  <hr>
+  <footer>
+    {% if site.back_to_top %}
+      <p><a href="#top" id="back-to-top">{{ site.back_to_top_text }}</a></p>
+    {% endif %}
+
+    {{ footer_custom }}
+
+    {% if site.last_edit_timestamp or site.gh_edit_link %}
+      <div class="d-flex mt-2">
+        {% if site.last_edit_timestamp and site.last_edit_time_format and page.last_modified_date %}
+          <p class="text-small text-grey-dk-000 mb-0 mr-2">
+            Page last modified: <span class="d-inline-block">{{ page.last_modified_date | date: site.last_edit_time_format }}</span>.
+          </p>
+        {% endif %}
+        {% if
+          site.gh_edit_link and
+          site.gh_edit_link_text and
+          site.gh_edit_repository and
+          site.gh_edit_branch and
+          site.gh_edit_view_mode
+        %}
+          <p class="text-small text-grey-dk-000 mb-0">
+            <a href="{{ site.gh_edit_repository }}/{{ site.gh_edit_view_mode }}/{{ site.gh_edit_branch }}{% if site.gh_edit_source %}/{{ site.gh_edit_source }}{% endif %}{% if page.collection and site.collections_dir %}/{{ site.collections_dir }}{% endif %}/{{ page.path }}" id="edit-this-page">{{ site.gh_edit_link_text }}</a>
+          </p>
+        {% endif %}
+      </div>
+    {% endif %}
+  </footer>
+{% endif %}
diff --git a/_includes/components/header.html b/_includes/components/header.html
new file mode 100644
index 00000000..f9c3386b
--- /dev/null
+++ b/_includes/components/header.html
@@ -0,0 +1,11 @@
+<div id="main-header" class="main-header">
+  {% if site.search_enabled != false %}
+    {% include components/search_header.html %}
+  {% else %}
+    <div></div>
+  {% endif %}
+  {% include header_custom.html %}
+  {% if site.aux_links %}
+    {% include components/aux_nav.html %}
+  {% endif %}
+</div>
diff --git a/_includes/components/mermaid.html b/_includes/components/mermaid.html
new file mode 100644
index 00000000..d6923e09
--- /dev/null
+++ b/_includes/components/mermaid.html
@@ -0,0 +1,5 @@
+<script>
+  var config = {% include mermaid_config.js %};
+  mermaid.initialize(config);
+  window.mermaid.init(undefined, document.querySelectorAll('.language-mermaid'));
+</script>
diff --git a/_includes/components/search_footer.html b/_includes/components/search_footer.html
new file mode 100644
index 00000000..fb4fe51b
--- /dev/null
+++ b/_includes/components/search_footer.html
@@ -0,0 +1,7 @@
+{% if site.search.button %}
+<a href="#" id="search-button" class="search-button">
+  <svg viewBox="0 0 24 24" class="icon"><use xlink:href="#svg-search"></use></svg>
+</a>
+{% endif %}
+
+<div class="search-overlay"></div>
diff --git a/_includes/components/search_header.html b/_includes/components/search_header.html
new file mode 100644
index 00000000..98425d5b
--- /dev/null
+++ b/_includes/components/search_header.html
@@ -0,0 +1,9 @@
+{% capture search_placeholder %}{% include search_placeholder_custom.html %}{% endcapture %}
+
+<div class="search">
+  <div class="search-input-wrap">
+    <input type="text" id="search-input" class="search-input" tabindex="0" placeholder="{{ search_placeholder | strip_html | strip }}" aria-label="{{ search_placeholder | strip_html| strip }}" autocomplete="off">
+    <label for="search-input" class="search-label"><svg viewBox="0 0 24 24" class="search-icon"><use xlink:href="#svg-search"></use></svg></label>
+  </div>
+  <div id="search-results" class="search-results"></div>
+</div>
diff --git a/_includes/components/sidebar.html b/_includes/components/sidebar.html
new file mode 100644
index 00000000..c9c1bb8f
--- /dev/null
+++ b/_includes/components/sidebar.html
@@ -0,0 +1,69 @@
+<div class="side-bar">
+  <div class="site-header">
+    <a href="{{ '/' | relative_url }}" class="site-title lh-tight">{% include title.html %}</a>
+    <a href="#" id="menu-button" class="site-button">
+      <svg viewBox="0 0 24 24" class="icon"><use xlink:href="#svg-menu"></use></svg>
+    </a>
+  </div>
+  <nav aria-label="Main" id="site-nav" class="site-nav">
+    {% assign pages_top_size = site.html_pages
+        | where_exp:"item", "item.title != nil"
+        | where_exp:"item", "item.parent == nil"
+        | where_exp:"item", "item.nav_exclude != true"
+        | size %}
+    {% if pages_top_size > 0 %}
+      {% include nav.html pages=site.html_pages key=nil %}
+    {% endif %}
+    {%- if site.nav_external_links -%}
+      <ul class="nav-list">
+        {%- for node in site.nav_external_links -%}
+          <li class="nav-list-item external">
+            <a href="{{ node.url | absolute_url }}" class="nav-list-link external">
+              {{ node.title }}
+              {% unless node.hide_icon %}<svg viewBox="0 0 24 24" aria-labelledby="svg-external-link-title"><use xlink:href="#svg-external-link"></use></svg>{% endunless %}
+            </a>
+          </li>
+        {%- endfor -%}
+      </ul>
+    {%- endif -%}
+    {% if site.just_the_docs.collections %}
+      {% assign collections_size = site.just_the_docs.collections | size %}
+      {% for collection_entry in site.just_the_docs.collections %}
+        {% assign collection_key = collection_entry[0] %}
+        {% assign collection_value = collection_entry[1] %}
+        {% assign collection = site[collection_key] %}
+        {% if collection_value.nav_exclude != true %}
+          {% if collections_size > 1 or pages_top_size > 0 %}
+            {% if collection_value.nav_fold == true %}
+              <ul class="nav-list nav-category-list">
+                <li class="nav-list-item{% if page.collection == collection_key %} active{% endif %}">
+                  {%- if collection.size > 0 -%}
+                  <a href="#" class="nav-list-expander"><svg viewBox="0 0 24 24"><use xlink:href="#svg-arrow-right"></use></svg></a>
+                  {%- endif -%}
+                  <div class="nav-category">{{ collection_value.name }}</div>
+                  {% include nav.html pages=collection key=collection_key %}
+                </li>
+              </ul>
+            {% else %}
+              <div class="nav-category">{{ collection_value.name }}</div>
+              {% include nav.html pages=collection key=collection_key %}
+            {% endif %}
+          {% else %}
+            {% include nav.html pages=collection key=collection_key %}
+          {% endif %}
+        {% endif %}
+      {% endfor %}
+    {% endif %}
+  </nav>
+
+  {% capture nav_footer_custom %}
+    {%- include nav_footer_custom.html -%}
+  {% endcapture %}
+  {% if nav_footer_custom != "" %}
+    {{ nav_footer_custom }}
+  {% else %}
+    <footer class="site-footer">
+      This site uses <a href="https://github.com/just-the-docs/just-the-docs">Just the Docs</a>, a documentation theme for Jekyll.
+    </footer>
+  {% endif %}
+</div>
diff --git a/_includes/icons/code_copy.html b/_includes/icons/code_copy.html
index 02f5068c..0538a232 100644
--- a/_includes/icons/code_copy.html
+++ b/_includes/icons/code_copy.html
@@ -12,4 +12,4 @@
     <path d="M6.5 0A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3Zm3 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3Z"/>
     <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1A2.5 2.5 0 0 1 9.5 5h-3A2.5 2.5 0 0 1 4 2.5v-1Zm6.854 7.354-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 0 1 .708-.708L7.5 10.793l2.646-2.647a.5.5 0 0 1 .708.708Z"/>
   </svg>
-</symbol>
\ No newline at end of file
+</symbol>
diff --git a/_includes/icons/document.html b/_includes/icons/document.html
new file mode 100644
index 00000000..c09e8a5c
--- /dev/null
+++ b/_includes/icons/document.html
@@ -0,0 +1,6 @@
+<symbol id="svg-doc" viewBox="0 0 24 24">
+  <title>Document</title>
+  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file">
+    <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline>
+  </svg>
+</symbol>
diff --git a/_includes/icons/expand.html b/_includes/icons/expand.html
new file mode 100644
index 00000000..79921a56
--- /dev/null
+++ b/_includes/icons/expand.html
@@ -0,0 +1,6 @@
+<symbol id="svg-arrow-right" viewBox="0 0 24 24">
+  <title>Expand</title>
+  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right">
+    <polyline points="9 18 15 12 9 6"></polyline>
+  </svg>
+</symbol>
diff --git a/_includes/icons/icons.html b/_includes/icons/icons.html
new file mode 100644
index 00000000..007a495b
--- /dev/null
+++ b/_includes/icons/icons.html
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
+  {% include icons/link.html %}
+  {% include icons/menu.html %}
+  {% include icons/expand.html %}
+  {% include icons/external_link.html %}
+  {% if site.search_enabled != false %}
+    {% include icons/document.html %}
+    {% include icons/search.html %}
+  {% endif %}
+  {% if site.enable_copy_code_button != false %}
+    {% include icons/code_copy.html %}
+  {% endif %}
+</svg>
diff --git a/_includes/icons/link.html b/_includes/icons/link.html
new file mode 100644
index 00000000..de24be70
--- /dev/null
+++ b/_includes/icons/link.html
@@ -0,0 +1,6 @@
+<symbol id="svg-link" viewBox="0 0 24 24">
+  <title>Link</title>
+  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link">
+    <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
+  </svg>
+</symbol>
diff --git a/_includes/icons/menu.html b/_includes/icons/menu.html
new file mode 100644
index 00000000..d2565758
--- /dev/null
+++ b/_includes/icons/menu.html
@@ -0,0 +1,6 @@
+<symbol id="svg-menu" viewBox="0 0 24 24">
+  <title>Menu</title>
+  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-menu">
+    <line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line>
+  </svg>
+</symbol>
diff --git a/_includes/icons/search.html b/_includes/icons/search.html
new file mode 100644
index 00000000..8f72c6a2
--- /dev/null
+++ b/_includes/icons/search.html
@@ -0,0 +1,6 @@
+<symbol id="svg-search" viewBox="0 0 24 24">
+  <title>Search</title>
+  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search">
+    <circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line>
+  </svg>
+</symbol>
diff --git a/_layouts/default.html b/_layouts/default.html
index 61ebc06f..3bc0e0ae 100644
--- a/_layouts/default.html
+++ b/_layouts/default.html
@@ -8,161 +8,12 @@ layout: table_wrappers
 {% include head.html %}
 <body>
   <a class="skip-to-main" href="#main-content">Skip to main content</a>
-  <svg xmlns="http://www.w3.org/2000/svg" class="d-none" >
-    <symbol id="svg-link" viewBox="0 0 24 24">
-      <title>Link</title>
-      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link">
-        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
-      </svg>
-    </symbol>
-    <symbol id="svg-search" viewBox="0 0 24 24">
-      <title>Search</title>
-      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search">
-        <circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line>
-      </svg>
-    </symbol>
-    <symbol id="svg-menu" viewBox="0 0 24 24">
-      <title>Menu</title>
-      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-menu">
-        <line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line>
-      </svg>
-    </symbol>
-    <symbol id="svg-arrow-right" viewBox="0 0 24 24">
-      <title>Expand</title>
-      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right">
-        <polyline points="9 18 15 12 9 6"></polyline>
-      </svg>
-    </symbol>
-    <symbol id="svg-doc" viewBox="0 0 24 24">
-      <title>Document</title>
-      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file">
-        <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline>
-      </svg>
-    </symbol>
-    {% include icons/external_link.html %}
-    {% include icons/code_copy.html %}
-  </svg>
-
-  <div class="side-bar">
-    <div class="site-header">
-      <a href="{{ '/' | relative_url }}" class="site-title lh-tight">{% include title.html %}</a>
-      <a href="#" id="menu-button" class="site-button">
-        <svg viewBox="0 0 24 24" class="icon"><use xlink:href="#svg-menu"></use></svg>
-      </a>
-    </div>
-    <nav aria-label="Main" id="site-nav" class="site-nav">
-      {% assign pages_top_size = site.html_pages
-          | where_exp:"item", "item.title != nil"
-          | where_exp:"item", "item.parent == nil"
-          | where_exp:"item", "item.nav_exclude != true"
-          | size %}
-      {% if pages_top_size > 0 %}
-        {% include nav.html pages=site.html_pages key=nil %}
-      {% endif %}
-      {%- if site.nav_external_links -%}
-        <ul class="nav-list">
-          {%- for node in site.nav_external_links -%}
-            <li class="nav-list-item external">
-              <a href="{{ node.url | absolute_url }}" class="nav-list-link external">
-                {{ node.title }}
-                {% unless node.hide_icon %}<svg viewBox="0 0 24 24" aria-labelledby="svg-external-link-title"><use xlink:href="#svg-external-link"></use></svg>{% endunless %}
-              </a>
-            </li>
-          {%- endfor -%}
-        </ul>
-      {%- endif -%}
-      {% if site.just_the_docs.collections %}
-        {% assign collections_size = site.just_the_docs.collections | size %}
-        {% for collection_entry in site.just_the_docs.collections %}
-          {% assign collection_key = collection_entry[0] %}
-          {% assign collection_value = collection_entry[1] %}
-          {% assign collection = site[collection_key] %}
-          {% if collection_value.nav_exclude != true %}
-            {% if collections_size > 1 or pages_top_size > 0 %}
-              {% if collection_value.nav_fold == true %}
-                <ul class="nav-list nav-category-list">
-                  <li class="nav-list-item{% if page.collection == collection_key %} active{% endif %}">
-                    {%- if collection.size > 0 -%}
-                    <a href="#" class="nav-list-expander"><svg viewBox="0 0 24 24"><use xlink:href="#svg-arrow-right"></use></svg></a>
-                    {%- endif -%}
-                    <div class="nav-category">{{ collection_value.name }}</div>
-                    {% include nav.html pages=collection key=collection_key %}
-                  </li>
-                </ul>
-              {% else %}
-                <div class="nav-category">{{ collection_value.name }}</div>
-                {% include nav.html pages=collection key=collection_key %}
-              {% endif %}
-            {% else %}
-              {% include nav.html pages=collection key=collection_key %}
-            {% endif %}
-          {% endif %}
-        {% endfor %}
-      {% endif %}
-    </nav>
-
-    {% capture nav_footer_custom %}
-      {%- include nav_footer_custom.html -%}
-    {% endcapture %}
-    {% if nav_footer_custom != "" %}
-      {{ nav_footer_custom }}
-    {% else %}
-      <footer class="site-footer">
-        This site uses <a href="https://github.com/just-the-docs/just-the-docs">Just the Docs</a>, a documentation theme for Jekyll.
-      </footer>
-    {% endif %}
-  </div>
+  {% include icons/icons.html %}
+  {% include components/sidebar.html %}
   <div class="main" id="top">
-    <div id="main-header" class="main-header">
-      {% if site.search_enabled != false %}
-
-        {% capture search_placeholder %}{% include search_placeholder_custom.html %}{% endcapture %}
-
-        <div class="search">
-          <div class="search-input-wrap">
-            <input type="text" id="search-input" class="search-input" tabindex="0" placeholder="{{ search_placeholder | strip_html | strip }}" aria-label="{{ search_placeholder | strip_html| strip }}" autocomplete="off">
-            <label for="search-input" class="search-label"><svg viewBox="0 0 24 24" class="search-icon"><use xlink:href="#svg-search"></use></svg></label>
-          </div>
-          <div id="search-results" class="search-results"></div>
-        </div>
-      {% else %}
-        <div></div>
-      {% endif %}
-      {% include header_custom.html %}
-      {% if site.aux_links %}
-        <nav aria-label="Auxiliary" class="aux-nav">
-          <ul class="aux-nav-list">
-            {% for link in site.aux_links %}
-              <li class="aux-nav-list-item">
-                <a href="{{ link.last }}" class="site-button"
-                  {% if site.aux_links_new_tab %}
-                  target="_blank" rel="noopener noreferrer"
-                  {% endif %}
-                >
-                  {{ link.first }}
-                </a>
-              </li>
-            {% endfor %}
-          </ul>
-        </nav>
-      {% endif %}
-    </div>
+    {% include components/header.html %}
     <div id="main-content-wrap" class="main-content-wrap">
-      {% unless page.url == "/" %}
-        {% if page.parent %}
-          <nav aria-label="Breadcrumb" class="breadcrumb-nav">
-            <ol class="breadcrumb-nav-list">
-              {% if page.grand_parent %}
-                <li class="breadcrumb-nav-list-item"><a href="{{ first_level_url }}">{{ page.grand_parent }}</a></li>
-                <li class="breadcrumb-nav-list-item"><a href="{{ second_level_url }}">{{ page.parent }}</a></li>
-              {% else %}
-                <li class="breadcrumb-nav-list-item"><a href="{{ first_level_url }}">{{ page.parent }}</a></li>
-              {% endif %}
-              <li class="breadcrumb-nav-list-item"><span>{{ page.title }}</span></li>
-            </ol>
-          </nav>
-        {% endif %}
-      {% endunless %}
+      {% include components/breadcrumbs.html %}
       <div id="main-content" class="main-content" role="main">
         {% if site.heading_anchors != false %}
           {% include vendor/anchor_headings.html html=content beforeHeading="true" anchorBody="<svg viewBox=\"0 0 16 16\" aria-hidden=\"true\"><use xlink:href=\"#svg-link\"></use></svg>" anchorClass="anchor-heading" anchorAttrs="aria-labelledby=\"%html_id%\"" %}
@@ -171,72 +22,20 @@ layout: table_wrappers
         {% endif %}
 
         {% if page.has_children == true and page.has_toc != false %}
-          <hr>
-          {% include toc_heading_custom.html %}
-          <ul>
-            {% for child in toc_list %}
-              <li>
-                <a href="{{ child.url | relative_url }}">{{ child.title }}</a>{% if child.summary %} - {{ child.summary }}{% endif %}
-              </li>
-            {% endfor %}
-          </ul>
+          {% include components/children_nav.html toc_list=toc_list %}
         {% endif %}
 
-        {% capture footer_custom %}
-          {%- include footer_custom.html -%}
-        {% endcapture %}
-        {% if footer_custom != "" or site.last_edit_timestamp or site.gh_edit_link %}
-          <hr>
-          <footer>
-            {% if site.back_to_top %}
-              <p><a href="#top" id="back-to-top">{{ site.back_to_top_text }}</a></p>
-            {% endif %}
-
-            {{ footer_custom }}
-
-            {% if site.last_edit_timestamp or site.gh_edit_link %}
-              <div class="d-flex mt-2">
-                {% if site.last_edit_timestamp and site.last_edit_time_format and page.last_modified_date %}
-                  <p class="text-small text-grey-dk-000 mb-0 mr-2">
-                    Page last modified: <span class="d-inline-block">{{ page.last_modified_date | date: site.last_edit_time_format }}</span>.
-                  </p>
-                {% endif %}
-                {% if
-                  site.gh_edit_link and
-                  site.gh_edit_link_text and
-                  site.gh_edit_repository and
-                  site.gh_edit_branch and
-                  site.gh_edit_view_mode
-                %}
-                  <p class="text-small text-grey-dk-000 mb-0">
-                    <a href="{{ site.gh_edit_repository }}/{{ site.gh_edit_view_mode }}/{{ site.gh_edit_branch }}{% if site.gh_edit_source %}/{{ site.gh_edit_source }}{% endif %}{% if page.collection and site.collections_dir %}/{{ site.collections_dir }}{% endif %}/{{ page.path }}" id="edit-this-page">{{ site.gh_edit_link_text }}</a>
-                  </p>
-                {% endif %}
-              </div>
-            {% endif %}
-          </footer>
-        {% endif %}
+        {% include components/footer.html %}
 
       </div>
     </div>
-
     {% if site.search_enabled != false %}
-      {% if site.search.button %}
-        <a href="#" id="search-button" class="search-button">
-          <svg viewBox="0 0 24 24" class="icon"><use xlink:href="#svg-search"></use></svg>
-        </a>
-      {% endif %}
-
-      <div class="search-overlay"></div>
+      {% include components/search_footer.html %}
     {% endif %}
   </div>
 
   {% if site.mermaid %}
-  <script>
-    var config = {% include mermaid_config.js %};
-    mermaid.initialize(config);
-    window.mermaid.init(undefined, document.querySelectorAll('.language-mermaid'));
-  </script>
+    {% include components/mermaid.html %}
   {% endif %}
 </body>
 </html>
diff --git a/_layouts/minimal.html b/_layouts/minimal.html
new file mode 100644
index 00000000..5cbac781
--- /dev/null
+++ b/_layouts/minimal.html
@@ -0,0 +1,60 @@
+---
+layout: table_wrappers
+---
+
+<!DOCTYPE html>
+
+<html lang="{{ site.lang | default: 'en-US' }}">
+{% include head.html %}
+<body>
+  <a class="skip-to-main" href="#main-content">Skip to main content</a>
+  {% include icons/icons.html %}
+  {% comment %}
+    This is a bandaid fix to properly render breadcrumbs; as of now, there is some variable leakage between the sidebar component (which computes parents, grandparents) and the breadcrumbs component. We plan to remove this in a future release to deduplicate code.
+
+    For more context, see https://github.com/just-the-docs/just-the-docs/pull/1058#discussion_r1057014053
+  {% endcomment %}
+  {% capture nav %}
+    {% assign pages_top_size = site.html_pages
+        | where_exp:"item", "item.title != nil"
+        | where_exp:"item", "item.parent == nil"
+        | where_exp:"item", "item.nav_exclude != true"
+        | size %}
+    {% if pages_top_size > 0 %}
+      {% include nav.html pages=site.html_pages key=nil %}
+    {% endif %}
+    {% if site.just_the_docs.collections %}
+      {% assign collections_size = site.just_the_docs.collections | size %}
+      {% for collection_entry in site.just_the_docs.collections %}
+        {% assign collection_key = collection_entry[0] %}
+        {% assign collection_value = collection_entry[1] %}
+        {% assign collection = site[collection_key] %}
+        {% if collection_value.nav_exclude != true %}
+          {% include nav.html pages=collection key=collection_key %}
+        {% endif %}
+      {% endfor %}
+    {% endif %}
+  {% endcapture %}
+  <div id="main-content-wrap" class="main-content-wrap" id="top">
+    {% include components/breadcrumbs.html %}
+    <div id="main-content" class="main-content" role="main">
+      {% if site.heading_anchors != false %}
+        {% include vendor/anchor_headings.html html=content beforeHeading="true" anchorBody="<svg viewBox=\"0 0 16 16\" aria-hidden=\"true\"><use xlink:href=\"#svg-link\"></use></svg>" anchorClass="anchor-heading" anchorAttrs="aria-labelledby=\"%html_id%\"" %}
+      {% else %}
+        {{ content }}
+      {% endif %}
+
+      {% if page.has_children == true and page.has_toc != false %}
+        {% include components/children_nav.html toc_list=toc_list %}
+      {% endif %}
+
+      {% include components/footer.html %}
+
+    </div>
+  </div>
+
+  {% if site.mermaid %}
+    {% include components/mermaid.html %}
+  {% endif %}
+</body>
+</html>
diff --git a/assets/js/just-the-docs.js b/assets/js/just-the-docs.js
index 8aabf325..f243f07e 100644
--- a/assets/js/just-the-docs.js
+++ b/assets/js/just-the-docs.js
@@ -74,7 +74,7 @@ function initSearch() {
   request.onload = function(){
     if (request.status >= 200 && request.status < 400) {
       var docs = JSON.parse(request.responseText);
-      
+
       lunr.tokenizer.separator = {{ site.search.tokenizer_separator | default: site.search_tokenizer_separator | default: "/[\s\-/]+/" }}
 
       var index = lunr(function(){
@@ -217,6 +217,7 @@ function searchLoaded(index, docs) {
       resultTitle.classList.add('search-result-title');
       resultLink.appendChild(resultTitle);
 
+      // note: the SVG svg-doc is only loaded as a Jekyll include if site.search_enabled is true; see _includes/icons/icons.html
       var resultDoc = document.createElement('div');
       resultDoc.classList.add('search-result-doc');
       resultDoc.innerHTML = '<svg viewBox="0 0 24 24" class="search-result-icon"><use xlink:href="#svg-doc"></use></svg>';
@@ -488,6 +489,7 @@ jtd.onReady(function(){
 
   var codeBlocks = document.querySelectorAll('div.highlighter-rouge, div.listingblock, figure.highlight');
 
+  // note: the SVG svg-copied and svg-copy is only loaded as a Jekyll include if site.enable_copy_code_button is true; see _includes/icons/icons.html
   var svgCopied =  '<svg viewBox="0 0 24 24" class="copy-icon"><use xlink:href="#svg-copied"></use></svg>';
   var svgCopy =  '<svg viewBox="0 0 24 24" class="copy-icon"><use xlink:href="#svg-copy"></use></svg>';
 
diff --git a/docs/customization.md b/docs/customization.md
index 67070ae9..fe42892c 100644
--- a/docs/customization.md
+++ b/docs/customization.md
@@ -212,3 +212,158 @@ Chercher notre site
 {% endraw %}
 
 would make the placeholder text "Chercher notre site". [Liquid code](https://jekyllrb.com/docs/liquid/) (including [Jekyll variables](https://jekyllrb.com/docs/variables/)) is also supported.
+
+## Custom layouts and includes
+{: .d-inline-block }
+
+New (v0.4.0)
+{: .label .label-green }
+
+Advanced
+{: .label .label-yellow }
+
+Just the Docs uses Jekyll's powerful [layouts](https://jekyllrb.com/docs/layouts/) and [includes](https://jekyllrb.com/docs/includes/) features to generate and compose various elements of the site. Jekyll users and developers can extend or replace existing layouts and includes to customize the entire site layout.
+
+### Default layout and includable components
+
+The `default` layout is inherited by most of the "out-of-the-box" pages provided by Just the Docs. It composes various re-usable components of the site, including the sidebar, navbar, footer, breadcrumbs, and various imports. Most users who create new pages or layouts will inherit from `default`.
+
+Here is a simplified code example of what it looks like:
+
+{% raw %}
+
+```liquid
+<!-- a simplified version of _layouts/default.html -->
+<html>
+{% include head.html %}
+<body>
+  {% include icons/icons.html %}
+  {% include components/sidebar.html %}
+  {% include components/header.html %}
+  {% include components/breadcrumbs.html %}
+
+  {% if site.heading_anchors != false %}
+    {% include vendor/anchor_headings.html html=content ... %}
+  {% else %}
+    {{ content }}
+  {% endif %}
+
+  {% if page.has_children == true and page.has_toc != false %}
+    {% include components/children_nav.html %}
+  {% endif %}
+
+  {% include components/footer.html %}
+
+  {% if site.search_enabled != false %}
+    {% include components/search_footer.html %}
+  {% endif %}
+
+  {% if site.mermaid %}
+    {% include components/mermaid.html %}
+  {% endif %}
+</body>
+</html>
+```
+
+{% endraw %}
+
+#### Component summary
+{: .no_toc }
+
+{: .warning }
+Defining a new `_includes` with the same name as any of these components will significantly change the existing layout. Please proceed with caution when adjusting them.
+
+To briefly summarize each component:
+
+- `_includes/head.html` is the entire `<head>` tag for the site; this imports stylesheets, various JavaScript files (ex: analytics, mermaid, search, and Just the Docs code), and SEO / meta information.
+- `_includes/icons/icons.html` imports all SVG icons that are used throughout the site. Some, such as those relating to search or code snippet copying, are only loaded when those features are enabled.
+- `_includes/components/sidebar.html` renders the sidebar, containing the site header, navigation links, external links, collections, and nav footer.
+- `_includes/components/header.html` renders the navigation header, containing the search bar, custom header, and aux links
+- `_includes/components/breadcrumbs.html` renders the breadcrumbs feature
+- `vendor/anchor_headings.html` is a local copy of Vladimir Jimenez's [jekyll-anchor-headings](https://github.com/allejo/jekyll-anchor-headings) snippet
+- `_includes/components/children_nav.html` renders a list of nav links to child pages on parent pages
+- `_includes/components/footer.html` renders the bottom-of-page footer
+- `_includes/components/search_footer.html` renders DOM elements that are necessary for the search bar to work
+- `_includes/components/mermaid.html` initializes mermaid if the feature is enabled
+
+Each of these components can be overridden individually using the same process described in the [Override includes](#override-includes) section. In particular, the granularity of components should allow users to replace certain components (such as the sidebar) without having to adjust the rest of the code.
+
+Future versions may subdivide components further; we guarantee that we will only place them in folders (ex `components/`, `icons/`, or a new `js/`) to avoid top-level namespace collisions.
+
+### Alternative layouts and example (`minimal`)
+
+Users can develop custom layouts that compose, omit, or add components differently. We provide one first-class example titled `minimal`, inspired by Kevin Lin's work in [just-the-class](https://github.com/kevinlin1/just-the-class). This `minimal` layout does not render the sidebar, header, or search. To see an example, visit the [minimal layout test]({{site.baseurl}}/docs/minimal-test/) page.
+
+Here is a simplified code example of what it looks like:
+
+{% raw %}
+
+```liquid
+<!-- a simplified version of _layouts/minimal.html -->
+<html>
+{% include head.html %}
+<body>
+  {% include icons/icons.html %}
+  {% comment %} Bandaid fix for breadcrumbs here! {% endcomment %}
+  {% include components/breadcrumbs.html %}
+
+  {% if site.heading_anchors != false %}
+    {% include vendor/anchor_headings.html html=content ... %}
+  {% else %}
+    {{ content }}
+  {% endif %}
+
+  {% if page.has_children == true and page.has_toc != false %}
+    {% include components/children_nav.html %}
+  {% endif %}
+
+  {% include components/footer.html %}
+
+  {% if site.mermaid %}
+    {% include components/mermaid.html %}
+  {% endif %}
+</body>
+</html>
+```
+
+{% endraw %}
+
+This layout is packaged in Just the Docs. Users can indicate this alternative layout in page front matter:
+
+{% raw %}
+
+```
+---
+layout: minimal
+title: Minimal layout test
+---
+```
+
+{% endraw %}
+
+Similarly, users and developers can create other alternative layouts using Just the Docs' reusable includable components.
+
+### Default layout and inheritance chain
+
+Under the hood,
+
+- `default` and `minimal` inherit from the `table_wrappers` layout, which wraps all HTML `<table>` tags with a `div .table-wrapper`
+- `table_wrappers` inherits from `vendor/compress`, which is a local copy of Anatol Broder's [jekyll-compress-html](https://github.com/penibelst/jekyll-compress-html) Jekyll plugin
+
+Note that as of now, `minimal` and `default` have no inheritance relationship.
+
+### Overridden default Jekyll layouts
+
+By default, Jekyll (and its default theme `minima`) provide the `about`, `home`, `page`, and `post` layouts. In Just the Docs, we override all of these layouts with the `default` layout. Each of those layouts is simply:
+
+{% raw %}
+
+```
+---
+layout: default
+---
+
+{{ content }}
+```
+
+{% endraw %}
diff --git a/docs/minimal-test.md b/docs/minimal-test.md
new file mode 100644
index 00000000..aa754d31
--- /dev/null
+++ b/docs/minimal-test.md
@@ -0,0 +1,9 @@
+---
+layout: minimal
+title: Minimal layout test
+nav_exclude: true
+---
+
+[Return to main website]({{site.baseurl}}/).
+
+This page demonstrates the packaged `minimal` layout, which does not render the sidebar or header. It can be used for standalone pages. It is also an example of using the new modular site components to define custom layouts; see ["Custom layouts and includes" in the customization docs]({{site.baseurl}}/docs/customization/#custom-layouts-and-includes) for more information.
-- 
GitLab