From 135ba387ce74ccbe7bd0202cc6edda13de9e2275 Mon Sep 17 00:00:00 2001 From: Sam Irons Date: Fri, 16 Oct 2026 10:01:58 +1100 Subject: [PATCH 1/5] Add light and dark Kubernetes diagrams that follow the site theme Replace the Deploy Container and Load Balancer diagrams with Figma exports in light and dark variants. Images marked theme-light-only or theme-dark-only are hidden by the html[data-theme] attribute, so the swap follows the site's theme toggle as well as the OS setting. SVG figures now fill the content column, and the zoom lightbox no longer caps vector images at their exported width. Co-Authored-By: Claude Opus 5.5 --- .../deploy-container/deploy-container.svg | 118 ------------ .../deployments/kubernetes/loadbalancer.svg | 179 ------------------ .../deploy-container-dark.svg | 30 +++ .../deploy-container-light.svg | 30 +++ .../kubernetes/steps/load-balancer-dark.svg | 48 +++++ .../kubernetes/steps/load-balancer-light.svg | 48 +++++ .../kubernetes/steps/kubernetes-resources.md | 6 +- .../kubernetes/steps/kubernetes-service.md | 3 +- src/scripts/modules/figures.js | 8 +- src/styles/main.css | 14 ++ 10 files changed, 182 insertions(+), 302 deletions(-) delete mode 100644 public/docs/deployments/kubernetes/deploy-container/deploy-container.svg delete mode 100644 public/docs/deployments/kubernetes/loadbalancer.svg create mode 100644 public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-dark.svg create mode 100644 public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-light.svg create mode 100644 public/docs/img/kubernetes/steps/load-balancer-dark.svg create mode 100644 public/docs/img/kubernetes/steps/load-balancer-light.svg diff --git a/public/docs/deployments/kubernetes/deploy-container/deploy-container.svg b/public/docs/deployments/kubernetes/deploy-container/deploy-container.svg deleted file mode 100644 index 1e837ab80a..0000000000 --- a/public/docs/deployments/kubernetes/deploy-container/deploy-container.svg +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - Kubernetes Cluster - - - - - - - - - - Deployment - - - - - - - - - - - - - ReplicaSet - - - - - - - - - - - - - Pod - - - - - - Container - - - - - - - Container - - - - - - - - - - - - Ingress - - - - - - */ingress-path - - - - - - - - - - - - - Service - - - - - - 80:TCP - - - - - - - - - - - - - - diff --git a/public/docs/deployments/kubernetes/loadbalancer.svg b/public/docs/deployments/kubernetes/loadbalancer.svg deleted file mode 100644 index dfb6403cf5..0000000000 --- a/public/docs/deployments/kubernetes/loadbalancer.svg +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - - - - - - - - - - - - - - Kubernetes Cluster - - - - - - - - - - - - - Deployment - - - - - - - Pod - - - - - - - Pod - - - - - - - - - - - - - - Deployment - - - - - - - Pod - - - - - - - Pod - - - - - - - - Load Balancer Service - - - - - - - - - - - - - - - - - - - - - - - - - - - Load Balancer - - - - - - - - - - - - - - - - - - - - - - - - - - - - Node - - - - - - - Node Port - - - - - - - - - Node - - - - - - - Node Port - - - - - - - - Internet - - - - - - - - - - - diff --git a/public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-dark.svg b/public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-dark.svg new file mode 100644 index 0000000000..2433334251 --- /dev/null +++ b/public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-dark.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-light.svg b/public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-light.svg new file mode 100644 index 0000000000..152cec6725 --- /dev/null +++ b/public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-light.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/docs/img/kubernetes/steps/load-balancer-dark.svg b/public/docs/img/kubernetes/steps/load-balancer-dark.svg new file mode 100644 index 0000000000..29a3a8da3e --- /dev/null +++ b/public/docs/img/kubernetes/steps/load-balancer-dark.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/docs/img/kubernetes/steps/load-balancer-light.svg b/public/docs/img/kubernetes/steps/load-balancer-light.svg new file mode 100644 index 0000000000..e1d0bbef2b --- /dev/null +++ b/public/docs/img/kubernetes/steps/load-balancer-light.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/docs/kubernetes/steps/kubernetes-resources.md b/src/pages/docs/kubernetes/steps/kubernetes-resources.md index d4a656a82d..290833afed 100644 --- a/src/pages/docs/kubernetes/steps/kubernetes-resources.md +++ b/src/pages/docs/kubernetes/steps/kubernetes-resources.md @@ -22,7 +22,8 @@ The second feature is the `Service`. This feature is used to build a [Service re The third feature is the `Ingress`. This feature is used to build a [Ingress resource](https://oc.to/KubernetesIngressResource). :::figure -![Deploy Container Resources](/docs/deployments/kubernetes/deploy-container/deploy-container.svg) +A Kubernetes cluster where an Ingress path routes traffic to a Service on port 80, which forwards it to a Deployment. The Deployment manages a ReplicaSet that runs a Pod with two containers. +A Kubernetes cluster where an Ingress path routes traffic to a Service on port 80, which forwards it to a Deployment. The Deployment manages a ReplicaSet that runs a Pod with two containers. ::: :::div{.hint} @@ -931,7 +932,8 @@ A Load Balancer Service resource provides the same Cluster IP and Node Ports tha The particular load balancer that is created depends on the environment in which the LoadBalancer Service resource is created. In AWS, an ELB or ALB can be created. Azure or Google Cloud will create their respective load balancers. :::figure -![Load balancer](/docs/deployments/kubernetes/loadbalancer.svg) +Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each of two nodes in the Kubernetes cluster. Each Node Port forwards to the Load Balancer Service, which routes requests to the Pods in two Deployments. +Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each of two nodes in the Kubernetes cluster. Each Node Port forwards to the Load Balancer Service, which routes requests to the Pods in two Deployments. ::: #### Cluster IP address diff --git a/src/pages/docs/kubernetes/steps/kubernetes-service.md b/src/pages/docs/kubernetes/steps/kubernetes-service.md index d5c36f3fa9..cc7b4dee44 100644 --- a/src/pages/docs/kubernetes/steps/kubernetes-service.md +++ b/src/pages/docs/kubernetes/steps/kubernetes-service.md @@ -38,7 +38,8 @@ A LoadBalancer Service resource provides the same Cluster IP and Node Ports that The particular load balancer that is created depends on the environment in which the LoadBalancer Service resource is created. In AWS, an ELB or ALB can be created. Azure or Google Cloud will create their respective load balancers. :::figure -![Load balancer](/docs/deployments/kubernetes/loadbalancer.svg) +Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each of two nodes in the Kubernetes cluster. Each Node Port forwards to the Load Balancer Service, which routes requests to the Pods in two Deployments. +Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each of two nodes in the Kubernetes cluster. Each Node Port forwards to the Load Balancer Service, which routes requests to the Pods in two Deployments. ::: ## Cluster IP address diff --git a/src/scripts/modules/figures.js b/src/scripts/modules/figures.js index 5db4dc0811..c282144d72 100644 --- a/src/scripts/modules/figures.js +++ b/src/scripts/modules/figures.js @@ -12,7 +12,8 @@ function stillFrames() { /** * Where the image lands: its own aspect ratio, centred, never wider than the - * source really is. + * source really is. Vector images have no real source size, so they fill the + * room. * * @param {HTMLImageElement} img */ @@ -24,7 +25,10 @@ function targetRect(img) { }; const ratio = img.naturalWidth / img.naturalHeight; - const native = Number(img.getAttribute('width')) || img.naturalWidth; + const vector = new URL(img.src).pathname.endsWith('.svg'); + const native = vector + ? Infinity + : Number(img.getAttribute('width')) || img.naturalWidth; let width = Math.min(room.width, room.height * ratio, native); let height = width / ratio; diff --git a/src/styles/main.css b/src/styles/main.css index d1cf27c084..09c96776a3 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -247,10 +247,24 @@ figure img { cursor: zoom-in; } +/* SVG diagrams scale without loss, so fill the column rather than sitting at + whatever size the design tool exported them at. */ +figure img[src$='.svg'] { + width: 100%; +} + figure img[data-hidden='true'] { visibility: hidden; } +/* Diagrams exported in a light and a dark variant. Keyed off data-theme rather + than prefers-color-scheme so the swap follows the site's theme toggle, not + just the OS setting. */ +html[data-theme='light'] .theme-dark-only, +html[data-theme='dark'] .theme-light-only { + display: none; +} + .image__caption { color: var(--color-heading); font-size: var(--fontSizeBase); From fe7a109bbf79e28bb8beee787d300c53d8fb0074 Mon Sep 17 00:00:00 2001 From: Sam Irons Date: Fri, 16 Oct 2026 10:46:34 +1100 Subject: [PATCH 2/5] Update Deploy Container and Load Balancer diagrams Replace both diagrams with revised Figma exports, and update the Deploy Container figure's width and height to the new 480x270 size. Co-Authored-By: Claude Opus 5.5 --- .../deploy-container-dark.svg | 58 +++++++++---------- .../deploy-container-light.svg | 58 +++++++++---------- .../kubernetes/steps/load-balancer-dark.svg | 38 ++++++------ .../kubernetes/steps/load-balancer-light.svg | 40 ++++++------- .../kubernetes/steps/kubernetes-resources.md | 4 +- 5 files changed, 99 insertions(+), 99 deletions(-) diff --git a/public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-dark.svg b/public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-dark.svg index 2433334251..06ecd74908 100644 --- a/public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-dark.svg +++ b/public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-dark.svg @@ -1,30 +1,30 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-light.svg b/public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-light.svg index 152cec6725..0ca62ddc1b 100644 --- a/public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-light.svg +++ b/public/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-light.svg @@ -1,30 +1,30 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/docs/img/kubernetes/steps/load-balancer-dark.svg b/public/docs/img/kubernetes/steps/load-balancer-dark.svg index 29a3a8da3e..a6678cdbb9 100644 --- a/public/docs/img/kubernetes/steps/load-balancer-dark.svg +++ b/public/docs/img/kubernetes/steps/load-balancer-dark.svg @@ -1,41 +1,41 @@ - - - - + + + + - + - + - - + + - + - + - - - + + + - - + + - - + + - - + + diff --git a/public/docs/img/kubernetes/steps/load-balancer-light.svg b/public/docs/img/kubernetes/steps/load-balancer-light.svg index e1d0bbef2b..4535a8df5f 100644 --- a/public/docs/img/kubernetes/steps/load-balancer-light.svg +++ b/public/docs/img/kubernetes/steps/load-balancer-light.svg @@ -1,41 +1,41 @@ - - - - + + + + - + - + - - + + - + - + - - - - + + + + - - + + - - + + - - + + diff --git a/src/pages/docs/kubernetes/steps/kubernetes-resources.md b/src/pages/docs/kubernetes/steps/kubernetes-resources.md index 290833afed..207c6252c8 100644 --- a/src/pages/docs/kubernetes/steps/kubernetes-resources.md +++ b/src/pages/docs/kubernetes/steps/kubernetes-resources.md @@ -22,8 +22,8 @@ The second feature is the `Service`. This feature is used to build a [Service re The third feature is the `Ingress`. This feature is used to build a [Ingress resource](https://oc.to/KubernetesIngressResource). :::figure -A Kubernetes cluster where an Ingress path routes traffic to a Service on port 80, which forwards it to a Deployment. The Deployment manages a ReplicaSet that runs a Pod with two containers. -A Kubernetes cluster where an Ingress path routes traffic to a Service on port 80, which forwards it to a Deployment. The Deployment manages a ReplicaSet that runs a Pod with two containers. +A Kubernetes cluster where an Ingress path routes traffic to a Service on port 80, which forwards it to a Deployment. The Deployment manages a ReplicaSet that runs a Pod with two containers. +A Kubernetes cluster where an Ingress path routes traffic to a Service on port 80, which forwards it to a Deployment. The Deployment manages a ReplicaSet that runs a Pod with two containers. ::: :::div{.hint} From fec5a644aeb75f98778cfa59126dfe2917f8919c Mon Sep 17 00:00:00 2001 From: Sam Irons Date: Fri, 16 Oct 2026 10:46:34 +1100 Subject: [PATCH 3/5] Document when to add screenshots and how to theme images Add guidance to the README on deciding whether a page needs a screenshot, sizing PNGs and SVGs for the content column, creating clear images, and adding light and dark variants with the theme-light-only and theme-dark-only classes. Co-Authored-By: Claude Opus 5.5 --- README.md | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 00513e106b..e4d479a06c 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,39 @@ Page structure, frontmatter, and where version-specific notes belong are governe ### Product screenshots -Product screenshots used in Docs should reflect the UI in the latest version of Octopus. The `figure` component will automatically add a curved border and outline to your image: +Product screenshots used in Docs should reflect the UI in the latest version of Octopus. + +#### Decide whether a page needs a screenshot + +Every screenshot has to be recaptured when the UI it shows changes, and a light and dark pair doubles that work. A screenshot that no longer matches the product misleads readers more than no screenshot at all. So write the text first, make sure the page works without images, and then add a screenshot only when it does one of these jobs: + +- **Help readers find something.** The control is small or easy to miss, it's hidden behind a menu, tab, or expander, or it sits among similar options that are easy to confuse. +- **Confirm readers are on track.** In a long or complex procedure, a screenshot at a checkpoint lets readers check the UI looks right before they continue. This includes the point where a procedure moves to a different area of the UI. Add one per stage of the procedure, not one per step. +- **Introduce an unfamiliar part of Octopus.** On an overview or getting-started page, a screenshot of the area the page covers helps readers recognize it when they get there. Use one per page, near the top. +- **Show something that's visual by nature.** On a dashboard, a deployment timeline, or a status view, the layout is the information. + +Don't add a screenshot: + +- For a step the text already makes clear, such as selecting a checkbox, entering a name, or selecting **Save**. +- To show navigation. Write the path in the step instead, such as **Infrastructure > Deployment Targets**. +- For code, commands, YAML, logs, or other output. Use a code block, which readers can copy and search. +- Of a part of the UI that's being redesigned or changes often. Describe it in text until it settles. +- To break up text or decorate the page. + +If a concept is about how parts relate rather than what a screen looks like, use a diagram instead. A diagram stays accurate when the UI changes. + +To keep the screenshots you add maintainable: + +- Crop to the part of the UI the step is about. A tight crop stays accurate when other parts of the screen change. +- Put every value and selection in the step text. Readers using a screen reader, and readers looking at an out-of-date screenshot, can still complete the task. +- Capture a light and dark pair only on high-traffic pages, such as getting-started pages, the overview page of a main section, and the most-used procedures. On other pages, use a single light variant. See [Add an image with a single variant](#add-an-image-with-a-single-variant). +- When you update a page, check its screenshots still match the product. If one doesn't and you can't recapture it, remove it rather than leave it on the page. + +For where a screenshot goes in a procedure, see [Inline screenshots in steps](contributing/standards/task-topic.md#inline-screenshots-in-steps). + +#### Add a screenshot to a page + +The `figure` component will automatically add a curved border and outline to your image: ```markdown :::figure @@ -280,3 +312,73 @@ Product screenshots used in Docs should reflect the UI in the latest version of ``` Images should be uploaded to the folder that relates to the position of the page in the Docs hierarchy. In the example above, where the image is destined for the Octopus Cloud overview page, the image has been uploaded to the Docs > Octopus Cloud > Images folder. If a folder has not been created for the page you are uploading an image to, create a new folder or use the Docs > Images folder as a backup. + +### Images and theming + +Use PNG for screenshots and SVG for diagrams, where possible. + +#### Size images for the content column + +The content column is up to 800px wide on desktop, where the side navigation and the "On this page" list sit either side of it. On tablets, where the page drops to a single column, it's up to 1056px wide. On phones it's narrower. + +A PNG shows at its pixel width, shrunk to fit the column. The site never enlarges a PNG and doesn't detect high-density images, so a screenshot is only sharp on a high-density display when it has at least twice as many pixels as the space it fills. + +To capture a sharp screenshot: + +1. Capture at 2x (144 dpi). On a Mac with a Retina display, the built-in screenshot tool does this by default. On Windows, set display scaling to 200% before you capture, or use your capture tool's 2x setting. +2. Crop the capture. Don't resize it in an image editor, because resampling blurs text. +3. Choose how to add the image to the page: + - For a screenshot that should fill the column, crop it to at least 1600px wide and add it with Markdown image syntax. + - For a smaller screenshot, such as a dialog or a single field, add it with an HTML `` tag and set `width` and `height` to half its pixel size. A 900 x 600 pixel capture gets `width="450" height="300"`. This shows the image at the size the reader sees in Octopus. Without it, a 2x capture shows at twice that size. +4. Compress the PNG with a tool such as [ImageOptim](https://imageoptim.com/) or [TinyPNG](https://tinypng.com/). Keep each file under 250 KB where you can. + +An SVG stretches to fill the column and stays sharp at any size, including when a reader zooms into it. Design diagrams in a frame 800px wide, the widest the column gets on desktop, so text shows at the size you set it. Use a font size of at least 14px. Body text on the site is 16px. + +When you export an SVG from Figma, leave **Outline text** selected. Browsers don't load fonts into an SVG shown with an `` tag, so live text would fall back to a different font. + +#### Create clear images + +- Crop to the part of the UI the step is about, with enough of the surrounding UI for readers to find it in Octopus. +- Capture the browser at 100% zoom. +- Leave the cursor out, and don't show hover or focus states unless the step is about them. +- Use realistic sample data. Don't show customer names, real email addresses, API keys, or other sensitive values. +- Capture the latest version of the Octopus UI. + +#### Make images follow the theme + +The docs site has a light and a dark theme. An image changes with the theme when you give it a light and a dark variant. This works the same way for PNG screenshots and SVG diagrams. + +To make an image follow the theme: + +1. Create a light and a dark variant of the image: + - For a screenshot, capture the same view with Octopus in its light theme and again in its dark theme. + - For a diagram, export each variant with a solid background in the page color for that theme: `#FFFFFF` for light and `#111A23` for dark. These are the `colorBackgroundPrimaryDefault` design token in each theme, so the diagram blends into the page. +2. Name the files with a `-light` and `-dark` suffix, such as `load-balancer-light.svg` and `load-balancer-dark.svg`. +3. Add both images to the `figure` with HTML `` tags, and give each one a theme class: + + ```markdown + :::figure + Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each node in the cluster + Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each node in the cluster + ::: + ``` + +4. Preview the page in both themes with the theme toggle in the top nav, and open the zoom view on the image. + +The site hides `.theme-dark-only` in the light theme and `.theme-light-only` in the dark theme. The classes follow the site's theme toggle, so don't use `` with a `prefers-color-scheme` media query instead. That only follows the reader's operating system setting, and the zoom view always shows its fallback image. + +A few things to keep in mind: + +- Give both variants the same alt text, so readers who can't see the image get the same description in either theme. +- Set `width` and `height` on both tags. For an SVG, use its exported size; it still scales to fill the column. For a 2x PNG, use half its pixel size, as described in [Size images for the content column](#size-images-for-the-content-column). +- When more than one page uses an image, store it in the nearest shared folder, such as `/docs/img/kubernetes/steps/`, and update every page that references it. + +#### Add an image with a single variant + +When you don't have a variant for each theme, use the light theme: + +- For a screenshot, capture Octopus in its light theme. +- For a diagram, export it with a solid `#FFFFFF` background. +- Add the image with Markdown image syntax, with no theme class. + +Don't export an image with a transparent background. The site puts every image on the page color, so the dark text and lines in a transparent image become hard to read in dark mode. A light image with a solid background shows as a light panel inside the figure border in the dark theme. From 1cc4568a2fabb584d92ee569c09b7395ab369fd9 Mon Sep 17 00:00:00 2001 From: Sam Irons Date: Mon, 19 Oct 2026 10:28:04 +1100 Subject: [PATCH 4/5] Skip SVG files in spellcheck SVG files are markup, not prose, so cspell flags attribute names such as stroke-dasharray. Diagram labels exported from Figma are outlined paths, so there's no text in them to check. Co-Authored-By: Claude Opus 5.5 --- cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell.json b/cspell.json index 02f597d725..1bd08eee1b 100644 --- a/cspell.json +++ b/cspell.json @@ -69,6 +69,7 @@ "tests/**", "node_modules/**", "public/docs/css/fa/**", + "**/*.svg", "assets/diagrams/**" ] } From f04e1635775d41938e18452a31fd9cdae4b69711 Mon Sep 17 00:00:00 2001 From: Sam Irons Date: Thu, 22 Oct 2026 10:05:20 +1100 Subject: [PATCH 5/5] Use the :img directive for themed diagrams The :img directive rewrote every image to a /docs/i/ path, but img.mjs only generates variants for raster images, so an SVG pointed at a file that never existed. It also dropped resp-img whenever a class was set. SVGs now keep their own path and take their width and height from the file, and an @2x image shows at half its pixel size. The Kubernetes diagrams and the README guidance use the directive instead of raw tags, so sizes no longer have to be set by hand. Co-Authored-By: Claude Opus 5.5 --- README.md | 20 ++++-- .../kubernetes/steps/kubernetes-resources.md | 8 +-- .../kubernetes/steps/kubernetes-service.md | 4 +- .../octopus/utilities/custom-markdown.mjs | 62 ++++++++++++++++++- 4 files changed, 79 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e4d479a06c..a3d7646546 100644 --- a/README.md +++ b/README.md @@ -321,7 +321,7 @@ Use PNG for screenshots and SVG for diagrams, where possible. The content column is up to 800px wide on desktop, where the side navigation and the "On this page" list sit either side of it. On tablets, where the page drops to a single column, it's up to 1056px wide. On phones it's narrower. -A PNG shows at its pixel width, shrunk to fit the column. The site never enlarges a PNG and doesn't detect high-density images, so a screenshot is only sharp on a high-density display when it has at least twice as many pixels as the space it fills. +A PNG shows at its pixel width, shrunk to fit the column. The site never enlarges a PNG, so a screenshot is only sharp on a high-density display when it has at least twice as many pixels as the space it fills. To capture a sharp screenshot: @@ -329,7 +329,15 @@ To capture a sharp screenshot: 2. Crop the capture. Don't resize it in an image editor, because resampling blurs text. 3. Choose how to add the image to the page: - For a screenshot that should fill the column, crop it to at least 1600px wide and add it with Markdown image syntax. - - For a smaller screenshot, such as a dialog or a single field, add it with an HTML `` tag and set `width` and `height` to half its pixel size. A 900 x 600 pixel capture gets `width="450" height="300"`. This shows the image at the size the reader sees in Octopus. Without it, a 2x capture shows at twice that size. + - For a smaller screenshot, such as a dialog or a single field, add `@2x` to the end of the file name, such as `deployment-target-dialog@2x.png`, and add it with the `:img` directive. The directive shows an `@2x` image at half its pixel size, so a 900 x 600 pixel capture shows at 450 x 300, the size the reader sees in Octopus. Without the suffix, a 2x capture shows at twice that size. + + ```markdown + :::figure + :img{src="/docs/img/infrastructure/deployment-target-dialog@2x.png" alt="The Add deployment target dialog"} + ::: + ``` + + The directive serves resized copies of a PNG from `public/docs/i/`. `npm run dev` generates them, so commit them with the image. 4. Compress the PNG with a tool such as [ImageOptim](https://imageoptim.com/) or [TinyPNG](https://tinypng.com/). Keep each file under 250 KB where you can. An SVG stretches to fill the column and stays sharp at any size, including when a reader zooms into it. Design diagrams in a frame 800px wide, the widest the column gets on desktop, so text shows at the size you set it. Use a font size of at least 14px. Body text on the site is 16px. @@ -354,12 +362,12 @@ To make an image follow the theme: - For a screenshot, capture the same view with Octopus in its light theme and again in its dark theme. - For a diagram, export each variant with a solid background in the page color for that theme: `#FFFFFF` for light and `#111A23` for dark. These are the `colorBackgroundPrimaryDefault` design token in each theme, so the diagram blends into the page. 2. Name the files with a `-light` and `-dark` suffix, such as `load-balancer-light.svg` and `load-balancer-dark.svg`. -3. Add both images to the `figure` with HTML `` tags, and give each one a theme class: +3. Add both images to the `figure` with the `:img` directive, and give each one a theme class: ```markdown :::figure - Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each node in the cluster - Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each node in the cluster + :img{.theme-light-only src="/docs/img/kubernetes/steps/load-balancer-light.svg" alt="Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each node in the cluster"} + :img{.theme-dark-only src="/docs/img/kubernetes/steps/load-balancer-dark.svg" alt="Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each node in the cluster"} ::: ``` @@ -370,7 +378,7 @@ The site hides `.theme-dark-only` in the light theme and `.theme-light-only` in A few things to keep in mind: - Give both variants the same alt text, so readers who can't see the image get the same description in either theme. -- Set `width` and `height` on both tags. For an SVG, use its exported size; it still scales to fill the column. For a 2x PNG, use half its pixel size, as described in [Size images for the content column](#size-images-for-the-content-column). +- Don't set `width` and `height`. The directive reads them from the image, and halves them for an `@2x` file. Put `@2x` after the theme suffix, such as `dashboard-light@2x.png`. - When more than one page uses an image, store it in the nearest shared folder, such as `/docs/img/kubernetes/steps/`, and update every page that references it. #### Add an image with a single variant diff --git a/src/pages/docs/kubernetes/steps/kubernetes-resources.md b/src/pages/docs/kubernetes/steps/kubernetes-resources.md index 207c6252c8..921961acd9 100644 --- a/src/pages/docs/kubernetes/steps/kubernetes-resources.md +++ b/src/pages/docs/kubernetes/steps/kubernetes-resources.md @@ -22,8 +22,8 @@ The second feature is the `Service`. This feature is used to build a [Service re The third feature is the `Ingress`. This feature is used to build a [Ingress resource](https://oc.to/KubernetesIngressResource). :::figure -A Kubernetes cluster where an Ingress path routes traffic to a Service on port 80, which forwards it to a Deployment. The Deployment manages a ReplicaSet that runs a Pod with two containers. -A Kubernetes cluster where an Ingress path routes traffic to a Service on port 80, which forwards it to a Deployment. The Deployment manages a ReplicaSet that runs a Pod with two containers. +:img{.theme-light-only src="/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-light.svg" alt="A Kubernetes cluster where an Ingress path routes traffic to a Service on port 80, which forwards it to a Deployment. The Deployment manages a ReplicaSet that runs a Pod with two containers."} +:img{.theme-dark-only src="/docs/img/kubernetes/steps/kubernetes-resources/deploy-container-dark.svg" alt="A Kubernetes cluster where an Ingress path routes traffic to a Service on port 80, which forwards it to a Deployment. The Deployment manages a ReplicaSet that runs a Pod with two containers."} ::: :::div{.hint} @@ -932,8 +932,8 @@ A Load Balancer Service resource provides the same Cluster IP and Node Ports tha The particular load balancer that is created depends on the environment in which the LoadBalancer Service resource is created. In AWS, an ELB or ALB can be created. Azure or Google Cloud will create their respective load balancers. :::figure -Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each of two nodes in the Kubernetes cluster. Each Node Port forwards to the Load Balancer Service, which routes requests to the Pods in two Deployments. -Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each of two nodes in the Kubernetes cluster. Each Node Port forwards to the Load Balancer Service, which routes requests to the Pods in two Deployments. +:img{.theme-light-only src="/docs/img/kubernetes/steps/load-balancer-light.svg" alt="Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each of two nodes in the Kubernetes cluster. Each Node Port forwards to the Load Balancer Service, which routes requests to the Pods in two Deployments."} +:img{.theme-dark-only src="/docs/img/kubernetes/steps/load-balancer-dark.svg" alt="Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each of two nodes in the Kubernetes cluster. Each Node Port forwards to the Load Balancer Service, which routes requests to the Pods in two Deployments."} ::: #### Cluster IP address diff --git a/src/pages/docs/kubernetes/steps/kubernetes-service.md b/src/pages/docs/kubernetes/steps/kubernetes-service.md index cc7b4dee44..852b54af7e 100644 --- a/src/pages/docs/kubernetes/steps/kubernetes-service.md +++ b/src/pages/docs/kubernetes/steps/kubernetes-service.md @@ -38,8 +38,8 @@ A LoadBalancer Service resource provides the same Cluster IP and Node Ports that The particular load balancer that is created depends on the environment in which the LoadBalancer Service resource is created. In AWS, an ELB or ALB can be created. Azure or Google Cloud will create their respective load balancers. :::figure -Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each of two nodes in the Kubernetes cluster. Each Node Port forwards to the Load Balancer Service, which routes requests to the Pods in two Deployments. -Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each of two nodes in the Kubernetes cluster. Each Node Port forwards to the Load Balancer Service, which routes requests to the Pods in two Deployments. +:img{.theme-light-only src="/docs/img/kubernetes/steps/load-balancer-light.svg" alt="Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each of two nodes in the Kubernetes cluster. Each Node Port forwards to the Load Balancer Service, which routes requests to the Pods in two Deployments."} +:img{.theme-dark-only src="/docs/img/kubernetes/steps/load-balancer-dark.svg" alt="Traffic from the internet reaches a cloud load balancer, which sends it to a Node Port on each of two nodes in the Kubernetes cluster. Each Node Port forwards to the Load Balancer Service, which routes requests to the Pods in two Deployments."} ::: ## Cluster IP address diff --git a/src/themes/octopus/utilities/custom-markdown.mjs b/src/themes/octopus/utilities/custom-markdown.mjs index bc8303cadc..853065d0bd 100644 --- a/src/themes/octopus/utilities/custom-markdown.mjs +++ b/src/themes/octopus/utilities/custom-markdown.mjs @@ -40,8 +40,60 @@ export function getDestination(uri, s) { return uri.replace(fromRegEx, replacement); } +/** + * The exported size of an SVG, read off its root element. Prefers the width and + * height attributes and falls back to the viewBox. Sizes in units other than px + * are ignored, since they don't map onto CSS pixels. + */ +export function getSvgSize(src) { + try { + const file = path.join(workingDirectory, 'public', src); + const root = fs.readFileSync(file, 'utf8').match(/]*>/)?.[0]; + + if (!root) { + return null; + } + + const attribute = (name) => + root.match(new RegExp(`\\s${name}\\s*=\\s*["']([^"']*)["']`))?.[1]; + const pixels = (value) => { + const match = value?.trim().match(/^(\d+(?:\.\d+)?)(px)?$/); + return match ? Math.round(Number(match[1])) : null; + }; + + const width = pixels(attribute('width')); + const height = pixels(attribute('height')); + + if (width && height) { + return { width, height }; + } + + const viewBox = attribute('viewBox')?.trim().split(/[\s,]+/).map(Number); + + if (viewBox?.length === 4 && viewBox[2] > 0 && viewBox[3] > 0) { + return { width: Math.round(viewBox[2]), height: Math.round(viewBox[3]) }; + } + } catch (e) { + console.warn(e); + } + + return null; +} + export function getImageInfo(src, className, sizes) { const info = {}; + const classes = ((className ?? '') + ' resp-img').trim(); + + // img.mjs only generates variants for raster images, so an SVG is served from + // where it is. It scales without loss, so it needs no srcset either. + if (path.extname(src).toLowerCase() === '.svg') { + info.src = src; + info.srcset = null; + info.sizes = null; + info.class = classes; + info.metadata = getSvgSize(src); + return info; + } let uri = src; uri = uri.replace(/.jpg|.jpeg|.png/, '.webp'); @@ -80,7 +132,7 @@ export function getImageInfo(src, className, sizes) { info.srcset = srcset; info.sizes = sizes; - info.class = (className ?? '' + ' resp-img').trim(); + info.class = classes; if ([imgSmall, imgMedium, imgLarge].includes(src)) { info.srcset = null; @@ -112,8 +164,12 @@ function directiveToHtml(node, ctx) { properties.class = info.class; if (info.metadata) { - properties.width = info.metadata.width; - properties.height = info.metadata.height; + // An `@2x` capture has twice the pixels of the space it fills, so it's + // shown at half its pixel size, the size the reader sees in Octopus + const scale = /@2x\.[a-z]+$/i.test(node.attributes.src) ? 2 : 1; + + properties.width = Math.round(info.metadata.width / scale); + properties.height = Math.round(info.metadata.height / scale); } }