Merge pull request #23960 from dvdksn/improve-markdown-rendering

improve markdown rendering
This commit is contained in:
David Karlsson
2026-01-13 16:54:52 +01:00
committed by GitHub
26 changed files with 370 additions and 82 deletions

View File

@@ -51,6 +51,7 @@ ARG DOCS_URL="https://docs.docker.com"
ENV HUGO_CACHEDIR="/tmp/hugo_cache"
RUN --mount=type=cache,target=/tmp/hugo_cache \
hugo --gc --minify -e $HUGO_ENV -b $DOCS_URL
RUN ./hack/flatten-markdown.sh public
# lint lints markdown files
FROM ghcr.io/igorshubovych/markdownlint-cli:v0.45.0 AS lint

22
hack/flatten-markdown.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/sh
set -e
# Flatten markdown files from public/path/to/page/index.md to public/path/to/page.md
# This makes markdown output links work correctly
PUBLIC_DIR="${1:-public}"
[ -d "$PUBLIC_DIR" ] || { echo "Error: Directory $PUBLIC_DIR does not exist"; exit 1; }
find "$PUBLIC_DIR" -type f -name 'index.md' | while read -r file; do
# Skip the root index.md
[ "$file" = "$PUBLIC_DIR/index.md" ] && continue
# Get the directory containing index.md
dir="${file%/*}"
# Move index.md to parent directory with directory name
mv "$file" "${dir%/*}/${dir##*/}.md"
done
echo "Flattened markdown files in $PUBLIC_DIR"

View File

@@ -52,20 +52,31 @@ exports.handler = (event, context, callback) => {
return
}
// Handle directory requests by appending index.html for requests without file extensions
// Check Accept header for markdown/text requests
const headers = request.headers;
const acceptHeader = headers.accept ? headers.accept[0].value : '';
const wantsMarkdown = acceptHeader.includes('text/markdown') ||
acceptHeader.includes('text/plain');
// Handle directory requests by appending index.html or index.md for requests without file extensions
let uri = request.uri;
// Check if the URI has a dot after the last slash (indicating a filename)
// This is more accurate than just checking the end of the URI
const hasFileExtension = /\.[^/]*$/.test(uri.split('/').pop());
// If it's not a file, treat it as a directory and append index.html
// If it's not a file, treat it as a directory
if (!hasFileExtension) {
// Ensure the URI ends with a slash before appending index.html
// Ensure the URI ends with a slash before appending index file
if (!uri.endsWith("/")) {
uri += "/";
}
uri += "index.html";
// Serve markdown if Accept header requests it, otherwise serve HTML
uri += wantsMarkdown ? "index.md" : "index.html";
request.uri = uri;
} else if (wantsMarkdown && uri.endsWith('.html')) {
// If requesting a specific HTML file but wants markdown, try the .md version
uri = uri.replace(/\.html$/, '.md');
request.uri = uri;
}

View File

@@ -92,6 +92,10 @@ outputs:
section:
- html
- markdown
taxonomy:
- html
term:
- html
languages:
en:

View File

@@ -0,0 +1,41 @@
---
title: {{ .Title }}
url: {{ .Permalink }}
{{- range .Ancestors }}
{{- if and (not .IsHome) .Permalink }}
parent:
title: {{ .Title }}
url: {{ .Permalink }}
{{- break }}
{{- end }}
{{- end }}
{{- if .Ancestors }}
breadcrumbs:
{{- range .Ancestors.Reverse }}
{{- if and (not .IsHome) .Permalink }}
- title: {{ .Title }}
url: {{ .Permalink }}
{{- end }}
{{- end }}
- title: {{ .Title }}
url: {{ .Permalink }}
{{- end }}
{{- with .NextInSection }}
next:
title: {{ .Title }}
url: {{ .Permalink }}
{{- end }}
{{- with .PrevInSection }}
prev:
title: {{ .Title }}
url: {{ .Permalink }}
{{- end }}
{{- $specURL := urls.Parse (printf "/%s%s.yaml" .File.Dir .File.ContentBaseName) }}
openapi_spec: {{ $specURL.String | absURL }}
---
{{ .Content }}
**OpenAPI Specification:** [{{ .Title }} API Spec]({{ $specURL.String | absURL }})
This page provides interactive API documentation. For the machine-readable OpenAPI specification, see the link above.

