From b8a41094d9be6a4d3e63fce7ab6bb33b6549a017 Mon Sep 17 00:00:00 2001
From: Colin Rotherham <work@colinr.com>
Date: Tue, 14 Nov 2023 15:00:35 +0000
Subject: [PATCH 1/4] Move Express.js params to `app.param()` locals

---
 packages/govuk-frontend-review/src/app.mjs | 80 +++++++++++++++-------
 1 file changed, 56 insertions(+), 24 deletions(-)

diff --git a/packages/govuk-frontend-review/src/app.mjs b/packages/govuk-frontend-review/src/app.mjs
index df72b045a4..24ac5473bf 100644
--- a/packages/govuk-frontend-review/src/app.mjs
+++ b/packages/govuk-frontend-review/src/app.mjs
@@ -80,6 +80,55 @@ export default async () => {
   // Configure nunjucks
   const env = nunjucks.renderer(app)
 
+  // Define parameters
+
+  /**
+   * Handle parameter :componentName
+   *
+   * Finds all component fixtures and default example
+   */
+  app.param('componentName', (req, res, next, componentName) => {
+    const exampleName = 'default'
+
+    // Find all fixtures for component
+    const componentFixtures = componentsFixtures.find(
+      ({ component }) => component === componentName
+    )
+
+    // Find default fixture for component
+    const componentFixture = componentFixtures?.fixtures.find(
+      ({ name }) => name === exampleName
+    )
+
+    // Add response locals
+    res.locals.componentName = componentName
+    res.locals.componentFixtures = componentFixtures
+    res.locals.componentFixture = componentFixture
+    res.locals.exampleName = 'default'
+
+    next()
+  })
+
+  /**
+   * Handle parameter :exampleName
+   *
+   * Finds component fixture for example and updates locals
+   */
+  app.param('exampleName', (req, res, next, exampleName) => {
+    const { componentFixtures } = res.locals
+
+    // Replace default fixture with named example
+    const componentFixture = componentFixtures?.fixtures.find(
+      ({ name }) => nunjucks.filters.slugify(name) === exampleName
+    )
+
+    // Update response locals
+    res.locals.componentFixture = componentFixture
+    res.locals.exampleName = exampleName
+
+    next()
+  })
+
   // Define routes
 
   // Index page - render the component list template
@@ -92,14 +141,6 @@ export default async () => {
     })
   })
 
