Subject: [pgjdbc/pgjdbc] PR #4088: docs: structure-only rework (file moves, Hugo skeleton, release pipeline)

From: "vlsi (@vlsi)" <[email-masked]>
Date: Sat, 23 May 2026 08:22:34 +0000

Documentation rework, structure only: file moves, Hugo template skeleton, lunr-based site search, front-matter-driven release pipeline, and CI wiring. Prose is byte-identical to master in every page that this PR touches.

## What and why

The documentation grew around a flat, historically-shaped set of files. Important topics like connection setup, SSL/TLS, prepared statements, and PostgreSQL extensions were either buried in oversized pages or hard to discover from the navigation. The release-presentation side had a parallel problem: home page, download page, and per-version cards were each driven by their own hand-maintained config files (`versions.toml`, `homepagedata.toml`), so cutting a release meant editing several files in lockstep.

This PR sets up the new shape of the site so the prose work that follows has somewhere to land:

* reorganises `docs/content/documentation/` into task-oriented sections (`connect`, `data-types`, `getting-started`, `postgresql-features`, `query`, `runtime`, `security`);
* slices the four largest legacy pages (`use.md`, `setup.md`, `server-prepare.md`, `query.md`) at their H2 boundaries into smaller topical pages, prose verbatim;
* installs the Hugo template skeleton (layouts, partials, shortcodes), the SCSS bundle, and the lunr site search with its camelCase / snake_case tokeniser;
* replaces `versions.toml` / `homepagedata.toml` with a release pipeline that reads `/changelogs/*-release.md` front-matter directly, plus a `release-history-overlay.yaml` for the facts that cannot be derived from git;
* wires `:docs-tools:buildDocs` and `:docs-tools:serveDocs` into a Gradle module and adds the GitHub Pages deploy workflow.

## Reviewing this PR

There is no prose to read in this PR. The check is mechanical.

1. Build locally:
   ```
   ./gradlew --quiet :docs-tools:serveDocs
   ```
   Open <http://localhost:1313/> and follow links from the home page through the sidebar. Every page renders, every legacy URL under `/documentation/<old-path>/` resolves via meta-refresh to its new home, and fragment-preserving redirects work too: `/documentation/use/#unix-sockets` lands on `/documentation/connect/unix-sockets/`.

2. `lintDocsLinks` runs as part of `buildDocs` and fails the build on root-relative `href="/..."` patterns that would skip Hugo's BasePath. The Pages workflow runs the same check on every PR.

3. Pick any row from the provenance map below and run the matching diff recipe. Empty output means the new page is byte-identical to the cited master range.

<details>
<summary><b>Provenance map: where every page comes from</b> (and how to verify byte-equality)</summary>

Every new or renamed page in this PR has a known master source. Type `move` is a whole-file rename: the body is byte-identical to the master file; only the front-matter's `aliases:` field is amended to keep the legacy URL alive. Type `slice` extracts a contiguous range of master lines into a new file with title-only front-matter; the one master line dropped per slice is the H2 whose text becomes the new page's `title:`. Where the master lines column lists two ranges, the first is the master file's intro paragraphs (everything between the front-matter and the first H2), preserved as a preface to the topical body.

### Whole-file renames (8)

| New page | Master source | Type |
|---|---|---|
| `documentation/connect/datasource.md` | `documentation/datasource.md` | move |
| `documentation/data-types/binary-bytea.md` | `documentation/binary-data.md` | move |
| `documentation/query/stored-procedures.md` | `documentation/callproc.md` | move |
| `documentation/query/jdbc-escapes.md` | `documentation/escapes.md` | move |
| `documentation/runtime/logging.md` | `documentation/logging.md` | move |
| `documentation/runtime/reading.md` | `documentation/reading.md` | move |
| `documentation/runtime/thread.md` | `documentation/thread.md` | move |
| `documentation/security/ssl-tls.md` | `documentation/ssl.md` | move |

### Slices from `documentation/use.md` (516 lines on master)

| New page | Master lines |
|---|---|
| `documentation/getting-started/importing-jdbc.md` | 11–12, 14–24 |
| `documentation/getting-started/loading-the-driver.md` | 26–34 |
| `documentation/connect/url-syntax.md` | 36–475 |
| `documentation/connect/unix-sockets.md` | 477–496 |
| `documentation/connect/failover.md` | 498–516 |

### Slices from `documentation/setup.md` (70 lines on master)

| New page | Master lines |
|---|---|
| `documentation/getting-started/getting-the-driver.md` | 11–12, 14–45 |
| `documentation/getting-started/setting-up-the-class-path.md` | 47–60 |
| `documentation/getting-started/server-prep.md` | 62–70 |

The `server-prep.md` slice spans both the H2 `## Preparing the Database Server for JDBC` (consumed by `title:`) and the in-body H2 `## Creating a Database` that follows; the second H2 stays in the rendered body as a section heading.

### Slices from `documentation/query.md` (223 lines on master)

| New page | Master lines |
|---|---|
| `documentation/query/fetch-size.md` | 44–97 |
| `documentation/query/basics.md` | 11–42, 99–175 |
| `documentation/data-types/date-time.md` | 177–223 |

The `query/basics.md` slice includes the master file's intro paragraphs and the "Example 5.1" code block (lines 11–42), then continues with the body of `## Using the Statement or PreparedStatement Interface` (lines 99–175). The in-body H2s `## Using the ResultSet Interface`, `## Performing Updates`, and `## Creating and Modifying Database Objects` are preserved as section headings inside the page.

### Slices from `documentation/server-prepare.md` (1020 lines on master)

| New page | Master lines |
|---|---|
| `documentation/postgresql-features/extensions-api.md` | 8–10, 12–21 |
| `documentation/data-types/infinity.md` | 23–43 |
| `documentation/data-types/geometric.md` | 45–94 |
| `documentation/data-types/large-objects.md` | 96–106 |
| `documentation/postgresql-features/listen-notify.md` | 108–206 |
| `documentation/query/prepared-statements.md` | 208–526 |
| `documentation/postgresql-features/parameter-status.md` | 528–569 |
| `documentation/postgresql-features/replication.md` | 571–938 |
| `documentation/data-types/arrays.md` | 940–962 |
| `documentation/postgresql-features/copy.md` | 964–1020 |

The `parameter-status.md` slice spans the four consecutive master H2s `## Parameter Status Messages`, `## Methods`, `## Example`, `## Other client drivers` (the first becomes `title:`, the rest stay as in-body H2 sub-sections). The `replication.md` slice does the same for `## Physical and Logical replication API` plus its three follow-on H2s (`## Configure database`, `## Logical replication`, `## Physical replication`).

### How to verify any row

The new files follow Hugo's convention of one blank line between the closing `---` of the front-matter and the body. The recipes below pipe the new file through `awk` (strip the front-matter) and `tail -n +2` (drop that conventional blank); the resulting body is byte-identical to the master source named in the table.

`move` row (whole-file rename; awk both sides, no `tail` needed since master files follow the same convention):

```
diff \
  <(git show master:docs/content/documentation/ssl.md | awk '/^---$/{c++;if(c==2){p=1;next}} p') \
  <(awk '/^---$/{c++;if(c==2){p=1;next}} p' docs/content/documentation/security/ssl-tls.md)
```

`slice` row, single master range (`sed -n 'A,Bp'` on master, awk+tail on new):

```
diff \
  <(git show master:docs/content/documentation/server-prepare.md | sed -n '96,106p') \
  <(awk '/^---$/{c++;if(c==2){p=1;next}} p' docs/content/documentation/data-types/large-objects.md | tail -n +2)
```

`slice` row, two master ranges (chain ranges in `sed` with `;`):

```
diff \
  <(git show master:docs/content/documentation/query.md | sed -n '11,42p;99,175p') \
  <(awk '/^---$/{c++;if(c==2){p=1;next}} p' docs/content/documentation/query/basics.md | tail -n +2)
```

Empty output means the new page is byte-identical to the cited master range. Any non-empty output is a drift the reviewer can flag.

For a holistic visual sweep, `git diff master..HEAD -- docs/content/ --color-moved=blocks --color-moved-ws=allow-indentation-change | less -R` colours every line as "moved" (a block deleted in one file and added in another) versus "modified". Every line of every slice should render as moved.

</details>

## Build the site locally

```
./gradlew --quiet :docs-tools:serveDocs
```

Then open <http://localhost:1313/>. The dev server hot-reloads on every change under `docs/`.

diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 42f8c114c7..075f5c978d 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -20,20 +20,72 @@ jobs:
     steps:
       - name: Checkout
         uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
-        
+        with:
+          # :docs-tools:generateReleaseHistory enumerates REL42.* tags and
+          # release/42.*.x branches via JGit to build the compatibility
+          # matrix. A shallow checkout would surface only `master` and
+          # produce an empty table on the published site.
+          fetch-depth: 0
+          fetch-tags: true
+
+      # Hugo (extended) goes on PATH; the :docs-tools:buildDocs Gradle task
+      # invokes it via Exec after the release-history generator has produced
+      # the data files. Keeping the dedicated Hugo install step (instead of
+      # e.g. `brew install hugo`) lets us pin a tested release without
+      # relying on whatever the runner happens to ship.
       - name: Setup Hugo
         uses: peaceiris/actions-hugo@2752ce1d29631191ea3f27c23495fa06139a5b78 # v3.2.1
         with:
           hugo-version: 'latest'
           extended: true
-          
+
+      # JDK is required for the docs build: :docs-tools:generateReleaseHistory
+      # walks the REL42.* tags via JGit and emits docs/data/release-history.yaml
+      # before Hugo runs. Same JDK as the main pipeline.
+      - name: 'Set up JDK 21'
+        uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+        with:
+          distribution: zulu
+          java-version: 21
+
+      # enablement: true lets a fresh fork run the workflow without first
+      # toggling Settings → Pages by hand; the action calls the Pages REST
+      # API (requires the `pages: write` permission declared above) and
+      # creates the site in "build from GitHub Actions" mode if missing.
+      # The `id: pages` is consumed below to forward the auto-detected
+      # base URL into Hugo.
       - name: Setup Pages
+        id: pages
         uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
-        
+        with:
+          enablement: true
+
+      # :docs-tools:buildDocs = :docs-tools:generateReleaseHistory then
+      # `hugo --gc --minify` in docs/. See docs-tools/build.gradle.kts.
+      #
+      # HUGO_BASEURL is read by :docs-tools:buildDocs and forwarded to
+      # hugo as `--baseURL` (CLI flag — config.toml's baseURL is the
+      # fallback for forks that don't override it). Order of precedence:
+      #   1. repo variable WEBSITE_BASE_URL (lets pgjdbc/pgjdbc keep
+      #      pinning the canonical https://jdbc.postgresql.org/ URL
+      #      without touching code);
+      #   2. otherwise the URL that GitHub considers "home" for this
+      #      repo, taken from actions/configure-pages — handles project
+      #      sites, user/org sites, and custom-domain (CNAME) cases.
+      # If configure-pages reports `http://` (happens on freshly
+      # enabled sites whose HTTPS cert isn't ready), turn on
+      # Settings → Pages → Enforce HTTPS in the repo; we don't want to
+      # paper over a security setting from CI.
       - name: Build
-        run: hugo --minify
-        working-directory: ./docs
-        
+        uses: burrunan/gradle-cache-action@663fbad34e03c8f12b27f4999ac46e3d90f87eca # v3.0.1
+        env:
+          HUGO_BASEURL: ${{ vars.WEBSITE_BASE_URL || steps.pages.outputs.base_url }}
+          S3_BUILD_CACHE_ACCESS_KEY_ID: ${{ secrets.S3_BUILD_CACHE_ACCESS_KEY_ID }}
+          S3_BUILD_CACHE_SECRET_KEY: ${{ secrets.S3_BUILD_CACHE_SECRET_KEY }}
+        with:
+          job-id: docs-build
+          arguments: --no-parallel --no-daemon --scan :docs-tools:buildDocs
+
       - name: Upload artifact
         uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
         with:
diff --git a/docs-tools/build.gradle.kts b/docs-tools/build.gradle.kts
new file mode 100644
index 0000000000..3a90f6b1f2
--- /dev/null
+++ b/docs-tools/build.gradle.kts
@@ -0,0 +1,424 @@
+/*
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+    id("build-logic.java-library")
+    id("build-logic.test-junit5")
+    id("org.jetbrains.kotlin.jvm")
+}
+
+// docs-tools is a build-time-only utility module. It is NOT shipped with
+// the driver jar; nothing here is on the runtime classpath of pgjdbc.
+
+dependencies {
+    implementation("org.jetbrains.kotlin:kotlin-stdlib")
+
+    // GenerateReleaseHistory walks git refs (release/*.x branches and
+    // REL42.* tags). JGit 7.x has first-class git-worktree support
+    // (commondir indirection) and a typed API — preferable to shelling
+    // out to `git`. JGit 7.x bytecode targets Java 17; pgjdbc builds on
+    // JDK 21, so the runtime is fine.
+    implementation("org.eclipse.jgit:org.eclipse.jgit:7.5.0.202512021534-r")
+
+    // SLF4J binding for JGit. Without one, SLF4J 2.x prints a per-JVM
+    // "No SLF4J providers were found" warning and silently routes JGit's
+    // diagnostics to NOP. slf4j-simple writes to stderr with no config
+    // file; tune via -Dorg.slf4j.simpleLogger.defaultLogLevel=warn.
+    runtimeOnly("org.slf4j:slf4j-simple:2.0.17")
+
+    // snakeyaml drives the release-history YAML emitter and parses the
+    // hand-maintained release-history-overlay.yaml.
+    implementation("org.yaml:snakeyaml:2.2")
+}
+
+// pgjdbc targets Java 8 bytecode via --release 8; pin Kotlin to the same
+// jvmTarget so the two compilers agree (Gradle aborts on a mismatch).
+kotlin {
+    compilerOptions {
+        jvmTarget.set(JvmTarget.JVM_1_8)
+        freeCompilerArgs.add("-Xjvm-default=all")
+    }
+}
+
+// docs-tools runs at build time on the project's buildJdk (Java 17+ in
+// practice — JGit 7.x already requires Java 17 at runtime). Its tests
+// freely use post-Java-8 APIs (e.g. ByteArrayOutputStream#toString(Charset),
+// since Java 10), so they cannot run on a Java 8 test toolchain even though
+// the produced bytecode is JVM 1.8.
+tasks.test {
+    onlyIf("docs-tools tests use post-Java-8 APIs (e.g. ByteArrayOutputStream#toString(Charset))") {
+        buildParameters.testJdkVersion > 8
+    }
+}
+
+// ===== generateReleaseHistory ============================================
+//
+// Scans the local clone's `release/<NN>.x` branches and `REL<NN>` tags,
+// merges with docs/data/release-history-overlay.yaml, emits
+// docs/data/release-history.yaml. Consumed by the `release-history`
+// Hugo shortcode on the compatibility page.
+//
+// For CI to produce a complete table the checkout must include all
+// `REL42.*` tags and the `release/42.*.x` branches (full-history clone
+// or an explicit `git fetch --tags`).
+// We pass the project root (where `.git` lives, as either a directory in
+// a regular clone or a pointer file in a git worktree); JGit's findGitDir()
+// walks up from there and follows the pointer in the worktree case.
+val projectRoot = isolated.rootProject.projectDirectory.asFile
+val releaseHistoryOverlay =
+    isolated.rootProject.projectDirectory.dir("docs/data")
+        .file("release-history-overlay.yaml")
+val releaseHistoryYaml =
+    isolated.rootProject.projectDirectory.dir("docs/data")
+        .file("release-history.yaml")
+
+val generateReleaseHistory by tasks.registering(JavaExec::class) {
+    group = "documentation"
+    description = "Generate docs/data/release-history.yaml from git refs " +
+        "(release/* branches, REL* tags) + release-history-overlay.yaml."
+
+    mainClass.set("org.postgresql.tools.docs.GenerateReleaseHistory")
+    classpath = sourceSets.main.get().runtimeClasspath
+    dependsOn(tasks.named("classes"))
+    dependsOn(tasks.named("processJandexIndex"))
+    // Karaf's :postgresql:generateKar declares the :postgresql jar as
+    // one of its outputs. In a full-graph CI build (`gradle jandex test
+    // jacocoReport`) generateKar runs and writes pgjdbc/build/libs/
+    // postgresql-<v>.jar, the same path :postgresql:jar produces.
+    // Gradle 9's strict overlap check then refuses to schedule any
+    // task that even touches that directory on its classpath without
+    // an explicit ordering. We do not actually consume the jar here,
+    // so mustRunAfter (not dependsOn) suffices: if generateKar is in
+    // the task graph it runs first, and if it is not (e.g. during
+    // local `serveDocs`) the constraint is a no-op.
+    mustRunAfter(":postgresql:generateKar")
+
+    argumentProviders.add(CommandLineArgumentProvider {
+        listOf(
+            projectRoot.absolutePath,
+            releaseHistoryOverlay.asFile.absolutePath,
+            releaseHistoryYaml.asFile.absolutePath
+        )
+    })
+
+    // The git directory's content drives the output, but Gradle cannot
+    // track .git efficiently — declare the overlay as the only file input
+    // and mark the task non-cacheable on the git side via outputs.upToDateWhen.
+    inputs.file(releaseHistoryOverlay).withPropertyName("overlay")
+    outputs.file(releaseHistoryYaml).withPropertyName("releaseHistoryYaml")
+    outputs.upToDateWhen { false }
+
+    standardOutput = System.out
+    errorOutput = System.err
+}
+
+// ----- Hugo wrappers -------------------------------------------------------
+//
+// buildDocs  — production build: regenerate release-history.yaml, then
+//              run a one-shot Hugo build.
+//
+// serveDocs  — local dev server: regenerate release-history.yaml, then
+//              start the Hugo dev server with hot-reload.
+//
+// Both require a recent extended Hugo on PATH. The doFirst hook fails
+// with a readable error if Hugo is missing, too old, or not the
+// extended build.
+
+val docsDir = isolated.rootProject.projectDirectory.dir("docs").asFile
+
+// The templates track Hugo's current API rather than its deprecated
+// surface. site.Language.Locale arrived in 0.158.0 (the release that
+// deprecated the older .LanguageCode) and hugo.Data in 0.156.0, so a
+// fixed floor is unavoidable. We pin above both and refuse to fall
+// back to deprecated APIs: contributors upgrade Hugo instead. Raise
+// this when the templates adopt a newer API; do not lower it to
+// accommodate an old local install.
+val minHugoVersion = "0.161.0"
+
+fun Exec.requireHugo() {
+    doFirst {
+        val versionLine = try {
+            val process = ProcessBuilder("hugo", "version")
+                .redirectErrorStream(true)
+                .start()
+            val output = process.inputStream.bufferedReader().use { it.readText() }
+            process.waitFor()
+            if (process.exitValue() != 0) {
+                throw GradleException(
+                    "`hugo version` returned exit code ${process.exitValue()}:\n$output"
+                )
+            }
+            output.trim()
+        } catch (e: java.io.IOException) {
+            throw GradleException(
+                "hugo binary is not on PATH. Install the extended Hugo " +
+                    "$minHugoVersion or newer: https://gohugo.io/installation/. " +
+                    "Underlying error: ${e.message}"
+            )
+        }
+
+        // `hugo version` prints e.g.
+        //   hugo v0.161.1+extended+withdeploy darwin/arm64 BuildDate=...
+        val match = Regex("""v(\d+)\.(\d+)\.(\d+)""").find(versionLine)
+            ?: throw GradleException("Could not parse a Hugo version from: $versionLine")
+        val found = match.destructured.toList().map(String::toInt)
+        val required = minHugoVersion.split(".").map(String::toInt)
+        val tooOld = found.zip(required)
+            .firstOrNull { (f, r) -> f != r }
+            ?.let { (f, r) -> f < r }
+            ?: false
+        if (tooOld) {
+            throw GradleException(
+                "This docs build needs the extended Hugo $minHugoVersion or " +
+                    "newer, but found ${match.value.removePrefix("v")}. The " +
+                    "templates use site.Language.Locale (Hugo 0.158.0+) and " +
+                    "hugo.Data (0.156.0+); we track the current API rather than " +
+                    "Hugo's deprecated surface. Upgrade Hugo: " +
+                    "https://gohugo.io/installation/."
+            )
+        }
+
+        // SCSS under docs/assets/sass/ is transpiled by Hugo Pipes, which
+        // only the extended build ships. A standard build fails mid-render
+        // with a less obvious message, so reject it up front.
+        if (!versionLine.contains("+extended")) {
+            throw GradleException(
+                "This docs build needs the *extended* Hugo build (SCSS " +
+                    "support), but found a standard build: $versionLine. " +
+                    "Reinstall the extended edition: " +
+                    "https://gohugo.io/installation/."
+            )
+        }
+    }
+}
+
+val buildDocs by tasks.registering(Exec::class) {
+    group = "documentation"
+    description = "Regenerate release-history.yaml, then run a production " +
+        "Hugo build into docs/public."
+    dependsOn(generateReleaseHistory)
+    workingDir = docsDir
+
+    // -PhugoBaseURL=… (or env HUGO_BASEURL) → `hugo --baseURL <value>`.
+    // The CLI flag wins over both config.toml and Hugo's env-override
+    // machinery, which has been unreliable for top-level keys since the
+    // 0.123 config rewrite. CI uses this to point a fork's deploy at
+    // <owner>.github.io/<repo>/ without editing config.toml; locally it
+    // can be set with `./gradlew :docs-tools:buildDocs -PhugoBaseURL=…`.
+    val hugoBaseUrl = providers.gradleProperty("hugoBaseURL")
+        .orElse(providers.environmentVariable("HUGO_BASEURL"))
+    argumentProviders.add(CommandLineArgumentProvider {
+        buildList {
+            add("--gc")
+            add("--minify")
+            if (hugoBaseUrl.isPresent) {
+                add("--baseURL")
+                add(hugoBaseUrl.get())
+            }
+        }
+    })
+    commandLine("hugo")
+    requireHugo()
+}
+
+val serveDocs by tasks.registering(Exec::class) {
+    group = "documentation"
+    description = "Start the Hugo dev server with hot-reload " +
+        "(release-history.yaml regenerated from git refs)."
+    dependsOn(generateReleaseHistory)
+    workingDir = docsDir
+
+    // -PdocsPort=NNNN overrides the default 1313.
+    val port = providers.gradleProperty("docsPort").orElse("1313")
+    argumentProviders.add(CommandLineArgumentProvider {
+        listOf(
+            "server", "--bind", "127.0.0.1", "--port", port.get(),
+            "--disableFastRender"
+        )
+    })
+    commandLine("hugo")
+    requireHugo()
+}
+
+// Lints source files for site-internal links that bypass Hugo's
+// BasePath. Three categories of mistake, all of which produce HTML
+// that breaks on a project-page deploy (https://<owner>.github.io/<repo>/)
+// but happens to "work" on https://jdbc.postgresql.org/ because the
+// BasePath is empty there:
+//
+//   1. Raw `href="/foo"` / `src="/foo"` in templates, not wrapped in
+//      `| relURL`. Hugo emits the path verbatim, missing `/<repo>/`.
+//
+//   2. `{{ "/foo" | relURL }}` with a leading slash. Hugo's relURL
+//      deliberately treats `/`-prefixed inputs as "you already know
+//      the absolute path" and does NOT prepend BasePath — so the
+//      result is identical to (1).
+//
+//   3. `url = "/foo"` in TOML / `url: "/foo"` in YAML data. The
+//      consumer template pipes the value through `| relURL`, but
+//      because the data still starts with `/`, the relURL is a no-op
+//      (same trap as (2)) — and the data file is then a quiet failure
+//      mode that's invisible from the template.
+//
+// Markdown content (`docs/content/**/*.md`) is intentionally NOT
+// linted: root-relative markdown links like `[text](/foo)` are the
+// idiomatic source-of-truth shape, and the `_default/_markup/render-link.html`
+// hook re-runs them through relURL at render time. We're betting that
+// no contributor will hand-write `<a href="/foo">` as raw HTML inside
+// a .md file; if that bet breaks, add a fourth check here.
+val lintDocsLinks by tasks.registering {
+    group = "verification"
+    description = "Lint docs sources for site-internal links that " +
+            "bypass Hugo's BasePath (root-relative href/src, leading-slash " +
+            "relURL arguments, `/`-prefixed URLs in data files). Run as a " +
+            "dependency of :docs-tools:buildDocs."
+    dependsOn(buildDocs)
+
+    val rootDirAbs = isolated.rootProject.projectDirectory
+    val layoutsDir = rootDirAbs.dir("docs/layouts").asFile
+    val dataDir = rootDirAbs.dir("docs/data").asFile
+    val menusFile = rootDirAbs.file("docs/config/_default/menus.toml").asFile
+    val repoRoot = rootDirAbs.asFile
+
+    inputs.dir(layoutsDir).withPropertyName("layouts")
+        .withPathSensitivity(PathSensitivity.RELATIVE)
+    inputs.dir(dataDir).withPropertyName("data")
+        .withPathSensitivity(PathSensitivity.RELATIVE)
+    inputs.file(menusFile).withPropertyName("menus")
+        .withPathSensitivity(PathSensitivity.RELATIVE)
+
+    val stamp = layout.buildDirectory.file("lint-docs-links.stamp")
+    outputs.file(stamp)
+
+    doLast {
+        val problems = mutableListOf<String>()
+
+        // Anchor on the literal `"` that opens the attribute value: if
+        // the very next char is `/` and the one after isn't `/` (so
+        // protocol-relative `//host` doesn't match), the value Hugo
+        // emits will start with `/` verbatim. Crucially this does NOT
+        // match `href="{{ $repo }}/edit/…"` — the first char inside
+        // the quotes is `{`, not `/` — so multi-piece templates that
+        // happen to interpolate a `/`-prefixed tail stay clean.
+        val rawRootAttr = Regex("""\b(href|src)\s*=\s*"(/[a-zA-Z0-9_\-][^"]*)"""")
+        // `{{ "/foo" | relURL }}` — leading `/` in the relURL arg
+        // makes Hugo skip BasePath.
+        val relURLLeadingSlash = Regex(
+            """\{\{[^}]*"\s*/[a-zA-Z0-9_\-][^"]*"[^}]*\|\s*[^}]*relURL[^}]*\}\}"""
+        )
+        // Multi-line Hugo comments: `{{- /* … */ -}}`. The single-line
+        // strip can't cover these; track open/close across lines and
+        // blank out the spanning region before matching.
+        val commentOpen = Regex("""\{\{-?\s*/\*""")
+        val commentClose = Regex("""\*/\s*-?\}\}""")
+
+        // render-link.html itself analyzes `/`-prefixed strings as its
+        // input, so the linter has to skip it (otherwise it'd flag the
+        // hook that fixes the very problem we're guarding against).
+        val renderLinkRelative = "_default/_markup/render-link.html"
+
+        layoutsDir.walkTopDown()
+            .filter { it.isFile && it.extension == "html" }
+            .filter {
+                !it.toRelativeString(layoutsDir).replace(File.separatorChar, '/')
+                    .equals(renderLinkRelative)
+            }
+            .forEach { f ->
+                val rel = f.relativeTo(repoRoot).invariantSeparatorsPath
+                var inComment = false
+                f.useLines { lines ->
+                    lines.forEachIndexed { idx, rawLine ->
+                        var line = rawLine
+                        if (inComment) {
+                            val close = commentClose.find(line)
+                            if (close != null) {
+                                line = line.substring(close.range.last + 1)
+                                inComment = false
+                            } else {
+                                return@forEachIndexed
+                            }
+                        }
+                        commentOpen.find(line)?.let { open ->
+                            val tail = line.substring(open.range.first)
+                            val close = commentClose.find(tail)
+                            if (close != null) {
+                                line = line.substring(0, open.range.first) +
+                                        tail.substring(close.range.last + 1)
+                            } else {
+                                line = line.substring(0, open.range.first)
+                                inComment = true
+                            }
+                        }
+
+                        rawRootAttr.findAll(line).forEach { m ->
+                            val attr = m.groupValues[1]
+                            val path = m.groupValues[2]
+                            problems += "$rel:${idx + 1}: ${m.value} — " +
+                                    "root-relative `$attr` bypasses Hugo's BasePath. " +
+                                    "Wrap in `{{ \"${path.trimStart('/')}\" | relURL }}`."
+                        }
+                        relURLLeadingSlash.findAll(line).forEach { m ->
+                            problems += "$rel:${idx + 1}: ${m.value} — " +
+                                    "leading `/` in relURL arg; strip it so Hugo " +
+                                    "prepends BasePath."
+                        }
+                    }
+                }
+            }
+
+        // Data files: bare `/foo` in `url = …` (TOML) / `url: …` (YAML).
+        val tomlRootURL = Regex("""^\s*url\s*=\s*"(/[a-zA-Z0-9_\-][^"]*)"""")
+        val yamlRootURL = Regex("""^\s*url\s*:\s*"?(/[a-zA-Z0-9_\-][^"\s]*)""")
+        val dataFiles = sequence {
+            yield(menusFile)
+            dataDir.walkTopDown().filter { it.isFile }.forEach { yield(it) }
+        }
+        dataFiles.filter { it.exists() }.forEach { f ->
+            val rel = f.relativeTo(repoRoot).invariantSeparatorsPath
+            val re = when (f.extension.lowercase()) {
+                "toml" -> tomlRootURL
+                "yaml", "yml" -> yamlRootURL
+                else -> return@forEach
+            }
+            f.useLines { lines ->
+                lines.forEachIndexed { idx, line ->
+                    re.find(line)?.let { m ->
+                        problems += "$rel:${idx + 1}: ${m.value.trim()} — " +
+                                "strip leading `/` so the consumer's `| relURL` " +
+                                "prepends BasePath."
+                    }
+                }
+            }
+        }
+
+        val stampFile = stamp.get().asFile
+        if (problems.isNotEmpty()) {
+            stampFile.delete()
+            throw GradleException(buildString {
+                appendLine(
+                    "Found ${problems.size} root-relative link(s) that " +
+                            "bypass Hugo's BasePath:"
+                )
+                problems.forEach { appendLine("  $it") }
+                appendLine()
+                appendLine(
+                    "Site-internal links must go through `relURL` so they " +
+                            "include the repo prefix on project-page deploys " +
+                            "(e.g. https://<owner>.github.io/<repo>/). Markdown " +
+                            "content is handled by the render-link hook; " +
+                            "templates and data files need the fix at the source."
+                )
+            })
+        }
+        stampFile.parentFile.mkdirs()
+        stampFile.writeText("OK\n")
+    }
+}
+
+tasks.check {
+    dependsOn(lintDocsLinks)
+}
diff --git a/docs-tools/src/main/kotlin/org/postgresql/tools/docs/GenerateReleaseHistory.kt b/docs-tools/src/main/kotlin/org/postgresql/tools/docs/GenerateReleaseHistory.kt
new file mode 100644
index 0000000000..b4a5e0eb82
--- /dev/null
+++ b/docs-tools/src/main/kotlin/org/postgresql/tools/docs/GenerateReleaseHistory.kt
@@ -0,0 +1,294 @@
+/*
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+@file:JvmName("GenerateReleaseHistory")
+
+package org.postgresql.tools.docs
+
+import org.eclipse.jgit.api.Git
+import org.eclipse.jgit.lib.ObjectId
+import org.eclipse.jgit.lib.Ref
+import org.eclipse.jgit.lib.Repository
+import org.eclipse.jgit.revwalk.RevWalk
+import org.yaml.snakeyaml.DumperOptions
+import org.yaml.snakeyaml.DumperOptions.FlowStyle
+import org.yaml.snakeyaml.DumperOptions.ScalarStyle
+import org.yaml.snakeyaml.Yaml
+import org.yaml.snakeyaml.nodes.MappingNode
+import org.yaml.snakeyaml.nodes.NodeTuple
+import org.yaml.snakeyaml.nodes.ScalarNode
+import org.yaml.snakeyaml.nodes.SequenceNode
+import org.yaml.snakeyaml.nodes.Tag
+import java.io.IOException
+import java.io.Writer
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.time.Instant
+import java.time.ZoneOffset
+import java.time.format.DateTimeFormatter
+import kotlin.system.exitProcess
+
+/**
+ * Generates `docs/data/release-history.yaml` from local git refs
+ * (release branches + release tags), merged with the hand-maintained
+ * `docs/data/release-history-overlay.yaml`.
+ *
+ * The output is a flat `rows` list with three columns per row:
+ *  - `release_line` (e.g. `"42.7.x"`),
+ *  - `version_range` (`"42.7.0-42.7.11"` for a line's main row,
+ *    `"42.2.29.jre7"` for a classifier row),
+ *  - `released` (ISO date of the latest tag in the row).
+ *
+ * Consumers: the `recent-versions` and `past-versions` Hugo shortcodes
+ * on the `/download/` page. Nothing else reads this file. The Java
+ * version of a classifier row is derived from the `.jreN` suffix in
+ * the template, so it is not part of the data.
+ *
+ * The overlay carries only what cannot be derived from git: the list
+ * of `.jre6` / `.jre7` classifier builds (id, branch, last_version).
+ */
+
+/** `refs/heads/release/42.X.x` or `refs/remotes/origin/release/42.X.x`. */
+private val BRANCH_PATTERN = Regex("""^refs/(?:heads|remotes/[^/]+)/release/(42\.\d+\.x)$""")
+
+/** Release tag. RC tags (REL42.7.11-rc1 etc.) are filtered out. */
+private val TAG_PATTERN = Regex("""^refs/tags/REL(42\.\d+\.\d+)$""")
+
+fun main(args: Array<String>) {
+    if (args.size < 3) {
+        System.err.println("usage: GenerateReleaseHistory " +
+            "<project-root> <overlay-yaml> <output-yaml>")
+        exitProcess(2)
+    }
+    val projectRoot = Paths.get(args[0])
+    val overlayYaml = Paths.get(args[1])
+    val outputYaml = Paths.get(args[2])
+
+    val facts = collectGitFacts(projectRoot)
+    val overlay = OverlayLoader.load(overlayYaml)
+    val rows = buildRows(facts, overlay)
+    ReleaseHistoryYamlEmitter.writeTo(rows, outputYaml)
+    println("Wrote ${rows.size} release-history rows to $outputYaml")
+}
+
+/* ===== Model =========================================================== */
+
+internal data class TaggedRelease(val version: String, val commitDate: String)
+
+internal data class GitFacts(
+    /** Branch ids, sorted descending: ["42.7.x", "42.6.x", …, "42.2.x"]. */
+    val branchIds: List<String>,
+    val tagsByBranch: Map<String, List<TaggedRelease>>,
+)
+
+/** One `.jre6` / `.jre7` classifier build. Emitted as an extra row under
+ *  its parent line. The `id` becomes the version-range suffix
+ *  (`<lastVersion>.<id>`); the template derives the Java version from
+ *  the suffix (`jre7` -> 7). */
+internal data class Classifier(
+    val branch: String,
+    val lastVersion: String,
+    val id: String,
+)
+
+internal data class Overlay(
+    val classifiers: List<Classifier> = emptyList(),
+)
+
+internal data class Row(
+    val releaseLine: String,
+    val versionRange: String,
+    val released: String,
+)
+
+/* ===== Git scan ======================================================== */
+
+internal fun collectGitFacts(projectRoot: Path): GitFacts =
+    Git.open(projectRoot.toFile()).use { scanRefs(it.repository) }
+
+private fun scanRefs(repo: Repository): GitFacts {
+    val refs = repo.refDatabase.refs
+
+    val branchIds = sortedSetOf(versionDescending())
+    for (ref in refs) {
+        BRANCH_PATTERN.matchEntire(ref.name)?.let { branchIds += it.groupValues[1] }
+    }
+
+    val tagsByBranch = mutableMapOf<String, MutableList<TaggedRelease>>()
+    RevWalk(repo).use { walk ->
+        for (ref in refs) {
+            val match = TAG_PATTERN.matchEntire(ref.name) ?: continue
+            val version = match.groupValues[1]
+            val branchId = toBranchId(version)
+            branchIds += branchId
+            val peeled = peelTag(repo, ref) ?: continue
+            val commit = walk.parseCommit(peeled)
+            tagsByBranch.getOrPut(branchId, ::mutableListOf)
+                .add(TaggedRelease(version, formatCommitDate(commit.commitTime)))
+        }
+    }
+    tagsByBranch.values.forEach { list -> list.sortBy { versionKey(it.version) } }
+
+    val orderedBranches = branchIds.toList()
+    val orderedTags = orderedBranches.associateWith { tagsByBranch[it].orEmpty() }
+    return GitFacts(orderedBranches, orderedTags)
+}
+
+private fun toBranchId(version: String): String {
+    val second = version.indexOf('.', version.indexOf('.') + 1)
+    return version.substring(0, second) + ".x"
+}
+
+private fun peelTag(repo: Repository, ref: Ref): ObjectId? {
+    val peeled = repo.refDatabase.peel(ref)
+    return peeled.peeledObjectId ?: ref.objectId
+}
+
+private fun formatCommitDate(epochSecond: Int): String =
+    DateTimeFormatter.ISO_LOCAL_DATE
+        .withZone(ZoneOffset.UTC)
+        .format(Instant.ofEpochSecond(epochSecond.toLong()))
+
+/* ===== Row building =================================================== */
+
+/**
+ * One row per release line spanning its first..last tag, plus one
+ * extra row per classifier listed in the overlay. Rows within a
+ * release line are sorted by `released` descending so the latest
+ * classifier appears next to the line's main row.
+ */
+internal fun buildRows(facts: GitFacts, overlay: Overlay): List<Row> {
+    val classifiersByBranch = overlay.classifiers.groupBy { it.branch }
+    val out = mutableListOf<Row>()
+    for (branchId in facts.branchIds) {
+        val tags = facts.tagsByBranch[branchId].orEmpty()
+        if (tags.isEmpty()) continue
+
+        val first = tags.first()
+        val last = tags.last()
+        val branchRows = mutableListOf<Row>()
+        branchRows += Row(
+            releaseLine = branchId,
+            versionRange = formatRange(first.version, last.version),
+            released = last.commitDate,
+        )
+
+        for (c in classifiersByBranch[branchId].orEmpty()) {
+            val date = tags.firstOrNull { it.version == c.lastVersion }?.commitDate.orEmpty()
+            branchRows += Row(
+                releaseLine = branchId,
+                versionRange = "${c.lastVersion}.${c.id}",
+                released = date,
+            )
+        }
+
+        // sortByDescending is stable, so a classifier with the same
+        // date as the line's main row stays after it (main row goes in
+        // first, classifier second).
+        branchRows.sortByDescending { it.released }
+        out += branchRows
+    }
+    return out
+}
+
+private fun formatRange(first: String, last: String): String =
+    if (first == last) last else "$first-$last"
+
+/* ===== Version helpers ================================================ */
+
+internal fun versionKey(version: String): Long {
+    val parts = version.split('.').take(5)
+    val padded = parts + List(5 - parts.size) { "0" }
+    return padded.fold(0L) { acc, raw ->
+        val part = if (raw == "x") "0" else raw
+        acc * 1000 + (part.toLongOrNull() ?: 0L)
+    }
+}
+
+internal fun versionDescending(): Comparator<String> =
+    compareByDescending(::versionKey)
+
+/* ===== Overlay loader (snakeyaml) ===================================== */
+
+internal object OverlayLoader {
+    @Suppress("UNCHECKED_CAST")
+    fun load(file: Path): Overlay {
+        if (!Files.isRegularFile(file)) {
+            System.err.println("Overlay not found at $file — proceeding with no overlay data.")
+            return Overlay()
+        }
+        val root: Map<String, Any?> = Files.newBufferedReader(file, StandardCharsets.UTF_8).use { r ->
+            (Yaml().load<Any?>(r) as? Map<String, Any?>)
+                ?: throw IOException("Overlay root must be a mapping in $file")
+        }
+        return Overlay(
+            classifiers = (root["classifiers"] as? List<Map<String, Any?>>).orEmpty().map {
+                Classifier(
+                    branch = it["branch"].toString(),
+                    lastVersion = it["last_version"].toString(),
+                    id = it["id"].toString(),
+                )
+            },
+        )
+    }
+}
+
+/* ===== YAML emitter (snakeyaml) ======================================= */
+
+internal object ReleaseHistoryYamlEmitter {
+    fun writeTo(rows: List<Row>, output: Path) {
+        Files.createDirectories(output.parent)
+        Files.newBufferedWriter(output, StandardCharsets.UTF_8).use { w ->
+            writeHeader(w)
+            Yaml(dumperOptions()).serialize(buildRoot(rows), w)
+        }
+    }
+
+    private fun dumperOptions() = DumperOptions().apply {
+        defaultFlowStyle = FlowStyle.BLOCK
+        indent = 2
+        indicatorIndent = 0
+        width = 80
+        splitLines = true
+        lineBreak = DumperOptions.LineBreak.UNIX
+    }
+
+    private fun writeHeader(w: Writer) {
+        w.write("# Generated by :docs-tools:generateReleaseHistory — DO NOT EDIT.\n")
+        w.write("# Source of truth: git refs (release/* branches, REL* tags)\n")
+        w.write("#                  + docs/data/release-history-overlay.yaml.\n")
+        w.write("# Run `./gradlew :docs-tools:generateReleaseHistory` to regenerate.\n")
+        w.write("\n")
+    }
+
+    private fun buildRoot(rows: List<Row>): MappingNode {
+        val tuple = NodeTuple(
+            ScalarNode(Tag.STR, "rows", null, null, ScalarStyle.PLAIN),
+            SequenceNode(Tag.SEQ, true, rows.map(::buildRow), null, null, FlowStyle.BLOCK),
+        )
+        return MappingNode(Tag.MAP, true, listOf(tuple), null, null, FlowStyle.BLOCK)
+    }
+
+    private fun buildRow(r: Row): MappingNode = MappingNode(
+        Tag.MAP,
+        true,
+        listOf(
+            field("release_line", r.releaseLine),
+            field("version_range", r.versionRange),
+            field("released", r.released),
+        ),
+        null, null, FlowStyle.BLOCK,
+    )
+
+    private fun field(key: String, value: String): NodeTuple = NodeTuple(
+        ScalarNode(Tag.STR, key, null, null, ScalarStyle.PLAIN),
+        // DOUBLE_QUOTED matches the prior hand-rolled emitter which
+        // always wrapped values in `"..."` so a future contributor knows
+        // every value is a string regardless of how it parses.
+        ScalarNode(Tag.STR, value, null, null, ScalarStyle.DOUBLE_QUOTED),
+    )
+}
diff --git a/docs-tools/src/test/kotlin/org/postgresql/tools/docs/GenerateReleaseHistoryTest.kt b/docs-tools/src/test/kotlin/org/postgresql/tools/docs/GenerateReleaseHistoryTest.kt
new file mode 100644
index 0000000000..a8cd2bd5e5
--- /dev/null
+++ b/docs-tools/src/test/kotlin/org/postgresql/tools/docs/GenerateReleaseHistoryTest.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.tools.docs
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+
+class GenerateReleaseHistoryTest {
+
+    /* ----- versionKey & ordering ----------------------------------------- */
+
+    @Test fun `versionKey orders multi-segment versions numerically`() {
+        // 42.2.10 must sort after 42.2.9, not lexicographically before it.
+        assertTrue(versionKey("42.2.10") > versionKey("42.2.9"))
+        assertTrue(versionKey("42.7.0") > versionKey("42.6.99"))
+        assertTrue(versionKey("42.10.0") > versionKey("42.9.99"))
+    }
+
+    @Test fun `versionKey treats x as zero in branch ids`() {
+        // Branch ids like "42.7.x" must sort consistently with their tags.
+        assertEquals(versionKey("42.7.0"), versionKey("42.7.x"))
+    }
+
+    @Test fun `versionDescending puts the highest version first`() {
+        val sorted = listOf("42.2.x", "42.7.x", "42.5.x").sortedWith(versionDescending())
+        assertEquals(listOf("42.7.x", "42.5.x", "42.2.x"), sorted)
+    }
+
+    /* ----- buildRows ----------------------------------------------------- */
+
+    @Test fun `buildRows emits one row per release line in branch order`() {
+        val facts = GitFacts(
+            branchIds = listOf("42.7.x", "42.6.x"),
+            tagsByBranch = mapOf(
+                "42.7.x" to listOf(
+                    TaggedRelease("42.7.0", "2024-01-01"),
+                    TaggedRelease("42.7.11", "2026-04-28"),
+                ),
+                "42.6.x" to listOf(
+                    TaggedRelease("42.6.0", "2023-03-17"),
+                    TaggedRelease("42.6.2", "2024-03-13"),
+                ),
+            ),
+        )
+        val rows = buildRows(facts, Overlay())
+        assertEquals(
+            listOf(
+                Row("42.7.x", "42.7.0-42.7.11", "2026-04-28"),
+                Row("42.6.x", "42.6.0-42.6.2", "2024-03-13"),
+            ),
+            rows,
+        )
+    }
+
+    @Test fun `buildRows collapses a single-tag line to that tag without a range`() {
+        val facts = GitFacts(
+            branchIds = listOf("42.8.x"),
+            tagsByBranch = mapOf("42.8.x" to listOf(TaggedRelease("42.8.0", "2026-06-01"))),
+        )
+        assertEquals(
+            listOf(Row("42.8.x", "42.8.0", "2026-06-01")),
+            buildRows(facts, Overlay()),
+        )
+    }
+
+    @Test fun `buildRows skips branches whose tag list is empty`() {
+        // A branch declared in branchIds without any tags (e.g. a release
+        // branch cut but never tagged) is silently dropped rather than
+        // emitting a row with an empty version_range.
+        val facts = GitFacts(
+            branchIds = listOf("42.9.x", "42.7.x"),
+            tagsByBranch = mapOf(
+                "42.9.x" to emptyList(),
+                "42.7.x" to listOf(TaggedRelease("42.7.0", "2024-01-01")),
+            ),
+        )
+        val rows = buildRows(facts, Overlay())
+        assertEquals(listOf("42.7.x"), rows.map { it.releaseLine })
+    }
+
+    @Test fun `buildRows appends classifier rows under their parent line`() {
+        val facts = GitFacts(
+            branchIds = listOf("42.2.x"),
+            tagsByBranch = mapOf(
+                "42.2.x" to listOf(
+                    TaggedRelease("42.2.0", "2018-01-17"),
+                    TaggedRelease("42.2.29", "2024-03-13"),
+                ),
+            ),
+        )
+        val overlay = Overlay(
+            classifiers = listOf(
+                Classifier(branch = "42.2.x", lastVersion = "42.2.29", id = "jre7"),
+                Classifier(branch = "42.2.x", lastVersion = "42.2.27", id = "jre6"),
+            ),
+        )
+        val rows = buildRows(facts, overlay)
+        assertEquals(
+            listOf("42.2.0-42.2.29", "42.2.29.jre7", "42.2.27.jre6"),
+            rows.map { it.versionRange },
+        )
+    }
+
+    @Test fun `buildRows sorts rows within a line by released descending`() {
+        // Main row and classifier rows on the same line are ordered by
+        // their `released` date, newest first.
+        val facts = GitFacts(
+            branchIds = listOf("42.2.x"),
+            tagsByBranch = mapOf(
+                "42.2.x" to listOf(
+                    TaggedRelease("42.2.27", "2023-01-01"),
+                    TaggedRelease("42.2.29", "2024-03-13"),
+                ),
+            ),
+        )
+        val overlay = Overlay(
+            classifiers = listOf(
+                Classifier(branch = "42.2.x", lastVersion = "42.2.27", id = "jre6"),
+            ),
+        )
+        val rows = buildRows(facts, overlay)
+        // Main row's `released` (2024) is newer than the classifier's
+        // (2023), so the main row stays first.
+        assertEquals(listOf("2024-03-13", "2023-01-01"), rows.map { it.released })
+    }
+
+    @Test fun `classifier without a matching tag gets an empty released date`() {
+        // Defensive: if a classifier's last_version is not in the tag list
+        // (typo or stale overlay), the row still emits with an empty date
+        // rather than crashing the build.
+        val facts = GitFacts(
+            branchIds = listOf("42.2.x"),
+            tagsByBranch = mapOf("42.2.x" to listOf(TaggedRelease("42.2.29", "2024-03-13"))),
+        )
+        val overlay = Overlay(
+            classifiers = listOf(
+                Classifier(branch = "42.2.x", lastVersion = "42.2.999", id = "jre7"),
+            ),
+        )
+        val classifierRow = buildRows(facts, overlay).single { "jre7" in it.versionRange }
+        assertEquals("", classifierRow.released)
+    }
+}
diff --git a/docs-tools/src/test/kotlin/org/postgresql/tools/docs/OverlayLoaderTest.kt b/docs-tools/src/test/kotlin/org/postgresql/tools/docs/OverlayLoaderTest.kt
new file mode 100644
index 0000000000..06f3849d7b
--- /dev/null
+++ b/docs-tools/src/test/kotlin/org/postgresql/tools/docs/OverlayLoaderTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.tools.docs
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+import java.nio.file.Path
+import kotlin.io.path.writeText
+
+class OverlayLoaderTest {
+
+    @TempDir lateinit var tmp: Path
+
+    @Test fun `missing overlay yields the default empty model`() {
+        val overlay = OverlayLoader.load(tmp.resolve("nope.yaml"))
+        assertEquals(emptyList<Classifier>(), overlay.classifiers)
+    }
+
+    @Test fun `classifiers block parses into the Overlay model`() {
+        val yaml = tmp.resolve("overlay.yaml")
+        yaml.writeText(
+            """
+            classifiers:
+              - {id: jre7, branch: 42.2.x, last_version: 42.2.29}
+              - {id: jre6, branch: 42.2.x, last_version: 42.2.27}
+            """.trimIndent()
+        )
+
+        val overlay = OverlayLoader.load(yaml)
+        assertEquals(
+            listOf(
+                Classifier(branch = "42.2.x", lastVersion = "42.2.29", id = "jre7"),
+                Classifier(branch = "42.2.x", lastVersion = "42.2.27", id = "jre6"),
+            ),
+            overlay.classifiers,
+        )
+    }
+
+    @Test fun `omitted classifiers section falls back to an empty list`() {
+        val yaml = tmp.resolve("overlay.yaml")
+        // An overlay containing only an unrelated top-level key should not
+        // throw — unknown keys are silently ignored, and the model stays
+        // at defaults.
+        yaml.writeText("future_field: ignored\n")
+
+        val overlay = OverlayLoader.load(yaml)
+        assertEquals(emptyList<Classifier>(), overlay.classifiers)
+    }
+}
diff --git a/docs/assets/sass/abstracts/_colors.scss b/docs/assets/sass/abstracts/_colors.scss
index 9a12cef5cf..1f2f982c9d 100644
--- a/docs/assets/sass/abstracts/_colors.scss
+++ b/docs/assets/sass/abstracts/_colors.scss
@@ -27,6 +27,40 @@ $colors: (
     200: hsl(0, 0%, 93%),
     300: hsl(0, 0%, 88%),
     400: hsl(0, 0%, 74%),
-    500: hsl(0, 0%, 52%)
+    500: hsl(0, 0%, 52%),
+    600: hsl(0, 0%, 32%),
+    700: hsl(0, 0%, 18%)
+  ),
+
+  info: (
+    100: hsl(210, 90%, 96%),
+    200: hsl(210, 85%, 88%),
+    300: hsl(210, 75%, 50%),
+    400: hsl(210, 80%, 38%),
+    500: hsl(210, 85%, 22%)
+  ),
+
+  success: (
+    100: hsl(145, 60%, 96%),
+    200: hsl(145, 55%, 85%),
+    300: hsl(145, 60%, 38%),
+    400: hsl(145, 70%, 28%),
+    500: hsl(145, 75%, 18%)
+  ),
+
+  warning: (
+    100: hsl(43, 100%, 95%),
+    200: hsl(43, 95%, 80%),
+    300: hsl(38, 92%, 50%),
+    400: hsl(32, 90%, 40%),
+    500: hsl(28, 90%, 25%)
+  ),
+
+  danger: (
+    100: hsl(0, 80%, 97%),
+    200: hsl(0, 70%, 88%),
+    300: hsl(0, 70%, 50%),
+    400: hsl(0, 75%, 40%),
+    500: hsl(0, 80%, 25%)
   ),
 );
\ No newline at end of file
diff --git a/docs/assets/sass/abstracts/_constants.scss b/docs/assets/sass/abstracts/_constants.scss
index 2678821d1f..d124442c66 100644
--- a/docs/assets/sass/abstracts/_constants.scss
+++ b/docs/assets/sass/abstracts/_constants.scss
@@ -1,10 +1,30 @@
 :root {
   --container-width: 62.5rem;
+  --prose-width: 46rem;
 
   --section-spacing: 3.5em;
   --container-spacing: 2.5em;
   --box-spacing: 2em;
   --primary-spacing: 1em;
+
+  --space-3xs: 0.25rem;
+  --space-2xs: 0.5rem;
+  --space-xs:  0.75rem;
+  --space-s:   1rem;
+  --space-m:   1.5rem;
+  --space-l:   2rem;
+  --space-xl:  3rem;
+
+  --radius-s: 0.25rem;
+  --radius-m: 0.5rem;
+
+  --border-subtle: 1px solid var(--clr-neutral-300);
+
+  /* Conservative fallback for the height of the sticky top navbar.
+     The actual measured height is published by static/js/navbar-height.js
+     and overrides this value on load. Other sticky elements (e.g.
+     .param-table thead) read --navbar-height to park flush below it. */
+  --navbar-height: 5rem;
 }
 
 $user: '\01F464';
diff --git a/docs/assets/sass/base/_base.scss b/docs/assets/sass/base/_base.scss
index d8acd6b108..73d50321b1 100644
--- a/docs/assets/sass/base/_base.scss
+++ b/docs/assets/sass/base/_base.scss
@@ -35,6 +35,7 @@ code {
 pre code {
     display: block;
     padding: 1em;
+    background-color: transparent;
     line-height: 2;
     overflow-x: auto;
 }
@@ -64,4 +65,4 @@ pre {
             margin-top: 3rem;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/docs/assets/sass/base/_syntax.scss b/docs/assets/sass/base/_syntax.scss
index 2969abb10a..2fcb7d79b5 100644
--- a/docs/assets/sass/base/_syntax.scss
+++ b/docs/assets/sass/base/_syntax.scss
@@ -1,6 +1,6 @@
 /* Background */
 .chroma {
-    background-color: #ffffff;
+    background-color: transparent;
 }
 
 /* Other */
@@ -429,4 +429,4 @@
 /* TextWhitespace */
 .chroma .w {
     color: #bbbbbb
-}
\ No newline at end of file
+}
diff --git a/docs/assets/sass/base/_typography.scss b/docs/assets/sass/base/_typography.scss
index ee0c4ebe49..3e455c83b4 100644
--- a/docs/assets/sass/base/_typography.scss
+++ b/docs/assets/sass/base/_typography.scss
@@ -55,7 +55,6 @@ a {
 
   &:hover,
   &:focus {
-    text-decoration: none;
     color: var(--clr-primary-300);
   }
 }
diff --git a/docs/assets/sass/components/_badge.scss b/docs/assets/sass/components/_badge.scss
new file mode 100644
index 0000000000..f291f602fd
--- /dev/null
+++ b/docs/assets/sass/components/_badge.scss
@@ -0,0 +1,43 @@
+.badge {
+  display: inline-flex;
+  align-items: center;
+  gap: var(--space-3xs);
+
+  padding: 0.1em 0.5em;
+  border-radius: 999px;
+
+  font-size: var(--fs-300);
+  font-weight: 600;
+  line-height: 1.4;
+  white-space: nowrap;
+
+  background: var(--clr-neutral-200);
+  color: var(--clr-neutral-700);
+
+  /* Version badges — readable text, never strikethrough. */
+  &--since {
+    background: var(--clr-success-100);
+    color:      var(--clr-success-500);
+  }
+
+  &--deprecated-in {
+    background: var(--clr-warning-100);
+    color:      var(--clr-warning-500);
+  }
+
+  &--hidden-in {
+    background: var(--clr-danger-100);
+    color:      var(--clr-danger-500);
+  }
+
+  /* Legacy aliases — used outside the param-table (since.html, deprecated.html). */
+  &--deprecated {
+    background: var(--clr-warning-100);
+    color:      var(--clr-warning-500);
+  }
+
+  &--removed {
+    background: var(--clr-danger-100);
+    color:      var(--clr-danger-500);
+  }
+}
diff --git a/docs/assets/sass/components/_button.scss b/docs/assets/sass/components/_button.scss
index 68346cdbe1..9dee74ae80 100644
--- a/docs/assets/sass/components/_button.scss
+++ b/docs/assets/sass/components/_button.scss
@@ -1,36 +1,60 @@
+/* The .btn classes apply directly to either a `<button>` element or
+   an `<a>` used for navigation. Legacy usages where `<a>` sits
+   INSIDE a `<button class="btn">` continue to work — the `a` selector
+   inherits the same colour / decoration so the rendered look is
+   identical regardless of which markup pattern a page uses.
+
+   Hover handling carries an extra responsibility: the global
+   `a { color: ...; } a:hover { color: primary-300; ... }` rule in
+   base/_typography.scss applies to every `<a>` on the site. With
+   the contained button using `<a class="btn--contained">`, the
+   default hover would flip text to primary-300 — the same colour as
+   the button's own hover background — so the text would visually
+   disappear. The button rules below re-pin colour / decoration in
+   hover and focus states explicitly. */
 .btn {
     border-radius: 100vmax;
     padding: 0.5em var(--primary-spacing);
     border-style: none;
     cursor: pointer;
     transition: 0.25s;
+    text-decoration: none;
+    display: inline-flex;
+    align-items: center;
+    font: inherit;
+
+    &:hover,
+    &:focus {
+        text-decoration: none;
+    }
 
     @include mq-max(small) {
-        a {
-            font-size: 1.05rem;
-        }
+        font-size: 1.05rem;
+
+        a { font-size: 1.05rem; }
     }
 
-    &--contained {
-        display: inline-flex;
-        align-items: start;
+    &--contained,
+    &--contained a {
         background-color: var(--clr-primary-400);
+        color: var(--clr-white);
 
-        a {
-            color: var(--clr-white);
-            padding-left: 4px;
-        }
-
-        &:hover {
+        &:hover,
+        &:focus {
             background-color: var(--clr-primary-300);
+            color: var(--clr-white);   // override global a:hover
         }
     }
 
-    &--outlined {
+    &--outlined,
+    &--outlined a {
         color: var(--clr-primary-400);
         background-color: transparent;
         border: 1px solid var(--clr-primary-400);
+        text-decoration: none;
+    }
 
+    &--outlined {
         &::after {
             content: $single-left-arrow;
             display: inline-block;
@@ -38,8 +62,13 @@
             transition: transform 0.3s ease-out;
         }
 
-        &:hover {
-            border-color: transparent;
+        /* Outlined hover: keep the border so the affordance does not
+           visually evaporate; add a quiet tint and slide the arrow. */
+        &:hover,
+        &:focus {
+            color: var(--clr-primary-400);
+            background-color: var(--clr-primary-50, #eaf3fc);
+            border-color: var(--clr-primary-400);
 
             &::after {
                 transform: translateX(4px);
diff --git a/docs/assets/sass/components/_card.scss b/docs/assets/sass/components/_card.scss
index 070bb5d6c9..e4a773c5af 100644
--- a/docs/assets/sass/components/_card.scss
+++ b/docs/assets/sass/components/_card.scss
@@ -29,23 +29,20 @@
         font-size: .875rem;
     }
 
+    &__date {
+        margin-left: 0.5em;
+        font-weight: 400;
+        color: var(--clr-neutral-600);
+        font-size: 0.8125rem;
+    }
+
     &__desc {
         font-size: .875rem;
     }
 
-    &__btn__left {
-        border: 0;
-        background-color: transparent;
-
-        &__link,
-        &__link:hover {
-            color: var(--clr-secondary-500);
-            text-decoration: none;
-        }
-
-        &:hover {
-            background-color: var(--clr-secondary-100);
-        }
+    &__download {
+        display: inline-block;
+        line-height: 24px;
     }
 
     &__btn__right {
diff --git a/docs/assets/sass/components/_code-tabs.scss b/docs/assets/sass/components/_code-tabs.scss
new file mode 100644
index 0000000000..2afca25de3
--- /dev/null
+++ b/docs/assets/sass/components/_code-tabs.scss
@@ -0,0 +1,114 @@
+.code-tabs {
+  margin-block: var(--space-m);
+  /* Shrink the container to the widest tab's content rather than
+     filling the parent column. On the homepage this keeps the copy
+     button (absolute-positioned to the right edge of the panels)
+     glued to the snippet instead of stranding it at the page edge.
+     On docs pages it gives the same visual: snippets sit at their
+     natural width, not stretched. Long lines still cap at parent
+     width via max-width and overflow horizontally inside <pre>. */
+  width: max-content;
+  max-width: 100%;
+  border: var(--border-subtle);
+  border-radius: var(--radius-m);
+  background: var(--clr-neutral-100);
+
+  &__list {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 0;
+    margin: 0;
+    padding: 0;
+    list-style: none;
+    border-block-end: var(--border-subtle);
+    background: var(--clr-neutral-200);
+    border-start-start-radius: var(--radius-m);
+    border-start-end-radius:   var(--radius-m);
+  }
+
+  &__tab {
+    margin: 0;
+    padding: 0;
+    list-style: none;
+  }
+
+  &__btn {
+    display: inline-block;
+    padding: var(--space-2xs) var(--space-m);
+
+    background: transparent;
+    border: 0;
+    border-block-end: 2px solid transparent;
+    cursor: pointer;
+
+    font: inherit;
+    font-weight: 500;
+    color: var(--clr-neutral-600);
+
+    &[aria-selected="true"] {
+      background: var(--clr-neutral-100);
+      color: var(--clr-primary-400);
+      border-block-end-color: var(--clr-primary-400);
+    }
+
+    &:hover { color: var(--clr-primary-400); }
+    &:focus-visible { outline: 2px solid var(--clr-primary-300); outline-offset: -2px; }
+  }
+
+  &__panels { position: relative; }
+
+  &__panel {
+    display: none;
+    padding: 0;
+
+    &[data-active="true"] { display: block; }
+
+    /* Reserve room on the right for the absolute-positioned
+       `__copy` button so a single-line snippet (Gradle Kotlin /
+       Groovy on the homepage) does not run under it. The
+       container's `width: max-content` then sizes to
+       `widest-line + 5rem`, keeping the button glued to the
+       block edge rather than overlapping the code. */
+    pre {
+      margin: 0;
+      border-radius: 0;
+      padding-inline-end: 5rem;
+    }
+  }
+
+  &__copy {
+    position: absolute;
+    inset-block-start: var(--space-2xs);
+    inset-inline-end:  var(--space-2xs);
+    z-index: 1;
+
+    padding: var(--space-3xs) var(--space-xs);
+    border: var(--border-subtle);
+    border-radius: var(--radius-s);
+    background: var(--clr-white);
+    cursor: pointer;
+    font: inherit;
+    font-size: var(--fs-300);
+    color: var(--clr-neutral-600);
+    line-height: 1;
+
+    &:hover  { background: var(--clr-neutral-100); color: var(--clr-primary-400); }
+    &:focus-visible {
+      outline: 2px solid var(--clr-primary-300);
+      outline-offset: 1px;
+    }
+
+    /* JS swaps the label text to "Copied" and adds .is-copied for the
+       brief confirmation window; mirror with a green tint so the
+       feedback is unmissable without an icon swap. */
+    &.is-copied {
+      color: var(--clr-success-400, #1f7a3d);
+      border-color: var(--clr-success-300, #2d9d57);
+    }
+
+    &.is-failed {
+      color: var(--clr-danger-400, #b3261e);
+      border-color: var(--clr-danger-300, #d9534f);
+    }
+  }
+}
diff --git a/docs/assets/sass/components/_page-meta.scss b/docs/assets/sass/components/_page-meta.scss
new file mode 100644
index 0000000000..e901267bdf
--- /dev/null
+++ b/docs/assets/sass/components/_page-meta.scss
@@ -0,0 +1,42 @@
+.page-meta {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: var(--space-s);
+  margin-block-end: var(--space-m);
+  padding-block: var(--space-2xs);
+  font-size: var(--fs-300);
+  color: var(--clr-neutral-600);
+  border-block-end: var(--border-subtle);
+
+  &__breadcrumbs {
+    flex: 1 1 auto;
+    margin: 0;
+    padding: 0;
+    list-style: none;
+    display: flex;
+    flex-wrap: wrap;
+    gap: var(--space-3xs);
+
+    li::after {
+      content: "›";
+      margin-inline-start: var(--space-3xs);
+      color: var(--clr-neutral-400);
+    }
+    li:last-child::after { content: ""; }
+
+    a { color: inherit; }
+  }
+
+  &__reviewed,
+  &__edit {
+    display: inline-flex;
+    align-items: center;
+    gap: var(--space-3xs);
+  }
+
+  &__edit a {
+    color: var(--clr-primary-400);
+    text-decoration: underline;
+  }
+}
diff --git a/docs/assets/sass/components/_release-history.scss b/docs/assets/sass/components/_release-history.scss
new file mode 100644
index 0000000000..40225409cc
--- /dev/null
+++ b/docs/assets/sass/components/_release-history.scss
@@ -0,0 +1,58 @@
+.release-history-wrap {
+  margin-block: var(--space-m);
+
+  /* On narrow viewports the seven columns push past the viewport;
+     wrap-scroll keeps row-by-row reading intact without forcing
+     line breaks inside individual cells. */
+  @include mq-max(medium) {
+    overflow-x: auto;
+  }
+}
+
+.release-history {
+  display: table;
+  overflow: visible;
+  border-collapse: separate;
+  border-spacing: 0;
+  width: 100%;
+  font-size: var(--fs-300);
+
+  th, td {
+    padding: var(--space-2xs) var(--space-s);
+    border-block-end: var(--border-subtle);
+    vertical-align: top;
+    text-align: start;
+  }
+
+  thead th {
+    background: var(--clr-neutral-100);
+    font-weight: 600;
+    color: var(--clr-neutral-700);
+    position: sticky;
+    inset-block-start: var(--navbar-height);
+    z-index: 1;
+    box-shadow: inset 0 -1px 0 var(--clr-neutral-300);
+  }
+
+  /* The line label, version range, ISO release date and the status
+     phrase ("Security until 2028-11" etc.) must never wrap: an ISO
+     date breaking on a hyphen is jarring, and "42.2.27.jre6" or
+     "42.7.0-42.7.4" sliced mid-token is unreadable. */
+  &__line,
+  &__range,
+  &__date,
+  &__status {
+    white-space: nowrap;
+  }
+
+  &__line code,
+  &__range code {
+    font-family: ui-monospace, "JetBrains Mono", Menlo, Consolas, monospace;
+  }
+
+  /* The active recommended segment of the active line — gets a quiet
+     tint so a reader scanning the table lands on it first. */
+  &__current {
+    background: var(--clr-primary-50, var(--clr-neutral-100));
+  }
+}
diff --git a/docs/assets/sass/components/_search.scss b/docs/assets/sass/components/_search.scss
index a8a0622446..fe7e137456 100644
--- a/docs/assets/sass/components/_search.scss
+++ b/docs/assets/sass/components/_search.scss
@@ -1,22 +1,31 @@
-/*  Search Form */
+/* Search Form
+ *
+ * The search box is a self-contained popover root: the input and submit
+ * button sit on top, the live results dropdown is positioned absolutely
+ * below. Width is fixed (not hover-expanded) because live results need
+ * a stable target the user can read while typing.
+ */
 
 .searchBox {
-  background: var(--clr-primary-500);
-  height: 45px;
-  border-radius: 45px;
-  padding: 2.5px;
+  position: relative;
 
-  &:hover>.searchInput {
-    width: 200px;
-    padding: 0 6px;
-    transition: 0.4s;
-  }
+  &__form {
+    display: flex;
+    align-items: center;
+    background: var(--clr-primary-500);
+    height: 45px;
+    border-radius: 45px;
+    padding: 2.5px 2.5px 2.5px 14px;
+    gap: 6px;
+    min-width: 240px;
 
-  &:hover>.searchButton {
-    background: var(--clr-white);
+    @include mq-max(medium) {
+      min-width: 0;
+      width: 100%;
+    }
 
-    svg {
-      fill: var(--clr-indigo);
+    &:focus-within {
+      box-shadow: 0 0 0 3px hsl(216, 70%, 78%);
     }
   }
 }
@@ -25,35 +34,177 @@
   border: none;
   background: none;
   outline: none;
-  float: left;
   padding: 0;
   color: var(--clr-white);
-  transition: 0.4s;
   line-height: 40px;
-  width: 0px;
+  flex: 1 1 auto;
+  min-width: 0;
+  font-size: 0.95rem;
+
+  &::placeholder {
+    color: hsla(0, 0%, 100%, 0.75);
+  }
+
+  // Hide the browser's default clear/cancel button — we manage state.
+  &::-webkit-search-decoration,
+  &::-webkit-search-cancel-button,
+  &::-webkit-search-results-button,
+  &::-webkit-search-results-decoration {
+    -webkit-appearance: none;
+  }
+}
+
+.searchHint {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 22px;
+  height: 22px;
+  padding: 0 6px;
+  border: 1px solid hsla(0, 0%, 100%, 0.5);
+  border-radius: 4px;
+  color: var(--clr-white);
+  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
+  font-size: 0.75rem;
+  line-height: 1;
+  background: hsla(0, 0%, 100%, 0.12);
+  user-select: none;
+
+  // Hide the hint once the user has engaged with the input — it's signage
+  // for first-time users, not a permanent label.
+  .searchBox:focus-within &,
+  .searchInput:not(:placeholder-shown) ~ & {
+    display: none;
+  }
+
+  @include mq-max(small) {
+    display: none;
+  }
 }
 
 .searchButton {
   color: var(--clr-white);
-  float: right;
+  flex: 0 0 auto;
   width: 40px;
   height: 40px;
   border-radius: 50%;
-  border-color: var(--clr-white);
+  border: 1px solid var(--clr-white);
   background: var(--clr-primary-500);
   display: flex;
   justify-content: center;
   align-items: center;
-  transition: 0.4s;
+  cursor: pointer;
+  transition: background 0.2s, color 0.2s;
+  padding: 0;
+
+  &:hover,
+  .searchBox:focus-within & {
+    background: var(--clr-white);
+
+    svg {
+      fill: var(--clr-primary-500);
+    }
+  }
 }
 
-/*  Search Template */
+/* Live results dropdown */
+
+.searchResults {
+  position: absolute;
+  top: calc(100% + 6px);
+  right: 0;
+  // unquote() keeps SASS from trying to evaluate CSS-native min() at compile time —
+  // its arguments mix runtime CSS vars / calc() and incompatible units (vh vs px).
+  width: unquote("min(840px, calc(100vw - 2 * var(--container-spacing, 1rem)))");
+  max-height: unquote("min(70vh, 600px)");
+  overflow-y: auto;
+  background: var(--clr-white);
+  border-radius: 8px;
+  box-shadow:
+    0 1px 3px hsla(222, 35%, 25%, 0.12),
+    0 8px 24px hsla(222, 35%, 25%, 0.18);
+  z-index: 3;
+  // Width is viewport-bound, not parent-bound: the search field is narrow,
+  // but we want the results list to use the available navbar width. Anchored
+  // by `right: 0`, the dropdown grows leftward up to container-spacing on the
+  // opposite side of the page.
+
+  &__list {
+    list-style: none;
+    margin: 0;
+    padding: 6px 0;
+  }
+
+  &__item {
+    margin: 0;
+    padding: 0;
+
+    &.is-active .searchResults__link,
+    .searchResults__link:focus {
+      background: var(--clr-primary-100, hsl(216, 70%, 96%));
+      outline: none;
+    }
+  }
+
+  &__link {
+    display: block;
+    padding: 10px 16px;
+    color: var(--clr-indigo);
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: none;
+    }
+  }
+
+  &__title {
+    color: var(--clr-indigo);
+    font-weight: 600;
+    font-size: 1rem;
+    line-height: 1.3;
+  }
+
+  &__snippet {
+    color: var(--clr-neutral-600, hsl(0, 0%, 32%));
+    font-size: 0.875rem;
+    line-height: 1.45;
+    margin-top: 3px;
+    // Clamp long snippets so each result stays roughly the same height,
+    // making the list scannable.
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
+  }
+
+  &__ellipsis {
+    color: var(--clr-neutral-500, hsl(0, 0%, 52%));
+  }
+
+  &__empty {
+    padding: 16px;
+    color: var(--clr-neutral-600, hsl(0, 0%, 32%));
+    font-size: 0.9rem;
+  }
+
+  mark {
+    background: hsl(43, 100%, 80%);
+    color: inherit;
+    padding: 0 1px;
+    border-radius: 2px;
+  }
+}
 
-.search-list {
-  margin-bottom: var(--primary-spacing);
+/* Body-level search-term highlights left behind by search-highlight.js
+   after a click from the dropdown. Same hue as the dropdown mark for
+   visual continuity, slightly lighter so the underlying prose still reads
+   comfortably across whole paragraphs. */
+mark.search-hit {
+  background: hsl(43, 100%, 86%);
+  color: inherit;
+  padding: 0 1px;
+  border-radius: 2px;
+  box-decoration-break: clone;
+  -webkit-box-decoration-break: clone;
 }
 
-.list-title {
-  @extend %container-style;
-  margin-block: var(--primary-spacing);
-}
\ No newline at end of file
diff --git a/docs/assets/sass/components/_section-children.scss b/docs/assets/sass/components/_section-children.scss
new file mode 100644
index 0000000000..0fa77110eb
--- /dev/null
+++ b/docs/assets/sass/components/_section-children.scss
@@ -0,0 +1,49 @@
+/* Card-list of child pages rendered at the bottom of a section's
+   `_index.md` landing. Gives the reader a continuation path inside
+   the section instead of pushing them back to the sidebar. */
+.section-children {
+  list-style: none;
+  margin: var(--space-l) 0 0;
+  padding: 0;
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: var(--space-s);
+
+  @include mq-min(small) {
+    grid-template-columns: repeat(2, 1fr);
+  }
+
+  li {
+    margin: 0;
+    padding: 0;
+  }
+
+  &__link {
+    display: block;
+    padding: var(--space-s) var(--space-m);
+    border: var(--border-subtle);
+    border-radius: var(--radius-m);
+    color: inherit;
+    text-decoration: none;
+    transition: border-color 80ms ease, background-color 80ms ease;
+
+    &:hover {
+      border-color: var(--clr-primary-300);
+      background: var(--clr-neutral-100);
+    }
+  }
+
+  &__title {
+    display: block;
+    color: var(--clr-primary-400);
+    font-weight: 600;
+    margin-block-end: var(--space-3xs);
+  }
+
+  &__summary {
+    display: block;
+    color: var(--clr-neutral-600);
+    font-size: var(--fs-300);
+    line-height: 1.4;
+  }
+}
diff --git a/docs/assets/sass/components/_toc.scss b/docs/assets/sass/components/_toc.scss
index a5d9002856..1d9d584254 100644
--- a/docs/assets/sass/components/_toc.scss
+++ b/docs/assets/sass/components/_toc.scss
@@ -30,7 +30,7 @@
     }
 
     a {
-        text-decoration: none;
+        text-decoration: underline;
     }
 
     @include mq-max(medium) {
diff --git a/docs/assets/sass/layout/_footer.scss b/docs/assets/sass/layout/_footer.scss
index d41536cdbb..ca2f14a754 100644
--- a/docs/assets/sass/layout/_footer.scss
+++ b/docs/assets/sass/layout/_footer.scss
@@ -24,7 +24,7 @@
 
         &__link {
             color: var(--clr-white);
-            text-decoration: none;
+            text-decoration: underline;
         }
     }
 
diff --git a/docs/assets/sass/layout/_navigation.scss b/docs/assets/sass/layout/_navigation.scss
index 904116c2a8..54f66be12a 100644
--- a/docs/assets/sass/layout/_navigation.scss
+++ b/docs/assets/sass/layout/_navigation.scss
@@ -5,7 +5,9 @@
 
   position: sticky;
   top: 0;
-  z-index: 1;
+  /* Above .param-table thead (z-index: 1) so the navbar always covers the
+     parking band even if --navbar-height under-estimates actual height. */
+  z-index: 2;
   padding: var(--primary-spacing) var(--container-spacing);
   box-shadow: 0px 3px 3px hsla(0, 0%, 0%, 0.15);
   background-color: var(--clr-white);
@@ -18,39 +20,26 @@
   &__item {
     display: flex;
 
+    // Lightened in the styling pass: the navbar is supporting
+    // chrome, not the page's primary task. 1.25rem / weight 600
+    // read as heading-class on small screens and crowded the
+    // homepage hero on wide ones; 1.05rem / weight 500 sits
+    // quietly enough to let the hero CTA stay the loudest
+    // element on the page.
     &__link {
       color: var(--clr-indigo);
-      text-decoration: none;
-      font-size: 1.25rem;
-      position: relative;
-      font-weight: 600;
+      text-decoration: underline;
+      font-size: 1.05rem;
+      font-weight: 500;
       margin-inline: var(--primary-spacing);
 
       &--active {
         color: var(--clr-primary-400);
       }
 
-      &::after {
-        content: '';
-        position: absolute;
-        width: 100%;
-        transform: scaleX(0);
-        height: 2px;
-        bottom: 0;
-        left: 0;
-        background-color: var(--clr-primary-100);
-        transform-origin: bottom right;
-        transition: transform 0.25s ease-out;
-      }
-
       &:hover {
         color: var(--clr-indigo);
       }
-
-      &:hover::after {
-        transform: scaleX(1);
-        transform-origin: bottom left;
-      }
     }
 
     @include mq-max(medium) {
@@ -94,17 +83,64 @@
   }
 
   a {
-    text-decoration: none;
+    text-decoration: underline;
     color: var(--clr-white);

   }
 
+  /* Indent the sub-page rows of the Docs Menu via list-item padding
+     rather than `&nbsp;&nbsp;` inside the link text. Padding belongs
+     to the `<li>` so it does not get caught by the link's underline,
+     which would otherwise put a visible underline in front of the
+     title text. */
+  .docsmenu__subpage {
+    padding-inline-start: 2.5em;
+  }
+
   padding: 1em 2em;
 
+  /* When a mobile dropdown is opened (the toggle removes `.dn`), cap
+     its height to the space between the sticky top navbar and the
+     fixed button row, and let the inner list scroll. Without the cap
+     the Docs Menu with ~25 entries overflows the viewport upward;
+     because the parent `.nav-mob` is position: fixed the user cannot
+     scroll to reach the top of the list (the page body scrolls
+     underneath instead, which moves nothing visible). Without the
+     `--navbar-height` term the top of the menu sits under the sticky
+     `.navbar` at the top of the viewport and hides the first entries.
+
+     `100dvh` follows the dynamic viewport on mobile so the cap stays
+     correct when the browser's address bar shows or hides; `100vh`
+     is a fallback for engines that do not yet implement dvh. The
+     5rem subtracted matches the intrinsic height of the button row
+     below (the same constant used by the body's padding-bottom).
+     `--navbar-height` is the measured height of the sticky `.navbar`
+     (published by static/js/navbar-height.js, with a 5rem default in
+     abstracts/_constants.scss). */
+  .mobilemenu:not(.dn) {
+    max-height: calc(100vh - 5rem - var(--navbar-height));
+    max-height: calc(100dvh - 5rem - var(--navbar-height));
+    overflow-y: auto;
+    -webkit-overflow-scrolling: touch;
+    overscroll-behavior: contain;
+  }
+
   @include mq-min(medium) {
     display: none;
+  }
+}
 
+// The bottom-fixed `.nav-mob` covers the last ~5rem of the viewport
+// on every page below the `medium` breakpoint. Without compensating
+// padding the page's footer (and the tail of any long article) sits
+// behind it and is unreachable. The padding-bottom matches the
+// nav-mob's intrinsic height (1em + 1em padding + ~3em of button row
+// ≈ 5rem) with a small buffer so the last content line is fully
+// visible above the bar.
+@include mq-max(medium) {
+  body {
+    padding-bottom: 5.5rem;
   }
 }
 
diff --git a/docs/assets/sass/layout/_sidebar.scss b/docs/assets/sass/layout/_sidebar.scss
index cee0bf2d32..135dad5d0a 100644
--- a/docs/assets/sass/layout/_sidebar.scss
+++ b/docs/assets/sass/layout/_sidebar.scss
@@ -5,23 +5,81 @@
 
   align-self: start;
   position: sticky;
+  /* Park below the top navbar so the sidebar header is not hidden when
+     the user scrolls. Mirrors the param-table thead behaviour. */
+  inset-block-start: var(--navbar-height);
+  max-height: calc(100vh - var(--navbar-height));
+  overflow-y: auto;
 
   &__wrapper {
     padding-inline: var(--primary-spacing);
   }
 
-  &__links__link {
-    color: var(--clr--indigo);
-    text-decoration: none;
+  /* First-level entries: section headings (e.g. "Connect", "Security"). */
+  &__section {
+    margin-block: var(--space-s);
+    list-style: none;
 
-    &--active {
-      color: var(--clr-primary-400);
+    /* Active and inactive titles share the same base look. The active
+       state only swaps the colour: the template appends `--active` to
+       the class name (rather than carrying both classes BEM-style), so
+       the modifier needs every property the base has, otherwise the
+       title loses its bold + underline when the user lands on the
+       section's own page. */
+    &__title,
+    &__title--active {
+      display: block;
+      color: var(--clr-indigo);
       font-weight: 700;
-      text-decoration: none;
+      text-decoration: underline;
+      padding-block: var(--space-3xs);
+    }
+
+    &__title:hover { color: var(--clr-primary-400); }
+
+    &__title--active {
+      color: var(--clr-primary-400);
+    }
+
+    &__pages {
+      margin: 0;
+      padding-inline-start: var(--space-s);
+      list-style: none;
+
+      /* Cancel the .article li margin reset that doesn't apply here, but
+         also matches the rest of the sidebar's tightness. */
+      li {
+        margin: 0;
+        padding: 0;
+      }
+    }
+
+    &--active &__title {
+      /* Bold the section title when the current page belongs to it. */
+      color: var(--clr-primary-400);
+    }
+  }
+
+  /* Leaf entries: individual pages. */
+  &__links {
+    margin: 0;
+    padding-block: var(--space-3xs);
+    list-style: none;
+
+    &__link {
+      color: var(--clr-indigo);
+      text-decoration: underline;
+
+      &:hover { color: var(--clr-primary-400); }
+
+      &--active {
+        color: var(--clr-primary-400);
+        font-weight: 700;
+      }
     }
   }
 
   @include mq-max(medium) {
     display: none;
   }
-}
\ No newline at end of file
+}
diff --git a/docs/assets/sass/main.scss b/docs/assets/sass/main.scss
index 74b68b860e..daa453edf3 100644
--- a/docs/assets/sass/main.scss
+++ b/docs/assets/sass/main.scss
@@ -21,14 +21,20 @@

 @import
+  'components/badge',
   'components/button',
   'components/card',
+  'components/code-tabs',
+  'components/page-meta',
   'components/pagination',
+  'components/section-children',
+  'components/release-history',
   'components/search',
   'components/table',
   'components/toc';
 
 @import
+  'pages/changelogs',
   'pages/community',
   'pages/development',
   'pages/docs',
@@ -40,4 +46,4 @@
 
 @import
   'utilities/container',
-  'utilities/flow';
\ No newline at end of file
+  'utilities/flow';
diff --git a/docs/assets/sass/pages/_changelogs.scss b/docs/assets/sass/pages/_changelogs.scss
new file mode 100644
index 0000000000..7ad7751145
--- /dev/null
+++ b/docs/assets/sass/pages/_changelogs.scss
@@ -0,0 +1,69 @@
+// Changelogs landing page. Renders as a vertically scannable list:
+// one row per release, columns are REL-version | ISO date | summary.
+// Grid lets the version and date stay in fixed columns so the eye
+// can run down them without saccading. On narrow viewports the
+// row collapses to a two-line block (version+date together, then
+// the summary on its own line).
+
+.changelogs {
+    &__list {
+        list-style: none;
+        margin: var(--space-l) 0 0;
+        padding: 0;
+    }
+
+    &__entry {
+        display: grid;
+        grid-template-columns: max-content max-content 1fr;
+        gap: var(--space-s) var(--space-m);
+        align-items: baseline;
+        padding: 0.55em 0;
+        border-bottom: 1px solid var(--clr-neutral-200);
+
+        &:last-child {
+            border-bottom: 0;
+        }
+    }
+
+    /* REL42.x.y — the link target. Monospace + weight 600 makes
+       the version column scan as a column even when summary text
+       wraps to multiple lines. Link blue per the styling-pass
+       discipline (links and CTAs only). */
+    &__version {
+        font-family: 'JetBrainsMono', monospace;
+        font-weight: 600;
+        font-size: 0.95rem;
+        color: var(--clr-primary-400);
+        text-decoration: underline;
+        white-space: nowrap;
+    }
+
+    /* ISO date — tabular-nums keeps each digit the same advance
+       width, so dates column-align even though the font is
+       proportional. */
+    &__date {
+        color: var(--clr-neutral-600);
+        font-size: 0.95rem;
+        font-variant-numeric: tabular-nums;
+        white-space: nowrap;
+    }
+
+    &__summary {
+        color: var(--clr-neutral-700);
+        line-height: 1.45;
+    }
+
+    @include mq-max(small) {
+        &__entry {
+            grid-template-columns: max-content 1fr;
+            gap: 0.2em var(--space-s);
+        }
+
+        /* On narrow screens the summary drops onto its own row
+           below the version+date pair so it doesn't squeeze the
+           date column. */
+        &__summary {
+            grid-column: 1 / -1;
+        }
+    }
+}
diff --git a/docs/assets/sass/pages/_download.scss b/docs/assets/sass/pages/_download.scss
index 1709f4c693..0fad9172c5 100644
--- a/docs/assets/sass/pages/_download.scss
+++ b/docs/assets/sass/pages/_download.scss
@@ -5,11 +5,13 @@
         max-width: 100%;
     }
 
-    .custom-marker>::marker {
-        content: $link;
-        font-size: large;
+    .past-versions > li {
+        padding-left: 0.25em;
     }
-    .custom-marker>li {
-        padding-left: 0.5em;
+
+    .past-versions__date {
+        margin-left: 0.5em;
+        color: var(--clr-neutral-600);
+        font-size: 0.875em;
     }
 }
\ No newline at end of file
diff --git a/docs/assets/sass/pages/_home.scss b/docs/assets/sass/pages/_home.scss
index 57c34cf967..324d7dbc60 100644
--- a/docs/assets/sass/pages/_home.scss
+++ b/docs/assets/sass/pages/_home.scss
@@ -1,3 +1,17 @@
+// Styling pass — six homepage blocks share three rules:
+//   1. Block headings carry the same size and bottom margin
+//      (var(--space-s)). Hierarchy comes from weight + colour
+//      below, not from per-block font-size variation.
+//   2. Blue is reserved for CTAs, headings, and link colour.
+//      Tile titles, bullet leads, decorative pills, and the
+//      release version display use the body's neutral colour
+//      and lean on weight/size for prominence. Each block-local
+//      override below is intentional.
+//   3. Block-level `<p>` styling is set per block (release
+//      summary, tasks desc, hero subtitle) — there is no
+//      `.home p { ... }` blanket rule, because the previous one
+//      bolded everything and was overridden in five places.
+
 .home {
     max-width: 70rem;
     margin: var(--box-spacing) auto;
@@ -12,16 +26,7 @@
         color: var(--clr-primary-400);
     }
 
-    p {
-        font-weight: 600;
-        letter-spacing: 1px;
-        margin: 1em 0;
-    }
-
-    .hero,
-    .info,
-    .feature {
-
+    .hero {
         @include mq-min(medium) {
             display: flex;
             justify-content: space-between;
@@ -37,37 +42,54 @@
     }
 }
 
+.home__decision {
+    display: grid;
+    grid-template-columns: minmax(0, 1fr);
+    gap: clamp(1.5rem, 3vw, 2.5rem);
+    align-items: start;
+
+    @include mq-min(medium) {
+        grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
+    }
+}
+
 // Hero Section
 
 .hero {
     align-items: center;
-    column-gap: 5em;
+    column-gap: 4em;
 
+    /* Elephant illustration stays but no longer dominates the fold;
+       the content side is the primary task-flow target. */
     &__img {
-        width: 45%;
+        width: 28%;
 
         @include mq-max(medium) {
-            width: 70%
+            width: 50%;
         }
 
     }
 
     &__content {
-        width: 45%;
+        width: 62%;
 
         @include mq-max(medium) {
             width: 100%
         }
 
+        /* Title kept under one viewport-line on a typical desktop so
+           the install path stays visible above the fold. Previous
+           5.4rem forced wrap on most widths. */
         &__head {
-            font-size: 5.4rem;
+            font-size: 3.5rem;
+            line-height: 1.15;
 
             @include mq-max(medium) {
-                font-size: 4.5rem;
+                font-size: 2.8rem;
             }
 
             @include mq-max(small) {
-                font-size: 3.5rem;
+                font-size: 2.2rem;
             }
         }
 
@@ -75,12 +97,10 @@
             font-size: 1.25rem;
             padding-block: 0.5em;
             font-weight: 600;
-            // letter-spacing: 1px;
             line-height: 1.5;
 
             @include mq-max(medium) {
                 font-size: 1.05rem;
-                // line-height: 1.25;
             }
         }
 
@@ -98,138 +118,434 @@
                 justify-content: center;
             }
         }
+
+        /* Tertiary links: a quiet row of three text links under the
+           CTAs, separated by middots. They are not the primary task
+           but should be reachable in one click. */
+        &__links {
+            list-style: none;
+            margin: 0;
+            padding: 1em 0 0;
+            display: flex;
+            flex-wrap: wrap;
+            gap: 0.25em 1em;
+            font-size: 0.95rem;
+            color: var(--clr-neutral-600);
+
+            li + li::before {
+                content: "·";
+                margin-inline-end: 1em;
+                color: var(--clr-neutral-400);
+            }
+
+            a {
+                color: var(--clr-primary-400);
+                text-decoration: underline;
+            }
+
+            @include mq-max(medium) {
+                justify-content: center;
+            }
+        }
+
+        /* Badge bar: shields.io-style badges. Each one is an
+           actionable signal (Maven Central version, Javadoc,
+           license, OpenSSF Scorecard) — not decoration. */
+        &__badges {
+            list-style: none;
+            margin: 0;
+            padding: 1.25em 0 0;
+            display: flex;
+            flex-wrap: wrap;
+            gap: 0.4em 0.6em;
+
+            img { display: block; }
+
+            @include mq-max(medium) {
+                justify-content: center;
+            }
+        }
     }
 }
 
-// Info Section
+// Quick install — pgJDBC dependency snippet, right under the hero.
+// The inner `.code-tabs` carries its own styling from
+// components/_code-tabs.scss; this rule only sets up the wrapper.
 
-.info {
+.quick-install {
+    min-width: 0;
 
-    ul {
-        margin: 0;
-        padding: 0;
+    &__head {
+        font-size: 1.6rem;
+        margin-bottom: var(--space-s);
+        line-height: 1.15;
 
-        li {
-            padding-inline: 0.5em;
+        @include mq-max(medium) {
+            text-align: center;
+        }
+    }
+
+    /* Two stacked code-tabs blocks (Maven, Gradle) share one grid
+       track sized to the widest block (Maven XML). Without this,
+       Gradle hugs its single-line snippet and the right edges step
+       — visually noisy under a shared heading. minmax(0, max-content)
+       lets the track shrink on narrow viewports (inner <pre>
+       overflow-scrolls) while still capping at the natural width. */
+    &__snippets {
+        display: grid;
+        grid-template-columns: minmax(0, max-content);
+        gap: var(--space-m);
+        max-width: 100%;
+
+        @include mq-max(medium) {
+            justify-content: center;
         }
+    }
+
+    /* Override the component's default `margin-block` and intrinsic
+       `width: max-content` so the children stretch to fill the
+       shared grid track instead of sizing to their own content. */
+    .code-tabs {
+        margin-block: 0;
+        width: auto;
+    }
+}
 
-        li:nth-child(even) {
-            background-color: var(--clr-neutral-200);
+// Release & compatibility card. Single primary card under
+// Quick-install. Type registers: block heading (1.6rem, blue,
+// shared across blocks), version display (1.7rem, neutral 700,
+// weight 600), body (1rem inherit). The version is intentionally
+// not blue — the styling pass reserves primary blue for headings
+// and CTAs; the version is a display value but neither of those.
+// Size + weight carry its prominence.
+
+.release {
+    min-width: 0;
+
+    &__head {
+        font-size: 1.6rem;
+        margin-bottom: var(--space-s);
+        line-height: 1.15;
+
+        @include mq-max(medium) {
+            text-align: center;
         }
     }
 
-    &__about {
+    /* No box around the release card. The Quick-install block on the
+       left already carries the bordered + filled visual treatment
+       (its inner code-tabs container), and matching it here forced
+       two heavy containers side-by-side with different content
+       densities (mono-code vs proportional text) — read messy. The
+       release block now flows as open text under its heading; the
+       grid spacing on .home__decision still separates it from the
+       neighbour. */
+    &__card {
+        padding: 0;
+    }
+
+    /* Version (left) and date (right), baseline-aligned. On phones
+       the row collapses to centred stack. */
+    &__primary {
+        margin: 0;
         display: flex;
-        flex-direction: column;
-        align-items: center;
-        max-width: 45%;
+        flex-wrap: wrap;
+        justify-content: space-between;
+        align-items: baseline;
+        gap: 0.4em 1em;
 
         @include mq-max(medium) {
-            max-width: 100%
+            justify-content: center;
         }
     }
 
-    &__release {
+    &__version {
+        font-size: 1.7rem;
+        font-weight: 600;
+        color: var(--clr-neutral-700);
+        line-height: 1.15;
+    }
+
+    &__date {
+        color: var(--clr-neutral-600);
+    }
+
+    /* Java / PostgreSQL pills. Decorative neutral chips — not
+       links, not CTAs. Border and text both neutral; the styling
+       pass moved them out of blue. */
+    &__pills {
+        list-style: none;
+        margin: 0.7em 0 0;
+        padding: 0;
         display: flex;
-        flex-direction: column;
-        max-width: 45%;
+        flex-wrap: wrap;
+        gap: 0.4em 0.6em;
 
-        h2 {
-            text-align: center;
+        li {
+            display: inline-block;
+            padding: 0.25em 0.8em;
+            border: 1px solid var(--clr-neutral-300);
+            border-radius: 100vmax;
+            background: var(--clr-white);
+            color: var(--clr-neutral-700);
+            font-weight: 500;
+            font-size: 0.95rem;
+            line-height: 1.3;
         }
 
         @include mq-max(medium) {
-            max-width: 100%
+            justify-content: center;
         }
-
     }
-}
 
-// Feature Section
+    &__summary {
+        margin: 0.85em 0 0;
+        color: var(--clr-neutral-700);
+        line-height: 1.45;
+
+        @include mq-max(medium) {
+            text-align: center;
+        }
+    }
 
-.feature {
-    column-gap: 5rem;
+    &__sep {
+        color: var(--clr-neutral-400);
+        margin-inline: 0.15em;
+    }
 
-    &__item {
+    &__actions {
+        list-style: none;
+        margin: 0.9em 0 0;
+        padding: 0;
         display: flex;
-        flex-direction: column;
-        align-items: center;
+        flex-wrap: wrap;
+        gap: 0.3em 1.25em;
 
-        &__media {
-            width: 4rem;
-            aspect-ratio: 1;
+        a {
+            color: var(--clr-primary-400);
+            text-decoration: underline;
+        }
+
+        @include mq-max(medium) {
+            justify-content: center;
+            text-align: center;
         }
     }
-}
 
-.example {
-    pre code {
+    &__recent {
+        margin: 0.9em 0 0 0;
+        padding-top: 0.85em;
+        border-top: 1px solid var(--clr-neutral-300);
+        color: var(--clr-neutral-600);
+
+        a {
+            color: var(--clr-primary-400);
+            text-decoration: underline;
+            font-weight: 500;
+        }
+
         @include mq-max(medium) {
-            display: block;
+            text-align: center;
         }
     }
 }
 
-// Report Section
+// Common tasks. 6-card grid linking into the relevant topical
+// documentation pages. Each card is one CSS Grid cell; the link
+// covers the whole card so the click target is the entire visual
+// box rather than just the title.
 
-.report {
-    &__row {
-        @include mq-min(small) {
-            display: flex;
-            justify-content: space-between;
-            padding-top: var(--primary-spacing);
+.tasks {
+    &__head {
+        font-size: 1.6rem;
+        margin-bottom: var(--space-s);
+        line-height: 1.15;
+
+        @include mq-max(medium) {
+            text-align: center;
+        }
+    }
+
+    &__grid {
+        list-style: none;
+        margin: 0;
+        padding: 0;
+        display: grid;
+        gap: var(--space-m);
+        grid-template-columns: repeat(3, minmax(0, 1fr));
+
+        @include mq-max(medium) {
+            grid-template-columns: repeat(2, minmax(0, 1fr));
+        }
+
+        @include mq-max(small) {
+            grid-template-columns: minmax(0, 1fr);
         }
     }
 
     &__item {
-        display: inline-flex;
-        column-gap: var(--primary-spacing);
-        align-items: center;
+        margin: 0;
+        padding: 0;
     }
 
-    &__desc {
-        font-size: var(--fs-400);
-        font-stretch: 100%;
+    /* Card-as-link: the whole tile is the click target. No underline
+       on the static state; a quiet border colour shift + slight lift
+       on hover/focus gives the affordance without competing with the
+       cards' role as scannable navigation. */
+    &__link {
+        display: block;
+        height: 100%;
+        padding: var(--space-m);
+        border: var(--border-subtle);
+        border-radius: var(--radius-m);
+        background: var(--clr-white);
+        color: inherit;
+        text-decoration: none;
+        transition: border-color 0.2s, transform 0.2s;
+
+        &:hover,
+        &:focus-visible {
+            border-color: var(--clr-primary-300);
+            transform: translateY(-1px);
+            text-decoration: none;
+        }
+
+        &:focus-visible {
+            outline: 2px solid var(--clr-primary-300);
+            outline-offset: 2px;
+        }
+    }
+
+    /* Tile title: the whole tile is the link target, so the
+       title itself isn't the active element — blue would
+       miscue. Neutral 700 + weight 600 carries hierarchy without
+       competing with CTAs. */
+    &__title {
+        margin: 0;
+        font-size: 1.05rem;
+        font-weight: 600;
+        color: var(--clr-neutral-700);
         line-height: 1.3;
     }
+
+    &__desc {
+        margin: 0.4em 0 0;
+        font-size: 0.95rem;
+        color: var(--clr-neutral-600);
+        line-height: 1.45;
+    }
 }
 
-// Support Section
+// Why pgJDBC? — short reassurance block under Common tasks. Four
+// strong-lead bullets, each with one short expansion line. No
+// marketing prose; if a fact isn't reachable from the surrounding
+// context (hero subtitle, badge bar, release card), put it here.
 
-.support {
-    position: relative;
-
-    &__desc {
-        position: absolute;
-        top: 25%;
-        bottom: 25%;
-        left: 50%;
-        background-color: hsla(0, 0%, 100%, 0.5);
-        padding: var(--container-spacing);
-        margin-left: var(--container-spacing);
-        border-radius: 3px;
+.why {
+    &__head {
+        font-size: 1.6rem;
+        margin-bottom: var(--space-s);
+        line-height: 1.15;
 
         @include mq-max(medium) {
-            top: 15%;
-            left: 0%;
-            bottom: 15%;
-            margin: var(--primary-spacing);
-            padding: var(--primary-spacing);
+            text-align: center;
+        }
+    }
 
+    &__list {
+        list-style: none;
+        margin: 0;
+        padding: 0;
+        display: grid;
+        gap: var(--space-s) var(--space-l);
+        grid-template-columns: repeat(2, minmax(0, 1fr));
+
+        @include mq-max(medium) {
+            grid-template-columns: minmax(0, 1fr);
         }
 
-        @include mq-max(x-small) {
-            top: 10%;
-            left: 0%;
-            bottom: 10%;
+        li {
+            margin: 0;
+            line-height: 1.5;
 
-            h2 {
-                font-size: 1rem;
+            /* Bullet lead — bold + dark neutral. Was blue; moved
+               out as part of the styling pass since these are
+               prose strongs, not links or CTAs. */
+            strong {
+                display: block;
+                font-weight: 600;
+                color: var(--clr-neutral-700);
+                margin-bottom: 0.15em;
             }
+        }
+    }
+}
 
-            .desc {
-                font-size: 0.65rem;
-            }
+// Closing block. Four actionable destinations (bug / security /
+// contribute / donate to PostgreSQL) rendered as plain text
+// columns — no border, no fill, no card chrome. This block is a
+// utility / footer band, not equal-feature primary content; the
+// card visual the previous version used over-claimed the role.
+
+.closing {
+    &__grid {
+        list-style: none;
+        margin: 0;
+        padding: 0;
+        display: grid;
+        gap: var(--space-s) var(--space-l);
+        grid-template-columns: repeat(4, minmax(0, 1fr));
+
+        @include mq-max(medium) {
+            grid-template-columns: repeat(2, minmax(0, 1fr));
+        }
+
+        @include mq-max(small) {
+            grid-template-columns: minmax(0, 1fr);
+        }
+    }
+
+    &__item {
+        margin: 0;
+        padding: 0;
+    }
+
+    &__link {
+        display: block;
+        color: inherit;
+        text-decoration: none;
+
+        &:hover strong,
+        &:focus-visible strong {
+            color: var(--clr-primary-400);
+        }
+
+        &:focus-visible {
+            outline: 2px solid var(--clr-primary-300);
+            outline-offset: 2px;
+            border-radius: var(--radius-s);
+        }
+
+        /* Item label — neutral by default; turns blue on hover
+           or keyboard focus, mirroring the link-affordance rule
+           used elsewhere in the site. The whole `<a>` is the
+           target, so the static blue would over-claim the role. */
+        strong {
+            display: block;
+            font-size: 1rem;
+            font-weight: 600;
+            color: var(--clr-neutral-700);
+            line-height: 1.3;
+            text-decoration: underline;
+            transition: color 0.15s;
+        }
+
+        span {
+            display: block;
+            margin-top: 0.2em;
+            font-size: 0.85rem;
+            color: var(--clr-neutral-600);
+            line-height: 1.4;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/docs/config/_default/config.toml b/docs/config/_default/config.toml
index fb136b1de8..f5d66ce67e 100644
--- a/docs/config/_default/config.toml
+++ b/docs/config/_default/config.toml
@@ -1,8 +1,16 @@
 # Production BaseURL
 #baseURL = "https://pgjdbc.github.io/pgjdbcdocs/"
 baseURL = "https://jdbc.postgresql.org/"
-languageCode = "en-us"
+locale = "en-US"
 title = "pgJDBC"
+
+# Used by partials/docs/page-meta.html to render "Edit this page on GitHub".
+# The full URL is <docsRepo>/edit/<docsBranch>/<docsContentRoot>/<page-path>.
+[params]
+docsRepo         = "https://github.com/pgjdbc/pgjdbc"
+docsBranch       = "master"
+docsContentRoot  = "docs/content"
+
 [permalinks]
 page = "/:slug/"
 posts = "/:year/:month/:slug/"
diff --git a/docs/config/_default/menus.toml b/docs/config/_default/menus.toml
index f433ade568..0278e4a405 100644
--- a/docs/config/_default/menus.toml
+++ b/docs/config/_default/menus.toml
@@ -3,109 +3,50 @@
 [[global]]
 identifier = "download"
 name = "Download"
-url = "/download/"
+url = "download/"
 weight = 1
 
 [[global]]
 identifier = "document"
 name = "Documentation"
-url = "/documentation/"
+url = "documentation/"
 weight = 2
 
 [[global]]
 identifier = "community"
 name = "Community"
-url = "/community/"
+url = "community/"
 weight = 3
 
 [[global]]
 identifier = "development"
 name = "Development"
-url = "/development/"
+url = "development/"
 weight = 4
 
 [[global]]
 identifier = "changelogs"
 name = "Changelogs"
-url = "/changelogs/"
+url = "changelogs/"
 weight = 5
 
 [[global]]
 identifier = "security"
 name = "Security"
-url = "/security/"
+url = "security/"
 weight = 6
 
 ### DOCS ITEMS
-
-[[docs]]
-identifier = "setup"
-name = "Getting Started"
-url = "/documentation/setup/"
-weight = 2
-
-[[docs]]
-identifier = "use"
-name = "Initializing the Driver"
-url = "/documentation/use/"
-weight = 3
-
-[[docs]]
-identifier = "ssl"
-name = "Using SSL"
-url = "/documentation/ssl/"
-weight = 4
-
-[[docs]]
-identifier = "query"
-name = "Issuing a Query and Processing the Result"
-url = "/documentation/query/"
-weight = 5
-
-[[docs]]
-identifier = "callproc"
-name = "Calling Stored Functions and Procedures"
-url = "/documentation/callproc/"
-weight = 6
-
-[[docs]]
-identifier = "binary-data"
-name = "Storing Binary Data"
-url = "/documentation/binary-data/"
-weight = 7
-
-[[docs]]
-identifier = "escapes"
-name = "JDBC escapes"
-url = "/documentation/escapes/"
-weight = 8
-
-[[docs]]
-identifier = "server-prepare"
-name = "PostgreSQL™ Extensions to the JDBC API"
-url = "/documentation/server-prepare/"
-weight = 9
-
-[[docs]]
-identifier = "thread"
-name = "Using the Driver in a Multithreaded or a Servlet Environment"
-url = "/documentation/thread/"
-weight = 10
-
-[[docs]]
-identifier = "datasource"
-name = "Connection Pools and Data Sources"
-url = "/documentation/datasource/"
-weight = 11
-
-[[docs]]
-identifier = "logging"
-name = "Logging using java.util.logging"
-url = "/documentation/logging/"
-weight = 12
-
-[[docs]]
-identifier = "reading"
-name = "Further Reading"
-url = "/documentation/reading/"
-weight = 13
+#
+# Phase 1b: the docs sidebar is driven directly from the page tree under
+# /documentation/ (see layouts/partials/docs/sidebar.html). Section
+# ordering comes from `weight:` in each section's `_index.md`. Page
+# ordering within a section comes from each page's own `weight:`.
+#
+# Pages with `build.list: never` in frontmatter (the four legacy-URL
+# hub pages: use.md, query.md, setup.md, server-prepare.md) are
+# excluded from the sidebar automatically.
+#
+# Adding a new docs page no longer needs a menu entry here — just
+# create the markdown file with frontmatter `weight:` and Hugo picks it
+# up.
diff --git a/docs/content/changelogs/2021-02-01-42.2.25-release.md b/docs/content/changelogs/2022-02-01-42.2.25-release.md
similarity index 92%
rename from docs/content/changelogs/2021-02-01-42.2.25-release.md
rename to docs/content/changelogs/2022-02-01-42.2.25-release.md
index e293199513..233ab32fcc 100644
--- a/docs/content/changelogs/2021-02-01-42.2.25-release.md
+++ b/docs/content/changelogs/2022-02-01-42.2.25-release.md
@@ -2,6 +2,8 @@
 title: PostgreSQL JDBC Driver 42.2.25 Released
 date: 2022-02-01 07:35:28 -0500
 version: 42.2.25
+aliases:
+  - /changelogs/2021-02-01-42.2.25-release/
 ---
 
 #### Security
diff --git a/docs/content/changelogs/2023-03-17-42.6.0-release.md b/docs/content/changelogs/2023-03-17-42.6.0-release.md
index 28cdd3d579..41c0c5e5c1 100644
--- a/docs/content/changelogs/2023-03-17-42.6.0-release.md
+++ b/docs/content/changelogs/2023-03-17-42.6.0-release.md
@@ -1,6 +1,6 @@
 ---
 title:  PostgreSQL JDBC Driver 42.6.0 Released
-date:   2023-02-17 15:34:34 -0400
+date:   2023-03-17 15:34:34 -0400
 categories:
     - new_release
 version: 42.6.0
diff --git a/docs/content/changelogs/2024-03-13-42.6.2-release.md b/docs/content/changelogs/2024-03-13-42.6.2-release.md
index 05d6919eff..284ede3291 100644
--- a/docs/content/changelogs/2024-03-13-42.6.2-release.md
+++ b/docs/content/changelogs/2024-03-13-42.6.2-release.md
@@ -1,6 +1,6 @@
 ---
 title:  PostgreSQL JDBC Driver 42.6.2 Released
-date:   2024-03-14 08:23:00 -0400
+date:   2024-03-13 08:23:00 -0400
 categories:
     - new_release
 version: 42.6.2
diff --git a/docs/content/documentation/connect/_index.md b/docs/content/documentation/connect/_index.md
new file mode 100644
index 0000000000..9558f8bf1f
--- /dev/null
+++ b/docs/content/documentation/connect/_index.md
@@ -0,0 +1,4 @@
+---
+title: "Connect"
+weight: 20
+---
diff --git a/docs/content/documentation/datasource.md b/docs/content/documentation/connect/datasource.md
similarity index 99%
rename from docs/content/documentation/datasource.md
rename to docs/content/documentation/connect/datasource.md
index 0f0a059966..fc7d91dbef 100644
--- a/docs/content/documentation/datasource.md
+++ b/docs/content/documentation/connect/datasource.md
@@ -6,6 +6,7 @@ weight: 10
 toc: true
 aliases:
     - "/documentation/head/ds-ds.html"
+    - "/documentation/datasource/"
 ---
 
 JDBC 2 introduced standard connection pooling features in an add-on API known as the JDBC 2.0 Optional Package
diff --git a/docs/content/documentation/connect/failover.md b/docs/content/documentation/connect/failover.md
new file mode 100644
index 0000000000..ce8c535133
--- /dev/null
+++ b/docs/content/documentation/connect/failover.md
@@ -0,0 +1,23 @@
+---
+title: "Connection Fail-over"
+---
+
+
+To support simple connection fail-over it is possible to define multiple endpoints (host and port pairs) in the connection 
+url separated by commas. The driver will try once to connect to each of them in order until the connection succeeds.
+If none succeeds a normal connection exception is thrown.
+
+The syntax for the connection url is: `jdbc:postgresql://host1:port1,host2:port2/database`
+
+The simple connection fail-over is useful when running against a high availability postgres installation that has identical 
+data on each node. For example streaming replication postgres or postgres-xc cluster.
+
+For example an application can create two connection pools.
+One data source is for writes, another for reads. The write pool limits connections only to a primary node:`jdbc:postgresql://node1,node2,node3/accounting?targetServerType=primary` .
+
+And the read pool balances connections between secondary nodes, but allows connections also to a primary if no secondaries
+are available: `jdbc:postgresql://node1,node2,node3/accounting?targetServerType=preferSecondary&loadBalanceHosts=true`
+
+If a secondary fails, all secondaries in the list will be tried first. In the case that there are no available secondaries
+the primary will be tried. If all the servers are marked as "can't connect" in the cache then an attempt
+will be made to connect to all the hosts in the URL, in order.
diff --git a/docs/content/documentation/connect/unix-sockets.md b/docs/content/documentation/connect/unix-sockets.md
new file mode 100644
index 0000000000..7542597835
--- /dev/null
+++ b/docs/content/documentation/connect/unix-sockets.md
@@ -0,0 +1,24 @@
+---
+title: "Unix sockets"
+---
+
+
+By adding junixsocket you can obtain a socket factory that works with the driver.
+Code can be found [here](https://github.com/kohlschutter/junixsocket) and instructions 
+[here](https://kohlschutter.github.io/junixsocket/dependency.html)
+
+Dependencies for junixsocket are :
+
+```xml
+<dependency>
+  <groupId>com.kohlschutter.junixsocket</groupId>
+  <artifactId>junixsocket-core</artifactId>
+  <version>2.5.1</version>
+</dependency>
+```
+
+Simply add  `?socketFactory=org.newsclub.net.unix.AFUNIXSocketFactory$FactoryArg&socketFactoryArg=[path-to-the-unix-socket]` 
+to the connection URL.
+
+For many distros the default path is /var/run/postgresql/.s.PGSQL.5432
+
diff --git a/docs/content/documentation/use.md b/docs/content/documentation/connect/url-syntax.md
similarity index 91%
rename from docs/content/documentation/use.md
rename to docs/content/documentation/connect/url-syntax.md
index ffb12a5a01..4976d8d75d 100644
--- a/docs/content/documentation/use.md
+++ b/docs/content/documentation/connect/url-syntax.md
@@ -1,38 +1,10 @@
 ---
-title: "Initializing the Driver"
-date: 2022-06-19T22:46:55+05:30
-draft: false
-weight: 2
-toc: true
+title: "Connecting to the Database"
 aliases:
+    - "/documentation/use/"
     - "/documentation/head/connect.html"
 ---
 
-This section describes how to load and initialize the JDBC driver in your programs.
-
-## Importing JDBC
-
-Any source file that uses JDBC needs to import the `java.sql` package, using:
-
-```java
-import java.sql.*;
-```
-
-> **NOTE**
->
-> You should not import the `org.postgresql` package unless you are using PostgreSQL® extensions to the JDBC API.
-
-## Loading the Driver
-
-Applications do not need to explicitly load the `org.postgresql.Driver` class because the pgJDBC driver jar supports the Java Service Provider mechanism. The driver will be loaded by the JVM when the application connects to PostgreSQL® (as long as the driver's jar file is on the classpath).
-
-> **NOTE**
->
-> Prior to Java 1.6, the driver had to be loaded by the application: either by calling `Class.forName("org.postgresql.Driver");` or by passing the driver class name as a JVM parameter `java -Djdbc.drivers=org.postgresql.Driver example.ImageViewer`
-
-These older methods of loading the driver are still supported, but they are no longer necessary.
-
-## Connecting to the Database
 
 With JDBC, a database is represented by a URL (Uniform Resource Locator). With PostgreSQL®, this takes one of the following forms:
 
@@ -473,44 +445,3 @@ During SCRAM-SHA-256 authentication, the server sends the iteration count used t
 This limits client CPU exposure if a malicious or compromised server sends an excessively large iteration count.
 A value of zero disables this check.
 
-### Unix sockets
-
-By adding junixsocket you can obtain a socket factory that works with the driver.
-Code can be found [here](https://github.com/kohlschutter/junixsocket) and instructions 
-[here](https://kohlschutter.github.io/junixsocket/dependency.html)
-
-Dependencies for junixsocket are :
-
-```xml
-<dependency>
-  <groupId>com.kohlschutter.junixsocket</groupId>
-  <artifactId>junixsocket-core</artifactId>
-  <version>2.5.1</version>
-</dependency>
-```
-
-Simply add  `?socketFactory=org.newsclub.net.unix.AFUNIXSocketFactory$FactoryArg&socketFactoryArg=[path-to-the-unix-socket]` 
-to the connection URL.
-
-For many distros the default path is /var/run/postgresql/.s.PGSQL.5432
-
-### Connection Fail-over
-
-To support simple connection fail-over it is possible to define multiple endpoints (host and port pairs) in the connection 
-url separated by commas. The driver will try once to connect to each of them in order until the connection succeeds.
-If none succeeds a normal connection exception is thrown.
-
-The syntax for the connection url is: `jdbc:postgresql://host1:port1,host2:port2/database`
-
-The simple connection fail-over is useful when running against a high availability postgres installation that has identical 
-data on each node. For example streaming replication postgres or postgres-xc cluster.
-
-For example an application can create two connection pools.
-One data source is for writes, another for reads. The write pool limits connections only to a primary node:`jdbc:postgresql://node1,node2,node3/accounting?targetServerType=primary` .
-
-And the read pool balances connections between secondary nodes, but allows connections also to a primary if no secondaries
-are available: `jdbc:postgresql://node1,node2,node3/accounting?targetServerType=preferSecondary&loadBalanceHosts=true`
-
-If a secondary fails, all secondaries in the list will be tried first. In the case that there are no available secondaries
-the primary will be tried. If all the servers are marked as "can't connect" in the cache then an attempt
-will be made to connect to all the hosts in the URL, in order.
diff --git a/docs/content/documentation/data-types/_index.md b/docs/content/documentation/data-types/_index.md
new file mode 100644
index 0000000000..4b6f980dd3
--- /dev/null
+++ b/docs/content/documentation/data-types/_index.md
@@ -0,0 +1,4 @@
+---
+title: "Data types"
+weight: 50
+---
diff --git a/docs/content/documentation/data-types/arrays.md b/docs/content/documentation/data-types/arrays.md
new file mode 100644
index 0000000000..5d093d37a6
--- /dev/null
+++ b/docs/content/documentation/data-types/arrays.md
@@ -0,0 +1,27 @@
+---
+title: "Arrays"
+---
+
+
+PostgreSQL® provides robust support for array data types as column types, function arguments
+and criteria in where clauses. There are several ways to create arrays with pgJDBC.
+
+The [java.sql. Connection.createArrayOf(String, Object\[\])](https://docs.oracle.com/javase/8/docs/api/java/sql/Connection.html#createArrayOf-java.lang.String-java.lang.Object:A-) can be used to create an [java.sql. Array](https://docs.oracle.com/javase/8/docs/api/java/sql/Array.html) from `Object[]` instances (Note: this includes both primitive and object multi-dimensional arrays).
+A similar method `org.postgresql.PGConnection.createArrayOf(String, Object)` provides support for primitive array types.
+The `java.sql.Array` object returned from these methods can be used in other methods, such as
+[PreparedStatement.setArray(int, Array)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setArray-int-java.sql.Array-).
+
+The following types of arrays support binary representation in requests and can be used in `PreparedStatement.setObject`
+
+|Java Type | Supported binary PostgreSQL® Types | Default PostgreSQL® Type|
+|--- | --- | ---|
+|`short[]` , `Short[]` | `int2[]` | `int2[]`|
+|`int[]` , `Integer[]` | `int4[]` | `int4[]`|
+|`long[]` , `Long[]` | `int8[]` | `int8[]`|
+|`float[]` , `Float[]` | `float4[]` | `float4[]`|
+|`double[]` , `Double[]` | `float8[]` | `float8[]`|
+|`boolean[]` , `Boolean[]` | `bool[]` | `bool[]`|
+|`String[]` | `varchar[]` , `text[]` | `varchar[]`|
+|`byte[][]` | `bytea[]` | `bytea[]`|
+
+
diff --git a/docs/content/documentation/binary-data.md b/docs/content/documentation/data-types/binary-bytea.md
similarity index 99%
rename from docs/content/documentation/binary-data.md
rename to docs/content/documentation/data-types/binary-bytea.md
index 47113ab57d..368d761f1c 100644
--- a/docs/content/documentation/binary-data.md
+++ b/docs/content/documentation/data-types/binary-bytea.md
@@ -4,6 +4,8 @@ date: 2022-06-19T22:46:55+05:30
 draft: false
 weight: 6
 toc: false
+aliases:
+    - "/documentation/binary-data/"
 ---
 
 PostgreSQL® provides two distinct ways to store binary data.  Binary data can be stored in a table using the data type
diff --git a/docs/content/documentation/data-types/date-time.md b/docs/content/documentation/data-types/date-time.md
new file mode 100644
index 0000000000..75db524fb3
--- /dev/null
+++ b/docs/content/documentation/data-types/date-time.md
@@ -0,0 +1,51 @@
+---
+title: "Using Java 8 Date and Time classes"
+---
+
+
+The PostgreSQL® JDBC driver implements native support for the [Java 8 Date and Time API](http://www.oracle.com/technetwork/articles/java/jf14-date-time-2125367.html)(JSR-310) using JDBC 4.2.
+
+##### Table 5.1. Supported Java 8 Date and Time classes
+
+|PostgreSQL®|Java SE 8|
+|---|---|
+|DATE|LocalDate|
+|TIME [ WITHOUT TIME ZONE ]|LocalTime|
+|TIMESTAMP [ WITHOUT TIME ZONE ]|LocalDateTime|
+|TIMESTAMP WITH TIME ZONE|OffsetDateTime|
+
+This is closely aligned with tables B-4 and B-5 of the JDBC 4.2 specification.
+
+> **Note**
+>
+> `ZonedDateTime` , `Instant` and `OffsetTime / TIME WITH TIME ZONE` are not supported. Also note that all `OffsetDateTime` instances will have be in UTC (have offset 0). This is because the backend stores them as UTC.
+
+**Example 5.2. Reading Java 8 Date and Time values using JDBC**
+
+```java
+Statement st = conn.createStatement();
+ResultSet rs = st.executeQuery("SELECT * FROM mytable WHERE columnfoo = 500");
+while (rs.next()) {
+    System.out.print("Column 1 returned ");
+    LocalDate localDate = rs.getObject(1, LocalDate.class);
+    System.out.println(localDate);
+}
+rs.close();
+st.close();
+```
+
+For other data types simply pass other classes to `#getObject` .
+
+> **Note**
+>
+> The Java data types needs to match the SQL data types in table 7.1.
+
+##### Example 5.3. Writing Java 8 Date and Time values using JDBC
+
+```java
+LocalDate localDate = LocalDate.now();
+PreparedStatement st = conn.prepareStatement("INSERT INTO mytable (columnfoo) VALUES (?)");
+st.setObject(1, localDate);
+st.executeUpdate();
+st.close();
+```
diff --git a/docs/content/documentation/data-types/geometric.md b/docs/content/documentation/data-types/geometric.md
new file mode 100644
index 0000000000..b8b3822773
--- /dev/null
+++ b/docs/content/documentation/data-types/geometric.md
@@ -0,0 +1,54 @@
+---
+title: "Geometric Data Types"
+---
+
+
+PostgreSQL® has a set of data types that can store geometric features into a table. These include single points, lines, and polygons.  We support these types in Java with the org.postgresql.geometric package. Please consult the Javadoc mentioned in [Further Reading](/documentation/reading) for details of available classes and features.
+
+##### Example 9.1. Using the CIRCLE datatype JDBC
+
+```java
+import java.sql.*;
+
+import org.postgresql.geometric.PGpoint;
+import org.postgresql.geometric.PGcircle;
+
+public class GeometricTest {
+    public static void main(String args[]) throws Exception {
+        String url = "jdbc:postgresql://localhost:5432/test";
+        try (Connection conn = DriverManager.getConnection(url, "test", "")) {
+            try (Statement stmt = conn.createStatement()) {
+                stmt.execute("CREATE TEMP TABLE geomtest(mycirc circle)");
+            }
+            insertCircle(conn);
+            retrieveCircle(conn);
+        }
+    }
+
+    private static void insertCircle(Connection conn) throws SQLException {
+        PGpoint center = new PGpoint(1, 2.5);
+        double radius = 4;
+        PGcircle circle = new PGcircle(center, radius);
+        try (PreparedStatement ps = conn.prepareStatement("INSERT INTO geomtest(mycirc) VALUES (?)")) {
+            ps.setObject(1, circle);
+            ps.executeUpdate();
+        }
+    }
+
+    private static void retrieveCircle(Connection conn) throws SQLException {
+        try (Statement stmt = conn.createStatement()) {
+            try (ResultSet rs = stmt.executeQuery("SELECT mycirc, area(mycirc) FROM geomtest")) {
+                while (rs.next()) {
+                    PGcircle circle = (PGcircle) rs.getObject(1);
+                    double area = rs.getDouble(2);
+
+                    System.out.println("Center (X, Y) = (" + circle.center.x + ", " + circle.center.y + ")");
+                    System.out.println("Radius = " + circle.radius);
+                    System.out.println("Area = " + area);
+                }
+            }
+        }
+    }
+}
+```
+
diff --git a/docs/content/documentation/data-types/infinity.md b/docs/content/documentation/data-types/infinity.md
new file mode 100644
index 0000000000..d9035e68c5
--- /dev/null
+++ b/docs/content/documentation/data-types/infinity.md
@@ -0,0 +1,25 @@
+---
+title: "Timestamp Infinity"
+---
+
+
+The driver uses the following values to represent negative and positive infinity:
+
+| type | Negative infinity | Positive infinity |
+| ------- | ---------------------| --------------------- |
+| `LocalDateTime` | `LocalDateTime.MIN` | `LocalDateTime.MAX` |
+| `OffsetDateTime`| `OffsetDateTime.MIN` | `OffsetDateTime.MAX` |
+| `java.sql.Timestamp`| when object's millisecond value equals `PGStatement.DATE_NEGATIVE_INFINITY`| when object's millisecond value equals `PGStatement.DATE_POSITIVE_INFINITY` |
+
+#### ResultSet example
+
+```java
+java.sql.Timestamp ts = myResultSet.getTimestamp("mycol");
+
+if (ts.getTime() == PGStatement.DATE_NEGATIVE_INFINITY) {
+  // The value in the database is '-infinity'
+}
+if (ts.getTime() == PGStatement.DATE_POSITIVE_INFINITY) {
+  // The value in the database is 'infinity'
+}
+```
diff --git a/docs/content/documentation/data-types/large-objects.md b/docs/content/documentation/data-types/large-objects.md
new file mode 100644
index 0000000000..24024a925a
--- /dev/null
+++ b/docs/content/documentation/data-types/large-objects.md
@@ -0,0 +1,15 @@
+---
+title: "Large Objects"
+---
+
+
+Large objects are supported in the standard JDBC specification. However, that
+interface is limited, and the API provided by PostgreSQL® allows for random
+access to the objects contents, as if it was a local file.
+
+The org.postgresql.largeobject package provides to Java the libpq C interface's
+large object API. It consists of two classes, `LargeObjectManager` , which deals
+with creating, opening and deleting large objects, and `LargeObject` which deals
+with an individual object.  For an example usage of this API, please see
+[Processing Binary Data in JDBC](/documentation/binary-data/#example71processing-binary-data-in-jdbc).
+
diff --git a/docs/content/documentation/getting-started/_index.md b/docs/content/documentation/getting-started/_index.md
new file mode 100644
index 0000000000..ca2b97cc1f
--- /dev/null
+++ b/docs/content/documentation/getting-started/_index.md
@@ -0,0 +1,4 @@
+---
+title: "Getting started"
+weight: 10
+---
diff --git a/docs/content/documentation/getting-started/getting-the-driver.md b/docs/content/documentation/getting-started/getting-the-driver.md
new file mode 100644
index 0000000000..4e3ab061d0
--- /dev/null
+++ b/docs/content/documentation/getting-started/getting-the-driver.md
@@ -0,0 +1,41 @@
+---
+title: "Getting the Driver"
+aliases:
+    - "/documentation/setup/"
+    - "/documentation/head/setup.html"
+---
+
+This section describes the steps you need to take before you can write or run programs that use the JDBC interface.
+
+
+Precompiled versions of the driver can be downloaded from the [PostgreSQL® JDBC web site](https://jdbc.postgresql.org).
+
+Alternatively you can build the driver from source, but you should only need to do this if you are making changes to the source code. To build the JDBC driver, you need gradle and a JDK (currently at least jdk1.8).
+
+If you have several Java compilers installed, maven will use the first one on the path. To use a different one set `JAVA_HOME` to the Java version you wish to use. For example, to use a different JDK than the default, this may work:
+
+```java
+ JAVA_HOME=/usr/local/jdk1.8.0_45
+ ```
+
+To compile the driver simply run **`gradlew assemble`** or **`gradlew build`** if you want to run the tests in the top level directory.
+
+> **NOTE**
+>
+> If you want to skip test execution, add the option `-DskipTests`. The compiled driver will be placed in `pgjdbc/build/libs/postgresql-MM.nn.pp.jar`
+
+Where MM is the major version, nn is the minor version and pp is the patch version. Versions for JDBC3 and lower can be found [here](https://repo1.maven.org/maven2/org/postgresql/postgresql/9.2-1003-jdbc3/)
+
+This is a very brief outline of how to build the driver. Much more detailed information can be found on the [github repo](https://github.com/pgjdbc/pgjdbc/blob/master/CONTRIBUTING.md)
+
+Even though the JDBC driver should be built with Gradle, for situations, where use of Gradle is not possible, e.g.,
+when building pgJDBC for distributions, the pgJDBC Gradle build provides a convenience source release artifact `*-src.tar.gz` - a Maven-based project.
+The Maven-based project contains a version of the JDBC driver with complete functionality, which can be used in production and is still validly buildable
+within the Maven build environment.
+
+The Maven-based project is created with **`gradlew -d :postgresql:sourceDistribution -Prelease -Psigning.gpg.enabled=OFF`**.
+The produced `*-src.tar.gz` can be then found in `pgjdbc/build/distributions/` directory. JDBC driver can be built from the Maven-based project with **mvn package** or,
+when the tests are to be skipped, with **`mvn -DskipTests package`**.
+
+Source files `*-src.tar.gz`'s are released in the [Maven central repository](https://repo1.maven.org/maven2/org/postgresql/postgresql/).
+
diff --git a/docs/content/documentation/getting-started/importing-jdbc.md b/docs/content/documentation/getting-started/importing-jdbc.md
new file mode 100644
index 0000000000..257ccca518
--- /dev/null
+++ b/docs/content/documentation/getting-started/importing-jdbc.md
@@ -0,0 +1,17 @@
+---
+title: "Importing JDBC"
+---
+
+This section describes how to load and initialize the JDBC driver in your programs.
+
+
+Any source file that uses JDBC needs to import the `java.sql` package, using:
+
+```java
+import java.sql.*;
+```
+
+> **NOTE**
+>
+> You should not import the `org.postgresql` package unless you are using PostgreSQL® extensions to the JDBC API.
+
diff --git a/docs/content/documentation/getting-started/loading-the-driver.md b/docs/content/documentation/getting-started/loading-the-driver.md
new file mode 100644
index 0000000000..c9bcf63a1e
--- /dev/null
+++ b/docs/content/documentation/getting-started/loading-the-driver.md
@@ -0,0 +1,13 @@
+---
+title: "Loading the Driver"
+---
+
+
+Applications do not need to explicitly load the `org.postgresql.Driver` class because the pgJDBC driver jar supports the Java Service Provider mechanism. The driver will be loaded by the JVM when the application connects to PostgreSQL® (as long as the driver's jar file is on the classpath).
+
+> **NOTE**
+>
+> Prior to Java 1.6, the driver had to be loaded by the application: either by calling `Class.forName("org.postgresql.Driver");` or by passing the driver class name as a JVM parameter `java -Djdbc.drivers=org.postgresql.Driver example.ImageViewer`
+
+These older methods of loading the driver are still supported, but they are no longer necessary.
+
diff --git a/docs/content/documentation/getting-started/server-prep.md b/docs/content/documentation/getting-started/server-prep.md
new file mode 100644
index 0000000000..18aafa2a22
--- /dev/null
+++ b/docs/content/documentation/getting-started/server-prep.md
@@ -0,0 +1,13 @@
+---
+title: "Preparing the Database Server for JDBC"
+---
+
+
+Out of the box, Java does not support unix sockets so the PostgreSQL® server must be configured to allow TCP/IP connections. Starting with server version 8.0 TCP/IP connections are allowed from `localhost` . To allow connections to other interfaces
+than the loopback interface, you must modify the `postgresql.conf` file's `listen_addresses` setting.
+
+Once you have made sure the server is correctly listening for TCP/IP connections the next step is to verify that users are allowed to connect to the server. Client authentication is setup in `pg_hba.conf` . Refer to the main PostgreSQL® [documentation](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html) for details .
+
+## Creating a Database
+
+When creating a database to be accessed via JDBC it is important to select an appropriate encoding for your data. Many other client interfaces do not care what data you send back and forth, and will allow you to do inappropriate things, but Java makes sure that your data is correctly encoded.  Do not use a database that uses the `SQL_ASCII` encoding. This is not a real encoding and you will have problems the moment you store data in it that does not fit in the seven bit ASCII character set. If you do not know what your encoding will be or are otherwise unsure about what you will be storing the `UNICODE` encoding is a reasonable default to use.
diff --git a/docs/content/documentation/getting-started/setting-up-the-class-path.md b/docs/content/documentation/getting-started/setting-up-the-class-path.md
new file mode 100644
index 0000000000..c6e26ba5cc
--- /dev/null
+++ b/docs/content/documentation/getting-started/setting-up-the-class-path.md
@@ -0,0 +1,18 @@
+---
+title: "Setting up the Class Path"
+---
+
+
+To use the driver, the JAR archive named `postgresql-MM.nn.pp.jar` needs to be included in the class path, either by putting it in the `CLASSPATH` environment variable, or by using flags on the **java** command line.
+
+For instance, assume we have an application that uses the JDBC driver to access a database, and that application is installed as `/usr/local/lib/myapp.jar` . The PostgreSQL® JDBC driver installed as `/usr/local/pgsql/share/java/postgresql-MM.nn.pp.jar` .
+To run the application, we would use:
+
+```bash
+export CLASSPATH=/usr/local/lib/myapp.jar:/usr/local/pgsql/share/java/postgresql-42.5.0.jar:. java MyApp
+```
+
+Current Java applications will likely use maven, gradle or some other package manager. [Use this to search](https://mvnrepository.com/artifact/org.postgresql/postgresql) for the latest jars and how to include them in your project
+
+Loading the driver from within the application is covered in [Initializing the Driver](/documentation/use/).
+
diff --git a/docs/content/documentation/postgresql-features/_index.md b/docs/content/documentation/postgresql-features/_index.md
new file mode 100644
index 0000000000..9af586d9ce
--- /dev/null
+++ b/docs/content/documentation/postgresql-features/_index.md
@@ -0,0 +1,4 @@
+---
+title: "PostgreSQL features"
+weight: 60
+---
diff --git a/docs/content/documentation/postgresql-features/copy.md b/docs/content/documentation/postgresql-features/copy.md
new file mode 100644
index 0000000000..0545097db0
--- /dev/null
+++ b/docs/content/documentation/postgresql-features/copy.md
@@ -0,0 +1,61 @@
+---
+title: "CopyManager"
+---
+
+The driver provides an extension for accessing `COPY`. Copy is an extension that PostreSQL provides. see [Copy](https://www.postgresql.org/docs/current/sql-copy.html)
+
+#### Example 9.15 Copying Data in
+```java
+
+/*
+* DDL for code below
+* create table copytest (stringvalue text, intvalue int, numvalue numeric(5,2));
+*/
+private static String[] origData =
+            {"First Row\t1\t1.10\n",
+                    "Second Row\t2\t-22.20\n",
+                    "\\N\t\\N\t\\N\n",
+                    "\t4\t444.40\n"};
+private int dataRows = origData.length;
+private String sql = "COPY copytest FROM STDIN";
+
+try (Connection con = DriverManager.getConnection(url, "postgres", "somepassword")){
+    PGConnection pgConnection = con.unwrap(org.postgresql.PGConnection.class);
+    CopyManager copyAPI = pgConnection.getCopyAPI();
+    CopyIn cp = copyAPI.copyIn(sql);
+
+    for (String anOrigData : origData) {
+        byte[] buf = anOrigData.getBytes();
+        cp.writeToCopy(buf, 0, buf.length);
+    }
+
+    long updatedRows = cp.endCopy();
+    long handledRowCount = cp.getHandledRowCount();
+    System.err.println(String.format("copy Updated %d Rows, and handled %d rows", updatedRows, handledRowCount));
+
+    int rowCount = getCount(con);
+    System.err.println(rowCount);
+
+}
+
+``` 
+
+#### Example 9.16 Copying Data out
+
+```java
+String sql = "COPY copytest TO STDOUT";
+try (Connection con = DriverManager.getConnection(url, "postgres", "somepassword")){
+    PGConnection pgConnection = con.unwrap(org.postgresql.PGConnection.class);
+    CopyManager copyAPI = pgConnection.getCopyAPI();
+    CopyOut cp = copyAPI.copyOut(sql);
+    int count = 0;
+    byte[] buf;  // This is a relatively simple example. buf will contain rows from the database
+
+    while ((buf = cp.readFromCopy()) != null) {
+        count++;
+    }
+    long rowCount = cp.getHandledRowCount();
+}
+```
+
+More examples can be found in the [Copy Test Code](https://github.com/pgjdbc/pgjdbc/blob/master/pgjdbc/src/test/java/org/postgresql/test/jdbc2/CopyTest.java)
diff --git a/docs/content/documentation/postgresql-features/extensions-api.md b/docs/content/documentation/postgresql-features/extensions-api.md
new file mode 100644
index 0000000000..e390ae9117
--- /dev/null
+++ b/docs/content/documentation/postgresql-features/extensions-api.md
@@ -0,0 +1,19 @@
+---
+title: "Accessing the Extensions"
+aliases:
+    - "/documentation/server-prepare/"
+---
+
+
+PostgreSQL® is an extensible database system. You can add your own functions to the server, which can then be called from queries, or even add your own data types. As these are facilities unique to PostgreSQL®, we support them from Java, with a set of extension APIs. Some features within the core of the standard driver actually use these extensions to implement Large Objects, etc.
+
+
+To access some of the extensions, you need to use some extra methods in the `org.postgresql.PGConnection` class. In this case, you would need to cast the return value of `Driver.getConnection()` . For example:
+
+```java
+Connection db = Driver.getConnection(url, username, password);
+// ...
+// later on
+Fastpath fp = db.unwrap(org.postgresql.PGConnection.class).getFastpathAPI();
+```
+
diff --git a/docs/content/documentation/postgresql-features/listen-notify.md b/docs/content/documentation/postgresql-features/listen-notify.md
new file mode 100644
index 0000000000..d442c37004
--- /dev/null
+++ b/docs/content/documentation/postgresql-features/listen-notify.md
@@ -0,0 +1,103 @@
+---
+title: "Listen / Notify"
+---
+
+
+Listen and Notify provide a simple form of signal or interprocess communication mechanism for a collection of processes accessing the same PostgreSQL® database. For more information on notifications consult the main server documentation. This section only deals with the JDBC specific aspects of notifications.
+
+Standard `LISTEN` , `NOTIFY` , and `UNLISTEN` commands are issued via the standard `Statement` interface. To retrieve and process retrieved notifications the `Connection` must be cast to the PostgreSQL® specific extension interface `PGConnection` . From there the `getNotifications()` method can be used to retrieve any outstanding notifications.
+
+> **NOTE**
+>
+> A key limitation of the JDBC driver is that it cannot receive asynchronous notifications and must poll the backend to check if any notifications were issued. A timeout can be given to the poll function, but then the execution of statements from other threads will block.
+
+##### Example 9.2. Receiving Notifications
+
+```java
+import java.sql.*;
+
+public class NotificationTest {
+    public static void main(String args[]) throws Exception {
+        Class.forName("org.postgresql.Driver");
+        String url = "jdbc:postgresql://localhost:5432/test";
+
+        // Create two distinct connections, one for the notifier
+        // and another for the listener to show the communication
+        // works across connections although this example would
+        // work fine with just one connection.
+
+        Connection lConn = DriverManager.getConnection(url, "test", "");
+        Connection nConn = DriverManager.getConnection(url, "test", "");
+
+        // Create two threads, one to issue notifications and
+        // the other to receive them.
+
+        Listener listener = new Listener(lConn);
+        Notifier notifier = new Notifier(nConn);
+        listener.start();
+        notifier.start();
+    }
+}
+
+class Listener extends Thread {
+    private Connection conn;
+    private org.postgresql.PGConnection pgconn;
+
+    Listener(Connection conn) throws SQLException {
+        this.conn = conn;
+        this.pgconn = conn.unwrap(org.postgresql.PGConnection.class);
+        Statement stmt = conn.createStatement();
+        stmt.execute("LISTEN mymessage");
+        stmt.close();
+    }
+
+    public void run() {
+        try {
+            while (true) {
+                org.postgresql.PGNotification notifications[] = pgconn.getNotifications();
+
+                // If this thread is the only one that uses the connection, a timeout can be used to
+                // receive notifications immediately:
+                // org.postgresql.PGNotification notifications[] = pgconn.getNotifications(10000);
+
+                for (int i = 0; i < notifications.length; i++) {
+                    System.out.println("Got notification: " + notifications[i].getName());
+                }
+
+                // wait a while before checking again for new
+                // notifications
+
+                Thread.sleep(500);
+            }
+        } catch (SQLException sqle) {
+            sqle.printStackTrace();
+        } catch (InterruptedException ie) {
+            ie.printStackTrace();
+        }
+    }
+}
+
+class Notifier extends Thread {
+    private Connection conn;
+
+    public Notifier(Connection conn) {
+        this.conn = conn;
+    }
+
+    public void run() {
+        while (true) {
+            try {
+                Statement stmt = conn.createStatement();
+                stmt.execute("NOTIFY mymessage");
+                stmt.close();
+                Thread.sleep(2000);
+            } catch (SQLException sqle) {
+                sqle.printStackTrace();
+            } catch (InterruptedException ie) {
+                ie.printStackTrace();
+            }
+        }
+    }
+}
+```
+
diff --git a/docs/content/documentation/postgresql-features/parameter-status.md b/docs/content/documentation/postgresql-features/parameter-status.md
new file mode 100644
index 0000000000..dd7cab1a8a
--- /dev/null
+++ b/docs/content/documentation/postgresql-features/parameter-status.md
@@ -0,0 +1,46 @@
+---
+title: "Parameter Status Messages"
+---
+
+
+PostgreSQL® supports server parameters, also called server variables or, internally, Grand Unified Configuration (GUC) variables.
+These variables are manipulated by the `SET` command, `postgresql.conf` , `ALTER SYSTEM SET` , `ALTER USER SET`, ` ALTER DATABASE SET `,
+the `set_config(...)` SQL-callable function, etc. See [The PostgreSQL manual](https://www.postgresql.org/docs/current/config-setting.html).
+
+For a subset of these variables the server will *automatically report changes to the value to the client driver and application*.
+These variables are known internally as `GUC_REPORT` variables after the name of the flag that enables the functionality.
+
+The server keeps track of all the variable scopes and reports when a variable reverts to a prior value, so the client doesn't
+have to guess what the current value is and whether some server-side function could've changed it.  Whenever the value changes,
+no matter why or how it changes, the server reports the new effective value in a *Parameter Status* protocol message to the client.
+pgJDBC uses many of these reports internally.
+
+As of pgJDBC 42.2.6, it also exposes the parameter status information to user applications via the PGConnection extensions interface.
+
+## Methods
+
+Two methods on `org.postgresql.PGConnection` provide the client interface to reported parameters. Parameter names are
+case-insensitive and case-preserving.
+
+* `Map PGConnection.getParameterStatuses()` - return a map of all reported parameters and their values.
+
+* `String PGConnection.getParameterStatus()` - shorthand to retrieve one value by name, or null if no value has been reported.
+
+See the `PGConnection` JavaDoc for details.
+
+## Example
+
+If you're working directly with a `java.sql.Connection` you can
+
+```java
+import org.postgresql.PGConnection;
+
+void my_function(Connection conn) {
+    System.out.println("My application name is " + ((PGConnection) conn).getParameterStatus("application_name"));
+}
+```
+
+## Other client drivers
+
+The `libpq` equivalent is the `PQparameterStatus(...)` API function.
+
diff --git a/docs/content/documentation/postgresql-features/replication.md b/docs/content/documentation/postgresql-features/replication.md
new file mode 100644
index 0000000000..bca6a0beb2
--- /dev/null
+++ b/docs/content/documentation/postgresql-features/replication.md
@@ -0,0 +1,372 @@
+---
+title: "Physical and Logical replication API"
+---
+
+
+Postgres 9.4 (released in December 2014) introduced a new feature called logical replication. Logical replication allows
+changes from a database to be streamed in real-time to an external system. The difference between physical replication and
+logical replication is that logical replication sends data over in a logical format whereas physical replication sends data
+over in a binary format. Additionally logical replication can send over a single table, or database. Binary replication
+replicates the entire cluster in an all or nothing fashion; which is to say there is no way to get a specific table or
+database using binary replication
+
+Prior to logical replication keeping an external system synchronized in real time was problematic. The application would
+have to update/invalidate the appropriate cache entries, reindex the data in your search engine, send it to your analytics
+system, and so on.
+
+This suffers from race conditions and reliability problems. For example if slightly different data gets written to two
+different datastores (perhaps due to a bug or a race condition), the contents of the datastores will gradually drift
+apart — they will become more and more inconsistent over time. Recovering from such gradual data corruption is difficult.
+
+Logical decoding takes the database’s write-ahead log (WAL), and gives us access to row-level change events:
+every time a row in a table is inserted, updated or deleted, that’s an event. Those events are grouped by transaction,
+and appear in the order in which they were committed to the database. Aborted/rolled-back transactions
+do not appear in the stream. Thus, if you apply the change events in the same order, you end up with an exact,
+transactionally consistent copy of the database. It's looks like the Event Sourcing pattern that you previously implemented
+in your application, but now it's available out of the box from the PostgreSQL® database.
+
+For access to real-time changes PostgreSQL® provides the streaming replication protocol. Replication protocol can be physical
+or logical. Physical replication protocol is used for Master/Secondary replication. Logical replication protocol can be used
+to stream changes to an external system.
+
+Since the JDBC API does not include replication `PGConnection` implements the PostgreSQL® API
+
+## Configure database
+
+Your database should be configured to enable logical or physical replication
+
+### postgresql.conf
+
+* Property `max_wal_senders` should be at least equal to the number of replication consumers
+* Property `wal_keep_segments` should contain count wal segments that can't be removed from database.
+* Property `wal_level` for logical replication should be equal to `logical`.
+* Property `max_replication_slots` should be greater than zero for logical replication, because logical replication can't
+ work without replication slot.
+
+### pg_hba.conf
+
+Enable connect user with replication privileges to replication stream.
+
+```sql
+local   replication   all                   trust
+host    replication   all   127.0.0.1/32    md5
+host    replication   all   ::1/128         md5
+```
+
+### Configuration for examples
+
+*postgresql.conf*
+
+```ini
+max_wal_senders = 4             # max number of walsender processes
+wal_keep_segments = 4           # in logfile segments, 16MB each; 0 disables
+wal_level = logical             # minimal, replica, or logical
+max_replication_slots = 4       # max number of replication slots
+```
+
+*pg_hba.conf*
+
+```sql
+# Allow replication connections from localhost, by a user with the
+# replication privilege.
+local   replication   all                   trust
+host    replication   all   127.0.0.1/32    md5
+host    replication   all   ::1/128         md5
+```
+
+## Logical replication
+
+Logical replication uses a replication slot to reserve WAL logs on the server and also defines which decoding plugin to
+use to decode the WAL logs to the required format, for example you can decode changes as json, protobuf, etc. To demonstrate
+how to  use the pgJDBC replication API we will use the `test_decoding` plugin that is included in the `postgresql-contrib`
+package, but you can use your own decoding plugin. There are a few on github which can be used as examples.
+
+In order to use the replication API, the Connection has to be created in replication mode, in this mode the connection
+is not available to execute SQL commands, and can only be used with replication API. This is a restriction imposed by PostgreSQL®.
+
+##### Example 9.4. Create replication connection.
+
+```java
+String url = "jdbc:postgresql://localhost:5432/postgres";
+Properties props = new Properties();
+PGProperty.USER.set(props, "postgres");
+PGProperty.PASSWORD.set(props, "postgres");
+PGProperty.ASSUME_MIN_SERVER_VERSION.set(props, "9.4");
+PGProperty.REPLICATION.set(props, "database");
+PGProperty.PREFER_QUERY_MODE.set(props, "simple");
+
+Connection con = DriverManager.getConnection(url, props);
+PGConnection replConnection = con.unwrap(PGConnection.class);
+```
+
+The entire replication API is grouped in `org.postgresql.replication.PGReplicationConnection` and is available via
+`org.postgresql.PGConnection#getReplicationAPI` .
+
+Before you can start replication protocol, you need to have replication slot, which can be also created via pgJDBC API.
+
+##### Example 9.5. Create replication slot via pgJDBC API
+
+```java
+replConnection.getReplicationAPI()
+    .createReplicationSlot()
+    .logical()
+    .withSlotName("demo_logical_slot")
+    .withOutputPlugin("test_decoding")
+    .make();
+```
+
+Once we have the replication slot, we can create a ReplicationStream.
+
+##### Example 9.6. Create logical replication stream.
+
+```java
+PGReplicationStream stream =
+    replConnection.getReplicationAPI()
+        .replicationStream()
+        .logical()
+        .withSlotName("demo_logical_slot")
+        .withSlotOption("include-xids", false)
+        .withSlotOption("skip-empty-xacts", true)
+        .start();
+```
+
+The replication stream will send all changes since the creation of the replication slot or from replication slot
+restart LSN if the slot was already used for replication. You can also start streaming changes from a particular LSN position,
+in that case LSN position should be specified when you create the replication stream.
+
+##### Example 9.7. Create logical replication stream from particular position.
+
+```java
+LogSequenceNumber waitLSN = LogSequenceNumber.valueOf("6F/E3C53568");
+
+PGReplicationStream stream =
+    replConnection.getReplicationAPI()
+        .replicationStream()
+        .logical()
+        .withSlotName("demo_logical_slot")
+        .withSlotOption("include-xids", false)
+        .withSlotOption("skip-empty-xacts", true)
+        .withStartPosition(waitLSN)
+        .start();
+```
+
+Via `withSlotOption` we also can specify options that will be sent to our output plugin, this allows the user to customize decoding.
+For example, I have my own output plugin that has a property `sensitive=true` which will include changes by sensitive columns to change
+event.
+
+##### Example 9.8. Example output with include-xids=true
+
+```sql
+BEGIN 105779
+table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'previous value'
+COMMIT 105779
+```
+
+##### Example 9.9. Example output with include-xids=false
+
+```sql
+BEGIN
+table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'previous value'
+COMMIT
+```
+
+During replication the database and consumer periodically exchange ping messages. When the database or client do not receive
+ping message within the configured timeout, replication has been deemed to have stopped and an exception will be thrown and
+the database will free resources. In PostgreSQL® the ping timeout is configured by the property `wal_sender_timeout`
+(default = 60 seconds). Replication stream in pgjdc can be configured to send feedback(ping) when required or by time interval.
+It is recommended to send feedback(ping) to the database more often than configured `wal_sender_timeout` . In production systems
+I use value equal to `wal_sender_timeout / 3` . It's avoids a potential problems with networks and changes to be
+streamed without disconnects by timeout. To specify the feedback interval use `withStatusInterval` method.
+
+##### Example 9.10. Replication stream with configured feedback interval equal to 20 sec
+
+```java
+PGReplicationStream stream =
+    replConnection.getReplicationAPI()
+        .replicationStream()
+        .logical()
+        .withSlotName("demo_logical_slot")
+        .withSlotOption("include-xids", false)
+        .withSlotOption("skip-empty-xacts", true)
+        .withStatusInterval(20, TimeUnit.SECONDS)
+        .start();
+```
+
+After create `PGReplicationStream` , it's time to start receive changes in real-time.
+
+Changes can be received from stream as blocking( `org.postgresql.replication.PGReplicationStream#read` ) or as
+non-blocking (`org.postgresql.replication.PGReplicationStream#readPending` ).
+Both methods receive changes as a `java.nio.ByteBuffer` with the payload from the send output plugin. We can't receive
+part of message, only the full message that was sent by the output plugin. ByteBuffer contains message in format that is
+defined by the decoding output plugin, it can be simple String, json, or whatever the plugin determines. That's why
+pgJDBC returns the raw ByteBuffer instead of making assumptions.
+
+##### Example 9.11. Example send message from output plugin.
+
+```java
+OutputPluginPrepareWrite(ctx, true);
+appendStringInfo(ctx->out, "BEGIN %u", txn->xid);
+OutputPluginWrite(ctx, true);
+```
+
+**Example 9.12. Receive changes via replication stream.**
+
+```java
+while (true) {
+    //non blocking receive message
+    ByteBuffer msg = stream.readPending();
+
+    if (msg == null) {
+        TimeUnit.MILLISECONDS.sleep(10 L);
+        continue;
+    }
+
+    int offset = msg.arrayOffset();
+    byte[] source = msg.array();
+    int length = source.length - offset;
+    System.out.println(new String(source, offset, length));
+}
+```
+
+As mentioned previously, replication stream should periodically send feedback to the database to prevent disconnect via
+timeout. Feedback is automatically sent when `read` or `readPending` are called if it's time to send feedback. Feedback
+can also be sent via `org.postgresql.replication.PGReplicationStream#forceUpdateStatus()` regardless of the timeout. Another
+important duty of feedback is to provide the  server with the Logical Sequence Number (LSN) that has been successfully received
+and applied to consumer, it is necessary for monitoring and to truncate/archive WAL's that that are no longer needed. In the
+event that replication has been restarted, it's will start from last successfully processed LSN that was sent via feedback to database.
+
+The API provides the following feedback mechanism to indicate the successfully applied LSN by the current consumer. LSN's
+before this can be truncated or archived. `org.postgresql.replication.PGReplicationStream#setFlushedLSN` and
+`org.postgresql.replication.PGReplicationStream#setAppliedLSN` . You always can get last receive LSN via
+`org.postgresql.replication.PGReplicationStream#getLastReceiveLSN` .
+
+##### Example 9.13. Add feedback indicating a successfully process LSN
+
+```java
+while (true) {
+    //Receive last successfully send to queue message. LSN ordered.
+    LogSequenceNumber successfullySendToQueue = getQueueFeedback();
+    if (successfullySendToQueue != null) {
+        stream.setAppliedLSN(successfullySendToQueue);
+        stream.setFlushedLSN(successfullySendToQueue);
+    }
+
+    //non blocking receive message
+    ByteBuffer msg = stream.readPending();
+
+    if (msg == null) {
+        TimeUnit.MILLISECONDS.sleep(10 L);
+        continue;
+    }
+
+    asyncSendToQueue(msg, stream.getLastReceiveLSN());
+}
+```
+
+##### Example 9.14. Full example of logical replication
+
+```java
+String url = "jdbc:postgresql://localhost:5432/test";
+Properties props = new Properties();
+PGProperty.USER.set(props, "postgres");
+PGProperty.PASSWORD.set(props, "postgres");
+PGProperty.ASSUME_MIN_SERVER_VERSION.set(props, "9.4");
+PGProperty.REPLICATION.set(props, "database");
+PGProperty.PREFER_QUERY_MODE.set(props, "simple");
+
+Connection con = DriverManager.getConnection(url, props);
+PGConnection replConnection = con.unwrap(PGConnection.class);
+
+replConnection.getReplicationAPI()
+    .createReplicationSlot()
+    .logical()
+    .withSlotName("demo_logical_slot")
+    .withOutputPlugin("test_decoding")
+    .make();
+
+//some changes after create replication slot to demonstrate receive it
+sqlConnection.setAutoCommit(true);
+Statement st = sqlConnection.createStatement();
+st.execute("insert into test_logic_table(name) values('first tx changes')");
+st.close();
+
+st = sqlConnection.createStatement();
+st.execute("update test_logic_table set name = 'second tx change' where pk = 1");
+st.close();
+
+st = sqlConnection.createStatement();
+st.execute("delete from test_logic_table where pk = 1");
+st.close();
+
+PGReplicationStream stream =
+    replConnection.getReplicationAPI()
+    .replicationStream()
+    .logical()
+    .withSlotName("demo_logical_slot")
+    .withSlotOption("include-xids", false)
+    .withSlotOption("skip-empty-xacts", true)
+    .withStatusInterval(20, TimeUnit.SECONDS)
+    .start();
+
+while (true) {
+    //non blocking receive message
+    ByteBuffer msg = stream.readPending();
+
+    if (msg == null) {
+        TimeUnit.MILLISECONDS.sleep(10 L);
+        continue;
+    }
+
+    int offset = msg.arrayOffset();
+    byte[] source = msg.array();
+    int length = source.length - offset;
+    System.out.println(new String(source, offset, length));
+
+    //feedback
+    stream.setAppliedLSN(stream.getLastReceiveLSN());
+    stream.setFlushedLSN(stream.getLastReceiveLSN());
+}
+```
+
+Where output looks like this, where each line is a separate message.
+
+```sql
+BEGIN
+table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'first tx changes'
+COMMIT
+BEGIN
+table public.test_logic_table: UPDATE: pk[integer]:1 name[character varying]:'second tx change'
+COMMIT
+BEGIN
+table public.test_logic_table: DELETE: pk[integer]:1
+COMMIT
+```
+
+## Physical replication
+
+API for physical replication looks like the API for logical replication. Physical replication does not require a replication
+slot. And ByteBuffer will contain the binary form of WAL logs. The binary WAL format is a very low level API, and can change
+from version to version. That is why replication between different major PostgreSQL® versions is not possible. But physical
+replication can contain many important data, that is not available via logical replication. That is why pgJDBC contains an
+implementation for both.
+
+**Example 9.15. Use physical replication**
+
+```java
+LogSequenceNumber lsn = getCurrentLSN();
+
+Statement st = sqlConnection.createStatement();
+st.execute("insert into test_physic_table(name) values('previous value')");
+st.close();
+
+PGReplicationStream stream =
+    pgConnection
+    .getReplicationAPI()
+    .replicationStream()
+    .physical()
+    .withStartPosition(lsn)
+    .start();
+
+ByteBuffer read = stream.read();
+```
+
diff --git a/docs/content/documentation/query/_index.md b/docs/content/documentation/query/_index.md
new file mode 100644
index 0000000000..673fa13de5
--- /dev/null
+++ b/docs/content/documentation/query/_index.md
@@ -0,0 +1,4 @@
+---
+title: "Query"
+weight: 40
+---
diff --git a/docs/content/documentation/query.md b/docs/content/documentation/query/basics.md
similarity index 57%
rename from docs/content/documentation/query.md
rename to docs/content/documentation/query/basics.md
index 80da1c8918..a0d7c2273c 100644
--- a/docs/content/documentation/query.md
+++ b/docs/content/documentation/query/basics.md
@@ -1,9 +1,5 @@
 ---
-title: "Issuing a Query and Processing the Result"
-date: 2022-06-19T22:46:55+05:30
-draft: false
-weight: 4
-toc: true
+title: "Using the Statement or PreparedStatement Interface"
 aliases:
     - "/documentation/head/query.html"
 ---
@@ -40,62 +36,6 @@ rs.close();
 st.close();
 ```
 
-## Getting results based on a cursor
-
-By default, the driver collects all the results for the query at once. This can be inconvenient for large data sets so 
-the JDBC driver provides a means of basing a `ResultSet` on a database cursor and only fetching a small number of rows.
-
-A small number of rows are cached on the client side of the connection and when exhausted the next block of rows is 
-retrieved by repositioning the cursor.
-
-> **NOTE**
->
-> Cursor based `ResultSets` cannot be used in all situations. There a number of restrictions which will make the driver 
-> silently fall back to fetching the whole `ResultSet` at once.
->
-> * The connection to the server must be using the V3 protocol. This is the default for (and is only supported by) 
-> server versions 7.4 and later.
->
-> * The `Connection` must not be in autocommit mode. The backend closes cursors at the end of transactions, so in 
-> autocommit mode the backend will have closed the cursor before anything can be fetched from it.
->
-> * The `Statement` must be created with a `ResultSet` type of `ResultSet.TYPE_FORWARD_ONLY`. This is the default,
-> so no code will need to be rewritten to take advantage of this, but it also means that you cannot scroll backwards or 
-> otherwise jump around in the `ResultSet`.
->
-> * The query given must be a single statement, not multiple statements strung together with semicolons.
-
-##### Example 5.2. Setting fetch size to turn cursors on and off.
-
-Changing the code to use cursor mode is as simple as setting the fetch size of the `Statement` to the appropriate size. 
-Setting the fetch size back to 0 will cause all rows to be cached (the default behaviour).
-
-```java
-// make sure autocommit is off
-conn.setAutoCommit(false);
-Statement st = conn.createStatement();
-
-// Turn use of the cursor on.
-st.setFetchSize(50);
-ResultSet rs = st.executeQuery("SELECT * FROM mytable");
-while (rs.next()) {
-    System.out.print("a row was returned.");
-}
-rs.close();
-
-// Turn the cursor off.
-st.setFetchSize(0);
-rs = st.executeQuery("SELECT * FROM mytable");
-while (rs.next()) {
-    System.out.print("many rows were returned.");
-}
-rs.close();
-
-// Close the statement.
-st.close();
-```
-
-## Using the Statement or PreparedStatement Interface
 
 The following must be considered when using the `Statement` or `PreparedStatement` interface:
 
@@ -173,51 +113,3 @@ st.execute("DROP TABLE mytable");
 st.close();
 ```
 
-## Using Java 8 Date and Time classes
-
-The PostgreSQL® JDBC driver implements native support for the [Java 8 Date and Time API](http://www.oracle.com/technetwork/articles/java/jf14-date-time-2125367.html)(JSR-310) using JDBC 4.2.
-
-##### Table 5.1. Supported Java 8 Date and Time classes
-
-|PostgreSQL®|Java SE 8|
-|---|---|
-|DATE|LocalDate|
-|TIME [ WITHOUT TIME ZONE ]|LocalTime|
-|TIMESTAMP [ WITHOUT TIME ZONE ]|LocalDateTime|
-|TIMESTAMP WITH TIME ZONE|OffsetDateTime|
-
-This is closely aligned with tables B-4 and B-5 of the JDBC 4.2 specification.
-
-> **Note**
->
-> `ZonedDateTime` , `Instant` and `OffsetTime / TIME WITH TIME ZONE` are not supported. Also note that all `OffsetDateTime` instances will have be in UTC (have offset 0). This is because the backend stores them as UTC.
-
-**Example 5.2. Reading Java 8 Date and Time values using JDBC**
-
-```java
-Statement st = conn.createStatement();
-ResultSet rs = st.executeQuery("SELECT * FROM mytable WHERE columnfoo = 500");
-while (rs.next()) {
-    System.out.print("Column 1 returned ");
-    LocalDate localDate = rs.getObject(1, LocalDate.class);
-    System.out.println(localDate);
-}
-rs.close();
-st.close();
-```
-
-For other data types simply pass other classes to `#getObject` .
-
-> **Note**
->
-> The Java data types needs to match the SQL data types in table 7.1.
-
-##### Example 5.3. Writing Java 8 Date and Time values using JDBC
-
-```java
-LocalDate localDate = LocalDate.now();
-PreparedStatement st = conn.prepareStatement("INSERT INTO mytable (columnfoo) VALUES (?)");
-st.setObject(1, localDate);
-st.executeUpdate();
-st.close();
-```
diff --git a/docs/content/documentation/query/fetch-size.md b/docs/content/documentation/query/fetch-size.md
new file mode 100644
index 0000000000..3b18189d10
--- /dev/null
+++ b/docs/content/documentation/query/fetch-size.md
@@ -0,0 +1,58 @@
+---
+title: "Getting results based on a cursor"
+---
+
+
+By default, the driver collects all the results for the query at once. This can be inconvenient for large data sets so 
+the JDBC driver provides a means of basing a `ResultSet` on a database cursor and only fetching a small number of rows.
+
+A small number of rows are cached on the client side of the connection and when exhausted the next block of rows is 
+retrieved by repositioning the cursor.
+
+> **NOTE**
+>
+> Cursor based `ResultSets` cannot be used in all situations. There a number of restrictions which will make the driver 
+> silently fall back to fetching the whole `ResultSet` at once.
+>
+> * The connection to the server must be using the V3 protocol. This is the default for (and is only supported by) 
+> server versions 7.4 and later.
+>
+> * The `Connection` must not be in autocommit mode. The backend closes cursors at the end of transactions, so in 
+> autocommit mode the backend will have closed the cursor before anything can be fetched from it.
+>
+> * The `Statement` must be created with a `ResultSet` type of `ResultSet.TYPE_FORWARD_ONLY`. This is the default,
+> so no code will need to be rewritten to take advantage of this, but it also means that you cannot scroll backwards or 
+> otherwise jump around in the `ResultSet`.
+>
+> * The query given must be a single statement, not multiple statements strung together with semicolons.
+
+##### Example 5.2. Setting fetch size to turn cursors on and off.
+
+Changing the code to use cursor mode is as simple as setting the fetch size of the `Statement` to the appropriate size. 
+Setting the fetch size back to 0 will cause all rows to be cached (the default behaviour).
+
+```java
+// make sure autocommit is off
+conn.setAutoCommit(false);
+Statement st = conn.createStatement();
+
+// Turn use of the cursor on.
+st.setFetchSize(50);
+ResultSet rs = st.executeQuery("SELECT * FROM mytable");
+while (rs.next()) {
+    System.out.print("a row was returned.");
+}
+rs.close();
+
+// Turn the cursor off.
+st.setFetchSize(0);
+rs = st.executeQuery("SELECT * FROM mytable");
+while (rs.next()) {
+    System.out.print("many rows were returned.");
+}
+rs.close();
+
+// Close the statement.
+st.close();
+```
+
diff --git a/docs/content/documentation/escapes.md b/docs/content/documentation/query/jdbc-escapes.md
similarity index 99%
rename from docs/content/documentation/escapes.md
rename to docs/content/documentation/query/jdbc-escapes.md
index 144cae276d..400f055c82 100644
--- a/docs/content/documentation/escapes.md
+++ b/docs/content/documentation/query/jdbc-escapes.md
@@ -4,6 +4,8 @@ date: 2022-06-19T22:46:55+05:30
 draft: false
 weight: 7
 toc: true
+aliases:
+    - "/documentation/escapes/"
 ---
 
 The JDBC specification (like the ODBC specification) acknowledges the fact that some vendor specific SQL may be required
diff --git a/docs/content/documentation/query/prepared-statements.md b/docs/content/documentation/query/prepared-statements.md
new file mode 100644
index 0000000000..0f1c2068c9
--- /dev/null
+++ b/docs/content/documentation/query/prepared-statements.md
@@ -0,0 +1,323 @@
+---
+title: "Server Prepared Statements"
+---
+
+
+### Motivation
+
+The PostgreSQL® server allows clients to compile sql statements that are expected to be reused to avoid the overhead of parsing and planning the statement for every execution. This functionality is available at the SQL level via PREPARE and EXECUTE beginning with server version 7.3, and at the protocol level beginning with server version 7.4, but as Java developers we really just want to use the standard `PreparedStatement` interface.
+
+> **NOTE**
+>
+> PostgreSQL® 9.2 release notes: prepared statements used to be optimized once, without any knowledge of the parameters' values. With 9.2, the planner will use specific plans regarding to the parameters sent (the query will be planned at execution), except if the query is executed several times and the planner decides that the generic plan is not too much more expensive than the specific plans.
+
+Server side prepared statements can improve execution speed as
+
+1. It sends just statement handle (e.g. `S_1`) instead of full SQL text
+1. It enables use of binary transfer (e.g. binary int4, binary timestamps, etc); the parameters and results are much faster to parse
+1. It enables the reuse server-side execution plan
+1. The client can reuse result set column definition, so it does not have to receive and parse metadata on each execution
+
+### Activation
+
+Previous versions of the driver used PREPARE and EXECUTE to implement server-prepared statements.  
+This is supported on all server versions beginning with 7.3, but produced application-visible changes in query results, 
+such as missing ResultSet metadata and row update counts. The current driver uses the V3 protocol-level equivalents 
+which avoid these changes in query results. The Extended Query protocol prepares a temporary "unnamed statement". 
+See [Extended Query](https://www.postgresql.org/docs/current/protocol-flow.html) Section 53.2.3 for details.
+
+The driver uses the Extended Protocol **by default** when the `PreparedStatement` API is used. 
+
+An internal counter keeps track of how many times the statement has been executed and when it reaches the `prepareThreshold` (default 5)
+the driver will switch to creating a named statement and using `Prepare` and `Execute`.
+
+It is generally a good idea to reuse the same `PreparedStatement` object for performance reasons, however the driver is able to server-prepare statements automatically across `connection.prepareStatement(...)` calls.
+
+For instance:
+
+```java
+PreparedStatement ps = con.prepareStatement("select /*test*/ ?::int4");
+ps.setInt(1, 42);
+ps.executeQuery().close();
+ps.close();
+
+PreparedStatement ps = con.prepareStatement("select /*test*/ ?::int4");
+ps.setInt(1, 43);
+ps.executeQuery().close();
+ps.close();
+```
+
+is less efficient than
+
+```java
+PreparedStatement ps = con.prepareStatement("select /*test*/ ?::int4");
+
+ps.setInt(1, 42);
+ps.executeQuery().close();
+
+ps.setInt(1, 43);
+ps.executeQuery().close();
+```
+
+however pgJDBC can use server side prepared statements in both cases.
+
+> **Note**
+>
+> The `Statement` object is bound to a `Connection` , and it is not a good idea to access the same `Statement` and/or
+> `Connection` from multiple concurrent threads (except `cancel()` , `close()` , and alike cases). It might be safer to
+> just `close()` the statement rather than trying to cache it somehow.
+
+Server-prepared statements consume memory both on the client and the server, so pgJDBC limits the number of server-prepared
+statements per connection. It can be configured via `preparedStatementCacheQueries` (default `256` , the number of queries
+known to pgJDBC), and `preparedStatementCacheSizeMiB` (default `5` , that is the client side cache size in megabytes per
+connection). Only a subset of `statement cache` is server-prepared as some statements might fail to reach `prepareThreshold` .
+
+### Deactivation
+
+There might be cases when you would want to disable use of server-prepared statements. For instance, if you route
+connections through a balancer that is incompatible with server-prepared statements, you have little choice.
+
+You can disable usage of server side prepared statements by setting `prepareThreshold=0`
+
+### Corner cases
+
+#### DDL
+
+V3 protocol avoids sending column metadata on each execution, and BIND message specifies output column format.
+That creates a problem for cases like
+
+```sql
+SELECT * FROM mytable;
+ALTER mytable ADD column ...;
+SELECT * FROM mytable;
+```
+
+That results in `cached plan must not change result type` error, and it causes the transaction to fail.
+
+The recommendation is:
+
+1. Use explicit column names in the SELECT list
+2. Avoid column type alters
+
+#### DEALLOCATE ALL, DISCARD ALL
+
+There are explicit commands to deallocate all server side prepared statements. It would result in the following server-side
+error message: `prepared statement name is invalid`. Of course, it could defeat pgJDBC, however there are cases when you need
+to discard statements (e.g. after lots of DDLs)
+
+The recommendation is:
+
+1. Use simple `DEALLOCATE ALL` and/or `DISCARD ALL` commands, avoid nesting the commands into pl/pgsql or alike. 
+The driver does understand top-level DEALLOCATE/DISCARD commands, and it invalidates client-side cache as well
+2. Reconnect. The cache is per connection, so it would get invalidated if you reconnect
+
+#### set search_path = ...
+
+PostgreSQL® allows to customize `search_path` , and it provides great power to the developer. With great power the 
+following case could happen:
+
+```sql
+set search_path='app_v1';
+SELECT * FROM mytable;
+set search_path='app_v2';
+SELECT * FROM mytable; -- Does mytable mean app_v1.mytable or app_v2.mytable here?
+```
+
+Server side prepared statements are linked to database object IDs, so it could fetch data from "old" `app_v1.mytable` table.
+It is hard to tell which behaviour is expected, however pgJDBC tries to track `search_path` changes, and it invalidates
+prepare cache accordingly.
+
+The recommendation is:
+
+1. Avoid changing `search_path` often, as it invalidates server side prepared statements
+2. Use simple `set search_path...` commands, avoid nesting the commands into pl/pgsql or alike, otherwise pgJDBC won't
+be able to identify `search_path` change
+
+#### Re-execution of failed statements
+
+It is a pity that a single `cached plan must not change result type` could cause the whole transaction to fail. The driver
+could re-execute the statement automatically in certain cases.
+
+1. In case the transaction has not failed (e.g. the transaction did not exist before execution of the statement that caused
+`cached plan...` error), then pgJDBC re-executes the statement automatically. This makes the application happy, and avoids
+unnecessary errors.
+2. In case the transaction is in a failed state, there's nothing to do but rollback it. pgJDBC does have "automatic savepoint"
+feature, and it could automatically rollback and retry the statement. The behaviour is controlled via `autosave` property
+(default `never` ). The value of `conservative` would auto-rollback for the errors related to invalid server-prepared statements.
+
+> **Note**
+>
+> `autosave` might result in **severe** performance issues for long transactions, as PostgreSQL® backend is not optimized
+> for the case of long transactions and lots of savepoints.
+
+#### Replication connection
+
+PostgreSQL® replication connection does not allow to use server side prepared statements, so pgJDBC
+uses simple queries in the case where `replication` connection property is activated.
+
+#### Use of server-prepared statements for con.createStatement()
+
+By default, pgJDBC uses server-prepared statements for `PreparedStatement` only, however you might want
+to activate server side prepared statements for regular `Statement` as well. For instance, if you
+execute the same statement through `con.createStatement().executeQuery(...)` , then you might improve
+performance by caching the statement. Of course, it is better to use `PreparedStatements` explicitly,
+however the driver has an option to cache simple statements as well.
+
+You can do that by setting `preferQueryMode` to `extendedCacheEverything`.
+
+> **Note**
+>
+> the option is more of a diagnostic/debugging sort, so be careful how you use it .
+
+#### Bind placeholder datatypes
+
+The database optimizes the execution plan for given parameter types.
+Consider the below case:
+
+```sql
+-- create table rooms (id int4, name varchar);
+-- create index name__rooms on rooms(name);
+PreparedStatement ps = con.prepareStatement("select id from rooms where name=?");
+ps.setString(1, "42");
+```
+
+It works as expected, however what would happen if one uses `setInt` instead? `ps.setInt(1, 42);`
+
+Even though the result would be identical, the first variation ( `setString` case) enables the database to use index
+`name__rooms` , and the latter does not. In case the database gets `42` as integer, it uses the plan like `where cast(name as int4) = ?`.
+
+The plan has to be specific for the ( `SQL text` ; `parameter types` ) combination, so the driver has to invalidate
+server side prepared statements in case the statement is used with different parameter types.
+
+This gets especially painful for batch operations as you don't want to interrupt the batch by using alternating datatypes.
+
+The most typical case is as follows (don't ever use this in production):
+
+```java
+PreparedStatement ps = con.prepareStatement("select id from rooms where ...");
+if (param instanceof String) {
+    ps.setString(1, param);
+} else if (param instanceof Integer) {
+    ps.setInt(1, ((Integer) param).intValue());
+} else {
+    // Does it really matter which type of NULL to use?
+    // In fact, it does since data types specify which server-procedure to call
+    ps.setNull(1, Types.INTEGER);
+}
+```
+
+As you might guess, `setString` vs `setNull(..., Types.INTEGER)` result in alternating datatypes,
+and it forces the driver to invalidate and re-prepare server side statement.
+
+Recommendation is to use the consistent datatype for each bind placeholder, and use the same type
+for `setNull` .
+Check out `org.postgresql.test.jdbc2.PreparedStatementTest.testAlternatingBindType` example for more details.
+
+#### Debugging
+
+In case you run into `cached plan must not change result type` or `prepared statement \"S_2\" does not exist` the
+following might be helpful to debug the case.
+
+1. Client logging. If you add `loggerLevel=TRACE&loggerFile=pgjdbc-trace.log`, you would get trace
+of the messages send between the driver and the backend
+1. You might check `org.postgresql.test.jdbc2.AutoRollbackTest` as it verifies lots of combinations
+
+##### Client Logging
+Logging is now configured using `java.util.logging`. Create a logging.properties file in resources similar to:
+
+```java
+handlers=java.util.logging.FileHandler
+.level= INFO
+
+java.util.logging.FileHandler.level=FINEST
+java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
+java.util.logging.FileHandler.pattern=/tmp/debug.log
+
+java.util.logging.ConsoleHandler.level = INFO
+java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
+org.postgresql.level = FINEST
+```
+Which can be loaded using:
+```java
+        LogManager.getLogManager().readConfiguration(YourClass.class.getResourceAsStream("/logging.properties"));
+
+```
+
+
+##### Example 9.3. Using server side prepared statements
+
+```java
+import java.sql.*;
+
+public class ServerSidePreparedStatement {
+
+    public static void main(String args[]) throws Exception {
+        
+        String url = "jdbc:postgresql://localhost:5432/test";
+        try (Connection conn = DriverManager.getConnection(url, "test", "")){
+
+            try (PreparedStatement pstmt = conn.prepareStatement("SELECT ?")){
+
+                // cast to the pg extension interface
+                org.postgresql.PGStatement pgstmt = pstmt.unwrap(org.postgresql.PGStatement.class);
+
+                // on the third execution start using server side statements
+                pgstmt.setPrepareThreshold(3);
+
+                for (int i = 1; i <= 5; i++) {
+                    pstmt.setInt(1, i);
+                    boolean usingServerPrepare = pgstmt.isUseServerPrepare();
+                    ResultSet rs = pstmt.executeQuery();
+                    rs.next();
+                    System.out.println("Execution: " + i + ", Used server side: " + usingServerPrepare + ", Result: " + rs.getInt(1));
+                    rs.close();
+                }
+            }        
+        }
+    }
+}
+```
+
+Which produces the expected result of using server side prepared statements upon
+the third execution.
+
+|Execution|Used server side|Result|
+|---|---|---|
+|1|false|1|
+|2|false|2|
+|3|true|3|
+|4|true|4|
+|5|true|5|
+
+The example shown above requires the programmer to use PostgreSQL® specific code in a supposedly portable API which is not ideal.
+Also it sets the threshold only for that particular statement which is some extra typing if we wanted to use that threshold for
+every statement. Let's take a look at the other ways to set the threshold to enable server side prepared statements.
+There is already a hierarchy in place above a `PreparedStatement` , the `Connection` it was created from, and above that
+the source of the connection be it a `Datasource` or a URL. The server side prepared statement threshold can be set at any
+of these levels such that the value will be the default for all of its children.
+
+```java
+// pg extension interfaces
+org.postgresql.PGConnection pgconn;
+org.postgresql.PGStatement pgstmt;
+
+// set a prepared statement threshold for connections created from this url
+String url = "jdbc:postgresql://localhost:5432/test?prepareThreshold=3";
+
+// see that the connection has picked up the correct threshold from the url
+Connection conn = DriverManager.getConnection(url, "test", "");
+pgconn = conn.unwrap(org.postgresql.PGConnection.class);
+System.out.println(pgconn.getPrepareThreshold()); // Should be 3
+
+// see that the statement has picked up the correct threshold from the connection
+PreparedStatement pstmt = conn.prepareStatement("SELECT ?");
+pgstmt = pstmt.unwrap(org.postgresql.PGStatement.class);
+System.out.println(pgstmt.getPrepareThreshold()); // Should be 3
+
+// change the connection's threshold and ensure that new statements pick it up
+pgconn.setPrepareThreshold(5);
+PreparedStatement pstmt = conn.prepareStatement("SELECT ?");
+pgstmt = pstmt.unwrap(org.postgresql.PGStatement.class);
+System.out.println(pgstmt.getPrepareThreshold()); // Should be 5
+```
+
diff --git a/docs/content/documentation/callproc.md b/docs/content/documentation/query/stored-procedures.md
similarity index 99%
rename from docs/content/documentation/callproc.md
rename to docs/content/documentation/query/stored-procedures.md
index ede6e2c38b..ba339f42a1 100644
--- a/docs/content/documentation/callproc.md
+++ b/docs/content/documentation/query/stored-procedures.md
@@ -4,6 +4,8 @@ date: 2022-06-19T22:46:55+05:30
 draft: false
 weight: 5
 toc: true
+aliases:
+    - "/documentation/callproc/"
 ---
 
 PostgreSQL® supports two types of stored objects, functions that can return a result value and - starting from v11 - procedures
diff --git a/docs/content/documentation/runtime/_index.md b/docs/content/documentation/runtime/_index.md
new file mode 100644
index 0000000000..27dd4aca4a
--- /dev/null
+++ b/docs/content/documentation/runtime/_index.md
@@ -0,0 +1,4 @@
+---
+title: "Runtime"
+weight: 80
+---
diff --git a/docs/content/documentation/logging.md b/docs/content/documentation/runtime/logging.md
similarity index 98%
rename from docs/content/documentation/logging.md
rename to docs/content/documentation/runtime/logging.md
index b25eb8072b..e4415bb38e 100644
--- a/docs/content/documentation/logging.md
+++ b/docs/content/documentation/runtime/logging.md
@@ -4,6 +4,8 @@ date: 2022-06-19T22:46:55+05:30
 draft: false
 weight: 11
 toc: true
+aliases:
+    - "/documentation/logging/"
 ---
 
 The PostgreSQL® JDBC Driver supports the use of logging (or tracing) to help resolve issues with the
diff --git a/docs/content/documentation/reading.md b/docs/content/documentation/runtime/reading.md
similarity index 100%
rename from docs/content/documentation/reading.md
rename to docs/content/documentation/runtime/reading.md
diff --git a/docs/content/documentation/thread.md b/docs/content/documentation/runtime/thread.md
similarity index 100%
rename from docs/content/documentation/thread.md
rename to docs/content/documentation/runtime/thread.md
diff --git a/docs/content/documentation/security/_index.md b/docs/content/documentation/security/_index.md
new file mode 100644
index 0000000000..9dc3f126b0
--- /dev/null
+++ b/docs/content/documentation/security/_index.md
@@ -0,0 +1,4 @@
+---
+title: "Security"
+weight: 30
+---
diff --git a/docs/content/documentation/ssl.md b/docs/content/documentation/security/ssl-tls.md
similarity index 99%
rename from docs/content/documentation/ssl.md
rename to docs/content/documentation/security/ssl-tls.md
index 28fb99b4b2..1e6783cc93 100644
--- a/docs/content/documentation/ssl.md
+++ b/docs/content/documentation/security/ssl-tls.md
@@ -6,6 +6,7 @@ weight: 3
 toc: true
 aliases:
     - "/documentation/head/ssl-client.html"
+    - "/documentation/ssl/"
 ---
 
 Configuring the PostgreSQL® server for SSL is covered in the [main documentation](https://www.postgresql.org/docs/current/ssl-tcp.html), so it will not be repeated here. There are also instructions in the source [certdir](https://github.com/pgjdbc/pgjdbc/tree/master/certdir)
diff --git a/docs/content/documentation/server-prepare.md b/docs/content/documentation/server-prepare.md
deleted file mode 100644
index f9e494ca63..0000000000
--- a/docs/content/documentation/server-prepare.md
+++ /dev/null
@@ -1,1020 +0,0 @@
----
-title: "PostgreSQL® Extensions to the JDBC API"
-date: 2022-06-19T22:46:55+05:30
-draft: false
-weight: 8
-toc: true
----
-
-PostgreSQL® is an extensible database system. You can add your own functions to the server, which can then be called from queries, or even add your own data types. As these are facilities unique to PostgreSQL®, we support them from Java, with a set of extension APIs. Some features within the core of the standard driver actually use these extensions to implement Large Objects, etc.
-
-## Accessing the Extensions
-
-To access some of the extensions, you need to use some extra methods in the `org.postgresql.PGConnection` class. In this case, you would need to cast the return value of `Driver.getConnection()` . For example:
-
-```java
-Connection db = Driver.getConnection(url, username, password);
-// ...
-// later on
-Fastpath fp = db.unwrap(org.postgresql.PGConnection.class).getFastpathAPI();
-```
-
-## Timestamp Infinity
-
-The driver uses the following values to represent negative and positive infinity:
-
-| type | Negative infinity | Positive infinity |
-| ------- | ---------------------| --------------------- |
-| `LocalDateTime` | `LocalDateTime.MIN` | `LocalDateTime.MAX` |
-| `OffsetDateTime`| `OffsetDateTime.MIN` | `OffsetDateTime.MAX` |
-| `java.sql.Timestamp`| when object's millisecond value equals `PGStatement.DATE_NEGATIVE_INFINITY`| when object's millisecond value equals `PGStatement.DATE_POSITIVE_INFINITY` |
-
-#### ResultSet example
-
-```java
-java.sql.Timestamp ts = myResultSet.getTimestamp("mycol");
-
-if (ts.getTime() == PGStatement.DATE_NEGATIVE_INFINITY) {
-  // The value in the database is '-infinity'
-}
-if (ts.getTime() == PGStatement.DATE_POSITIVE_INFINITY) {
-  // The value in the database is 'infinity'
-}
-```
-## Geometric Data Types
-
-PostgreSQL® has a set of data types that can store geometric features into a table. These include single points, lines, and polygons.  We support these types in Java with the org.postgresql.geometric package. Please consult the Javadoc mentioned in [Further Reading](/documentation/reading) for details of available classes and features.
-
-##### Example 9.1. Using the CIRCLE datatype JDBC
-
-```java
-import java.sql.*;
-
-import org.postgresql.geometric.PGpoint;
-import org.postgresql.geometric.PGcircle;
-
-public class GeometricTest {
-    public static void main(String args[]) throws Exception {
-        String url = "jdbc:postgresql://localhost:5432/test";
-        try (Connection conn = DriverManager.getConnection(url, "test", "")) {
-            try (Statement stmt = conn.createStatement()) {
-                stmt.execute("CREATE TEMP TABLE geomtest(mycirc circle)");
-            }
-            insertCircle(conn);
-            retrieveCircle(conn);
-        }
-    }
-
-    private static void insertCircle(Connection conn) throws SQLException {
-        PGpoint center = new PGpoint(1, 2.5);
-        double radius = 4;
-        PGcircle circle = new PGcircle(center, radius);
-        try (PreparedStatement ps = conn.prepareStatement("INSERT INTO geomtest(mycirc) VALUES (?)")) {
-            ps.setObject(1, circle);
-            ps.executeUpdate();
-        }
-    }
-
-    private static void retrieveCircle(Connection conn) throws SQLException {
-        try (Statement stmt = conn.createStatement()) {
-            try (ResultSet rs = stmt.executeQuery("SELECT mycirc, area(mycirc) FROM geomtest")) {
-                while (rs.next()) {
-                    PGcircle circle = (PGcircle) rs.getObject(1);
-                    double area = rs.getDouble(2);
-
-                    System.out.println("Center (X, Y) = (" + circle.center.x + ", " + circle.center.y + ")");
-                    System.out.println("Radius = " + circle.radius);
-                    System.out.println("Area = " + area);
-                }
-            }
-        }
-    }
-}
-```
-
-## Large Objects
-
-Large objects are supported in the standard JDBC specification. However, that
-interface is limited, and the API provided by PostgreSQL® allows for random
-access to the objects contents, as if it was a local file.
-
-The org.postgresql.largeobject package provides to Java the libpq C interface's
-large object API. It consists of two classes, `LargeObjectManager` , which deals
-with creating, opening and deleting large objects, and `LargeObject` which deals
-with an individual object.  For an example usage of this API, please see
-[Processing Binary Data in JDBC](/documentation/binary-data/#example71processing-binary-data-in-jdbc).
-
-## Listen / Notify
-
-Listen and Notify provide a simple form of signal or interprocess communication mechanism for a collection of processes accessing the same PostgreSQL® database. For more information on notifications consult the main server documentation. This section only deals with the JDBC specific aspects of notifications.
-
-Standard `LISTEN` , `NOTIFY` , and `UNLISTEN` commands are issued via the standard `Statement` interface. To retrieve and process retrieved notifications the `Connection` must be cast to the PostgreSQL® specific extension interface `PGConnection` . From there the `getNotifications()` method can be used to retrieve any outstanding notifications.
-
-> **NOTE**
->
-> A key limitation of the JDBC driver is that it cannot receive asynchronous notifications and must poll the backend to check if any notifications were issued. A timeout can be given to the poll function, but then the execution of statements from other threads will block.
-
-##### Example 9.2. Receiving Notifications
-
-```java
-import java.sql.*;
-
-public class NotificationTest {
-    public static void main(String args[]) throws Exception {
-        Class.forName("org.postgresql.Driver");
-        String url = "jdbc:postgresql://localhost:5432/test";
-
-        // Create two distinct connections, one for the notifier
-        // and another for the listener to show the communication
-        // works across connections although this example would
-        // work fine with just one connection.
-
-        Connection lConn = DriverManager.getConnection(url, "test", "");
-        Connection nConn = DriverManager.getConnection(url, "test", "");
-
-        // Create two threads, one to issue notifications and
-        // the other to receive them.
-
-        Listener listener = new Listener(lConn);
-        Notifier notifier = new Notifier(nConn);
-        listener.start();
-        notifier.start();
-    }
-}
-
-class Listener extends Thread {
-    private Connection conn;
-    private org.postgresql.PGConnection pgconn;
-
-    Listener(Connection conn) throws SQLException {
-        this.conn = conn;
-        this.pgconn = conn.unwrap(org.postgresql.PGConnection.class);
-        Statement stmt = conn.createStatement();
-        stmt.execute("LISTEN mymessage");
-        stmt.close();
-    }
-
-    public void run() {
-        try {
-            while (true) {
-                org.postgresql.PGNotification notifications[] = pgconn.getNotifications();
-
-                // If this thread is the only one that uses the connection, a timeout can be used to
-                // receive notifications immediately:
-                // org.postgresql.PGNotification notifications[] = pgconn.getNotifications(10000);
-
-                for (int i = 0; i < notifications.length; i++) {
-                    System.out.println("Got notification: " + notifications[i].getName());
-                }
-
-                // wait a while before checking again for new
-                // notifications
-
-                Thread.sleep(500);
-            }
-        } catch (SQLException sqle) {
-            sqle.printStackTrace();
-        } catch (InterruptedException ie) {
-            ie.printStackTrace();
-        }
-    }
-}
-
-class Notifier extends Thread {
-    private Connection conn;
-
-    public Notifier(Connection conn) {
-        this.conn = conn;
-    }
-
-    public void run() {
-        while (true) {
-            try {
-                Statement stmt = conn.createStatement();
-                stmt.execute("NOTIFY mymessage");
-                stmt.close();
-                Thread.sleep(2000);
-            } catch (SQLException sqle) {
-                sqle.printStackTrace();
-            } catch (InterruptedException ie) {
-                ie.printStackTrace();
-            }
-        }
-    }
-}
-```
-
-## Server Prepared Statements
-
-### Motivation
-
-The PostgreSQL® server allows clients to compile sql statements that are expected to be reused to avoid the overhead of parsing and planning the statement for every execution. This functionality is available at the SQL level via PREPARE and EXECUTE beginning with server version 7.3, and at the protocol level beginning with server version 7.4, but as Java developers we really just want to use the standard `PreparedStatement` interface.
-
-> **NOTE**
->
-> PostgreSQL® 9.2 release notes: prepared statements used to be optimized once, without any knowledge of the parameters' values. With 9.2, the planner will use specific plans regarding to the parameters sent (the query will be planned at execution), except if the query is executed several times and the planner decides that the generic plan is not too much more expensive than the specific plans.
-
-Server side prepared statements can improve execution speed as
-
-1. It sends just statement handle (e.g. `S_1`) instead of full SQL text
-1. It enables use of binary transfer (e.g. binary int4, binary timestamps, etc); the parameters and results are much faster to parse
-1. It enables the reuse server-side execution plan
-1. The client can reuse result set column definition, so it does not have to receive and parse metadata on each execution
-
-### Activation
-
-Previous versions of the driver used PREPARE and EXECUTE to implement server-prepared statements.  
-This is supported on all server versions beginning with 7.3, but produced application-visible changes in query results, 
-such as missing ResultSet metadata and row update counts. The current driver uses the V3 protocol-level equivalents 
-which avoid these changes in query results. The Extended Query protocol prepares a temporary "unnamed statement". 
-See [Extended Query](https://www.postgresql.org/docs/current/protocol-flow.html) Section 53.2.3 for details.
-
-The driver uses the Extended Protocol **by default** when the `PreparedStatement` API is used. 
-
-An internal counter keeps track of how many times the statement has been executed and when it reaches the `prepareThreshold` (default 5)
-the driver will switch to creating a named statement and using `Prepare` and `Execute`.
-
-It is generally a good idea to reuse the same `PreparedStatement` object for performance reasons, however the driver is able to server-prepare statements automatically across `connection.prepareStatement(...)` calls.
-
-For instance:
-
-```java
-PreparedStatement ps = con.prepareStatement("select /*test*/ ?::int4");
-ps.setInt(1, 42);
-ps.executeQuery().close();
-ps.close();
-
-PreparedStatement ps = con.prepareStatement("select /*test*/ ?::int4");
-ps.setInt(1, 43);
-ps.executeQuery().close();
-ps.close();
-```
-
-is less efficient than
-
-```java
-PreparedStatement ps = con.prepareStatement("select /*test*/ ?::int4");
-
-ps.setInt(1, 42);
-ps.executeQuery().close();
-
-ps.setInt(1, 43);
-ps.executeQuery().close();
-```
-
-however pgJDBC can use server side prepared statements in both cases.
-
-> **Note**
->
-> The `Statement` object is bound to a `Connection` , and it is not a good idea to access the same `Statement` and/or
-> `Connection` from multiple concurrent threads (except `cancel()` , `close()` , and alike cases). It might be safer to
-> just `close()` the statement rather than trying to cache it somehow.
-
-Server-prepared statements consume memory both on the client and the server, so pgJDBC limits the number of server-prepared
-statements per connection. It can be configured via `preparedStatementCacheQueries` (default `256` , the number of queries
-known to pgJDBC), and `preparedStatementCacheSizeMiB` (default `5` , that is the client side cache size in megabytes per
-connection). Only a subset of `statement cache` is server-prepared as some statements might fail to reach `prepareThreshold` .
-
-### Deactivation
-
-There might be cases when you would want to disable use of server-prepared statements. For instance, if you route
-connections through a balancer that is incompatible with server-prepared statements, you have little choice.
-
-You can disable usage of server side prepared statements by setting `prepareThreshold=0`
-
-### Corner cases
-
-#### DDL
-
-V3 protocol avoids sending column metadata on each execution, and BIND message specifies output column format.
-That creates a problem for cases like
-
-```sql
-SELECT * FROM mytable;
-ALTER mytable ADD column ...;
-SELECT * FROM mytable;
-```
-
-That results in `cached plan must not change result type` error, and it causes the transaction to fail.
-
-The recommendation is:
-
-1. Use explicit column names in the SELECT list
-2. Avoid column type alters
-
-#### DEALLOCATE ALL, DISCARD ALL
-
-There are explicit commands to deallocate all server side prepared statements. It would result in the following server-side
-error message: `prepared statement name is invalid`. Of course, it could defeat pgJDBC, however there are cases when you need
-to discard statements (e.g. after lots of DDLs)
-
-The recommendation is:
-
-1. Use simple `DEALLOCATE ALL` and/or `DISCARD ALL` commands, avoid nesting the commands into pl/pgsql or alike. 
-The driver does understand top-level DEALLOCATE/DISCARD commands, and it invalidates client-side cache as well
-2. Reconnect. The cache is per connection, so it would get invalidated if you reconnect
-
-#### set search_path = ...
-
-PostgreSQL® allows to customize `search_path` , and it provides great power to the developer. With great power the 
-following case could happen:
-
-```sql
-set search_path='app_v1';
-SELECT * FROM mytable;
-set search_path='app_v2';
-SELECT * FROM mytable; -- Does mytable mean app_v1.mytable or app_v2.mytable here?
-```
-
-Server side prepared statements are linked to database object IDs, so it could fetch data from "old" `app_v1.mytable` table.
-It is hard to tell which behaviour is expected, however pgJDBC tries to track `search_path` changes, and it invalidates
-prepare cache accordingly.
-
-The recommendation is:
-
-1. Avoid changing `search_path` often, as it invalidates server side prepared statements
-2. Use simple `set search_path...` commands, avoid nesting the commands into pl/pgsql or alike, otherwise pgJDBC won't
-be able to identify `search_path` change
-
-#### Re-execution of failed statements
-
-It is a pity that a single `cached plan must not change result type` could cause the whole transaction to fail. The driver
-could re-execute the statement automatically in certain cases.
-
-1. In case the transaction has not failed (e.g. the transaction did not exist before execution of the statement that caused
-`cached plan...` error), then pgJDBC re-executes the statement automatically. This makes the application happy, and avoids
-unnecessary errors.
-2. In case the transaction is in a failed state, there's nothing to do but rollback it. pgJDBC does have "automatic savepoint"
-feature, and it could automatically rollback and retry the statement. The behaviour is controlled via `autosave` property
-(default `never` ). The value of `conservative` would auto-rollback for the errors related to invalid server-prepared statements.
-
-> **Note**
->
-> `autosave` might result in **severe** performance issues for long transactions, as PostgreSQL® backend is not optimized
-> for the case of long transactions and lots of savepoints.
-
-#### Replication connection
-
-PostgreSQL® replication connection does not allow to use server side prepared statements, so pgJDBC
-uses simple queries in the case where `replication` connection property is activated.
-
-#### Use of server-prepared statements for con.createStatement()
-
-By default, pgJDBC uses server-prepared statements for `PreparedStatement` only, however you might want
-to activate server side prepared statements for regular `Statement` as well. For instance, if you
-execute the same statement through `con.createStatement().executeQuery(...)` , then you might improve
-performance by caching the statement. Of course, it is better to use `PreparedStatements` explicitly,
-however the driver has an option to cache simple statements as well.
-
-You can do that by setting `preferQueryMode` to `extendedCacheEverything`.
-
-> **Note**
->
-> the option is more of a diagnostic/debugging sort, so be careful how you use it .
-
-#### Bind placeholder datatypes
-
-The database optimizes the execution plan for given parameter types.
-Consider the below case:
-
-```sql
--- create table rooms (id int4, name varchar);
--- create index name__rooms on rooms(name);
-PreparedStatement ps = con.prepareStatement("select id from rooms where name=?");
-ps.setString(1, "42");
-```
-
-It works as expected, however what would happen if one uses `setInt` instead? `ps.setInt(1, 42);`
-
-Even though the result would be identical, the first variation ( `setString` case) enables the database to use index
-`name__rooms` , and the latter does not. In case the database gets `42` as integer, it uses the plan like `where cast(name as int4) = ?`.
-
-The plan has to be specific for the ( `SQL text` ; `parameter types` ) combination, so the driver has to invalidate
-server side prepared statements in case the statement is used with different parameter types.
-
-This gets especially painful for batch operations as you don't want to interrupt the batch by using alternating datatypes.
-
-The most typical case is as follows (don't ever use this in production):
-
-```java
-PreparedStatement ps = con.prepareStatement("select id from rooms where ...");
-if (param instanceof String) {
-    ps.setString(1, param);
-} else if (param instanceof Integer) {
-    ps.setInt(1, ((Integer) param).intValue());
-} else {
-    // Does it really matter which type of NULL to use?
-    // In fact, it does since data types specify which server-procedure to call
-    ps.setNull(1, Types.INTEGER);
-}
-```
-
-As you might guess, `setString` vs `setNull(..., Types.INTEGER)` result in alternating datatypes,
-and it forces the driver to invalidate and re-prepare server side statement.
-
-Recommendation is to use the consistent datatype for each bind placeholder, and use the same type
-for `setNull` .
-Check out `org.postgresql.test.jdbc2.PreparedStatementTest.testAlternatingBindType` example for more details.
-
-#### Debugging
-
-In case you run into `cached plan must not change result type` or `prepared statement \"S_2\" does not exist` the
-following might be helpful to debug the case.
-
-1. Client logging. If you add `loggerLevel=TRACE&loggerFile=pgjdbc-trace.log`, you would get trace
-of the messages send between the driver and the backend
-1. You might check `org.postgresql.test.jdbc2.AutoRollbackTest` as it verifies lots of combinations
-
-##### Client Logging
-Logging is now configured using `java.util.logging`. Create a logging.properties file in resources similar to:
-
-```java
-handlers=java.util.logging.FileHandler
-.level= INFO
-
-java.util.logging.FileHandler.level=FINEST
-java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
-java.util.logging.FileHandler.pattern=/tmp/debug.log
-
-java.util.logging.ConsoleHandler.level = INFO
-java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
-org.postgresql.level = FINEST
-```
-Which can be loaded using:
-```java
-        LogManager.getLogManager().readConfiguration(YourClass.class.getResourceAsStream("/logging.properties"));
-
-```
-
-
-##### Example 9.3. Using server side prepared statements
-
-```java
-import java.sql.*;
-
-public class ServerSidePreparedStatement {
-
-    public static void main(String args[]) throws Exception {
-        
-        String url = "jdbc:postgresql://localhost:5432/test";
-        try (Connection conn = DriverManager.getConnection(url, "test", "")){
-
-            try (PreparedStatement pstmt = conn.prepareStatement("SELECT ?")){
-
-                // cast to the pg extension interface
-                org.postgresql.PGStatement pgstmt = pstmt.unwrap(org.postgresql.PGStatement.class);
-
-                // on the third execution start using server side statements
-                pgstmt.setPrepareThreshold(3);
-
-                for (int i = 1; i <= 5; i++) {
-                    pstmt.setInt(1, i);
-                    boolean usingServerPrepare = pgstmt.isUseServerPrepare();
-                    ResultSet rs = pstmt.executeQuery();
-                    rs.next();
-                    System.out.println("Execution: " + i + ", Used server side: " + usingServerPrepare + ", Result: " + rs.getInt(1));
-                    rs.close();
-                }
-            }        
-        }
-    }
-}
-```
-
-Which produces the expected result of using server side prepared statements upon
-the third execution.
-
-|Execution|Used server side|Result|
-|---|---|---|
-|1|false|1|
-|2|false|2|
-|3|true|3|
-|4|true|4|
-|5|true|5|
-
-The example shown above requires the programmer to use PostgreSQL® specific code in a supposedly portable API which is not ideal.
-Also it sets the threshold only for that particular statement which is some extra typing if we wanted to use that threshold for
-every statement. Let's take a look at the other ways to set the threshold to enable server side prepared statements.
-There is already a hierarchy in place above a `PreparedStatement` , the `Connection` it was created from, and above that
-the source of the connection be it a `Datasource` or a URL. The server side prepared statement threshold can be set at any
-of these levels such that the value will be the default for all of its children.
-
-```java
-// pg extension interfaces
-org.postgresql.PGConnection pgconn;
-org.postgresql.PGStatement pgstmt;
-
-// set a prepared statement threshold for connections created from this url
-String url = "jdbc:postgresql://localhost:5432/test?prepareThreshold=3";
-
-// see that the connection has picked up the correct threshold from the url
-Connection conn = DriverManager.getConnection(url, "test", "");
-pgconn = conn.unwrap(org.postgresql.PGConnection.class);
-System.out.println(pgconn.getPrepareThreshold()); // Should be 3
-
-// see that the statement has picked up the correct threshold from the connection
-PreparedStatement pstmt = conn.prepareStatement("SELECT ?");
-pgstmt = pstmt.unwrap(org.postgresql.PGStatement.class);
-System.out.println(pgstmt.getPrepareThreshold()); // Should be 3
-
-// change the connection's threshold and ensure that new statements pick it up
-pgconn.setPrepareThreshold(5);
-PreparedStatement pstmt = conn.prepareStatement("SELECT ?");
-pgstmt = pstmt.unwrap(org.postgresql.PGStatement.class);
-System.out.println(pgstmt.getPrepareThreshold()); // Should be 5
-```
-
-## Parameter Status Messages
-
-PostgreSQL® supports server parameters, also called server variables or, internally, Grand Unified Configuration (GUC) variables.
-These variables are manipulated by the `SET` command, `postgresql.conf` , `ALTER SYSTEM SET` , `ALTER USER SET`, ` ALTER DATABASE SET `,
-the `set_config(...)` SQL-callable function, etc. See [The PostgreSQL manual](https://www.postgresql.org/docs/current/config-setting.html).
-
-For a subset of these variables the server will *automatically report changes to the value to the client driver and application*.
-These variables are known internally as `GUC_REPORT` variables after the name of the flag that enables the functionality.
-
-The server keeps track of all the variable scopes and reports when a variable reverts to a prior value, so the client doesn't
-have to guess what the current value is and whether some server-side function could've changed it.  Whenever the value changes,
-no matter why or how it changes, the server reports the new effective value in a *Parameter Status* protocol message to the client.
-pgJDBC uses many of these reports internally.
-
-As of pgJDBC 42.2.6, it also exposes the parameter status information to user applications via the PGConnection extensions interface.
-
-## Methods
-
-Two methods on `org.postgresql.PGConnection` provide the client interface to reported parameters. Parameter names are
-case-insensitive and case-preserving.
-
-* `Map PGConnection.getParameterStatuses()` - return a map of all reported parameters and their values.
-
-* `String PGConnection.getParameterStatus()` - shorthand to retrieve one value by name, or null if no value has been reported.
-
-See the `PGConnection` JavaDoc for details.
-
-## Example
-
-If you're working directly with a `java.sql.Connection` you can
-
-```java
-import org.postgresql.PGConnection;
-
-void my_function(Connection conn) {
-    System.out.println("My application name is " + ((PGConnection) conn).getParameterStatus("application_name"));
-}
-```
-
-## Other client drivers
-
-The `libpq` equivalent is the `PQparameterStatus(...)` API function.
-
-## Physical and Logical replication API
-
-Postgres 9.4 (released in December 2014) introduced a new feature called logical replication. Logical replication allows
-changes from a database to be streamed in real-time to an external system. The difference between physical replication and
-logical replication is that logical replication sends data over in a logical format whereas physical replication sends data
-over in a binary format. Additionally logical replication can send over a single table, or database. Binary replication
-replicates the entire cluster in an all or nothing fashion; which is to say there is no way to get a specific table or
-database using binary replication
-
-Prior to logical replication keeping an external system synchronized in real time was problematic. The application would
-have to update/invalidate the appropriate cache entries, reindex the data in your search engine, send it to your analytics
-system, and so on.
-
-This suffers from race conditions and reliability problems. For example if slightly different data gets written to two
-different datastores (perhaps due to a bug or a race condition), the contents of the datastores will gradually drift
-apart — they will become more and more inconsistent over time. Recovering from such gradual data corruption is difficult.
-
-Logical decoding takes the database’s write-ahead log (WAL), and gives us access to row-level change events:
-every time a row in a table is inserted, updated or deleted, that’s an event. Those events are grouped by transaction,
-and appear in the order in which they were committed to the database. Aborted/rolled-back transactions
-do not appear in the stream. Thus, if you apply the change events in the same order, you end up with an exact,
-transactionally consistent copy of the database. It's looks like the Event Sourcing pattern that you previously implemented
-in your application, but now it's available out of the box from the PostgreSQL® database.
-
-For access to real-time changes PostgreSQL® provides the streaming replication protocol. Replication protocol can be physical
-or logical. Physical replication protocol is used for Master/Secondary replication. Logical replication protocol can be used
-to stream changes to an external system.
-
-Since the JDBC API does not include replication `PGConnection` implements the PostgreSQL® API
-
-## Configure database
-
-Your database should be configured to enable logical or physical replication
-
-### postgresql.conf
-
-* Property `max_wal_senders` should be at least equal to the number of replication consumers
-* Property `wal_keep_segments` should contain count wal segments that can't be removed from database.
-* Property `wal_level` for logical replication should be equal to `logical`.
-* Property `max_replication_slots` should be greater than zero for logical replication, because logical replication can't
- work without replication slot.
-
-### pg_hba.conf
-
-Enable connect user with replication privileges to replication stream.
-
-```sql
-local   replication   all                   trust
-host    replication   all   127.0.0.1/32    md5
-host    replication   all   ::1/128         md5
-```
-
-### Configuration for examples
-
-*postgresql.conf*
-
-```ini
-max_wal_senders = 4             # max number of walsender processes
-wal_keep_segments = 4           # in logfile segments, 16MB each; 0 disables
-wal_level = logical             # minimal, replica, or logical
-max_replication_slots = 4       # max number of replication slots
-```
-
-*pg_hba.conf*
-
-```sql
-# Allow replication connections from localhost, by a user with the
-# replication privilege.
-local   replication   all                   trust
-host    replication   all   127.0.0.1/32    md5
-host    replication   all   ::1/128         md5
-```
-
-## Logical replication
-
-Logical replication uses a replication slot to reserve WAL logs on the server and also defines which decoding plugin to
-use to decode the WAL logs to the required format, for example you can decode changes as json, protobuf, etc. To demonstrate
-how to  use the pgJDBC replication API we will use the `test_decoding` plugin that is included in the `postgresql-contrib`
-package, but you can use your own decoding plugin. There are a few on github which can be used as examples.
-
-In order to use the replication API, the Connection has to be created in replication mode, in this mode the connection
-is not available to execute SQL commands, and can only be used with replication API. This is a restriction imposed by PostgreSQL®.
-
-##### Example 9.4. Create replication connection.
-
-```java
-String url = "jdbc:postgresql://localhost:5432/postgres";
-Properties props = new Properties();
-PGProperty.USER.set(props, "postgres");
-PGProperty.PASSWORD.set(props, "postgres");
-PGProperty.ASSUME_MIN_SERVER_VERSION.set(props, "9.4");
-PGProperty.REPLICATION.set(props, "database");
-PGProperty.PREFER_QUERY_MODE.set(props, "simple");
-
-Connection con = DriverManager.getConnection(url, props);
-PGConnection replConnection = con.unwrap(PGConnection.class);
-```
-
-The entire replication API is grouped in `org.postgresql.replication.PGReplicationConnection` and is available via
-`org.postgresql.PGConnection#getReplicationAPI` .
-
-Before you can start replication protocol, you need to have replication slot, which can be also created via pgJDBC API.
-
-##### Example 9.5. Create replication slot via pgJDBC API
-
-```java
-replConnection.getReplicationAPI()
-    .createReplicationSlot()
-    .logical()
-    .withSlotName("demo_logical_slot")
-    .withOutputPlugin("test_decoding")
-    .make();
-```
-
-Once we have the replication slot, we can create a ReplicationStream.
-
-##### Example 9.6. Create logical replication stream.
-
-```java
-PGReplicationStream stream =
-    replConnection.getReplicationAPI()
-        .replicationStream()
-        .logical()
-        .withSlotName("demo_logical_slot")
-        .withSlotOption("include-xids", false)
-        .withSlotOption("skip-empty-xacts", true)
-        .start();
-```
-
-The replication stream will send all changes since the creation of the replication slot or from replication slot
-restart LSN if the slot was already used for replication. You can also start streaming changes from a particular LSN position,
-in that case LSN position should be specified when you create the replication stream.
-
-##### Example 9.7. Create logical replication stream from particular position.
-
-```java
-LogSequenceNumber waitLSN = LogSequenceNumber.valueOf("6F/E3C53568");
-
-PGReplicationStream stream =
-    replConnection.getReplicationAPI()
-        .replicationStream()
-        .logical()
-        .withSlotName("demo_logical_slot")
-        .withSlotOption("include-xids", false)
-        .withSlotOption("skip-empty-xacts", true)
-        .withStartPosition(waitLSN)
-        .start();
-```
-
-Via `withSlotOption` we also can specify options that will be sent to our output plugin, this allows the user to customize decoding.
-For example, I have my own output plugin that has a property `sensitive=true` which will include changes by sensitive columns to change
-event.
-
-##### Example 9.8. Example output with include-xids=true
-
-```sql
-BEGIN 105779
-table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'previous value'
-COMMIT 105779
-```
-
-##### Example 9.9. Example output with include-xids=false
-
-```sql
-BEGIN
-table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'previous value'
-COMMIT
-```
-
-During replication the database and consumer periodically exchange ping messages. When the database or client do not receive
-ping message within the configured timeout, replication has been deemed to have stopped and an exception will be thrown and
-the database will free resources. In PostgreSQL® the ping timeout is configured by the property `wal_sender_timeout`
-(default = 60 seconds). Replication stream in pgjdc can be configured to send feedback(ping) when required or by time interval.
-It is recommended to send feedback(ping) to the database more often than configured `wal_sender_timeout` . In production systems
-I use value equal to `wal_sender_timeout / 3` . It's avoids a potential problems with networks and changes to be
-streamed without disconnects by timeout. To specify the feedback interval use `withStatusInterval` method.
-
-##### Example 9.10. Replication stream with configured feedback interval equal to 20 sec
-
-```java
-PGReplicationStream stream =
-    replConnection.getReplicationAPI()
-        .replicationStream()
-        .logical()
-        .withSlotName("demo_logical_slot")
-        .withSlotOption("include-xids", false)
-        .withSlotOption("skip-empty-xacts", true)
-        .withStatusInterval(20, TimeUnit.SECONDS)
-        .start();
-```
-
-After create `PGReplicationStream` , it's time to start receive changes in real-time.
-
-Changes can be received from stream as blocking( `org.postgresql.replication.PGReplicationStream#read` ) or as
-non-blocking (`org.postgresql.replication.PGReplicationStream#readPending` ).
-Both methods receive changes as a `java.nio.ByteBuffer` with the payload from the send output plugin. We can't receive
-part of message, only the full message that was sent by the output plugin. ByteBuffer contains message in format that is
-defined by the decoding output plugin, it can be simple String, json, or whatever the plugin determines. That's why
-pgJDBC returns the raw ByteBuffer instead of making assumptions.
-
-##### Example 9.11. Example send message from output plugin.
-
-```java
-OutputPluginPrepareWrite(ctx, true);
-appendStringInfo(ctx->out, "BEGIN %u", txn->xid);
-OutputPluginWrite(ctx, true);
-```
-
-**Example 9.12. Receive changes via replication stream.**
-
-```java
-while (true) {
-    //non blocking receive message
-    ByteBuffer msg = stream.readPending();
-
-    if (msg == null) {
-        TimeUnit.MILLISECONDS.sleep(10 L);
-        continue;
-    }
-
-    int offset = msg.arrayOffset();
-    byte[] source = msg.array();
-    int length = source.length - offset;
-    System.out.println(new String(source, offset, length));
-}
-```
-
-As mentioned previously, replication stream should periodically send feedback to the database to prevent disconnect via
-timeout. Feedback is automatically sent when `read` or `readPending` are called if it's time to send feedback. Feedback
-can also be sent via `org.postgresql.replication.PGReplicationStream#forceUpdateStatus()` regardless of the timeout. Another
-important duty of feedback is to provide the  server with the Logical Sequence Number (LSN) that has been successfully received
-and applied to consumer, it is necessary for monitoring and to truncate/archive WAL's that that are no longer needed. In the
-event that replication has been restarted, it's will start from last successfully processed LSN that was sent via feedback to database.
-
-The API provides the following feedback mechanism to indicate the successfully applied LSN by the current consumer. LSN's
-before this can be truncated or archived. `org.postgresql.replication.PGReplicationStream#setFlushedLSN` and
-`org.postgresql.replication.PGReplicationStream#setAppliedLSN` . You always can get last receive LSN via
-`org.postgresql.replication.PGReplicationStream#getLastReceiveLSN` .
-
-##### Example 9.13. Add feedback indicating a successfully process LSN
-
-```java
-while (true) {
-    //Receive last successfully send to queue message. LSN ordered.
-    LogSequenceNumber successfullySendToQueue = getQueueFeedback();
-    if (successfullySendToQueue != null) {
-        stream.setAppliedLSN(successfullySendToQueue);
-        stream.setFlushedLSN(successfullySendToQueue);
-    }
-
-    //non blocking receive message
-    ByteBuffer msg = stream.readPending();
-
-    if (msg == null) {
-        TimeUnit.MILLISECONDS.sleep(10 L);
-        continue;
-    }
-
-    asyncSendToQueue(msg, stream.getLastReceiveLSN());
-}
-```
-
-##### Example 9.14. Full example of logical replication
-
-```java
-String url = "jdbc:postgresql://localhost:5432/test";
-Properties props = new Properties();
-PGProperty.USER.set(props, "postgres");
-PGProperty.PASSWORD.set(props, "postgres");
-PGProperty.ASSUME_MIN_SERVER_VERSION.set(props, "9.4");
-PGProperty.REPLICATION.set(props, "database");
-PGProperty.PREFER_QUERY_MODE.set(props, "simple");
-
-Connection con = DriverManager.getConnection(url, props);
-PGConnection replConnection = con.unwrap(PGConnection.class);
-
-replConnection.getReplicationAPI()
-    .createReplicationSlot()
-    .logical()
-    .withSlotName("demo_logical_slot")
-    .withOutputPlugin("test_decoding")
-    .make();
-
-//some changes after create replication slot to demonstrate receive it
-sqlConnection.setAutoCommit(true);
-Statement st = sqlConnection.createStatement();
-st.execute("insert into test_logic_table(name) values('first tx changes')");
-st.close();
-
-st = sqlConnection.createStatement();
-st.execute("update test_logic_table set name = 'second tx change' where pk = 1");
-st.close();
-
-st = sqlConnection.createStatement();
-st.execute("delete from test_logic_table where pk = 1");
-st.close();
-
-PGReplicationStream stream =
-    replConnection.getReplicationAPI()
-    .replicationStream()
-    .logical()
-    .withSlotName("demo_logical_slot")
-    .withSlotOption("include-xids", false)
-    .withSlotOption("skip-empty-xacts", true)
-    .withStatusInterval(20, TimeUnit.SECONDS)
-    .start();
-
-while (true) {
-    //non blocking receive message
-    ByteBuffer msg = stream.readPending();
-
-    if (msg == null) {
-        TimeUnit.MILLISECONDS.sleep(10 L);
-        continue;
-    }
-
-    int offset = msg.arrayOffset();
-    byte[] source = msg.array();
-    int length = source.length - offset;
-    System.out.println(new String(source, offset, length));
-
-    //feedback
-    stream.setAppliedLSN(stream.getLastReceiveLSN());
-    stream.setFlushedLSN(stream.getLastReceiveLSN());
-}
-```
-
-Where output looks like this, where each line is a separate message.
-
-```sql
-BEGIN
-table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'first tx changes'
-COMMIT
-BEGIN
-table public.test_logic_table: UPDATE: pk[integer]:1 name[character varying]:'second tx change'
-COMMIT
-BEGIN
-table public.test_logic_table: DELETE: pk[integer]:1
-COMMIT
-```
-
-## Physical replication
-
-API for physical replication looks like the API for logical replication. Physical replication does not require a replication
-slot. And ByteBuffer will contain the binary form of WAL logs. The binary WAL format is a very low level API, and can change
-from version to version. That is why replication between different major PostgreSQL® versions is not possible. But physical
-replication can contain many important data, that is not available via logical replication. That is why pgJDBC contains an
-implementation for both.
-
-**Example 9.15. Use physical replication**
-
-```java
-LogSequenceNumber lsn = getCurrentLSN();
-
-Statement st = sqlConnection.createStatement();
-st.execute("insert into test_physic_table(name) values('previous value')");
-st.close();
-
-PGReplicationStream stream =
-    pgConnection
-    .getReplicationAPI()
-    .replicationStream()
-    .physical()
-    .withStartPosition(lsn)
-    .start();
-
-ByteBuffer read = stream.read();
-```
-
-## Arrays
-
-PostgreSQL® provides robust support for array data types as column types, function arguments
-and criteria in where clauses. There are several ways to create arrays with pgJDBC.
-
-The [java.sql. Connection.createArrayOf(String, Object\[\])](https://docs.oracle.com/javase/8/docs/api/java/sql/Connection.html#createArrayOf-java.lang.String-java.lang.Object:A-) can be used to create an [java.sql. Array](https://docs.oracle.com/javase/8/docs/api/java/sql/Array.html) from `Object[]` instances (Note: this includes both primitive and object multi-dimensional arrays).
-A similar method `org.postgresql.PGConnection.createArrayOf(String, Object)` provides support for primitive array types.
-The `java.sql.Array` object returned from these methods can be used in other methods, such as
-[PreparedStatement.setArray(int, Array)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setArray-int-java.sql.Array-).
-
-The following types of arrays support binary representation in requests and can be used in `PreparedStatement.setObject`
-
-|Java Type | Supported binary PostgreSQL® Types | Default PostgreSQL® Type|
-|--- | --- | ---|
-|`short[]` , `Short[]` | `int2[]` | `int2[]`|
-|`int[]` , `Integer[]` | `int4[]` | `int4[]`|
-|`long[]` , `Long[]` | `int8[]` | `int8[]`|
-|`float[]` , `Float[]` | `float4[]` | `float4[]`|
-|`double[]` , `Double[]` | `float8[]` | `float8[]`|
-|`boolean[]` , `Boolean[]` | `bool[]` | `bool[]`|
-|`String[]` | `varchar[]` , `text[]` | `varchar[]`|
-|`byte[][]` | `bytea[]` | `bytea[]`|
-
-
-## CopyManager
-The driver provides an extension for accessing `COPY`. Copy is an extension that PostreSQL provides. see [Copy](https://www.postgresql.org/docs/current/sql-copy.html)
-
-#### Example 9.15 Copying Data in
-```java
-
-/*
-* DDL for code below
-* create table copytest (stringvalue text, intvalue int, numvalue numeric(5,2));
-*/
-private static String[] origData =
-            {"First Row\t1\t1.10\n",
-                    "Second Row\t2\t-22.20\n",
-                    "\\N\t\\N\t\\N\n",
-                    "\t4\t444.40\n"};
-private int dataRows = origData.length;
-private String sql = "COPY copytest FROM STDIN";
-
-try (Connection con = DriverManager.getConnection(url, "postgres", "somepassword")){
-    PGConnection pgConnection = con.unwrap(org.postgresql.PGConnection.class);
-    CopyManager copyAPI = pgConnection.getCopyAPI();
-    CopyIn cp = copyAPI.copyIn(sql);
-
-    for (String anOrigData : origData) {
-        byte[] buf = anOrigData.getBytes();
-        cp.writeToCopy(buf, 0, buf.length);
-    }
-
-    long updatedRows = cp.endCopy();
-    long handledRowCount = cp.getHandledRowCount();
-    System.err.println(String.format("copy Updated %d Rows, and handled %d rows", updatedRows, handledRowCount));
-
-    int rowCount = getCount(con);
-    System.err.println(rowCount);
-
-}
-
-``` 
-
-#### Example 9.16 Copying Data out
-
-```java
-String sql = "COPY copytest TO STDOUT";
-try (Connection con = DriverManager.getConnection(url, "postgres", "somepassword")){
-    PGConnection pgConnection = con.unwrap(org.postgresql.PGConnection.class);
-    CopyManager copyAPI = pgConnection.getCopyAPI();
-    CopyOut cp = copyAPI.copyOut(sql);
-    int count = 0;
-    byte[] buf;  // This is a relatively simple example. buf will contain rows from the database
-
-    while ((buf = cp.readFromCopy()) != null) {
-        count++;
-    }
-    long rowCount = cp.getHandledRowCount();
-}
-```
-
-More examples can be found in the [Copy Test Code](https://github.com/pgjdbc/pgjdbc/blob/master/pgjdbc/src/test/java/org/postgresql/test/jdbc2/CopyTest.java)
diff --git a/docs/content/documentation/setup.md b/docs/content/documentation/setup.md
deleted file mode 100644
index cffa539477..0000000000
--- a/docs/content/documentation/setup.md
+++ /dev/null
@@ -1,70 +0,0 @@
----
-title: "Setting up the JDBC Driver"
-date: 2022-06-19T22:46:55+05:30
-draft: false
-weight: 1
-toc: true
-aliases:
-    - "/documentation/head/setup.html"
----
-
-This section describes the steps you need to take before you can write or run programs that use the JDBC interface.
-
-## Getting the Driver
-
-Precompiled versions of the driver can be downloaded from the [PostgreSQL® JDBC web site](https://jdbc.postgresql.org).
-
-Alternatively you can build the driver from source, but you should only need to do this if you are making changes to the source code. To build the JDBC driver, you need gradle and a JDK (currently at least jdk1.8).
-
-If you have several Java compilers installed, maven will use the first one on the path. To use a different one set `JAVA_HOME` to the Java version you wish to use. For example, to use a different JDK than the default, this may work:
-
-```java
- JAVA_HOME=/usr/local/jdk1.8.0_45
- ```
-
-To compile the driver simply run **`gradlew assemble`** or **`gradlew build`** if you want to run the tests in the top level directory.
-
-> **NOTE**
->
-> If you want to skip test execution, add the option `-DskipTests`. The compiled driver will be placed in `pgjdbc/build/libs/postgresql-MM.nn.pp.jar`
-
-Where MM is the major version, nn is the minor version and pp is the patch version. Versions for JDBC3 and lower can be found [here](https://repo1.maven.org/maven2/org/postgresql/postgresql/9.2-1003-jdbc3/)
-
-This is a very brief outline of how to build the driver. Much more detailed information can be found on the [github repo](https://github.com/pgjdbc/pgjdbc/blob/master/CONTRIBUTING.md)
-
-Even though the JDBC driver should be built with Gradle, for situations, where use of Gradle is not possible, e.g.,
-when building pgJDBC for distributions, the pgJDBC Gradle build provides a convenience source release artifact `*-src.tar.gz` - a Maven-based project.
-The Maven-based project contains a version of the JDBC driver with complete functionality, which can be used in production and is still validly buildable
-within the Maven build environment.
-
-The Maven-based project is created with **`gradlew -d :postgresql:sourceDistribution -Prelease -Psigning.gpg.enabled=OFF`**.
-The produced `*-src.tar.gz` can be then found in `pgjdbc/build/distributions/` directory. JDBC driver can be built from the Maven-based project with **mvn package** or,
-when the tests are to be skipped, with **`mvn -DskipTests package`**.
-
-Source files `*-src.tar.gz`'s are released in the [Maven central repository](https://repo1.maven.org/maven2/org/postgresql/postgresql/).
-
-## Setting up the Class Path
-
-To use the driver, the JAR archive named `postgresql-MM.nn.pp.jar` needs to be included in the class path, either by putting it in the `CLASSPATH` environment variable, or by using flags on the **java** command line.
-
-For instance, assume we have an application that uses the JDBC driver to access a database, and that application is installed as `/usr/local/lib/myapp.jar` . The PostgreSQL® JDBC driver installed as `/usr/local/pgsql/share/java/postgresql-MM.nn.pp.jar` .
-To run the application, we would use:
-
-```bash
-export CLASSPATH=/usr/local/lib/myapp.jar:/usr/local/pgsql/share/java/postgresql-42.5.0.jar:. java MyApp
-```
-
-Current Java applications will likely use maven, gradle or some other package manager. [Use this to search](https://mvnrepository.com/artifact/org.postgresql/postgresql) for the latest jars and how to include them in your project
-
-Loading the driver from within the application is covered in [Initializing the Driver](/documentation/use/).
-
-## Preparing the Database Server for JDBC
-
-Out of the box, Java does not support unix sockets so the PostgreSQL® server must be configured to allow TCP/IP connections. Starting with server version 8.0 TCP/IP connections are allowed from `localhost` . To allow connections to other interfaces
-than the loopback interface, you must modify the `postgresql.conf` file's `listen_addresses` setting.
-
-Once you have made sure the server is correctly listening for TCP/IP connections the next step is to verify that users are allowed to connect to the server. Client authentication is setup in `pg_hba.conf` . Refer to the main PostgreSQL® [documentation](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html) for details .
-
-## Creating a Database
-
-When creating a database to be accessed via JDBC it is important to select an appropriate encoding for your data. Many other client interfaces do not care what data you send back and forth, and will allow you to do inappropriate things, but Java makes sure that your data is correctly encoded.  Do not use a database that uses the `SQL_ASCII` encoding. This is not a real encoding and you will have problems the moment you store data in it that does not fit in the seven bit ASCII character set. If you do not know what your encoding will be or are otherwise unsure about what you will be storing the `UNICODE` encoding is a reasonable default to use.
diff --git a/docs/data/footer.toml b/docs/data/footer.toml
index 8acb9d4496..9ff3eeb87c 100644
--- a/docs/data/footer.toml
+++ b/docs/data/footer.toml
@@ -26,10 +26,10 @@ url = "https://www.postgresql.org/list/pgsql-jdbc/"
 title = "More"
 [[external.links]]
 name = "FAQ"
-url = "/faq/"
+url = "faq/"
 [[external.links]]
 name = "Privacy"
 url = "https://www.postgresql.org/about/policies/privacy/"
 [[external.links]]
 name = "License"
-url = "/license/"
+url = "license/"
diff --git a/docs/data/homepagedata.toml b/docs/data/homepagedata.toml
deleted file mode 100644
index 7554ec76d0..0000000000
--- a/docs/data/homepagedata.toml
+++ /dev/null
@@ -1,79 +0,0 @@
-# Features
-
-[[feature]]
-desc = "pgJDBC allows Java programs to connect to a PostgreSQL database using standard, database independent Java code."
-path = "/icons/java-icon.svg"
-
-[[feature]]
-desc = "pgJDBC provides a reasonably complete implementation of the JDBC specification in addition to some PostgreSQL specific extensions."
-path = "/icons/api-icon.svg"
-
-[[feature]]
-desc = "The current development driver supports eleven server versions and three java environments."
-path = "/icons/driver-icon.svg"
-
-# Releases Info
-[[info]]
-date = "28 April 2026"
-url = "/changelogs/2026-04-28-42/"
-version = "42.7.11"
-
-[[info]]
-date = "15 January 2026"
-url = "/changelogs/2026-01-15-42/"
-version = "42.7.9"
-
-[[info]]
-date = "18 September 2025"
-url = "/changelogs/2025-09-18-42/"
-version = "42.7.8"
-
-[[info]]
-date = "11 June 2025"
-url = "/changelogs/2025-06-11-42/"
-version = "42.7.7"
-
-[[info]]
-date = "28 May 2025"
-url = "/changelogs/2025-05-28-42/"
-version = "42.7.6"
-
-[[info]]
-date = "14 January 2025"
-url = "/changelogs/2025-01-14-42.7.5-release/"
-version = "42.7.5"
-
-[[info]]
-date = "20 August 2024"
-url = "/changelogs/2024-08-22-42.7.4-release/"
-version = "42.7.4"
-
-[[info]]
-date = "14 March 2024"
-url = "/changelogs/2024-03-14-42.7.3-release/"
-version = "42.7.3"
-
-[[info]]
-date = "13 March 2024"
-url = "/changelogs/2024-03-13-42.6.2-release/"
-version = "42.6.2"
-
-[[info]]
-date = "13 March 2024"
-url = "/changelogs/2024-03-13-42.5.6-release/"
-version = "42.5.6"
-
-[[info]]
-date = "13 March 2024"
-url = "/changelogs/2024-03-13-42.4.5-release/"
-version = "42.4.5"
-
-[[info]]
-date = "13 March 2024"
-url = "/changelogs/2024-03-13-42.3.10-release/"
-version = "42.3.10"
-
-[[info]]
-date = "13 March 2024"
-url = "/changelogs/2024-03-13-42.2.29-release/"
-version = "42.2.29"
diff --git a/docs/data/release-history-overlay.yaml b/docs/data/release-history-overlay.yaml
new file mode 100644
index 0000000000..3b9f518bd3
--- /dev/null
+++ b/docs/data/release-history-overlay.yaml
@@ -0,0 +1,25 @@
+# Hand-maintained companion to docs/data/release-history.yaml (generated
+# by :docs-tools:generateReleaseHistory from refs in the local clone).
+#
+# Carries only what cannot be derived from git itself: the `.jre6` and
+# `.jre7` classifier builds of the 42.2.x line. Each entry produces one
+# extra row under its parent line in the release-history.yaml output,
+# rendered as a download card by the `recent-versions` shortcode and as
+# the .jre7 cutoff hint by `past-versions`.
+#
+# Schema (per entry):
+#   id            classifier suffix; emitted into `version_range` as
+#                 `<last_version>.<id>`, and the templates derive the
+#                 Java version from this suffix (`jre7` -> 7).
+#   branch        the parent release line (`42.X.x`).
+#   last_version  the newest tagged release of that classifier; used to
+#                 build the row's version_range and to look up the
+#                 release date from git tags.
+
+classifiers:
+  - id: jre7
+    branch: 42.2.x
+    last_version: 42.2.29
+  - id: jre6
+    branch: 42.2.x
+    last_version: 42.2.27
diff --git a/docs/data/versions.toml b/docs/data/versions.toml
deleted file mode 100644
index 964788dea8..0000000000
--- a/docs/data/versions.toml
+++ /dev/null
@@ -1,119 +0,0 @@
-# Recent Versions
-[[recent]]
-j_name= "Java 8"
-version= "42.7.11"
-suffix=""
-description= "If you are using Java 8 or newer then you should use the JDBC 4.2 version."
-url= "/download/postgresql-42.7.11.jar"
-
-[[recent]]
-j_name= "Java 7"
-version= "42.2.29"
-suffix="jre7"
-description= "If you are using Java 7  then you should use the  JDBC 4.1 version."
-url= "/download/postgresql-42.2.29.jre7.jar"
-
-[[recent]]
-j_name= "Java 6"
-version= "42.2.27"
-suffix="jre6"
-description= "If you are using Java 6  then you should use the  JDBC 4.0 version."
-url= "/download/postgresql-42.2.27.jre6.jar"
-
-# Past Versions
-[[past]]
-j_name= "Java 8"
-version= "42.7.10"
-suffix=""
-description= "If you are using Java 8 or newer then you should use the JDBC 4.2 version."
-url= "/download/postgresql-42.7.10.jar"
-
-[[past]]
-j_name= "Java 8"
-version= "42.7.9"
-suffix=""
-description= "If you are using Java 8 or newer then you should use the JDBC 4.2 version."
-url= "/download/postgresql-42.7.9.jar"
-
-[[past]]
-j_name= "Java 8"
-version= "42.7.8"
-suffix=""
-description= "If you are using Java 8 or newer then you should use the JDBC 4.2 version."
-url= "/download/postgresql-42.7.8.jar"
-
-[[past]]
-j_name= "Java 8"
-version= "42.7.7"
-suffix=""
-description= "If you are using Java 8 or newer then you should use the JDBC 4.2 version."
-url= "/download/postgresql-42.7.7.jar"
-
-[[past]]
-j_name= "Java 8"
-version= "42.7.6"
-suffix=""
-description= "If you are using Java 8 or newer then you should use the JDBC 4.2 version."
-url= "/download/postgresql-42.7.6.jar"
-
-[[past]]
-j_name= "Java 8"
-version= "42.7.5"
-suffix=""
-description= "If you are using Java 8 or newer then you should use the JDBC 4.2 version."
-url= "/download/postgresql-42.7.5.jar"
-
-[[past]]
-j_name= "Java 8"
-version= "42.7.4"
-suffix=""
-description= "If you are using Java 8 or newer then you should use the JDBC 4.2 version."
-url= "/download/postgresql-42.7.4.jar"
-
-[[past]]
-j_name= "Java 8"
-version= "42.7.3"
-suffix=""
-description= "If you are using Java 8 or newer then you should use the JDBC 4.2 version."
-url= "/download/postgresql-42.7.3.jar"
-
-[[past]]
-j_name= "Java 8"
-version= "42.7.2"
-suffix=""
-description= "This version has some minor regressions, 42.7.3 is preferred."
-url= "/download/postgresql-42.7.2.jar"
-
-[[past]]
-j_name= "Java 8"
-version= "42.6.2"
-suffix=""
-description= "If you are using Java 8 or newer then you should use the JDBC 4.2 version."
-url= "/download/postgresql-42.6.2.jar"
-
-[[past]]
-j_name= "Java 8"
-version= "42.5.6"
-suffix=""
-description= "If you are using Java 8 or newer then you should use the JDBC 4.2 version."
-url= "/download/postgresql-42.5.6.jar"
-
-[[past]]
-v_name= "Postgresql JDBC 42.4.4"
-version= "42.4.4"
-suffix=""
-url= "/download/postgresql-42.4.4.jar"
-
-[[past]]
-v_name= "Postgresql JDBC 42.3.9"
-version= "42.3.9"
-suffix=""
-url= "/download/postgresql-42.3.9.jar"
-
-[[past]]
-j_name= "Java 7"
-version= "42.2.28"
-suffix="jre7"
-description= "If you are using Java 7  then you should use the  JDBC 4.1 version."
-url= "/download/postgresql-42.2.28.jre7.jar"
-
diff --git a/docs/layouts/_default/_markup/render-link.html b/docs/layouts/_default/_markup/render-link.html
new file mode 100644
index 0000000000..1a2028ca4c
--- /dev/null
+++ b/docs/layouts/_default/_markup/render-link.html
@@ -0,0 +1,33 @@
+{{- /*
+  Markdown link render hook.
+
+  Source-of-truth links in markdown content are written as root-relative
+  paths (e.g. `[Compatibility](/documentation/getting-started/compatibility/)`)
+  because that's what production https://jdbc.postgresql.org/ has always
+  served. On a project-page deployment (https://<owner>.github.io/<repo>/),
+  the same path lands the reader on the *wrong* host root instead of the
+  repo prefix.
+
+  Goldmark's default link hook emits .Destination verbatim, so we
+  intercept it and run any root-relative URL through relURL, which
+  prepends the site BasePath. Hugo's relURL deliberately does NOT
+  prepend BasePath when the input already starts with `/` (it treats
+  that as "you already know the absolute path"), so we trim the leading
+  slash first. The query/fragment tail (`?foo=1#bar`) survives the
+  transform untouched — relURL only manipulates the path portion.
+
+  External links, anchor-only links, mailto:, and relative ./foo paths
+  are passed through unchanged.
+*/ -}}
+{{- /*
+  A simple prefix check is enough here — and more robust than
+  urls.Parse, which throws on the malformed-but-historically-tolerated
+  destinations a few legacy changelog entries carry. Root-relative
+  links start with exactly one `/`; protocol-relative (`//host/...`)
+  and external (`https://...`) links don't match the condition.
+*/ -}}
+{{- $dest := .Destination -}}
+{{- if and (hasPrefix $dest "/") (not (hasPrefix $dest "//")) -}}
+  {{- $dest = $dest | strings.TrimPrefix "/" | relURL -}}
+{{- end -}}
+<a href="{{ $dest | safeURL }}"{{ with .Title }} title="{{ . | safeHTML }}"{{ end }}>{{ .Text | safeHTML }}</a>
diff --git a/docs/layouts/alias.html b/docs/layouts/alias.html
new file mode 100644
index 0000000000..2d21b73612
--- /dev/null
+++ b/docs/layouts/alias.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html{{ with site.Language.Locale }} lang="{{.}}"{{end}}>
+<head>
+<title>{{ .Permalink }}</title>
+<link rel="canonical" href="{{ .Permalink }}">
+<meta charset="utf-8">
+{{/*
+  Fragment-level redirects for legacy hub URLs. Hugo's default alias HTML
+  is a meta-refresh that rewrites the path but drops the URL fragment,
+  so a deep link like /documentation/use/#unix-sockets would otherwise
+  land at /documentation/connect/url-syntax/ with no scroll anchor.
+
+  The inline script below runs before the meta-refresh, checks the
+  current URL hash against a union of the four split-hub maps (use,
+  setup, server-prepare, query), and replaces the location with the
+  topical-page URL plus the original anchor when the hash matches. When
+  the hash is empty or unknown, the script falls through and the
+  meta-refresh below takes the user to the destination this alias was
+  declared for.
+
+  Anchor keys are the slug forms emitted by Hugo (lowercased, runs of
+  non-word chars collapsed to '-'). Names are unique across the four
+  hubs, so a flat lookup is sufficient.
+
+  All target URLs are passed through relURL so the script keeps working
+  on project-page deploys (<owner>.github.io/<repo>/). The argument has
+  no leading slash for the same reason: a leading `/` makes relURL skip
+  BasePath, which is the trap lintDocsLinks flags.
+*/}}
+{{- $map := dict
+    "#connecting-to-the-database"                          (relURL "documentation/connect/url-syntax/")
+    "#system-properties"                                   (relURL "documentation/connect/url-syntax/")
+    "#connection-parameters"                               (relURL "documentation/connect/url-syntax/")
+    "#unix-sockets"                                        (relURL "documentation/connect/unix-sockets/")
+    "#connection-fail-over"                                (relURL "documentation/connect/failover/")
+    "#getting-the-driver"                                  (relURL "documentation/getting-started/getting-the-driver/")
+    "#setting-up-the-class-path"                           (relURL "documentation/getting-started/setting-up-the-class-path/")
+    "#preparing-the-database-server-for-jdbc"              (relURL "documentation/getting-started/server-prep/")
+    "#creating-a-database"                                 (relURL "documentation/getting-started/server-prep/")
+    "#getting-results-based-on-a-cursor"                   (relURL "documentation/query/fetch-size/")
+    "#using-the-statement-or-preparedstatement-interface"  (relURL "documentation/query/basics/")
+    "#using-the-resultset-interface"                       (relURL "documentation/query/basics/")
+    "#performing-updates"                                  (relURL "documentation/query/basics/")
+    "#creating-and-modifying-database-objects"             (relURL "documentation/query/basics/")
+    "#using-java-8-date-and-time-classes"                  (relURL "documentation/data-types/date-time/")
+    "#accessing-the-extensions"                            (relURL "documentation/postgresql-features/extensions-api/")
+    "#timestamp-infinity"                                  (relURL "documentation/data-types/infinity/")
+    "#geometric-data-types"                                (relURL "documentation/data-types/geometric/")
+    "#large-objects"                                       (relURL "documentation/data-types/large-objects/")
+    "#listen--notify"                                      (relURL "documentation/postgresql-features/listen-notify/")
+    "#server-prepared-statements"                          (relURL "documentation/query/prepared-statements/")
+    "#parameter-status-messages"                           (relURL "documentation/postgresql-features/parameter-status/")
+    "#methods"                                             (relURL "documentation/postgresql-features/parameter-status/")
+    "#physical-and-logical-replication-api"                (relURL "documentation/postgresql-features/replication/")
+    "#configure-database"                                  (relURL "documentation/postgresql-features/replication/")
+    "#logical-replication"                                 (relURL "documentation/postgresql-features/replication/")
+    "#physical-replication"                                (relURL "documentation/postgresql-features/replication/")
+    "#arrays"                                              (relURL "documentation/data-types/arrays/")
+    "#copymanager"                                         (relURL "documentation/postgresql-features/copy/")
+-}}
+<script>
+(function () {
+  var map = {{ $map | jsonify | safeJS }};
+  var hash = window.location.hash.toLowerCase();
+  if (hash && map[hash]) {
+    window.location.replace(map[hash] + hash);
+  }
+})();
+</script>
+<meta http-equiv="refresh" content="0; url={{ .Permalink }}">
+</head>
+</html>
diff --git a/docs/layouts/changelogs/list.html b/docs/layouts/changelogs/list.html
index 7fe663e110..f72080ddbe 100644
--- a/docs/layouts/changelogs/list.html
+++ b/docs/layouts/changelogs/list.html
@@ -1,15 +1,36 @@
 {{ define "main" }}
-<article class="container">
+<article class="container changelogs">
     <h1>
-        <img src="{{"/icons/changelogs.svg" | relURL}}" alt="changelogs">
+        <img src="{{"icons/changelogs.svg" | relURL}}" alt="" aria-hidden="true">
         {{ .Title }}
     </h1>
-    {{ range .Data.Pages.ByDate.Reverse }}
-    <ul>
-        <li>
-            <a href="{{.Permalink}}"> {{.Params.Version}}</a>
+
+    {{- with .Content }}{{ . }}{{ end }}
+
+    {{/*
+      One line per release in reverse chronological order. The
+      .Date is taken from each entry's YAML front-matter, which we
+      cross-checked against the REL42.x.y git-tag dates so it's
+      authoritative. ISO date format keeps the date column the
+      same width on every row so the eye can scan vertically.
+
+      Non-release pages historically filed under /changelogs/ (e.g.
+      the Log4j CVE-2021-44228 status notice) have a `version`
+      field that does not start with a digit. Filter those out of
+      the listing — they remain reachable via their own URL.
+    */}}
+    <ul class="changelogs__list" role="list">
+        {{- range .Data.Pages.ByDate.Reverse }}
+        {{- if findRE "^[0-9]" .Params.version }}
+        <li class="changelogs__entry">
+            <a class="changelogs__version" href="{{ .Permalink }}">{{ .Params.Version }}</a>
+            <time class="changelogs__date" datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "2006-01-02" }}</time>
+            {{- with .Params.summary }}
+            <span class="changelogs__summary">{{ . }}</span>
+            {{- end }}
         </li>
+        {{- end }}
+        {{- end }}
     </ul>
-    {{ end }}
 </article>
-{{ end }}
\ No newline at end of file
+{{ end }}
diff --git a/docs/layouts/changelogs/single.html b/docs/layouts/changelogs/single.html
index e9cabb7ad6..66c6f7baa3 100644
--- a/docs/layouts/changelogs/single.html
+++ b/docs/layouts/changelogs/single.html
@@ -3,4 +3,4 @@
     <h2>Notable Changes</h2>
     {{ .Content }}
 </article>
-{{ end }}
\ No newline at end of file
+{{ end }}
diff --git a/docs/layouts/community/list.html b/docs/layouts/community/list.html
index 961e56c922..313eab85a4 100644
--- a/docs/layouts/community/list.html
+++ b/docs/layouts/community/list.html
@@ -1,7 +1,7 @@
 {{ define "main" }}
 <article class="community container flow">
     <h1>
-        <img src="{{"/icons/community.svg" | relURL}}" alt="community">
+        <img src="{{"icons/community.svg" | relURL}}" alt="community">
         {{ .Title }}
     </h1>
     {{ .Content }}
diff --git a/docs/layouts/development/list.html b/docs/layouts/development/list.html
index 76bcbe6502..5e0abf3748 100644
--- a/docs/layouts/development/list.html
+++ b/docs/layouts/development/list.html
@@ -1,7 +1,7 @@
 {{ define "main" }}
 <article class="development container flow">
     <h1>
-        <img src="{{"/icons/development.svg" | relURL}}" alt="development" >
+        <img src="{{"icons/development.svg" | relURL}}" alt="development" >
         {{ .Title }}
     </h1>
     {{ .Content }}
diff --git a/docs/layouts/development/single.html b/docs/layouts/development/single.html
new file mode 100644
index 0000000000..8ce35c0df8
--- /dev/null
+++ b/docs/layouts/development/single.html
@@ -0,0 +1,9 @@
+{{ define "main" }}
+<article class="development container flow">
+    <h1>
+        <img src="{{"icons/development.svg" | relURL}}" alt="development" >
+        {{ .Title }}
+    </h1>
+    {{ .Content }}
+</article>
+{{ end }}
diff --git a/docs/layouts/download/list.html b/docs/layouts/download/list.html
index f02e65349f..8c0f51239a 100644
--- a/docs/layouts/download/list.html
+++ b/docs/layouts/download/list.html
@@ -1,7 +1,7 @@
 {{ define "main" }}
 <article class="download container flow">
     <h1>
-        <img src="{{"/icons/download.svg" | relURL}}" alt="download" >
+        <img src="{{"icons/download.svg" | relURL}}" alt="download" >
         {{ .Title }}
     </h1>
     {{ .Content }}
diff --git a/docs/layouts/faq/list.html b/docs/layouts/faq/list.html
index d2512d6095..c65e43d29f 100644
--- a/docs/layouts/faq/list.html
+++ b/docs/layouts/faq/list.html
@@ -1,7 +1,7 @@
 {{ define "main" }}
 <section class="faq container">
     <h2>Frequently Asked Questions</h2>
-    {{ range $.Site.Data.faqs.faq }}
+    {{ range hugo.Data.faqs.faq }}
     <div class="accordion">
         <h4>{{ .title }}</h4>
         {{ range $ind, $item := .item}}
@@ -11,11 +11,28 @@ <h4>{{ .title }}</h4>
                 <span class="accordion__btn__icon" aria-hidden="true"></span>
             </button>
             <div class="accordion__content">
-                <div class="accordion__content__inner">{{ $item.ans | safeHTML }}</div>
+                {{- /*
+                  FAQ entries historically embed anchors as raw HTML
+                  (`<a href="/foo">`) rather than markdown, and the
+                  `safeHTML` pipe skips Goldmark — so the render-link
+                  hook never sees them. Rewrite root-relative hrefs to
+                  include the site BasePath so the links don't break
+                  on a project-page deployment. `relURL ""` returns the
+                  BasePath with trailing slash; on prod it's just `/`
+                  and the loop becomes a no-op (no matches replaced).
+                */ -}}
+                {{- $ans := $item.ans -}}
+                {{- range findRE `href="/[^/"][^"]*"` $ans -}}
+                    {{- $orig := . -}}
+                    {{- $path := . | strings.TrimPrefix `href="/` | strings.TrimSuffix `"` -}}
+                    {{- $rel := relURL $path -}}
+                    {{- $ans = replace $ans $orig (printf `href="%s"` $rel) -}}
+                {{- end -}}
+                <div class="accordion__content__inner">{{ $ans | safeHTML }}</div>
             </div>
         </div>
         {{ end }}
     </div>
     {{ end }}
 </section>
-{{ end }}
\ No newline at end of file
+{{ end }}
diff --git a/docs/layouts/index.html b/docs/layouts/index.html
index 1136f18925..e48d449c16 100644
--- a/docs/layouts/index.html
+++ b/docs/layouts/index.html
@@ -1,10 +1,9 @@
 {{ define "main" }}
 <div class="home flow">
     {{ partial "home/hero.html" . }}
-    {{ partial "home/info.html" . }}
-    {{ partial "home/feature.html" . }}
-    {{ partial "home/example.html" . }}
-    {{ partial "home/report.html" . }}
-    {{ partial "home/support.html" . }}
+    <section class="home__decision">
+        {{ partial "home/quick-install.html" . }}
+        {{ partial "home/release.html" . }}
+    </section>
 </div>
-{{ end }}
\ No newline at end of file
+{{ end }}
diff --git a/docs/layouts/index.searchindex.json b/docs/layouts/index.searchindex.json
index dff377319e..76fcea0d22 100644
--- a/docs/layouts/index.searchindex.json
+++ b/docs/layouts/index.searchindex.json
@@ -1,10 +1,71 @@
+{{- /*
+  Search index consumed by static/js/lunr-search.js.
+
+  Two kinds of entries are emitted:
+    1. One entry per regular page (title, plain text body, description, categories).
+    2. One entry per connection property from data/connection-properties.yaml,
+       so the search picks up property names directly and deep-links into the
+       reference table via #prop-<anchor>.
+*/ -}}
 [
-    {{- range $index, $page := .Site.RegularPages -}}
-      {{- if gt $index 0 -}} , {{- end -}}
-      {{- $entry := dict "uri" $page.RelPermalink "title" $page.Title -}}
-      {{- $entry = merge $entry (dict "content" ($page.Plain | htmlUnescape)) -}}
-      {{- $entry = merge $entry (dict "description" $page.Description) -}}
-      {{- $entry = merge $entry (dict "categories" $page.Params.categories) -}}
-      {{- $entry | jsonify -}}
+{{- $first := true -}}
+{{- range .Site.RegularPages -}}
+  {{- if not $first -}},{{- end -}}{{- $first = false -}}
+  {{/* Strip the {{< review >}} shortcode's output before plainifying so that
+       file paths and line numbers inside review entries (e.g. PGProperty.java)
+       don't match unrelated search queries and don't leak into result
+       snippets as "Reviewed YYYY-MM-DD against source: ...". */}}
+  {{- $body := replaceRE `<p class="review-meta">[\s\S]*?</p>\s*` "" .Content -}}
+  {{- $entry := dict
+      "uri"         .RelPermalink
+      "title"       .Title
+      "content"     ($body | plainify | htmlUnescape)
+      "description" .Description
+      "categories"  .Params.categories
+  -}}
+  {{- $entry | jsonify -}}
+{{- end -}}
+{{/* Resolve the connection-properties reference page through Hugo so its
+     URL carries the configured basePath (e.g. /pgjdbc/ on GitHub Pages
+     forks). RelPermalink is `/documentation/.../` for the production site
+     and `/pgjdbc/documentation/.../` for forks — and updates automatically
+     if the page is ever moved. */}}
+{{- $propsPage := .Site.GetPage "/documentation/reference/connection-properties" -}}
+{{- $propsRef  := $propsPage.RelPermalink -}}
+{{- range index .Site.Data "connection-properties" -}}
+  {{- $status := upper (.status | default "STABLE") -}}
+  {{/* Skip HIDDEN properties: the reference table renders them but they're
+       deliberately not advertised to users, so they don't belong in search. */}}
+  {{- if ne $status "HIDDEN" -}}
+    {{- if not $first -}},{{- end -}}{{- $first = false -}}
+    {{- $anchor      := anchorize .name -}}
+    {{- $desc        := .description | default "" | plainify | htmlUnescape -}}
+    {{- $tags        := .tags | default slice -}}
+    {{- $versionBits := slice -}}
+    {{- with .introducedIn -}}{{- $versionBits = $versionBits | append (printf "since %s" .) -}}{{- end -}}
+    {{- with .deprecatedIn -}}{{- $versionBits = $versionBits | append (printf "deprecated in %s" .) -}}{{- end -}}
+    {{- $statusPhrase := "" -}}
+    {{- if and (ne $status "STABLE") (ne $status "") -}}
+      {{- $statusPhrase = lower $status -}}
     {{- end -}}
-]
\ No newline at end of file
+    {{/* Content packs the property name (so partial matches light up the body
+         field too), description, tags, version markers, default value and
+         status into one string. camelCase splitting is done by the JS-side
+         identifier-aware tokenizer in lunr-search.js, so no Hugo-side
+         n-gram expansion is needed here. */}}
+    {{- $contentBits := slice .name $desc -}}
+    {{- with $tags -}}{{- $contentBits = $contentBits | append (delimit . " ") -}}{{- end -}}
+    {{- with $versionBits -}}{{- $contentBits = $contentBits | append (delimit . " ") -}}{{- end -}}
+    {{- with .default -}}{{- $contentBits = $contentBits | append (printf "default %s" .) -}}{{- end -}}
+    {{- with $statusPhrase -}}{{- $contentBits = $contentBits | append . -}}{{- end -}}
+    {{- $entry := dict
+        "uri"         (printf "%s#prop-%s" $propsRef $anchor)
+        "title"       .name
+        "content"     (delimit $contentBits " — ")
+        "description" $desc
+        "categories"  $tags
+    -}}
+    {{- $entry | jsonify -}}
+  {{- end -}}
+{{- end -}}
+]
diff --git a/docs/layouts/partials/docs/article.html b/docs/layouts/partials/docs/article.html
index 9a9b7f9855..2242437e4b 100644
--- a/docs/layouts/partials/docs/article.html
+++ b/docs/layouts/partials/docs/article.html
@@ -1,10 +1,56 @@
 <article class="article">
+    {{- partial "docs/page-meta.html" . -}}
     <h1>
         {{if eq .Title "Documentation"}}
-        <img src="{{"/icons/document.svg" | relURL}}" alt="document">
+        <img src="{{"icons/document.svg" | relURL}}" alt="document">
         {{end}}
         {{ .Title }}
     </h1>
     {{ .Content }}
+
+    {{/* On section pages, render a card list of child pages
+         (subsections and regular pages alike, ordered by weight).
+         Without this, readers landing on /documentation/connect/
+         see only the section's short intro paragraph and have to
+         retreat to the sidebar to continue. */}}
+    {{- if .IsSection }}
+      {{- with .Pages.ByWeight }}
+        <ul class="section-children">
+          {{- range . }}
+            {{/* Front-matter description may carry inline Markdown
+                 (`code` spans for type and property names). Render it
+                 through markdownify, then strip Hugo's wrapping <p>
+                 so the snippet stays inline inside <span>. */}}
+            {{- $desc := "" -}}
+            {{- $isMarkdown := false -}}
+            {{- with .Description -}}
+              {{- $desc = . | markdownify -}}
+              {{- $desc = replaceRE `^<p>` "" $desc -}}
+              {{- $desc = replaceRE `</p>\s*$` "" $desc -}}
+              {{- $isMarkdown = true -}}
+            {{- end -}}
+            {{- if not $desc -}}
+              {{/* Fallback: strip the {{< review >}} shortcode's output
+                   before Hugo auto-truncates, otherwise pages that lead
+                   with a review block surface "Reviewed YYYY-MM-DD
+                   against source: ..." as their card description. */}}
+              {{- $cleaned := replaceRE `<p class="review-meta">[\s\S]*?</p>\s*` "" .Content -}}
+              {{- $desc = $cleaned | plainify | htmlUnescape | strings.TrimSpace | truncate 160 -}}
+            {{- end -}}
+            <li>
+              <a href="{{ .RelPermalink }}" class="section-children__link">
+                <span class="section-children__title">{{ .Title }}</span>
+                {{- with $desc }}
+                  <span class="section-children__summary">
+                    {{- if $isMarkdown }}{{ . | safeHTML }}{{ else }}{{ . }}{{ end -}}
+                  </span>
+                {{- end }}
+              </a>
+            </li>
+          {{- end }}
+        </ul>
+      {{- end }}
+    {{- end }}
+
     {{- partial "previous-next-links.html" . -}}
 </article>
\ No newline at end of file
diff --git a/docs/layouts/partials/docs/page-meta.html b/docs/layouts/partials/docs/page-meta.html
new file mode 100644
index 0000000000..b16612faf0
--- /dev/null
+++ b/docs/layouts/partials/docs/page-meta.html
@@ -0,0 +1,56 @@
+{{/*
+  page-meta: renders a thin band above the article body containing
+  breadcrumbs, last_reviewed date, and an "Edit this page on GitHub" link.
+
+  Reads frontmatter fields:
+    last_reviewed   string (ISO date), optional
+
+  Reads Site.Params:
+    docsRepo         (string)  github repo URL, e.g. "https://github.com/pgjdbc/pgjdbc"
+    docsBranch       (string)  branch name on the repo
+    docsContentRoot  (string)  path within the repo where the Hugo content/ lives
+
+  Breadcrumbs walk up Section -> Section.Parent -> ... -> home.
+*/}}
+{{- $home := .Site.Home -}}
+{{- $crumbs := slice -}}
+{{- $section := .CurrentSection -}}
+{{- range seq 8 -}}{{/* bounded loop to climb up the section tree */}}
+  {{- if and $section (ne $section.RelPermalink "/") -}}
+    {{- $crumbs = $crumbs | append $section -}}
+    {{- $section = $section.Parent -}}
+  {{- end -}}
+{{- end -}}
+{{- $reversed := slice -}}
+{{- range $i, $c := $crumbs -}}
+  {{- $reversed = $reversed | append (index $crumbs (sub (sub (len $crumbs) 1) $i)) -}}
+{{- end -}}
+<div class="page-meta">
+  <ol class="page-meta__breadcrumbs" aria-label="Breadcrumb">
+    <li><a href="{{ $home.RelPermalink }}">Home</a></li>
+    {{- range $reversed }}
+      <li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
+    {{- end }}
+    {{- if and .CurrentSection (ne .RelPermalink .CurrentSection.RelPermalink) }}
+      <li aria-current="page">{{ .Title }}</li>
+    {{- end }}
+  </ol>
+
+  {{- with .Params.last_reviewed }}
+    <span class="page-meta__reviewed" title="Date this page was last reviewed for accuracy">
+      Last reviewed <time datetime="{{ . }}">{{ . }}</time>
+    </span>
+  {{- end }}
+
+  {{- $repo := .Site.Params.docsRepo -}}
+  {{- $branch := .Site.Params.docsBranch | default "master" -}}
+  {{- $root := .Site.Params.docsContentRoot | default "docs/content" -}}
+  {{- with .File }}
+    {{- if $repo }}
+      <span class="page-meta__edit">
+        <a href="{{ $repo }}/edit/{{ $branch }}/{{ $root }}/{{ .Path }}"
+           target="_blank" rel="noopener">Edit this page on GitHub</a>
+      </span>
+    {{- end }}
+  {{- end }}
+</div>
diff --git a/docs/layouts/partials/docs/sidebar.html b/docs/layouts/partials/docs/sidebar.html
index 0dead43485..05c8dcd719 100644
--- a/docs/layouts/partials/docs/sidebar.html
+++ b/docs/layouts/partials/docs/sidebar.html
@@ -1,13 +1,60 @@
-{{ $currentPage := . }}
+{{/*
+  Two-level documentation sidebar driven by the actual page tree under
+  /documentation/, not by menus.toml.
 
-<nav role="navigation" class="sidebar">
+  Top level: each section (subdirectory with an _index.md), ordered by
+  `weight:` in the section's _index.md.
+
+  Second level: each page within the section, ordered by the page's
+  own `weight:`.
+
+  Pages with `build.list: never` (the legacy-URL hub pages) are
+  excluded from .Pages by Hugo automatically.
+*/}}
+{{- $currentPage := . -}}
+{{- $docsRoot := .Site.GetPage "/documentation" -}}
+
+<nav role="navigation" class="sidebar" aria-label="Documentation">
   <ul role="list" class="flow sidebar__wrapper">
-    {{ range .Site.Menus.docs.ByWeight }}
-    <li role="listitem" class="sidebar__links">
-      <a href="{{ .URL }}" class="sidebar__links__link{{ if eq $currentPage.RelPermalink .URL }}--active{{end}}">
-        {{.Name }}
-      </a>
-    </li>
-    {{- end}}
+
+    {{/* Top-level pages directly under /documentation/ (i.e. not in any
+         sub-section). With install.md moved into getting-started/, this
+         list is normally empty — but we render it defensively in case
+         a future page lives at the top level. */}}
+    {{- range $docsRoot.RegularPages.ByWeight }}
+      <li role="listitem" class="sidebar__links">
+        <a href="{{ .RelPermalink }}"
+           class="sidebar__links__link{{ if eq $currentPage.RelPermalink .RelPermalink }}--active{{ end }}">
+          {{ .Title }}
+        </a>
+      </li>
+    {{- end }}
+
+    {{/* Sections, ordered by _index.md weight. Each section is a
+         collapsible group with its pages underneath. */}}
+    {{- range $docsRoot.Sections.ByWeight }}
+      {{- $sectionActive := or
+            (eq $currentPage.RelPermalink .RelPermalink)
+            (strings.HasPrefix $currentPage.RelPermalink .RelPermalink) -}}
+      <li role="listitem" class="sidebar__section{{ if $sectionActive }} sidebar__section--active{{ end }}">
+        <a href="{{ .RelPermalink }}"
+           class="sidebar__section__title{{ if eq $currentPage.RelPermalink .RelPermalink }}--active{{ end }}">
+          {{ .Title }}
+        </a>
+        {{- with .RegularPages.ByWeight }}
+          <ul role="list" class="sidebar__section__pages">
+            {{- range . }}
+              <li role="listitem" class="sidebar__links">
+                <a href="{{ .RelPermalink }}"
+                   class="sidebar__links__link{{ if eq $currentPage.RelPermalink .RelPermalink }}--active{{ end }}">
+                  {{ .Title }}
+                </a>
+              </li>
+            {{- end }}
+          </ul>
+        {{- end }}
+      </li>
+    {{- end }}
+
   </ul>
-</nav>
\ No newline at end of file
+</nav>
diff --git a/docs/layouts/partials/home/example.html b/docs/layouts/partials/home/example.html
deleted file mode 100644
index 6f9edf3b3e..0000000000
--- a/docs/layouts/partials/home/example.html
+++ /dev/null
@@ -1,4 +0,0 @@
-<section class="example flow">
-    <h2>Processing a simple query</h2>
-    {{.Content}}
-</section>
\ No newline at end of file
diff --git a/docs/layouts/partials/home/feature.html b/docs/layouts/partials/home/feature.html
deleted file mode 100644
index fa81d06ad7..0000000000
--- a/docs/layouts/partials/home/feature.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<section class="feature">
-    {{ range $.Site.Data.homepagedata.feature }}
-    <div class="feature__item flow">
-        <img class="feature__item__media" src="{{ .path | relURL}}" alt="feature"/>
-        <p> {{ .desc }} </p>
-    </div>
-    {{ end }}
-</section>
\ No newline at end of file
diff --git a/docs/layouts/partials/home/hero.html b/docs/layouts/partials/home/hero.html
index bd5e41d6a7..59f98e60b2 100644
--- a/docs/layouts/partials/home/hero.html
+++ b/docs/layouts/partials/home/hero.html
@@ -1,21 +1,41 @@
 <section class="hero">
-    <img class="hero__img" src="{{"/images/elephants.png" | relURL}}" alt="elephant"/>
+    <img class="hero__img" src="{{"images/elephants.png" | relURL}}" alt=""/>
     <article class="hero__content">
         <h1 class="hero__content__head">
             PostgreSQL JDBC Driver
         </h1>
-        <h3 class="hero__content__desc">
-            An Open source JDBC driver written in Pure Java (Type 4), which communicates using the PostgreSQL native
-            network protocol
-        </h3>
+        <p class="hero__content__desc">
+            The official JDBC driver for PostgreSQL® &mdash; pure Java, JDBC&nbsp;4.2.
+        </p>
         <div class="hero__content__actions">
-            <button class="btn btn--contained">
-                <img src="{{"/icons/download2.svg" | relURL}}" alt="download2">
-                <a href="/download">Download</a>
-            </button>
-            <button class="btn btn--outlined">
-                <a href="/documentation">Go To Docs</a>
-            </button>
+            <a class="btn btn--contained" href="{{ "documentation/" | relURL }}">Documentation</a>
+            <a class="btn btn--outlined" href="{{ "download/" | relURL }}">Download</a>
         </div>
+        <ul class="hero__content__links" role="list">
+            <li><a href="{{ "changelogs/" | relURL }}">Changelog</a></li>
+            <li><a href="{{ "security/" | relURL }}">Security</a></li>
+        </ul>
+        <ul class="hero__content__badges" role="list">
+            <li>
+                <a href="https://central.sonatype.com/artifact/org.postgresql/postgresql" aria-label="View pgJDBC on Maven Central">
+                    <img src="https://img.shields.io/maven-central/v/org.postgresql/postgresql?label=Maven%20Central" alt="Maven Central"/>
+                </a>
+            </li>
+            <li>
+                <a href="https://javadoc.io/doc/org.postgresql/postgresql" aria-label="View pgJDBC Javadoc">
+                    <img src="https://javadoc.io/badge/org.postgresql/postgresql.svg" alt="Javadoc"/>
+                </a>
+            </li>
+            <li>
+                <a href="https://opensource.org/licenses/BSD-2-Clause" aria-label="BSD-2-Clause license">
+                    <img src="https://img.shields.io/badge/License-BSD--2--Clause-blue.svg" alt="License: BSD-2-Clause"/>
+                </a>
+            </li>
+            <li>
+                <a href="https://scorecard.dev/viewer/?uri=github.com/pgjdbc/pgjdbc" aria-label="OpenSSF Scorecard">
+                    <img src="https://api.scorecard.dev/projects/github.com/pgjdbc/pgjdbc/badge" alt="OpenSSF Scorecard"/>
+                </a>
+            </li>
+        </ul>
     </article>
-</section>
\ No newline at end of file
+</section>
diff --git a/docs/layouts/partials/home/info.html b/docs/layouts/partials/home/info.html
deleted file mode 100644
index 5cc7e2a66d..0000000000
--- a/docs/layouts/partials/home/info.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<section class="info">
-    <article class="info__about flow">
-        <h2>
-            Why pgJDBC?
-        </h2>
-        <p>
-            The PostgreSQL JDBC Driver allows Java programs to connect to a PostgreSQL database using standard, database
-            independent Java code. pgJDBC is an open source JDBC driver written in Pure Java (Type 4), and communicates in the
-            PostgreSQL native network protocol. Because of this, the driver is platform independent; once compiled, the
-            driver can be used on any system.
-        </p>
-    </article>
-    <article class="info__release flow">
-        <h2>
-            Latest Releases
-        </h2>
-        <p>
-            Current release is 42.7.11 This is a security and maintenance release. This fixes a SCRAM PBKDF2 iteration DoS vulnerability (CVE-2026-42198), adds the require_auth connection property, and includes numerous bug fixes.
-            See the <a href="https://jdbc.postgresql.org/changelogs/2026-04-28-42/">Changelog</a> for details
-        </p>
-        <ul role="list">
-            {{ range $.Site.Data.homepagedata.info }}
-            <li role="listitem">
-                <strong>{{ .version }}</strong> · {{ .date }} · <a href="{{ .url }}">Notes</a>
-            </li>
-            {{ end}}
-        </ul>
-    </article>
-</section>
diff --git a/docs/layouts/partials/home/quick-install.html b/docs/layouts/partials/home/quick-install.html
new file mode 100644
index 0000000000..ec73a4a7b7
--- /dev/null
+++ b/docs/layouts/partials/home/quick-install.html
@@ -0,0 +1,76 @@
+{{/*
+  Quick install block — Maven + Gradle examples stacked rather
+  than tabbed. The previous tab UI hid two of three snippets
+  behind clicks; this layout shows both Maven and Gradle in one
+  scan. Only Kotlin DSL is shown for Gradle — the Groovy DSL
+  differs by `()` vs `''` quoting, which Groovy-DSL users adapt
+  trivially.
+
+  Both blocks reuse the `code-tabs` markup (with a single tab
+  acting as the label) so the existing components/_code-tabs.scss
+  styling and static/js/code-tabs.js copy-button handling pick
+  this up unchanged.
+
+  The version string is the same "latest release" the release
+  card shows, read from /changelogs/*-release.md sorted reverse-
+  chronological. Single source of truth shared with release.html.
+*/}}
+{{- $version := "" -}}
+{{- range (where site.RegularPages "Section" "changelogs").ByDate.Reverse -}}
+  {{- if and (not $version) (findRE "^[0-9]" (.Params.version | default "")) -}}
+    {{- $version = .Params.version -}}
+  {{- end -}}
+{{- end -}}
+{{- $maven := printf `<dependency>
+    <groupId>org.postgresql</groupId>
+    <artifactId>postgresql</artifactId>
+    <version>%s</version>
+</dependency>` $version -}}
+{{- $gradleKotlin := printf `implementation("org.postgresql:postgresql:%s")` $version -}}
+
+<section class="quick-install">
+    <h2 class="quick-install__head">Add it to your build</h2>
+
+    {{/* Snippets share a single grid track sized to the widest
+         block (Maven XML), so the Gradle one-liner stretches to
+         match instead of leaving a stepped right edge. */}}
+    <div class="quick-install__snippets">
+    <div class="code-tabs" data-code-tabs id="qi-maven">
+        <ul class="code-tabs__list" role="tablist">
+            <li class="code-tabs__tab" role="presentation">
+                <button class="code-tabs__btn" role="tab" type="button"
+                        aria-controls="qi-maven-panel-0" aria-selected="true"
+                        data-tab-target="0">Maven</button>
+            </li>
+        </ul>
+        <div class="code-tabs__panels">
+            <button class="code-tabs__copy" type="button"
+                    aria-label="Copy snippet to clipboard">
+                <span class="code-tabs__copy-label">Copy</span>
+            </button>
+            <div id="qi-maven-panel-0" class="code-tabs__panel" role="tabpanel" data-active="true">
+                {{ transform.Highlight $maven "xml" "" }}
+            </div>
+        </div>
+    </div>
+
+    <div class="code-tabs" data-code-tabs id="qi-gradle">
+        <ul class="code-tabs__list" role="tablist">
+            <li class="code-tabs__tab" role="presentation">
+                <button class="code-tabs__btn" role="tab" type="button"
+                        aria-controls="qi-gradle-panel-0" aria-selected="true"
+                        data-tab-target="0">Gradle</button>
+            </li>
+        </ul>
+        <div class="code-tabs__panels">
+            <button class="code-tabs__copy" type="button"
+                    aria-label="Copy snippet to clipboard">
+                <span class="code-tabs__copy-label">Copy</span>
+            </button>
+            <div id="qi-gradle-panel-0" class="code-tabs__panel" role="tabpanel" data-active="true">
+                {{ transform.Highlight $gradleKotlin "kotlin" "" }}
+            </div>
+        </div>
+    </div>
+    </div>
+</section>
diff --git a/docs/layouts/partials/home/release.html b/docs/layouts/partials/home/release.html
new file mode 100644
index 0000000000..76cbf434f2
--- /dev/null
+++ b/docs/layouts/partials/home/release.html
@@ -0,0 +1,54 @@
+{{/*
+  Current release card.
+
+  Single source of truth for "the latest release" is the
+  /changelogs/ section's content tree. Each release file's
+  front-matter carries `version`, `date`, and `summary`; the
+  permalink is the URL. Listing in `.ByDate.Reverse` and taking
+  the first entry whose version starts with a digit (so any
+  non-release status notices filed in the section in the past
+  are skipped) gives the same data the homepage used to read from
+  homepagedata.toml, minus the duplication.
+
+  When a new version ships, only one file changes: the new
+  /changelogs/<YYYY-MM-DD>-<version>-release.md. The homepage
+  picks it up automatically on the next build.
+*/}}
+{{- $releases := slice -}}
+{{- range (where site.RegularPages "Section" "changelogs").ByDate.Reverse -}}
+  {{- if findRE "^[0-9]" (.Params.version | default "") -}}
+    {{- $releases = $releases | append . -}}
+  {{- end -}}
+{{- end -}}
+{{- $latest := index $releases 0 -}}
+{{- $recent := first 3 (after 1 $releases) -}}
+
+<section class="release">
+    <h2 class="release__head">Latest release</h2>
+
+    <div class="release__card">
+        <div class="release__primary">
+            <strong class="release__version">{{ $latest.Params.version }}</strong>
+            <span class="release__date">{{ $latest.Date.Format "2 January 2006" }}</span>
+        </div>
+
+        {{- with $latest.Params.summary }}
+        <p class="release__summary">{{ . }}</p>
+        {{- end }}
+
+        <ul class="release__actions" role="list">
+            <li><a href="{{ $latest.RelPermalink }}">Release notes &rarr;</a></li>
+            <li><a href="{{ "security/" | relURL }}">Security &rarr;</a></li>
+        </ul>
+
+        {{- if $recent }}
+        <p class="release__recent">
+            Recent:
+            {{- range $i, $r := $recent }}
+                {{ if $i }}<span class="release__sep">·</span>{{ end }}
+                <a href="{{ $r.RelPermalink }}">{{ $r.Params.version }}</a>
+            {{- end }}
+        </p>
+        {{- end }}
+    </div>
+</section>
diff --git a/docs/layouts/partials/home/report.html b/docs/layouts/partials/home/report.html
deleted file mode 100644
index c315bd0a28..0000000000
--- a/docs/layouts/partials/home/report.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<section class="report flow">
-    <h2>Report a bug or Contribute ?</h2>
-    <div class="report__row">
-        <article class="report__item">
-            <img src="{{"/icons/security.svg" | relURL}}" alt="bug">
-            <p class="report__desc">
-                Report a security issue,<br>
-                <a href="https://github.com/pgjdbc/pgjdbc/security/policy">
-                    here.
-                </a>
-            </p>
-        </article>
-        <article class="report__item">
-            <img src="{{"/icons/pull-request.svg" | relURL}}" alt="pull-rquest">
-            <p class="report__desc">
-                See something that's wrong or unclear?<br>
-                <a href="https://github.com/pgjdbc/pgjdbc/issues/new">
-                    submit a PR.
-                </a>
-            </p>
-        </article>
-    </div>
-</section>
\ No newline at end of file
diff --git a/docs/layouts/partials/home/support.html b/docs/layouts/partials/home/support.html
deleted file mode 100644
index 593fca09b2..0000000000
--- a/docs/layouts/partials/home/support.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<div class="support">
-    <img class="support__media" src="{{"/images/support.png" | relURL}}" alt="support">
-    <div class="support__desc">
-        <h2>Support Us</h2>
-        <p class="desc">PostgreSQL is free. <br>Please support our work by making a
-            <a href="https://www.postgresql.org/about/donate/">donation</a>.
-        </p>
-    </div>
-</div>
\ No newline at end of file
diff --git a/docs/layouts/partials/nav-mobile.html b/docs/layouts/partials/nav-mobile.html
index e4dbb86bb2..93bf6f247e 100644
--- a/docs/layouts/partials/nav-mobile.html
+++ b/docs/layouts/partials/nav-mobile.html
@@ -2,21 +2,32 @@
     <ul role="list">
         {{ range .Site.Menus.global }}
         <li role="listitem" class="hover-bg-black">
-            <a href="{{ .URL }}" title="{{ .Title }}">
+            <a href="{{ .URL | relURL }}" title="{{ .Title }}">
                 {{ .Name }}</a>
         </li>
         {{ end }}
     </ul>
 </div>
 <div class="docsmenu mobilemenu dn">
+    {{/* Mirror the desktop sidebar: section landings + their pages,
+         driven by the page tree instead of menus.toml. */}}
+    {{- $docsRoot := .Site.GetPage "/documentation" -}}
     <ul role="list">
-        {{ range .Site.Menus.docs.ByWeight }}
+        {{- range $docsRoot.RegularPages.ByWeight }}
         <li role="listitem" class="hover-bg-black">
-            <a href="{{ .URL }}">
-                {{.Name }}
-            </a>
+            <a href="{{ .RelPermalink }}">{{ .Title }}</a>
         </li>
-        {{- end}}
+        {{- end }}
+        {{- range $docsRoot.Sections.ByWeight }}
+        <li role="listitem" class="hover-bg-black">
+            <a href="{{ .RelPermalink }}"><strong>{{ .Title }}</strong></a>
+        </li>
+            {{- range .RegularPages.ByWeight }}
+            <li role="listitem" class="hover-bg-black docsmenu__subpage">
+                <a href="{{ .RelPermalink }}">{{ .Title }}</a>
+            </li>
+            {{- end }}
+        {{- end }}
     </ul>
 </div>
 <div class="nav-inside">
diff --git a/docs/layouts/partials/previous-next-links.html b/docs/layouts/partials/previous-next-links.html
index ce7db6db7f..34b685cd0f 100644
--- a/docs/layouts/partials/previous-next-links.html
+++ b/docs/layouts/partials/previous-next-links.html
@@ -1,4 +1,10 @@
-{{ $pages := where .Site.RegularPages "Section" .Section }}
+{{/* Sibling pages only. `.Section` is the top-level section (e.g.
+     "documentation"), so the previous form pulled in every page
+     across /documentation/* and navigated by global weight order —
+     a getting-started page would land you in postgresql-features
+     because some other section's page had the next-higher weight.
+     `.CurrentSection.RegularPages` restricts to the immediate folder. */}}
+{{ $pages := .CurrentSection.RegularPages }}
 
 <div class="pagination">
 	<div class="pagination__left">
diff --git a/docs/layouts/partials/scripts.html b/docs/layouts/partials/scripts.html
index 8c6144498f..2cf56cc4d5 100644
--- a/docs/layouts/partials/scripts.html
+++ b/docs/layouts/partials/scripts.html
@@ -1,4 +1,4 @@
 <script src="https://cdnjs.cloudflare.com/ajax/libs/lunr.js/2.3.9/lunr.min.js"
     integrity="sha512-4xUl/d6D6THrAnXAwGajXkoWaeMNwEKK4iNfq5DotEbLPAfk6FSxSP3ydNxqDgCw1c/0Z1Jg6L8h2j+++9BZmg=="
     crossorigin="anonymous" referrerpolicy="no-referrer"></script>
-<script defer language="javascript" type="module" src="{{ "/js/index.js" | urlize | relURL }}"></script>
\ No newline at end of file
+<script defer language="javascript" type="module" src="{{ "js/index.js" | urlize | relURL }}"></script>
\ No newline at end of file
diff --git a/docs/layouts/partials/search.html b/docs/layouts/partials/search.html
index dec16a7072..2e9f9121b4 100644
--- a/docs/layouts/partials/search.html
+++ b/docs/layouts/partials/search.html
@@ -1,19 +1,16 @@
-<form id="search" class="searchBox" role="search">
-    <input class="searchInput" type="search" name="" placeholder="Search" id="search-input" autocomplete="off">
-    <button class="searchButton" for="search-input">
-        <svg fill="#ffffff" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="24px" height="24px">
-            <path
-                d="M 24 2.8886719 C 12.365714 2.8886719 2.8886719 12.365723 2.8886719 24 C 2.8886719 35.634277 12.365714 45.111328 24 45.111328 C 29.036552 45.111328 33.664698 43.331333 37.298828 40.373047 L 52.130859 58.953125 C 52.130859 58.953125 55.379484 59.435984 57.396484 57.333984 C 59.427484 55.215984 58.951172 52.134766 58.951172 52.134766 L 40.373047 37.298828 C 43.331332 33.664697 45.111328 29.036548 45.111328 24 C 45.111328 12.365723 35.634286 2.8886719 24 2.8886719 z M 24 7.1113281 C 33.352549 7.1113281 40.888672 14.647457 40.888672 24 C 40.888672 33.352543 33.352549 40.888672 24 40.888672 C 14.647451 40.888672 7.1113281 33.352543 7.1113281 24 C 7.1113281 14.647457 14.647451 7.1113281 24 7.1113281 z" />
-        </svg>
-    </button>
-</form>
-
-<template id="search-result" hidden>
-    <article class="search-list container flow">
-        <h2><a class="summary-title-link"></a></h2>
-        <summary class="summary"></summary>
-        <div class="read-more-container">
-            <a class="read-more-link">Read More »</a>
-        </div>
-    </article>
-</template>
\ No newline at end of file
+<div class="searchBox" role="search">
+    <form id="search" class="searchBox__form">
+        <input class="searchInput" type="search" name="" placeholder="Search" id="search-input"
+               autocomplete="off" spellcheck="false" aria-label="Search the site"
+               aria-controls="search-results-dropdown" aria-expanded="false"
+               aria-autocomplete="list" aria-activedescendant="">
+        <kbd class="searchHint" aria-hidden="true">/</kbd>
+        <button type="submit" class="searchButton" aria-label="Search">
+            <svg fill="#ffffff" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="24px" height="24px">
+                <path
+                    d="M 24 2.8886719 C 12.365714 2.8886719 2.8886719 12.365723 2.8886719 24 C 2.8886719 35.634277 12.365714 45.111328 24 45.111328 C 29.036552 45.111328 33.664698 43.331333 37.298828 40.373047 L 52.130859 58.953125 C 52.130859 58.953125 55.379484 59.435984 57.396484 57.333984 C 59.427484 55.215984 58.951172 52.134766 58.951172 52.134766 L 40.373047 37.298828 C 43.331332 33.664697 45.111328 29.036548 45.111328 24 C 45.111328 12.365723 35.634286 2.8886719 24 2.8886719 z M 24 7.1113281 C 33.352549 7.1113281 40.888672 14.647457 40.888672 24 C 40.888672 33.352543 33.352549 40.888672 24 40.888672 C 14.647451 40.888672 7.1113281 33.352543 7.1113281 24 C 7.1113281 14.647457 14.647451 7.1113281 24 7.1113281 z" />
+            </svg>
+        </button>
+    </form>
+    <div id="search-results-dropdown" class="searchResults" role="listbox" hidden></div>
+</div>
diff --git a/docs/layouts/partials/site-footer.html b/docs/layouts/partials/site-footer.html
index 699684aef7..a1b3759299 100644
--- a/docs/layouts/partials/site-footer.html
+++ b/docs/layouts/partials/site-footer.html
@@ -1,6 +1,6 @@
 <footer class="footer" role="contentinfo">
     <div class="footer__item">
-        {{ range $.Site.Data.footer.external }}
+        {{ range hugo.Data.footer.external }}
         <ul role="list" class="list">
             <li class="list__head">{{ .title }}</li>
             {{/*  <br />  */}}
@@ -11,6 +11,6 @@
         {{ end }}
     </div>
     <div class="copyrights">
-        Copyright © 1996-2023 The PostgreSQL Global Development Group
+        Copyright © 1996-{{ now.Year }} The PostgreSQL Global Development Group
     </div>
-</footer>
\ No newline at end of file
+</footer>
diff --git a/docs/layouts/partials/site-nav.html b/docs/layouts/partials/site-nav.html
index f145d4fe9a..bcf104a0dd 100644
--- a/docs/layouts/partials/site-nav.html
+++ b/docs/layouts/partials/site-nav.html
@@ -3,12 +3,12 @@
 <nav class="navbar" role="navigation">
 
     <a href="{{.Site.BaseURL | relLangURL }}" id="host">
-        <img class="logo" src="{{"/icons/postgreslogo.svg" | relURL}}"  alt="logo" />
+        <img class="logo" src="{{"icons/postgreslogo.svg" | relURL}}"  alt="logo" />
     </a>
 
     <div class="navbar__item">
         {{ range .Site.Menus.global }}
-        <a href="{{ .URL }}" title="{{ .Title }}" class="navbar__item__link {{ if eq $currentPage .URL }} navbar__item__link--active {{end}}">{{ .Name }}</a>
+        <a href="{{ .URL | relURL }}" title="{{ .Title }}" class="navbar__item__link {{ if eq $currentPage .URL }} navbar__item__link--active {{end}}">{{ .Name }}</a>
         {{ end }}
     </div>
 
diff --git a/docs/layouts/security/list.html b/docs/layouts/security/list.html
index 61c02f88ea..fb09bfbd02 100644
--- a/docs/layouts/security/list.html
+++ b/docs/layouts/security/list.html
@@ -1,7 +1,7 @@
 {{ define "main" }}
 <article class="security container flow">
     <h1>
-        <img src="{{"/icons/shield.svg" | relURL}}" alt="shield">
+        <img src="{{"icons/shield.svg" | relURL}}" alt="shield">
         {{ .Title }}
     </h1>
     {{ .Content }}
diff --git a/docs/layouts/shortcodes/code-tab.html b/docs/layouts/shortcodes/code-tab.html
new file mode 100644
index 0000000000..21a8c3c732
--- /dev/null
+++ b/docs/layouts/shortcodes/code-tab.html
@@ -0,0 +1,29 @@
+{{/*
+  code-tab: a single tab inside {{< code-tabs >}}. Emits nothing on its own;
+  writes a (name, html) entry into the parent's Scratch under "code-tabs".
+
+  Params (named):
+    name  required, label shown on the tab button (e.g. "Maven", "Gradle (Kotlin)")
+    lang  optional, language hint for syntax highlighting (e.g. "xml", "kotlin", "java")
+*/}}
+{{- $name := .Get "name" -}}
+{{- $lang := .Get "lang" | default "" -}}
+{{- $body := trim .Inner "\n" -}}
+{{- $html := "" -}}
+{{- if $lang -}}
+  {{- $html = transform.Highlight $body $lang "" -}}
+{{- else -}}
+  {{- $html = printf "<pre><code>%s</code></pre>" (htmlEscape $body) -}}
+{{- end -}}
+{{- $entry := dict "name" $name "lang" $lang "html" $html -}}
+{{- /*
+  We cannot use .Parent.Scratch.Add because Hugo renders nested shortcodes
+  before the parent template runs, so the slice cannot be pre-initialised
+  by the parent. Instead we Get/append/Set, which works with an absent key.
+*/ -}}
+{{- $existing := .Parent.Scratch.Get "code-tabs" -}}
+{{- if $existing -}}
+  {{- .Parent.Scratch.Set "code-tabs" ($existing | append $entry) -}}
+{{- else -}}
+  {{- .Parent.Scratch.Set "code-tabs" (slice $entry) -}}
+{{- end -}}
diff --git a/docs/layouts/shortcodes/code-tabs.html b/docs/layouts/shortcodes/code-tabs.html
new file mode 100644
index 0000000000..9ba83358fe
--- /dev/null
+++ b/docs/layouts/shortcodes/code-tabs.html
@@ -0,0 +1,51 @@
+{{/*
+  code-tabs: container for one or more {{< code-tab >}} children.
+
+  Children push entries into .Scratch via .Parent.Scratch.Add. We render
+  .Inner first (which triggers the children but emits nothing, since each
+  child writes to Scratch instead of producing output), then read the
+  collected slice and emit the structured tabs/panels.
+
+  Usage:
+    {{< code-tabs >}}
+    {{< code-tab name="Maven" lang="xml" >}}
+    <dependency>...</dependency>
+    {{< /code-tab >}}
+    {{< code-tab name="Gradle (Kotlin)" lang="kotlin" >}}
+    implementation("org.postgresql:postgresql:42.7.11")
+    {{< /code-tab >}}
+    {{< /code-tabs >}}
+*/}}
+{{- $id := printf "ct-%d" .Ordinal -}}
+{{- /* trigger child shortcodes; they Set/append into Scratch and emit nothing */ -}}
+{{- $_ := .Inner -}}
+{{- $tabs := .Scratch.Get "code-tabs" -}}
+{{- if not $tabs -}}{{- $tabs = slice -}}{{- end -}}
+<div class="code-tabs" data-code-tabs id="{{ $id }}">
+  <ul class="code-tabs__list" role="tablist">
+    {{- range $i, $t := $tabs -}}
+      <li class="code-tabs__tab" role="presentation">
+        <button class="code-tabs__btn"
+                role="tab"
+                type="button"
+                aria-controls="{{ $id }}-panel-{{ $i }}"
+                aria-selected="{{ if eq $i 0 }}true{{ else }}false{{ end }}"
+                data-tab-target="{{ $i }}">{{ $t.name }}</button>
+      </li>
+    {{- end -}}
+  </ul>
+  <div class="code-tabs__panels">
+    <button class="code-tabs__copy" type="button"
+            aria-label="Copy snippet to clipboard">
+      <span class="code-tabs__copy-label">Copy</span>
+    </button>
+    {{- range $i, $t := $tabs -}}
+      <div id="{{ $id }}-panel-{{ $i }}"
+           class="code-tabs__panel"
+           role="tabpanel"
+           data-active="{{ if eq $i 0 }}true{{ else }}false{{ end }}">
+        {{ $t.html | safeHTML }}
+      </div>
+    {{- end -}}
+  </div>
+</div>
diff --git a/docs/layouts/shortcodes/legacy-anchors.html b/docs/layouts/shortcodes/legacy-anchors.html
new file mode 100644
index 0000000000..b82e302c10
--- /dev/null
+++ b/docs/layouts/shortcodes/legacy-anchors.html
@@ -0,0 +1,96 @@
+{{/*
+  legacy-anchors: emits an inline JSON map of legacy-anchor → new-URL
+  pairs for a hub page. Runtime JS (static/js/legacy-anchors.js) reads
+  the script tag on page load and redirects the browser when a known
+  legacy hash is present.
+
+  Params:
+    hub  required, one of "use" | "query" | "server-prepare" | "setup".
+
+  Used by the four Phase 1a hub pages whose content was split into
+  multiple topical successors. We can't rely on Hugo's `aliases:`
+  mechanism for this because aliases drop the URL fragment, so a deep
+  link like `/documentation/use/#sslmode` lands at
+  `/documentation/reference/connection-properties/` with no anchor.
+  The JSON map + JS preserves the deep link.
+*/}}
+{{- $hub := .Get "hub" -}}
+{{- $map := dict -}}
+
+{{- if eq $hub "use" -}}
+  {{- /* Section anchors that the original `use.md` defined. */ -}}
+  {{- $map = merge $map (dict
+      "#connecting-to-the-database" "/documentation/connect/url-syntax/"
+      "#system-properties"          "/documentation/reference/connection-properties/"
+      "#connection-parameters"      "/documentation/reference/connection-properties/"
+      "#unix-sockets"               "/documentation/connect/unix-sockets/"
+      "#connection-fail-over"       "/documentation/connect/failover/"
+  ) -}}
+  {{- /* Per-property anchors. The original `use.md` rendered each
+         property as a `### <name>` heading (Hugo auto-slug = lowercased
+         camelCase name). The new reference page emits `id=prop-<slug>`
+         per row, where slug is `anchorize <name>`. Map both. */ -}}
+  {{- range (index hugo.Data "connection-properties") -}}
+    {{- $name := .name -}}
+    {{- $slug := anchorize $name -}}
+    {{- $target := printf "/documentation/reference/connection-properties/#prop-%s" $slug -}}
+    {{/* legacy plain anchor (lowercased name): */}}
+    {{- $map = merge $map (dict (printf "#%s" (lower $name)) $target) -}}
+    {{/* anchorized: */}}
+    {{- $map = merge $map (dict (printf "#%s" $slug) $target) -}}
+  {{- end -}}
+
+{{- else if eq $hub "query" -}}
+  {{- $map = merge $map (dict
+      "#getting-results-based-on-a-cursor"      "/documentation/query/fetch-size/"
+      "#using-the-statement-or-preparedstatement-interface" "/documentation/query/basics/"
+      "#using-the-resultset-interface"          "/documentation/query/basics/"
+      "#performing-updates"                     "/documentation/query/basics/"
+      "#creating-and-modifying-database-objects" "/documentation/query/basics/"
+      "#using-java-8-date-and-time-classes"     "/documentation/data-types/date-time/"
+  ) -}}
+
+{{- else if eq $hub "server-prepare" -}}
+  {{- $map = merge $map (dict
+      "#accessing-the-extensions"                 "/documentation/postgresql-features/extensions-api/"
+      "#timestamp-infinity"                       "/documentation/data-types/infinity/"
+      "#geometric-data-types"                     "/documentation/data-types/geometric/"
+      "#large-objects"                            "/documentation/data-types/large-objects/"
+      "#listen--notify"                           "/documentation/postgresql-features/listen-notify/"
+      "#server-prepared-statements"               "/documentation/query/prepared-statements/"
+      "#parameter-status-messages"                "/documentation/postgresql-features/parameter-status/"
+      "#methods"                                  "/documentation/postgresql-features/parameter-status/"
+      "#physical-and-logical-replication-api"     "/documentation/postgresql-features/replication/"
+      "#configure-database"                       "/documentation/postgresql-features/replication/"
+      "#logical-replication"                      "/documentation/postgresql-features/replication/"
+      "#physical-replication"                     "/documentation/postgresql-features/replication/"
+      "#arrays"                                   "/documentation/data-types/arrays/"
+      "#copymanager"                              "/documentation/postgresql-features/copy/"
+  ) -}}
+
+{{- else if eq $hub "setup" -}}
+  {{- $map = merge $map (dict
+      "#getting-the-driver"                "/documentation/install/"
+      "#setting-up-the-class-path"         "/documentation/install/"
+      "#preparing-the-database-server-for-jdbc" "/documentation/getting-started/server-prep/"
+      "#creating-a-database"               "/documentation/getting-started/server-prep/"
+  ) -}}
+
+{{- end -}}
+{{/* Rewrite each target through relURL so the JSON map carries the
+     site BasePath. Without this, the runtime JS would redirect to
+     `/documentation/foo/` and skip the `/<repo>/` prefix when the
+     site is served from a project page (e.g. <user>.github.io/<repo>/).
+     Strip the leading `/` first — Hugo's relURL deliberately doesn't
+     prepend BasePath when the input already starts with `/`. The
+     `#prop-<slug>` fragment survives the transform; relURL leaves the
+     `#…` tail intact. */}}
+{{- $relMap := dict -}}
+{{- range $k, $v := $map -}}
+  {{- $relMap = merge $relMap (dict $k (relURL (strings.TrimPrefix "/" $v))) -}}
+{{- end -}}
+{{/* Emit as JSON using a custom MIME type. With type="application/json",
+     Hugo's HTML minifier parses the body and re-emits it as a JSON-encoded
+     string (doubly quoting the content). A non-recognised type makes the
+     minifier leave the body alone. */}}
+<script type="text/plain" data-legacy-anchors>{{- jsonify $relMap -}}</script>
diff --git a/docs/layouts/shortcodes/past-versions.html b/docs/layouts/shortcodes/past-versions.html
index fb710128e5..92c52950ca 100644
--- a/docs/layouts/shortcodes/past-versions.html
+++ b/docs/layouts/shortcodes/past-versions.html
@@ -1,5 +1,91 @@
-<ul class="custom-marker">
-    {{ range $ind, $ver := $.Site.Data.versions.past }}
-    <li><a href="/download/postgresql-{{$ver.version}}{{if $ver.suffix}}.{{$ver.suffix}}{{end}}.jar">{{$ver.version}}{{if $ver.suffix}}.{{$ver.suffix}}{{end}}</a></li>
-    {{end}}
+{{/*
+  Older download links, curated by release line:
+   * Current line (the latest changelog's X.Y.x) — every patch
+     except the newest one, which lives in the "recent versions"
+     card above.
+   * Each older non-classifier line (42.6.x, 42.5.x, 42.4.x,
+     42.3.x) — only its latest patch.
+   * 42.2.x line — the most recent prior .jre7 patch (the current
+     .jre7 lives in the "recent" card; the unclassified 42.2.x
+     build hasn't shipped since the line went JRE-7-only).
+
+  Sources of truth:
+   * /changelogs/*-release.md for tagged versions, ordering, and
+     the per-release date shown next to each link (from the
+     changelog's `date` front-matter).
+   * release-history.yaml (generated from git tags + the overlay)
+     for per-line latest patches, the .jre7 cutoff, and the
+     `Released` date of each older line's latest patch.
+  No data file needs to be touched when a new version ships.
+*/}}
+{{- $changelogs := where site.RegularPages "Section" "changelogs" -}}
+
+{{- /* Latest Java-8+ release: top of /changelogs/. */ -}}
+{{- $latest := "" -}}
+{{- range $changelogs.ByDate.Reverse -}}
+  {{- if and (not $latest) (findRE "^[0-9]" (.Params.version | default "")) -}}
+    {{- $latest = .Params.version -}}
+  {{- end -}}
+{{- end -}}
+{{- $parts := split $latest "." -}}
+{{- $currentLine := printf "%s.%s." (index $parts 0) (index $parts 1) -}}
+
+{{- /* .jre7 cutoff from release-history.yaml. */ -}}
+{{- $jre7Last := "" -}}
+{{- range (index hugo.Data "release-history").rows -}}
+  {{- if hasSuffix .version_range ".jre7" -}}
+    {{- $jre7Last = replaceRE "\\.jre7$" "" .version_range -}}
+  {{- end -}}
+{{- end -}}
+
+{{- /* Assemble the past list in display order. */ -}}
+{{- $past := slice -}}
+
+{{- /* 1. Current-line patches, newest first. */ -}}
+{{- range $changelogs.ByDate.Reverse -}}
+  {{- $v := .Params.version | default "" -}}
+  {{- if and (hasPrefix $v $currentLine) (ne $v $latest) -}}
+    {{- $past = $past | append (dict "version" $v "suffix" "" "date" (.Date.Format "2 January 2006")) -}}
+  {{- end -}}
+{{- end -}}
+
+{{- /* 2. Latest patch of each older non-classifier line. Skip the
+       current line (already covered) and 42.2.x (handled via jre7).
+       The `released >= "2018-01-01"` cut-off keeps the pre-42.2.x
+       lines (42.0.x and 42.1.x, both 2017) off the download page;
+       they shipped before pgJDBC settled into its current support
+       model and are not surfaced to users any more. */ -}}
+{{- range (index hugo.Data "release-history").rows -}}
+  {{- $vr := .version_range -}}
+  {{- $rl := .release_line -}}
+  {{- $linePrefix := replaceRE "x$" "" $rl -}}
+  {{- if and (not (findRE "\\.jre[0-9]+$" $vr)) (ne $linePrefix $currentLine) (ne $rl "42.2.x") (ge .released "2018-01-01") -}}
+    {{- $rangeParts := split $vr "-" -}}
+    {{- $lastPatch := index $rangeParts (sub (len $rangeParts) 1) -}}
+    {{- $past = $past | append (dict "version" $lastPatch "suffix" "" "date" (dateFormat "2 January 2006" .released)) -}}
+  {{- end -}}
+{{- end -}}
+
+{{- /* 3. Most recent prior .jre7 patch. */ -}}
+{{- $jre7Past := "" -}}
+{{- $jre7PastDate := "" -}}
+{{- range $changelogs.ByDate.Reverse -}}
+  {{- $v := .Params.version | default "" -}}
+  {{- if and (not $jre7Past) (hasPrefix $v "42.2.") (ne $v $jre7Last) -}}
+    {{- $jre7Past = $v -}}
+    {{- $jre7PastDate = .Date.Format "2 January 2006" -}}
+  {{- end -}}
+{{- end -}}
+{{- with $jre7Past -}}
+  {{- $past = $past | append (dict "version" . "suffix" "jre7" "date" $jre7PastDate) -}}
+{{- end -}}
+
+<ul class="past-versions">
+    {{- range $past }}
+    {{- $full := .version }}{{ if .suffix }}{{ $full = printf "%s.%s" .version .suffix }}{{ end }}
+    <li>
+        <a href="{{ printf "download/postgresql-%s.jar" $full | relURL }}">{{ $full }}</a>
+        <span class="past-versions__date">{{ .date }}</span>
+    </li>
+    {{- end }}
 </ul>
diff --git a/docs/layouts/shortcodes/recent-versions.html b/docs/layouts/shortcodes/recent-versions.html
index 5f2abd678c..a9e686ea99 100644
--- a/docs/layouts/shortcodes/recent-versions.html
+++ b/docs/layouts/shortcodes/recent-versions.html
@@ -1,20 +1,56 @@
+{{/*
+  Recent download cards — top patch for Java 8+, Java 7, Java 6.
+
+  Sources of truth:
+   * Java 8+ → newest /changelogs/*-release.md whose `version`
+     frontmatter starts with a digit. Same pick as the homepage
+     release card and quick-install snippet.
+   * Java 7 / Java 6 → release-history.yaml classifier rows
+     (`version_range: "42.2.29.jre7"` etc.), generated from git
+     tags + release-history-overlay.yaml. The classifier suffix
+     itself names the Java version: `jre7` -> 7, `jre6` -> 6.
+
+  When a new version ships, only the changelog file needs to land:
+  this shortcode refreshes automatically.
+*/}}
+{{- $latest := "" -}}
+{{- $latestDate := "" -}}
+{{- range (where site.RegularPages "Section" "changelogs").ByDate.Reverse -}}
+  {{- if and (not $latest) (findRE "^[0-9]" (.Params.version | default "")) -}}
+    {{- $latest = .Params.version -}}
+    {{- $latestDate = .Date.Format "2 January 2006" -}}
+  {{- end -}}
+{{- end -}}
+
+{{- $cards := slice (dict "version" $latest "suffix" "" "java" "8" "date" $latestDate) -}}
+{{- range (index hugo.Data "release-history").rows -}}
+  {{- $vr := .version_range -}}
+  {{- if findRE "\\.jre[0-9]+$" $vr -}}
+    {{- $suf  := replaceRE "^.*\\.(jre[0-9]+)$" "$1" $vr -}}
+    {{- $base := replaceRE "\\.jre[0-9]+$" "" $vr -}}
+    {{- $date := dateFormat "2 January 2006" .released -}}
+    {{- $java := strings.TrimPrefix "jre" $suf -}}
+    {{- $cards = $cards | append (dict "version" $base "suffix" $suf "java" $java "date" $date) -}}
+  {{- end -}}
+{{- end -}}
+
+{{- $jdbcByJava := dict "8" "JDBC 4.2" "7" "JDBC 4.1" "6" "JDBC 4.0" -}}
 <div class="card">
-    {{ range $ind, $ver := $.Site.Data.versions.recent }}
+    {{- range $ind, $c := $cards }}
+    {{- $full := $c.version }}{{ if $c.suffix }}{{ $full = printf "%s.%s" $c.version $c.suffix }}{{ end }}
     <div class="card__item">
-        <div class="card__title">{{$ver.j_name}}</div>
-        <div class="card__subtitle">{{$ver.version}}</div>
-        <p class="card__desc">{{$ver.description}}</p>
-        <button class="card__btn__left">
-            <a href="{{$ver.url}}" class="card__btn__left__link">Download</a>
-        </button>
-        <script class="maven" id="coordinates-{{$ind}}" type="application/xml">
+        <div class="card__title">Java {{ $c.java }}</div>
+        <div class="card__subtitle">{{ $full }}<span class="card__date">{{ $c.date }}</span></div>
+        <p class="card__desc">If you are using Java {{ $c.java }}{{ if eq $c.java "8" }} or newer{{ end }} then you should use the {{ index $jdbcByJava $c.java }} version.</p>
+        <a href="{{ printf "download/postgresql-%s.jar" $full | relURL }}" class="card__download">Download</a>
+        <script class="maven" id="coordinates-{{ $ind }}" type="application/xml">
             <dependency>
                 <groupId>org.postgresql</groupId>
                 <artifactId>postgresql</artifactId>
-                <version>{{$ver.version}}{{if $ver.suffix}}.{{$ver.suffix}}{{end}}</version>
+                <version>{{ $full }}</version>
             </dependency>
         </script>
-        <button class="card__btn__right" id="copyBtn-{{$ind}}" data="{{$ind}}">Copy Maven</button>
+        <button class="card__btn__right" id="copyBtn-{{ $ind }}" data="{{ $ind }}">Copy Maven</button>
     </div>
-    {{ end }}
+    {{- end }}
 </div>
diff --git a/docs/static/images/support.png b/docs/static/images/support.png
deleted file mode 100644
index acfe0294f9..0000000000
Binary files a/docs/static/images/support.png and /dev/null differ
diff --git a/docs/static/js/code-tabs.js b/docs/static/js/code-tabs.js
new file mode 100644
index 0000000000..eb4ea3e4ea
--- /dev/null
+++ b/docs/static/js/code-tabs.js
@@ -0,0 +1,88 @@
+// Tab switching + clipboard-copy for <div data-code-tabs>...</div>.
+// Buttons carry data-tab-target="<index>"; panels carry data-active="true|false".
+// A single .code-tabs__copy button per container reads the currently-active
+// panel's text and writes it to the clipboard.
+//
+// Copy uses navigator.clipboard.writeText() where available (secure
+// context: https://, localhost, 127.0.0.1) and falls back to a hidden
+// <textarea> + document.execCommand('copy') for non-secure contexts
+// (the dev server bound to a LAN IP, file://). The legacy path is
+// deprecated but still works in every current browser.
+
+async function copyText(text) {
+  if (navigator.clipboard && window.isSecureContext) {
+    try {
+      await navigator.clipboard.writeText(text);
+      return true;
+    } catch (e) {
+      // fall through to legacy path
+    }
+  }
+  const ta = document.createElement('textarea');
+  ta.value = text;
+  ta.setAttribute('readonly', '');
+  ta.style.position = 'fixed';
+  ta.style.top = '-9999px';
+  ta.style.left = '-9999px';
+  document.body.appendChild(ta);
+  ta.select();
+  let ok = false;
+  try {
+    ok = document.execCommand('copy');
+  } catch (e) {
+    ok = false;
+  }
+  document.body.removeChild(ta);
+  return ok;
+}
+
+document.querySelectorAll('[data-code-tabs]').forEach((root) => {
+  const buttons = root.querySelectorAll('.code-tabs__btn');
+  const panels  = root.querySelectorAll('.code-tabs__panel');
+
+  buttons.forEach((btn) => {
+    btn.addEventListener('click', () => {
+      const idx = btn.getAttribute('data-tab-target');
+      buttons.forEach((b) => b.setAttribute('aria-selected', b === btn ? 'true' : 'false'));
+      panels.forEach((p) => p.setAttribute(
+        'data-active',
+        p.id.endsWith(`-panel-${idx}`) ? 'true' : 'false',
+      ));
+    });
+  });
+
+  // Optional copy button. Absent on tab blocks that opted out.
+  const copyBtn = root.querySelector('.code-tabs__copy');
+  if (!copyBtn) return;
+
+  const label = copyBtn.querySelector('.code-tabs__copy-label') || copyBtn;
+  const originalText = label.textContent;
+  let resetTimer = null;
+
+  copyBtn.addEventListener('click', async () => {
+    const active = root.querySelector('.code-tabs__panel[data-active="true"]');
+    if (!active) return;
+
+    // textContent is preferred over innerText: it ignores CSS visibility
+    // and returns the source text verbatim. Chroma's per-line span
+    // wrappers (<span class="line"><span class="cl">...</span>\n</span>)
+    // keep the original newlines as text nodes, so textContent reads
+    // back the same snippet the user sees.
+    const codeEl = active.querySelector('code');
+    const text = (codeEl ? codeEl.textContent : active.textContent || '').replace(/\s+$/, '');
+    if (!text) {
+      console.warn('code-tabs: empty snippet, nothing to copy');
+      return;
+    }
+
+    const ok = await copyText(text);
+    label.textContent = ok ? 'Copied' : 'Copy failed';
+    copyBtn.classList.add(ok ? 'is-copied' : 'is-failed');
+
+    if (resetTimer) clearTimeout(resetTimer);
+    resetTimer = setTimeout(() => {
+      label.textContent = originalText;
+      copyBtn.classList.remove('is-copied', 'is-failed');
+    }, 1500);
+  });
+});
diff --git a/docs/static/js/index.js b/docs/static/js/index.js
index 41de176938..c76230cc12 100644
--- a/docs/static/js/index.js
+++ b/docs/static/js/index.js
@@ -1,4 +1,8 @@
 import './accordion.js'
+import './code-tabs.js'
 import './copyToClipboard.js'
+import './legacy-anchors.js'
 import './lunr-search.js'
+import './search-highlight.js'
 import './menutoggle.js'
+import './navbar-height.js'
diff --git a/docs/static/js/legacy-anchors.js b/docs/static/js/legacy-anchors.js
new file mode 100644
index 0000000000..2fd57a2c21
--- /dev/null
+++ b/docs/static/js/legacy-anchors.js
@@ -0,0 +1,27 @@
+// If the page carries a [data-legacy-anchors] script tag (emitted by
+// the {{< legacy-anchors >}} shortcode on hub pages) AND the current
+// URL has a hash that matches a known legacy anchor, redirect to the
+// new URL+anchor.
+//
+// Hub pages exist because Hugo's `aliases:` mechanism drops URL
+// fragments — without this script, a deep link like
+//   /documentation/use/#sslmode
+// would land on /documentation/reference/connection-properties/
+// without scrolling to the right row.
+const dataEl = document.querySelector('script[data-legacy-anchors]');
+if (dataEl && window.location.hash) {
+  let map;
+  try {
+    map = JSON.parse(dataEl.textContent);
+  } catch (e) {
+    map = null;
+  }
+  if (map) {
+    const target = map[window.location.hash.toLowerCase()];
+    if (target) {
+      // replace() (not assign()) so the broken legacy URL doesn't end
+      // up in the back-button history.
+      window.location.replace(target);
+    }
+  }
+}
diff --git a/docs/static/js/lunr-search.js b/docs/static/js/lunr-search.js
index 6760c97f7c..a97763a2ed 100644
--- a/docs/static/js/lunr-search.js
+++ b/docs/static/js/lunr-search.js
@@ -1,133 +1,518 @@
-window.addEventListener( "DOMContentLoaded", function ( event ) {
+// This module is loaded via `<script defer type="module">` (see scripts.html),
+// which by spec executes after the document is parsed — the DOM is ready,
+// so no DOMContentLoaded gate is needed.
+(function () {
+    const MAX_RESULTS = 8;
+    const FRAGMENT_RADIUS = 120;
+    const MAX_FRAGMENTS = 2;
+    const DEBOUNCE_MS = 120;
+
+    const form = document.getElementById("search");
+    const input = document.getElementById("search-input");
+    const dropdown = document.getElementById("search-results-dropdown");
+    const host = document.getElementById("host");
+    if (!form || !input || !dropdown || !host) return;
+    const base = host.href.replace(/\/$/, "");
+
     let index = null;
     let lookup = null;
-    let queuedTerm = null;
+    let indexPromise = null;
+    let debounceTimer = null;
+    let activeIndex = -1;
+    let currentResults = [];
 
-    let form = document.getElementById( "search" );
-    let input = document.getElementById( "search-input" );
-    let path = document.getElementById( "host" ).href;
+    // Open dropdown on focus when there's a query; trigger live search on input.
+    input.addEventListener("input", function () {
+        scheduleSearch();
+    });
+    input.addEventListener("focus", function () {
+        if (input.value.trim()) scheduleSearch(0);
+    });
 
-    form.addEventListener( "submit", function ( event ) {
+    // Keyboard navigation within the dropdown.
+    input.addEventListener("keydown", function (event) {
+        if (event.key === "ArrowDown") {
+            event.preventDefault();
+            moveActive(1);
+        } else if (event.key === "ArrowUp") {
+            event.preventDefault();
+            moveActive(-1);
+        } else if (event.key === "Enter") {
+            if (activeIndex >= 0 && currentResults[activeIndex]) {
+                event.preventDefault();
+                window.location.href = currentResults[activeIndex].uri;
+            }
+        } else if (event.key === "Escape") {
+            if (!dropdown.hidden) {
+                event.preventDefault();
+                closeDropdown();
+            } else {
+                input.blur();
+            }
+        }
+    });
+
+    // Submit handler: only matters if the user presses Enter without a selection.
+    // The first result is the safe default; if there is none, do nothing.
+    form.addEventListener("submit", function (event) {
         event.preventDefault();
+        if (currentResults[0]) window.location.href = currentResults[0].uri;
+    });
+
+    // Click outside closes the dropdown; click on a result follows the link.
+    document.addEventListener("click", function (event) {
+        if (!form.contains(event.target) && !dropdown.contains(event.target)) {
+            closeDropdown();
+        }
+    });
 
-        let term = input.value.trim();
-        if ( !term )
+    // Global `/` shortcut: focus the search input when not already typing in a field.
+    document.addEventListener("keydown", function (event) {
+        if (event.key !== "/" || event.ctrlKey || event.metaKey || event.altKey) return;
+        const t = event.target;
+        if (t === input) return;
+        const tag = t && t.tagName;
+        const editable = t && (
+            tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" ||
+            (t.isContentEditable === true)
+        );
+        if (editable) return;
+        event.preventDefault();
+        input.focus();
+        input.select();
+    });
+
+    function scheduleSearch(delay) {
+        clearTimeout(debounceTimer);
+        const ms = typeof delay === "number" ? delay : DEBOUNCE_MS;
+        debounceTimer = setTimeout(runSearch, ms);
+    }
+
+    function runSearch() {
+        const term = input.value.trim();
+        if (!term) {
+            closeDropdown();
             return;
+        }
+        ensureIndex().then(function () {
+            renderResults(term, performSearch(term));
+        }).catch(function () {
+            renderError();
+        });
+    }
 
-        startSearch( term );
-    }, false );
+    // Drop-in replacement for lunr.tokenizer that splits each separator-delimited
+    // chunk further into camelCase and PascalCase components, and emits adjacent
+    // bigrams. The original (lowercased) chunk is always retained, so verbatim
+    // queries like `defaultRowFetchSize` still hit. Snake_case is handled via
+    // the broadened separator, which now includes underscores.
+    //
+    // All emitted sub-tokens share the parent chunk's `position` metadata, so
+    // a hit on `executor` inside `QueryExecutorImpl` highlights the whole
+    // identifier in fragments — that's the right granularity for code names.
+    const IDENTIFIER_SEPARATOR = /[\s\-_]+/;
 
-    function startSearch( term ) {
-        if ( index ) {
-            // Index already present, search directly.
-            search( term );
+    function identifierAwareTokenizer(obj, metadata) {
+        if (obj == null || obj == undefined) return [];
+        if (Array.isArray(obj)) {
+            return obj.reduce(function (acc, item) {
+                return acc.concat(identifierAwareTokenizer(item, Object.assign({}, metadata)));
+            }, []);
+        }
+        const str = String(obj);
+        const tokens = [];
+        let sliceStart = 0;
+        for (let sliceEnd = 0; sliceEnd <= str.length; sliceEnd++) {
+            const char = str.charAt(sliceEnd);
+            const atEnd = sliceEnd === str.length;
+            if (atEnd || IDENTIFIER_SEPARATOR.test(char)) {
+                const sliceLength = sliceEnd - sliceStart;
+                if (sliceLength > 0) {
+                    expandIdentifier(
+                        tokens,
+                        str.slice(sliceStart, sliceEnd),
+                        sliceStart,
+                        sliceLength,
+                        metadata
+                    );
+                }
+                sliceStart = sliceEnd + 1;
+            }
         }
-        else if ( queuedTerm ) {
-            // Index is being loaded, replace the term we want to search for.
-            queuedTerm = term;
+        return tokens;
+    }
+    identifierAwareTokenizer.separator = IDENTIFIER_SEPARATOR;
+
+    function expandIdentifier(tokens, text, sliceStart, sliceLength, baseMetadata) {
+        const orig = text.toLowerCase();
+        tokens.push(makeToken(orig, sliceStart, sliceLength, tokens.length, baseMetadata));
+        // Plain lowercase text has nothing to split; skip the regex work.
+        if (!/[A-Z]/.test(text)) return;
+        // 1st pass: split lower→Upper boundaries (`getName` → `get Name`).
+        // 2nd pass: split ACRONYM→Word boundaries (`JSONFile` → `JSON File`,
+        //           `PgSQLInput` → `Pg SQL Input`).
+        const parts = text
+            .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
+            .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
+            .split(/\s+/)
+            .filter(Boolean)
+            .map(function (p) { return p.toLowerCase(); });
+        if (parts.length <= 1) return;
+        for (const p of parts) {
+            if (p !== orig) {
+                tokens.push(makeToken(p, sliceStart, sliceLength, tokens.length, baseMetadata));
+            }
         }
-        else {
-            // Start loading index, perform the search when done.
-            queuedTerm = term;
-            initIndex();
+        // Adjacent bigrams so concatenated-substring queries like `fetchsize`
+        // hit `defaultRowFetchSize` (which lunr would otherwise store as a
+        // single stemmed token without `fetchsize` as a substring).
+        for (let i = 0; i + 1 < parts.length; i++) {
+            const bigram = parts[i] + parts[i + 1];
+            if (bigram !== orig) {
+                tokens.push(makeToken(bigram, sliceStart, sliceLength, tokens.length, baseMetadata));
+            }
         }
     }
 
-    function searchDone() {
-        queuedTerm = null;
+    function makeToken(text, sliceStart, sliceLength, index, baseMetadata) {
+        const meta = Object.assign({}, baseMetadata, {
+            position: [sliceStart, sliceLength],
+            index: index,
+        });
+        return new lunr.Token(text, meta);
     }
 
-    function initIndex() {
-        let request = new XMLHttpRequest();
-        // console.log( request );
-        request.open( "GET", `${path}/search.json` );
-        request.responseType = "json";
-        request.addEventListener( "load", function ( event ) {
-            let documents = request.response;
-            lookup = {};
-            index = lunr( function () {
+    function ensureIndex() {
+        if (index) return Promise.resolve();
+        if (indexPromise) return indexPromise;
+        indexPromise = fetch(base + "/search.json", { credentials: "same-origin" })
+            .then(function (r) {
+                if (!r.ok) throw new Error("search index fetch failed: " + r.status);
+                return r.json();
+            })
+            .then(function (documents) {
+                lookup = {};
+                index = lunr(function () {
+                    this.ref("uri");
+                    this.field("title", { boost: 5 });
+                    this.field("description", { boost: 2 });
+                    this.field("categories");
+                    this.field("content");
+                    // Token positions are required to extract fragments around hits.
+                    this.metadataWhitelist = ["position"];
+                    // Replace lunr's default tokenizer so identifier-shaped
+                    // strings (camelCase, snake_case, ALLCAPSToWord) get split
+                    // into searchable parts in addition to their original form.
+                    this.tokenizer = identifierAwareTokenizer;
+                    for (const doc of documents) {
+                        this.add(doc);
+                        lookup[doc.uri] = doc;
+                    }
+                });
+            });
+        return indexPromise;
+    }
 
-                this.ref( "uri" );
+    function performSearch(term) {
+        // Tokenize the user's input the same way the index tokenizer does
+        // (whitespace + hyphens + underscores), then build a wildcard-tolerant
+        // lunr query so partial matches surface results while typing.
+        const tokens = term.toLowerCase().split(IDENTIFIER_SEPARATOR).filter(Boolean);
+        if (tokens.length === 0) return [];
+        try {
+            return index.query(function (q) {
+                for (const t of tokens) {
+                    const safe = t.replace(/[~^:*+\-]/g, "");
+                    if (!safe) continue;
+                    q.term(safe, { usePipeline: true, boost: 100 });
+                    q.term(safe, { usePipeline: false, wildcard: lunr.Query.wildcard.TRAILING, boost: 10 });
+                    // Both-sided wildcard turns the term into a substring match
+                    // (`*fetchsize*`) so camelCase property names like
+                    // `defaultRowFetchSize`, which lunr stores as a single
+                    // token, are still discoverable by their internal parts.
+                    // Guarded on length to avoid pulling half the index on
+                    // short tokens (e.g. "ssl" would otherwise match anything
+                    // containing those three letters).
+                    if (safe.length >= 4) {
+                        const both = lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING;
+                        q.term(safe, { usePipeline: false, wildcard: both, boost: 3 });
+                    }
+                    if (safe.length > 3) {
+                        q.term(safe, { usePipeline: false, editDistance: 1, boost: 1 });
+                    }
+                }
+            }).slice(0, MAX_RESULTS);
+        } catch (e) {
+            return [];
+        }
+    }
 
-                this.field( "title" );
-                this.field( "content" );
-                this.field( "description" );
-                this.field( "categories" );
+    // Build the destination URL for a result: keep any `#anchor` fragment
+    // but inject `?q=<term>` before it. search-highlight.js reads `?q` on
+    // page load and marks matching words in the body so the user lands with
+    // visible context, not just a scrolled-to row.
+    function withQueryParam(uri, q) {
+        if (!q) return uri;
+        const hashIdx = uri.indexOf("#");
+        const path = hashIdx === -1 ? uri : uri.slice(0, hashIdx);
+        const hash = hashIdx === -1 ? "" : uri.slice(hashIdx);
+        const sep = path.indexOf("?") === -1 ? "?" : "&";
+        return path + sep + "q=" + encodeURIComponent(q) + hash;
+    }
 
-                for ( let document of documents ) {
-                    this.add( document );
-                    lookup[document.uri] = document;
-                }
-            } );
+    function renderResults(term, results) {
+        currentResults = results.map(function (r) {
+            const doc = lookup[r.ref];
+            return Object.assign(
+                { matchData: r.matchData },
+                doc,
+                { uri: withQueryParam(doc.uri, term) }
+            );
+        });
+        activeIndex = -1;
+
+        clear(dropdown);
+
+        if (currentResults.length === 0) {
+            const empty = document.createElement("div");
+            empty.className = "searchResults__empty";
+            empty.textContent = "No results for “" + term + "”";
+            dropdown.appendChild(empty);
+        } else {
+            const list = document.createElement("ul");
+            list.className = "searchResults__list";
+            list.setAttribute("role", "listbox");
+            currentResults.forEach(function (doc, i) {
+                list.appendChild(renderResult(doc, i));
+            });
+            dropdown.appendChild(list);
+        }
 
-            // Search index is ready, perform the search now
-            search( queuedTerm );
-        }, false );
-        request.addEventListener( "error", searchDone, false );
-        request.send( null );
+        openDropdown();
     }
 
-    function search( term ) {
-        let results = index.search( term );
+    function renderResult(doc, i) {
+        const li = document.createElement("li");
+        li.className = "searchResults__item";
+        li.setAttribute("role", "option");
+        li.id = "search-result-" + i;
 
-        // The element where search results should be displayed, adjust as needed.
-        let target = document.querySelector( ".main-inner" );
+        const link = document.createElement("a");
+        link.className = "searchResults__link";
+        link.href = doc.uri;
+        link.addEventListener("mouseenter", function () { setActive(i); });
+
+        const title = document.createElement("div");
+        title.className = "searchResults__title";
+        appendHighlighted(title, doc.title || doc.uri, matchedTokensForField(doc.matchData, "title"));
+        link.appendChild(title);
+
+        const fragments = buildFragments(doc.content || "", matchData(doc.matchData, "content"));
+        if (fragments.length > 0) {
+            const snippet = document.createElement("div");
+            snippet.className = "searchResults__snippet";
+            fragments.forEach(function (frag, idx) {
+                if (idx > 0) {
+                    const sep = document.createElement("span");
+                    sep.className = "searchResults__ellipsis";
+                    sep.textContent = " … ";
+                    snippet.appendChild(sep);
+                }
+                appendHighlightedRanges(snippet, frag.text, frag.ranges);
+            });
+            link.appendChild(snippet);
+        } else if (doc.description) {
+            const snippet = document.createElement("div");
+            snippet.className = "searchResults__snippet";
+            snippet.textContent = doc.description;
+            link.appendChild(snippet);
+        }
 
-        while ( target.firstChild )
-            target.removeChild( target.firstChild );
+        li.appendChild(link);
+        return li;
+    }
 
-        let title = document.createElement( "h1" );
-        title.id = "search-results";
-        title.className = "list-title";
+    // matchData :: lunr metadata for a single field — array of {term, start, length}
+    function matchData(meta, field) {
+        const out = [];
+        if (!meta || !meta.metadata) return out;
+        for (const token of Object.keys(meta.metadata)) {
+            const fields = meta.metadata[token];
+            const fieldMeta = fields && fields[field];
+            if (!fieldMeta || !fieldMeta.position) continue;
+            for (const [start, length] of fieldMeta.position) {
+                out.push({ term: token, start: start, length: length });
+            }
+        }
+        return out;
+    }
 
-        if ( results.length == 0 )
-            title.textContent = `No results found for “${term}”`;
-        else if ( results.length == 1 )
-            title.textContent = `Found one result for “${term}”`;
-        else
-            title.textContent = `Found ${results.length} results for “${term}”`;
-        target.appendChild( title );
-        document.title = title.textContent;
+    function matchedTokensForField(meta, field) {
+        if (!meta || !meta.metadata) return [];
+        const tokens = [];
+        for (const token of Object.keys(meta.metadata)) {
+            const fields = meta.metadata[token];
+            if (fields && fields[field]) tokens.push(token);
+        }
+        return tokens;
+    }
 
-        let template = document.getElementById( "search-result" );
-        for ( let result of results ) {
-            let doc = lookup[result.ref];
+    // Build up to MAX_FRAGMENTS non-overlapping windows around real matches.
+    // `hits` is sorted by start; we greedily pick hits whose windows don't
+    // overlap an already-chosen one, then expand each to word boundaries.
+    function buildFragments(text, hits) {
+        if (!text || hits.length === 0) return [];
+        const sorted = hits.slice().sort(function (a, b) { return a.start - b.start; });
+        const windows = [];
+        for (const hit of sorted) {
+            if (windows.length >= MAX_FRAGMENTS) break;
+            const winStart = Math.max(0, hit.start - FRAGMENT_RADIUS);
+            const winEnd = Math.min(text.length, hit.start + hit.length + FRAGMENT_RADIUS);
+            const prev = windows[windows.length - 1];
+            if (prev && winStart <= prev.end) {
+                prev.end = Math.max(prev.end, winEnd);
+                prev.hits.push(hit);
+            } else {
+                windows.push({ start: winStart, end: winEnd, hits: [hit] });
+            }
+        }
+        return windows.map(function (w) {
+            const snapStart = snapToWordBoundary(text, w.start, -1);
+            const snapEnd = snapToWordBoundary(text, w.end, 1);
+            const slice = text.substring(snapStart, snapEnd);
+            const ranges = w.hits
+                .map(function (h) {
+                    return { start: h.start - snapStart, length: h.length };
+                })
+                .filter(function (r) { return r.start >= 0 && r.start < slice.length; });
+            return {
+                text: (snapStart > 0 ? "… " : "") + slice + (snapEnd < text.length ? " …" : ""),
+                ranges: ranges.map(function (r) {
+                    return { start: r.start + (snapStart > 0 ? 2 : 0), length: r.length };
+                })
+            };
+        });
+    }
 
-            // Fill out search result template, adjust as needed.
-            let element = template.content.cloneNode( true );
-            element.querySelector( ".summary-title-link" ).href =
-                element.querySelector( ".read-more-link" ).href = doc.uri;
-            element.querySelector( ".summary-title-link" ).textContent = doc.title;
-            element.querySelector( ".summary" ).textContent = truncate( doc.content, 70 );
-            target.appendChild( element );
+    function snapToWordBoundary(text, pos, dir) {
+        const len = text.length;
+        if (pos <= 0) return 0;
+        if (pos >= len) return len;
+        let i = pos;
+        const limit = Math.min(40, dir < 0 ? i : len - i);
+        for (let step = 0; step < limit; step++) {
+            const ch = text.charAt(i);
+            if (/\s/.test(ch)) return i;
+            i += dir;
+            if (i < 0 || i > len) break;
         }
-        title.scrollIntoView( true );
+        return pos;
+    }
 
-        searchDone();
+    // Append `text` to `parent`, wrapping any positions in `ranges` with <mark>.
+    // Uses DOM APIs throughout to keep user-controllable strings out of innerHTML.
+    function appendHighlightedRanges(parent, text, ranges) {
+        if (!ranges || ranges.length === 0) {
+            parent.appendChild(document.createTextNode(text));
+            return;
+        }
+        const sorted = ranges.slice().sort(function (a, b) { return a.start - b.start; });
+        let cursor = 0;
+        for (const r of sorted) {
+            if (r.start < cursor) continue;
+            if (r.start > cursor) {
+                parent.appendChild(document.createTextNode(text.substring(cursor, r.start)));
+            }
+            const mark = document.createElement("mark");
+            mark.textContent = text.substr(r.start, r.length);
+            parent.appendChild(mark);
+            cursor = r.start + r.length;
+        }
+        if (cursor < text.length) {
+            parent.appendChild(document.createTextNode(text.substring(cursor)));
+        }
     }
 
-    // This matches Hugo's own summary logic:
-    // https://github.com/gohugoio/hugo/blob/b5f39d23b8/helpers/content.go#L543
-    function truncate( text, minWords ) {
+    // Highlight by tokens (used for title where computing per-char ranges is
+    // overkill — title matches are short and we just want each occurrence bolded).
+    function appendHighlighted(parent, text, tokens) {
+        if (!tokens || tokens.length === 0) {
+            parent.appendChild(document.createTextNode(text));
+            return;
+        }
+        const escaped = tokens
+            .filter(Boolean)
+            .map(function (t) { return t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); });
+        if (escaped.length === 0) {
+            parent.appendChild(document.createTextNode(text));
+            return;
+        }
+        const re = new RegExp("(" + escaped.join("|") + ")", "gi");
+        let last = 0;
         let match;
-        let result = "";
-        let wordCount = 0;
-        let regexp = /(\S+)(\s*)/g;
-        while ( match = regexp.exec( text ) ) {
-            wordCount++;
-            if ( wordCount <= minWords )
-                result += match[0];
-            else {
-                let char1 = match[1][match[1].length - 1];
-                let char2 = match[2][0];
-                if ( /[.?!"]/.test( char1 ) || char2 == "\n" ) {
-                    result += match[1];
-                    break;
-                }
-                else
-                    result += match[0];
+        while ((match = re.exec(text)) !== null) {
+            if (match.index > last) {
+                parent.appendChild(document.createTextNode(text.substring(last, match.index)));
             }
+            const mark = document.createElement("mark");
+            mark.textContent = match[0];
+            parent.appendChild(mark);
+            last = match.index + match[0].length;
+            if (match[0].length === 0) re.lastIndex++;
         }
-        return result;
+        if (last < text.length) {
+            parent.appendChild(document.createTextNode(text.substring(last)));
+        }
+    }
+
+    function setActive(i) {
+        const items = dropdown.querySelectorAll(".searchResults__item");
+        items.forEach(function (el, idx) {
+            const on = idx === i;
+            el.classList.toggle("is-active", on);
+            el.setAttribute("aria-selected", on ? "true" : "false");
+        });
+        activeIndex = i;
+        if (i >= 0 && items[i]) {
+            input.setAttribute("aria-activedescendant", items[i].id);
+        } else {
+            input.setAttribute("aria-activedescendant", "");
+        }
+    }
+
+    function moveActive(delta) {
+        const count = currentResults.length;
+        if (count === 0) return;
+        let next = activeIndex + delta;
+        if (next < 0) next = count - 1;
+        if (next >= count) next = 0;
+        setActive(next);
+        const items = dropdown.querySelectorAll(".searchResults__item");
+        if (items[next]) items[next].scrollIntoView({ block: "nearest" });
+    }
+
+    function openDropdown() {
+        dropdown.hidden = false;
+        input.setAttribute("aria-expanded", "true");
+    }
+
+    function closeDropdown() {
+        dropdown.hidden = true;
+        input.setAttribute("aria-expanded", "false");
+        input.setAttribute("aria-activedescendant", "");
+        activeIndex = -1;
+    }
+
+    function renderError() {
+        clear(dropdown);
+        const err = document.createElement("div");
+        err.className = "searchResults__empty";
+        err.textContent = "Search is unavailable right now.";
+        dropdown.appendChild(err);
+        openDropdown();
+    }
+
+    function clear(node) {
+        while (node.firstChild) node.removeChild(node.firstChild);
     }
-}, false );
\ No newline at end of file
+})();
diff --git a/docs/static/js/navbar-height.js b/docs/static/js/navbar-height.js
new file mode 100644
index 0000000000..5fbede8dff
--- /dev/null
+++ b/docs/static/js/navbar-height.js
@@ -0,0 +1,24 @@
+// Measure the sticky .navbar and publish its rendered height as a CSS
+// custom property on :root. Sticky elements below it (e.g. .param-table
+// thead) read --navbar-height to park flush with its bottom edge.
+//
+// The static default in assets/sass/abstracts/_constants.scss is a sensible
+// fallback if this script doesn't run (or runs late); the measured value
+// overrides it once available.
+const sync = () => {
+  const navbar = document.querySelector('.navbar');
+  if (!navbar) return;
+  const h = navbar.getBoundingClientRect().height;
+  document.documentElement.style.setProperty('--navbar-height', `${h}px`);
+};
+
+sync();
+
+// Re-measure on viewport changes (font scaling, breakpoint hits, etc.).
+const ro = ('ResizeObserver' in window) ? new ResizeObserver(sync) : null;
+if (ro) {
+  const navbar = document.querySelector('.navbar');
+  if (navbar) ro.observe(navbar);
+} else {
+  window.addEventListener('resize', sync);
+}
diff --git a/docs/static/js/search-highlight.js b/docs/static/js/search-highlight.js
new file mode 100644
index 0000000000..3ea257aa77
--- /dev/null
+++ b/docs/static/js/search-highlight.js
@@ -0,0 +1,101 @@
+// Highlight search terms on the destination page after a click from the
+// site search dropdown. Reads `?q=<term>` (set by lunr-search.js), splits
+// the query into identifier-shaped tokens, then wraps every case-insensitive
+// occurrence in <mark class="search-hit"> within the main content area.
+//
+// If the URL has no `#anchor`, scrolls the first hit into view so the user
+// lands on context immediately.
+(function () {
+    const params = new URLSearchParams(window.location.search);
+    const q = params.get("q");
+    if (!q) return;
+
+    // Same separator as lunr-search.js's identifier-aware tokenizer, so the
+    // query splits the same way it indexed: "channel_binding" → [channel,
+    // binding]. Keeps camelCase as one token; case-insensitive regex below
+    // handles substring matches inside identifiers like `defaultRowFetchSize`.
+    const tokens = q.split(/[\s\-_]+/).filter(function (t) { return t.length >= 2; });
+    if (tokens.length === 0) return;
+
+    const escape = function (s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); };
+    const pattern = "(" + tokens.map(escape).join("|") + ")";
+    const re = new RegExp(pattern, "gi");
+
+    // Scope the walk to article content. We skip the navbar (already
+    // contains the user's query in its search input), the search dropdown
+    // (rendered above an article, would re-mark its own children), code
+    // blocks (sequence boundary is delicate, plus their styling already
+    // foregrounds identifiers), and non-content elements that can't carry
+    // text (script/style/noscript/template).
+    const main = document.querySelector("main") || document.body;
+    const SKIP_TAGS = new Set([
+        "SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "INPUT", "TEXTAREA", "SELECT", "BUTTON",
+    ]);
+    const SKIP_SELECTORS = [
+        ".navbar",
+        ".searchResults",
+        ".nav-mob",
+        ".param-table__row-anchor",
+    ];
+
+    function shouldSkip(textNode) {
+        let el = textNode.parentElement;
+        while (el && el !== main) {
+            if (SKIP_TAGS.has(el.tagName)) return true;
+            if (el.classList && el.classList.contains("search-hit")) return true;
+            for (const sel of SKIP_SELECTORS) {
+                if (el.matches && el.matches(sel)) return true;
+            }
+            el = el.parentElement;
+        }
+        return false;
+    }
+
+    const walker = document.createTreeWalker(main, NodeFilter.SHOW_TEXT, {
+        acceptNode: function (node) {
+            if (!node.nodeValue || node.nodeValue.length < 2) return NodeFilter.FILTER_REJECT;
+            if (shouldSkip(node)) return NodeFilter.FILTER_REJECT;
+            return NodeFilter.FILTER_ACCEPT;
+        },
+    });
+
+    // Collect first, mutate after — mutating during walk breaks the iterator.
+    const nodes = [];
+    let n;
+    while ((n = walker.nextNode())) nodes.push(n);
+
+    let firstHit = null;
+    for (const node of nodes) {
+        const text = node.nodeValue;
+        re.lastIndex = 0;
+        if (!re.test(text)) continue;
+        re.lastIndex = 0;
+
+        const frag = document.createDocumentFragment();
+        let last = 0;
+        let m;
+        while ((m = re.exec(text)) !== null) {
+            if (m.index > last) {
+                frag.appendChild(document.createTextNode(text.slice(last, m.index)));
+            }
+            const mark = document.createElement("mark");
+            mark.className = "search-hit";
+            mark.textContent = m[0];
+            frag.appendChild(mark);
+            if (!firstHit) firstHit = mark;
+            last = m.index + m[0].length;
+            // Guard against zero-width matches looping forever.
+            if (m[0].length === 0) re.lastIndex++;
+        }
+        if (last < text.length) {
+            frag.appendChild(document.createTextNode(text.slice(last)));
+        }
+        node.parentNode.replaceChild(frag, node);
+    }
+
+    // If the URL pointed to a specific anchor, the browser already scrolled
+    // there and we leave the viewport alone. Otherwise center the first hit.
+    if (firstHit && !window.location.hash) {
+        firstHit.scrollIntoView({ block: "center" });
+    }
+})();
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 8da6e3abed..16d7762440 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -47,6 +47,7 @@ if (providers.gradleProperty("jdkTestVersion").orNull?.toInt() != 8) {
 }
 include("postgresql")
 include("testkit")
+include("docs-tools")
 
 project(":postgresql").projectDir = file("pgjdbc")

---

From: "davecramer (@davecramer)" <[email-masked]>
Date: Thu, 28 May 2026 11:59:05 +0000

Error: error building site: render: "/Users/davecra/projects/jdbc/pgjdbc.git/docs/layouts/alias.html:2:13": execute of template failed: template: alias.html:2:13: executing "alias.html" at <site>: can't evaluate field Locale in type *langs.Language