diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000000..0dc4c3f88e
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,97 @@
+# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
+name: Deploy Website and Docs
+
+on:
+  pull_request:
+  push:
+    branches:
+      - master
+
+  # Allows you to run this workflow manually from the Actions tab
+  workflow_dispatch:
+
+jobs:
+  build_website:
+    runs-on: ubuntu-latest
+    steps:
+      # Build website from gazebosim-web-frontend
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          repository: gazebo-web/gazebosim-web-frontend
+          ref: main
+      - name: Setup Node
+        uses: actions/setup-node@v4
+        with:
+          node-version: "20"
+          cache: npm
+          cache-dependency-path: package-lock.json
+      - name: Setup Pages
+        id: pages
+        uses: actions/configure-pages@v5
+      - name: Install Website dependencies
+        run: npm ci
+      - name: Build Website
+        run: npm run build -- --base-href "${{ steps.pages.outputs.base_url }}/"
+      # Upload the artifact for local preview
+      - name: Upload artifact
+        uses: actions/upload-artifact@v4
+        with:
+          name: website
+          path: dist
+
+      # Build Docs
+  build_docs:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Setup Pages
+        id: pages
+        uses: actions/configure-pages@v5
+      - name: Setup Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: '3.12'
+          cache: 'pip'
+      - name: Install Docs dependencies
+        run: pip install -r requirements.txt
+      - name: Build Docs
+        run: python build_multiversion.py --pointers --libs --output_dir .build
+        env:
+          GZ_DEPLOY_URL: "${{ steps.pages.outputs.base_url }}"
+
+      # Upload the artifact for local preview
+      - name: Upload artifact
+        uses: actions/upload-artifact@v4
+        with:
+          name: docs
+          path: .build
+
+  deploy:
+    runs-on: ubuntu-latest
+    needs: [build_website, build_docs]
+    permissions:
+      contents: write
+    # Allow only one concurrent deployment between this and the nightly-upload workflow.
+    concurrency:
+      group: pages
+      cancel-in-progress: false
+    steps:
+      - uses: actions/download-artifact@v4
+        with:
+          merge-multiple: true
+      - name: Upload merged
+        uses: actions/upload-artifact@v4
+        with:
+          name: website-docs-merged
+          path: ./
+      - name: Commit
+        uses: peaceiris/actions-gh-pages@v4
+        # The workflow upto this point is good for generating a preview,
+        # but only commit to deploy if we are on the master branch (not a pull request).
+        if: github.ref == 'refs/heads/master'
+        with:
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          publish_dir: ./
+          keep_files: true
diff --git a/.github/workflows/nightly-upload.yml b/.github/workflows/nightly-upload.yml
index cd80dfcfa5..0f9e92cdca 100644
--- a/.github/workflows/nightly-upload.yml
+++ b/.github/workflows/nightly-upload.yml
@@ -58,17 +58,14 @@ jobs:
     needs: build
     runs-on: ubuntu-latest
     permissions:
-      id-token: write
-      contents: read
+      contents: write
+    # Allow only one concurrent deployment between this and the deploy workflow.
+    concurrency:
+      group: pages
+      cancel-in-progress: false
     steps:
     - name: Checkout
       uses: actions/checkout@v4
-    - name: Configure AWS Credentials
-      id: creds
-      uses: aws-actions/configure-aws-credentials@v4
-      with:
-        aws-region: us-east-1
-        role-to-assume: arn:aws:iam::200670743174:role/github-oidc-deployment-gz-web-app
     - uses: actions/download-artifact@v4
       id: download
       with:
@@ -81,8 +78,10 @@ jobs:
       with:
         name: api-docs
         path: .api-out/*
-    - name: Run nightly upload
-      run: aws s3 sync .api-out/ s3://gazebosim.org/api/
-    - name: Invalidate Cloudfront distribution
-      run: |
-        aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths '/*' --region us-east-1
+    - name: Commit
+      uses: peaceiris/actions-gh-pages@v4
+      with:
+        github_token: ${{ secrets.GITHUB_TOKEN }}
+        publish_dir: ./.api-out
+        destination_dir: api
+        keep_files: true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..7afd5d306b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.build
+.tmp
+.venv
diff --git a/README.md b/README.md
index 0761587011..731656c61b 100644
--- a/README.md
+++ b/README.md
@@ -15,9 +15,45 @@ found under the `API Reference` section of [https://gazebosim.org/docs](https://
 
 ## Main docs
 
-The documentation in this repository is updated whenever the
-[gazebosim-web-backend](https://github.com/gazebo-web/gazebosim-web-backend),
-is deployed. The gazebosim-web-backend webserver maintains a clone of this repository, and serves the markdown pages to https://gazebosim.org/docs.
+The documentation in this repository is built using [Sphinx](https://www.sphinx-doc.org/).
+To build, you need to install the following:
+
+* python virtualenv
+
+Create the virtual env and activate it:
+
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+```
+
+Then install the necessary dependencies:
+
+```bash
+pip install -r requirements.txt
+```
+
+```bash
+python3 build_multiversion.py
+```
+
+This will build all the documentation for all versions of Gazebo.
+You can preview the result locally by running an HTTP server on
+the output directory `.build`. For example:
+
+```bash
+python3 -m http.server 8000 -d .build
+
+```
+
+This will serve the website on <http://localhost:8000>
+
+For quicker iteration, you can build the documentation for a subset
+of Gazebo versions. To build `garden` and `harmonic`:
+
+```bash
+python3 build_multiversion.py --release garden harmonic
+```
 
 ## Library docs
 
diff --git a/_static/css/gazebo.css b/_static/css/gazebo.css
new file mode 100644
index 0000000000..aa9cd46bcd
--- /dev/null
+++ b/_static/css/gazebo.css
@@ -0,0 +1,149 @@
+html {
+  --pst-font-family-base: Roboto, var(--pst-font-family-base-system);
+  --pst-font-size-base: 14px;
+  --pst-header-height: 64px;
+  --gz-doc-header-height: 120px;
+  scroll-padding-top: calc(var(--pst-header-height) + var(--gz-doc-header-height) + 1rem);
+}
+
+html[data-theme="light"] {
+  --gz-color-doc-header: #4fc3f7;
+  --gz-color-doc-header-text: #fff;
+  --pst-color-primary: #0277bd;
+  --gz-color-primary-sidebar: #f8f9fa;
+}
+
+html[data-theme="dark"] {
+  --gz-color-doc-header: rgb(15 23 36 / 30%);
+  --gz-color-doc-header-text: #fff;
+  --pst-color-primary: #0277bd;
+  --gz-color-primary-sidebar: #1a1c1e;
+  --pst-color-background: #131416;
+}
+
+a {
+  text-decoration: none;
+}
+
+pre {
+  border: none;
+}
+.bd-main .bd-content .bd-article-container {
+  max-width: 160em;
+}
+
+.bd-page-width {
+  max-width: 100%;
+}
+
+.bd-sidebar {
+  max-width: 20em;
+}
+
+.sidebar-primary-items__end {
+  display: none;
+}
+
+.bd-links__title {
+  display: none;
+}
+
+.navbar-nav .nav-item {
+  letter-spacing: normal;
+  text-transform: uppercase;
+  font-size: 16px;
+  text-decoration: none;
+  outline: 0;
+  transition: 0.5s;
+  font-weight: 400;
+  color: #6e6e6e;
+  border-bottom: 1px solid rgba(0, 0, 0, 0);
+}
+.navbar-nav li a {
+  margin-right: 20px;
+}
+
+.bd-header .navbar-header-items {
+  padding-left: 5em;
+}
+
+.nav-link.nav-external:after {
+  display: none;
+}
+
+.doc-header {
+  width: 100%;
+  background-color: var(--gz-color-doc-header);
+  color: var(--gz-color-doc-header-text);
+  height: var(--gz-doc-header-height);
+}
+
+.banner {
+  align-items: center;
+  padding: 20px 40px;
+  place-content: center space-between;
+  height: 100%;
+}
+
+header.navbar {
+  display: block;
+}
+
+.bd-sidebar-primary {
+  max-height: calc(100vh - var(--pst-header-height) + var(--gz-doc-header-height));
+  top: calc(var(--pst-header-height) + var(--gz-doc-header-height));
+  background-color: var(--gz-color-primary-sidebar);
+}
+
+.bd-sidebar-secondary {
+  max-height: calc(100vh - var(--pst-header-height) + var(--gz-doc-header-height));
+  top: calc(var(--pst-header-height) + var(--gz-doc-header-height));
+}
+
+.gz-version-switcher {
+  display: flex;
+}
+button.btn.version-switcher__button {
+  color: var(--gz-color-doc-header-text);
+  border-color: var(--gz-color-doc-header-text);
+}
+.bd-header-version-info {
+    background-color: var(--pst-color-info-bg);
+}
+
+.navbar-brand img {
+  height: 84px;
+}
+
+.warning {
+  border-left: 3px solid rgb(228, 167, 2);
+  padding: 15px;
+  margin: 15px 0;
+  color: #8a6c40;
+  background: rgb(252, 248, 228);
+}
+
+/* Style for /libs */
+.gz-libs-lists {
+  display: inline;
+  padding: 0;
+}
+
+.gz-libs-lists p {
+  display: inline;
+}
+
+.gz-libs-lists li, .gz-libs-cards li {
+  list-style-type: none;
+  display: inline;
+  padding-left: 0px;
+  padding-right: 30px;
+}
+
+.gz-libs-cards ul {
+  padding-left: 1em;
+}
+
+.gz-libs-cards .sd-card-text {
+  display: inline;
+}
diff --git a/_static/icon/favicon.ico b/_static/icon/favicon.ico
new file mode 100644
index 0000000000..3979b1cf68
Binary files /dev/null and b/_static/icon/favicon.ico differ
diff --git a/_static/images/logos/gazebo_horz_neg.svg b/_static/images/logos/gazebo_horz_neg.svg
new file mode 100644
index 0000000000..1b0aaab8d8
--- /dev/null
+++ b/_static/images/logos/gazebo_horz_neg.svg
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   width="979.84998"
+   height="478.5"
+   id="svg2"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="gazebo_horz_neg.svg">
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1664"
+     inkscape:window-height="920"
+     id="namedview3462"
+     showgrid="false"
+     inkscape:zoom="0.74470588"
+     inkscape:cx="525.78645"
+     inkscape:cy="15.753716"
+     inkscape:window-x="299"
+     inkscape:window-y="126"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg2" />
+  <defs
+     id="defs4">
+    <clipPath
+       id="clipPath3020">
+      <path
+         d="m 0,0 5286.97,0 0,1276 L 0,1276 0,0 z"
+         id="path3022"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+  </defs>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <path
+     style="fill:#000000;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
+     d="m 163.48736,206.58057 1.00711,67.97986 60.76224,37.26303 0.50356,-34.07386 33.2346,-19.80647 z"
+     id="path3924"
+     inkscape:connector-curvature="0" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+     id="path3024"
+     d="m 222.38486,244.55712 2.02975,1.25975 c 0.72813,0.45075 1.55275,0.67725 2.38037,0.67725 0.82576,0 1.65338,-0.2265 2.38338,-0.68062 l 2.03225,-1.26763 20.60537,12.76275 -27.51212,17.13138 c -1.32725,0.82362 -2.13288,2.27775 -2.1265,3.84512 l 0.0805,28.07713 -49.7045,-30.86425 49.8315,-30.94088 z m -54.03223,-33.55225 45.47998,28.24225 -45.47998,28.23876 0,-56.48101 z m 125.63548,28.15038 c -0.004,-1.63275 -0.8925,-3.14163 -2.32625,-3.93313 -1.43,-0.79437 -3.17875,-0.7465 -4.5675,0.11775 l -26.73337,16.64738 -20.60313,-12.75875 52.209,-32.51563 c 1.325,-0.82425 2.13125,-2.27387 2.13125,-3.83637 -0.004,-1.55763 -0.8125,-3.00838 -2.1375,-3.82813 l -62.79113,-38.88212 c -1.45749,-0.90375 -3.29824,-0.90125 -4.75487,0.001 l -62.79292,38.99413 c -1.3252,0.82024 -2.13233,2.27049 -2.13233,3.8315 0,0.40574 0,72.09862 0,72.50487 0,1.56062 0.80713,3.01025 2.13233,3.8345 l 62.79292,38.98977 c 0.0307,0.019 0.0625,0.0303 0.0929,0.0478 0.0346,0.0186 0.0634,0.0449 0.0991,0.0659 0.0708,0.0376 0.1455,0.0664 0.21962,0.10254 0.0733,0.0376 0.145,0.0737 0.22226,0.10645 0.11712,0.0474 0.23674,0.0889 0.35637,0.125 0.0709,0.0249 0.13925,0.0513 0.2115,0.0688 0.13425,0.041 0.27187,0.0625 0.40863,0.0884 0.0571,0.0117 0.11725,0.0254 0.17574,0.0288 0.19688,0.0303 0.3955,0.0439 0.59238,0.0439 l 0,0 c 0,0 10e-4,0 0.002,0 0.40925,0 0.82188,-0.0576 1.21825,-0.16748 0.004,-0.004 0.006,-0.004 0.006,-0.004 0.15387,-0.041 0.30525,-0.10254 0.45412,-0.16113 0.0449,-0.0186 0.0928,-0.0298 0.13675,-0.0474 0.12601,-0.0552 0.24263,-0.12891 0.36225,-0.19532 0.066,-0.0361 0.13576,-0.0649 0.20076,-0.10253 l 0.002,-0.003 c 0.003,0 0.003,0 0.003,0 l 0.12063,-0.0762 62.66537,-38.91018 c 1.33125,-0.82762 2.13625,-2.28175 2.13375,-3.84912 l -0.11,-36.32813" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
+     id="path3026"
+     d="m 231.20561,280.76812 0.0772,25.623 53.784,-33.398 -0.075,-25.71475 -53.78625,33.48975" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:#f58113;fill-opacity:1;fill-rule:nonzero;stroke:none"
+     id="path3028"
+     d="m 172.55336,202.99313 54.23975,33.67974 54.23925,-33.77937 -54.23438,-33.58475 -54.24462,33.68438" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+     id="path3030"
+     d="m 408.19061,242.22075 c -1.685,21.81875 -20.725,35.00975 -38.77875,35.00975 -21.9175,0 -39.375,-17.75 -39.375,-37.98738 0,-19.039 15.77125,-37.98537 39.27625,-37.98537 19.43875,0 31.835,13.39112 34.71375,20.82862 l -8.52875,0 c -4.165,-7.23975 -13.8875,-14.183 -26.08625,-14.183 -18.545,0 -32.035,15.07663 -32.035,31.44288 0,16.36175 13.49,31.24112 32.335,31.24112 15.56875,0 27.075,-11.50637 29.6525,-21.72362 l -40.96,0 0,-6.643 49.78625,0" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+     id="path3032"
+     d="m 450.02686,244.60112 28.465,0 -14.08375,-33.72075 -14.38125,33.72075 z m 10.215,-42.05175 8.2325,0 31.53625,73.392 -8.035,0 -10.605,-24.69337 -34.12,0 -10.71125,24.69337 -7.63875,0 31.34125,-73.392" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+     id="path3034"
+     d="m 524.92811,269.29487 36.1,-60.103 -35.00875,0 0,-6.6425 43.2375,0 0,6.6425 -36.00125,60.103 37.09375,0 0,6.6465 -45.42125,0 0,-6.6465" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+     id="path3036"
+     d="m 597.93561,202.54937 40.17,0 0,6.6425 -32.8275,0 0,26.38575 31.73625,0 0,6.64313 -31.73625,0 0,27.07412 32.8275,0 0,6.6465 -40.17,0 0,-73.392" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+     id="path3038"
+     d="m 676.79936,269.29487 10.3125,0 c 6.1525,0 9.92125,-0.29687 13.8825,-1.88225 4.5625,-1.88287 7.73875,-6.94 7.73875,-12.49562 0,-5.65388 -3.76875,-10.51363 -8.625,-12.39938 -3.475,-1.38825 -6.44875,-1.68512 -14.28625,-1.68512 l -9.0225,0 0,28.46237 z m 0,-35.10887 9.21875,0 c 5.75875,0 9.02875,-0.29688 12.60375,-2.08 3.4675,-1.78713 5.94625,-5.94725 5.94625,-10.3165 0,-3.371 -1.195,-5.7495 -3.0725,-8.03463 -2.87375,-3.27 -7.3425,-4.563 -14.87875,-4.563 l -9.8175,0 0,24.99413 z m -7.3425,-31.63663 18.445,0 c 23.10875,0 24.0025,16.5595 24.0025,19.537 0,8.63138 -5.45375,12.69288 -8.025,14.77638 7.32875,2.97362 12.1925,9.62063 12.1925,18.05425 0,7.0425 -3.27125,13.48775 -8.43125,16.9595 -6.25,3.6655 -11.1125,4.1635 -19.73875,4.06487 l -18.445,0 0,-73.392" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+     id="path3040"
+     d="m 781.48436,270.58737 c 16.95875,0 31.5375,-13.78762 31.5375,-31.24112 0,-17.16075 -14.18,-31.44288 -31.5375,-31.44288 -16.565,0 -31.44625,13.49063 -31.44625,31.73925 0,16.75775 14.38375,30.94475 31.44625,30.94475 z m 0,-69.32962 c 20.625,0 38.87625,16.36475 38.87625,37.98538 0,21.62112 -18.34625,37.98737 -38.87625,37.98737 -20.83,0 -38.7825,-16.96288 -38.7825,-37.58788 0,-23.11187 19.23875,-38.38487 38.7825,-38.38487" />
+</svg>
diff --git a/_static/images/logos/gazebo_horz_pos.svg b/_static/images/logos/gazebo_horz_pos.svg
new file mode 100644
index 0000000000..c3f296ee8e
--- /dev/null
+++ b/_static/images/logos/gazebo_horz_pos.svg
@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   width="979.84998"
+   height="478.5"
+   id="svg2"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="gazebo_horz_pos.svg">
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1393"
+     inkscape:window-height="1081"
+     id="namedview3542"
+     showgrid="false"
+     inkscape:zoom="0.74470588"
+     inkscape:cx="472.68004"
+     inkscape:cy="83.902598"
+     inkscape:window-x="1041"
+     inkscape:window-y="140"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg2" />
+  <defs
+     id="defs4">
+    <clipPath
+       id="clipPath3020">
+      <path
+         d="m 0,0 5286.97,0 0,1276 L 0,1276 0,0 z"
+         id="path3022"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+    <clipPath
+       id="clipPath3111">
+      <path
+         d="m 0,0 5286.98,0 0,1276 L 0,1276 0,0 z"
+         id="path3113"
+         inkscape:connector-curvature="0" />
+    </clipPath>
+  </defs>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <path
+     style="fill:#ffffff;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
+     d="m 164.74024,205.99021 1.89902,67.41531 59.81923,38.92997 0.94951,-34.65717 35.13192,-19.46498 z"
+     id="path4004"
+     inkscape:connector-curvature="0" />
+  <g
+     transform="translate(-170.935,-653.61217)"
+     id="layer1">
+    <g
+       transform="matrix(1.25,0,0,-1.25,330.42374,972.61217)"
+       id="g3103">
+      <g
+         transform="scale(0.1,0.1)"
+         id="g3105">
+        <g
+           id="g3107">
+          <g
+             clip-path="url(#clipPath3111)"
+             id="g3109">
+            <path
+               d="m 503.164,595.555 16.238,-10.078 c 5.825,-3.625 12.422,-5.422 19.043,-5.422 6.606,0 13.227,1.797 19.067,5.449 L 573.77,595.621 738.613,493.543 518.516,356.48 c -10.618,-6.589 -17.063,-18.222 -17.012,-30.761 L 502.148,101.102 104.512,348.016 503.164,595.555 z M 70.9062,863.949 434.746,638.031 70.9062,412.125 l 0,451.824 z M 1075.99,638.766 c -0.03,13.066 -7.14,25.117 -18.61,31.464 -11.44,6.336 -25.43,5.977 -36.54,-0.937 L 806.973,536.109 642.148,638.18 1059.82,898.297 c 10.6,6.601 17.05,18.203 17.05,30.691 -0.03,12.461 -6.5,24.067 -17.1,30.637 L 557.441,1270.67 c -11.66,7.23 -26.386,7.21 -38.039,-0.02 L 17.0586,958.719 C 6.45703,952.145 0,940.543 0,928.055 0,924.809 0,351.266 0,348.016 0,335.531 6.45703,323.934 17.0586,317.34 L 519.402,5.42188 c 0.246,-0.15235 0.5,-0.24219 0.743,-0.38282 0.277,-0.14844 0.507,-0.35937 0.793,-0.52734 0.566,-0.30078 1.164,-0.53125 1.757,-0.82031 0.586,-0.30079 1.16,-0.58985 1.778,-0.85157 0.937,-0.3789 1.894,-0.71093 2.851,-1 0.567,-0.19922 1.114,-0.41015 1.692,-0.55078 1.074,-0.328122 2.175,-0.499998 3.269,-0.707029 0.457,-0.09375 0.938,-0.203125 1.406,-0.230469 C 535.266,0.109375 536.855,0 538.43,0 l 0,0 c 0,0 0.008,0 0.015,0 3.274,0 6.575,0.460938 9.746,1.33984 0.032,0.03125 0.051,0.03125 0.051,0.03125 1.231,0.32813 2.442,0.82032 3.633,1.28907 0.359,0.14843 0.742,0.23828 1.094,0.3789 1.008,0.44141 1.941,1.03125 2.898,1.5625 0.528,0.28906 1.086,0.51953 1.606,0.82032 l 0.015,0.02734 c 0.024,0 0.024,0 0.024,0 L 558.477,6.05859 1059.8,317.34 c 10.65,6.621 17.09,18.254 17.07,30.793 l -0.88,290.633"
+               id="path3115"
+               style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
+               inkscape:connector-curvature="0" />
+            <path
+               d="m 573.73,305.855 0.618,-204.984 430.272,267.184 -0.6,205.73 -430.29,-267.93"
+               id="path3117"
+               style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+               inkscape:connector-curvature="0" />
+            <path
+               d="M 104.512,928.055 538.43,658.629 972.344,928.844 538.469,1197.53 104.512,928.055"
+               id="path3119"
+               style="fill:#f58113;fill-opacity:1;fill-rule:nonzero;stroke:none"
+               inkscape:connector-curvature="0" />
+            <path
+               d="m 1989.61,614.242 c -13.48,-174.551 -165.8,-280.086 -310.23,-280.086 -175.34,0 -315,142.012 -315,303.887 0,152.336 126.17,303.895 314.21,303.895 155.51,0 254.68,-107.118 277.71,-166.622 l -68.23,0 c -33.32,57.911 -111.1,113.457 -208.69,113.457 -148.36,0 -256.28,-120.601 -256.28,-251.531 0,-130.898 107.92,-249.933 258.68,-249.933 124.55,0 216.6,92.05 237.22,173.773 l -327.68,0 0,53.16 398.29,0"
+               id="path3121"
+               style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
+               inkscape:connector-curvature="0" />
+            <path
+               d="m 2324.3,595.199 227.72,0 -112.67,269.758 -115.05,-269.758 z m 81.72,336.418 65.86,0 252.29,-587.148 -64.28,0 -84.84,197.558 -272.96,0 -85.69,-197.558 -61.11,0 250.73,587.148"
+               id="path3123"
+               style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
+               inkscape:connector-curvature="0" />
+            <path
+               d="m 2923.5,397.633 288.81,480.82 -280.08,0 0,53.164 345.92,0 0,-53.164 -288.03,-480.82 296.76,0 0,-53.164 -363.38,0 0,53.164"
+               id="path3125"
+               style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
+               inkscape:connector-curvature="0" />
+            <path
+               d="m 3507.56,931.617 321.37,0 0,-53.164 -262.62,0 0,-211.062 253.89,0 0,-53.149 -253.89,0 0,-216.609 262.62,0 0,-53.164 -321.37,0 0,587.148"
+               id="path3127"
+               style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
+               inkscape:connector-curvature="0" />
+            <path
+               d="m 4138.48,397.633 82.5,0 c 49.22,0 79.38,2.375 111.06,15.058 36.5,15.059 61.91,55.536 61.91,99.981 0,45.234 -30.16,84.109 -69,99.18 -27.8,11.125 -51.59,13.496 -114.28,13.496 l -72.19,0 0,-227.715 z m 0,280.887 73.75,0 c 46.08,0 72.23,2.375 100.82,16.64 27.76,14.301 47.58,47.578 47.58,82.512 0,26.98 -9.55,46.016 -24.57,64.297 -23.01,26.164 -58.75,36.484 -119.04,36.484 l -78.54,0 0,-199.933 z m -58.75,253.097 147.58,0 c 184.86,0 192.01,-132.48 192.01,-156.301 0,-69.05 -43.63,-101.558 -64.2,-118.211 58.63,-23.808 97.54,-76.964 97.54,-144.433 0,-56.336 -26.17,-107.902 -67.44,-135.672 -50,-29.336 -88.91,-33.32 -157.91,-32.531 l -147.58,0 0,587.148"
+               id="path3129"
+               style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
+               inkscape:connector-curvature="0" />
+            <path
+               d="m 4975.96,387.309 c 135.67,0 252.31,110.304 252.31,249.933 0,137.266 -113.44,251.531 -252.31,251.531 -132.52,0 -251.56,-107.918 -251.56,-253.906 0,-134.078 115.06,-247.558 251.56,-247.558 z m 0,554.629 c 165,0 311.02,-130.918 311.02,-303.895 0,-172.969 -146.78,-303.887 -311.02,-303.887 -166.64,0 -310.25,135.711 -310.25,300.711 0,184.895 153.9,307.071 310.25,307.071"
+               id="path3131"
+               style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
+               inkscape:connector-curvature="0" />
+          </g>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/_templates/fuel_app_link.html b/_templates/fuel_app_link.html
new file mode 100644
index 0000000000..5e9c34e13e
--- /dev/null
+++ b/_templates/fuel_app_link.html
@@ -0,0 +1 @@
+<a class="btn btn-outline-secondary" href="https://app.gazebosim.org">APP</a>
diff --git a/_templates/gz-footer.html b/_templates/gz-footer.html
new file mode 100644
index 0000000000..1af95b7893
--- /dev/null
+++ b/_templates/gz-footer.html
@@ -0,0 +1,4 @@
+<p> Brought to you by <a href="https://openrobotics.org">Open Robotics</a>. </p>
+<p> Except where otherwise noted, the gazebosim.org web pages are licensed under <a
+    href="https://creativecommons.org/licenses/by/3.0/us/">Creative Commons
+    Attribution 3.0</a>. </p>
diff --git a/_templates/gz-navbar-nav.html b/_templates/gz-navbar-nav.html
new file mode 100644
index 0000000000..c52a58fbb1
--- /dev/null
+++ b/_templates/gz-navbar-nav.html
@@ -0,0 +1,19 @@
+<nav class="navbar-nav">
+  <ul class="bd-navbar-elements navbar-nav">
+    <li class="nav-item"><a class="nav-link nav-internal" href="{{ deploy_url }}/features">Features</a></li>
+    <li class="nav-item"><a class="nav-link nav-internal" href="{{ deploy_url }}/showcase">Showcase</a></li>
+    <li class="nav-item"><a class="nav-link nav-internal" href="{{ deploy_url }}/docs">Docs</a></li>
+    <li class="nav-item"><a class="nav-link nav-internal" href="https://community.gazebosim.org/">Community</a></li>
+    <li class="nav-item dropdown">
+      <button class="btn dropdown-toggle nav-item" type="button" data-bs-toggle="dropdown" aria-expanded="false" aria-controls="pst-nav-more-links">More</button>
+      <ul class="dropdown-menu">
+        <li class="nav-item"><a class="nav-link nav-internal" href="{{ deploy_url }}/about">About</a></li>
+        <li class="nav-item"><a class="nav-link nav-internal" href='https://community.gazebosim.org' title="Discussion forum">Community</a>
+        <li class="nav-item"><a class="nav-link nav-internal" href='https://robotics.stackexchange.org' title="Technical help">Answers</a>
+        <li class="nav-item"><a class="nav-link nav-internal" href="https://blog.openrobotics.org/tag/gazebo">Blog</a>
+        <li class="nav-item"><a class="nav-link nav-internal" href="{{ deploy_url }}/media">Media</a></li>
+      </ul>
+    </li>
+  </ul>
+</nav>
+
diff --git a/_templates/gz-sidebar-nav.html b/_templates/gz-sidebar-nav.html
new file mode 100644
index 0000000000..db9b8f866b
--- /dev/null
+++ b/_templates/gz-sidebar-nav.html
@@ -0,0 +1,16 @@
+{# Temlate to generate sidedbar navigation from level 0 toc items #}
+<nav class="bd-docs-nav bd-links"
+     aria-label="{{ _('Section Navigation') }}">
+  <div class="bd-toc-item navbar-nav">
+    {{- generate_toctree_html(
+      "sidebar",
+      startdepth=0,
+      show_nav_level=theme_show_nav_level | int,
+      maxdepth=theme_navigation_depth | int,
+      collapse=theme_collapse_navigation | tobool,
+      includehidden=theme_sidebar_includehidden | tobool,
+      titles_only=True
+      )
+    -}}
+  </div>
+</nav>
diff --git a/_templates/gz-version-switcher.html b/_templates/gz-version-switcher.html
new file mode 100644
index 0000000000..5808190226
--- /dev/null
+++ b/_templates/gz-version-switcher.html
@@ -0,0 +1,4 @@
+<div class="gz-version-switcher">
+  <span class="fs-4 fw-light pe-2">Release:</span>
+{%- include 'version-switcher.html' -%}
+</div>
diff --git a/_templates/sections/header.html b/_templates/sections/header.html
new file mode 100644
index 0000000000..d046f3e16d
--- /dev/null
+++ b/_templates/sections/header.html
@@ -0,0 +1,52 @@
+{% if release_info %}
+  {% if not release_info.preferred %}
+  <aside aria-label="Version warning" class="bd-header-announcement bd-header-version-{{'warning' if release_info.eol else 'info'}}" style="min-height: 3rem;">
+    <div class="bd-header-announcement__content">
+      <div class="sidebar-message">
+        {% if release_info.eol %}
+          This is documentation for <strong>Gazebo {{ release_info.name | capitalize}}</strong>, which has reached its EOL
+          (end-of-life) and is no longer officially supported.
+        {% elif release_info.dev %}
+          This is documentation for <strong> Gazebo {{ release_info.name | capitalize}}</strong> which is still under
+          development.
+        {% else %}
+          This is documentation for <strong> Gazebo {{ release_info.name | capitalize}}</strong>,
+          an older but still supported version of Gazebo. {% endif %}
+        <a class="btn text-wrap font-weight-bold ms-3 my-1 align-baseline pst-button-link-to-stable-version"
+        href="{{ pathto(root_doc) }}../{{preferred_release.name}}/{{pagename}}">Switch to the latest stable version</a>
+      </div>
+    </div>
+  </aside>
+  {% endif %}
+{% endif %}
+
+{% include "!sections/header.html" %}
+
+{% if release_info %}
+<div class="doc-header">
+  <div class="banner d-flex flex-column-reverse flex-sm-row">
+    <div>
+      <div class="d-none d-sm-block">
+        <span class="d-inline fs-2">Docs / Gazebo {{ release_info.name | capitalize }}</span>
+        {% if release_info.lts %}
+          <span class="badge bg-primary fs-4 fw-light ms-2">LTS</span>
+        {% elif release_info.eol %}
+          <span class="badge bg-danger fs-4 fw-light ms-2">EOL</span>
+        {% endif %}
+      </div>
+      <span>{{ release_info.description}}</span>
+    </div>
+    {%- include 'gz-version-switcher.html' -%}
+  </div>
+</div>
+{% else %}
+{# Assume /libs #}
+<div class="doc-heading">
+  <div class="banner">
+    <div class="d-sm-block">
+      <div class="h1">Development Libraries</div>
+      <span>Explore the complete set of development libraries offered by Gazebo.</span>
+    </div>
+  </div>
+</div>
+{% endif %}
diff --git a/base_conf.py b/base_conf.py
new file mode 100644
index 0000000000..b29260d9db
--- /dev/null
+++ b/base_conf.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2024 Open Source Robotics Foundation
+#
+# 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.
+
+# Configuration file for the Sphinx documentation builder.
+#
+# For the full list of built-in configuration values, see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+
+import os
+
+project = "Gazebo"
+copyright = "2024, Open Robotics"
+author = "Gazebo Team"
+
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+
+extensions = [
+    "myst_parser",
+    "sphinx_copybutton",
+    "sphinx_design",
+    # 'sphinx_sitemap',
+]
+
+templates_path = ["_templates"]
+
+source_suffix = [
+    ".md",
+]
+
+myst_heading_anchors = 4
+myst_all_links_external = True
+
+myst_enable_extensions = [
+    "amsmath",
+    "attrs_inline",
+    "attrs_block",
+    "colon_fence",
+    "deflist",
+    "dollarmath",
+    "fieldlist",
+    "html_admonition",
+    "html_image",
+    "linkify",
+    "replacements",
+    "smartquotes",
+    "strikethrough",
+    "substitution",
+    "tasklist",
+]
+
+# -- Options for HTML output -------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
+
+html_theme = "pydata_sphinx_theme"
+html_static_path = ["_static"]
+html_css_files = ["css/gazebo.css"]
+html_favicon = "_static/icon/favicon.ico"
+
+html_theme_options = {
+    "header_links_before_dropdown": 4,
+    "use_edit_page_button": True,
+    "show_toc_level": 1,
+    "navigation_with_keys": False,
+    "show_prev_next": False,
+    "footer_center": ["gz-footer"],
+    "footer_start": ["sphinx-version"],
+    "secondary_sidebar_items": ["page-toc", "edit-this-page"],
+    "navbar_align": "left",
+    "navbar_center": ["gz-navbar-nav"],
+    "navbar_end": ["navbar-icon-links", "theme-switcher", "fuel_app_link"],
+    "pygments_light_style": "tango",
+    "pygments_dark_style": "monokai",
+    "logo": {
+        "image_light": "_static/images/logos/gazebo_horz_pos.svg",
+        "image_dark": "_static/images/logos/gazebo_horz_neg.svg",
+    },
+    "check_switcher": False,
+    # We have our own version, so we disable the one from the theme.
+    "show_version_warning_banner": False,
+}
+
+html_sidebars = {"**": ["gz-sidebar-nav"]}
+
+html_context = {
+    "deploy_url":  os.environ.get("GZ_DEPLOY_URL", "")
+}
diff --git a/blueprint/ros_integration.md b/blueprint/ros_integration.md
index be30d83f92..520865f38f 100644
--- a/blueprint/ros_integration.md
+++ b/blueprint/ros_integration.md
@@ -101,4 +101,4 @@ The screenshot shows all the shell windows and their expected content
 (it was taken using ROS Melodic):
 
 
-![Ignition Transport images and ROS 1 rqt](../acropolis/images/bridge_image_exchange_ign-gazebo.png)
+![Ignition Transport images and ROS 1 rqt](img/bridge_image_exchange_gz-sim.png)
diff --git a/build_multiversion.py b/build_multiversion.py
new file mode 100644
index 0000000000..3662bc108c
--- /dev/null
+++ b/build_multiversion.py
@@ -0,0 +1,454 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2024 Open Source Robotics Foundation
+#
+# 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.
+
+from pathlib import Path
+from string import Template
+import argparse
+import copy
+import json
+import os
+import requests
+import shutil
+import sys
+import subprocess
+import yaml
+
+additional_shared_directories = ["images", "releasing"]
+
+
+def _combine_nav(common_nav, release_nav):
+    combined = copy.deepcopy(common_nav)
+    # Release are added after 'get_started'
+    for i, item in enumerate(release_nav):
+        combined.insert(i + 1, item)
+    return combined
+
+
+def copy_pages(pages, root_src_dir, dst):
+    for page in pages:
+        full_dst = Path(dst) / page["file"]
+        if full_dst.parent != dst:
+            full_dst.parent.mkdir(parents=True, exist_ok=True)
+
+        shutil.copy2(root_src_dir / page["file"], full_dst)
+        if "children" in page:
+            copy_pages(page["children"], root_src_dir, dst)
+
+
+def generate_sources(gz_nav_yaml, root_src_dir, tmp_dir, gz_release):
+
+    if not gz_release:
+        raise RuntimeError("gz_release not provided")
+    # Copy release-specific directory
+    version_src_dir = Path(root_src_dir) / gz_release
+
+    matching_release = [
+        release for release in gz_nav_yaml["releases"] if release["name"] == gz_release
+    ]
+    if not matching_release:
+        raise RuntimeError(
+            f"Provided gz_release '{gz_release}' not registered in `index.yaml`"
+        )
+    elif len(matching_release) > 1:
+        raise RuntimeError(f"More than one releases named '{gz_release}' found.")
+
+    release_info = matching_release[0]
+
+    tmp_dir.mkdir(exist_ok=True)
+    version_tmp_dir = tmp_dir / gz_release
+
+    shutil.copytree(version_src_dir, version_tmp_dir, dirs_exist_ok=True)
+    for dir in ["_static", "_templates"]:
+        shutil.copytree(root_src_dir / dir, version_tmp_dir / dir, dirs_exist_ok=True)
+
+    shutil.copy2(root_src_dir / "base_conf.py", version_tmp_dir)
+    shutil.copy2(root_src_dir / "conf.py", version_tmp_dir)
+
+    for dir in additional_shared_directories:
+        shutil.copytree(root_src_dir / dir, version_tmp_dir / dir, dirs_exist_ok=True)
+
+    copy_pages(gz_nav_yaml["pages"], root_src_dir, version_tmp_dir)
+
+    deploy_url = os.environ.get("GZ_DEPLOY_URL", "")
+    # Write switcher.json file
+    switcher = []
+    for release in gz_nav_yaml["releases"]:
+        name = release["name"].capitalize()
+        if release["eol"]:
+            name += " (EOL)"
+        elif release["lts"]:
+            name += " (LTS)"
+        elif release.get("dev", False):
+            name += " (dev)"
+
+        switcher.append(
+            {
+                "name": name,
+                "version": release["name"],
+                "url": f"{deploy_url}/docs/{release['name']}/",
+                "preferred": release.get("preferred", False)
+            }
+        )
+
+    static_dir = version_tmp_dir / "_static"
+    static_dir.mkdir(exist_ok=True)
+    json.dump(switcher, open(static_dir / "switcher.json", "w"))
+
+    def handle_file_url_rename(file_path, file_url):
+        computed_url, ext = os.path.splitext(file_path)
+        # print("renames:", file_path, file_url)
+        if file_url != computed_url:
+            new_path = file_url + ext
+            # If the file url is inside a directory, we want the new path to end up in the same directory
+            # print("Moving", version_tmp_dir / file_path, version_tmp_dir / new_path)
+            shutil.move(version_tmp_dir / file_path, version_tmp_dir / new_path)
+            return new_path
+        return file_path
+
+    toc_directives = ["{toctree}", ":hidden:", ":maxdepth: 1", ":titlesonly:"]
+
+    with open(version_tmp_dir / "index.yaml") as f:
+        version_nav_yaml = yaml.safe_load(f)
+        combined_nav = _combine_nav(gz_nav_yaml["pages"], version_nav_yaml["pages"])
+
+        nav_md = []
+        # TODO(azeey) Make this recursive so multiple levels of
+        # 'children' can be supported.
+        for page in combined_nav:
+            file_url = page["name"]
+            file_path = page["file"]
+
+            children = page.get("children")
+            nav_md.append(f"{page['title']} <{page['name']}>")
+            new_file_path = handle_file_url_rename(file_path, file_url)
+
+            if children:
+                child_md = []
+                for child in children:
+                    file_url = child["name"]
+                    file_path = child["file"]
+                    handle_file_url_rename(file_path, file_url)
+                    child_md.append(f"{child['title']} <{file_url}>")
+
+                with open(version_tmp_dir / new_file_path, "a") as ind_f:
+                    ind_f.write("```")
+                    ind_f.write("\n".join(toc_directives) + "\n")
+                    ind_f.writelines("\n".join(child_md) + "\n")
+                    ind_f.write("```\n")
+
+        library_reference_nav = "library_reference_nav"
+        libraries = release_info["libraries"]
+        if libraries:
+            nav_md.append(library_reference_nav)
+            # Add Library Reference
+            with open(version_tmp_dir / f"{library_reference_nav}.md", "w") as ind_f:
+                ind_f.write("# Library Reference\n\n")
+                ind_f.write("```")
+                ind_f.write("{toctree}\n")
+                for library in libraries:
+                    ind_f.write(
+                        f"{library['name']} <https://gazebosim.org/api/{library['name']}/{library['version']}>\n"
+                    )
+                ind_f.write("```\n\n")
+
+        with open(version_tmp_dir / "index.md", "w") as ind_f:
+            ind_f.write(
+                """---
+myst:
+    html_meta:
+      "http-equiv=refresh": "0; url=getstarted"
+---
+"""
+            )
+            ind_f.write("# Index\n\n")
+            ind_f.write("```")
+            ind_f.write("\n".join(toc_directives) + "\n")
+            ind_f.writelines("\n".join(nav_md) + "\n")
+            ind_f.write("```\n\n")
+
+
+def get_preferred_release(releases: dict):
+    preferred = [rel for rel in releases if rel.get("preferred", False)]
+    assert len(preferred) == 1
+    return preferred[0]
+
+
+def github_repo_name(lib_name):
+    prefix = "gz-" if lib_name != "sdformat" else ""
+    return f"{prefix}{lib_name.replace('_','-')}"
+
+
+def github_branch(repo_name, version):
+    return f"{repo_name}{version}" if repo_name != "sdformat" else f"sdf{version}"
+
+
+def github_url(lib_name):
+    return f"https://github.com/gazebosim/{github_repo_name(lib_name)}"
+
+
+def api_url(lib_name, version):
+    if lib_name == "sdformat":
+        return "http://sdformat.org/api"
+    else:
+        return f"https://gazebosim.org/api/{lib_name}/{version}"
+
+
+def get_github_content(lib_name, version, file_path):
+    repo_name = github_repo_name(lib_name)
+    branch = github_branch(repo_name, version)
+    url = f"https://raw.githubusercontent.com/gazebosim/{repo_name}/{branch}/{file_path}"
+    if os.environ.get("SKIP_FETCH_CONTENT", False):
+        return f"Skipped fetching context from {url}"
+
+    print(f"fetching {url}")
+    result = requests.get(url, allow_redirects=True)
+    return result.text
+
+
+def generate_individual_lib(library, libs_dir):
+    lib_name = library["name"]
+    version = library["version"]
+    cur_lib_dir = libs_dir / lib_name
+    cur_lib_dir.mkdir(exist_ok=True)
+
+    template = Template("""\
+# $name
+
+{.gz-libs-lists}
+- [{material-regular}`code;2em` Source Code]($github_url)
+- [{material-regular}`description;2em` API & Tutorials]($api_url)
+
+::::{tab-set}
+
+:::{tab-item} Readme
+$readme
+:::
+
+:::{tab-item} Changelog
+$changelog
+:::
+
+::::
+    """)
+
+    mapping = {
+        "name": lib_name,
+        "readme": get_github_content(lib_name, version, "README.md"),
+        "changelog": get_github_content(lib_name, version, "Changelog.md"),
+        "github_url": github_url(lib_name),
+        "api_url": api_url(lib_name, version),
+    }
+    with open(cur_lib_dir / "index.md", "w") as f:
+        f.write(template.substitute(mapping))
+
+
+def generate_libs(gz_nav_yaml, libs_dir):
+    libraries = get_preferred_release(gz_nav_yaml["releases"])["libraries"]
+    library_directives = "\n".join([
+        f"{library['name'].capitalize()} <{library['name']}/index>"
+        for library in libraries
+    ])
+
+    index_md_header_template = Template("""\
+# Libraries
+
+```{toctree}
+:maxdepth: 1
+:hidden:
+:titlesonly:
+$library_directives
+```
+
+""")
+
+    library_card_template = Template("""\
+:::{card} [$name_cap]($name/index)
+:class-card: gz-libs-cards
+   - [{material-regular}`fullscreen;2em` Details]($name/index)
+   - [{material-regular}`code;2em` Source Code]($github_url)
+   - [{material-regular}`description;2em` API & Tutorials]($api_url)
++++,
+
+$description
+:::
+
+
+""")
+    with open(libs_dir / "index.md", "w") as f:
+        f.write(
+            index_md_header_template.substitute(library_directives=library_directives)
+        )
+
+        for library in sorted(libraries, key=lambda lib: lib["name"]):
+            name = library["name"]
+            try:
+                description = gz_nav_yaml["library_info"][name]["description"]
+            except KeyError as e:
+                print(
+                    f"Description for library {name} not found."
+                    "Make sure there is an entry for it in index.yaml"
+                )
+                print(e)
+                description = ""
+            mapping = {
+                "name": name,
+                "name_cap": name.capitalize(),
+                "github_url": github_url(name),
+                "api_url": api_url(name, library["version"]),
+                "description": description,
+            }
+            f.write(library_card_template.substitute(mapping))
+
+            generate_individual_lib(library, libs_dir)
+
+
+def build_libs(gz_nav_yaml, src_dir, tmp_dir, build_dir):
+    libs_dir = tmp_dir / "libs"
+    libs_dir.mkdir(exist_ok=True)
+    shutil.copy2(src_dir / "base_conf.py", libs_dir/"base_conf.py")
+    shutil.copy2(src_dir / "libs_conf.py", libs_dir/"conf.py")
+    if len(gz_nav_yaml["releases"]) == 0:
+        print("No releases found in 'index.yaml'.")
+        return
+
+    for dir in ["_static", "_templates"]:
+        shutil.copytree(src_dir / dir, libs_dir / dir, dirs_exist_ok=True)
+
+    build_dir = build_dir / "libs"
+
+    generate_libs(gz_nav_yaml, libs_dir)
+
+    sphinx_args = [
+        "sphinx-build",
+        "-b",
+        "dirhtml",
+        f"{libs_dir}",
+        f"{build_dir}",
+    ]
+    subprocess.run(sphinx_args)
+
+
+def main(argv=None):
+    src_dir = Path(__file__).parent
+
+    # We will assume that this file is in the same directory as documentation
+    # sources and conf.py files.
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "-r",
+        "--releases",
+        metavar="GZ_RELEASES",
+        nargs="*",
+        help="Names of releases to build. Builds all known releases if empty.",
+    )
+    parser.add_argument(
+        "--output_dir", default=src_dir / ".build", help="Path to output directory"
+    )
+    parser.add_argument(
+        "--libs", action="store_true", default=False, help="Build /libs page"
+    )
+    parser.add_argument(
+        "--libs_only", action="store_true", default=False, help="Build only /libs page"
+    )
+    parser.add_argument(
+        "--pointers",
+        action="store_true",
+        default=False,
+        help="Build 'latest' and 'all'",
+    )
+
+    args, unknown_args = parser.parse_known_args(argv)
+
+    index_yaml = src_dir / "index.yaml"
+    assert index_yaml.exists()
+
+    with open(index_yaml) as top_index_file:
+        gz_nav_yaml = yaml.safe_load(top_index_file)
+
+    if not args.releases:
+        args.releases = [release["name"] for release in gz_nav_yaml["releases"]]
+
+    preferred_release = get_preferred_release(gz_nav_yaml["releases"])
+    tmp_dir = src_dir / ".tmp"
+    tmp_dir.mkdir(exist_ok=True)
+    build_dir = Path(args.output_dir)
+    build_dir.mkdir(exist_ok=True)
+    if args.libs or args.libs_only:
+        build_libs(gz_nav_yaml, src_dir, tmp_dir, build_dir)
+
+    if args.libs_only:
+        return
+
+    build_docs_dir = build_dir / "docs"
+    for release in args.releases:
+        generate_sources(gz_nav_yaml, src_dir, tmp_dir, release)
+        release_build_dir = build_docs_dir / release
+        sphinx_args = [
+            "sphinx-build",
+            "-b",
+            "dirhtml",
+            f"{tmp_dir/release}",
+            f"{release_build_dir }",
+            "-D",
+            f"gz_release={release}",
+            "-D",
+            f"gz_root_index_file={index_yaml}",
+            *unknown_args,
+        ]
+        subprocess.run(sphinx_args)
+
+    # Handle "latest" and "all"
+    release = preferred_release["name"]
+    if args.pointers and (release in args.releases):
+        for pointer in ["latest", "all"]:
+            release_build_dir = build_docs_dir / pointer
+            pointer_tmp_dir = tmp_dir/pointer
+            try:
+                pointer_tmp_dir.symlink_to(tmp_dir/release)
+            except FileExistsError:
+                # It's okay for it to exist, but make sure it's a symlink
+                if not pointer_tmp_dir.is_symlink:
+                    raise RuntimeError(
+                        f"{pointer_tmp_dir} already exists and is not a symlink"
+                    )
+
+            sphinx_args = [
+                "sphinx-build",
+                "-b",
+                "dirhtml",
+                f"{pointer_tmp_dir}",
+                f"{release_build_dir}",
+                "-D",
+                f"gz_release={release}",
+                "-D",
+                f"gz_root_index_file={index_yaml}",
+                *unknown_args,
+            ]
+            subprocess.run(sphinx_args)
+
+        # Create a redirect to "/latest"
+        redirect_page = build_docs_dir / "index.html"
+        redirect_page.write_text("""\
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta content="0; url=latest" http-equiv="refresh" />
+  </head>
+</html>
+""")
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))
diff --git a/citadel/index.yaml b/citadel/index.yaml
index f58d5ec5d0..f293b7a473 100644
--- a/citadel/index.yaml
+++ b/citadel/index.yaml
@@ -41,6 +41,9 @@ pages:
       - name: troubleshooting
         title: Troubleshooting
         file: troubleshooting.md
