feat(api): add Hugo-native API rendering POC

Add Hugo-native rendering as alternative to RapiDoc for API documentation.
Renders operations, parameters, request bodies, schemas, and responses
using Hugo templates styled after docusaurus-openapi aesthetic.

New partials:
- tag-renderer.html: Main renderer that loads OpenAPI spec
- operation.html: Operation block with header and sections
- parameters.html: Groups query/path/header parameters
- parameter-row.html: Individual parameter rendering
- request-body.html: Request body with schema
- schema.html: Recursive JSON schema rendering
- responses.html: Response status codes with schemas

Features:
- Parses OpenAPI YAML at build time (no JS required)
- Resolves $ref references for parameters and schemas
- Displays JSON examples from spec
- Color-coded status badges (2xx green, 4xx red)
- x-influxdata-related links rendered at page bottom
- Dark mode support
- Responsive design

Enable with `useHugoNative: true` in page frontmatter.
claude/api-code-samples-plan-MEkQO
Jason Stirnaman 2026-02-13 00:07:18 -06:00
parent 17301e0833
commit 4959d93c26
10 changed files with 1163 additions and 4 deletions

View File

@ -0,0 +1,598 @@
// Hugo-Native API Documentation Styles
// Styled after docusaurus-openapi aesthetic with clean, readable layouts
// Variables
$api-border-radius: 6px;
$api-spacing-sm: 0.5rem;
$api-spacing-md: 1rem;
$api-spacing-lg: 1.5rem;
$api-spacing-xl: 2rem;
// Method colors (matching existing theme)
$method-get: #00A3FF;
$method-post: #34BB55;
$method-put: #FFB94A;
$method-delete: #F95F53;
$method-patch: #9b2aff;
// Status code colors
$status-success: #34BB55;
$status-redirect: #FFB94A;
$status-client-error: #F95F53;
$status-server-error: #9b2aff;
// ============================================
// Operation Block
// ============================================
.api-hugo-native {
width: 100%;
}
.api-operation {
margin-bottom: $api-spacing-xl;
padding: $api-spacing-lg;
background: var(--article-bg, #fff);
border: 1px solid var(--article-border, #e8e8f0);
border-radius: $api-border-radius;
&:target {
animation: highlight-operation 1.5s ease-out;
}
}
@keyframes highlight-operation {
0% {
box-shadow: 0 0 0 4px rgba($method-get, 0.3);
}
100% {
box-shadow: none;
}
}
// Operation Header
.api-operation-header {
margin-bottom: $api-spacing-md;
}
.api-operation-endpoint {
display: flex;
align-items: center;
gap: $api-spacing-sm;
margin-bottom: $api-spacing-sm;
}
.api-method {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
border-radius: 4px;
color: #fff;
&--get { background-color: $method-get; }
&--post { background-color: $method-post; }
&--put { background-color: $method-put; }
&--delete { background-color: $method-delete; }
&--patch { background-color: $method-patch; }
}
.api-path {
font-family: var(--mono-font, 'IBM Plex Mono', monospace);
font-size: 0.95rem;
color: var(--article-text, #2b2b2b);
background: var(--code-bg, #f5f5f5);
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.api-operation-summary {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--article-heading, #2b2b2b);
}
.api-operation-description {
margin: $api-spacing-md 0;
color: var(--article-text, #545667);
line-height: 1.6;
p:last-child {
margin-bottom: 0;
}
}
// ============================================
// Section Titles
// ============================================
.api-section-title {
display: flex;
align-items: center;
gap: $api-spacing-sm;
margin: $api-spacing-lg 0 $api-spacing-md;
padding-bottom: $api-spacing-sm;
font-size: 1rem;
font-weight: 600;
color: var(--article-heading, #2b2b2b);
border-bottom: 1px solid var(--article-border, #e8e8f0);
}
// ============================================
// Parameters Section
// ============================================
.api-parameters {
margin: $api-spacing-lg 0;
}
.api-param-group {
margin-bottom: $api-spacing-md;
}
.api-param-group-title {
margin: $api-spacing-sm 0;
font-size: 0.85rem;
font-weight: 500;
color: var(--article-text-muted, #777);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.api-param-list {
border: 1px solid var(--article-border, #e8e8f0);
border-radius: $api-border-radius;
overflow: hidden;
}
.api-param-row {
padding: $api-spacing-md;
border-bottom: 1px solid var(--article-border, #e8e8f0);
background: var(--article-bg, #fff);
&:last-child {
border-bottom: none;
}
&:hover {
background: var(--article-bg-hover, #f9f9fb);
}
&--required {
background: rgba($method-post, 0.03);
&:hover {
background: rgba($method-post, 0.06);
}
}
}
.api-param-name-line {
display: flex;
align-items: center;
gap: $api-spacing-sm;
margin-bottom: 0.25rem;
}
.api-param-name {
font-family: var(--mono-font, 'IBM Plex Mono', monospace);
font-size: 0.9rem;
font-weight: 600;
color: var(--article-heading, #2b2b2b);
}
.api-param-type {
font-size: 0.8rem;
color: var(--article-text-muted, #777);
}
.api-param-description {
margin-top: 0.25rem;
font-size: 0.9rem;
color: var(--article-text, #545667);
line-height: 1.5;
p {
margin: 0;
}
}
.api-param-enum,
.api-param-default {
margin-top: 0.5rem;
font-size: 0.85rem;
}
.api-param-enum-label,
.api-param-default-label {
color: var(--article-text-muted, #777);
}
.api-param-enum-value,
.api-param-default-value {
font-family: var(--mono-font, 'IBM Plex Mono', monospace);
font-size: 0.8rem;
background: var(--code-bg, #f5f5f5);
padding: 0.125rem 0.375rem;
border-radius: 3px;
}
// ============================================
// Badges
// ============================================
.api-badge {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.375rem;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
border-radius: 3px;
&--required {
background: rgba($method-delete, 0.1);
color: $method-delete;
}
}
// ============================================
// Request Body Section
// ============================================
.api-request-body {
margin: $api-spacing-lg 0;
}
.api-request-body-description {
margin: $api-spacing-sm 0;
color: var(--article-text, #545667);
p:last-child {
margin-bottom: 0;
}
}
.api-content-type {
margin: $api-spacing-sm 0;
font-size: 0.85rem;
code {
font-family: var(--mono-font, 'IBM Plex Mono', monospace);
background: var(--code-bg, #f5f5f5);
padding: 0.125rem 0.375rem;
border-radius: 3px;
}
}
.api-content-type-label {
color: var(--article-text-muted, #777);
}
// ============================================
// Schema Section
// ============================================
.api-schema {
margin: $api-spacing-md 0;
&--nested {
margin-left: $api-spacing-lg;
padding-left: $api-spacing-md;
border-left: 2px solid var(--article-border, #e8e8f0);
}
}
.api-schema-properties {
border: 1px solid var(--article-border, #e8e8f0);
border-radius: $api-border-radius;
overflow: hidden;
}
.api-schema-property {
padding: $api-spacing-md;
border-bottom: 1px solid var(--article-border, #e8e8f0);
background: var(--article-bg, #fff);
&:last-child {
border-bottom: none;
}
&:hover {
background: var(--article-bg-hover, #f9f9fb);
}
&--required {
.api-schema-property-name {
&::after {
content: '*';
color: $method-delete;
margin-left: 0.25rem;
}
}
}
}
.api-schema-property-header {
display: flex;
align-items: center;
gap: $api-spacing-sm;
margin-bottom: 0.25rem;
}
.api-schema-property-name {
font-family: var(--mono-font, 'IBM Plex Mono', monospace);
font-size: 0.9rem;
font-weight: 600;
color: var(--article-heading, #2b2b2b);
}
.api-schema-property-type {
font-size: 0.8rem;
color: var(--article-text-muted, #777);
}
.api-schema-property-description {
margin-top: 0.25rem;
font-size: 0.9rem;
color: var(--article-text, #545667);
line-height: 1.5;
p {
margin: 0;
}
}
.api-schema-property-enum,
.api-schema-property-default,
.api-schema-property-example {
margin-top: 0.5rem;
font-size: 0.85rem;
}
.api-enum-label,
.api-default-label,
.api-example-label {
color: var(--article-text-muted, #777);
}
.api-enum-value,
.api-default-value,
.api-example-value {
font-family: var(--mono-font, 'IBM Plex Mono', monospace);
font-size: 0.8rem;
background: var(--code-bg, #f5f5f5);
padding: 0.125rem 0.375rem;
border-radius: 3px;
}
// Schema Example Block
.api-schema-example {
margin-top: $api-spacing-md;
padding: $api-spacing-md;
background: var(--code-bg, #f5f5f5);
border-radius: $api-border-radius;
}
.api-schema-example-title {
margin: 0 0 $api-spacing-sm;
font-size: 0.85rem;
font-weight: 600;
color: var(--article-text-muted, #777);
}
.api-schema-example-code {
margin: 0;
padding: 0;
background: transparent;
overflow-x: auto;
code {
font-family: var(--mono-font, 'IBM Plex Mono', monospace);
font-size: 0.85rem;
}
}
// ============================================
// Responses Section
// ============================================
.api-responses {
margin: $api-spacing-lg 0;
}
.api-response-list {
display: flex;
flex-direction: column;
gap: $api-spacing-sm;
}
.api-response {
border: 1px solid var(--article-border, #e8e8f0);
border-radius: $api-border-radius;
overflow: hidden;
}
.api-response-header {
display: flex;
align-items: center;
gap: $api-spacing-sm;
padding: $api-spacing-sm $api-spacing-md;
background: var(--article-bg-alt, #f9f9fb);
}
.api-response-status {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 3rem;
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
font-weight: 600;
border-radius: 4px;
color: #fff;
&--success { background-color: $status-success; }
&--redirect { background-color: $status-redirect; }
&--client-error { background-color: $status-client-error; }
&--server-error { background-color: $status-server-error; }
&--info { background-color: var(--article-text-muted, #777); }
}
.api-response-description {
font-size: 0.9rem;
color: var(--article-text, #545667);
}
.api-response-body {
padding: $api-spacing-md;
border-top: 1px solid var(--article-border, #e8e8f0);
}
// ============================================
// Tag Overview
// ============================================
.api-tag-overview {
margin-bottom: $api-spacing-xl;
padding-bottom: $api-spacing-lg;
border-bottom: 1px solid var(--article-border, #e8e8f0);
}
.api-tag-description {
color: var(--article-text, #545667);
line-height: 1.7;
h4, h5 {
margin-top: $api-spacing-lg;
margin-bottom: $api-spacing-sm;
color: var(--article-heading, #2b2b2b);
}
ul, ol {
padding-left: $api-spacing-lg;
}
a {
color: var(--link-color, #00A3FF);
&:hover {
text-decoration: underline;
}
}
}
// ============================================
// Related Guides Section
// ============================================
.api-related-guides {
margin-top: $api-spacing-xl;
padding-top: $api-spacing-lg;
border-top: 1px solid var(--article-border, #e8e8f0);
}
.api-related-title {
margin: 0 0 $api-spacing-md;
font-size: 1rem;
font-weight: 600;
color: var(--article-heading, #2b2b2b);
}
.api-related-list {
margin: 0;
padding: 0;
list-style: none;
li {
margin-bottom: $api-spacing-sm;
}
a {
color: var(--link-color, #00A3FF);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
// ============================================
// Dark Mode Overrides
// ============================================
[data-theme='dark'],
html:has(link[title='dark-theme']:not([disabled])) {
.api-operation {
background: var(--article-bg, #1a1a2a);
border-color: var(--article-border, #2a2a3a);
}
.api-related-guides {
border-color: var(--article-border, #2a2a3a);
}
.api-related-title {
color: var(--article-heading, #e0e0e0);
}
.api-path {
background: var(--code-bg, #252535);
color: var(--article-text, #d4d7dd);
}
.api-param-row,
.api-schema-property {
background: var(--article-bg, #1a1a2a);
&:hover {
background: var(--article-bg-hover, #252535);
}
}
.api-param-enum-value,
.api-param-default-value,
.api-enum-value,
.api-default-value,
.api-example-value {
background: var(--code-bg, #252535);
}
.api-schema-example {
background: var(--code-bg, #252535);
}
.api-response-header {
background: var(--article-bg-alt, #252535);
}
}
// ============================================
// Responsive Adjustments
// ============================================
@media (max-width: 768px) {
.api-operation {
padding: $api-spacing-md;
}
.api-operation-endpoint {
flex-wrap: wrap;
}
.api-path {
font-size: 0.85rem;
word-break: break-all;
}
.api-param-name-line,
.api-schema-property-header {
flex-wrap: wrap;
}
}

View File

@ -34,7 +34,8 @@
"layouts/code-controls",
"layouts/v3-wayfinding",
"layouts/api-layout",
"layouts/api-security-schemes";
"layouts/api-security-schemes",
"layouts/api-hugo-native";
// Import Components
@import "components/influxdb-version-detector",

View File

@ -124,10 +124,20 @@ param) - shows RapiDoc with all operations for the tag For conceptual pages
{{/* Hugo page content if any (for custom intro content) */}}
{{ with .Content }}
<section class="api-content-body">{{ . }}</section>
{{ end }} {{/* RapiDoc renders all operations for this tag */}} {{ with
.Params.staticFilePath }}
{{ end }}
{{/* Choose rendering mode: Hugo-native (POC) or RapiDoc (default) */}}
{{ $useHugoNative := .Params.useHugoNative | default false }}
{{ with .Params.staticFilePath }}
<section class="api-operations-section">
{{ partial "api/rapidoc-tag.html" $ }}
{{ if $useHugoNative }}
{{/* Hugo-Native rendering (docusaurus-openapi style) */}}
{{ partial "api/hugo-native/tag-renderer.html" $ }}
{{ else }}
{{/* RapiDoc rendering (current default) */}}
{{ partial "api/rapidoc-tag.html" $ }}
{{ end }}
</section>
{{ end }} {{ end }} {{ end }} {{ partial "article/related.html" . }}
</article>

View File

@ -0,0 +1,61 @@
{{/*
Hugo-Native Operation Renderer
Renders a single API operation with parameters, request body, and responses.
Styled to match docusaurus-openapi aesthetic.
Params:
- operation: Map with method, path, summary, operationId
- spec: The full OpenAPI spec object
- context: The page context for URL generation
*/}}
{{ $operation := .operation }}
{{ $spec := .spec }}
{{ $method := lower $operation.method }}
{{ $path := $operation.path }}
{{ $operationId := $operation.operationId }}
{{/* Find the operation definition in the spec */}}
{{ $pathDef := index $spec.paths $path }}
{{ $opDef := dict }}
{{ if $pathDef }}
{{ $opDef = index $pathDef $method | default dict }}
{{ end }}
{{/* Generate anchor ID matching existing TOC format */}}
{{ $anchorId := printf "%s-%s" $method $path }}
<div class="api-operation" id="{{ $anchorId }}" data-method="{{ $method }}" data-operation-id="{{ $operationId }}">
{{/* Operation Header */}}
<div class="api-operation-header">
<div class="api-operation-endpoint">
<span class="api-method api-method--{{ $method }}">{{ upper $method }}</span>
<code class="api-path">{{ $path }}</code>
</div>
<h3 class="api-operation-summary">{{ $operation.summary }}</h3>
</div>
{{/* Operation Description */}}
{{ with $opDef.description }}
<div class="api-operation-description">
{{ . | markdownify }}
</div>
{{ end }}
{{/* Parameters Section */}}
{{ $params := $opDef.parameters | default slice }}
{{ if gt (len $params) 0 }}
{{ partial "api/hugo-native/parameters.html" (dict "parameters" $params "spec" $spec) }}
{{ end }}
{{/* Request Body Section */}}
{{ with $opDef.requestBody }}
{{ partial "api/hugo-native/request-body.html" (dict "requestBody" . "spec" $spec) }}
{{ end }}
{{/* Responses Section */}}
{{ with $opDef.responses }}
{{ partial "api/hugo-native/responses.html" (dict "responses" . "spec" $spec) }}
{{ end }}
</div>

View File

@ -0,0 +1,65 @@
{{/*
Hugo-Native Parameter Row Renderer
Renders a single parameter with name, type, required badge, and description.
Params:
- param: Parameter object with name, schema, required, description
- spec: The full OpenAPI spec object for resolving schema $ref
*/}}
{{ $param := .param }}
{{ $spec := .spec }}
{{ $name := $param.name }}
{{ $required := $param.required | default false }}
{{ $description := $param.description | default "" }}
{{/* Resolve schema type */}}
{{ $schema := $param.schema | default dict }}
{{ $type := $schema.type | default "string" }}
{{ $format := $schema.format | default "" }}
{{ $enum := $schema.enum | default slice }}
{{ $default := $schema.default }}
{{/* Build type display string */}}
{{ $typeDisplay := $type }}
{{ if $format }}
{{ $typeDisplay = printf "%s <%s>" $type $format }}
{{ end }}
<div class="api-param-row{{ if $required }} api-param-row--required{{ end }}">
<div class="api-param-info">
<div class="api-param-name-line">
<code class="api-param-name">{{ $name }}</code>
{{ if $required }}
<span class="api-badge api-badge--required">required</span>
{{ end }}
<span class="api-param-type">{{ $typeDisplay }}</span>
</div>
{{ if $description }}
<div class="api-param-description">
{{ $description | markdownify }}
</div>
{{ end }}
{{/* Show enum values if present */}}
{{ if gt (len $enum) 0 }}
<div class="api-param-enum">
<span class="api-param-enum-label">Allowed values:</span>
{{ range $i, $val := $enum }}
{{ if $i }}, {{ end }}<code class="api-param-enum-value">{{ $val }}</code>
{{ end }}
</div>
{{ end }}
{{/* Show default value if present */}}
{{ if $default }}
<div class="api-param-default">
<span class="api-param-default-label">Default:</span>
<code class="api-param-default-value">{{ $default }}</code>
</div>
{{ end }}
</div>
</div>

View File

@ -0,0 +1,85 @@
{{/*
Hugo-Native Parameters Renderer
Renders a table of API operation parameters (query, path, header).
Resolves $ref references to component parameters.
Params:
- parameters: Array of parameter objects
- spec: The full OpenAPI spec object for resolving $ref
*/}}
{{ $parameters := .parameters }}
{{ $spec := .spec }}
{{/* Resolve $ref parameters and group by location */}}
{{ $queryParams := slice }}
{{ $pathParams := slice }}
{{ $headerParams := slice }}
{{ range $parameters }}
{{ $param := . }}
{{/* Resolve $ref if present */}}
{{ if isset . "$ref" }}
{{ $refPath := index . "$ref" }}
{{/* Parse ref like "#/components/parameters/db" */}}
{{ $refParts := split $refPath "/" }}
{{ if ge (len $refParts) 4 }}
{{ $paramName := index $refParts 3 }}
{{ with index $spec.components.parameters $paramName }}
{{ $param = . }}
{{ end }}
{{ end }}
{{ end }}
{{/* Group by 'in' location */}}
{{ $location := $param.in | default "query" }}
{{ if eq $location "query" }}
{{ $queryParams = $queryParams | append $param }}
{{ else if eq $location "path" }}
{{ $pathParams = $pathParams | append $param }}
{{ else if eq $location "header" }}
{{ $headerParams = $headerParams | append $param }}
{{ end }}
{{ end }}
<div class="api-parameters">
<h4 class="api-section-title">Parameters</h4>
{{/* Path Parameters */}}
{{ if gt (len $pathParams) 0 }}
<div class="api-param-group">
<h5 class="api-param-group-title">Path parameters</h5>
<div class="api-param-list">
{{ range $pathParams }}
{{ partial "api/hugo-native/parameter-row.html" (dict "param" . "spec" $spec) }}
{{ end }}
</div>
</div>
{{ end }}
{{/* Query Parameters */}}
{{ if gt (len $queryParams) 0 }}
<div class="api-param-group">
<h5 class="api-param-group-title">Query parameters</h5>
<div class="api-param-list">
{{ range $queryParams }}
{{ partial "api/hugo-native/parameter-row.html" (dict "param" . "spec" $spec) }}
{{ end }}
</div>
</div>
{{ end }}
{{/* Header Parameters */}}
{{ if gt (len $headerParams) 0 }}
<div class="api-param-group">
<h5 class="api-param-group-title">Header parameters</h5>
<div class="api-param-list">
{{ range $headerParams }}
{{ partial "api/hugo-native/parameter-row.html" (dict "param" . "spec" $spec) }}
{{ end }}
</div>
</div>
{{ end }}
</div>

View File

@ -0,0 +1,60 @@
{{/*
Hugo-Native Request Body Renderer
Renders the request body section including schema properties.
Params:
- requestBody: OpenAPI requestBody object
- spec: The full OpenAPI spec object for resolving $ref
*/}}
{{ $requestBody := .requestBody }}
{{ $spec := .spec }}
{{ $required := $requestBody.required | default false }}
{{ $description := $requestBody.description | default "" }}
{{/* Get content schema - typically application/json */}}
{{ $content := $requestBody.content | default dict }}
{{ $jsonContent := index $content "application/json" | default dict }}
{{ $schema := $jsonContent.schema | default dict }}
{{/* Resolve $ref if present */}}
{{ $resolvedSchema := $schema }}
{{ if isset $schema "$ref" }}
{{ $refPath := index $schema "$ref" }}
{{/* Parse ref like "#/components/schemas/DistinctCacheCreateRequest" */}}
{{ $refParts := split $refPath "/" }}
{{ if ge (len $refParts) 4 }}
{{ $schemaName := index $refParts 3 }}
{{ with index $spec.components.schemas $schemaName }}
{{ $resolvedSchema = . }}
{{ end }}
{{ end }}
{{ end }}
<div class="api-request-body">
<h4 class="api-section-title">
Request body
{{ if $required }}
<span class="api-badge api-badge--required">required</span>
{{ end }}
</h4>
{{ if $description }}
<div class="api-request-body-description">
{{ $description | markdownify }}
</div>
{{ end }}
{{/* Content type indicator */}}
<div class="api-content-type">
<span class="api-content-type-label">Content-Type:</span>
<code>application/json</code>
</div>
{{/* Render schema properties */}}
{{ with $resolvedSchema }}
{{ partial "api/hugo-native/schema.html" (dict "schema" . "spec" $spec "level" 0) }}
{{ end }}
</div>

View File

@ -0,0 +1,79 @@
{{/*
Hugo-Native Responses Renderer
Renders the responses section for an API operation.
Shows status codes, descriptions, and response schemas.
Params:
- responses: Map of status codes to response objects
- spec: The full OpenAPI spec object for resolving $ref
*/}}
{{ $responses := .responses }}
{{ $spec := .spec }}
<div class="api-responses">
<h4 class="api-section-title">Responses</h4>
<div class="api-response-list">
{{ range $statusCode, $response := $responses }}
{{/* Resolve $ref if present */}}
{{ $resolvedResponse := $response }}
{{ if isset $response "$ref" }}
{{ $refPath := index $response "$ref" }}
{{ $refParts := split $refPath "/" }}
{{ if ge (len $refParts) 4 }}
{{ $responseName := index $refParts 3 }}
{{ with index $spec.components.responses $responseName }}
{{ $resolvedResponse = . }}
{{ end }}
{{ end }}
{{ end }}
{{ $description := $resolvedResponse.description | default "" }}
{{ $content := $resolvedResponse.content | default dict }}
{{/* Determine status category for styling */}}
{{ $statusCategory := "info" }}
{{ if hasPrefix $statusCode "2" }}
{{ $statusCategory = "success" }}
{{ else if hasPrefix $statusCode "3" }}
{{ $statusCategory = "redirect" }}
{{ else if hasPrefix $statusCode "4" }}
{{ $statusCategory = "client-error" }}
{{ else if hasPrefix $statusCode "5" }}
{{ $statusCategory = "server-error" }}
{{ end }}
<div class="api-response api-response--{{ $statusCategory }}">
<div class="api-response-header">
<span class="api-response-status api-response-status--{{ $statusCategory }}">{{ $statusCode }}</span>
<span class="api-response-description">{{ $description }}</span>
</div>
{{/* Response body schema if present */}}
{{ with $content }}
{{ $jsonContent := index . "application/json" | default dict }}
{{ with $jsonContent.schema }}
{{/* Resolve schema $ref if present */}}
{{ $resolvedSchema := . }}
{{ if isset . "$ref" }}
{{ $refPath := index . "$ref" }}
{{ $refParts := split $refPath "/" }}
{{ if ge (len $refParts) 4 }}
{{ $schemaName := index $refParts 3 }}
{{ with index $spec.components.schemas $schemaName }}
{{ $resolvedSchema = . }}
{{ end }}
{{ end }}
{{ end }}
<div class="api-response-body">
{{ partial "api/hugo-native/schema.html" (dict "schema" $resolvedSchema "spec" $spec "level" 0) }}
</div>
{{ end }}
{{ end }}
</div>
{{ end }}
</div>
</div>

View File

@ -0,0 +1,117 @@
{{/*
Hugo-Native Schema Renderer
Renders a JSON schema as a property table with nested object support.
Similar to docusaurus-openapi's schema tables.
Params:
- schema: OpenAPI schema object
- spec: The full OpenAPI spec object for resolving $ref
- level: Nesting level (0 = root)
*/}}
{{ $schema := .schema }}
{{ $spec := .spec }}
{{ $level := .level | default 0 }}
{{ $type := $schema.type | default "object" }}
{{ $properties := $schema.properties | default dict }}
{{ $required := $schema.required | default slice }}
{{ $example := $schema.example }}
{{/* Convert required slice to map for easy lookup */}}
{{ $requiredMap := dict }}
{{ range $required }}
{{ $requiredMap = merge $requiredMap (dict . true) }}
{{ end }}
<div class="api-schema{{ if gt $level 0 }} api-schema--nested{{ end }}" data-level="{{ $level }}">
{{ if gt (len $properties) 0 }}
<div class="api-schema-properties">
{{ range $propName, $propSchema := $properties }}
{{ $isRequired := index $requiredMap $propName | default false }}
{{ $propType := $propSchema.type | default "string" }}
{{ $propDescription := $propSchema.description | default "" }}
{{ $propFormat := $propSchema.format | default "" }}
{{ $propEnum := $propSchema.enum | default slice }}
{{ $propDefault := $propSchema.default }}
{{ $propExample := $propSchema.example }}
{{/* Build type display */}}
{{ $typeDisplay := $propType }}
{{ if eq $propType "array" }}
{{ $itemsType := "object" }}
{{ with $propSchema.items }}
{{ $itemsType = .type | default "object" }}
{{ end }}
{{ $typeDisplay = printf "%s[]" $itemsType }}
{{ else if $propFormat }}
{{ $typeDisplay = printf "%s <%s>" $propType $propFormat }}
{{ end }}
<div class="api-schema-property{{ if $isRequired }} api-schema-property--required{{ end }}">
<div class="api-schema-property-header">
<code class="api-schema-property-name">{{ $propName }}</code>
{{ if $isRequired }}
<span class="api-badge api-badge--required">required</span>
{{ end }}
<span class="api-schema-property-type">{{ $typeDisplay }}</span>
</div>
{{ if $propDescription }}
<div class="api-schema-property-description">
{{ $propDescription | markdownify }}
</div>
{{ end }}
{{/* Enum values */}}
{{ if gt (len $propEnum) 0 }}
<div class="api-schema-property-enum">
<span class="api-enum-label">Allowed:</span>
{{ range $i, $val := $propEnum }}
{{ if $i }}, {{ end }}<code class="api-enum-value">{{ $val }}</code>
{{ end }}
</div>
{{ end }}
{{/* Default value */}}
{{ if $propDefault }}
<div class="api-schema-property-default">
<span class="api-default-label">Default:</span>
<code class="api-default-value">{{ $propDefault }}</code>
</div>
{{ end }}
{{/* Example value */}}
{{ if $propExample }}
<div class="api-schema-property-example">
<span class="api-example-label">Example:</span>
<code class="api-example-value">{{ jsonify $propExample }}</code>
</div>
{{ end }}
{{/* Nested object/array rendering (limit depth to prevent infinite loops) */}}
{{ if and (eq $propType "object") (lt $level 2) }}
{{ with $propSchema.properties }}
{{ partial "api/hugo-native/schema.html" (dict "schema" $propSchema "spec" $spec "level" (add $level 1)) }}
{{ end }}
{{ else if and (eq $propType "array") (lt $level 2) }}
{{ with $propSchema.items }}
{{ if isset . "properties" }}
{{ partial "api/hugo-native/schema.html" (dict "schema" . "spec" $spec "level" (add $level 1)) }}
{{ end }}
{{ end }}
{{ end }}
</div>
{{ end }}
</div>
{{ end }}
{{/* Show example at schema level */}}
{{ if and $example (eq $level 0) }}
<div class="api-schema-example">
<h5 class="api-schema-example-title">Example</h5>
<pre class="api-schema-example-code"><code class="language-json">{{ jsonify (dict "indent" " ") $example }}</code></pre>
</div>
{{ end }}
</div>

View File

@ -0,0 +1,83 @@
{{/*
Hugo-Native Tag Page Renderer
Renders all operations for a tag page using Hugo templates instead of RapiDoc.
Parses the OpenAPI spec file and renders each operation natively.
Required page params:
- staticFilePath: Path to the OpenAPI specification file (YAML)
- operations: Array of operation metadata from frontmatter
Usage:
{{ partial "api/hugo-native/tag-renderer.html" . }}
*/}}
{{ $page := . }}
{{ $specPath := .Params.staticFilePath }}
{{ $operations := .Params.operations | default slice }}
{{/* Load and parse the OpenAPI spec from static/ directory */}}
{{ $spec := dict }}
{{ if $specPath }}
{{/* Build path to static file (staticFilePath has leading slash, e.g. /openapi/...) */}}
{{ $staticFile := printf "static%s" $specPath }}
{{/* Use os.ReadFile (Hugo 0.121+) to read from static directory */}}
{{ with os.ReadFile $staticFile }}
{{ $spec = . | transform.Unmarshal }}
{{ else }}
{{/* Fallback: try unmounted resources (for assets mount configuration) */}}
{{ $cleanPath := strings.TrimPrefix "/" $specPath }}
{{ with resources.Get $cleanPath }}
{{ $spec = .Content | transform.Unmarshal }}
{{ end }}
{{ end }}
{{ end }}
{{/* Tag description and related links from spec */}}
{{ $tagDescription := "" }}
{{ $tagRelated := slice }}
{{ $tagName := .Params.tag | default "" }}
{{ range $spec.tags }}
{{ if eq .name $tagName }}
{{ $tagDescription = .description | default "" }}
{{/* Extract x-influxdata-related from the tag */}}
{{ with index . "x-influxdata-related" }}
{{ $tagRelated = . }}
{{ end }}
{{ end }}
{{ end }}
<div class="api-hugo-native" data-component="api-hugo-native">
{{/* Tag Overview/Description */}}
{{ if $tagDescription }}
<section class="api-tag-overview">
<div class="api-tag-description">
{{ $tagDescription | markdownify }}
</div>
</section>
{{ end }}
{{/* Operations List */}}
<section class="api-operations">
{{ range $operations }}
{{ partial "api/hugo-native/operation.html" (dict
"operation" .
"spec" $spec
"context" $page
) }}
{{ end }}
</section>
{{/* Related Guides - displayed at bottom like standard page template */}}
{{ if gt (len $tagRelated) 0 }}
<section class="api-related-guides">
<h4 class="api-related-title">Related guides</h4>
<ul class="api-related-list">
{{ range $tagRelated }}
<li><a href="{{ .href }}">{{ .title }}</a></li>
{{ end }}
</ul>
</section>
{{ end }}
</div>