View File

@@ -10,7 +10,12 @@
<div class="flex w-full">
<article class="prose min-w-0 flex-[2_2_0%] max-w-4xl dark:prose-invert">
{{ partial "breadcrumbs.html" . }}
<h1>{{ .Title }}</h1>
<div class="flex items-start justify-between gap-4">
<h1>{{ .Title }}</h1>
<div class="md-dropdown ml-auto mr-4 hidden lg:block">
{{ partial "md-dropdown.html" . }}
</div>
</div>
<div class="overflow-x-auto">
<table>
<tbody>

View File

@@ -0,0 +1,88 @@
{{- $data := "" }}
{{- if .Params.datafolder }}
{{- $data = index (index site.Data .Params.datafolder) .Params.datafile }}
{{- else }}
{{- $data = index site.Data .Params.datafile }}
{{- end -}}
---
title: {{ .Title }}
url: {{ .Permalink }}
{{- range .Ancestors }}
{{- if and (not .IsHome) .Permalink }}
parent:
title: {{ .Title }}
url: {{ .Permalink }}
{{- break }}
{{- end }}
{{- end }}
{{- if .Ancestors }}
breadcrumbs:
{{- range .Ancestors.Reverse }}
{{- if and (not .IsHome) .Permalink }}
- title: {{ .Title }}
url: {{ .Permalink }}
{{- end }}
{{- end }}
- title: {{ .Title }}
url: {{ .Permalink }}
{{- end }}
{{- with .NextInSection }}
next:
title: {{ .Title }}
url: {{ .Permalink }}
{{- end }}
{{- with .PrevInSection }}
prev:
title: {{ .Title }}
url: {{ .Permalink }}
{{- end }}
---
{{ with $data.short }}**Description:** {{ . }}{{ end }}
{{ with $data.usage }}**Usage:** `{{ . }}`{{ end }}
{{ with $data.aliases }}{{ $aliases := strings.Replace . (printf "%s, " $.Title) "" }}**Aliases:** {{ range $i, $alias := (strings.Split $aliases ", ") }}{{ if $i }}, {{ end }}`{{ $alias }}`{{ end }}{{ end }}
{{ .Content }}
{{ if $data.deprecated }}> [!WARNING]
> **Deprecated**
>
> This command is deprecated. It may be removed in a future Docker version.
{{ end }}
{{ if or $data.experimental $data.experimentalcli }}> [!NOTE]
> **Experimental**
>
> This command is experimental. Experimental features are intended for testing and feedback as their functionality or design may change between releases without warning or can be removed entirely in a future release.
{{ end }}
{{ with $data.kubernetes }}**Orchestrator:** Kubernetes{{ end }}
{{ with $data.swarm }}**Orchestrator:** Swarm{{ end }}
{{ with $data.long }}## Description
{{ . }}
{{ end }}
{{ with $data.options }}{{ $opts := where . "hidden" false }}{{ with $opts }}## Options
| Option | Default | Description |
|--------|---------|-------------|
{{ range . }}{{ $short := .shorthand }}{{ $long := .option }}| {{ with $short }}`-{{ . }}`, {{ end }}`--{{ $long }}` | {{ with .default_value }}{{ $skipDefault := `[],map[],false,0,0s,default,'',""` }}{{ cond (in $skipDefault .) "" (printf "`%s`" .) }}{{ end }} | {{ with .min_api_version }}API {{ . }}+{{ end }}{{ with .deprecated }} **Deprecated**{{ end }}{{ with .experimental }} **experimental (daemon)**{{ end }}{{ with .experimentalcli }} **experimental (CLI)**{{ end }}{{ with .kubernetes }} **Kubernetes**{{ end }}{{ with .swarm }} **Swarm**{{ end }}{{ if .description }} {{ strings.Replace .description "\n" "<br>" }}{{ end }} |
{{ end }}
{{ end }}{{ end }}
{{ with $data.examples }}## Examples
{{ . }}
{{ end }}
{{ if eq .Kind "section" }}## Subcommands
| Command | Description |
|---------|-------------|
{{ range .Pages }}{{ if and .Params.datafolder .Params.datafile }}{{ $subdata := index (index site.Data .Params.datafolder) .Params.datafile }}| [`{{ .Title }}`]({{ .Permalink }}) | {{ $subdata.short }} |
{{ end }}{{ end }}
{{ end }}

