diff --git a/docusaurus.config.js b/docusaurus.config.js
index 84c9add7..e2cdf19a 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -121,6 +121,7 @@ const config = {
{ to: '/organisations', label: 'Adopters', position: 'left' },
{ to: '/contribute', label: 'Contribute', position: 'left' },
{ to: '/resources', label: 'Resources', position: 'left' },
+ { to: '/docs/latest/Reference/LTS', label: 'Support', position: 'left' },
{
type: 'docsVersionDropdown',
position: 'right',
diff --git a/src/theme/DocPage/Layout/Main/index.js b/src/theme/DocPage/Layout/Main/index.js
new file mode 100644
index 00000000..65704fd0
--- /dev/null
+++ b/src/theme/DocPage/Layout/Main/index.js
@@ -0,0 +1,23 @@
+import React from 'react'
+import clsx from 'clsx'
+import { useDocsSidebar } from '@docusaurus/theme-common/internal'
+import styles from './styles.module.css'
+export default function DocPageLayoutMain({ hiddenSidebarContainer, children }) {
+ const sidebar = useDocsSidebar()
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/src/theme/DocPage/Layout/Main/styles.module.css b/src/theme/DocPage/Layout/Main/styles.module.css
new file mode 100644
index 00000000..b0319384
--- /dev/null
+++ b/src/theme/DocPage/Layout/Main/styles.module.css
@@ -0,0 +1,19 @@
+.docMainContainer {
+ display: flex;
+ width: 100%;
+}
+
+@media (min-width: 997px) {
+ .docMainContainer {
+ flex-grow: 1;
+ max-width: calc(100% - var(--doc-sidebar-width));
+ }
+
+ .docMainContainerEnhanced {
+ max-width: calc(100% - var(--doc-sidebar-hidden-width));
+ }
+
+ .docItemWrapperEnhanced {
+ max-width: calc(var(--ifm-container-width) + var(--doc-sidebar-width)) !important;
+ }
+}
diff --git a/src/theme/DocPage/Layout/Sidebar/ExpandButton/index.js b/src/theme/DocPage/Layout/Sidebar/ExpandButton/index.js
new file mode 100644
index 00000000..932fb431
--- /dev/null
+++ b/src/theme/DocPage/Layout/Sidebar/ExpandButton/index.js
@@ -0,0 +1,26 @@
+import React from 'react'
+import { translate } from '@docusaurus/Translate'
+import IconArrow from '@theme/Icon/Arrow'
+import styles from './styles.module.css'
+export default function DocPageLayoutSidebarExpandButton({ toggleSidebar }) {
+ return (
+
+
+
+ )
+}
diff --git a/src/theme/DocPage/Layout/Sidebar/ExpandButton/styles.module.css b/src/theme/DocPage/Layout/Sidebar/ExpandButton/styles.module.css
new file mode 100644
index 00000000..f4cd944d
--- /dev/null
+++ b/src/theme/DocPage/Layout/Sidebar/ExpandButton/styles.module.css
@@ -0,0 +1,27 @@
+@media (min-width: 997px) {
+ .expandButton {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background-color var(--ifm-transition-fast) ease;
+ background-color: var(--docusaurus-collapse-button-bg);
+ }
+
+ .expandButton:hover,
+ .expandButton:focus {
+ background-color: var(--docusaurus-collapse-button-bg-hover);
+ }
+
+ .expandButtonIcon {
+ transform: rotate(0);
+ }
+
+ [dir='rtl'] .expandButtonIcon {
+ transform: rotate(180deg);
+ }
+}
diff --git a/src/theme/DocPage/Layout/Sidebar/index.js b/src/theme/DocPage/Layout/Sidebar/index.js
new file mode 100644
index 00000000..53f3e8b7
--- /dev/null
+++ b/src/theme/DocPage/Layout/Sidebar/index.js
@@ -0,0 +1,53 @@
+import React, { useState, useCallback } from 'react'
+import clsx from 'clsx'
+import { prefersReducedMotion, ThemeClassNames } from '@docusaurus/theme-common'
+import { useDocsSidebar } from '@docusaurus/theme-common/internal'
+import { useLocation } from '@docusaurus/router'
+import DocSidebar from '@theme/DocSidebar'
+import ExpandButton from '@theme/DocPage/Layout/Sidebar/ExpandButton'
+import styles from './styles.module.css'
+// Reset sidebar state when sidebar changes
+// Use React key to unmount/remount the children
+// See https://github.com/facebook/docusaurus/issues/3414
+function ResetOnSidebarChange({ children }) {
+ const sidebar = useDocsSidebar()
+ return {children}
+}
+export default function DocPageLayoutSidebar({ sidebar, hiddenSidebarContainer, setHiddenSidebarContainer }) {
+ const { pathname } = useLocation()
+ const [hiddenSidebar, setHiddenSidebar] = useState(false)
+ const toggleSidebar = useCallback(() => {
+ if (hiddenSidebar) {
+ setHiddenSidebar(false)
+ }
+ // onTransitionEnd won't fire when sidebar animation is disabled
+ // fixes https://github.com/facebook/docusaurus/issues/8918
+ if (!hiddenSidebar && prefersReducedMotion()) {
+ setHiddenSidebar(true)
+ }
+ setHiddenSidebarContainer((value) => !value)
+ }, [setHiddenSidebarContainer, hiddenSidebar])
+ return (
+
+ )
+}
diff --git a/src/theme/DocPage/Layout/Sidebar/styles.module.css b/src/theme/DocPage/Layout/Sidebar/styles.module.css
new file mode 100644
index 00000000..221aabf5
--- /dev/null
+++ b/src/theme/DocPage/Layout/Sidebar/styles.module.css
@@ -0,0 +1,32 @@
+:root {
+ --doc-sidebar-width: 300px;
+ --doc-sidebar-hidden-width: 30px;
+}
+
+.docSidebarContainer {
+ display: none;
+}
+
+@media (min-width: 997px) {
+ .docSidebarContainer {
+ display: block;
+ width: var(--doc-sidebar-width);
+ margin-top: calc(-1 * var(--ifm-navbar-height));
+ border-right: 1px solid var(--ifm-toc-border-color);
+ will-change: width;
+ transition: width var(--ifm-transition-fast) ease;
+ clip-path: inset(0);
+ }
+
+ .docSidebarContainerHidden {
+ width: var(--doc-sidebar-hidden-width);
+ cursor: pointer;
+ }
+
+ .sidebarViewport {
+ top: 0;
+ position: sticky;
+ height: 100%;
+ max-height: 100vh;
+ }
+}
diff --git a/src/theme/DocPage/Layout/index.js b/src/theme/DocPage/Layout/index.js
new file mode 100644
index 00000000..09be5603
--- /dev/null
+++ b/src/theme/DocPage/Layout/index.js
@@ -0,0 +1,26 @@
+import React, { useState } from 'react'
+import { useDocsSidebar } from '@docusaurus/theme-common/internal'
+import Layout from '@theme/Layout'
+import BackToTopButton from '@theme/BackToTopButton'
+import DocPageLayoutSidebar from '@theme/DocPage/Layout/Sidebar'
+import DocPageLayoutMain from '@theme/DocPage/Layout/Main'
+import styles from './styles.module.css'
+export default function DocPageLayout({ children }) {
+ const sidebar = useDocsSidebar()
+ const [hiddenSidebarContainer, setHiddenSidebarContainer] = useState(false)
+ return (
+
+
+
+ {sidebar && (
+
+ )}
+ {children}
+
+
+ )
+}
diff --git a/src/theme/DocPage/Layout/styles.module.css b/src/theme/DocPage/Layout/styles.module.css
new file mode 100644
index 00000000..008456fe
--- /dev/null
+++ b/src/theme/DocPage/Layout/styles.module.css
@@ -0,0 +1,18 @@
+.docPage {
+ display: flex;
+ width: 100%;
+ flex: 1 0;
+}
+
+.docsWrapper {
+ display: flex;
+ flex: 1 0 auto;
+}
+
+/*
+JS disabled??? Show light version by default => better than showing nothing
+TODO bad, but we currently always show light mode when there's no data-theme
+ */
+html:not([data-theme]) .themedComponent--light {
+ display: initial;
+}
diff --git a/src/theme/DocPage/index.js b/src/theme/DocPage/index.js
new file mode 100644
index 00000000..f79b5457
--- /dev/null
+++ b/src/theme/DocPage/index.js
@@ -0,0 +1,78 @@
+import React from 'react'
+import clsx from 'clsx'
+import { HtmlClassNameProvider, ThemeClassNames, PageMetadata } from '@docusaurus/theme-common'
+import {
+ docVersionSearchTag,
+ DocsSidebarProvider,
+ DocsVersionProvider,
+ useDocRouteMetadata,
+} from '@docusaurus/theme-common/internal'
+import DocPageLayout from '@theme/DocPage/Layout'
+import NotFound from '@theme/NotFound'
+import SearchMetadata from '@theme/SearchMetadata'
+import { useLocation } from '@docusaurus/router'
+
+function DocPageMetadata(props) {
+ const { versionMetadata } = props
+ return (
+ <>
+
+ {versionMetadata.noIndex && }
+ >
+ )
+}
+export default function DocPage(props) {
+ const { versionMetadata } = props
+ const currentDocRouteMetadata = useDocRouteMetadata(props)
+ const location = useLocation()
+
+ if (!currentDocRouteMetadata) {
+ return
+ }
+ const { docElement, sidebarName, sidebarItems } = currentDocRouteMetadata
+
+ // Show this warning only on migration guide pages for the latest version.
+ const isMigrationGuide = location.pathname.toLowerCase().includes('migration-guide')
+ const versionNumber = parseVersion(versionMetadata.version)
+
+ return (
+ <>
+
+
+
+
+
+ {/* NaN indicates the latest version */}
+ {isMigrationGuide && (isNaN(versionNumber) || versionNumber === 5) && (
+
+
Version 3 and before of Fastify are no longer maintained.
+ For information about support options for end-of-life versions, see the{' '}
+
Long Term Support page.
+
+ )}
+ {docElement}
+
+
+
+
+ >
+ )
+}
+
+const parseVersion = (versionString) => {
+ // Remove 'v' prefix if present
+ const cleanVersion = versionString.startsWith('v') ? versionString.slice(1) : versionString
+ // Split the version string and get the first part (major version)
+ const majorVersion = cleanVersion.split('.')[0]
+ // Parse the major version to an integer
+ return parseInt(majorVersion, 10)
+}
diff --git a/src/theme/DocVersionBanner/index.js b/src/theme/DocVersionBanner/index.js
new file mode 100644
index 00000000..f0fc58af
--- /dev/null
+++ b/src/theme/DocVersionBanner/index.js
@@ -0,0 +1,126 @@
+import React from 'react'
+import clsx from 'clsx'
+import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
+import Link from '@docusaurus/Link'
+import Translate from '@docusaurus/Translate'
+import { useActivePlugin, useDocVersionSuggestions } from '@docusaurus/plugin-content-docs/client'
+import { ThemeClassNames } from '@docusaurus/theme-common'
+import { useDocsPreferredVersion, useDocsVersion } from '@docusaurus/theme-common/internal'
+function UnreleasedVersionLabel({ siteTitle, versionMetadata }) {
+ return (
+ {versionMetadata.label},
+ }}>
+ {'This is unreleased documentation for {siteTitle} {versionLabel} version.'}
+
+ )
+}
+function UnmaintainedVersionLabel({ siteTitle, versionMetadata }) {
+ const versionNumber = parseInt(versionMetadata.label.replace(/^v/, '').split('.')[0], 10)
+
+ // Return the label only if the version number is 3 or lower.
+ if (versionNumber <= 3) {
+ return (
+ {versionMetadata.label},
+ }}>
+ {'This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.'}
+
+ )
+ }
+ return null
+}
+const BannerLabelComponents = {
+ unreleased: UnreleasedVersionLabel,
+ unmaintained: UnmaintainedVersionLabel,
+}
+function BannerLabel(props) {
+ const BannerLabelComponent = BannerLabelComponents[props.versionMetadata.banner]
+ return
+}
+function LatestVersionSuggestionLabel({ versionLabel, to, onClick }) {
+ return (
+
+
+
+ latest version
+
+
+
+ ),
+ }}>
+ {'For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).'}
+
+ )
+}
+function DocVersionBannerEnabled({ className, versionMetadata }) {
+ const {
+ siteConfig: { title: siteTitle },
+ } = useDocusaurusContext()
+ const { pluginId } = useActivePlugin({ failfast: true })
+ const getVersionMainDoc = (version) => version.docs.find((doc) => doc.id === version.mainDocId)
+ const { savePreferredVersionName } = useDocsPreferredVersion(pluginId)
+ const { latestDocSuggestion, latestVersionSuggestion } = useDocVersionSuggestions(pluginId)
+
+ // Parse the version number
+ const versionNumber = parseVersion(versionMetadata.version)
+
+ // Try to link to same doc in latest version (not always possible), falling
+ // back to main doc of latest version
+ const latestVersionSuggestedDoc = latestDocSuggestion ?? getVersionMainDoc(latestVersionSuggestion)
+
+ return (
+
+
+
+
+
+ savePreferredVersionName(latestVersionSuggestion.name)}
+ />
+
+ {/* When a new version is released, increment the value below. */}
+ {versionNumber < 4 && (
+
+ For information about support options for end-of-life versions, see the{' '}
+
Long Term Support page.
+
+ )}
+
+ )
+}
+export default function DocVersionBanner({ className }) {
+ const versionMetadata = useDocsVersion()
+ if (versionMetadata.banner) {
+ return
+ }
+ return null
+}
+const parseVersion = (versionString) => {
+ // Remove 'v' prefix if present
+ const cleanVersion = versionString.startsWith('v') ? versionString.slice(1) : versionString
+ // Split the version string and get the first part (major version)
+ const majorVersion = cleanVersion.split('.')[0]
+ // Parse the major version to an integer
+ return parseInt(majorVersion, 10)
+}