+      - name: ign_docker_env
+        title: Dockerized Dev Env
+        file: ign_docker_env.md
   - name: comparison
     title: Feature Comparison
     file: comparison.md
diff --git a/citadel/install_osx_src.md b/citadel/install_osx_src.md
index 77e564cb4a..8c9b78edaa 100644
--- a/citadel/install_osx_src.md
+++ b/citadel/install_osx_src.md
@@ -122,7 +122,7 @@ If you want to compile Ignition Libraries in MacOS Catalina (10.15) you will nee
 
 Create a file called `intern.patch` with the following content:
 
-```patch
+```diff
 --- intern.h    2019-12-16 18:17:08.000000000 +0100
 +++ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Ruby.framework/Headers/ruby/ruby/intern.h
 @@ -14,6 +14,10 @@
@@ -140,7 +140,7 @@ Create a file called `intern.patch` with the following content:
 
 Now we can apply the patch:
 
-```{.sh}
+```sh
 sudo patch -p0 < intern.patch
 ```
 
@@ -164,7 +164,7 @@ Create a file called `config.patch` with the following content:
 
 Now we can appply the patch:
 
-```{.sh}
+```sh
 sudo patch -p0 < config.patch
 ```
 
diff --git a/citadel/ros_integration.md b/citadel/ros_integration.md
index 460a698549..22184cefa4 100644
--- a/citadel/ros_integration.md
+++ b/citadel/ros_integration.md
@@ -96,4 +96,4 @@ The screenshot shows all the shell windows and their expected content
 (it was taken using ROS Melodic):
 
 