View File

@@ -1,5 +1,36 @@
# {{ .Title }}
---
title: {{ .Title }}
url: {{ .Permalink }}
{{- range .Ancestors }}
{{- if and (not .IsHome) .Permalink }}
parent:
title: {{ .Title }}
url: {{ .Permalink }}
{{- break }}
{{- end }}
{{- end }}
{{- if .Ancestors }}
breadcrumbs:
{{- range .Ancestors.Reverse }}
{{- if and (not .IsHome) .Permalink }}
- title: {{ .Title }}
url: {{ .Permalink }}
{{- end }}
{{- end }}
- title: {{ .Title }}
url: {{ .Permalink }}
{{- end }}
{{- $children := where .Pages "Permalink" "ne" "" }}
{{- if $children }}
children:
{{- range $children }}
- title: {{ .Title }}
url: {{ .Permalink }}
{{- with .Description }}
description: {{ . }}
{{- end }}
{{- end }}
{{- end }}
---
{{ .RenderShortcodes }}
{{ range where .Pages "Permalink" "ne" "" }}
- [{{ .Title }}]({{ .Permalink }})
{{ end }}

View File

@@ -1,2 +1,35 @@
# {{ .Title }}
---
title: {{ .Title }}
url: {{ .Permalink }}
{{- range .Ancestors }}
{{- if and (not .IsHome) .Permalink }}
parent:
title: {{ .Title }}
url: {{ .Permalink }}
{{- break }}
{{- end }}
{{- end }}
{{- if .Ancestors }}
breadcrumbs:
{{- range .Ancestors.Reverse }}
{{- if and (not .IsHome) .Permalink }}
- title: {{ .Title }}
url: {{ .Permalink }}
{{- end }}
{{- end }}
- title: {{ .Title }}
url: {{ .Permalink }}
{{- end }}
{{- with .NextInSection }}
next:
title: {{ .Title }}
url: {{ .Permalink }}
{{- end }}
{{- with .PrevInSection }}
prev:
title: {{ .Title }}
url: {{ .Permalink }}
{{- end }}
---
{{ .RenderShortcodes }}

View File

@@ -1,6 +1,9 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{{ partial "meta.html" . }}
{{- range .AlternativeOutputFormats -}}
<link rel="{{ .Rel }}" type="{{ .MediaType.Type }}" href="{{ .Permalink }}" />
{{ end -}}
{{ partial "utils/css.html" "-" }}
{{- if hugo.IsProduction -}}
<script

View File