-  // Whenever the route includes a :componentName parameter, read the component fixtures
-  app.param('componentName', function (req, res, next, componentName) {
-    res.locals.componentFixtures = componentsFixtures.find(
-      ({ component }) => component === componentName
-    )
-    next()
-  })
-
   // All components redirect
   app.get('/components/all', function (req, res) {
     res.redirect('./')
@@ -119,14 +160,11 @@ export default async () => {
   app.get(
     '/components/:componentName/:exampleName?/preview',
     function (req, res, next) {
-      const { componentName, exampleName = 'default' } = req.params
-
-      /** @type {ComponentFixtures | undefined} */
-      const componentFixtures = res.locals.componentFixtures
-
-      const fixture = componentFixtures?.fixtures.find(
-        (fixture) => nunjucks.filters.slugify(fixture.name) === exampleName
-      )
+      const {
+        componentName,
+        componentFixtures: fixtures,
+        componentFixture: fixture
+      } = res.locals
 
       if (!fixture) {
         return next()
@@ -153,17 +191,15 @@ export default async () => {
 
       res.render('component-preview', {
         bodyClasses,
-        componentName,
         componentView,
-        exampleName,
-        previewLayout: componentFixtures.previewLayout
+        previewLayout: fixtures.previewLayout
       })
     }
   )
 
   // Example view
   app.get('/examples/:exampleName', function (req, res) {
-    const { exampleName } = req.params
+    const { exampleName } = res.locals
 
     res.render(`examples/${exampleName}/index`, {
       exampleName,
@@ -179,10 +215,6 @@ export default async () => {
   return app
 }
 
-/**
- * @typedef {import('@govuk-frontend/lib/components').ComponentFixtures} ComponentFixtures
- */
-
 /**
  * @typedef {object} FeatureFlags
  * @property {boolean} isDeployedToHeroku - Review app using `HEROKU_APP`

From 12ce02a777649aa2d8cbfd79705e74eba611b4b2 Mon Sep 17 00:00:00 2001
From: Colin Rotherham <work@colinr.com>
Date: Tue, 14 Nov 2023 13:08:04 +0000
Subject: [PATCH 2/4] Add routes and views for 404, 500 errors

---
 packages/govuk-frontend-review/src/app.mjs    | 36 ++++++++++++++++---
 .../src/common/lib/files.mjs                  |  1 +
 .../src/common/nunjucks/filters/index.mjs     |  1 +
 .../src/common/nunjucks/filters/inspect.mjs   | 16 +++++++++
 .../nunjucks/globals/get-nunjucks-code.mjs    | 11 ++----
 .../src/routes/full-page-examples.mjs         |  8 ++++-
 .../src/views/errors/404.njk                  | 18 ++++++++++
 .../src/views/errors/500.njk                  | 24 +++++++++++++
 8 files changed, 101 insertions(+), 14 deletions(-)
 create mode 100644 packages/govuk-frontend-review/src/common/nunjucks/filters/inspect.mjs
 create mode 100644 packages/govuk-frontend-review/src/views/errors/404.njk
 create mode 100644 packages/govuk-frontend-review/src/views/errors/500.njk

diff --git a/packages/govuk-frontend-review/src/app.mjs b/packages/govuk-frontend-review/src/app.mjs
index 24ac5473bf..00f63215c8 100644
--- a/packages/govuk-frontend-review/src/app.mjs
+++ b/packages/govuk-frontend-review/src/app.mjs
@@ -147,8 +147,13 @@ export default async () => {
   })
 
   // Component examples
-  app.get('/components/:componentName?', (req, res) => {
-    const { componentName } = req.params
+  app.get('/components/:componentName?', (req, res, next) => {
+    const { componentName } = res.locals
+
+    // Unknown component, continue to page not found
+    if (componentName && !componentNames.includes(componentName)) {
+      return next()
+    }
 
     res.render(componentName ? 'component' : 'components', {
       componentsFixtures,
@@ -166,7 +171,8 @@ export default async () => {
         componentFixture: fixture
       } = res.locals
 
-      if (!fixture) {
+      // Unknown component or fixture, continue to page not found
+      if (!componentNames.includes(componentName) || !fixtures || !fixture) {
         return next()
       }
 
@@ -198,9 +204,14 @@ export default async () => {
   )
 
   // Example view
-  app.get('/examples/:exampleName', function (req, res) {
+  app.get('/examples/:exampleName', function (req, res, next) {
     const { exampleName } = res.locals
 
+    // Unknown example, continue to page not found
+    if (!exampleNames.includes(exampleName)) {
+      return next()
+    }
+
     res.render(`examples/${exampleName}/index`, {
       exampleName,
 
@@ -212,6 +223,23 @@ export default async () => {
   // Full page example views
   routes.fullPageExamples(app)
 
+  /**
+   * Page not found handler
+   */
+  app.use((req, res) => {
+    res.status(404).render('errors/404')
+  })
+
+  /**
+   * Error handler
+   */
+  app.use((error, req, res, next) => {
+    console.error(error)
+    res.status(500).render('errors/500', {
+      error
+    })
+  })
+
   return app
 }
 
diff --git a/packages/govuk-frontend-review/src/common/lib/files.mjs b/packages/govuk-frontend-review/src/common/lib/files.mjs
index 35e973036c..37b08f2ae9 100644
--- a/packages/govuk-frontend-review/src/common/lib/files.mjs
+++ b/packages/govuk-frontend-review/src/common/lib/files.mjs
@@ -55,6 +55,7 @@ export async function getFullPageExamples() {
  *
  * @typedef {object} FullPageExample
  * @property {string} name - Example name
+ * @property {string} path - Example directory name
  * @property {string} [scenario] - Description explaining the example
  * @property {string} [notes] - Additional notes about the example
  */
diff --git a/packages/govuk-frontend-review/src/common/nunjucks/filters/index.mjs b/packages/govuk-frontend-review/src/common/nunjucks/filters/index.mjs
index 74f82137f8..5991549605 100644
--- a/packages/govuk-frontend-review/src/common/nunjucks/filters/index.mjs
+++ b/packages/govuk-frontend-review/src/common/nunjucks/filters/index.mjs
@@ -3,6 +3,7 @@
  */
 export { componentNameToMacroName } from '@govuk-frontend/lib/names'
 export { highlight } from './highlight.mjs'
+export { inspect } from './inspect.mjs'
 export { markdown } from './markdown.mjs'
 export { slugify } from './slugify.mjs'
 export { unslugify } from './unslugify.mjs'
diff --git a/packages/govuk-frontend-review/src/common/nunjucks/filters/inspect.mjs b/packages/govuk-frontend-review/src/common/nunjucks/filters/inspect.mjs
new file mode 100644
index 0000000000..1b2c25b6f3
--- /dev/null
+++ b/packages/govuk-frontend-review/src/common/nunjucks/filters/inspect.mjs
@@ -0,0 +1,16 @@
+import util from 'util'
+
+/**
+ * Format JavaScript objects as strings
+ *
+ * @param {unknown} object - JavaScript object to format
+ * @returns {string} Formatted string
+ */
+export function inspect(object) {
+  return util.inspect(object, {
+    compact: false,
+    depth: Infinity,
+    maxArrayLength: Infinity,
+    maxStringLength: Infinity
+  })
+}
diff --git a/packages/govuk-frontend-review/src/common/nunjucks/globals/get-nunjucks-code.mjs b/packages/govuk-frontend-review/src/common/nunjucks/globals/get-nunjucks-code.mjs
index 95fc9f3a15..6c785affae 100644
--- a/packages/govuk-frontend-review/src/common/nunjucks/globals/get-nunjucks-code.mjs
+++ b/packages/govuk-frontend-review/src/common/nunjucks/globals/get-nunjucks-code.mjs
@@ -1,9 +1,7 @@
-import { inspect } from 'util'
-
 import prettier from '@prettier/sync'
 import { outdent } from 'outdent'
 
-import { componentNameToMacroName } from '../filters/index.mjs'
+import { inspect, componentNameToMacroName } from '../filters/index.mjs'
 
 /**
  * Component Nunjucks code (formatted)
@@ -16,12 +14,7 @@ export function getNunjucksCode(componentName, options) {
   const macroName = componentNameToMacroName(componentName)
 
   // Allow nested HTML strings to wrap at `\n`
-  const paramsFormatted = inspect(options.context, {
-    compact: false,
-    depth: Infinity,
-    maxArrayLength: Infinity,
-    maxStringLength: Infinity
-  })
+  const paramsFormatted = inspect(options.context)
 
   // Format Nunjucks safely with double quotes
   const macroFormatted = prettier.format(`${macroName}(${paramsFormatted})`, {
diff --git a/packages/govuk-frontend-review/src/routes/full-page-examples.mjs b/packages/govuk-frontend-review/src/routes/full-page-examples.mjs
index 90c38d0a75..088c227e0b 100644
--- a/packages/govuk-frontend-review/src/routes/full-page-examples.mjs
+++ b/packages/govuk-frontend-review/src/routes/full-page-examples.mjs
@@ -2,6 +2,7 @@ import { getFullPageExamples } from '../common/lib/files.mjs'
 import * as routes from '../views/full-page-examples/index.mjs'
 
 const fullPageExamples = await getFullPageExamples()
+const fullPageExampleNames = fullPageExamples.map(({ path }) => path)
 
 /**
  * @param {import('express').Application} app
@@ -29,9 +30,14 @@ export default (app) => {
   })
 
   // Display full page examples index by default if not handled already
-  app.get('/full-page-examples/:exampleName', (req, res) => {
+  app.get('/full-page-examples/:exampleName', (req, res, next) => {
     const { exampleName } = req.params
 
+    // No matching example so continue to page not found
+    if (!fullPageExampleNames.includes(exampleName)) {
+      return next()
+    }
+
     res.render(`full-page-examples/${exampleName}/index`)
   })
 }
diff --git a/packages/govuk-frontend-review/src/views/errors/404.njk b/packages/govuk-frontend-review/src/views/errors/404.njk
new file mode 100644
index 0000000000..c7f3a2c7a8
--- /dev/null
+++ b/packages/govuk-frontend-review/src/views/errors/404.njk
@@ -0,0 +1,18 @@
+{% extends "layouts/layout.njk" %}
+
+{% block pageTitle %}Page not found - GOV.UK Frontend{% endblock %}
+
+{% block content %}
+  <div class="govuk-grid-row">
+    <div class="govuk-grid-column-three-quarters govuk-grid-column-two-thirds-from-desktop">
+      <h1 class="govuk-heading-l">Page not found</h1>
+
+      <p class="govuk-body">If you typed the web address, check it is correct.</p>
+      <p class="govuk-body">If you pasted the web address, check you copied the entire address.</p>
+
+      <p class="govuk-body">
+        <a class="govuk-link" href="https://design-system.service.gov.uk/get-in-touch/" rel="noreferrer noopener" target="_blank">Contact the Design System team (opens in new tab)</a> if you believe you are seeing this message in error.
+      </p>
+    </div>
+  </div>
+{% endblock %}
diff --git a/packages/govuk-frontend-review/src/views/errors/500.njk b/packages/govuk-frontend-review/src/views/errors/500.njk
new file mode 100644
index 0000000000..f872676087
--- /dev/null
+++ b/packages/govuk-frontend-review/src/views/errors/500.njk
@@ -0,0 +1,24 @@
+{% extends "layouts/layout.njk" %}
+
+{% block pageTitle %}Sorry, there is a problem with the service - GOV.UK Frontend{% endblock %}
+
+{% block content %}
+  <div class="govuk-grid-row">
+    <div class="govuk-grid-column-three-quarters govuk-grid-column-two-thirds-from-desktop">
+      <h1 class="govuk-heading-l">Sorry, there is a problem with the service</h1>
+
+      <p class="govuk-body">Try again later.</p>
+
+      <p class="govuk-body">
+        <a class="govuk-link" href="https://design-system.service.gov.uk/get-in-touch/" rel="noreferrer noopener" target="_blank">Contact the Design System team (opens in new tab)</a> if you believe you are seeing this message in error.
+      </p>
+    </div>
+  </div>
+
+  <div class="govuk-grid-row">
+    <div class="govuk-grid-column-full">
+      <pre class="app-code"><code tabindex="0" class="app-code__container">
+        {{- error | inspect | safe -}}
+      </code></pre>
+  </div>
+{% endblock %}

From 9ab2a333d867dd5e2c70ddab952235b820b59797 Mon Sep 17 00:00:00 2001
From: Colin Rotherham <work@colinr.com>
Date: Mon, 20 Nov 2023 11:52:12 +0000
Subject: [PATCH 3/4] Add breadcrumbs to top-level pages (including errors)

Review app error pages include breadcrumbs only because we have no header component with navigation links back to home etc
---
 .../src/views/component.njk                      |  8 ++++++--
 .../src/views/components.njk                     | 14 +++++++++++---
 .../src/views/errors/404.njk                     | 16 ++++++++++++++++
 .../src/views/errors/500.njk                     | 16 ++++++++++++++++
 .../src/views/full-page-examples/index.njk       | 14 +++++++++++---
 5 files changed, 60 insertions(+), 8 deletions(-)

diff --git a/packages/govuk-frontend-review/src/views/component.njk b/packages/govuk-frontend-review/src/views/component.njk
index f687ded549..a853ab803c 100644
--- a/packages/govuk-frontend-review/src/views/component.njk
+++ b/packages/govuk-frontend-review/src/views/component.njk
@@ -15,11 +15,15 @@
 
 {% block beforeContent %}
   {{ govukBreadcrumbs({
-    "items": [
+    items: [
       {
-        text: 'GOV.UK Frontend',
+        text: "GOV.UK Frontend",
         href: "/"
       },
+      {
+        text: "All components",
+        href: "/components"
+      },
       {
         text: componentName | unslugify
       }
diff --git a/packages/govuk-frontend-review/src/views/components.njk b/packages/govuk-frontend-review/src/views/components.njk
index 48f263aad9..ea1c6f906c 100644
--- a/packages/govuk-frontend-review/src/views/components.njk
+++ b/packages/govuk-frontend-review/src/views/components.njk
@@ -1,6 +1,6 @@
 {% extends "layouts/full-width-landmarks.njk" %}
 
-{% from "govuk/components/back-link/macro.njk" import govukBackLink %}
+{% from "govuk/components/breadcrumbs/macro.njk" import govukBreadcrumbs %}
 {% from "macros/showExamples.njk" import showExamples %}
 
 {% block pageTitle %}All components - GOV.UK Frontend{% endblock %}
@@ -10,8 +10,16 @@
 {% endset %}
 
 {% block beforeContent %}
-  {{ govukBackLink({
-    href: "/"
+  {{ govukBreadcrumbs({
+    items: [
+      {
+        text: "GOV.UK Frontend",
+        href: "/"
+      },
+      {
+        text: "All components"
+      }
+    ]
   }) }}
 {% endblock %}
 
diff --git a/packages/govuk-frontend-review/src/views/errors/404.njk b/packages/govuk-frontend-review/src/views/errors/404.njk
index c7f3a2c7a8..677678bfa1 100644
--- a/packages/govuk-frontend-review/src/views/errors/404.njk
+++ b/packages/govuk-frontend-review/src/views/errors/404.njk
@@ -1,7 +1,23 @@
 {% extends "layouts/layout.njk" %}
 
+{% from "govuk/components/breadcrumbs/macro.njk" import govukBreadcrumbs %}
+
 {% block pageTitle %}Page not found - GOV.UK Frontend{% endblock %}
 
+{% block beforeContent %}
+  {{ govukBreadcrumbs({
+    items: [
+      {
+        text: "GOV.UK Frontend",
+        href: "/"
+      },
+      {
+        text: "Page not found"
+      }
+    ]
+  }) }}
+{% endblock %}
+
 {% block content %}
   <div class="govuk-grid-row">
     <div class="govuk-grid-column-three-quarters govuk-grid-column-two-thirds-from-desktop">
diff --git a/packages/govuk-frontend-review/src/views/errors/500.njk b/packages/govuk-frontend-review/src/views/errors/500.njk
index f872676087..6bfcd5192d 100644
--- a/packages/govuk-frontend-review/src/views/errors/500.njk
+++ b/packages/govuk-frontend-review/src/views/errors/500.njk
@@ -1,7 +1,23 @@
 {% extends "layouts/layout.njk" %}
 
+{% from "govuk/components/breadcrumbs/macro.njk" import govukBreadcrumbs %}
+
 {% block pageTitle %}Sorry, there is a problem with the service - GOV.UK Frontend{% endblock %}
 
+{% block beforeContent %}
+  {{ govukBreadcrumbs({
+    items: [
+      {
+        text: "GOV.UK Frontend",
+        href: "/"
+      },
+      {
+        text: "Server error"
+      }
+    ]
+  }) }}
+{% endblock %}
+
 {% block content %}
   <div class="govuk-grid-row">
     <div class="govuk-grid-column-three-quarters govuk-grid-column-two-thirds-from-desktop">
diff --git a/packages/govuk-frontend-review/src/views/full-page-examples/index.njk b/packages/govuk-frontend-review/src/views/full-page-examples/index.njk
index b059179e15..86c261ce68 100644
--- a/packages/govuk-frontend-review/src/views/full-page-examples/index.njk
+++ b/packages/govuk-frontend-review/src/views/full-page-examples/index.njk
@@ -1,11 +1,19 @@
 {% extends "layouts/layout.njk" %}
-{% from "govuk/components/back-link/macro.njk" import govukBackLink %}
+{% from "govuk/components/breadcrumbs/macro.njk" import govukBreadcrumbs %}
 
 {% block pageTitle %}Full page examples - GOV.UK Frontend{% endblock %}
 
 {% block beforeContent %}
-  {{ govukBackLink({
-    href: "/"
+  {{ govukBreadcrumbs({
+    items: [
+      {
+        text: "GOV.UK Frontend",
+        href: "/"
+      },
+      {
+        text: "Full page examples"
+      }
+    ]
   }) }}
 {% endblock %}
 

From d5eac385c957acc361f338ab53fb63ab837cd802 Mon Sep 17 00:00:00 2001
From: Colin Rotherham <work@colinr.com>
Date: Tue, 14 Nov 2023 17:17:37 +0000
Subject: [PATCH 4/4] Add JSDoc declarations

---
 packages/govuk-frontend-review/src/app.mjs    | 197 ++++++++++++------
 .../src/routes/full-page-examples.mjs         |   7 +-
 2 files changed, 137 insertions(+), 67 deletions(-)

diff --git a/packages/govuk-frontend-review/src/app.mjs b/packages/govuk-frontend-review/src/app.mjs
index 00f63215c8..a4cf3afea1 100644
--- a/packages/govuk-frontend-review/src/app.mjs
+++ b/packages/govuk-frontend-review/src/app.mjs
@@ -87,52 +87,74 @@ export default async () => {
    *
    * Finds all component fixtures and default example
    */
-  app.param('componentName', (req, res, next, componentName) => {
-    const exampleName = 'default'
-
-    // Find all fixtures for component
-    const componentFixtures = componentsFixtures.find(
-      ({ component }) => component === componentName
-    )
-
-    // Find default fixture for component
-    const componentFixture = componentFixtures?.fixtures.find(
-      ({ name }) => name === exampleName
-    )
-
-    // Add response locals
-    res.locals.componentName = componentName
-    res.locals.componentFixtures = componentFixtures
-    res.locals.componentFixture = componentFixture
-    res.locals.exampleName = 'default'
-
-    next()
-  })
+  app.param(
+    'componentName',
+
+    /**
+     * @param {import('express').Request} req
+     * @param {import('express').Response<{}, Partial<PreviewLocals>>} res
+     * @param {import('express').NextFunction} next
+     * @param {string} componentName
+     */
+    (req, res, next, componentName) => {
+      const exampleName = 'default'
+
+      // Find all fixtures for component
+      const componentFixtures = componentsFixtures.find(
+        ({ component }) => component === componentName
+      )
+
+      // Find default fixture for component
+      const componentFixture = componentFixtures?.fixtures.find(
+        ({ name }) => name === exampleName
+      )
+
+      // Add response locals
+      res.locals.componentName = componentName
+      res.locals.componentFixtures = componentFixtures
+      res.locals.componentFixture = componentFixture
+      res.locals.exampleName = 'default'
+
+      next()
+    }
+  )
 
   /**
    * Handle parameter :exampleName
    *
    * Finds component fixture for example and updates locals
    */
-  app.param('exampleName', (req, res, next, exampleName) => {
-    const { componentFixtures } = res.locals
-
-    // Replace default fixture with named example
-    const componentFixture = componentFixtures?.fixtures.find(
-      ({ name }) => nunjucks.filters.slugify(name) === exampleName
-    )
-
-    // Update response locals
-    res.locals.componentFixture = componentFixture
-    res.locals.exampleName = exampleName
-
-    next()
-  })
+  app.param(
+    'exampleName',
+
+    /**
+     * @param {import('express').Request} req
+     * @param {import('express').Response<{}, Partial<PreviewLocals>>} res
+     * @param {import('express').NextFunction} next
+     * @param {string} exampleName
+     */
+    (req, res, next, exampleName) => {
+      const { componentFixtures } = res.locals
+
+      // Replace default fixture with named example
+      const componentFixture = componentFixtures?.fixtures.find(
+        ({ name }) => nunjucks.filters.slugify(name) === exampleName
+      )
+
+      // Update response locals
+      res.locals.componentFixture = componentFixture
+      res.locals.exampleName = exampleName
+
+      next()
+    }
+  )
 
   // Define routes
 
-  // Index page - render the component list template
-  app.get('/', async function (req, res) {
+  /**
+   * Review app home page
+   */
+  app.get('/', (req, res) => {
     res.render('index', {
       componentNames,
       componentNamesWithJavaScript,
@@ -141,30 +163,53 @@ export default async () => {
     })
   })
 
-  // All components redirect
+  /**
+   * All components redirect
+   */
   app.get('/components/all', function (req, res) {
     res.redirect('./')
   })
 
-  // Component examples
-  app.get('/components/:componentName?', (req, res, next) => {
-    const { componentName } = res.locals
+  /**
+   * Component examples
+   */
+  app.get(
+    '/components/:componentName?',
+
+    /**
+     * @param {import('express').Request} req
+     * @param {import('express').Response<{}, Partial<PreviewLocals>>} res
+     * @param {import('express').NextFunction} next
+     * @returns {void}
+     */
+    (req, res, next) => {
+      const { componentName } = res.locals
+
+      // Unknown component, continue to page not found
+      if (componentName && !componentNames.includes(componentName)) {
+        return next()
+      }
 
-    // Unknown component, continue to page not found
-    if (componentName && !componentNames.includes(componentName)) {
-      return next()
+      res.render(componentName ? 'component' : 'components', {
+        componentsFixtures,
+        componentName
+      })
     }
+  )
 
-    res.render(componentName ? 'component' : 'components', {
-      componentsFixtures,
-      componentName
-    })
-  })
-
-  // Component example preview
+  /**
+   * Component example preview
+   */
   app.get(
     '/components/:componentName/:exampleName?/preview',
-    function (req, res, next) {
+
+    /**
+     * @param {import('express').Request} req
+     * @param {import('express').Response<{}, Partial<PreviewLocals>>} res
+     * @param {import('express').NextFunction} next
+     * @returns {void}
+     */
+    (req, res, next) => {
       const {
         componentName,
         componentFixtures: fixtures,
@@ -203,22 +248,34 @@ export default async () => {
     }
   )
 
-  // Example view
-  app.get('/examples/:exampleName', function (req, res, next) {
-    const { exampleName } = res.locals
-
-    // Unknown example, continue to page not found
-    if (!exampleNames.includes(exampleName)) {
-      return next()
-    }
+  /**
+   * Example view
+   */
+  app.get(
+    '/examples/:exampleName',
+
+    /**
+     * @param {import('express').Request} req
+     * @param {import('express').Response<{}, Partial<PreviewLocals>>} res
+     * @param {import('express').NextFunction} next
+     * @returns {void}
+     */
+    (req, res, next) => {
+      const { exampleName } = res.locals
+
+      // Unknown example, continue to page not found
+      if (!exampleNames.includes(exampleName)) {
+        return next()
+      }
 
-    res.render(`examples/${exampleName}/index`, {
-      exampleName,
+      res.render(`examples/${exampleName}/index`, {
+        exampleName,
 
-      // Render with random number for unique non-visited links
-      randomPageHash: (Math.random() * 1000000).toFixed()
-    })
-  })
+        // Render with random number for unique non-visited links
+        randomPageHash: (Math.random() * 1000000).toFixed()
+      })
+    }
+  )
 
   // Full page example views
   routes.fullPageExamples(app)
@@ -243,6 +300,14 @@ export default async () => {
   return app
 }
 
+/**
+ * @typedef {object} PreviewLocals
+ * @property {import('@govuk-frontend/lib/components').ComponentFixtures} componentFixtures - All Component fixtures
+ * @property {import('@govuk-frontend/lib/components').ComponentFixture} [componentFixture] - Single component fixture
+ * @property {string} componentName - Component name
+ * @property {string} [exampleName] - Example name
+ */
+
 /**
  * @typedef {object} FeatureFlags
  * @property {boolean} isDeployedToHeroku - Review app using `HEROKU_APP`
diff --git a/packages/govuk-frontend-review/src/routes/full-page-examples.mjs b/packages/govuk-frontend-review/src/routes/full-page-examples.mjs
index 088c227e0b..641401dadb 100644
--- a/packages/govuk-frontend-review/src/routes/full-page-examples.mjs
+++ b/packages/govuk-frontend-review/src/routes/full-page-examples.mjs
@@ -23,13 +23,18 @@ export default (app) => {
   routes.whatIsYourPostcode(app)
   routes.whatWasTheLastCountryYouVisited(app)
 
+  /**
+   * Full page examples index
+   */
   app.get('/full-page-examples', async (req, res) => {
     res.render('full-page-examples/index', {
       fullPageExamples
     })
   })
 
-  // Display full page examples index by default if not handled already
+  /**
+   * Full page example
+   */
   app.get('/full-page-examples/:exampleName', (req, res, next) => {
     const { exampleName } = req.params