-![Ignition Transport images and ROS 1 rqt](../acropolis/images/bridge_image_exchange_ign-gazebo.png)
+![Ignition Transport images and ROS 1 rqt](img/bridge_image_exchange_ign-gazebo.png)
diff --git a/citadel/sensors.md b/citadel/sensors.md
index c5f8f101fe..34d0e120a8 100644
--- a/citadel/sensors.md
+++ b/citadel/sensors.md
@@ -362,14 +362,14 @@ Inside the main we subscribe to the `lidar` topic, and wait until the node is sh
 
 Download the [CMakeLists.txt](https://github.com/ignitionrobotics/docs/blob/master/citadel/tutorials/sensors/CMakeLists.txt), and in the same folder of `lidar_node` create `build/` directory:
 
-```{.sh}
+```sh
 mkdir build
 cd build
 ```
 
 Run cmake and build the code:
 
-```{.sh}
+```sh
 cmake ..
 make lidar_node
 ```
@@ -378,13 +378,13 @@ make lidar_node
 
 Run the node from terminal 1:
 
-```{.sh}
+```sh
 ./build/lidar_node
 ```
 
 Run the world from terminal 2:
 
-```{.sh}
+```sh
 ign gazebo sensor_tutorial.sdf
 ```
 
@@ -413,7 +413,7 @@ The first command is `ign gazebo sensor_tutorial.sdf` which launches the world.
 And the second command is `./build/lidar_node` which runs the `lidar_node`.
 Save the file as `sensor_launch.ign`, and then run it using the following command:
 
-```{.sh}
+```sh
 ign launch sensor_launch.ign
 ```
 
diff --git a/conf.py b/conf.py
new file mode 100644
index 0000000000..6145489349
--- /dev/null
+++ b/conf.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2024 Open Source Robotics Foundation
+#
+# 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.
+
+# Configuration file for the Sphinx documentation builder.
+#
+# For the full list of built-in configuration values, see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+import sys
+from pathlib import Path
+import yaml
+
+
+from sphinx.application import Sphinx
+from sphinx.config import Config
+
+
+sys.path.append(str(Path(__file__).parent))
+from base_conf import *  # noqa
+
+html_baseurl = f"{html_context['deploy_url']}/docs/latest"  # noqa
+
+html_context.update({
+    "github_user": "gazebosim",
+    "github_repo": "docs",
+    "github_version": "master",
+    "edit_page_url_template": "{{ github_url }}/{{ github_user }}/{{ github_repo }}"
+    "/edit/{{ github_version }}/{{ get_file_from_map(file_name) }}",
+    "edit_page_provider_name": "GitHub",
+})
+
+
+def setup_file_map(app: Sphinx, pagename: str, templatename: str, context, doctree):
+    def get_file_from_map(file_name: str):
+        result = context["file_name_map"].get(Path(file_name).stem)
+        if result:
+            return result
+        return file_name
+
+    context["get_file_from_map"] = get_file_from_map
+
+
+def load_releases(index_file):
+    with open(index_file) as top_index_file:
+        gz_nav_yaml = yaml.safe_load(top_index_file)
+
+    return dict([(release["name"], release) for release in gz_nav_yaml["releases"]])
+
+
+def get_preferred_release(releases: dict):
+    preferred = [rel for rel in releases.values() if rel.get("preferred", False)]
+    assert len(preferred) == 1
+    return preferred[0]
+
+
+def create_file_rename_map(nav_yaml_pages, release):
+    file_name_map = {}
+
+    prefix = f"{release}/" if release is not None else ""
+
+    for page in nav_yaml_pages:
+        file_name_map[page["name"]] = f"{prefix}{page['file']}"
+
+        children = page.get("children")
+        if children:
+            file_name_map.update(create_file_rename_map(children, release))
+
+    return file_name_map
+
+
+def config_init(app: Sphinx, config: Config):
+    if not config.gz_release:
+        raise RuntimeError("gz_release not provided")
+    config.release = config.gz_release  # type: ignore
+    config.version = config.gz_release  # type: ignore
+
+    file_name_map = {}
+
+    with open(app.config.gz_root_index_file) as f:
+        file_name_map.update(create_file_rename_map(yaml.safe_load(f)["pages"], None))
+
+    with open(Path(app.srcdir) / "index.yaml") as f:
+        file_name_map.update(
+            create_file_rename_map(yaml.safe_load(f)["pages"], config.release)
+        )
+
+    config.html_context["file_name_map"] = file_name_map
+
+    # We've disabled "check_switcher" since it doesn't play well with our directory structure.
+    # So we check for the existence of switcher.json here
+    #
+    assert Path(f"{app.srcdir}/_static/switcher.json").exists()
+    config.html_theme_options["switcher"] = {
+        "json_url": f"{html_context['deploy_url']}/docs/{config.gz_release}/_static/switcher.json",
+        "version_match": config.gz_release,
+    }
+
+    try:
+        releases = load_releases(config.gz_root_index_file)
+        app.config.html_context["release_info"] = releases[config.gz_release]
+        app.config.html_context["preferred_release"] = get_preferred_release(releases)
+
+    except KeyError as e:
+        print(e)
+        raise RuntimeError(
+            f"Provided gz_release '{config.gz_release}' not registered in `index.yaml`"
+        )
+
+
+def setup(app: Sphinx):
+    app.add_config_value("gz_release", "", rebuild="env", types=[str])
+    app.add_config_value("gz_root_index_file", "", rebuild="env", types=[str])
+    app.connect("html-page-context", setup_file_map)
+    app.connect("config-inited", config_init)
diff --git a/contributing.md b/contributing.md
index b7c3c4d5ec..0c5459a259 100644
--- a/contributing.md
+++ b/contributing.md
@@ -8,29 +8,6 @@ Organization](https://github.com/gazebosim) on GitHub. These
 are mostly guidelines, not rules. Use your best judgment, and feel free to
 propose changes to this document in a pull request.
 
-#### Table of Contents
-
-[Code of Conduct](https://gazebosim.org/docs/all/contributing#code-of-conduct)
-
-[Project Design](https://gazebosim.org/docs/all/contributing#project-design)
-
-  * [Repository List](https://gazebosim.org/docs/all/contributing#repository-list)
-
-[How to Contribute](https://gazebosim.org/docs/all/contributing#how-to-contribute)
-
-  * [Reporting Bugs](https://gazebosim.org/docs/all/contributing#reporting-bugs)
-  * [Suggesting Enhancements](https://gazebosim.org/docs/all/contributing#suggesting-enhancements)
-  * [Contributing Code](https://gazebosim.org/docs/all/contributing#contributing-code)
-  * [Tracking Progress](https://gazebosim.org/docs/all/contributing#tracking-progress)
-
-[Writing Tests](https://gazebosim.org/docs/all/contributing#writing-tests)
-
-  * [Test Coverage](https://gazebosim.org/docs/all/contributing#test-coverage)
-
-[Styleguides](https://gazebosim.org/docs/all/contributing#style-guides)
-
-[Appendix](https://gazebosim.org/docs/all/contributing#appendix)
-
 ## Code of Conduct
 
 This project and everyone participating in it is governed by the [Gazebo
diff --git a/dome/install_osx_src.md b/dome/install_osx_src.md
index ceaa16d487..2c4e5ceae0 100644
--- a/dome/install_osx_src.md
+++ b/dome/install_osx_src.md
@@ -122,7 +122,7 @@ If you want to compile Ignition Libraries in MacOS Catalina (10.15) you will nee
 
 Create a file called `intern.patch` with the following content:
 
-```patch
+```diff
 --- intern.h    2019-12-16 18:17:08.000000000 +0100
 +++ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Ruby.framework/Headers/ruby/ruby/intern.h
 @@ -14,6 +14,10 @@
@@ -140,7 +140,7 @@ Create a file called `intern.patch` with the following content:
 
 Now we can apply the patch:
 
-```{.sh}
+```sh
 sudo patch -p0 < intern.patch
 ```
 
@@ -164,7 +164,7 @@ Create a file called `config.patch` with the following content:
 
 Now we can appply the patch:
 
-```{.sh}
+```sh
 sudo patch -p0 < config.patch
 ```
 
diff --git a/dome/sensors.md b/dome/sensors.md
index 31ab5597ec..6bb641cfd8 100644
--- a/dome/sensors.md
+++ b/dome/sensors.md
@@ -362,14 +362,14 @@ Inside the main we subscribe to the `lidar` topic, and wait until the node is sh
 
 Download the [CMakeLists.txt](https://github.com/ignitionrobotics/docs/blob/master/dome/tutorials/sensors/CMakeLists.txt), and in the same folder of `lidar_node` create `build/` directory:
 
-```{.sh}
+```sh
 mkdir build
 cd build
 ```
 
 Run cmake and build the code:
 
-```{.sh}
+```sh
 cmake ..
 make lidar_node
 ```
@@ -378,13 +378,13 @@ make lidar_node
 
 Run the node from terminal 1:
 
-```{.sh}
+```sh
 ./build/lidar_node
 ```
 
 Run the world from terminal 2:
 
-```{.sh}
+```sh
 ign gazebo sensor_tutorial.sdf
 ```
 
@@ -413,7 +413,7 @@ The first command is `ign gazebo sensor_tutorial.sdf` which launches the world.
 And the second command is `./build/lidar_node` which runs the `lidar_node`.
 Save the file as `sensor_launch.ign`, and then run it using the following command:
 
-```{.sh}
+```sh
 ign launch sensor_launch.ign
 ```
 
diff --git a/dome/web_visualization.md b/dome/web_visualization.md
index 97239a74ee..4cd1a1c01f 100644
--- a/dome/web_visualization.md
+++ b/dome/web_visualization.md
@@ -62,7 +62,7 @@ matching key using an "auth" call on the websocket. If the `<admin_authorization
 
 1. Is you notice an issue with web visualization, then please
    file a ticket at
-   [https://gitlab.com/ignitionrobotics/web/app/-/issues](https://gitlab.com/ignitionrobotics/web/app/-/issues).
+   [https://github.com/gazebo-web/gzweb/issues](https://github.com/gazebo-web/gzweb/issues).
 
 ## Troubleshooting
 
diff --git a/edifice/install_osx_src.md b/edifice/install_osx_src.md
index d476c1054f..5bb5b06add 100644
--- a/edifice/install_osx_src.md
+++ b/edifice/install_osx_src.md
@@ -122,7 +122,7 @@ If you want to compile Ignition Libraries in MacOS Catalina (10.15) you will nee
 
 Create a file called `intern.patch` with the following content:
 
-```patch
+```diff
 --- intern.h    2019-12-16 18:17:08.000000000 +0100
 +++ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Ruby.framework/Headers/ruby/ruby/intern.h
 @@ -14,6 +14,10 @@
@@ -140,7 +140,7 @@ Create a file called `intern.patch` with the following content:
 
 Now we can apply the patch:
 
-```{.sh}
+```sh
 sudo patch -p0 < intern.patch
 ```
 
@@ -164,7 +164,7 @@ Create a file called `config.patch` with the following content:
 
 Now we can appply the patch:
 
-```{.sh}
+```sh
 sudo patch -p0 < config.patch
 ```
 
diff --git a/edifice/sensors.md b/edifice/sensors.md
index ef80024ef0..22b560c77b 100644
--- a/edifice/sensors.md
+++ b/edifice/sensors.md
@@ -362,14 +362,14 @@ Inside the main we subscribe to the `lidar` topic, and wait until the node is sh
 
 Download the [CMakeLists.txt](https://github.com/ignitionrobotics/docs/blob/master/edifice/tutorials/sensors/CMakeLists.txt), and in the same folder of `lidar_node` create `build/` directory:
 
-```{.sh}
+```sh
 mkdir build
 cd build
 ```
 
 Run cmake and build the code:
 
-```{.sh}
+```sh
 cmake ..
 make lidar_node
 ```
@@ -378,13 +378,13 @@ make lidar_node
 
 Run the node from terminal 1:
 
-```{.sh}
+```sh
 ./build/lidar_node
 ```
 
 Run the world from terminal 2:
 
-```{.sh}
+```sh
 ign gazebo sensor_tutorial.sdf
 ```
 
@@ -413,7 +413,7 @@ The first command is `ign gazebo sensor_tutorial.sdf` which launches the world.
 And the second command is `./build/lidar_node` which runs the `lidar_node`.
 Save the file as `sensor_launch.ign`, and then run it using the following command:
 
-```{.sh}
+```sh
 ign launch sensor_launch.ign
 ```
 
diff --git a/fortress/install_osx_src.md b/fortress/install_osx_src.md
index 4eea0450ee..9f3ae62103 100644
--- a/fortress/install_osx_src.md
+++ b/fortress/install_osx_src.md
@@ -122,7 +122,7 @@ If you want to compile Ignition Libraries in MacOS Catalina (10.15) you will nee
 
 Create a file called `intern.patch` with the following content:
 
-```patch
+```diff
 --- intern.h    2019-12-16 18:17:08.000000000 +0100
 +++ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Ruby.framework/Headers/ruby/ruby/intern.h
 @@ -14,6 +14,10 @@
@@ -140,7 +140,7 @@ Create a file called `intern.patch` with the following content:
 
 Now we can apply the patch:
 
-```{.sh}
+```sh
 sudo patch -p0 < intern.patch
 ```
 
@@ -148,7 +148,7 @@ sudo patch -p0 < intern.patch
 
 Create a file called `config.patch` with the following content:
 
-```patch
+```diff
 --- config.h    2019-12-16 18:19:13.000000000 +0100
 +++ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Ruby.framework/Headers/ruby/ruby/config.h
 @@ -410,6 +410,6 @@
@@ -164,7 +164,7 @@ Create a file called `config.patch` with the following content:
 
 Now we can appply the patch:
 
-```{.sh}
+```sh
 sudo patch -p0 < config.patch
 ```
 
diff --git a/fortress/sensors.md b/fortress/sensors.md
index bfa6d5ccf4..2ac11c522a 100644
--- a/fortress/sensors.md
+++ b/fortress/sensors.md
@@ -384,14 +384,14 @@ Inside the main we subscribe to the `lidar` topic, and wait until the node is sh
 
 Download the [CMakeLists.txt](https://github.com/ignitionrobotics/docs/blob/master/fortress/tutorials/sensors/CMakeLists.txt), and in the same folder of `lidar_node` create `build/` directory:
 
-```{.sh}
+```sh
 mkdir build
 cd build
 ```
 
 Run cmake and build the code:
 
-```{.sh}
+```sh
 cmake ..
 make lidar_node
 ```
@@ -400,13 +400,13 @@ make lidar_node
 
 Run the node from terminal 1:
 
-```{.sh}
+```sh
 ./build/lidar_node
 ```
 
 Run the world from terminal 2:
 
-```{.sh}
+```sh
 ign gazebo sensor_tutorial.sdf
 ```
 
@@ -435,7 +435,7 @@ The first command is `ign gazebo sensor_tutorial.sdf` which launches the world.
 And the second command is `./build/lidar_node` which runs the `lidar_node`.
 Save the file as `sensor_launch.ign`, and then run it using the following command:
 
-```{.sh}
+```sh
 ign launch sensor_launch.ign
 ```
 
diff --git a/garden/install_osx_src.md b/garden/install_osx_src.md
index 7038e44fb6..3caba545cc 100644
--- a/garden/install_osx_src.md
+++ b/garden/install_osx_src.md
@@ -136,7 +136,7 @@ If you want to compile Gazebo Libraries in MacOS Catalina (10.15) you will need
 
 Create a file called `intern.patch` with the following content:
 
-```patch
+```diff
 --- intern.h    2019-12-16 18:17:08.000000000 +0100
 +++ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Ruby.framework/Headers/ruby/ruby/intern.h
 @@ -14,6 +14,10 @@
@@ -154,7 +154,7 @@ Create a file called `intern.patch` with the following content:
 
 Now we can apply the patch:
 
-```{.sh}
+```sh
 sudo patch -p0 < intern.patch
 ```
 
@@ -162,7 +162,7 @@ sudo patch -p0 < intern.patch
 
 Create a file called `config.patch` with the following content:
 
-```patch
+```diff
 --- config.h    2019-12-16 18:19:13.000000000 +0100
 +++ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Ruby.framework/Headers/ruby/ruby/config.h
 @@ -410,6 +410,6 @@
@@ -178,7 +178,7 @@ Create a file called `config.patch` with the following content:
 
 Now we can appply the patch:
 
-```{.sh}
+```sh
 sudo patch -p0 < config.patch
 ```
 
diff --git a/garden/migration_from_ignition.md b/garden/migration_from_ignition.md
index 07ad69c901..b0fe2a9f67 100644
--- a/garden/migration_from_ignition.md
+++ b/garden/migration_from_ignition.md
@@ -239,7 +239,7 @@ In `CMakeLists.txt` files (and their references in your source files!):
 
 **Variables and macro/function calls**
 
-```cpp
+```
 Find: IGN(ITION)?_GAZEBO
 Replace: GZ_SIM
 
@@ -255,7 +255,7 @@ Replace: gz_
 
 **Includes**
 
-```cpp
+```
 Find: include\(Ign
 Replace: include(Gz
 
@@ -271,7 +271,7 @@ Replace: gz_find_package(Gz-
 
 **Project Names**
 
-```cpp
+```
 Find: ignition-gazebo
 Replace: gz-sim
 
@@ -288,7 +288,7 @@ Replace: gz-
 
 Migrate source macros and environment variables
 
-```cpp
+```
 Find: IGN(ITION)?_GAZEBO
 Replace: GZ_SIM
 
@@ -319,7 +319,7 @@ Additionally, the logging macros have also been migrated! Migrate any uses!
 
 In `.sdf` files:
 
-```cpp
+```
 Find: <ignition
 Replace: <gz
 
@@ -341,7 +341,7 @@ The plugin finder is able to find plugins even if their filenames are stripped o
 
 In `.sdf` files and source files (e.g. `.cc`):
 
-```cpp
+```
 Find: (lib)?ign(ition)?-gazebo([^. ]*)\.so
 Replace: gz-sim\3
 
@@ -359,7 +359,7 @@ Replace: gz::
 
 In Python files (e.g. `.py`)
 
-```cpp
+```
 Find: ignition.gazebo
 Replace: gz.sim
 
@@ -369,7 +369,7 @@ Replace: gz.
 
 In Ruby files (e.g. `.i`, `.rb`)
 
-```cpp
+```
 Find: ign(ition)?/
 Replace: gz/
 ```
@@ -378,7 +378,7 @@ Replace: gz/
 
 In your message definitions
 
-```cpp
+```
 Find: ign(ition)?\.gazebo
 Replace: gz.sim
 
@@ -398,7 +398,7 @@ Sweeping checks everywhere (pay special attention to reviewing these!)
 
 **Headers**
 
-```cpp
+```
 Find: #include\s*([<"])ign(ition)?/gazebo
 Replace: #include \1gz/sim
 
@@ -416,7 +416,7 @@ Replace: #endif  // GZ$1_H
 
 **Namespaces**
 
-```cpp
+```
 Find: namespace\s*ignition
 Replace: namespace gz
 
@@ -449,13 +449,13 @@ And also be mindful that certain instances of `gazebo` (usually as part of an AP
 
 Where you used to use:
 
-```cpp
+```
 ign gazebo shapes.sdf
 ```
 
 Now you should use:
 
-```cpp
+```
 gz sim shapes.sdf
 ```
 
diff --git a/garden/sensors.md b/garden/sensors.md
index 079e154233..fbf229c243 100644
--- a/garden/sensors.md
+++ b/garden/sensors.md
@@ -361,14 +361,14 @@ Inside the main we subscribe to the `lidar` topic, and wait until the node is sh
 
 Download the [CMakeLists.txt](https://github.com/gazebosim/docs/blob/master/garden/tutorials/sensors/CMakeLists.txt), and in the same folder of `lidar_node` create `build/` directory:
 
-```{.sh}
+```sh
 mkdir build
 cd build
 ```
 
 Run cmake and build the code:
 
-```{.sh}
+```sh
 cmake ..
 make lidar_node
 ```
@@ -377,13 +377,13 @@ make lidar_node
 
 Run the node from terminal 1:
 
-```{.sh}
+```sh
 ./build/lidar_node
 ```
 
 Run the world from terminal 2:
 
-```{.sh}
+```sh
 gz sim sensor_tutorial.sdf
 ```
 
@@ -412,7 +412,7 @@ The first command is `gz sim sensor_tutorial.sdf` which launches the world.
 And the second command is `./build/lidar_node` which runs the `lidar_node`.
 Save the file as `sensor_launch.gzlaunch`, and then run it using the following command:
 
-```{.sh}
+```sh
 gz launch sensor_launch.gzlaunch
 ```
 
diff --git a/get_started.md b/get_started.md
index bac67148b0..bfc8d40c6b 100644
--- a/get_started.md
+++ b/get_started.md
@@ -17,22 +17,22 @@ packages available for the platform to use:
 
 |Platform|Gazebo Versions|
 |---|---|
-| Ubuntu 22.04 Jammy | [Gazebo Harmonic](/docs/harmonic/install_ubuntu) (recommended), [Gazebo Garden](/docs/garden/install_ubuntu) and [Gazebo Fortress](/docs/fortress/install_ubuntu) (recommended if using ROS 2 Humble or Iron)
-| Ubuntu 20.04 Focal | [Gazebo Garden](/docs/garden/install_ubuntu) (recommended), [Gazebo Fortress](/docs/fortress/install_ubuntu) and [Gazebo Citadel](/docs/citadel/install_ubuntu)
-| Ubuntu 18.04 Bionic | [Gazebo Citadel](/docs/citadel/install_ubuntu)
-| Mac Ventura | [Gazebo Harmonic](/docs/harmonic/install_osx) (recommended), [Gazebo Garden](/docs/garden/install_osx), [Gazebo Fortress](/docs/fortress/install_osx) and [Gazebo Citadel](/docs/citadel/install_osx)
-| Mac Monterey | [Gazebo Harmonic](/docs/harmonic/install_osx) (recommended), [Gazebo Garden](/docs/garden/install_osx), [Gazebo Fortress](/docs/fortress/install_osx) and [Gazebo Citadel](/docs/citadel/install_osx)
+| Ubuntu 22.04 Jammy | [Gazebo Harmonic](/docs/harmonic/install_ubuntu){.external} (recommended), [Gazebo Garden](/docs/garden/install_ubuntu){.external} and [Gazebo Fortress](/docs/fortress/install_ubuntu){.external} (recommended if using ROS 2 Humble or Iron)
+| Ubuntu 20.04 Focal | [Gazebo Garden](/docs/garden/install_ubuntu){.external} (recommended), [Gazebo Fortress](/docs/fortress/install_ubuntu){.external} and [Gazebo Citadel](/docs/citadel/install_ubuntu){.external}
+| Ubuntu 18.04 Bionic | [Gazebo Citadel](/docs/citadel/install_ubuntu){.external}
+| Mac Ventura | [Gazebo Harmonic](/docs/harmonic/install_osx){.external} (recommended), [Gazebo Garden](/docs/garden/install_osx){.external}, [Gazebo Fortress](/docs/fortress/install_osx){.external} and [Gazebo Citadel](/docs/citadel/install_osx){.external}
+| Mac Monterey | [Gazebo Harmonic](/docs/harmonic/install_osx){.external} (recommended), [Gazebo Garden](/docs/garden/install_osx){.external}, [Gazebo Fortress](/docs/fortress/install_osx){.external} and [Gazebo Citadel](/docs/citadel/install_osx){.external}
 | Windows | Support via Conda-Forge is not fully functional, as there are known runtime issues [see this issue for details](https://github.com/gazebosim/gz-sim/issues/168).
 
 If the desired platform is not listed above or if a particular feature in a
-given [Gazebo release](/docs/latest/releases) is needed,
+given [Gazebo release](releases) is needed,
 there is an installation package per release available with all the
 installation options:
 
-* [Gazebo Harmonic installation](/docs/harmonic/install) options (EOL 2028 Sep)
-* [Gazebo Garden installation](/docs/garden/install) options (EOL 2024 Sep)
-* [Gazebo Fortress (LTS) installation](/docs/fortress/install) options (EOL 2026 Sep)
-* [Gazebo Citadel (LTS) installation](/docs/citadel/install) options (EOL 2024 Dec)
+* [Gazebo Harmonic installation](/docs/harmonic/install){.external} options (EOL 2028 Sep)
+* [Gazebo Garden installation](/docs/garden/install){.external} options (EOL 2024 Sep)
+* [Gazebo Fortress (LTS) installation](/docs/fortress/install){.external} options (EOL 2026 Sep)
+* [Gazebo Citadel (LTS) installation](/docs/citadel/install){.external} options (EOL 2024 Dec)
 
 ## Step 2: Run
 
@@ -95,14 +95,14 @@ custom SDF file.
 ## Step 4: Explore and learn
 
 This tutorial has covered the basics of getting started with Gazebo.
-Starting with Citadel, there are more [versioned tutorials](/docs/citadel/tutorials)
+Starting with Citadel, there are more [versioned tutorials](/docs/citadel/tutorials){.external}
 covering the basics of the GUI, creating worlds and robots, and more.
 
 Each [Gazebo library](/libs) also has a set of tutorials and
 examples. Explore these resources, and don't forget to ask questions and
 find solutions at [answers.gazebosim.org](http://answers.gazebosim.org).
 
-# macOS
+## macOS
 
 On macOS, you will need to run Gazebo using two terminals, one for the server
 and another for the GUI:
diff --git a/harmonic/install_windows_src.md b/harmonic/install_windows_src.md
index 59e28efec1..b3e3a3d5ff 100644
--- a/harmonic/install_windows_src.md
+++ b/harmonic/install_windows_src.md
@@ -144,7 +144,7 @@ page to start using Gazebo!
 
 > **NOTE**
 > As Gazebo GUI is not yet working, running `gz sim` will not work. You can run only the server with
-> ```cmd
+> ```bat
 > gz sim -s -v
 > ```
 
@@ -152,7 +152,7 @@ page to start using Gazebo!
 > If your username contains spaces (which is quite common on Windows), you will probably get errors
 >  saying `Invalid partition name [Computer:My User With Spaces]`. Fix this by changing `GZ_PARTITION`
 >  to something else:
-> ```cmd
+> ```bat
 > set GZ_PARTITION=test
 > ```
 > Remember to set the same partition in all other consoles.
diff --git a/harmonic/migration_from_ignition.md b/harmonic/migration_from_ignition.md
index 30e5da93ae..d630b85584 100644
--- a/harmonic/migration_from_ignition.md
+++ b/harmonic/migration_from_ignition.md
@@ -239,7 +239,7 @@ In `CMakeLists.txt` files (and their references in your source files!):
 
 **Variables and macro/function calls**
 
-```cpp
+```
 Find: IGN(ITION)?_GAZEBO
 Replace: GZ_SIM
 
@@ -255,7 +255,7 @@ Replace: gz_
 
 **Includes**
 
-```cpp
+```
 Find: include\(Ign
 Replace: include(Gz
 
@@ -271,7 +271,7 @@ Replace: gz_find_package(Gz-
 
 **Project Names**
 
-```cpp
+```
 Find: ignition-gazebo
 Replace: gz-sim
 
@@ -288,7 +288,7 @@ Replace: gz-
 
 Migrate source macros and environment variables
 
-```cpp
+```
 Find: IGN(ITION)?_GAZEBO
 Replace: GZ_SIM
 
@@ -319,7 +319,7 @@ Additionally, the logging macros have also been migrated! Migrate any uses!
 
 In `.sdf` files:
 
-```cpp
+```
 Find: <ignition
 Replace: <gz
 
@@ -341,7 +341,7 @@ The plugin finder is able to find plugins even if their filenames are stripped o
 
 In `.sdf` files and source files (e.g. `.cc`):
 
-```cpp
+```
 Find: (lib)?ign(ition)?-gazebo([^. ]*)\.so
 Replace: gz-sim\3
 
@@ -359,7 +359,7 @@ Replace: gz::
 
 In Python files (e.g. `.py`)
 
-```cpp
+```
 Find: ignition.gazebo
 Replace: gz.sim
 
@@ -369,7 +369,7 @@ Replace: gz.
 
 In Ruby files (e.g. `.i`, `.rb`)
 
-```cpp
+```
 Find: ign(ition)?/
 Replace: gz/
 ```
@@ -378,7 +378,7 @@ Replace: gz/
 
 In your message definitions
 
-```cpp
+```
 Find: ign(ition)?\.gazebo
 Replace: gz.sim
 
@@ -398,7 +398,7 @@ Sweeping checks everywhere (pay special attention to reviewing these!)
 
 **Headers**
 
-```cpp
+```
 Find: #include\s*([<"])ign(ition)?/gazebo
 Replace: #include \1gz/sim
 
@@ -416,7 +416,7 @@ Replace: #endif  // GZ$1_H
 
 **Namespaces**
 
-```cpp
+```
 Find: namespace\s*ignition
 Replace: namespace gz
 
@@ -449,13 +449,13 @@ And also be mindful that certain instances of `gazebo` (usually as part of an AP
 
 Where you used to use:
 
-```cpp
+```
 ign gazebo shapes.sdf
 ```
 
 Now you should use:
 
-```cpp
+```
 gz sim shapes.sdf
 ```
 
diff --git a/harmonic/sensors.md b/harmonic/sensors.md
index 24f3ea514b..e958298278 100644
--- a/harmonic/sensors.md
+++ b/harmonic/sensors.md
@@ -382,14 +382,14 @@ Inside the main we subscribe to the `lidar` topic, and wait until the node is sh
 
 Download the [CMakeLists.txt](https://github.com/gazebosim/docs/blob/master/harmonic/tutorials/sensors/CMakeLists.txt), and in the same folder of `lidar_node` create `build/` directory:
 
-```{.sh}
+```sh
 mkdir build
 cd build
 ```
 
 Run cmake and build the code:
 
-```{.sh}
+```sh
 cmake ..
 make lidar_node
 ```
@@ -398,13 +398,13 @@ make lidar_node
 
 Run the node from terminal 1:
 
-```{.sh}
+```sh
 ./build/lidar_node
 ```
 
 Run the world from terminal 2:
 
-```{.sh}
+```sh
 gz sim sensor_tutorial.sdf
 ```
 
@@ -433,7 +433,7 @@ The first command is `gz sim sensor_tutorial.sdf` which launches the world.
 And the second command is `./build/lidar_node` which runs the `lidar_node`.
 Save the file as `sensor_launch.gzlaunch`, and then run it using the following command:
 
-```{.sh}
+```sh
 gz launch sensor_launch.gzlaunch
 ```
 
diff --git a/index.yaml b/index.yaml
index 15dfd4d34e..d77496a73a 100644
--- a/index.yaml
+++ b/index.yaml
@@ -97,9 +97,48 @@ pages:
         title: What is Fair Use
         file: fuel/fair_use.md
 releases:
+  - name: ionic
+    lts: false
+    eol: false
+    dev: true
+    description: Supported Sep, 2024 to Sep, 2026
+    libraries:
+      - name: cmake
+        version: 4
+      - name: common
+        version: 6
+      - name: fuel_tools
+        version: 10
+      - name: gui
+        version: 9
+      - name: launch
+        version: 8
+      - name: math
+        version: 8
+      - name: msgs
+        version: 11
+      - name: physics
+        version: 8
+      - name: plugin
+        version: 3
+      - name: rendering
+        version: 9
+      - name: sensors
+        version: 9
+      - name: sim
+        version: 9
+      - name: tools
+        version: 2
+      - name: transport
+        version: 14
+      - name: utils
+        version: 3
+      - name: sdformat
+        version: 15
   - name: harmonic
     lts: true
     eol: false
+    preferred: true # Only one preferred=true is allowed
     description: Supported Sep, 2023 to Sep, 2028
     libraries:
       - name: cmake
@@ -385,4 +424,38 @@ releases:
         version: 6
       - name: sdformat
         version: 8
-
+# This dictionary is used to supply the description of each library and any other
+# information about each library that doesn't change between releases
+library_info:
+  cmake:
+    description: Provides modules that are used to find dependencies of Gazebo projects and generate cmake targets for consumers of Gazebo projects to link against.
+  common:
+    description: A collection of useful classes and functions for handling many command tasks. This includes parsing 3D mesh files, managing console output, and using PID controllers.
+  fuel_tools:
+    description: A C++ client library and command line tools for interacting with Gazebo Fuel servers
+  gui:
+    description: A framework for graphical user interfaces centered around QT. Each component in Gazebo GUI is an independent plugin
+  launch:
+    description: Launch is a system that runs and manages plugins and programs. A configuration script can be used to specify which programs and plugins to run. Alternatively, individual programs and plugins can be run from the command line.
+  math:
+    description: A small, fast, and high performance math library. This library is a self-contained set of classes and functions suitable for robot applications.
+  msgs:
+    description: Standard set of message definitions, used by Gazebo Transport, and other applications.
+  physics:
+    description: A plugin based interface to physics engines, such as ODE, Bullet, and DART.
+  plugin:
+    description: A plugin loading library
+  rendering:
+    description: A plugin based interface to rendering engines, such as OGRE and Optix.
+  sensors:
+    description: A large set of sensor and noise models suitable for generating realistic data in simulation.
+  sim:
+    description: Gazebo simulates multiple robots in a 3D environment, with extensive dynamic interaction between objects.
+  tools:
+    description: Gazebo tools provides the gz command line tool that accepts multiple subcommands.
+  transport:
+    description: The transport library combines ZeroMQ with Protobufs to create a fast and efficient message passing system. Asynchronous message publication and subscription is provided along with service calls and discovery.
+  sdformat:
+    description: Simulation Description Format parser and description files.
+  utils:
+    description: General purpose classes and functions with minimal dependencies. It includes command line parsing, a helper class to implement the PIMPL pattern, macros to suppress warnings, etc.
diff --git a/ionic/install_windows_src.md b/ionic/install_windows_src.md
index 3506a2ab5c..2bf62c2603 100644
--- a/ionic/install_windows_src.md
+++ b/ionic/install_windows_src.md
@@ -144,7 +144,7 @@ page to start using Gazebo!
 
 > **NOTE**
 > As Gazebo GUI is not yet working, running `gz sim` will not work. You can run only the server with
-> ```cmd
+> ```batch
 > gz sim -s -v
 > ```
 
@@ -152,7 +152,7 @@ page to start using Gazebo!
 > If your username contains spaces (which is quite common on Windows), you will probably get errors
 >  saying `Invalid partition name [Computer:My User With Spaces]`. Fix this by changing `GZ_PARTITION`
 >  to something else:
-> ```cmd
+> ```batch
 > set GZ_PARTITION=test
 > ```
 > Remember to set the same partition in all other consoles.
diff --git a/ionic/migration_from_ignition.md b/ionic/migration_from_ignition.md
index 6798db1d87..5d0eafa300 100644
--- a/ionic/migration_from_ignition.md
+++ b/ionic/migration_from_ignition.md
@@ -239,7 +239,7 @@ In `CMakeLists.txt` files (and their references in your source files!):
 
 **Variables and macro/function calls**
 
-```cpp
+```
 Find: IGN(ITION)?_GAZEBO
 Replace: GZ_SIM
 
@@ -255,7 +255,7 @@ Replace: gz_
 
 **Includes**
 
-```cpp
+```
 Find: include\(Ign
 Replace: include(Gz
 
@@ -271,7 +271,7 @@ Replace: gz_find_package(Gz-
 
 **Project Names**
 
-```cpp
+```
 Find: ignition-gazebo
 Replace: gz-sim
 
@@ -288,7 +288,7 @@ Replace: gz-
 
 Migrate source macros and environment variables
 
-```cpp
+```
 Find: IGN(ITION)?_GAZEBO
 Replace: GZ_SIM
 
@@ -319,7 +319,7 @@ Additionally, the logging macros have also been migrated! Migrate any uses!
 
 In `.sdf` files:
 
-```cpp
+```
 Find: <ignition
 Replace: <gz
 
@@ -341,7 +341,7 @@ The plugin finder is able to find plugins even if their filenames are stripped o
 
 In `.sdf` files and source files (e.g. `.cc`):
 
-```cpp
+```
 Find: (lib)?ign(ition)?-gazebo([^. ]*)\.so
 Replace: gz-sim\3
 
@@ -359,7 +359,7 @@ Replace: gz::
 
 In Python files (e.g. `.py`)
 
-```cpp
+```
 Find: ignition.gazebo
 Replace: gz.sim
 
@@ -369,7 +369,7 @@ Replace: gz.
 
 In Ruby files (e.g. `.i`, `.rb`)
 
-```cpp
+```
 Find: ign(ition)?/
 Replace: gz/
 ```
@@ -378,7 +378,7 @@ Replace: gz/
 
 In your message definitions
 
-```cpp
+```
 Find: ign(ition)?\.gazebo
 Replace: gz.sim
 
@@ -398,7 +398,7 @@ Sweeping checks everywhere (pay special attention to reviewing these!)
 
 **Headers**
 
-```cpp
+```
 Find: #include\s*([<"])ign(ition)?/gazebo
 Replace: #include \1gz/sim
 
@@ -416,7 +416,7 @@ Replace: #endif  // GZ$1_H
 
 **Namespaces**
 
-```cpp
+```
 Find: namespace\s*ignition
 Replace: namespace gz
 
@@ -449,13 +449,13 @@ And also be mindful that certain instances of `gazebo` (usually as part of an AP
 
 Where you used to use:
 
-```cpp
+```
 ign gazebo shapes.sdf
 ```
 
 Now you should use:
 
-```cpp
+```
 gz sim shapes.sdf
 ```
 
diff --git a/ionic/sensors.md b/ionic/sensors.md
index c8afc7ed07..0d2d3705bd 100644
--- a/ionic/sensors.md
+++ b/ionic/sensors.md
@@ -382,14 +382,14 @@ Inside the main we subscribe to the `lidar` topic, and wait until the node is sh
 
 Download the [CMakeLists.txt](https://github.com/gazebosim/docs/blob/master/ionic/tutorials/sensors/CMakeLists.txt), and in the same folder of `lidar_node` create `build/` directory:
 
-```{.sh}
+```sh
 mkdir build
 cd build
 ```
 
 Run cmake and build the code:
 
-```{.sh}
+```sh
 cmake ..
 make lidar_node
 ```
@@ -398,13 +398,13 @@ make lidar_node
 
 Run the node from terminal 1:
 
-```{.sh}
+```sh
 ./build/lidar_node
 ```
 
 Run the world from terminal 2:
 
-```{.sh}
+```sh
 gz sim sensor_tutorial.sdf
 ```
 
@@ -433,7 +433,7 @@ The first command is `gz sim sensor_tutorial.sdf` which launches the world.
 And the second command is `./build/lidar_node` which runs the `lidar_node`.
 Save the file as `sensor_launch.gzlaunch`, and then run it using the following command:
 
-```{.sh}
+```sh
 gz launch sensor_launch.gzlaunch
 ```
 
diff --git a/libs_conf.py b/libs_conf.py
new file mode 100644
index 0000000000..59518ed694
--- /dev/null
+++ b/libs_conf.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2024 Open Source Robotics Foundation
+#
+# 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.
+
+# Configuration file for the Sphinx documentation builder.
+#
+# For the full list of built-in configuration values, see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+import sys
+from pathlib import Path
+
+sys.path.append(str(Path(__file__).parent))
+
+# Import the base_conf.py and override settings for /libs
+from base_conf import * # noqa
+
+html_baseurl = f"{html_context['deploy_url']}/libs"  # noqa
+
+html_theme_options["use_edit_page_button"] = False
+html_theme_options["secondary_sidebar_items"] = []
diff --git a/release.md b/release.md
index 9286e877f1..9d66bcd3e3 100644
--- a/release.md
+++ b/release.md
@@ -233,4 +233,10 @@ During the Gazebo Garden development period, this packaage was
 to use stable and nightly binaries.
 It is customary to use nightly binaries for all unreleased package versions.
 
+```{toctree}
+:hidden:
+:maxdepth: 1
+:titlesonly:
 
+releasing/versioning_pre_nightly
+```
diff --git a/release_instructions.md b/release_instructions.md
index d157fd074a..927d756240 100644
--- a/release_instructions.md
+++ b/release_instructions.md
@@ -64,7 +64,7 @@ features), patch number (patches and bugfixes).
 
 **Bumping major number** of the version implies some work to have the
 [metadata](#metadata-for-releasing) updated correctly. There is a [dedicated
-document](releasing/bump_major) that you should go through before continuing to work through the steps in this
+document](releasing/bump_major.md) that you should go through before continuing to work through the steps in this
 document.
 
    1. To update the upstream version a local checkout of the Gz library is
diff --git a/releasing/bump_major.md b/releasing/bump_major.md
index fea81aad14..8b37507478 100644
--- a/releasing/bump_major.md
+++ b/releasing/bump_major.md
@@ -1,3 +1,6 @@
+---
+orphan: true
+---
 # Bump major versions
 
 > WARNING: this document is no more than a list of steps. Check with the infra-team
diff --git a/releasing/release_repositories.md b/releasing/release_repositories.md
index bddbdf3d1b..52b7157bca 100644
--- a/releasing/release_repositories.md
+++ b/releasing/release_repositories.md
@@ -1,3 +1,6 @@
+---
+orphan: true
+---
 # Release repositories
 
 > TODO: the document needs to be completed with real information. The points
diff --git a/releasing/versioning_pre_nightly.md b/releasing/versioning_pre_nightly.md
index 80e7b36fcc..ed90409899 100644
--- a/releasing/versioning_pre_nightly.md
+++ b/releasing/versioning_pre_nightly.md
@@ -1,4 +1,4 @@
-## Debian/Ubuntu versioning in nightly and prerelease binaries
+# Debian/Ubuntu versioning in nightly and prerelease binaries
 
 Binary packages produced for prerelease and nightly builds have some
 particularities to establish the priority among them nicely.
@@ -17,7 +17,7 @@ this precedence, the nighlty version uses the trick of setting the version to
 `{X-1.99.99}` (i.e: if the version to release is `9.0.0`, the nightlies used before
 the version will use `8.99.99`).
 
-### Version schemes
+## Version schemes
 
 **Prerelease** versioning scheme: `{upcoming_version}~pre{prerelease_version}`
 
@@ -39,7 +39,7 @@ the version will use `8.99.99`).
  * `nightly_revision`:  revision number to apply to the nightly. It is also
    used to generate a new nightly using the same date timestamp.
 
-### Versions when mixing stable, prerelease and nightly
+## Versions when mixing stable, prerelease and nightly
 
 Which version has priority when using prerelease and stable repositories?
 
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000..78dc33dcab
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,7 @@
+myst-parser[linkify]
+pydata-sphinx-theme
+pyyaml
+sphinx
+sphinx-copybutton
+sphinx-design
+requests
diff --git a/ros_installation.md b/ros_installation.md
index 40eb95fb2d..5518ad1780 100644
--- a/ros_installation.md
+++ b/ros_installation.md
@@ -98,9 +98,9 @@ versions that have the same major number (`gz-sim7_7.0.0`, `gz-sim7_7.1.0`,
 `gz-sim7_7.0.1`, ...) are binary compatible and thus interchangeable with a
 given ROS distro.
 
-## Installing Gazebo
+### Installing Gazebo
 
-### Gazebo Packages for Ubuntu
+#### Gazebo Packages for Ubuntu
 
 The easiest way of installing Gazebo on Ubuntu is to use binary packages. There
 are two main repositories that host Gazebo simulator and Gazebo libraries: one
@@ -254,7 +254,7 @@ Getting the latest versions of the Gazebo libraries and simulator is as easy
 as installing the [`osrfoundation.org` repository](https://gazebosim.org/docs/latest/install_ubuntu_src#install-dependencies)
 together with the ROS repository. Updates should be fully compatible.
 
-## FAQ
+### FAQ
 
 #### I am not using ROS at all, which version should I use?
 
diff --git a/tutorials.yaml b/tutorials.yaml
deleted file mode 100644
index 72ec21e964..0000000000
--- a/tutorials.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-# This file is an index of the pages to display on the documentation website
-# (https://gazebosim.org/tutorials). The order of the pages in this file
-# is reflected on the website's left sidebar.
-
-releases:
-  - citadel
-  - blueprint
-  - acropolis