diff --git a/.gitignore b/.gitignore
index f9cab2f80..debc86d46 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,13 @@
+.DS_Store
+.hugo_build.lock
/.idea
/.vscode
-/public
/dist
-node_modules
+/public
+hugo_stats.json
+node_modules/
nohup.out
-.DS_Store
-trace.out
-.hugo_build.lock
-resources/_gen/images/
\ No newline at end of file
+package-lock.json
+public/
+resources/
+trace.out
\ No newline at end of file
diff --git a/LICENSE.md b/LICENSE.md
index b09cd7856..979711275 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,201 +1,3 @@
-Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
+See [content/LICENSE.md](content/LICENSE.md) for the license of the content of this repository.
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+The theme (layouts, CSS, JavaScript etc.) of this repository has no open source license. It is custom made for the Hugo sites and is not meant for reuse.
\ No newline at end of file
diff --git a/README.md b/README.md
index 7550a93cd..64ba31a48 100644
--- a/README.md
+++ b/README.md
@@ -16,3 +16,12 @@ Please see the [contributing] section for guidelines, examples, and process.
[friends]: https://github.com/gohugoio/hugo/graphs/contributors
[go]: https://go.dev/
[contributing]: https://gohugo.io/contribute/documentation
+
+# Install
+
+```bash
+npm i
+hugo server
+```
+
+**Note:** We're working on removing the need to run `npm i` for local development. Stay tuned.
\ No newline at end of file
diff --git a/assets/css/components/all.css b/assets/css/components/all.css
new file mode 100644
index 000000000..117f19998
--- /dev/null
+++ b/assets/css/components/all.css
@@ -0,0 +1,7 @@
+/* The ordeer of these does not matter. */
+@import "./content.css";
+@import "./fonts.css";
+@import "./helpers.css";
+@import "./shortcodes.css";
+@import "./tableofcontents.css";
+@import "./view-transitions.css";
diff --git a/assets/css/components/chroma.css b/assets/css/components/chroma.css
new file mode 100644
index 000000000..9d4c91f7b
--- /dev/null
+++ b/assets/css/components/chroma.css
@@ -0,0 +1,85 @@
+/* Background */ .bg { background-color: var(--color-light); }
+/* PreWrapper */ .chroma { background-color: var(--color-light); }
+/* Other */ .chroma .x { }
+/* Error */ .chroma .err { color: #a61717; background-color: #e3d2d2 }
+/* CodeLine */ .chroma .cl { }
+/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
+/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
+/* LineHighlight */ .chroma .hl { background-color: #ffffcc }
+/* LineNumbersTable */ .chroma .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
+/* LineNumbers */ .chroma .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
+/* Line */ .chroma .line { display: flex; }
+/* Keyword */ .chroma .k { font-weight: bold }
+/* KeywordConstant */ .chroma .kc { font-weight: bold }
+/* KeywordDeclaration */ .chroma .kd { font-weight: bold }
+/* KeywordNamespace */ .chroma .kn { font-weight: bold }
+/* KeywordPseudo */ .chroma .kp { font-weight: bold }
+/* KeywordReserved */ .chroma .kr { font-weight: bold }
+/* KeywordType */ .chroma .kt { color: #445588; font-weight: bold }
+/* Name */ .chroma .n { }
+/* NameAttribute */ .chroma .na { color: #008080 }
+/* NameBuiltin */ .chroma .nb { color: #999999 }
+/* NameBuiltinPseudo */ .chroma .bp { }
+/* NameClass */ .chroma .nc { color: #445588; font-weight: bold }
+/* NameConstant */ .chroma .no { color: #008080 }
+/* NameDecorator */ .chroma .nd { }
+/* NameEntity */ .chroma .ni { color: #800080 }
+/* NameException */ .chroma .ne { color: #990000; font-weight: bold }
+/* NameFunction */ .chroma .nf { color: #990000; font-weight: bold }
+/* NameFunctionMagic */ .chroma .fm { }
+/* NameLabel */ .chroma .nl { }
+/* NameNamespace */ .chroma .nn { color: #555555 }
+/* NameOther */ .chroma .nx { }
+/* NameProperty */ .chroma .py { }
+/* NameTag */ .chroma .nt { color: #000080 }
+/* NameVariable */ .chroma .nv { color: #008080 }
+/* NameVariableClass */ .chroma .vc { }
+/* NameVariableGlobal */ .chroma .vg { }
+/* NameVariableInstance */ .chroma .vi { }
+/* NameVariableMagic */ .chroma .vm { }
+/* Literal */ .chroma .l { }
+/* LiteralDate */ .chroma .ld { }
+/* LiteralString */ .chroma .s { color: #bb8844 }
+/* LiteralStringAffix */ .chroma .sa { color: #bb8844 }
+/* LiteralStringBacktick */ .chroma .sb { color: #bb8844 }
+/* LiteralStringChar */ .chroma .sc { color: #bb8844 }
+/* LiteralStringDelimiter */ .chroma .dl { color: #bb8844 }
+/* LiteralStringDoc */ .chroma .sd { color: #bb8844 }
+/* LiteralStringDouble */ .chroma .s2 { color: #bb8844 }
+/* LiteralStringEscape */ .chroma .se { color: #bb8844 }
+/* LiteralStringHeredoc */ .chroma .sh { color: #bb8844 }
+/* LiteralStringInterpol */ .chroma .si { color: #bb8844 }
+/* LiteralStringOther */ .chroma .sx { color: #bb8844 }
+/* LiteralStringRegex */ .chroma .sr { color: #808000 }
+/* LiteralStringSingle */ .chroma .s1 { color: #bb8844 }
+/* LiteralStringSymbol */ .chroma .ss { color: #bb8844 }
+/* LiteralNumber */ .chroma .m { color: #009999 }
+/* LiteralNumberBin */ .chroma .mb { color: #009999 }
+/* LiteralNumberFloat */ .chroma .mf { color: #009999 }
+/* LiteralNumberHex */ .chroma .mh { color: #009999 }
+/* LiteralNumberInteger */ .chroma .mi { color: #009999 }
+/* LiteralNumberIntegerLong */ .chroma .il { color: #009999 }
+/* LiteralNumberOct */ .chroma .mo { color: #009999 }
+/* Operator */ .chroma .o { font-weight: bold }
+/* OperatorWord */ .chroma .ow { font-weight: bold }
+/* Punctuation */ .chroma .p { }
+/* Comment */ .chroma .c { color: #999988; font-style: italic }
+/* CommentHashbang */ .chroma .ch { color: #999988; font-style: italic }
+/* CommentMultiline */ .chroma .cm { color: #999988; font-style: italic }
+/* CommentSingle */ .chroma .c1 { color: #999988; font-style: italic }
+/* CommentSpecial */ .chroma .cs { color: #999999; font-weight: bold; font-style: italic }
+/* CommentPreproc */ .chroma .cp { color: #999999; font-weight: bold }
+/* CommentPreprocFile */ .chroma .cpf { color: #999999; font-weight: bold }
+/* Generic */ .chroma .g { }
+/* GenericDeleted */ .chroma .gd { color: #000000; background-color: #ffdddd }
+/* GenericEmph */ .chroma .ge { font-style: italic }
+/* GenericError */ .chroma .gr { color: #aa0000 }
+/* GenericHeading */ .chroma .gh { color: #999999 }
+/* GenericInserted */ .chroma .gi { color: #000000; background-color: #ddffdd }
+/* GenericOutput */ .chroma .go { color: #888888 }
+/* GenericPrompt */ .chroma .gp { color: #555555 }
+/* GenericStrong */ .chroma .gs { font-weight: bold }
+/* GenericSubheading */ .chroma .gu { color: #aaaaaa }
+/* GenericTraceback */ .chroma .gt { color: #aa0000 }
+/* GenericUnderline */ .chroma .gl { text-decoration: underline }
+/* TextWhitespace */ .chroma .w { color: #bbbbbb }
diff --git a/assets/css/components/chroma_dark.css b/assets/css/components/chroma_dark.css
new file mode 100644
index 000000000..0dd9d7bfa
--- /dev/null
+++ b/assets/css/components/chroma_dark.css
@@ -0,0 +1,85 @@
+/* Background */.dark .bg { background-color: var(--color-dark); }
+/* PreWrapper */ .dark .chroma { background-color: var(--color-dark); }
+/* Other */ .dark .chroma .x { }
+/* Error */ .dark .chroma .err { color: #ef6155 }
+/* CodeLine */ .dark .chroma .cl { }
+/* LineTableTD */ .dark .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
+/* LineTable */ .dark .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
+/* LineHighlight */ .dark .chroma .hl { background-color: #ffffcc }
+/* LineNumbersTable */ .dark .chroma .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
+/* LineNumbers */ .dark .chroma .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
+/* Line */ .dark .chroma .line { display: flex; }
+/* Keyword */ .dark .chroma .k { color: #815ba4 }
+/* KeywordConstant */ .dark .chroma .kc { color: #815ba4 }
+/* KeywordDeclaration */ .dark .chroma .kd { color: #815ba4 }
+/* KeywordNamespace */ .dark .chroma .kn { color: #5bc4bf }
+/* KeywordPseudo */ .dark .chroma .kp { color: #815ba4 }
+/* KeywordReserved */ .dark .chroma .kr { color: #815ba4 }
+/* KeywordType */ .dark .chroma .kt { color: #fec418 }
+/* Name */ .dark .chroma .n { }
+/* NameAttribute */ .dark .chroma .na { color: #06b6ef }
+/* NameBuiltin */ .dark .chroma .nb { }
+/* NameBuiltinPseudo */ .dark .chroma .bp { }
+/* NameClass */ .dark .chroma .nc { color: #fec418 }
+/* NameConstant */ .dark .chroma .no { color: #ef6155 }
+/* NameDecorator */ .dark .chroma .nd { color: #5bc4bf }
+/* NameEntity */ .dark .chroma .ni { }
+/* NameException */ .dark .chroma .ne { color: #ef6155 }
+/* NameFunction */ .dark .chroma .nf { color: #06b6ef }
+/* NameFunctionMagic */ .dark .chroma .fm { }
+/* NameLabel */ .dark .chroma .nl { }
+/* NameNamespace */ .dark .chroma .nn { color: #fec418 }
+/* NameOther */ .dark .chroma .nx { color: #06b6ef }
+/* NameProperty */ .dark .chroma .py { }
+/* NameTag */ .dark .chroma .nt { color: #5bc4bf }
+/* NameVariable */ .dark .chroma .nv { color: #ef6155 }
+/* NameVariableClass */ .dark .chroma .vc { }
+/* NameVariableGlobal */ .dark .chroma .vg { }
+/* NameVariableInstance */ .dark .chroma .vi { }
+/* NameVariableMagic */ .dark .chroma .vm { }
+/* Literal */ .dark .chroma .l { color: #f99b15 }
+/* LiteralDate */ .dark .chroma .ld { color: #48b685 }
+/* LiteralString */ .dark .chroma .s { color: #48b685 }
+/* LiteralStringAffix */ .dark .chroma .sa { color: #48b685 }
+/* LiteralStringBacktick */ .dark .chroma .sb { color: #48b685 }
+/* LiteralStringChar */ .dark .chroma .sc { }
+/* LiteralStringDelimiter */ .dark .chroma .dl { color: #48b685 }
+/* LiteralStringDoc */ .dark .chroma .sd { color: #776e71 }
+/* LiteralStringDouble */ .dark .chroma .s2 { color: #48b685 }
+/* LiteralStringEscape */ .dark .chroma .se { color: #f99b15 }
+/* LiteralStringHeredoc */ .dark .chroma .sh { color: #48b685 }
+/* LiteralStringInterpol */ .dark .chroma .si { color: #f99b15 }
+/* LiteralStringOther */ .dark .chroma .sx { color: #48b685 }
+/* LiteralStringRegex */ .dark .chroma .sr { color: #48b685 }
+/* LiteralStringSingle */ .dark .chroma .s1 { color: #48b685 }
+/* LiteralStringSymbol */ .dark .chroma .ss { color: #48b685 }
+/* LiteralNumber */ .dark .chroma .m { color: #f99b15 }
+/* LiteralNumberBin */ .dark .chroma .mb { color: #f99b15 }
+/* LiteralNumberFloat */ .dark .chroma .mf { color: #f99b15 }
+/* LiteralNumberHex */ .dark .chroma .mh { color: #f99b15 }
+/* LiteralNumberInteger */ .dark .chroma .mi { color: #f99b15 }
+/* LiteralNumberIntegerLong */ .dark .chroma .il { color: #f99b15 }
+/* LiteralNumberOct */ .dark .chroma .mo { color: #f99b15 }
+/* Operator */ .dark .chroma .o { color: #5bc4bf }
+/* OperatorWord */ .dark .chroma .ow { color: #5bc4bf }
+/* Punctuation */ .dark .chroma .p { }
+/* Comment */ .dark .chroma .c { color: #776e71 }
+/* CommentHashbang */ .dark .chroma .ch { color: #776e71 }
+/* CommentMultiline */ .dark .chroma .cm { color: #776e71 }
+/* CommentSingle */ .dark .chroma .c1 { color: #776e71 }
+/* CommentSpecial */ .dark .chroma .cs { color: #776e71 }
+/* CommentPreproc */ .dark .chroma .cp { color: #776e71 }
+/* CommentPreprocFile */ .dark .chroma .cpf { color: #776e71 }
+/* Generic */ .dark .chroma .g { }
+/* GenericDeleted */ .dark .chroma .gd { color: #ef6155 }
+/* GenericEmph */ .dark .chroma .ge { font-style: italic }
+/* GenericError */ .dark .chroma .gr { }
+/* GenericHeading */ .dark .chroma .gh { font-weight: bold }
+/* GenericInserted */ .dark .chroma .gi { color: #48b685 }
+/* GenericOutput */ .dark .chroma .go { }
+/* GenericPrompt */ .dark .chroma .gp { color: #776e71; font-weight: bold }
+/* GenericStrong */ .dark .chroma .gs { font-weight: bold }
+/* GenericSubheading */ .dark .chroma .gu { color: #5bc4bf; font-weight: bold }
+/* GenericTraceback */ .dark .chroma .gt { }
+/* GenericUnderline */ .dark .chroma .gl { }
+/* TextWhitespace */ .dark .chroma .w { }
diff --git a/assets/css/components/content.css b/assets/css/components/content.css
new file mode 100644
index 000000000..6ad1c6bc6
--- /dev/null
+++ b/assets/css/components/content.css
@@ -0,0 +1,38 @@
+@import "./chroma_dark.css";
+@import "./chroma.css";
+@import "./highlight.css";
+
+/* Some contrast ratio fixes as reported by Google Page Speed. */
+.chroma .c1 {
+ @apply text-gray-500;
+}
+
+.dark .chroma .c1 {
+ @apply text-gray-400;
+}
+
+.content {
+ @apply prose prose-stone max-w-none dark:prose-invert dark:text-slate-400;
+ /* headings */
+ @apply prose-h6:font-bold;
+ /* lead */
+ @apply prose-lead:text-slate-500 prose-lead:text-xl prose-lead:mt-2 sm:prose-lead:mt-4 prose-lead:leading-relaxed dark:prose-lead:text-slate-400;
+ /* links */
+ @apply prose-a:text-primary prose-a:hover:text-primary/70 prose-a:underline;
+ @apply prose-a:prose-code:underline prose-a:prose-code:hover:text-primary/70 prose-a:prose-code:hover:underline;
+ /* pre */
+ @apply prose-pre:text-gray-800 prose-pre:border-1 prose-pre:border-gray-100 prose-pre:bg-light dark:prose-pre:bg-dark dark:prose-pre:ring-1 dark:prose-pre:ring-slate-300/10;
+ /* code */
+ @apply prose-code:px-0.5 prose-code:text-gray-900 prose-code:dark:text-gray-300 border-none;
+ @apply prose-code:before:hidden prose-code:after:hidden prose-code:font-mono;
+ /* tables */
+ @apply prose-table:border-2 prose-table:border-gray-100 prose-table:dark:border-gray-800 prose-table:relative prose-table:overflow-scroll prose-table:prose-th:font-bold prose-table:prose-th:bg-blue-500 dark:prose-table:prose-th:bg-blue-500/50 prose-table:prose-th:p-2 prose-table:prose-td:p-2 prose-table:prose-th:text-white;
+ /* hr */
+ @apply dark:prose-hr:border-slate-800;
+}
+
+/* This will not match highlighting inside e.g. the code-toggle shortcode. */
+/* For more fine grained control of this, see components/shortcodes.css. */
+.content > .highlight {
+ @apply border-1 border-gray-200 dark:border-slate-600 mt-6 mb-8;
+}
diff --git a/assets/css/components/fonts.css b/assets/css/components/fonts.css
new file mode 100644
index 000000000..06f40b4bf
--- /dev/null
+++ b/assets/css/components/fonts.css
@@ -0,0 +1,15 @@
+@font-face {
+ font-family: "Mulish";
+ font-style: normal;
+ src: url("../fonts/Mulish-VariableFont_wght.ttf") format("truetype");
+ font-weight: 1 999;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "Mulish";
+ font-style: italic;
+ src: url("../fonts/Mulish-Italic-VariableFont_wght.ttf") format("truetype");
+ font-weight: 1 999;
+ font-display: swap;
+}
diff --git a/assets/css/components/helpers.css b/assets/css/components/helpers.css
new file mode 100644
index 000000000..8eb6930b8
--- /dev/null
+++ b/assets/css/components/helpers.css
@@ -0,0 +1,19 @@
+/* Helper class to limit a text block to two lines. */
+.two-lines-ellipsis {
+ display: block;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* Helper class to limit a text block to three lines. */
+.three-lines-ellipsis {
+ display: block;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/assets/css/components/highlight.css b/assets/css/components/highlight.css
new file mode 100644
index 000000000..5f25fe368
--- /dev/null
+++ b/assets/css/components/highlight.css
@@ -0,0 +1,11 @@
+.highlight {
+ @apply bg-light dark:bg-dark rounded-none;
+}
+
+.highlight pre {
+ @apply m-0 p-3 w-full h-full overflow-x-auto dark:border-black rounded-none;
+}
+
+.highlight pre code {
+ @apply m-0 p-0 w-full h-full;
+}
diff --git a/assets/css/components/shortcodes.css b/assets/css/components/shortcodes.css
new file mode 100644
index 000000000..7314d5b20
--- /dev/null
+++ b/assets/css/components/shortcodes.css
@@ -0,0 +1,4 @@
+.shortcode-code {
+ .highlight {
+ }
+}
diff --git a/assets/css/components/tableofcontents.css b/assets/css/components/tableofcontents.css
new file mode 100644
index 000000000..3640adf6d
--- /dev/null
+++ b/assets/css/components/tableofcontents.css
@@ -0,0 +1,14 @@
+.tableofcontents {
+ ul {
+ @apply list-none;
+ li {
+ @apply mb-2;
+ a {
+ @apply text-primary;
+ &:hover {
+ @apply text-primary/60;
+ }
+ }
+ }
+ }
+}
diff --git a/assets/css/components/view-transitions.css b/assets/css/components/view-transitions.css
new file mode 100644
index 000000000..3e7217c4e
--- /dev/null
+++ b/assets/css/components/view-transitions.css
@@ -0,0 +1,22 @@
+/* Global slight fade */
+::view-transition-old(root),
+::view-transition-new(root) {
+ animation-duration: 200ms;
+}
+
+::view-transition-old(qr),
+::view-transition-new(qr) {
+ animation-duration: 800ms;
+ animation-delay: 250ms;
+}
+
+.view-transition-qr {
+ view-transition-name: qr;
+}
+
+/* Turbo styles */
+.turbo-progress-bar {
+ @apply bg-blue-500;
+ opacity: 0.35;
+ height: 3px;
+}
diff --git a/assets/css/styles.css b/assets/css/styles.css
new file mode 100644
index 000000000..5e872b2a0
--- /dev/null
+++ b/assets/css/styles.css
@@ -0,0 +1,120 @@
+@import "tailwindcss";
+@plugin "@tailwindcss/typography";
+@variant dark (&:where(.dark, .dark *));
+
+@import "components/all.css";
+
+/* TailwindCSS ignores files in .gitignore, so make it explicit. */
+@source "hugo_stats.json";
+
+@theme {
+ /* Breakpoints. */
+ --breakpoint-sm: 40rem;
+ --breakpoint-md: 48rem;
+ --breakpoint-lg: 68rem; /* Default 64rem; */
+ --breakpoint-xl: 80rem;
+ --breakpoint-2xl: 96rem;
+
+ /* Colors. */
+ --color-primary: var(--color-blue-600);
+ --color-dark: #000;
+ --color-light: var(--color-gray-50);
+ --color-accent: var(--color-orange-500);
+ --color-accent-light: var(--color-pink-500);
+ --color-accent-dark: var(--color-green-500);
+
+ /* https://www.tints.dev/blue/0594CB */
+ --color-blue-50: #e1f6fe;
+ --color-blue-100: #c3edfe;
+ --color-blue-200: #88dbfc;
+ --color-blue-300: #4cc9fb;
+ --color-blue-400: #15b9f9;
+ --color-blue-500: #0594cb;
+ --color-blue-600: #0477a4;
+ --color-blue-700: #035677;
+ --color-blue-800: #023a50;
+ --color-blue-900: #011d28;
+ --color-blue-950: #000e14;
+
+ /* https://www.tints.dev/orange/EBB951 */
+ --color-orange-50: #fdf8ed;
+ --color-orange-100: #fbf1da;
+ --color-orange-200: #f7e4ba;
+ --color-orange-300: #f3d596;
+ --color-orange-400: #efc976;
+ --color-orange-500: #ebb951;
+ --color-orange-600: #e5a51a;
+ --color-orange-700: #a97a13;
+ --color-orange-800: #72520d;
+ --color-orange-900: #372806;
+ --color-orange-950: #1b1403;
+
+ /* https://www.tints.dev/pink/FF4088 */
+ --color-pink-50: #ffebf2;
+ --color-pink-100: #ffdbe9;
+ --color-pink-200: #ffb3d0;
+ --color-pink-300: #ff8fba;
+ --color-pink-400: #ff66a1;
+ --color-pink-500: #ff4088;
+ --color-pink-600: #ff0062;
+ --color-pink-700: #c2004a;
+ --color-pink-800: #800031;
+ --color-pink-900: #420019;
+ --color-pink-950: #1f000c;
+
+ /* https://www.tints.dev/green/33BA91 */
+ --color-green-50: #ebfaf5;
+ --color-green-100: #d3f3e9;
+ --color-green-200: #abe8d6;
+ --color-green-300: #7fdcc0;
+ --color-green-400: #53d0aa;
+ --color-green-500: #33ba91;
+ --color-green-600: #299474;
+ --color-green-700: #1f7058;
+ --color-green-800: #154c3b;
+ --color-green-900: #0a241c;
+ --color-green-950: #051410;
+
+ /* Fonts. */
+ --font-sans: "Mulish", ui-sans-serif, system-ui, sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+}
+
+html {
+ scroll-padding-top: 100px;
+}
+
+body {
+ @apply antialiased font-sans text-black dark:text-gray-100;
+}
+
+.p-safe-area-x {
+ padding-left: env(safe-area-inset-left);
+ padding-right: env(safe-area-inset-right);
+}
+
+.p-safe-area-y {
+ padding-top: env(safe-area-inset-top);
+ padding-bottom: env(safe-area-inset-bottom);
+}
+
+.px-main {
+ padding-left: max(env(safe-area-inset-left), 1rem);
+ padding-right: max(env(safe-area-inset-right), 1rem);
+}
+
+@media screen(md) {
+ .px-main {
+ padding-left: max(env(safe-area-inset-left), 2rem);
+ padding-right: max(env(safe-area-inset-right), 2rem);
+ }
+}
+
+@media screen(lg) {
+ .px-main {
+ padding-left: max(env(safe-area-inset-left), 3rem);
+ padding-right: max(env(safe-area-inset-right), 3rem);
+ }
+}
+
+/* flex-1 mx-auto lg:mx-0 px-4 md:px-8 lg:px-12 w-full max-w-3x lg:max-w-3x py-8 sm:py-14 */
diff --git a/assets/images/examples/landscape-exif-orientation-5.jpg b/assets/images/examples/landscape-exif-orientation-5.jpg
new file mode 100644
index 000000000..ad64835eb
Binary files /dev/null and b/assets/images/examples/landscape-exif-orientation-5.jpg differ
diff --git a/assets/images/examples/mask.png b/assets/images/examples/mask.png
new file mode 100644
index 000000000..c3005a669
Binary files /dev/null and b/assets/images/examples/mask.png differ
diff --git a/assets/images/examples/zion-national-park.jpg b/assets/images/examples/zion-national-park.jpg
new file mode 100644
index 000000000..7980abccb
Binary files /dev/null and b/assets/images/examples/zion-national-park.jpg differ
diff --git a/assets/images/hugo-github-screenshot.png b/assets/images/hugo-github-screenshot.png
new file mode 100644
index 000000000..275b6969d
Binary files /dev/null and b/assets/images/hugo-github-screenshot.png differ
diff --git a/assets/images/logos/logo-128x128.png b/assets/images/logos/logo-128x128.png
new file mode 100644
index 000000000..ec1a2d6e1
Binary files /dev/null and b/assets/images/logos/logo-128x128.png differ
diff --git a/assets/images/logos/logo-256x256.png b/assets/images/logos/logo-256x256.png
new file mode 100644
index 000000000..d9fdb888a
Binary files /dev/null and b/assets/images/logos/logo-256x256.png differ
diff --git a/assets/images/logos/logo-512x512.png b/assets/images/logos/logo-512x512.png
new file mode 100644
index 000000000..76d463600
Binary files /dev/null and b/assets/images/logos/logo-512x512.png differ
diff --git a/assets/images/logos/logo-64x64.png b/assets/images/logos/logo-64x64.png
new file mode 100644
index 000000000..9857bcea1
Binary files /dev/null and b/assets/images/logos/logo-64x64.png differ
diff --git a/assets/images/logos/logo-96x96.png b/assets/images/logos/logo-96x96.png
new file mode 100644
index 000000000..48d0cb98e
Binary files /dev/null and b/assets/images/logos/logo-96x96.png differ
diff --git a/assets/images/sponsors/Route4MeLogoBlueOnWhite.svg b/assets/images/sponsors/Route4MeLogoBlueOnWhite.svg
new file mode 100644
index 000000000..d4334e8d8
--- /dev/null
+++ b/assets/images/sponsors/Route4MeLogoBlueOnWhite.svg
@@ -0,0 +1,15 @@
+
diff --git a/assets/images/sponsors/bep-consulting.svg b/assets/images/sponsors/bep-consulting.svg
new file mode 100644
index 000000000..598a1eb71
--- /dev/null
+++ b/assets/images/sponsors/bep-consulting.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/images/sponsors/butter-dark.svg b/assets/images/sponsors/butter-dark.svg
new file mode 100644
index 000000000..657b75c50
--- /dev/null
+++ b/assets/images/sponsors/butter-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/sponsors/butter-light.svg b/assets/images/sponsors/butter-light.svg
new file mode 100644
index 000000000..a0697df08
--- /dev/null
+++ b/assets/images/sponsors/butter-light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/sponsors/cloudcannon-blue.svg b/assets/images/sponsors/cloudcannon-blue.svg
new file mode 100644
index 000000000..79b13f431
--- /dev/null
+++ b/assets/images/sponsors/cloudcannon-blue.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/images/sponsors/cloudcannon-white.svg b/assets/images/sponsors/cloudcannon-white.svg
new file mode 100644
index 000000000..83e319a6d
--- /dev/null
+++ b/assets/images/sponsors/cloudcannon-white.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/images/sponsors/esolia-logo.svg b/assets/images/sponsors/esolia-logo.svg
new file mode 100644
index 000000000..3f5344c61
--- /dev/null
+++ b/assets/images/sponsors/esolia-logo.svg
@@ -0,0 +1,67 @@
+
+
+
diff --git a/assets/images/sponsors/goland.svg b/assets/images/sponsors/goland.svg
new file mode 100644
index 000000000..c32f25d7f
--- /dev/null
+++ b/assets/images/sponsors/goland.svg
@@ -0,0 +1,20 @@
+
diff --git a/assets/images/sponsors/graitykit-dark.svg b/assets/images/sponsors/graitykit-dark.svg
new file mode 100644
index 000000000..fd7d12f5c
--- /dev/null
+++ b/assets/images/sponsors/graitykit-dark.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/assets/images/sponsors/linode-logo.svg b/assets/images/sponsors/linode-logo.svg
new file mode 100644
index 000000000..873678398
--- /dev/null
+++ b/assets/images/sponsors/linode-logo.svg
@@ -0,0 +1 @@
+
diff --git a/assets/images/sponsors/linode-logo_standard_light_medium.png b/assets/images/sponsors/linode-logo_standard_light_medium.png
new file mode 100644
index 000000000..269e6af84
Binary files /dev/null and b/assets/images/sponsors/linode-logo_standard_light_medium.png differ
diff --git a/assets/images/sponsors/your-company-dark.svg b/assets/images/sponsors/your-company-dark.svg
new file mode 100644
index 000000000..58fd601f5
--- /dev/null
+++ b/assets/images/sponsors/your-company-dark.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/images/sponsors/your-company.svg b/assets/images/sponsors/your-company.svg
new file mode 100644
index 000000000..3b85ece5c
--- /dev/null
+++ b/assets/images/sponsors/your-company.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/js/alpinejs/data/explorer.js b/assets/js/alpinejs/data/explorer.js
new file mode 100644
index 000000000..b1496d35d
--- /dev/null
+++ b/assets/js/alpinejs/data/explorer.js
@@ -0,0 +1,123 @@
+var debug = 0 ? console.log.bind(console, '[explorer]') : function () {};
+
+// This is cureently not used, but kept in case I change my mind.
+export const explorer = (Alpine) => ({
+ uiState: {
+ containerScrollTop: -1,
+ lastActiveRef: '',
+ },
+ treeState: {
+ // The href of the current page.
+ currentNode: '',
+ // The state of each node in the tree.
+ nodes: {},
+
+ // We currenty only list the sections, not regular pages, in the side bar.
+ // This strikes me as the right balance. The pages gets listed on the section pages.
+ // This array is sorted by length, so we can find the longest prefix of the current page
+ // without having to iterate over all the keys.
+ nodeRefsByLength: [],
+ },
+ async init() {
+ let keys = Reflect.ownKeys(this.$refs);
+ for (let key of keys) {
+ let n = {
+ open: false,
+ active: false,
+ };
+ this.treeState.nodes[key] = n;
+ this.treeState.nodeRefsByLength.push(key);
+ }
+
+ this.treeState.nodeRefsByLength.sort((a, b) => b.length - a.length);
+
+ this.setCurrentActive();
+ },
+
+ longestPrefix(ref) {
+ let longestPrefix = '';
+ for (let key of this.treeState.nodeRefsByLength) {
+ if (ref.startsWith(key)) {
+ longestPrefix = key;
+ break;
+ }
+ }
+ return longestPrefix;
+ },
+
+ setCurrentActive() {
+ let ref = this.longestPrefix(window.location.pathname);
+ let activeChanged = this.uiState.lastActiveRef !== ref;
+ debug('setCurrentActive', this.uiState.lastActiveRef, window.location.pathname, '=>', ref, activeChanged);
+ this.uiState.lastActiveRef = ref;
+ if (this.uiState.containerScrollTop === -1 && activeChanged) {
+ // Navigation outside of the explorer menu.
+ let el = document.querySelector(`[x-ref="${ref}"]`);
+ if (el) {
+ this.$nextTick(() => {
+ debug('scrolling to', ref);
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ });
+ }
+ }
+ this.treeState.currentNode = ref;
+ for (let key in this.treeState.nodes) {
+ let n = this.treeState.nodes[key];
+ n.active = false;
+ n.open = ref == key || ref.startsWith(key);
+ if (n.open) {
+ debug('open', key);
+ }
+ }
+
+ let n = this.treeState.nodes[this.longestPrefix(ref)];
+ if (n) {
+ n.active = true;
+ }
+ },
+
+ getScrollingContainer() {
+ return document.getElementById('leftsidebar');
+ },
+
+ onLoad() {
+ debug('onLoad', this.uiState.containerScrollTop);
+ if (this.uiState.containerScrollTop >= 0) {
+ debug('onLoad: scrolling to', this.uiState.containerScrollTop);
+ this.getScrollingContainer().scrollTo(0, this.uiState.containerScrollTop);
+ }
+ this.uiState.containerScrollTop = -1;
+ },
+
+ onBeforeRender() {
+ debug('onBeforeRender', this.uiState.containerScrollTop);
+ this.setCurrentActive();
+ },
+
+ toggleNode(ref) {
+ this.uiState.containerScrollTop = this.getScrollingContainer().scrollTop;
+ this.uiState.lastActiveRef = '';
+ debug('toggleNode', ref, this.uiState.containerScrollTop);
+
+ let node = this.treeState.nodes[ref];
+ if (!node) {
+ debug('node not found', ref);
+ return;
+ }
+ let wasOpen = node.open;
+ },
+
+ isCurrent(ref) {
+ let n = this.treeState.nodes[ref];
+ return n && n.active;
+ },
+
+ isOpen(ref) {
+ let node = this.treeState.nodes[ref];
+ if (!node) return false;
+ if (node.open) {
+ debug('isOpen', ref);
+ }
+ return node.open;
+ },
+});
diff --git a/assets/js/alpinejs/data/index.js b/assets/js/alpinejs/data/index.js
new file mode 100644
index 000000000..7bf0532e3
--- /dev/null
+++ b/assets/js/alpinejs/data/index.js
@@ -0,0 +1,3 @@
+export * from './navbar';
+export * from './search';
+export * from './toc';
diff --git a/assets/js/alpinejs/data/navbar.js b/assets/js/alpinejs/data/navbar.js
new file mode 100644
index 000000000..ec1ce73fa
--- /dev/null
+++ b/assets/js/alpinejs/data/navbar.js
@@ -0,0 +1,12 @@
+export const navbar = (Alpine) => ({
+ atTop: true,
+
+ init: function () {
+ Alpine.bind(this.$root, this.root);
+ },
+ root: {
+ ['@scroll.window.debounce.10ms'](event) {
+ this.atTop = window.scrollY < 40 ? true : false;
+ },
+ },
+});
diff --git a/assets/js/alpinejs/data/search.js b/assets/js/alpinejs/data/search.js
new file mode 100644
index 000000000..c1572d020
--- /dev/null
+++ b/assets/js/alpinejs/data/search.js
@@ -0,0 +1,85 @@
+const designMode = false;
+
+const groupByLvl0 = (array) => {
+ if (!array) return [];
+ return array.reduce((result, currentValue) => {
+ (result[currentValue.hierarchy.lvl0] = result[currentValue.hierarchy.lvl0] || []).push(currentValue);
+ return result;
+ }, {});
+};
+
+export const search = (Alpine, cfg) => ({
+ query: designMode ? 'shortcodes' : '',
+ open: designMode,
+ result: {},
+
+ init() {
+ Alpine.bind(this.$root, this.root);
+
+ this.checkOpen();
+ return this.$nextTick(() => {
+ this.$watch('query', () => {
+ this.search();
+ });
+ });
+ },
+ toggleOpen: function () {
+ this.open = !this.open;
+ this.checkOpen();
+ },
+ checkOpen: function () {
+ if (!this.open) {
+ return;
+ }
+ this.search();
+ this.$nextTick(() => {
+ this.$refs.input.focus();
+ });
+ },
+
+ search: function () {
+ if (!this.query) {
+ this.result = {};
+ return;
+ }
+ var queries = {
+ requests: [
+ {
+ indexName: cfg.index,
+ params: `query=${encodeURIComponent(this.query)}`,
+ attributesToHighlight: ['hierarchy', 'content'],
+ attributesToRetrieve: ['hierarchy', 'url', 'content'],
+ },
+ ],
+ };
+
+ const host = `https://${cfg.app_id}-dsn.algolia.net`;
+ const url = `${host}/1/indexes/*/queries`;
+
+ fetch(url, {
+ method: 'POST',
+ headers: {
+ 'X-Algolia-Application-Id': cfg.app_id,
+ 'X-Algolia-API-Key': cfg.api_key,
+ },
+ body: JSON.stringify(queries),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ this.result = groupByLvl0(data.results[0].hits);
+ });
+ },
+ root: {
+ ['@click']() {
+ if (!this.open) {
+ this.toggleOpen();
+ }
+ },
+ ['@search-toggle.window']() {
+ this.toggleOpen();
+ },
+ ['@keydown.meta.k.window']() {
+ this.toggleOpen();
+ },
+ },
+});
diff --git a/assets/js/alpinejs/data/toc.js b/assets/js/alpinejs/data/toc.js
new file mode 100644
index 000000000..e0818fa98
--- /dev/null
+++ b/assets/js/alpinejs/data/toc.js
@@ -0,0 +1,65 @@
+var debug = 0 ? console.log.bind(console, '[toc]') : function () {};
+
+export const toc = (Alpine) => ({
+ contentScrollSpy: null,
+ activeHeading: '',
+ justClicked: false,
+
+ setActive(id) {
+ debug('setActive', id);
+ this.activeHeading = id;
+ // Prevent the intersection observer from changing the active heading right away.
+ this.justClicked = true;
+ setTimeout(() => {
+ this.justClicked = false;
+ }, 200);
+ },
+
+ init() {
+ return this.$nextTick(() => {
+ let contentEl = document.getElementById('content');
+ if (contentEl) {
+ const handleIntersect = (entries) => {
+ if (this.justClicked) {
+ return;
+ }
+ for (let entry of entries) {
+ if (entry.isIntersecting) {
+ let id = entry.target.id;
+ this.activeHeading = id;
+ let liEl = this.$refs[id];
+ if (liEl) {
+ // If liEl is not in the viewport, scroll it into view.
+ let bounding = liEl.getBoundingClientRect();
+ if (bounding.top < 0 || bounding.bottom > window.innerHeight) {
+ this.$root.scrollTop = liEl.offsetTop - 100;
+ }
+ }
+ debug('intersecting', id);
+ break;
+ }
+ }
+ };
+
+ let opts = {
+ rootMargin: '0px 0px -75%',
+ threshold: 0.75,
+ };
+
+ this.contentScrollSpy = new IntersectionObserver(handleIntersect, opts);
+ // Observe all headings.
+ let headings = contentEl.querySelectorAll('h2, h3, h4, h5, h6');
+ for (let heading of headings) {
+ this.contentScrollSpy.observe(heading);
+ }
+ }
+ });
+ },
+
+ destroy() {
+ if (this.contentScrollSpy) {
+ debug('disconnecting');
+ this.contentScrollSpy.disconnect();
+ }
+ },
+});
diff --git a/assets/js/alpinejs/magics/helpers.js b/assets/js/alpinejs/magics/helpers.js
new file mode 100644
index 000000000..f7046259d
--- /dev/null
+++ b/assets/js/alpinejs/magics/helpers.js
@@ -0,0 +1,29 @@
+'use strict';
+
+export function registerMagics(Alpine) {
+ Alpine.magic('copy', (currentEl) => {
+ return function (el) {
+ if (!el) {
+ el = currentEl;
+ }
+ let lntds = el.querySelectorAll('.lntable .lntd');
+ if (lntds && lntds.length === 2) {
+ el = lntds[1];
+ }
+
+ // Trim whitespace.
+ let text = el.textContent.trim();
+
+ navigator.clipboard.writeText(text);
+ };
+ });
+
+ Alpine.magic('isScrollX', (currentEl) => {
+ return function (el) {
+ if (!el) {
+ el = currentEl;
+ }
+ return el.clientWidth < el.scrollWidth;
+ };
+ });
+}
diff --git a/assets/js/alpinejs/magics/index.js b/assets/js/alpinejs/magics/index.js
new file mode 100644
index 000000000..c5f595cf9
--- /dev/null
+++ b/assets/js/alpinejs/magics/index.js
@@ -0,0 +1 @@
+export * from './helpers';
diff --git a/assets/js/alpinejs/stores/index.js b/assets/js/alpinejs/stores/index.js
new file mode 100644
index 000000000..17e2a347b
--- /dev/null
+++ b/assets/js/alpinejs/stores/index.js
@@ -0,0 +1 @@
+export * from './nav.js';
diff --git a/assets/js/alpinejs/stores/nav.js b/assets/js/alpinejs/stores/nav.js
new file mode 100644
index 000000000..7e67d85ce
--- /dev/null
+++ b/assets/js/alpinejs/stores/nav.js
@@ -0,0 +1,90 @@
+var debug = 1 ? console.log.bind(console, '[navStore]') : function () {};
+
+var ColorScheme = {
+ System: 1,
+ Light: 2,
+ Dark: 3,
+};
+
+const localStorageUserSettingsKey = 'hugoDocsUserSettings';
+
+export const navStore = (Alpine) => ({
+ init() {
+ // There is no $watch available in Alpine stores,
+ // but this has the same effect.
+ this.userSettings.onColorSchemeChanged = Alpine.effect(() => {
+ if (this.userSettings.settings.colorScheme) {
+ this.userSettings.isDark = isDark(this.userSettings.settings.colorScheme);
+ toggleDarkMode(this.userSettings.isDark);
+ }
+ });
+
+ // Also react to changes in system settings.
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
+ this.userSettings.setColorScheme(ColorScheme.System);
+ });
+ },
+
+ destroy() {},
+
+ userSettings: {
+ // settings gets persisted between page navigations.
+ settings: Alpine.$persist({
+ // light, dark or system mode.
+ // If not set, we use the OS setting.
+ colorScheme: ColorScheme.System,
+ // Used to show the most relevant tab in config listings etc.
+ configFileType: 'toml',
+ }).as(localStorageUserSettingsKey),
+
+ isDark: false,
+
+ setColorScheme(colorScheme) {
+ this.settings.colorScheme = colorScheme;
+ this.isDark = isDark(colorScheme);
+ },
+
+ toggleColorScheme() {
+ let next = this.settings.colorScheme + 1;
+ if (next > ColorScheme.Dark) {
+ next = ColorScheme.System;
+ }
+ this.setColorScheme(next);
+ },
+ colorScheme() {
+ return this.settings.colorScheme ? this.settings.colorScheme : ColorScheme.System;
+ },
+ },
+});
+
+function isMediaDark() {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
+}
+
+function isDark(colorScheme) {
+ if (!colorScheme || colorScheme == ColorScheme.System) {
+ return isMediaDark();
+ }
+
+ return colorScheme == ColorScheme.Dark;
+}
+
+export function initColorScheme() {
+ // The AlpineJS store has not have been initialized yet, so access the
+ // localStorage directly.
+ let settingsJSON = localStorage[localStorageUserSettingsKey];
+ if (settingsJSON) {
+ let settings = JSON.parse(settingsJSON);
+ toggleDarkMode(isDark(settings.colorScheme));
+ return;
+ }
+ toggleDarkMode(isDark(null));
+}
+
+const toggleDarkMode = function (dark) {
+ if (dark) {
+ document.body.classList.add('dark');
+ } else {
+ document.body.classList.remove('dark');
+ }
+};
diff --git a/assets/js/body-start.js b/assets/js/body-start.js
new file mode 100644
index 000000000..f9b596671
--- /dev/null
+++ b/assets/js/body-start.js
@@ -0,0 +1,6 @@
+import { initColorScheme } from './alpinejs/stores/index';
+
+(function () {
+ // This allows us to initialize the color scheme before AlpineJS etc. is loaded.
+ initColorScheme();
+})();
diff --git a/assets/js/head-early.js b/assets/js/head-early.js
new file mode 100644
index 000000000..ca86b5694
--- /dev/null
+++ b/assets/js/head-early.js
@@ -0,0 +1,11 @@
+import { scrollToActive } from 'js/helpers/index';
+
+(function () {
+ // Now we know that the browser has JS enabled.
+ document.documentElement.classList.remove('no-js');
+
+ // Wait for the DOM to be ready.
+ document.addEventListener('DOMContentLoaded', function () {
+ scrollToActive('DOMContentLoaded');
+ });
+})();
diff --git a/assets/js/helpers/bridgeTurboAndAlpine.js b/assets/js/helpers/bridgeTurboAndAlpine.js
new file mode 100644
index 000000000..0494d02f2
--- /dev/null
+++ b/assets/js/helpers/bridgeTurboAndAlpine.js
@@ -0,0 +1,67 @@
+export function bridgeTurboAndAlpine(Alpine) {
+ document.addEventListener('turbo:before-render', (event) => {
+ event.detail.newBody.querySelectorAll('[data-alpine-generated]').forEach((el) => {
+ if (el.hasAttribute('data-alpine-generated')) {
+ el.removeAttribute('data-alpine-generated');
+ el.remove();
+ }
+ });
+ });
+
+ document.addEventListener('turbo:render', () => {
+ if (document.documentElement.hasAttribute('data-turbo-preview')) {
+ return;
+ }
+
+ document.querySelectorAll('[data-alpine-ignored]').forEach((el) => {
+ el.removeAttribute('x-ignore');
+ el.removeAttribute('data-alpine-ignored');
+ });
+
+ document.body.querySelectorAll('[x-data]').forEach((el) => {
+ if (el.hasAttribute('data-turbo-permanent')) {
+ return;
+ }
+ Alpine.initTree(el);
+ });
+
+ Alpine.startObservingMutations();
+ });
+
+ // Cleanup Alpine state on navigation.
+ document.addEventListener('turbo:before-cache', () => {
+ // This will be restarted in turbo:render.
+ Alpine.stopObservingMutations();
+
+ document.body.querySelectorAll('[data-turbo-permanent]').forEach((el) => {
+ if (!el.hasAttribute('x-ignore')) {
+ el.setAttribute('x-ignore', true);
+ el.setAttribute('data-alpine-ignored', true);
+ }
+ });
+
+ document.body.querySelectorAll('[x-for],[x-if],[x-teleport]').forEach((el) => {
+ if (el.hasAttribute('x-for') && el._x_lookup) {
+ Object.values(el._x_lookup).forEach((el) => el.setAttribute('data-alpine-generated', true));
+ }
+
+ if (el.hasAttribute('x-if') && el._x_currentIfEl) {
+ el._x_currentIfEl.setAttribute('data-alpine-generated', true);
+ }
+
+ if (el.hasAttribute('x-teleport') && el._x_teleport) {
+ el._x_teleport.setAttribute('data-alpine-generated', true);
+ }
+ });
+
+ document.body.querySelectorAll('[x-data]').forEach((el) => {
+ if (!el.hasAttribute('data-turbo-permanent')) {
+ Alpine.destroyTree(el);
+ // Turbo leaks DOM elements via their data-turbo-permanent handling.
+ // That needs to be fixed upstream, but until then.
+ let clone = el.cloneNode(true);
+ el.replaceWith(clone);
+ }
+ });
+ });
+}
diff --git a/assets/js/helpers/helpers.js b/assets/js/helpers/helpers.js
new file mode 100644
index 000000000..818eac40c
--- /dev/null
+++ b/assets/js/helpers/helpers.js
@@ -0,0 +1,17 @@
+export const scrollToActive = (when) => {
+ let els = document.querySelectorAll('.scroll-active');
+ if (!els.length) {
+ return;
+ }
+ els.forEach((el) => {
+ // Find scrolling container.
+ let container = el.closest('[data-turbo-preserve-scroll-container]');
+ if (container) {
+ // Avoid scrolling if el is already in view.
+ if (el.offsetTop >= container.scrollTop && el.offsetTop <= container.scrollTop + container.clientHeight) {
+ return;
+ }
+ container.scrollTop = el.offsetTop - container.offsetTop;
+ }
+ });
+};
diff --git a/assets/js/helpers/index.js b/assets/js/helpers/index.js
new file mode 100644
index 000000000..b02d81cf0
--- /dev/null
+++ b/assets/js/helpers/index.js
@@ -0,0 +1,2 @@
+export * from './bridgeTurboAndAlpine';
+export * from './helpers';
diff --git a/assets/js/main.js b/assets/js/main.js
new file mode 100644
index 000000000..14440044b
--- /dev/null
+++ b/assets/js/main.js
@@ -0,0 +1,89 @@
+import Alpine from 'alpinejs';
+import { registerMagics } from './alpinejs/magics/index';
+import { navbar, search, toc } from './alpinejs/data/index';
+import { navStore, initColorScheme } from './alpinejs/stores/index';
+import { bridgeTurboAndAlpine } from './helpers/index';
+import persist from '@alpinejs/persist';
+import focus from '@alpinejs/focus';
+
+var debug = 0 ? console.log.bind(console, '[index]') : function () {};
+
+// Turbolinks init.
+(function () {
+ document.addEventListener('turbo:render', function (e) {
+ // This is also called right after the body start. This is added to prevent flicker on navigation.
+ initColorScheme();
+ });
+})();
+
+// Set up and start Alpine.
+(function () {
+ // Register AlpineJS plugins.
+ {
+ Alpine.plugin(focus);
+ Alpine.plugin(persist);
+ }
+ // Register AlpineJS magics and directives.
+ {
+ // Handles copy to clipboard etc.
+ registerMagics(Alpine);
+ }
+
+ // Register AlpineJS controllers.
+ {
+ // Register AlpineJS data controllers.
+ let searchConfig = {
+ index: 'hugodocs',
+ app_id: 'D1BPLZHGYQ',
+ api_key: '6df94e1e5d55d258c56f60d974d10314',
+ };
+
+ Alpine.data('navbar', () => navbar(Alpine));
+ Alpine.data('search', () => search(Alpine, searchConfig));
+ Alpine.data('toc', () => toc(Alpine));
+ }
+
+ // Register AlpineJS stores.
+ {
+ Alpine.store('nav', navStore(Alpine));
+ }
+
+ // Start AlpineJS.
+ Alpine.start();
+
+ // Start the Turbo-Alpine bridge.
+ bridgeTurboAndAlpine(Alpine);
+
+ {
+ let containerScrollTops = {};
+
+ // To preserve scroll position in scrolling elements on navigation add data-turbo-preserve-scroll-container="somename" to the scrolling container.
+ addEventListener('turbo:click', () => {
+ document.querySelectorAll('[data-turbo-preserve-scroll-container]').forEach((el2) => {
+ containerScrollTops[el2.dataset.turboPreserveScrollContainer] = el2.scrollTop;
+ });
+ });
+
+ addEventListener('turbo:render', () => {
+ document.querySelectorAll('[data-turbo-preserve-scroll-container]').forEach((ele) => {
+ const containerScrollTop = containerScrollTops[ele.dataset.turboPreserveScrollContainer];
+ if (containerScrollTop) {
+ ele.scrollTop = containerScrollTop;
+ } else {
+ let els = ele.querySelectorAll('.scroll-active');
+ if (els.length) {
+ els.forEach((el) => {
+ // Avoid scrolling if el is already in view.
+ if (el.offsetTop >= ele.scrollTop && el.offsetTop <= ele.scrollTop + ele.clientHeight) {
+ return;
+ }
+ ele.scrollTop = el.offsetTop - ele.offsetTop;
+ });
+ }
+ }
+ });
+
+ containerScrollTops = {};
+ });
+ }
+})();
diff --git a/assets/js/turbo.js b/assets/js/turbo.js
new file mode 100644
index 000000000..c007896f6
--- /dev/null
+++ b/assets/js/turbo.js
@@ -0,0 +1 @@
+import * as Turbo from '@hotwired/turbo';
diff --git a/assets/jsconfig.json b/assets/jsconfig.json
new file mode 100644
index 000000000..377218ccb
--- /dev/null
+++ b/assets/jsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "*": [
+ "*"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/assets/opengraph/gohugoio-card-base-1.png b/assets/opengraph/gohugoio-card-base-1.png
new file mode 100644
index 000000000..65555845b
Binary files /dev/null and b/assets/opengraph/gohugoio-card-base-1.png differ
diff --git a/assets/opengraph/mulish-black.ttf b/assets/opengraph/mulish-black.ttf
new file mode 100644
index 000000000..db680a088
Binary files /dev/null and b/assets/opengraph/mulish-black.ttf differ
diff --git a/content/LICENSE.md b/content/LICENSE.md
new file mode 100644
index 000000000..b09cd7856
--- /dev/null
+++ b/content/LICENSE.md
@@ -0,0 +1,201 @@
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/go.mod b/go.mod
index 56a48deac..4b9e0a369 100644
--- a/go.mod
+++ b/go.mod
@@ -1,5 +1,3 @@
module github.com/gohugoio/hugoDocs
go 1.22.0
-
-require github.com/gohugoio/gohugoioTheme v0.0.0-20250116152525-2d382cae7743 // indirect
diff --git a/hugo.toml b/hugo.toml
index 2a271b6e3..1e7aaa4b7 100644
--- a/hugo.toml
+++ b/hugo.toml
@@ -1,9 +1,7 @@
-# This his the main configuration file. There are also environment specific configuration stored in the /config directory.
-
baseURL = "https://gohugo.io/"
defaultContentLanguage = "en"
enableEmoji = true
-ignoreErrors = ["error-remote-getjson", "error-missing-instagram-accesstoken"]
+ignoreLogs = ["error-missing-instagram-accesstoken"]
languageCode = "en-us"
pluralizeListTitles = false
timeZone = "Europe/Oslo"
@@ -13,90 +11,161 @@ title = "Hugo"
disableAliases = true
[pagination]
-pagerSize = 100
+ pagerSize = 100
[services.googleAnalytics]
-ID = 'G-MBZGKNMDWC'
-
-[minify]
- [minify.tdewolff]
- [minify.tdewolff.html]
- keepWhitespace = true
-
-[module]
- [module.hugoVersion]
- min = "0.56.0"
- [[module.imports]]
- path = "github.com/gohugoio/gohugoioTheme"
+ ID = 'G-MBZGKNMDWC'
[outputs]
- home = ["HTML", "RSS", "REDIR", "HEADERS"]
- section = ["HTML"]
+ home = ["html", "rss", "redir", "headers"]
+ section = ["html"]
+ page = ["html"]
+ taxonomy = ["html"]
+ term = ["html"]
-[mediaTypes]
- [mediaTypes."text/netlify"]
- delimiter = ""
+[params]
+ description = "The world’s fastest framework for building websites"
+ ghrepo = "https://github.com/gohugoio/hugoDocs/"
+
+[languages]
+ [languages.en]
+ languageName = "English"
+ weight = 1
+
+[security]
+ enableInlineShortcodes = false
+ [security.funcs]
+ getenv = ['^HUGO_', '^REPOSITORY_URL$', '^BRANCH$']
+ [security.http]
+ methods = ['(?i)GET|POST']
+ urls = ['.*']
[outputFormats]
- [outputFormats.REDIR]
- mediatype = "text/netlify"
- baseName = "_redirects"
- isPlainText = true
- notAlternative = true
- [outputFormats.HEADERS]
- mediatype = "text/netlify"
- baseName = "_headers"
- isPlainText = true
- notAlternative = true
+ [outputFormats.redir]
+ mediatype = "text/netlify"
+ baseName = "_redirects"
+ isPlainText = true
+ [outputFormats.headers]
+ mediatype = "text/netlify"
+ baseName = "_headers"
+ isPlainText = true
+ notAlternative = true
-[caches]
- [caches.getjson]
- dir = ":cacheDir/:project"
- maxAge = -1
- [caches.getcsv]
- dir = ":cacheDir/:project"
- maxAge = -1
- [caches.images]
- dir = ":cacheDir/images"
- maxAge = "1440h"
- [caches.assets]
- dir = ":resourceDir/_gen"
- maxAge = -1
- [caches.getresource]
- dir = ":cacheDir/:project"
- maxage = '1h'
+[markup]
+ [markup.highlight]
+ style = 'solarized-dark'
+ lineNumbersInTable = true
+ noClasses = false
+ wrapperClass = 'highlight not-prose'
-[related]
- threshold = 80
- includeNewer = true
- toLower = false
- [[related.indices]]
- name = "keywords"
- weight = 60
- [[related.indices]]
- # Can be used as a front matter slice to link to other page fragments (headings) using their ID.
- # This isn't particular useful in the current docs, but we're planning on getting a auto generated
- # reference section with a better ID setup.
- # For now, we just use it to give pages with same headings some similarity score.
- name = "fragmentrefs"
- type = "fragments"
- applyFilter = false
- weight = 60
- cardinalityThreshold = 50
+ [markup.goldmark.renderer]
+ hardWraps = false
+ unsafe = false
+ xhtml = false
-[imaging]
- # See https://github.com/disintegration/imaging
- # CatmullRom is a sharp bicubic filter which should fit the docs site well with its many screenshots.
- # Note that you can also set this per image processing.
- resampleFilter = "CatmullRom"
- # Default JPEG quality setting. Default is 75.
- quality = 75
- anchor = "smart"
+ [markup.goldmark.extensions]
+ definitionList = true
+ footnote = true
+ linkify = true
+ strikethrough = true
+ table = true
+ taskList = true
+ typographer = true
-[taxonomies]
- category = "categories"
+ [markup.goldmark.extensions.passthrough]
+ enable = true
-[[cascade]]
-categories = ['commands']
-[cascade._target]
-path = '/commands/**'
+ [markup.goldmark.extensions.passthrough.delimiters]
+ block = [['\[', '\]'], ['$$', '$$']]
+ inline = [['\(', '\)']]
+
+ [markup.goldmark.parser]
+ autoHeadingID = true
+ autoHeadingIDType = "github"
+
+ [markup.goldmark.parser.attribute]
+ block = true
+ title = true
+
+[mediaTypes]
+ [mediaTypes."text/netlify"]
+ delimiter = ""
+
+[module]
+ [module.hugoVersion]
+ min = "0.141.0"
+ [[module.mounts]]
+ source = "assets"
+ target = "assets"
+ [[module.mounts]]
+ lang = 'en'
+ source = 'content/en'
+ target = 'content'
+ [[module.mounts]]
+ source = "hugo_stats.json"
+ target = "assets/notwatching/hugo_stats.json"
+ disableWatch = true
+
+[build]
+ [build.buildStats]
+ disableIDs = true
+ enable = true
+ [[build.cachebusters]]
+ source = "assets/notwatching/hugo_stats\\.json"
+ target = "css"
+ [[build.cachebusters]]
+ source = "(postcss|tailwind)\\.config\\.js"
+ target = "css"
+
+[server]
+ [[server.headers]]
+ for = "/*"
+
+ [server.headers.values]
+ X-Frame-Options = "DENY"
+ X-XSS-Protection = "1; mode=block"
+ X-Content-Type-Options = "nosniff"
+ Referrer-Policy = "no-referrer"
+
+ [[server.headers]]
+ for = "/**.{css,js}"
+
+[minify]
+ [minify.tdewolff]
+ [minify.tdewolff.html]
+ keepSpecialComments = true
+ keepWhitespace = false
+
+######## GLOBAL ITEMS TO BE SHARED WITH THE HUGO SITES ########
+[menus]
+ [[menus.global]]
+ name = 'News'
+ weight = 1
+ identifier = 'news'
+ pageRef = '/news/'
+
+ [[menus.global]]
+ name = 'Docs'
+ weight = 5
+ identifier = 'docs'
+ url = '/documentation/'
+
+ [[menus.global]]
+ name = 'Themes'
+ weight = 10
+ identifier = 'themes'
+ url = 'https://themes.gohugo.io/'
+
+ [[menus.global]]
+ name = 'Community'
+ weight = 150
+ identifier = 'community'
+ post = 'external'
+ url = 'https://discourse.gohugo.io/'
+
+ [[menus.global]]
+ name = 'GitHub'
+ weight = 200
+ identifier = 'github'
+ post = 'external'
+ url = 'https://github.com/gohugoio/hugo'
diff --git a/hugo.work b/hugo.work
index b2ae38c07..02c0ba91f 100644
--- a/hugo.work
+++ b/hugo.work
@@ -1,4 +1,4 @@
go 1.22.0
use .
-use ../gohugoioTheme
+
diff --git a/layouts/404.html b/layouts/404.html
new file mode 100644
index 000000000..bb92ea160
--- /dev/null
+++ b/layouts/404.html
@@ -0,0 +1,22 @@
+{{ define "main" }}
+
+
+
+{{ end }}
diff --git a/layouts/_default/_markup/render-heading.html b/layouts/_default/_markup/render-heading.html
new file mode 100644
index 000000000..c0e6c63ec
--- /dev/null
+++ b/layouts/_default/_markup/render-heading.html
@@ -0,0 +1,10 @@
+{{ .Text | safeHTML }}
+ {{- if in (slice 2 3 4 6) .Level }}{{" " -}}
+
+
+
+
+{{- end -}}
+
diff --git a/layouts/_default/_markup/render-link.html b/layouts/_default/_markup/render-link.html
new file mode 100644
index 000000000..726610258
--- /dev/null
+++ b/layouts/_default/_markup/render-link.html
@@ -0,0 +1,317 @@
+{{- /* Last modified: 2025-01-19T14:44:56-08:00 */}}
+
+{{- /*
+Copyright 2025 Veriphor LLC
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not
+use this file except in compliance with the License. You may obtain a copy of
+the License at
+
+https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
+*/}}
+
+{{- /*
+This render hook resolves internal destinations by looking for a matching:
+
+ 1. Content page
+ 2. Page resource (a file in the current page bundle)
+ 3. Section resource (a file in the current section)
+ 4. Global resource (a file in the assets directory)
+
+It skips the section resource lookup if the current page is a leaf bundle.
+
+External destinations are not modified.
+
+You must place global resources in the assets directory. If you have placed
+your resources in the static directory, and you are unable or unwilling to move
+them, you must mount the static directory to the assets directory by including
+both of these entries in your site configuration:
+
+ [[module.mounts]]
+ source = 'assets'
+ target = 'assets'
+
+ [[module.mounts]]
+ source = 'static'
+ target = 'assets'
+
+By default, if this render hook is unable to resolve a destination, including a
+fragment if present, it passes the destination through without modification. To
+emit a warning or error, set the error level in your site configuration:
+
+ [params.render_hooks.link]
+ errorLevel = 'warning' # ignore (default), warning, or error (fails the build)
+
+When you set the error level to warning, and you are in a development
+environment, you can visually highlight broken internal links:
+
+ [params.render_hooks.link]
+ errorLevel = 'warning' # ignore (default), warning, or error (fails the build)
+ highlightBroken = true # true or false (default)
+
+This will add a "broken" class to anchor elements with invalid src attributes.
+Add a rule to your CSS targeting the broken links:
+
+ a.broken {
+ background: #ff0;
+ border: 2px solid #f00;
+ padding: 0.1em 0.2em;
+ }
+
+This render hook may be unable to resolve destinations created with the ref and
+relref shortcodes. Unless you set the error level to ignore you should not use
+either of these shortcodes in conjunction with this render hook.
+
+@context {string} Destination The link destination.
+@context {page} Page A reference to the page containing the link.
+@context {string} PlainText The link description as plain text.
+@context {string} Text The link description.
+@context {string} Title The link title.
+
+@returns {template.html}
+*/}}
+
+{{- /* Initialize. */}}
+{{- $renderHookName := "link" }}
+
+{{- /* Verify minimum required version. */}}
+{{- $minHugoVersion := "0.141.0" }}
+{{- if lt hugo.Version $minHugoVersion }}
+ {{- errorf "The %q render hook requires Hugo v%s or later." $renderHookName $minHugoVersion }}
+{{- end }}
+
+{{- /* Error level when unable to resolve destination: ignore, warning, or error. */}}
+{{- $errorLevel := or site.Params.render_hooks.link.errorLevel "ignore" | lower }}
+
+{{- /* If true, adds "broken" class to broken links. Applicable in development environment when errorLevel is warning. */}}
+{{- $highlightBrokenLinks := or site.Params.render_hooks.link.highlightBroken false }}
+
+{{- /* Validate error level. */}}
+{{- if not (in (slice "ignore" "warning" "error") $errorLevel) }}
+ {{- errorf "The %q render hook is misconfigured. The errorLevel %q is invalid. Please check your site configuration." $renderHookName $errorLevel }}
+{{- end }}
+
+{{- /* Determine content path for warning and error messages. */}}
+{{- $contentPath := .Page.String }}
+
+{{- /* Parse destination. */}}
+{{- $u := urls.Parse .Destination }}
+
+{{- /* Set common message. */}}
+{{- $msg := printf "The %q render hook was unable to resolve the destination %q in %s" $renderHookName $u.String $contentPath }}
+
+{{- /* Set attributes for anchor element. */}}
+{{- $attrs := dict "href" $u.String }}
+{{- if eq $u.String "g" }}
+ {{- /* Destination is a glossary term. */}}
+ {{- $ctx := dict
+ "contentPath" $contentPath
+ "errorLevel" $errorLevel
+ "renderHookName" $renderHookName
+ "text" .Text
+ }}
+ {{- $attrs = partial "inline/h-rh-l/get-glossary-link-attributes.html" $ctx }}
+{{- else if $u.IsAbs }}
+ {{- /* Destination is a remote resource. */}}
+ {{- $attrs = merge $attrs (dict "rel" "external") }}
+{{- else }}
+ {{- with $u.Path }}
+ {{- with $p := or ($.PageInner.GetPage .) ($.PageInner.GetPage (strings.TrimRight "/" .)) }}
+ {{- /* Destination is a page. */}}
+ {{- $href := .RelPermalink }}
+ {{- with $u.RawQuery }}
+ {{- $href = printf "%s?%s" $href . }}
+ {{- end }}
+ {{- with $u.Fragment }}
+ {{- $ctx := dict
+ "contentPath" $contentPath
+ "errorLevel" $errorLevel
+ "page" $p
+ "parsedURL" $u
+ "renderHookName" $renderHookName
+ }}
+ {{- partial "inline/h-rh-l/validate-fragment.html" $ctx }}
+ {{- $href = printf "%s#%s" $href . }}
+ {{- end }}
+ {{- $attrs = dict "href" $href }}
+ {{- else with $.PageInner.Resources.Get $u.Path }}
+ {{- /* Destination is a page resource; drop query and fragment. */}}
+ {{- $attrs = dict "href" .RelPermalink }}
+ {{- else with (and (ne $.Page.BundleType "leaf") ($.Page.CurrentSection.Resources.Get $u.Path)) }}
+ {{- /* Destination is a section resource, and current page is not a leaf bundle. */}}
+ {{- $attrs = dict "href" .RelPermalink }}
+ {{- else with resources.Get $u.Path }}
+ {{- /* Destination is a global resource; drop query and fragment. */}}
+ {{- $attrs = dict "href" .RelPermalink }}
+ {{- else }}
+ {{- if eq $errorLevel "warning" }}
+ {{- warnf $msg }}
+ {{- if and $highlightBrokenLinks hugo.IsDevelopment }}
+ {{- $attrs = merge $attrs (dict "class" "broken") }}
+ {{- end }}
+ {{- else if eq $errorLevel "error" }}
+ {{- errorf $msg }}
+ {{- end }}
+ {{- end }}
+ {{- else }}
+ {{- with $u.Fragment }}
+ {{- /* Destination is on the same page; prepend relative permalink. */}}
+ {{- $ctx := dict
+ "contentPath" $contentPath
+ "errorLevel" $errorLevel
+ "page" $.Page
+ "parsedURL" $u
+ "renderHookName" $renderHookName
+ }}
+ {{- partial "inline/h-rh-l/validate-fragment.html" $ctx }}
+ {{- $attrs = dict "href" (printf "%s#%s" $.Page.RelPermalink .) }}
+ {{- else }}
+ {{- if eq $errorLevel "warning" }}
+ {{- warnf $msg }}
+ {{- if and $highlightBrokenLinks hugo.IsDevelopment }}
+ {{- $attrs = merge $attrs (dict "class" "broken") }}
+ {{- end }}
+ {{- else if eq $errorLevel "error" }}
+ {{- errorf $msg }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+{{- end }}
+
+{{- /* Render anchor element. */ -}}
+{{ .Text }}
+
+{{- define "partials/inline/h-rh-l/validate-fragment.html" }}
+ {{- /*
+ Validates the fragment portion of a link destination.
+
+ @context {string} contentPath The page containing the link.
+ @context {string} errorLevel The error level when unable to resolve destination; ignore (default), warning, or error.
+ @context {page} page The page corresponding to the link destination
+ @context {struct} parsedURL The link destination parsed by urls.Parse.
+ @context {string} renderHookName The name of the render hook.
+ */}}
+
+ {{- /* Initialize. */}}
+ {{- $contentPath := .contentPath }}
+ {{- $errorLevel := .errorLevel }}
+ {{- $p := .page }}
+ {{- $u := .parsedURL }}
+ {{- $renderHookName := .renderHookName }}
+
+ {{- /* Validate. */}}
+ {{- with $u.Fragment }}
+ {{- if $p.Fragments.Identifiers.Contains . }}
+ {{- if gt ($p.Fragments.Identifiers.Count .) 1 }}
+ {{- $msg := printf "The %q render hook detected duplicate heading IDs %q in %s" $renderHookName . $contentPath }}
+ {{- if eq $errorLevel "warning" }}
+ {{- warnf $msg }}
+ {{- else if eq $errorLevel "error" }}
+ {{- errorf $msg }}
+ {{- end }}
+ {{- end }}
+ {{- else }}
+ {{- /* Determine target path for warning and error message. */}}
+ {{- $targetPath := "" }}
+ {{- with $p.File }}
+ {{- $targetPath = .Path }}
+ {{- else }}
+ {{- $targetPath = .Path }}
+ {{- end }}
+ {{- /* Set common message. */}}
+ {{- $msg := printf "The %q render hook was unable to find heading ID %q in %s. See %s" $renderHookName . $targetPath $contentPath }}
+ {{- if eq $targetPath $contentPath }}
+ {{- $msg = printf "The %q render hook was unable to find heading ID %q in %s" $renderHookName . $targetPath }}
+ {{- end }}
+ {{- /* Throw warning or error. */}}
+ {{- if eq $errorLevel "warning" }}
+ {{- warnf $msg }}
+ {{- else if eq $errorLevel "error" }}
+ {{- errorf $msg }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+{{- end }}
+
+{{- define "partials/inline/h-rh-l/get-glossary-link-attributes.html" }}
+ {{- /*
+ Returns the anchor element attributes for a link to the given glossary term.
+
+ It first checks for the existence of a glossary page for the given term. If
+ no page is found, it then checks for a glossary page for the singular form of
+ the term. If neither page exists it throws a warning or error dependent on
+ the errorLevel setting
+
+ The returned href attribute does not point to the glossary term page.
+ Instead, via its fragment, it points to an entry on the glossary page.
+
+ @context {string} contentPath The page containing the link.
+ @context {string} errorLevel The error level when unable to resolve destination; ignore (default), warning, or error.
+ @context {string} renderHookName The name of the render hook.
+ @context {string} text The link text.
+ */}}
+
+ {{- /* Get context.. */}}
+ {{- $contentPath := .contentPath }}
+ {{- $errorLevel := .errorLevel }}
+ {{- $renderHookName := .renderHookName }}
+ {{- $text := .text | transform.Plainify | strings.ToLower }}
+
+ {{- /* Initialize. */}}
+ {{- $glossaryPath := "/quick-reference/glossary" }}
+ {{- $termGiven := $text }}
+ {{- $termActual := "" }}
+ {{- $termSingular := inflect.Singularize $termGiven }}
+
+ {{- /* Verify that the glossary page exists. */}}
+ {{- $glossaryPage := site.GetPage $glossaryPath }}
+ {{- if not $glossaryPage }}
+ {{- errorf "The %q render hook was unable to find %s: see %s" $renderHookName $glossaryPath $contentPath }}
+ {{- end }}
+
+ {{- /* There's a better way to handle this, but it works for now. */}}
+ {{- $cheating := dict
+ "chaining" "chain"
+ "localize" "localization"
+ "localized" "localization"
+ "paginating" "paginate"
+ "walking" "walk"
+ }}
+
+ {{- /* Verify that a glossary term page exists for the given term. */}}
+ {{- if site.GetPage (urls.JoinPath $glossaryPath ($termGiven | urlize)) }}
+ {{- $termActual = $termGiven }}
+ {{- else if site.GetPage (urls.JoinPath $glossaryPath ($termSingular | urlize)) }}
+ {{- $termActual = $termSingular }}
+ {{- else }}
+ {{- $termToTest := index $cheating $termGiven }}
+ {{- if site.GetPage (urls.JoinPath $glossaryPath ($termToTest | urlize)) }}
+ {{- $termActual = $termToTest }}
+ {{- end }}
+ {{- end }}
+
+ {{- if not $termActual }}
+ {{- errorf "The %q render hook was unable to find a glossary page for either the singular or plural form of the term %q: see %s" $renderHookName $termGiven $contentPath }}
+ {{- end }}
+
+ {{- /* Create the href attribute. */}}
+ {{- $href := ""}}
+ {{- if $termActual }}
+ {{- $href = fmt.Printf "%s#%s" $glossaryPage.RelPermalink (anchorize $termActual) }}
+ {{- end }}
+
+ {{- return (dict "href" $href) }}
+{{- end -}}
diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html
new file mode 100644
index 000000000..f8258b33b
--- /dev/null
+++ b/layouts/_default/baseof.html
@@ -0,0 +1,67 @@
+
+
+
+ The world’s fastest framework for building websites
+
+
+ Hugo is one of the most popular open-source static site generators.
+ With its amazing speed and flexibility, Hugo makes building websites
+ fun again.
+
+
+ {{ with site.GetPage "/getting-started" }}
+ {{ .LinkTitle }}
+ {{ end }}
+
+{{- end }}
diff --git a/layouts/partials/layouts/search/input-standalone.html b/layouts/partials/layouts/search/input-standalone.html
new file mode 100644
index 000000000..15251eff2
--- /dev/null
+++ b/layouts/partials/layouts/search/input-standalone.html
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/layouts/partials/layouts/search/input.html b/layouts/partials/layouts/search/input.html
new file mode 100644
index 000000000..e9862e5ca
--- /dev/null
+++ b/layouts/partials/layouts/search/input.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ {{ partial "layouts/search/results.html" . }}
+
diff --git a/layouts/partials/layouts/search/results.html b/layouts/partials/layouts/search/results.html
new file mode 100644
index 000000000..6f4ba698e
--- /dev/null
+++ b/layouts/partials/layouts/search/results.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ partial "layouts/blocks/alert.html" (dict "text" `We're currently reorganizing the content, so the search experience isn't great. Will be fixed soon!`) }}
+
+
diff --git a/layouts/shortcodes/deprecated-in.html b/layouts/shortcodes/deprecated-in.html
new file mode 100644
index 000000000..0272ea4a6
--- /dev/null
+++ b/layouts/shortcodes/deprecated-in.html
@@ -0,0 +1,17 @@
+{{ $_hugo_config := `{ "version": 1 }` }}
+
+{{ with .Get 0 }}
+ {{ $version := printf "v%v" (strings.TrimLeft "vV" .) }}
+ {{ $href := printf "https://github.com/gohugoio/hugo/releases/tag/%s" $version }}
+ {{ $text := (printf `Deprecated in %s.
+%s` $href $version $.Inner) | safeHTML }}
+
+ {{ partial "layouts/blocks/alert.html" (dict
+ "text" $text
+ "color" "orange"
+ "icon" "exclamation"
+ )
+}}
+{{ else }}
+ {{ errorf "The %q shortcode requires a single positional parameter indicating version. See %s" .Name .Position }}
+{{ end }}
diff --git a/layouts/shortcodes/eturl.html b/layouts/shortcodes/eturl.html
new file mode 100644
index 000000000..c0cf30aec
--- /dev/null
+++ b/layouts/shortcodes/eturl.html
@@ -0,0 +1,36 @@
+{{- /*
+Renders an absolute URL to the source code for an embedded template.
+
+Accepts either positional or named parameters, and depends on the
+embedded_templates.toml file in the data directory.
+
+@param {string} filename The embedded template's file name, excluding extension.
+
+@returns template.HTML
+
+@example {{% et robots.txt %}}
+@example {{% et filename=robots.txt %}}
+*/}}
+
+{{- /* Get parameters. */}}
+{{- $filename := "" -}}
+{{- if .IsNamedParams -}}
+ {{- $filename = .Get "filename" -}}
+{{- else -}}
+ {{- $filename = .Get 0 -}}
+{{- end -}}
+
+{{- /* Render. */}}
+{{- with $filename -}}
+ {{- with site.Data.embedded_template_urls -}}
+ {{- with index . $filename -}}
+ {{- urls.JoinPath site.Data.embedded_template_urls.base_url . -}}
+ {{- else -}}
+ {{- errorf "The %q shortcode was unable to find a URL for the embedded template named %q. Check the name. See %s" $.Name $filename $.Position -}}
+ {{- end -}}
+ {{- else -}}
+ {{- errorf "The %q shortcode was unable to find the embedded_template_urls data file in the site's data directory. See %s" $.Name $.Position -}}
+ {{- end -}}
+{{- else -}}
+ {{- errorf "The %q shortcodes requires a named or positional parameter, the file name of the embedded template, excluding its extension. See %s" .Name .Position -}}
+{{- end -}}
diff --git a/layouts/shortcodes/glossary-term.html b/layouts/shortcodes/glossary-term.html
new file mode 100644
index 000000000..7aace730e
--- /dev/null
+++ b/layouts/shortcodes/glossary-term.html
@@ -0,0 +1,20 @@
+{{- /*
+Renders the definition of the given glossary term.
+
+@param {string} (.Get 0) The glossary term.
+@returns {template.HTML}
+
+@example {{% glossary-term float %}}
+@example {{% glossary-term "floating point" %}}
+*/}}
+
+{{- with .Get 0 }}
+ {{- $path := printf "/quick-reference/glossary/%s" (urlize .) }}
+ {{- with site.GetPage $path }}
+{{ .RenderShortcodes }} {{/* Do not indent. Do not remove non-breaking space. */}}
+ {{- else }}
+ {{- errorf "The glossary term (%s) shortcode was unable to find %s: see %s" $.Name $path $.Position }}
+ {{- end }}
+{{- else }}
+ {{- errorf "The glossary term (%s) shortcode requires one positional parameter: see %s" $.Name $.Position }}
+{{- end -}}
diff --git a/layouts/shortcodes/glossary.html b/layouts/shortcodes/glossary.html
new file mode 100644
index 000000000..e91be32ce
--- /dev/null
+++ b/layouts/shortcodes/glossary.html
@@ -0,0 +1,56 @@
+{{- /*
+Renders the glossary of terms.
+
+When you call this shortcode using the {{% %}} notation, the glossary terms are
+Markdown headings (level 6) which means they are members of .Page.Fragments.
+This allows the link render hook to verify links to glossary terms.
+
+Yes, the terms themselves are pages, but we don't want to link to the pages, at
+least not right now. Instead, we want to link to the fragments rendered by this
+shortcode.
+
+@returns {template.HTML}
+
+@example {{% glossary %}}
+*/}}
+{{- $path := "/quick-reference/glossary" }}
+{{- with site.GetPage $path }}
+
+ {{- /* Build and render alphabetical index. */}}
+ {{- $m := dict }}
+ {{- range $p := .Pages.ByTitle }}
+ {{- $k := substr .Title 0 1 | strings.ToUpper }}
+ {{- if index $m $k }}
+ {{- continue }}
+ {{- end }}
+ {{- $anchor := path.BaseName .Path | anchorize }}
+ {{- $m = merge $m (dict $k $anchor) }}
+ {{- end }}
+ {{- range $k, $v := $m }}
+[{{ $k }}](#{{ $v }}) {{/* Do not indent. */}}
+ {{- end }}
+
+ {{- /* Render glossary terms. */}}
+ {{- range $p := .Pages.ByTitle }}
+###### {{ .Title }}{{/* Do not indent. */}}
+{{ .RenderShortcodes }}{{/* Do not indent. */}}
+ {{- with .Params.reference }}
+ {{- $destination := "" }}
+ {{- with $u := urls.Parse . }}
+ {{- if $u.IsAbs }}
+ {{- $destination = $u.String }}
+ {{- else }}
+ {{- with site.GetPage $u.Path -}}
+ {{- $destination = .RelPermalink }}
+ {{- else }}
+ {{- errorf "The %q shortcode was unable to find the reference link %s: see %s" $.Name . $p.String }}
+ {{- end }}
+ {{- end }}
+ {{- end -}}
+ See [details]({{ $destination }}).{{/* Do not indent. */}}
+ {{- end }}
+ {{- end }}
+
+{{- else }}
+ {{- errorf "The %q shortcode was unable to get %s: see %s" .Name $path .Position}}
+{{- end }}
diff --git a/layouts/shortcodes/gomodules-info.html b/layouts/shortcodes/gomodules-info.html
new file mode 100644
index 000000000..126d846c0
--- /dev/null
+++ b/layouts/shortcodes/gomodules-info.html
@@ -0,0 +1,12 @@
+{{ $text := `
+ Most of the commands for **Hugo Modules** require a newer version (>= 1.18) of Go installed (see https://golang.org/dl/) and the relevant VCS client (e.g. Git, see https://git-scm.com/downloads/ ).
+ If you have an "older" site running on Netlify, you may have to set GO_VERSION to 1.19 or newer in your Environment settings.
+
+ For more information about Go Modules, see:
+
+ * https://go.dev/wiki/Modules
+ * https://blog.golang.org/using-go-modules
+ `
+}}
+
+{{ partial "layouts/blocks/alert.html" (dict "title" "Go Modules" "text" ($text | markdownify) "color" "orange" "icon" "exclamation") }}
diff --git a/layouts/shortcodes/hl.html b/layouts/shortcodes/hl.html
new file mode 100644
index 000000000..1a5b4e1ec
--- /dev/null
+++ b/layouts/shortcodes/hl.html
@@ -0,0 +1,11 @@
+{{- /*
+Returns syntax-highlighted code from the given text.
+
+This is useful as a terse way to highlight inline code snippets. Calling the
+highlight shortcode for inline snippets is verbose.
+*/}}
+
+{{- $code := .Inner | strings.TrimSpace }}
+{{- $lang := or (.Get 0) "go" }}
+{{- $opts := dict "hl_inline" true "noClasses" true }}
+{{- transform.Highlight $code $lang $opts }}
diff --git a/layouts/shortcodes/img.html b/layouts/shortcodes/img.html
new file mode 100644
index 000000000..7c2d805d2
--- /dev/null
+++ b/layouts/shortcodes/img.html
@@ -0,0 +1,391 @@
+{{- /*
+Renders the given image using the given filter, if any.
+
+@param {string} src The path to the image which must be a remote, page, or global resource.
+@param {string} [filter] The filter to apply to the image (case-insensitive).
+@param {string} [filterArgs] A comma-delimited list of arguments to pass to the filter.
+@param {bool} [example=false] If true, renders a before/after example.
+@param {int} [exampleWidth=384] Image width, in pixels, when rendering a before/after example.
+
+@returns {template.HTML}
+
+@examples
+
+ {{< img src="zion-national-park.jpg" >}}
+
+ {{< img src="zion-national-park.jpg" alt="Zion National Park" >}}
+
+ {{< img
+ src="zion-national-park.jpg"
+ alt="Zion National Park"
+ filter="grayscale"
+ >}}
+
+ {{< img
+ src="zion-national-park.jpg"
+ alt="Zion National Park"
+ filter="process"
+ filterArgs="resize 400x webp"
+ >}}
+
+ {{< img
+ src="zion-national-park.jpg"
+ alt="Zion National Park"
+ filter="colorize"
+ filterArgs="180,50,20"
+ >}}
+
+ {{< img
+ src="zion-national-park.jpg"
+ alt="Zion National Park"
+ filter="grayscale"
+ example=true
+ >}}
+
+ {{< img
+ src="zion-national-park.jpg"
+ alt="Zion National Park"
+ filter="grayscale"
+ example=true
+ exampleWidth=400
+ >}}
+
+ When using the text filter, provide the arguments in this order:
+
+ 0. The text
+ 1. The horizontal offset, in pixels, relative to the left of the image (default 20)
+ 2. The vertical offset, in pixels, relative to the top of the image (default 20)
+ 3. The font size in pixels (default 64)
+ 4. The line height (default 1.2)
+ 5. The font color (default #ffffff)
+
+ {{< img
+ src="images/examples/zion-national-park.jpg"
+ alt="Zion National Park"
+ filter="Text"
+ filterArgs="Zion National Park,25,250,56"
+ example=true
+ >}}
+
+ When using the padding filter, provide all arguments in this order:
+
+ 0. Padding top
+ 1. Padding right
+ 2. Padding bottom
+ 3. Padding right
+ 4. Canvas color
+
+ {{< img
+ src="images/examples/zion-national-park.jpg"
+ alt="Zion National Park"
+ filter="Padding"
+ filterArgs="20,50,20,50,#0705"
+ example=true
+ >}}
+
+*/}}
+
+{{- /* Initialize. */}}
+{{- $alt := "" }}
+{{- $src := "" }}
+{{- $filter := "" }}
+{{- $filterArgs := slice }}
+{{- $example := false }}
+{{- $exampleWidth := 384 }}
+
+{{- /* Default values to use with the text filter. */}}
+{{ $textFilterOpts := dict
+ "xOffset" 20
+ "yOffset" 20
+ "fontSize" 64
+ "lineHeight" 1.2
+ "fontColor" "#ffffff"
+ "fontPath" "https://github.com/google/fonts/raw/main/ofl/lato/Lato-Regular.ttf"
+}}
+
+{{- /* Get and validate parameters. */}}
+{{- with .Get "alt" }}
+ {{- $alt = .}}
+{{- end }}
+
+{{- with .Get "src" }}
+ {{- $src = . }}
+{{- else }}
+ {{- errorf "The %q shortcode requires a file parameter. See %s" .Name .Position }}
+{{- end }}
+
+{{- with .Get "filter" }}
+ {{- $filter = . | lower }}
+{{- end }}
+
+{{- $validFilters := slice
+ "autoorient" "brightness" "colorbalance" "colorize" "contrast" "dither"
+ "gamma" "gaussianblur" "grayscale" "hue" "invert" "mask" "none" "opacity"
+ "overlay" "padding" "pixelate" "process" "saturation" "sepia" "sigmoid" "text"
+ "unsharpmask"
+}}
+
+{{- with $filter }}
+ {{- if not (in $validFilters .) }}
+ {{- errorf "The filter passed to the %q shortcode is invalid. The filter must be one of %s. See %s" $.Name (delimit $validFilters ", " ", or ") $.Position }}
+ {{- end }}
+{{- end }}
+
+{{- with .Get "filterArgs" }}
+ {{- $filterArgs = split . "," }}
+ {{- $filterArgs = apply $filterArgs "trim" "." " " }}
+{{- end }}
+
+{{- if in (slice "false" false 0) (.Get "example") }}
+ {{- $example = false }}
+{{- else if in (slice "true" true 1) (.Get "example")}}
+ {{- $example = true }}
+{{- end }}
+
+{{- with .Get "exampleWidth" }}
+ {{- $exampleWidth = . | int }}
+{{- end }}
+
+{{- /* Get image. */}}
+{{- $ctx := dict "page" .Page "src" $src "name" .Name "position" .Position }}
+{{- $i := partial "inline/get-resource.html" $ctx }}
+
+{{- /* Resize if rendering before/after examples. */}}
+{{- if $example }}
+ {{- $i = $i.Resize (printf "%dx" $exampleWidth) }}
+{{- end }}
+
+{{- /* Create filter. */}}
+{{- $f := "" }}
+{{- $ctx := dict "filter" $filter "args" $filterArgs "name" .Name "position" .Position }}
+{{- if eq $filter "autoorient" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 0) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $f = images.AutoOrient }}
+{{- else if eq $filter "brightness" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" -100 "max" 100) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Brightness (index $filterArgs 0) }}
+{{- else if eq $filter "colorbalance" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 3) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage red" "argValue" (index $filterArgs 0) "min" -100 "max" 500) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage green" "argValue" (index $filterArgs 1) "min" -100 "max" 500) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage blue" "argValue" (index $filterArgs 2) "min" -100 "max" 500) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.ColorBalance (index $filterArgs 0) (index $filterArgs 1) (index $filterArgs 2) }}
+{{- else if eq $filter "colorize" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 3) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "hue" "argValue" (index $filterArgs 0) "min" 0 "max" 360) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $ctx = merge $ctx (dict "argName" "saturation" "argValue" (index $filterArgs 1) "min" 0 "max" 100) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 2) "min" 0 "max" 100) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Colorize (index $filterArgs 0) (index $filterArgs 1) (index $filterArgs 2) }}
+{{- else if eq $filter "contrast" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" -100 "max" 100) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Contrast (index $filterArgs 0) }}
+{{- else if eq $filter "dither" }}
+ {{- $f = images.Dither }}
+{{- else if eq $filter "gamma" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "gamma" "argValue" (index $filterArgs 0) "min" 0 "max" 100) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Gamma (index $filterArgs 0) }}
+{{- else if eq $filter "gaussianblur" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "sigma" "argValue" (index $filterArgs 0) "min" 0 "max" 1000) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.GaussianBlur (index $filterArgs 0) }}
+{{- else if eq $filter "grayscale" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 0) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $f = images.Grayscale }}
+{{- else if eq $filter "hue" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "shift" "argValue" (index $filterArgs 0) "min" -180 "max" 180) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Hue (index $filterArgs 0) }}
+{{- else if eq $filter "invert" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 0) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $f = images.Invert }}
+{{- else if eq $filter "mask" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $ctx := dict "src" (index $filterArgs 0) "name" .Name "position" .Position }}
+ {{- $maskImage := partial "inline/get-resource.html" $ctx }}
+ {{- $f = images.Mask $maskImage }}
+{{- else if eq $filter "opacity" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "opacity" "argValue" (index $filterArgs 0) "min" 0 "max" 1) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Opacity (index $filterArgs 0) }}
+{{- else if eq $filter "overlay" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 3) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $ctx := dict "src" (index $filterArgs 0) "name" .Name "position" .Position }}
+ {{- $overlayImg := partial "inline/get-resource.html" $ctx }}
+ {{- $f = images.Overlay $overlayImg (index $filterArgs 1 | float ) (index $filterArgs 2 | float) }}
+{{- else if eq $filter "padding" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 5) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $f = images.Padding
+ (index $filterArgs 0 | int)
+ (index $filterArgs 1 | int)
+ (index $filterArgs 2 | int)
+ (index $filterArgs 3 | int)
+ (index $filterArgs 4)
+ }}
+{{- else if eq $filter "pixelate" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "size" "argValue" (index $filterArgs 0) "min" 0 "max" 1000) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Pixelate (index $filterArgs 0) }}
+{{- else if eq $filter "process" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $f = images.Process (index $filterArgs 0) }}
+{{- else if eq $filter "saturation" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" -100 "max" 500) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Saturation (index $filterArgs 0) }}
+{{- else if eq $filter "sepia" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" 0 "max" 100) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Sepia (index $filterArgs 0) }}
+{{- else if eq $filter "sigmoid" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 2) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "midpoint" "argValue" (index $filterArgs 0) "min" 0 "max" 1) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $ctx = merge $ctx (dict "argName" "factor" "argValue" (index $filterArgs 1) "min" -10 "max" 10) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Sigmoid (index $filterArgs 0) (index $filterArgs 1) }}
+{{- else if eq $filter "text" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $ctx := dict "src" $textFilterOpts.fontPath "name" .Name "position" .Position }}
+ {{- $font := or (partial "inline/get-resource.html" $ctx) }}
+ {{- $fontSize := or (index $filterArgs 3 | int) $textFilterOpts.fontSize }}
+ {{- $lineHeight := math.Max (or (index $filterArgs 4 | float) $textFilterOpts.lineHeight) 1 }}
+ {{- $opts := dict
+ "x" (or (index $filterArgs 1 | int) $textFilterOpts.xOffset)
+ "y" (or (index $filterArgs 2 | int) $textFilterOpts.yOffset)
+ "size" $fontSize
+ "linespacing" (mul (sub $lineHeight 1) $fontSize)
+ "color" (or (index $filterArgs 5) $textFilterOpts.fontColor)
+ "font" $font
+ }}
+ {{- $f = images.Text (index $filterArgs 0) $opts }}
+{{- else if eq $filter "unsharpmask" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 3) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "sigma" "argValue" (index $filterArgs 0) "min" 0 "max" 500) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $ctx = merge $ctx (dict "argName" "amount" "argValue" (index $filterArgs 1) "min" 0 "max" 100) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $ctx = merge $ctx (dict "argName" "threshold" "argValue" (index $filterArgs 2) "min" 0 "max" 1) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.UnsharpMask (index $filterArgs 0) (index $filterArgs 1) (index $filterArgs 2) }}
+{{- end }}
+
+{{- /* Apply filter. */}}
+{{- $fi := $i }}
+{{- with $f }}
+ {{- $fi = $i.Filter . }}
+{{- end }}
+
+{{- /* Render. */}}
+{{- $class := "di va b--black-20" }}
+{{- if eq $filter "mask" }}
+ {{- $class = "di va" }}
+{{- end }}
+{{- if $example }}
+
Original
+
+
Processed
+
+{{- else -}}
+
+{{- end }}
+
+{{- define "validate-arg-count" }}
+ {{- $msg := "When using the %q filter, the %q shortcode requires an args parameter with %d %s. See %s" }}
+ {{- if lt (len .args) .argsRequired }}
+ {{- $text := "values" }}
+ {{- if eq 1 .argsRequired }}
+ {{- $text = "value" }}
+ {{- end }}
+ {{- errorf $msg .filter .name .argsRequired $text .position }}
+ {{- end }}
+{{- end }}
+
+{{- define "validate-arg-value" }}
+ {{- $msg := "The %q argument passed to the %q shortcode is invalid. Expected a value in the range [%v,%v], but received %v. See %s" }}
+ {{- if or (lt .argValue .min) (gt .argValue .max) }}
+ {{- errorf $msg .argName .name .min .max .argValue .position }}
+ {{- end }}
+{{- end }}
+
+{{- define "partials/inline/get-resource.html" }}
+ {{- $r := "" }}
+ {{- $u := urls.Parse .src }}
+ {{- $msg := "The %q shortcode was unable to resolve %s. See %s" }}
+ {{- if $u.IsAbs }}
+ {{- with try (resources.GetRemote $u.String) }}
+ {{- with .Err }}
+ {{- errorf "%s" . }}
+ {{- else with .Value }}
+ {{- /* This is a remote resource. */}}
+ {{- $r = . }}
+ {{- else }}
+ {{- errorf $msg $.name $u.String $.position }}
+ {{- end }}
+ {{- end }}
+ {{- else }}
+ {{- with .page.Resources.Get (strings.TrimPrefix "./" $u.Path) }}
+ {{- /* This is a page resource. */}}
+ {{- $r = . }}
+ {{- else }}
+ {{- with resources.Get $u.Path }}
+ {{- /* This is a global resource. */}}
+ {{- $r = . }}
+ {{- else }}
+ {{- errorf $msg $.name $u.Path $.position }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ {{- return $r}}
+{{- end -}}
diff --git a/layouts/shortcodes/imgproc.html b/layouts/shortcodes/imgproc.html
new file mode 100644
index 000000000..c09133ba8
--- /dev/null
+++ b/layouts/shortcodes/imgproc.html
@@ -0,0 +1,37 @@
+{{- /*
+Renders the given image using the given process specification.
+
+@param {string} (positional parameter 0) The path to the image, relative to the current page. The image must be a page resource.
+@param {string}} (positional parameter 1) The image processing specification.
+
+@returns template.HTML
+
+@example {{< imgproc "sunset.jpg" "resize 300x" />}}
+*/}}
+
+{{- with $.Get 0 }}
+ {{- with $i := $.Page.Resources.Get . }}
+ {{- with $spec := $.Get 1 }}
+ {{- with $i.Process . }}
+
+
+
+
+ {{- with $.Inner }}
+ {{ . }}
+ {{- else }}
+ {{ $spec }}
+ {{- end }}
+
+
+
+ {{- end }}
+ {{- else }}
+ {{- errorf "The %q shortcode requires a positional parameter (1) containing the image processing specification. See %s" $.Name $.Position }}
+ {{- end }}
+ {{- else }}
+ {{- errorf "The %q shortcode was unable to find %q. See %s" $.Name . $.Position }}
+ {{- end }}
+{{- else }}
+ {{- errorf "The %q shortcode requires a positional parameter (0) indicating the image path, relative to the current page. See %s" $.Name $.Position }}
+{{- end }}
diff --git a/layouts/shortcodes/include.html b/layouts/shortcodes/include.html
new file mode 100644
index 000000000..b4a20cd72
--- /dev/null
+++ b/layouts/shortcodes/include.html
@@ -0,0 +1,21 @@
+{{- /*
+Renders the page using the RenderShortcode method on the Page object.
+
+You must call this shortcode using the {{% %}} notation.
+
+@param {string} (positional parameter 0) The path to the page, relative to the content directory.
+@returns template.HTML
+
+@example {{% include "functions/_common/glob-patterns" %}}
+*/}}
+
+{{- with .Get 0 }}
+ {{- with or ($.Page.GetPage .) (site.GetPage .) }}
+ {{- .RenderShortcodes }}
+ {{- else }}
+ {{/* TODO1 make error */}}
+ {{- warnf "The %q shortcode was unable to find %q. See %s" $.Name . $.Position }}
+ {{- end }}
+{{- else }}
+ {{- errorf "The %q shortcode requires a positional parameter indicating the path of the file to include. See %s" .Name .Position }}
+{{- end }}
diff --git a/layouts/shortcodes/list-pages-in-section.html b/layouts/shortcodes/list-pages-in-section.html
new file mode 100644
index 000000000..73e7f85a9
--- /dev/null
+++ b/layouts/shortcodes/list-pages-in-section.html
@@ -0,0 +1,96 @@
+{{- /*
+Renders a description list of the pages in the given section.
+
+Render a subset of the pages in the section by specifying a predefined filter,
+and whether to include those pages.
+
+Filters are defined in the data directory, in the file named page_filters. Each
+filter is an array of paths to a file, relative to the root of the content
+directory. Hugo will throw an error if the specified filter does not exist, or
+if any of the pages in the filter do not exist.
+
+The definition term elements (dt) have an id attribute derived from the title
+of the page. This is probably unique, because pages of the same title in the
+same section is unlikely.
+
+If you render a complete list on a page, then call the shortcode again to
+render a subset, you will generate duplicate element ids. In this case, set
+omitElementIDs to true for the subset.
+
+@param {string} path The path to the section.
+@param {string} [filter=""] The name of filter list.
+@param {string} [filterType=""] The type of filter, either include or exclude.
+@param {string} [omitElementIDs=false] Whether to omit dt element ids.
+@param {string} [titlePrefix=""] The string to prepend to the link title.
+
+@returns template.HTML
+
+@example {{< list-pages-in-section path=/methods/resources >}}
+@example {{< list-pages-in-section path=/functions/images filter=some_filter filterType=exclude >}}
+@example {{< list-pages-in-section path=/functions/images filter=some_filter filterType=exclude titlePrefix=foo >}}
+@example {{< list-pages-in-section path=/functions/images filter=some_filter filterType=exclude titlePrefix=foo omitElementIDs=true >}}
+*/}}
+
+{{- /* Initialize. */}}
+{{- $filter := or "" (.Get "filter" | lower)}}
+{{- $filterType := or (.Get "filterType") "none" | lower }}
+{{- $filteredPages := slice }}
+{{- $titlePrefix := or (.Get "titlePrefix") "" }}
+{{- $omitElementIDs := false }}
+
+{{- /* Get boolean parameters. */}}
+{{- if in (slice "false" false 0) (.Get "omitElementIDs") }}
+ {{- $omitElementIDs = false }}
+{{- else if in (slice "true" true 1) (.Get "omitElementIDs")}}
+ {{- $omitElementIDs = true }}
+{{- end }}
+
+{{- /* Build slice of filtered pages. */}}
+{{- with $filter }}
+ {{- with index site.Data.page_filters . }}
+ {{- range . }}
+ {{- with site.GetPage . }}
+ {{- $filteredPages = $filteredPages | append . }}
+ {{- else }}
+ {{- errorf "The %q shortcode was unable to find %q as specified in the page_filters data file. See %s" $.Name . $.Position }}
+ {{- end }}
+ {{- end }}
+ {{- else }}
+ {{- errorf "The %q shortcode was unable to find the %q filter in the page_filters data file. See %s" $.Name . $.Position }}
+ {{- end }}
+{{- end }}
+
+{{- /* Render */}}
+{{- with $sectionPath := .Get "path" }}
+ {{- with site.GetPage . }}
+ {{- with .RegularPages }}
+
+ {{- range $page := .ByTitle }}
+ {{- if or
+ (and (eq $filterType "include") (in $filteredPages $page))
+ (and (eq $filterType "exclude") (not (in $filteredPages $page)))
+ (eq $filterType "none")
+ }}
+ {{- $linkTitle := .LinkTitle }}
+ {{- with $titlePrefix }}
+ {{- $linkTitle = printf "%s%s" . $linkTitle }}
+ {{- end }}
+ {{- $idAttribute := "" }}
+ {{- if not $omitElementIDs }}
+ {{- $id := path.Join .File.Dir .File.ContentBaseName | replaceRE `[\|/]` ":" | lower }}
+ {{- $idAttribute = printf " id=%q" $id }}
+ {{- end }}
+