You can modify the default extensions the Milo Docs theme has set for TailwindCSS. This is useful when you’d like to change the branded fonts or change Tailwind’s default font sizes.
Updating Fonts?
If you are updating the fonts, make sure that you:
Install your font files at /static/fonts.
Add them to your CSS at /themes/milo/assets/css/src/input.css.
Hugo can reference CSS from multiple locations (e.g., static and assets). The MiloDocs theme keeps all CSS in the assets folder since we want to process the files in a variety of ways — such as concatenating many modular CSS files together into a bundled output that we can minify for production deployments.
The fonts.css file is where you can add/replace the font references to adhere to your brand aesthetic.
/* Rubik Variable Font */@font-face {
font-family:'Rubik';src:url('/fonts/Rubik-VariableFont_wght.ttf')format('truetype');font-weight:300900;/* Range of weights available in the variable font */font-display:swap;}
/* Rubik Italic Variable Font */@font-face {
font-family:'Rubik';src:url('/fonts/Rubik-Italic-VariableFont_wght.ttf')format('truetype');font-style:italic;font-weight:300900;/* Range of weights available in the variable font */font-display:swap;}
The colors.css file is where you can replace the color references to adhere to your brand aesthetic.
:root {
--primary-gradient-color: #ffffff;
--secondary-gradient-color: rgb(138, 193, 149, 0.500);
--icon-color: #000000; /* Black *//* HPE Brand Colors */ --color-brand:#8ac195; /*primary color*/ --color-brand-1:rgba(131, 122, 117, 0.667); /*secondary color*/ --color-brand-2:#acc18a; /*tertiary color*/ --color-brand-3:#86a6cf; /*note color*/ --color-brand-4:#ADD9F4; /*tip color */ --color-brand-5:#dbd985; /*security color */ --color-brand-6:#d4ac84; /*warning color */ --color-brand-7:#F3B3A6; /*danger color */}
.dark {
--primary-gradient-color: rgb(138, 193, 149, 0.300); /* Change this to your dark mode color */ --secondary-gradient-color: #202020; /* Change this to your dark mode color */ --icon-color: #ffffff; /* Change this to your dark mode color */}
#articleContent [id]:target {
scroll-margin-top: 50px;
}
Affordable: Technical Writers usually have a razor-thin budget; you can deploy docs with Hugo + Netlify (or Render, Vercel) for free in most cases (Startup, Open Source)
Scalable: Hugo is the fastest SSG, supports localization, is un-opinionated in terms of style, and is easy to evolve alongside your product
Ergonomic: The drafting UX is markdown focused with near-instant local previews; for non-tehchnical contributors, you can plug into CMS interfaces (e.g., Frontmatter)
Agnostic: You’ll always own your docs, and transforming content into JSON or XML is as easy as defining an output template (great for search tools like Algolia!)
No Manual Menus: All sections are auto-sorted based on either a weight value or a-z title order
Deep Section Nesting: Tech docs tend to need sub-sub-sub sections, y’know?
Discovery UX Components: Algolia Search & ChatGPT UIs are OOTB for easy hookup
Battle Tested Shortcodes: I’ve been deploying Hugo for tech docs for 5+ years; this is my personal collection of need-to-haves, made as agnostically as possible
TailwindCSS + VanillaJS: You’ll be able to modify this theme to your liking using the basics with minimal dependencies
Brandable: Colors and fonts have their own CSS files; the TailwindCSS extensions mapped to these styles use generic names like font-brand, font-semibold, and brand-color-1.
You can install this theme as a submodule, however the default content files will not be removable. I may separate these in the future depending on what people would like.
You’ve done the hardest part: installing and deploying Hugo with a theme. See the next page to learn how to clear out my default content and start drafting.
This partial layout concatenates all of the modular VanillaJS files found in themes/milo/assets/js and outputs a bundle.js file for deployment use. It also minifies and fingerprints the output.
This theme uses a variety of shortcodes to enable authors of technical documentation.
By default, content stored in the theme’s content directory (themes/milo/content/) is served alongside content stored in the site’s content directory (content/). You’re going to want to clear that out once you’re ready to write your own docs.
Test Feature Config
The default documentation is great for testing out different feature configs, such as:
disabling next/prev
changing truncation values
updating the number of child articles to displayed in a tile.
That’s it! Now start drafting in your top-level content/ directory.
feedback encouraged
I’m by no means a JS expert; if you feel these files can be optimized or re-written in any way, don’t hesitate to reach out on GitHub / submit a pull request.
Layouts in the _default folder define the major content types and outputs of your Hugo site.
renders markdown files as a stacked list in a directory (dir/_index.md).
branch
layout: glossary
tutorial.html
renders markdown files as a wizard with steps (dir/_index.md).
branch
layout: tutorial
tutorialstep.html
renders a child markdown file as a tutorial step (tutorial/step.md).
leaf
layout: tutStep
Hugo supports JSON outputs out of the box. But to utilize it, you should define an output template based on the data available to your page kinds and any frontmatter included in the markdown file.
This partial is used in the following default layouts:
By itself, this partial doesn’t do anything. When piped reference from a few of our default layouts, it acts as a standardized blueprint for how each article should look like in json. Updating this file will cascade to all outputs where this is referenced.
Quisque consequat consectetur quam at consequat. Integer efficitur quam id eros auctor, et malesuada justo congue. Fusce non feugiat tellus. Proin a eros in dolor bibendum malesuada ut et elit. Sed sollicitudin, ipsum et fermentum facilisis, urna nisl vehicula neque, in malesuada dolor orci et dolor. Etiam in libero ut turpis cursus tincidunt. Integer in libero non ante laoreet vestibulum in sed orci. Vivamus luctus bibendum velit, at egestas justo fermentum in.
name
partial refs
default refs
next-prev.html
single.html, section.html
tiles.html
home.html, section.html
chat.html
sidebar-right.html
baseof.html
toc.html
sidebar-right.html
baseof.html
Quisque consequat consectetur quam at consequat. Integer efficitur quam id eros auctor, et malesuada justo congue. Fusce non feugiat tellus. Proin a eros in dolor bibendum malesuada ut et elit. Sed sollicitudin, ipsum et fermentum facilisis, urna nisl vehicula neque, in malesuada dolor orci et dolor. Etiam in libero ut turpis cursus tincidunt. Integer in libero non ante laoreet vestibulum in sed orci. Vivamus luctus bibendum velit, at egestas justo fermentum in.
The baseof.html template is the centralization point that glues the site theme together. All other templates defined in the theme are embedded into this one at build – meaning that global logic and stylings defined here.
Using an LLM to enhance the discoverability of your content is quickly becoming a baseline requirement for documentation. Thankfully, it’s not too hard to do thanks to Hugo’s output to JSON.
At a high level, you’ll need to provide some server-side code in Python or JS that routes user questions to chatGPT after being passed some embeddings (created from your docs JSON) for context.
This partial sends an API request to a GCP cloud function you’ll need to set up that uses Flask (built in) to:
Search a Pinecone vector database filled with embeddings created from your documentation.
Perform a similarity search and return the 4 most relevant chunks.
Forward those chunks to the OpenAI API via LangChain to perform RAG services.
Return an answer based on the question and content provided.
have it your way
There are several ways to implement a RAG LLM UX — this is just the way that currently works for me. It seems like in the future people may shift from LangChain to the official Assistant API. Hopefully sharing this implementation helps you achieve yours!
import os
from dotenv import load_dotenv
import time
from langchain.document_loaders import JSONLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import Pinecone
from langchain.embeddings.openai import OpenAIEmbeddings
import pinecone
load_dotenv()
openai_key = os.environ.get('OPENAI_API_KEY')
pinecone_key = os.environ.get('PINECONE_API_KEY')
pinecone_environment = os.environ.get('PINECONE_ENVIRONMENT')
pinecone_index = os.environ.get('PINECONE_INDEX')
docs_index_path ="./docs.json"docs_index_schema =".[]"# [{"body:..."}] -> .[].body; see JSONLoader docs for more infoembeddings = OpenAIEmbeddings(openai_api_key=openai_key)
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=0,)
defchunks(lst, n):
"""Yield successive n-sized chunks from lst."""for i in range(0, len(lst), n):
yield lst[i:i + n]
defmetadata_func(record: dict, metadata: dict) -> dict:
metadata["title"] = record.get("title")
metadata["relURI"] = record.get("relURI")
return metadata
loader = JSONLoader(docs_index_path, jq_schema=docs_index_schema, metadata_func=metadata_func, content_key="body")
data = loader.load()
texts = text_splitter.split_documents(data)
pinecone.init(
api_key=pinecone_key,
environment=pinecone_environment,
)
if pinecone_index in pinecone.list_indexes():
print(f'The {pinecone_index} index already exists! We need to replace it with a new one.')
print("Erasing existing index...")
pinecone.delete_index(pinecone_index)
time.sleep(60)
print("Recreating index...")
# wait a minute for the index to be deletedpinecone.create_index(pinecone_index, metric="cosine", dimension=1536, pods=1, pod_type="p1")
if pinecone_index in pinecone.list_indexes():
print(f"Loading {len(texts)} texts to index {pinecone_index}... \n This may take a while. Here's a preview of the first text: \n{texts[0].metadata}\n{texts[0].page_content}")
for chunk in chunks(texts, 25):
for doc in chunk:
if doc.page_content.strip():
print(f"Indexing: {doc.metadata['title']}")
print(f"Content: {doc.page_content}")
Pinecone.from_texts([doc.page_content], embedding=embeddings, index_name=pinecone_index, metadatas=[doc.metadata])
else:
print("Ignoring blank document")
print("Done!")
If you know how to successfully separate this JS into its own file in assets/js, please submit a PR. It doesn’t work for me!
<divid="chatContainer"class="hidden sticky top-16 h-[calc(100vh-5rem)] flex flex-col flex justify-end">
<divid="chat-messages"class="flex flex-col overflow-y-auto text-base">
</div>
<divid="chat-controls"class="flex flex-row text-xs mt-2">
<formonsubmit="submitQuestion(event)"class="flex flex-row">
<inputid="question"type="text"aria-label="Question Input"placeholder="Ask the docs"class="h-10 border rounded-lg p-1 mr-1 focus:outline-none focus:ring-2 focus:ring-brand"/>
<buttonid="sendButton"aria-label="Send"class="flex items-center bg-brand my-1 hover:bg-black text-white p-1 mr-1 rounded-lg shadow-lg transition duration-300"><imgsrc="/icons/send.svg"alt="Send"class="w-5 h-5"></button>
</form>
<buttonid="clearAll"aria-label="Delete All"onclick="clearConversation()"class="flex items-center bg-black my-1 hover:bg-red-600 text-white p-1 rounded-lg shadow-lg transition duration-300"><imgsrc="/icons/delete.svg"alt="Delete"class="w-5 h-5"></button>
</div>
</div>
<script>
// Define a function to handle form submission
functionsubmitQuestion(event) {
event.preventDefault();
constquestionInput = document.getElementById('question');
constquestionText = questionInput.value.trim();
if (!questionText) return; // Exit if the question is empty
questionInput.value = ''; // Clear the input field
addChatBubble(questionText, 'user');
fetchAnswer(questionText);
}
// Define a function to fetch answer from the API
asyncfunctionfetchAnswer(question) {
constresponse = awaitfetch(`https://milodocs-lc4762co7a-uc.a.run.app/?query=${encodeURIComponent(question)}`);
constdata = awaitresponse.json();
constanswer = data.answer||'Sorry, Icouldnotfetchtheanswer.';
addChatBubble(answer, 'bot');
}
// Define a function to add chat bubble
functionaddChatBubble(text, sender) {
constchatMessages = document.getElementById('chat-messages');
letpair = chatMessages.lastElementChild;
if (!pair|| !pair.classList.contains('chat-pair') ||sender=== 'user') {
pair = document.createElement('div');
pair.className = 'chat-pairbg-zinc-100flexflex-colmy-2p-2rounded-lg';
chatMessages.appendChild(pair);
}
constbubble = document.createElement('div');
bubble.className = `chat-bubble ${sender} p-2 rounded-lg text-black ${sender === 'user' ? 'font-brand font-semibold' : 'font-brand font-regular'}`;
bubble.innerText = text;
pair.appendChild(bubble);
if (sender=== 'user') {
bubble.classList.add('animate-pulse'); // Add pulsing animation to user bubble
} else {
constuserBubble = pair.querySelector('.user');
if (userBubble) userBubble.classList.remove('animate-pulse'); // Remove pulsing animation when bot responds
constdeleteButtonWrapper = document.createElement('div');
deleteButtonWrapper.className = 'w-fullflexjustify-end';
constdeleteButton = document.createElement('button');
deleteButton.className = 'w-fitp-2roundedbg-zinc-200text-xslowercasehover:bg-red-600hover:text-whitetransitionduration-300text-black';
deleteButton.innerText = 'Delete';
deleteButton.addEventListener('click', () => {
chatMessages.removeChild(pair);
saveChatHistory();
});
deleteButtonWrapper.appendChild(deleteButton);
pair.appendChild(deleteButtonWrapper);
}
// Scroll to the bottom of the chat container
chatMessages.scrollTop = chatMessages.scrollHeight;
saveChatHistory();
}
// Define a function to clear conversation
functionclearConversation() {
constchatMessages = document.getElementById('chat-messages');
chatMessages.innerHTML = '';
saveChatHistory();
}
// Define a function to save chat history
functionsaveChatHistory() {
constchatMessages = Array.from(document.getElementById('chat-messages').children);
constchatHistory = chatMessages.map(pair => {
constbubbles = Array.from(pair.children);
consttexts = bubbles.map(bubble => bubble.innerText);
return {
user: texts[0],
bot: texts[1]
};
});
localStorage.setItem('chatHistory', JSON.stringify(chatHistory));
}
// Define a function to load chat history
functionloadChatHistory() {
constchatHistory = JSON.parse(localStorage.getItem('chatHistory'));
if (chatHistory) {
constchatMessages = document.getElementById('chat-messages');
chatMessages.innerHTML = ''; // Clear any existing messages
for (constpairofchatHistory) {
addChatBubble(pair.user, 'user');
addChatBubble(pair.bot, 'bot');
}
}
}
// Load chat history on page load
document.addEventListener('DOMContentLoaded', loadChatHistory);
</script>
The chatTocToggle.js file is used to manage the user’s discovery preference globally across the site and is associated with the following partial layouts:
This script defaults to displaying the chatGPT UX experience initially. When a user selects the toggle, the ToC UX is activated and will persist site-wide.
document.addEventListener("DOMContentLoaded", function (event) {
constchatTocToggle= document.getElementById("chatTocToggle");
constchatContainer= document.getElementById("chatContainer");
consttocContainer= document.getElementById("tocContainer");
// Check if chatTocSettings in user's local storage is set; if not set or value is 'chat', toggle hidden on chatContainer; if value is 'toc', toggle the tocContainer
constchatTocSettings=localStorage.getItem("chatTocSettings");
if (chatTocSettings===null||chatTocSettings==="chat") {
chatContainer.classList.remove("hidden");
tocContainer.classList.add("hidden");
} elseif (chatTocSettings==="toc") {
chatContainer.classList.add("hidden");
tocContainer.classList.remove("hidden");
}
// Update the button content based on the visibility of chatContainer
updateButtonContent();
// Add a click event listener to the chatTocToggle button
chatTocToggle.addEventListener("click", function () {
// Toggle both the chatContainer and tocContainer visibility
chatContainer.classList.toggle("hidden");
tocContainer.classList.toggle("hidden");
// Update the preference and button content based on the visibility of chatContainer
if (!chatContainer.classList.contains("hidden")) {
localStorage.setItem("chatTocSettings", "chat");
} else {
localStorage.setItem("chatTocSettings", "toc");
}
// Update the button content after toggling
updateButtonContent();
});
// Function to update the button content based on the visibility of chatContainer
functionupdateButtonContent() {
constisChatVisible=!chatContainer.classList.contains("hidden");
chatTocToggle.innerHTML=isChatVisible?'<img src="/icons/toggle-right.svg" alt="toggle" class="mr-4">':'<img src="/icons/toggle-left.svg" alt="toggle" class="mr-4">';
}
});
Quisque consequat consectetur quam at consequat. Integer efficitur quam id eros auctor, et malesuada justo congue. Fusce non feugiat tellus. Proin a eros in dolor bibendum malesuada ut et elit. Sed sollicitudin, ipsum et fermentum facilisis, urna nisl vehicula neque, in malesuada dolor orci et dolor. Etiam in libero ut turpis cursus tincidunt. Integer in libero non ante laoreet vestibulum in sed orci. Vivamus luctus bibendum velit, at egestas justo fermentum in.
The darkmode.js file is used to manage the user’s theme preference and is associated with the navigation/top.htmlpartial layout.
Quisque consequat consectetur quam at consequat. Integer efficitur quam id eros auctor, et malesuada justo congue. Fusce non feugiat tellus. Proin a eros in dolor bibendum malesuada ut et elit. Sed sollicitudin, ipsum et fermentum facilisis, urna nisl vehicula neque, in malesuada dolor orci et dolor. Etiam in libero ut turpis cursus tincidunt. Integer in libero non ante laoreet vestibulum in sed orci. Vivamus luctus bibendum velit, at egestas justo fermentum in.
Quisque consequat consectetur quam at consequat. Integer efficitur quam id eros auctor, et malesuada justo congue. Fusce non feugiat tellus. Proin a eros in dolor bibendum malesuada ut et elit. Sed sollicitudin, ipsum et fermentum facilisis, urna nisl vehicula neque, in malesuada dolor orci et dolor. Etiam in libero ut turpis cursus tincidunt. Integer in libero non ante laoreet vestibulum in sed orci. Vivamus luctus bibendum velit, at egestas justo fermentum in.
It’s common to want to build out a glossary of terms that are either unique to your product or essential to understanding the larger industry it lives in.
Creating a glossary is an essential part of your strategy for bolstering organic inbound traffic through authoritative SEO content.
It’s common to want to build out a glossary of terms that are either unique to your product or essential to understanding the larger industry it lives in.
Creating a glossary is an essential part of your strategy for bolstering organic inbound traffic through authoritative SEO content.
The glossary.js file is used to manage the visual “spotlight effect” experience on the glossary entries found in the glossary.htmlpartial layout.
This functionality is nearly identical to the js/tiles.js functionality – with the exception of the utilized class name, glossary-entry. This is so that you can more easily customize each experience on your own if you wish.
If the page has elements with the glossary-entry class, this script updates their background radial gradient values to mirror the position of the mouse.
The color of the spotlight effect is determined by css/colors.css, specifically:
Run pnpm run watch-tw from siteThemeName directory to enable watching for design changes.
You are ready to start creating and editing your theme.
Inline code samples are great — but code samples that are pulled from source files can be even better! This {{%include%}} shortcode is inspired by Sphinx’s .. literalinclude:: functionality.
The {{%include%}} shortcode accepts 3 positional args: lang, start, and stop. All are optional.
Don't forget to use %
This shortcode relies on Hugo’s markdown rendering to automatically handle code syntax highlighting. If you surround this shortcode with <> instead, it will break.
---
title: include.html
description: learn how to use the literal shortcode
---
<!--start -->Inline code samples are great --- but code samples that are pulled from source files can be even better! This `{{%/*include*/%}}` shortcode is inspired by Sphinx's `.. literalinclude::` functionality.
{{<noticesnack>}}
This document is going to be a bit meta.
{{</notice>}}
## How it Works
The `{{%/*include*/%}}` shortcode accepts 3 **positional** args: `lang`, `start`, and `stop`. All are optional.
{{<noticewarning"Don'tforgettouse%">}}
This shortcode relies on Hugo's markdown rendering to automatically handle code syntax highlighting. If you surround this shortcode with `<``>` instead, it will break.
{{</notice>}}
### Examples
### This File
{{%include "reference/layouts/shortcodes/include.md" "md" %}}
### Python File With Comments
{{%include "static/demo-package.py" "python" "# Start 1" "# End 1" %}}
## Source Code
{{%include "layouts/shortcodes/include.html" "go" %}}
defdemo_function(arg1, arg2):
"""Demo Function
This function takes two arguments and returns their sum.
Args:
arg1 (int): The first argument.
arg2 (int): The second argument.
Returns:
int: The sum of arg1 and arg2.
"""return arg1 + arg2
{{< notice"""withouttype" >}}
This is a **default** notice.
{{< /notice >}}
{{< noticesnack"wantacookie?" >}}
This is a **snack** notice.
{{< /notice >}}
{{< noticetip"youdon'thavetoaddatitle">}}
This is a **tip** notice.
{{< /notice >}}
{{< noticenote"there'salotofoptions" >}}
This is a **note** notice.
{{< /notice >}}
{{< noticeinfo"probablyredundantwithnote" >}}
This is a **info** notice.
{{< /notice >}}
{{< noticesecurity"hugoissafe" >}}
This is a **security** notice.
{{< /notice >}}
{{< noticewarning"don'tuselightmodeatnight" >}}
This is a **warning** notice.
{{< /notice >}}
{{< noticedanger"catsmaydestroyfurniture" >}}
This is a **danger** notice.
{{< /notice >}}
{{ $type:= .Get0 | default"info" }}
{{ $title:= .Get1 }}
<divclass="notice p-4 mt-2 mb-2 rounded-md shadow-md
{{- with $type }}
{{if eq . "danger" }} bg-brand-7
{{else if eq . "warning" }} bg-brand-6
{{else if eq . "security" }} bg-brand-5
{{else if eq . "tip" }} bg-brand-4
{{else if eq . "note" }} bg-brand-3
{{else if eq . "info" }} bg-brand-2
{{else if eq . "snack" }} bg-brand-1
{{ end }}
{{- end }}
">
<divclass="flex">
<divclass="flex-shrink-0">
<imgsrc="/icons/light/
{{- with $type}}
{{- if eq . "danger" -}} danger
{{- else if eq . "warning" -}} warning
{{- else if eq . "security" -}} security
{{- else if eq . "info" -}} info
{{- else if eq . "note" -}} note
{{- else if eq . "tip" -}} tip
{{- else if eq . "snack" -}} snack
{{- end -}}
{{- end -}}
.svg"class="icon w-5 h-5">
</div>
<divclass="ml-3 w-full text-black text-sm">
{{-if$title-}}
<divclass="font-brand font-bold mb-2">{{ $title }}</div>
{{-end-}}
{{ $.Page.RenderString .Inner }}
</div>
</div>
</div>
It’s common for developers to build packages in Python that need auto-generated code documentation based off of Python strings.
To better support integrating that collection to your larger docs site, I’ve built out a {{<pdoc>}} shortcode that enables you to automatically target links for:
Supermodules
Submodules
Functions
Classes
Methods
How complex is the package?
If you are integrating pdoc documentation for a package that has submodules, use the default pdoc shortcode. For simple packages without submodules, use pdoc-2.
The {{<pdoc>}} shortcode accepts 3 positional args: type, target, and linktitle (optional). If linktitle is not set, it automatically configures the link text as show in the following sections.
You can change the default directory where this shortcode looks for pdoc collections by updating the value of $baseurl. Alternatively, you could make this shortcode more advanced and remove that static baseurl piece altogether.
Occasionally it is useful to use a variable that represents your product name — especially when documenting a project or startup product that is likely to undergo rebranding.
To ensure Hugo resolves this shortcode correctly in the Table of Contents of your articles, make sure that you use the % wrapper instead of < > in your headers.
This script automatically toggles the view of a regular page versus the search page when a user inputs a search string. With every added letter, a new search is performed against the index.
Results returned are grouped by parent article and then provided as a stacked series of links.
free search limit
Algolia typically allows 10,000 free monthly searches — though this is subject to change.
Provide your App ID and Search Only API Key to the searchClient (these are safe to reveal; the Admin API Key is not.).
Push or upload your site’s index, found at /index.json.
That’s it! Start searching.
I personally just download this file and upload it per release; it’s a manual process — but super easy. You are welcome to integrate the Algolia API with your Admin API Key to push auto updates.
have it your way
There are several ways to implement Algolia; DocSearch is popular and free. I personally like to integrate the style of the UX more, but this can require more knowledge of InstantSearch.js.
If you like the default implementation but wish to style the search hits differently, you can do so in the performAlgoliaSearch(query) function.
document.addEventListener("DOMContentLoaded", function () {
varactiveLink= document.querySelector('[data-current="true"]');
// Function to scroll to the link with a delay
functionscrollToLink(link) {
if (link) {
setTimeout(function () {
link.scrollIntoView({
behavior:"smooth",
block:"nearest",
inline:"start",
});
}, 300);
}
}
scrollToLink(activeLink);
varlinks= document.querySelectorAll("a[data-level]");
varcurrentUrl= window.location.href;
// Function to handle the visibility of nested lists
functionhandleVisibility() {
// Initially hide all levels greater than 2
links.forEach(function (link) {
varlevel= parseInt(link.getAttribute("data-level"));
if (level>1) {
link.closest("li").classList.add("hidden");
}
});
links.forEach(function (link) {
if (link.href===currentUrl) {
link.classList.add("text-brand");
// Unhide all ancestor li elements
varancestor=link.closest("li");
while (ancestor) {
if (ancestor.tagName.toLowerCase() ==="li") {
ancestor.classList.remove("hidden");
}
ancestor=ancestor.parentElement;
}
// Unhide direct siblings at the same level
varparentLi=link.closest("li");
varsiblingLis= Array.from(parentLi.parentElement.children).filter(
function (child) {
returnchild!==parentLi;
}
);
siblingLis.forEach(function (siblingLi) {
siblingLi.classList.remove("hidden");
});
}
});
}
// Call the handleVisibility function
handleVisibility();
});
Tabs are a great way to organize content that is contextually relevant but divergent in format or procedure (without necessarily needing its own page). This combination of shortcodes allows you to create a tabbed interface. I first encountered this implementation strategy while reading the MiniKube docs.
If a page has elements with [data-component="tabs"], the script collects them all into an array.
For each collection of tabs, it then collects the button options ([data-tab-id])and corresponding tabbed markdown content ([data-tabcontent]).
Event listeners are setup for each button; when selected, the corresponding content is revealed and the button highlighted; other options are hidden/muted.
document.addEventListener("DOMContentLoaded", function(event) {
if (document.querySelectorAll('[data-component="tabs"]')) {
vartabs= document.querySelectorAll('[data-component="tabs"]');
console.log("tabs ", tabs)
tabs.forEach(function(tab) {
// get each child step element with the data-wizard-id attribute
letoptions=tab.querySelectorAll('[data-tab-id]');
letoptionContent=tab.querySelectorAll('[data-tabcontent]'); // Changed to lowercase 'c'
getAnswers({tab, optionContent})
// listen for click on button elements within each step
options.forEach((option) => {
// get all of the buttons within the step
letbuttons=option.querySelectorAll('button')
// listen for click on each button
buttons.forEach((button) => {
button.addEventListener('click', (e) => {
// add green class to the clicked button
e.target.classList.remove('bg-white', 'text-black')
e.target.classList.add('bg-brand', 'text-white')
// remove green class from the other buttons
buttons.forEach((button) => {
if (button!==e.target) {
button.classList.remove('bg-brand', 'text-white')
button.classList.add('bg-white', 'text-black')
}
})
getAnswers({tab, optionContent})
})
})
})
});
functiongetAnswers({tab, optionContent}){
// get all buttons with the green class
letactiveButtons=tab.querySelectorAll('button.bg-brand')
// get the data-tab-option attribute of all the buttons with the green class
letactiveButtonOptions= []
console.log("activeButtonOptions ", activeButtonOptions)
activeButtons.forEach((button) => {
console.log("activeButton ", button)
activeButtonOptions.push(button.getAttribute('data-tab-option')) // Changed to data-tab-option
})
console.log("activeButtonOptions ", activeButtonOptions)
letconvertedText=activeButtonOptions.join('/').toLowerCase()
// hide all other answers
optionContent.forEach((content) => {
letvalue=content.getAttribute('data-tabcontent')
console.log(`value: `+value)
console.log(`convertedText: `+convertedText)
if (value!==convertedText) {
content.classList.add('hidden')
} else {
content.classList.remove('hidden')
}
})
}
}
});
The toc.js file is used to manage the “scrolling-section-highlight effect” experience on the table of contents found in the navigation/sidebar-right.htmlpartial layout.
document.addEventListener("DOMContentLoaded", () => {
// Get all TOC links
consttocLinks= document.querySelectorAll("#TableOfContents a");
// Function to highlight the currently viewed section
functionhighlightInView() {
constsections= document.querySelectorAll("h2, h3"); // You may need to adjust the selector based on your HTML structure
// Define the top offset (50px in this example)
constoffset=50;
// Loop through the sections to find the currently viewed one
sections.forEach((section) => {
constrect=section.getBoundingClientRect();
// Adjust the condition with the offset
if (
rect.top+offset>=0&&rect.bottom<= window.innerHeight+offset ) {
// Section is in view
constsectionId=section.id;
tocLinks.forEach((link) => {
if (link.getAttribute("href").substring(1) ===sectionId) {
// Remove the highlight class from all TOC links
tocLinks.forEach((tocLink) => {
tocLink.classList.remove("text-brand");
});
// Add the highlight class to the currently viewed TOC link
link.classList.add("text-brand");
}
});
}
});
}
// Attach the scroll event listener to the window
window.addEventListener("scroll", highlightInView);
});
If the project you are documenting must be installed, it is likely that your documentation needs to be versioned. In this scenario, it’s especially useful to have a shortcode that can also version your download links, github links, announcements, and similar assets without having to manually update them across all of your articles.
The default functionality for this shortcode uses the version numbers cascading from the root of the versioned directory (e.g., content/latest, content/1.0.2).
In cases where you want to mention or link to the latest version in older versions of your content, you can add {{<version "global">}}. This uses the site-wide parameter to determine what version number to use.