Infrastructure
Api Documentation code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/api-documentation.blade.php
@props([
'baseUrl' => 'https://api.harbour.test',
'version' => '2026-06-14',
'endpoints' => [],
'webhooks' => [],
])
@php
$endpoints = $endpoints ?: [
[
'method' => 'GET',
'path' => '/v1/posts',
'model' => 'Posts',
'title' => 'List posts',
'description' => 'Returns published and draft blog posts for the authenticated workspace.',
'scope' => 'posts:read',
'response' => '200 OK',
'responseExample' => <<<'JSON'
{
"data": [
{
"id": "pst_01HX9",
"title": "Designing a calmer changelog",
"slug": "designing-a-calmer-changelog",
"status": "published",
"published_at": "2026-06-14T03:12:00Z"
}
]
}
JSON,
'schema' => [
['key' => 'data', 'source' => 'Response', 'type' => 'array<object>'],
['key' => 'data[].id', 'source' => 'Response', 'type' => 'string'],
['key' => 'data[].title', 'source' => 'Response', 'type' => 'string'],
['key' => 'data[].slug', 'source' => 'Response', 'type' => 'string'],
['key' => 'data[].status', 'source' => 'Response', 'type' => 'string'],
['key' => 'data[].published_at', 'source' => 'Response', 'type' => 'string<date-time>'],
],
],
[
'method' => 'POST',
'path' => '/v1/posts',
'model' => 'Posts',
'title' => 'Create post',
'description' => 'Creates a draft blog post and returns the editable resource.',
'scope' => 'posts:write',
'response' => '201 Created',
'requestExample' => <<<'JSON'
{
"title": "Shipping better release notes",
"slug": "shipping-better-release-notes",
"excerpt": "A short guide to writing product updates readers can scan.",
"body": "Start with the user impact, then include the operational detail.",
"status": "draft",
"author_id": "usr_01H8R",
"tags": ["release-notes", "product"]
}
JSON,
'responseExample' => <<<'JSON'
{
"data": {
"id": "pst_01HYA",
"title": "Shipping better release notes",
"slug": "shipping-better-release-notes",
"status": "draft",
"edit_url": "https://harbour.test/posts/shipping-better-release-notes/edit"
}
}
JSON,
'schema' => [
['key' => 'title', 'source' => 'Request', 'type' => 'string', 'required' => true],
['key' => 'slug', 'source' => 'Request', 'type' => 'string', 'required' => true],
['key' => 'excerpt', 'source' => 'Request', 'type' => 'string', 'required' => false],
['key' => 'body', 'source' => 'Request', 'type' => 'string', 'required' => true],
['key' => 'status', 'source' => 'Request', 'type' => 'string', 'required' => false],
['key' => 'author_id', 'source' => 'Request', 'type' => 'string', 'required' => true],
['key' => 'tags', 'source' => 'Request', 'type' => 'array<string>', 'required' => false],
['key' => 'data', 'source' => 'Response', 'type' => 'object'],
['key' => 'data.id', 'source' => 'Response', 'type' => 'string'],
['key' => 'data.title', 'source' => 'Response', 'type' => 'string'],
['key' => 'data.slug', 'source' => 'Response', 'type' => 'string'],
['key' => 'data.status', 'source' => 'Response', 'type' => 'string'],
['key' => 'data.edit_url', 'source' => 'Response', 'type' => 'url'],
],
],
[
'method' => 'PATCH',
'path' => '/v1/posts/{post}',
'model' => 'Posts',
'title' => 'Update post',
'description' => 'Updates blog post metadata, editorial status, or scheduled publish time.',
'scope' => 'posts:write',
'response' => '200 OK',
'requestExample' => <<<'JSON'
{
"title": "Shipping better release notes",
"status": "scheduled",
"published_at": "2026-06-20T09:00:00Z"
}
JSON,
'responseExample' => <<<'JSON'
{
"data": {
"id": "pst_01HYA",
"title": "Shipping better release notes",
"status": "scheduled",
"published_at": "2026-06-20T09:00:00Z"
}
}
JSON,
'schema' => [
['key' => 'title', 'source' => 'Request', 'type' => 'string', 'required' => false],
['key' => 'status', 'source' => 'Request', 'type' => 'string', 'required' => false],
['key' => 'published_at', 'source' => 'Request', 'type' => 'string<date-time>', 'required' => false],
['key' => 'data', 'source' => 'Response', 'type' => 'object'],
['key' => 'data.id', 'source' => 'Response', 'type' => 'string'],
['key' => 'data.title', 'source' => 'Response', 'type' => 'string'],
['key' => 'data.status', 'source' => 'Response', 'type' => 'string'],
['key' => 'data.published_at', 'source' => 'Response', 'type' => 'string<date-time>'],
],
],
[
'method' => 'DELETE',
'path' => '/v1/posts/{post}',
'model' => 'Posts',
'title' => 'Delete post',
'description' => 'Deletes a draft blog post or archives a published post for audit history.',
'scope' => 'posts:write',
'response' => '204 No Content',
'responseExample' => <<<'TEXT'
HTTP/1.1 204 No Content
No response body is returned for a successful post deletion.
TEXT,
],
];
$webhooks = $webhooks ?: [
['event' => 'post.published', 'description' => 'A blog post moved from draft or scheduled into the published feed.'],
['event' => 'post.updated', 'description' => 'A title, body, tag, or editorial status changed.'],
['event' => 'post.deleted', 'description' => 'A draft post was deleted or a published post was archived.'],
];
$endpoints = collect($endpoints)
->map(function (array $endpoint, int $index): array {
$endpoint['key'] ??= strtolower($endpoint['method']).'-'.trim((string) preg_replace('/[^a-z0-9]+/i', '-', $endpoint['path']), '-').'-'.$index;
return $endpoint;
})
->values()
->all();
$methodVariants = [
'GET' => 'green',
'POST' => 'blue',
'PATCH' => 'amber',
'DELETE' => 'red',
];
$navigation = [
['href' => '#authentication', 'label' => 'Authentication', 'description' => 'Bearer token headers'],
['href' => '#endpoints', 'label' => 'Endpoints', 'description' => count($endpoints).' REST operations'],
['href' => '#webhooks', 'label' => 'Webhooks', 'description' => count($webhooks).' event types'],
];
$codeExampleClasses = 'overflow-x-auto px-4 py-4 font-mono text-xs leading-6 text-gray-700 dark:text-gray-300 '
.'[&_.code-line]:block [&_.code-line]:whitespace-pre '
.'[&_.tok-key]:text-brand-700 dark:[&_.tok-key]:text-brand-300 '
.'[&_.tok-str]:text-green-700 dark:[&_.tok-str]:text-green-300 '
.'[&_.tok-com]:text-gray-400 dark:[&_.tok-com]:text-gray-500 [&_.tok-com]:italic '
.'[&_.tok-num]:text-amber-600 dark:[&_.tok-num]:text-amber-200 '
.'[&_.tok-bool]:text-accent-700 dark:[&_.tok-bool]:text-accent-300 '
.'[&_.tok-punc]:text-gray-400 dark:[&_.tok-punc]:text-gray-500';
$highlightCodeLine = function (string $line): string {
$escaped = e($line);
if ($escaped === '') {
return ' ';
}
if (str_starts_with($escaped, 'HTTP/') || str_starts_with($escaped, 'No response body')) {
return '<span class="tok-com">'.$escaped.'</span>';
}
$escaped = preg_replace('/(:\s*)("[^&]+")/', '$1<span class="tok-str">$2</span>', $escaped);
$escaped = preg_replace('/(:\s*)(\d+(?:\.\d+)?)/', '$1<span class="tok-num">$2</span>', $escaped);
$escaped = preg_replace('/(:\s*)(true|false|null)\b/', '$1<span class="tok-bool">$2</span>', $escaped);
$escaped = preg_replace('/("[^&]+")(\s*:)/', '<span class="tok-key">$1</span><span class="tok-punc">$2</span>', $escaped);
return $escaped;
};
@endphp
<div {{ $attributes->merge(['class' => 'grid gap-8 lg:grid-cols-[16rem_minmax(0,1fr)]']) }}>
<aside class="lg:sticky lg:top-24 lg:self-start">
<nav aria-label="API documentation sections" class="overflow-hidden rounded-2xl border border-gray-200 bg-white text-sm shadow-sm ring-1 ring-gray-900/5 dark:border-gray-800 dark:bg-gray-900 dark:ring-white/5">
<div class="border-b border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-950/40">
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.12em] text-gray-500 dark:text-gray-400">API reference</p>
<h2 class="mt-2 text-base font-semibold tracking-tight text-gray-900 dark:text-white">Harbour API</h2>
</div>
</div>
<div class="mt-4 rounded-xl border border-gray-200 bg-white p-3 dark:border-gray-800 dark:bg-gray-900">
<p class="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">Base URL</p>
<p class="mt-1 break-all font-mono text-xs font-semibold text-gray-800 dark:text-gray-100">{{ $baseUrl }}</p>
</div>
</div>
<div class="p-3">
<p class="px-2 pb-2 text-xs font-semibold uppercase tracking-[0.12em] text-gray-500 dark:text-gray-400">Sections</p>
<div class="space-y-1">
@foreach ($navigation as $item)
<a href="{{ $item['href'] }}" class="group flex items-start gap-3 rounded-xl px-3 py-2.5 transition hover:bg-brand-50 dark:hover:bg-brand-950/30">
<span class="min-w-0">
<span class="block font-semibold text-gray-800 transition group-hover:text-brand-800 dark:text-gray-200 dark:group-hover:text-brand-200">{{ $item['label'] }}</span>
<span class="mt-0.5 block text-xs leading-5 text-gray-500 dark:text-gray-400">{{ $item['description'] }}</span>
</span>
</a>
@endforeach
</div>
</div>
<div class="border-t border-gray-200 p-4 dark:border-gray-800">
<div class="grid grid-cols-2 gap-3">
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">Version</p>
<p class="mt-1 font-mono text-xs font-semibold text-gray-900 dark:text-white">{{ $version }}</p>
</div>
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">Limit</p>
<p class="mt-1 font-mono text-xs font-semibold text-gray-900 dark:text-white">600/min</p>
</div>
<div class="col-span-2">
<p class="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">Format</p>
<p class="mt-1 font-mono text-xs font-semibold text-gray-900 dark:text-white">application/json</p>
</div>
</div>
<div class="mt-4 rounded-xl bg-gray-50 p-3 dark:bg-gray-950/50">
<p class="flex items-center gap-2 text-xs font-medium text-gray-600 dark:text-gray-300">
<x-ui.icon name="shield-check" size="sm" weight="fill" class="text-brand-500 dark:text-brand-400" />
Token scopes required
</p>
<p class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-400">Read and write scopes are shown beside every endpoint.</p>
</div>
</div>
</nav>
<div class="mt-4 rounded-2xl border border-gray-200 bg-white p-4 text-sm shadow-sm dark:border-gray-800 dark:bg-gray-900">
<p class="text-xs font-semibold uppercase tracking-[0.12em] text-gray-500 dark:text-gray-400">Quick links</p>
<div class="mt-3 space-y-2">
@foreach ([
['href' => '#authentication', 'label' => 'Copy auth example', 'icon' => 'copy'],
['href' => '#webhooks', 'label' => 'Review signatures', 'icon' => 'signature'],
] as $link)
<a href="{{ $link['href'] }}" class="flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs font-medium text-gray-600 transition hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white">
<x-ui.icon :name="$link['icon']" size="sm" />
{{ $link['label'] }}
</a>
@endforeach
</div>
</div>
</aside>
<div class="min-w-0 space-y-10">
<section id="authentication" class="scroll-mt-24 space-y-5">
<div>
<h2 class="text-2xl font-semibold tracking-tight text-gray-900 dark:text-white">Authenticate every request with a workspace token</h2>
<p class="mt-2 max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-400">
Tokens are scoped by workspace and permission. Send the token in the Authorization header and rotate it from the workspace security screen.
</p>
</div>
<x-ui.code-window :tabs="['curl' => 'Request', 'json' => 'Response']" default="curl" lang="http">
<x-slot:curl>
<div class="code-line"><span class="tok-fn">curl</span> <span class="tok-punc">{{ $baseUrl }}</span><span class="tok-str">/v1/posts</span> \</div>
<div class="code-line"> <span class="tok-key">-H</span> <span class="tok-str">"Authorization: Bearer hbr_live_****"</span> \</div>
<div class="code-line"> <span class="tok-key">-H</span> <span class="tok-str">"Accept: application/json"</span></div>
</x-slot:curl>
<x-slot:json>
<div class="code-line"><span class="tok-punc">{</span></div>
<div class="code-line"> <span class="tok-key">"data"</span><span class="tok-punc">:</span> <span class="tok-punc">[</span></div>
<div class="code-line"> <span class="tok-punc">{</span><span class="tok-key">"id"</span><span class="tok-punc">:</span> <span class="tok-str">"pst_01HX9"</span><span class="tok-punc">,</span> <span class="tok-key">"title"</span><span class="tok-punc">:</span> <span class="tok-str">"Designing a calmer changelog"</span><span class="tok-punc">,</span> <span class="tok-key">"status"</span><span class="tok-punc">:</span> <span class="tok-str">"published"</span><span class="tok-punc">}</span></div>
<div class="code-line"> <span class="tok-punc">]</span></div>
<div class="code-line"><span class="tok-punc">}</span></div>
</x-slot:json>
</x-ui.code-window>
</section>
<section id="endpoints" class="scroll-mt-24 space-y-5">
<div>
<h2 class="text-2xl font-semibold tracking-tight text-gray-900 dark:text-white">Core endpoints</h2>
<p class="mt-2 max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-400">
The mock API is organized around blog post publishing, editorial updates, and post lifecycle events.
</p>
</div>
<div
x-data="{ activeEndpoint: @js($endpoints[0]['key'] ?? '') }"
data-api-endpoint-tabs
class="space-y-5"
>
<div class="overflow-x-auto rounded-2xl border border-gray-200 bg-white p-2 shadow-sm dark:border-gray-800 dark:bg-gray-900" role="tablist" aria-label="API endpoint examples">
<div class="grid min-w-full grid-cols-2 gap-2 md:grid-cols-4">
@foreach ($endpoints as $endpoint)
<button
type="button"
role="tab"
data-endpoint-tab="{{ $endpoint['key'] }}"
x-on:click="activeEndpoint = @js($endpoint['key'])"
x-on:keydown.enter.prevent="activeEndpoint = @js($endpoint['key'])"
x-on:keydown.space.prevent="activeEndpoint = @js($endpoint['key'])"
:aria-selected="activeEndpoint === @js($endpoint['key'])"
:class="activeEndpoint === @js($endpoint['key']) ? 'border-brand-300 bg-brand-50 text-gray-900 shadow-sm dark:border-brand-800 dark:bg-brand-950/40 dark:text-white' : 'border-transparent text-gray-500 hover:bg-gray-50 hover:text-gray-800 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'"
class="flex min-h-14 items-center gap-2 rounded-xl border px-3 py-3 text-left transition cursor-pointer"
>
<x-ui.badge :variant="$methodVariants[$endpoint['method']] ?? 'gray'" size="sm" :pill="false">{{ $endpoint['method'] }}</x-ui.badge>
<span class="text-sm font-semibold">{{ $endpoint['model'] ?? 'Posts' }}</span>
</button>
@endforeach
</div>
</div>
@foreach ($endpoints as $endpoint)
<article
x-show="activeEndpoint === @js($endpoint['key'])"
x-cloak
role="tabpanel"
data-endpoint-panel="{{ $endpoint['key'] }}"
class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-800 dark:bg-gray-900"
>
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<x-ui.badge :variant="$methodVariants[$endpoint['method']] ?? 'gray'" :pill="false">{{ $endpoint['method'] }}</x-ui.badge>
<code class="break-all rounded-md bg-gray-100 px-2 py-1 font-mono text-sm text-gray-800 dark:bg-gray-800 dark:text-gray-100">{{ $endpoint['path'] }}</code>
</div>
<h3 class="mt-4 text-base font-semibold text-gray-900 dark:text-white">{{ $endpoint['title'] }}</h3>
<p class="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-400">{{ $endpoint['description'] }}</p>
</div>
<div class="flex shrink-0 flex-wrap gap-2 sm:justify-end">
<x-ui.badge variant="gray">{{ $endpoint['scope'] }}</x-ui.badge>
<x-ui.badge variant="outline">{{ $endpoint['response'] }}</x-ui.badge>
</div>
</div>
@if (! empty($endpoint['requestExample']))
<div class="mt-5 overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-950/30">
<div class="flex items-center justify-between gap-3 border-b border-gray-200 px-4 py-2.5 dark:border-gray-800">
<div class="flex items-center gap-2 text-xs font-semibold text-gray-700 dark:text-gray-200">
<x-ui.icon name="upload-simple" size="sm" />
Request body
</div>
<span class="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">application/json</span>
</div>
<pre class="{{ $codeExampleClasses }}"><code>@foreach (explode("\n", $endpoint['requestExample']) as $line)<span class="code-line">{!! $highlightCodeLine($line) !!}</span>@endforeach</code></pre>
</div>
@endif
@if (! empty($endpoint['responseExample']))
<div class="{{ ! empty($endpoint['requestExample']) ? 'mt-3' : 'mt-5' }} overflow-hidden rounded-xl border border-gray-200 bg-gray-50 dark:border-gray-800 dark:bg-gray-950/50">
<div class="flex items-center justify-between gap-3 border-b border-gray-200 px-4 py-2.5 dark:border-gray-800">
<div class="flex items-center gap-2 text-xs font-semibold text-gray-700 dark:text-gray-200">
<x-ui.icon name="file-code" size="sm" />
Response example
</div>
<span class="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">{{ $endpoint['response'] }}</span>
</div>
<pre class="{{ $codeExampleClasses }}"><code>@foreach (explode("\n", $endpoint['responseExample']) as $line)<span class="code-line">{!! $highlightCodeLine($line) !!}</span>@endforeach</code></pre>
</div>
@endif
@if (! empty($endpoint['schema']))
<div class="{{ (! empty($endpoint['requestExample']) || ! empty($endpoint['responseExample'])) ? 'mt-3' : 'mt-5' }} overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-950/30">
<div class="flex items-center justify-between gap-3 border-b border-gray-200 px-4 py-2.5 dark:border-gray-800">
<div class="flex items-center gap-2 text-xs font-semibold text-gray-700 dark:text-gray-200">
<x-ui.icon name="list-checks" size="sm" />
Field reference
</div>
<span class="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">schema</span>
</div>
<x-ui.table flush>
<x-slot:head>
<x-ui.table.heading>Key</x-ui.table.heading>
<x-ui.table.heading>Location</x-ui.table.heading>
<x-ui.table.heading>Data type</x-ui.table.heading>
<x-ui.table.heading>Required</x-ui.table.heading>
</x-slot:head>
@foreach ($endpoint['schema'] as $field)
<x-ui.table.row>
<x-ui.table.cell>
<code class="font-mono text-xs text-gray-900 dark:text-white">{{ $field['key'] }}</code>
</x-ui.table.cell>
<x-ui.table.cell>
<x-ui.badge :variant="($field['source'] ?? null) === 'Request' ? 'brand' : 'outline'" size="sm" :pill="false">{{ $field['source'] ?? 'Response' }}</x-ui.badge>
</x-ui.table.cell>
<x-ui.table.cell>
<code class="font-mono text-xs text-gray-600 dark:text-gray-300">{{ $field['type'] }}</code>
</x-ui.table.cell>
<x-ui.table.cell>
@if (($field['source'] ?? null) === 'Request')
<x-ui.badge :variant="($field['required'] ?? false) ? 'green' : 'gray'" size="sm" :pill="false">
{{ ($field['required'] ?? false) ? 'Required' : 'Optional' }}
</x-ui.badge>
@else
<span class="text-gray-400 dark:text-gray-500">-</span>
@endif
</x-ui.table.cell>
</x-ui.table.row>
@endforeach
</x-ui.table>
</div>
@endif
</article>
@endforeach
</div>
</section>
<section id="webhooks" class="scroll-mt-24 space-y-5">
<div>
<h2 class="text-2xl font-semibold tracking-tight text-gray-900 dark:text-white">Subscribe to workspace events</h2>
<p class="mt-2 max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-400">
Webhooks are delivered with an HMAC signature in the Harbour-Signature header and retried for 24 hours.
</p>
</div>
<x-ui.table>
<x-slot:head>
<x-ui.table.heading>Event</x-ui.table.heading>
<x-ui.table.heading>Description</x-ui.table.heading>
</x-slot:head>
@foreach ($webhooks as $webhook)
<x-ui.table.row>
<x-ui.table.cell>
<code class="font-mono text-sm text-gray-900 dark:text-white">{{ $webhook['event'] }}</code>
</x-ui.table.cell>
<x-ui.table.cell muted>{{ $webhook['description'] }}</x-ui.table.cell>
</x-ui.table.row>
@endforeach
</x-ui.table>
</section>
</div>
</div>