@@ -1,84 +1,80 @@
<details id="markdownDropdown" class="ml-3 group relative z-10 inline-block" data-heap-id="markdown-dropdown">
<summary
class="dropdown-base hover:bg-gray-50 dark:hover:bg-gray-900 inline-flex cursor-pointer items-center gap-0 py-1 pl-2 text-sm font-semibold transition-colors"
data-heap-id="markdown-dropdown-toggle"
<div class="ml-3 relative inline-block" x-data="{ open: false }" @click.outside="open = false">
<div class="flex shadow rounded-sm overflow-hidden border border-gray-200 dark:border-gray-700">
<!-- Primary copy button -->
<button
onclick="copyMarkdown()"
data-heap-id="copy-markdown-button"
class="bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 flex cursor-pointer items-center gap-2 px-3 py-2 text-sm transition-colors whitespace-nowrap text-gray-900 dark:text-white"
>
<span class="font-normal">Page options</span>
<span class="icon-svg transition-transform group-open:rotate-180">
{{ partialCached "icon" "arrow_drop_down" "arrow_drop_down" }}
<span class="icon-svg icon-sm">
{{ partial "icon" "content_copy" }}
</span>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</summary>
<span class="icon-svg icon-sm hidden">
{{ partial "icon" "check_circle" }}
</span>
<span>Copy as Markdown</span>
</button>
<!-- Dropdown menu -->
<div
class="dropdown-base absolute right-0 z-50 mt-1 w-65 origin-top-right p-2 text-sm shadow-md [display:none] group-open:[display:block]"
data-heap-id="markdown-dropdown-menu"
<!-- Dropdown toggle -->
<button
@click="open = !open"
type="button"
data-heap-id="markdown-dropdown-toggle"
class="bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center justify-center px-2 border-l border-gray-200 dark:border-gray-700 transition-colors text-gray-900 dark:text-white"
aria-label="More options"
>
<button
onclick="copyMarkdown()"
data-heap-id="copy-markdown-button"
class="sub-button"
>
<span class="icon-svg mt-[2px] text-base leading-none">
{{ partial "icon" "content_copy" }}
</span>
<span class="icon-svg hidden mt-[2px] text-base leading-none">
{{ partial "icon" "check_circle" }}
</span>
<div class="leading-tight">
<div class="text-base">Copy page as Markdown for LLMs</div>
</div>
</button>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
</div>
<button
onclick="viewPlainText()"
data-heap-id="view-markdown-button"
class="sub-button"
>
<span class="icon-svg mt-[2px] text-base leading-none">
{{ partial "icon" "description" }}
</span>
<div class="leading-tight">
<div class="text-base">View page as plain text</div>
</div>
</button>
<!-- Dropdown menu -->
<div
x-show="open"
x-collapse
x-cloak
class="absolute right-0 top-full mt-1 min-w-full bg-white dark:bg-gray-800 rounded-sm shadow-lg overflow-hidden z-50 border border-gray-200 dark:border-gray-700"
data-heap-id="markdown-dropdown-menu"
>
<button
onclick="viewPlainText()"
data-heap-id="view-markdown-button"
class="flex w-full items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 whitespace-nowrap"
>
<span class="icon-svg icon-sm">
{{ partialCached "icon" "open_in_new" "open_in_new" }}
</span>
<span>Open Markdown</span>
</button>
<button
onclick="openInDocsAI()"
data-heap-id="search-docs-ai-button"
class="sub-button"
>
<span class="icon-svg mt-[2px] text-base leading-none">
{{ partial "icon" "search" }}
</span>
<div class="leading-tight">
<div class="text-base">Ask questions with Docs AI</div>
</div>
</button>
{{ if eq hugo.Environment "production" }}
<button
onclick="openInClaude()"
data-heap-id="search-docs-ai-button"
class="sub-button"
>
<span class="icon-svg mt-[2px] text-base leading-none">
{{ partial "icon" "/icons/claude.svg" }}
</span>
<div class="leading-tight">
<div class="text-base">Open in Claude</div>
</div>
</button>
{{ end }}
</div>
</details>
<button
onclick="openInDocsAI()"
data-heap-id="search-docs-ai-button"
class="flex w-full items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 whitespace-nowrap"
>
<span class="icon-svg icon-sm">
{{ partialCached "icon" "search" "search" }}
</span>
<span>Ask Docs AI</span>
</button>
<button
onclick="openInClaude()"
data-heap-id="open-claude-button"
class="flex w-full items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 whitespace-nowrap"
>
<span class="icon-svg icon-sm">
{{ partialCached "icon" "/icons/claude.svg" "claude" }}
</span>
<span>Open in Claude</span>
</button>
</div>
</div>
<script>
function getCurrentPlaintextUrl() {
const url = window.location.href.split("#")[0].replace(/\/$/, "");
return `${url}/index.md`;
return `${url}.md`;
}
function copyMarkdown() {

View File

@@ -0,0 +1,5 @@
{{ $title := .Get "title" -}}
{{ $body := .InnerDeindent -}}
**{{ $title }}**
{{ $body }}

View File

@@ -0,0 +1 @@
{{ $text := .Get "text" }}\[{{ $text }}\]

View File

@@ -0,0 +1,3 @@
{{ $text := .Get "text" -}}
{{ $url := .Get "url" -}}
[{{ $text }}]({{ $url }})

View File

@@ -0,0 +1 @@
{{/* Card omitted from markdown output */}}

View File

@@ -0,0 +1 @@
{{/* CTA omitted from markdown output */}}

View File

@@ -0,0 +1,22 @@
{{- $all := .Get "all" -}}
{{- $win := .Get "win" -}}
{{- $win_arm_release := .Get "win_arm_release" -}}
{{- $mac := .Get "mac" -}}
{{- $linux := .Get "linux" -}}
{{- $build_path := .Get "build_path" -}}
Download Docker Desktop:
{{- if or $all $win }}
- [Windows](https://desktop.docker.com/win/main/amd64{{ $build_path }}Docker%20Desktop%20Installer.exe)
{{- end }}
{{- if $win_arm_release }}
- [Windows ARM {{ $win_arm_release }}](https://desktop.docker.com/win/main/arm64{{ $build_path }}Docker%20Desktop%20Installer.exe)
{{- end }}
{{- if or $all $mac }}
- [Mac (Apple chip)](https://desktop.docker.com/mac/main/arm64{{ $build_path }}Docker.dmg)
- [Mac (Intel chip)](https://desktop.docker.com/mac/main/amd64{{ $build_path }}Docker.dmg)
{{- end }}
{{- if or $all $linux }}
- [Linux (Debian)](https://desktop.docker.com/linux/main/amd64{{ $build_path }}docker-desktop-amd64.deb)
- [Linux (RPM)](https://desktop.docker.com/linux/main/amd64{{ $build_path }}docker-desktop-x86_64.rpm)
- [Linux (Arch)](https://desktop.docker.com/linux/main/amd64{{ $build_path }}docker-desktop-x86_64.pkg.tar.zst)
{{- end }}

View File

@@ -0,0 +1,3 @@
> **{{ .Get "title" | default (i18n "experimental") }}**
>
> {{ .InnerDeindent }}

View File

@@ -0,0 +1 @@
{{/* Grid omitted from markdown output */}}

View File

@@ -0,0 +1,4 @@
{{ $src := .Get "src" }}
{{ $alt := .Get "alt" }}
{{ $title := .Get "title" }}
![{{ $alt }}]({{ $src }}{{ with $title }} "{{ . }}"{{ end }})

View File

@@ -0,0 +1,3 @@
> **{{ .Get "title" | default (i18n "restricted") }}**
>
> {{ .InnerDeindent }}

View File

@@ -0,0 +1 @@
{{/* RSS button omitted from markdown output */}}

View File

@@ -0,0 +1 @@
{{/* Summary bar omitted from markdown output */}}

View File

@@ -0,0 +1,6 @@
{{ with .Inner }}{{/* don't do anything, just call it */}}{{ end -}}
{{ range (.Store.Get "tabs") -}}
**{{ .name }}**
{{ .content }}
{{- end -}}

View File

@@ -0,0 +1 @@
{{/* YouTube embed omitted from markdown output */}}

View File

@@ -10,4 +10,4 @@ HUGO_ENVIRONMENT = "preview"
SECRETS_SCAN_OMIT_PATHS = "public/contribute/file-conventions/index.html"
[context.deploy-preview]
command = "hugo --gc --minify -b $DEPLOY_PRIME_URL && npx pagefind@v1.3.0"
command = "hugo --gc --minify -b $DEPLOY_PRIME_URL && ./hack/flatten-markdown.sh && npx pagefind@v1.3.0"