]> BookStack Code Mirror - bookstack/commitdiff
API: Re-ordered routes, Improved navigation
authorDan Brown <redacted>
Sun, 2 Nov 2025 14:29:00 +0000 (14:29 +0000)
committerDan Brown <redacted>
Sun, 2 Nov 2025 14:31:08 +0000 (14:31 +0000)
Updated route order to follow some kind of logic.
Updated scrolling sidebar to not be so cut-off in various scenarios.
Added new nav helper to quick jump to specific API models.

Closes #5865

resources/js/components/api-nav.ts [new file with mode: 0644]
resources/js/components/index.ts
resources/sass/_blocks.scss
resources/views/api-docs/index.blade.php
routes/api.php

diff --git a/resources/js/components/api-nav.ts b/resources/js/components/api-nav.ts
new file mode 100644 (file)
index 0000000..a84c1cb
--- /dev/null
@@ -0,0 +1,32 @@
+import {Component} from "./component";
+
+export class ApiNav extends Component {
+    private select!: HTMLSelectElement;
+    private sidebar!: HTMLElement;
+    private body!: HTMLElement;
+
+    setup() {
+        this.select = this.$refs.select as HTMLSelectElement;
+        this.sidebar = this.$refs.sidebar;
+        this.body = this.$el.ownerDocument.documentElement;
+        this.select.addEventListener('change', () => {
+            const section = this.select.value;
+            const sidebarTarget = document.getElementById(`sidebar-header-${section}`);
+            const contentTarget = document.getElementById(`section-${section}`);
+            if (sidebarTarget && contentTarget) {
+
+                const sidebarPos = sidebarTarget.getBoundingClientRect().top - this.sidebar.getBoundingClientRect().top + this.sidebar.scrollTop;
+                this.sidebar.scrollTo({
+                    top: sidebarPos - 120,
+                    behavior: 'smooth',
+                });
+
+                const bodyPos = contentTarget.getBoundingClientRect().top + this.body.scrollTop;
+                this.body.scrollTo({
+                    top: bodyPos - 20,
+                    behavior: 'smooth',
+                });
+            }
+        });
+    }
+}
\ No newline at end of file
index 63e1ad0dbf79eab21d5cd52ebe97c7dc96448676..736d93f05959209b66b211211b8fe93063a2de20 100644 (file)
@@ -1,6 +1,7 @@
 export {AddRemoveRows} from './add-remove-rows';
 export {AjaxDeleteRow} from './ajax-delete-row';
 export {AjaxForm} from './ajax-form';
+export {ApiNav} from './api-nav';
 export {Attachments} from './attachments';
 export {AttachmentsList} from './attachments-list';
 export {AutoSuggest} from './auto-suggest';
index 8c248caee141a82020313d2113f9b0b82d3de69b..4857c2ceee5ee750ef807d64c79774016df360f7 100644 (file)
 
 .sticky-sidebar {
   position: sticky;
-  top: vars.$m;
-  max-height: calc(100vh - #{vars.$m});
+  top: 0;
+  padding-left: 2px;
+  max-height: calc(100vh);
   overflow-y: auto;
+  .sticky-sidebar-header {
+    position: sticky;
+    top: 0;
+    background: #F2F2F2;
+    background: linear-gradient(180deg,rgba(242, 242, 242, 1) 66%, rgba(242, 242, 242, 0) 100%);
+    z-index: 4;
+  }
+}
+.dark-mode .sticky-sidebar-header {
+  background: #111;
+  background: linear-gradient(180deg,rgba(17, 17, 17, 1) 66%, rgba(17, 17, 17, 0) 100%);
 }
index 84c6d21acfbcba9279bf3340765edb8850150e56..c331f070784c55f768fe4f5ff0b3555a740bb7a4 100644 (file)
@@ -2,48 +2,65 @@
 
 @section('body')
 
-    <div class="container pt-xl">
+    <div component="api-nav" class="container">
 
         <div class="grid right-focus reverse-collapse">
             <div>
 
-                <div class="sticky-sidebar">
-                    <p class="text-uppercase text-muted mb-xm mt-l"><strong>Getting Started</strong></p>
+                <div refs="api-nav@sidebar" class="sticky-sidebar">
 
-                    <div class="text-mono">
-                        <div class="mb-xs"><a href="#authentication">Authentication</a></div>
-                        <div class="mb-xs"><a href="#request-format">Request Format</a></div>
-                        <div class="mb-xs"><a href="#listing-endpoints">Listing Endpoints</a></div>
-                        <div class="mb-xs"><a href="#error-handling">Error Handling</a></div>
-                        <div class="mb-xs"><a href="#rate-limits">Rate Limits</a></div>
-                        <div class="mb-xs"><a href="#content-security">Content Security</a></div>
+                    <div class="sticky-sidebar-header py-xl">
+                        <select refs="api-nav@select" name="navigation" id="navigation">
+                            <option value="getting-started" selected>Jump To Section</option>
+                            <option value="getting-started">Getting Started</option>
+                            @foreach($docs as $model => $endpoints)
+                                <option value="{{ str_replace(' ', '-', $model) }}">{{ ucfirst($model) }}</option>
+                                @if($model === 'docs' || $model === 'shelves')
+                                    <hr>
+                                @endif
+                            @endforeach
+                        </select>
+                    </div>
+
+                    <div class="mb-xl">
+                        <p id="sidebar-header-getting-started" class="text-uppercase text-muted mb-xm"><strong>Getting Started</strong></p>
+                        <div class="text-mono">
+                            <div class="mb-xs"><a href="#authentication">Authentication</a></div>
+                            <div class="mb-xs"><a href="#request-format">Request Format</a></div>
+                            <div class="mb-xs"><a href="#listing-endpoints">Listing Endpoints</a></div>
+                            <div class="mb-xs"><a href="#error-handling">Error Handling</a></div>
+                            <div class="mb-xs"><a href="#rate-limits">Rate Limits</a></div>
+                            <div class="mb-xs"><a href="#content-security">Content Security</a></div>
+                        </div>
                     </div>
 
                     @foreach($docs as $model => $endpoints)
-                        <p class="text-uppercase text-muted mb-xm mt-l"><strong>{{ $model }}</strong></p>
+                        <div class="mb-xl">
+                            <p id="sidebar-header-{{ str_replace(' ', '-', $model) }}" class="text-uppercase text-muted mb-xm"><strong>{{ $model }}</strong></p>
 
-                        @foreach($endpoints as $endpoint)
-                            <div class="mb-xs">
-                                <a href="#{{ $endpoint['name'] }}" class="text-mono inline block mr-s">
-                                    <span class="api-method" data-method="{{ $endpoint['method'] }}">{{ $endpoint['method'] }}</span>
-                                </a>
-                                <a href="#{{ $endpoint['name'] }}" class="text-mono">
-                                    {{ $endpoint['controller_method_kebab'] }}
-                                </a>
-                            </div>
-                        @endforeach
+                            @foreach($endpoints as $endpoint)
+                                <div class="mb-xs">
+                                    <a href="#{{ $endpoint['name'] }}" class="text-mono inline block mr-s">
+                                        <span class="api-method" data-method="{{ $endpoint['method'] }}">{{ $endpoint['method'] }}</span>
+                                    </a>
+                                    <a href="#{{ $endpoint['name'] }}" class="text-mono">
+                                        {{ $endpoint['controller_method_kebab'] }}
+                                    </a>
+                                </div>
+                            @endforeach
+                        </div>
                     @endforeach
                 </div>
             </div>
 
-            <div style="overflow: auto;">
+            <div class="pt-xl" style="overflow: auto;">
 
-                <section component="code-highlighter" class="card content-wrap auto-height">
+                <section id="section-getting-started" component="code-highlighter" class="card content-wrap auto-height">
                     @include('api-docs.parts.getting-started')
                 </section>
 
                 @foreach($docs as $model => $endpoints)
-                    <section class="card content-wrap auto-height">
+                    <section id="section-{{ str_replace(' ', '-', $model) }}" class="card content-wrap auto-height">
                         <h1 class="list-heading text-capitals">{{ $model }}</h1>
                         @if($endpoints[0]['model_description'])
                             <p>{{ $endpoints[0]['model_description'] }}</p>
index 1466e638cb5924fc06a2275ceb4167c49ec28842..308a95d8c28dd15d6db7ece8faae897e7c316baa 100644 (file)
@@ -2,7 +2,7 @@
 
 /**
  * Routes for the BookStack API.
- * Routes have a uri prefix of /api/.
+ * Routes have a URI prefix of /api/.
  * Controllers all end with "ApiController"
  */
 
@@ -19,25 +19,18 @@ use BookStack\Users\Controllers\RoleApiController;
 use BookStack\Users\Controllers\UserApiController;
 use Illuminate\Support\Facades\Route;
 
-Route::get('docs.json', [ApiDocsController::class, 'json']);
-
-Route::get('attachments', [AttachmentApiController::class, 'list']);
-Route::post('attachments', [AttachmentApiController::class, 'create']);
-Route::get('attachments/{id}', [AttachmentApiController::class, 'read']);
-Route::put('attachments/{id}', [AttachmentApiController::class, 'update']);
-Route::delete('attachments/{id}', [AttachmentApiController::class, 'delete']);
+// Main Entity Routes
 
-Route::get('books', [EntityControllers\BookApiController::class, 'list']);
-Route::post('books', [EntityControllers\BookApiController::class, 'create']);
-Route::get('books/{id}', [EntityControllers\BookApiController::class, 'read']);
-Route::put('books/{id}', [EntityControllers\BookApiController::class, 'update']);
-Route::delete('books/{id}', [EntityControllers\BookApiController::class, 'delete']);
-
-Route::get('books/{id}/export/html', [ExportControllers\BookExportApiController::class, 'exportHtml']);
-Route::get('books/{id}/export/pdf', [ExportControllers\BookExportApiController::class, 'exportPdf']);
-Route::get('books/{id}/export/plaintext', [ExportControllers\BookExportApiController::class, 'exportPlainText']);
-Route::get('books/{id}/export/markdown', [ExportControllers\BookExportApiController::class, 'exportMarkdown']);
-Route::get('books/{id}/export/zip', [ExportControllers\BookExportApiController::class, 'exportZip']);
+Route::get('pages', [EntityControllers\PageApiController::class, 'list']);
+Route::post('pages', [EntityControllers\PageApiController::class, 'create']);
+Route::get('pages/{id}', [EntityControllers\PageApiController::class, 'read']);
+Route::put('pages/{id}', [EntityControllers\PageApiController::class, 'update']);
+Route::delete('pages/{id}', [EntityControllers\PageApiController::class, 'delete']);
+Route::get('pages/{id}/export/html', [ExportControllers\PageExportApiController::class, 'exportHtml']);
+Route::get('pages/{id}/export/pdf', [ExportControllers\PageExportApiController::class, 'exportPdf']);
+Route::get('pages/{id}/export/plaintext', [ExportControllers\PageExportApiController::class, 'exportPlainText']);
+Route::get('pages/{id}/export/markdown', [ExportControllers\PageExportApiController::class, 'exportMarkdown']);
+Route::get('pages/{id}/export/zip', [ExportControllers\PageExportApiController::class, 'exportZip']);
 
 Route::get('chapters', [EntityControllers\ChapterApiController::class, 'list']);
 Route::post('chapters', [EntityControllers\ChapterApiController::class, 'create']);
@@ -50,27 +43,32 @@ Route::get('chapters/{id}/export/plaintext', [ExportControllers\ChapterExportApi
 Route::get('chapters/{id}/export/markdown', [ExportControllers\ChapterExportApiController::class, 'exportMarkdown']);
 Route::get('chapters/{id}/export/zip', [ExportControllers\ChapterExportApiController::class, 'exportZip']);
 
-Route::get('pages', [EntityControllers\PageApiController::class, 'list']);
-Route::post('pages', [EntityControllers\PageApiController::class, 'create']);
-Route::get('pages/{id}', [EntityControllers\PageApiController::class, 'read']);
-Route::put('pages/{id}', [EntityControllers\PageApiController::class, 'update']);
-Route::delete('pages/{id}', [EntityControllers\PageApiController::class, 'delete']);
+Route::get('books', [EntityControllers\BookApiController::class, 'list']);
+Route::post('books', [EntityControllers\BookApiController::class, 'create']);
+Route::get('books/{id}', [EntityControllers\BookApiController::class, 'read']);
+Route::put('books/{id}', [EntityControllers\BookApiController::class, 'update']);
+Route::delete('books/{id}', [EntityControllers\BookApiController::class, 'delete']);
+Route::get('books/{id}/export/html', [ExportControllers\BookExportApiController::class, 'exportHtml']);
+Route::get('books/{id}/export/pdf', [ExportControllers\BookExportApiController::class, 'exportPdf']);
+Route::get('books/{id}/export/plaintext', [ExportControllers\BookExportApiController::class, 'exportPlainText']);
+Route::get('books/{id}/export/markdown', [ExportControllers\BookExportApiController::class, 'exportMarkdown']);
+Route::get('books/{id}/export/zip', [ExportControllers\BookExportApiController::class, 'exportZip']);
 
-Route::get('pages/{id}/export/html', [ExportControllers\PageExportApiController::class, 'exportHtml']);
-Route::get('pages/{id}/export/pdf', [ExportControllers\PageExportApiController::class, 'exportPdf']);
-Route::get('pages/{id}/export/plaintext', [ExportControllers\PageExportApiController::class, 'exportPlainText']);
-Route::get('pages/{id}/export/markdown', [ExportControllers\PageExportApiController::class, 'exportMarkdown']);
-Route::get('pages/{id}/export/zip', [ExportControllers\PageExportApiController::class, 'exportZip']);
+Route::get('shelves', [EntityControllers\BookshelfApiController::class, 'list']);
+Route::post('shelves', [EntityControllers\BookshelfApiController::class, 'create']);
+Route::get('shelves/{id}', [EntityControllers\BookshelfApiController::class, 'read']);
+Route::put('shelves/{id}', [EntityControllers\BookshelfApiController::class, 'update']);
+Route::delete('shelves/{id}', [EntityControllers\BookshelfApiController::class, 'delete']);
 
-Route::get('image-gallery', [ImageGalleryApiController::class, 'list']);
-Route::post('image-gallery', [ImageGalleryApiController::class, 'create']);
-Route::get('image-gallery/url/data', [ImageGalleryApiController::class, 'readDataForUrl']);
-Route::get('image-gallery/{id}', [ImageGalleryApiController::class, 'read']);
-Route::get('image-gallery/{id}/data', [ImageGalleryApiController::class, 'readData']);
-Route::put('image-gallery/{id}', [ImageGalleryApiController::class, 'update']);
-Route::delete('image-gallery/{id}', [ImageGalleryApiController::class, 'delete']);
+// Additional Model Routes, in alphabetical order
 
-Route::get('search', [SearchApiController::class, 'all']);
+Route::get('attachments', [AttachmentApiController::class, 'list']);
+Route::post('attachments', [AttachmentApiController::class, 'create']);
+Route::get('attachments/{id}', [AttachmentApiController::class, 'read']);
+Route::put('attachments/{id}', [AttachmentApiController::class, 'update']);
+Route::delete('attachments/{id}', [AttachmentApiController::class, 'delete']);
+
+Route::get('audit-log', [ActivityControllers\AuditLogApiController::class, 'list']);
 
 Route::get('comments', [ActivityControllers\CommentApiController::class, 'list']);
 Route::post('comments', [ActivityControllers\CommentApiController::class, 'create']);
@@ -78,23 +76,18 @@ Route::get('comments/{id}', [ActivityControllers\CommentApiController::class, 'r
 Route::put('comments/{id}', [ActivityControllers\CommentApiController::class, 'update']);
 Route::delete('comments/{id}', [ActivityControllers\CommentApiController::class, 'delete']);
 
-Route::get('shelves', [EntityControllers\BookshelfApiController::class, 'list']);
-Route::post('shelves', [EntityControllers\BookshelfApiController::class, 'create']);
-Route::get('shelves/{id}', [EntityControllers\BookshelfApiController::class, 'read']);
-Route::put('shelves/{id}', [EntityControllers\BookshelfApiController::class, 'update']);
-Route::delete('shelves/{id}', [EntityControllers\BookshelfApiController::class, 'delete']);
+Route::get('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'read']);
+Route::put('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'update']);
 
-Route::get('users', [UserApiController::class, 'list']);
-Route::post('users', [UserApiController::class, 'create']);
-Route::get('users/{id}', [UserApiController::class, 'read']);
-Route::put('users/{id}', [UserApiController::class, 'update']);
-Route::delete('users/{id}', [UserApiController::class, 'delete']);
+Route::get('docs.json', [ApiDocsController::class, 'json']);
 
-Route::get('roles', [RoleApiController::class, 'list']);
-Route::post('roles', [RoleApiController::class, 'create']);
-Route::get('roles/{id}', [RoleApiController::class, 'read']);
-Route::put('roles/{id}', [RoleApiController::class, 'update']);
-Route::delete('roles/{id}', [RoleApiController::class, 'delete']);
+Route::get('image-gallery', [ImageGalleryApiController::class, 'list']);
+Route::post('image-gallery', [ImageGalleryApiController::class, 'create']);
+Route::get('image-gallery/url/data', [ImageGalleryApiController::class, 'readDataForUrl']);
+Route::get('image-gallery/{id}', [ImageGalleryApiController::class, 'read']);
+Route::get('image-gallery/{id}/data', [ImageGalleryApiController::class, 'readData']);
+Route::put('image-gallery/{id}', [ImageGalleryApiController::class, 'update']);
+Route::delete('image-gallery/{id}', [ImageGalleryApiController::class, 'delete']);
 
 Route::get('imports', [ExportControllers\ImportApiController::class, 'list']);
 Route::post('imports', [ExportControllers\ImportApiController::class, 'create']);
@@ -106,9 +99,18 @@ Route::get('recycle-bin', [EntityControllers\RecycleBinApiController::class, 'li
 Route::put('recycle-bin/{deletionId}', [EntityControllers\RecycleBinApiController::class, 'restore']);
 Route::delete('recycle-bin/{deletionId}', [EntityControllers\RecycleBinApiController::class, 'destroy']);
 
-Route::get('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'read']);
-Route::put('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'update']);
+Route::get('roles', [RoleApiController::class, 'list']);
+Route::post('roles', [RoleApiController::class, 'create']);
+Route::get('roles/{id}', [RoleApiController::class, 'read']);
+Route::put('roles/{id}', [RoleApiController::class, 'update']);
+Route::delete('roles/{id}', [RoleApiController::class, 'delete']);
 
-Route::get('audit-log', [ActivityControllers\AuditLogApiController::class, 'list']);
+Route::get('search', [SearchApiController::class, 'all']);
 
 Route::get('system', [SystemApiController::class, 'read']);
+
+Route::get('users', [UserApiController::class, 'list']);
+Route::post('users', [UserApiController::class, 'create']);
+Route::get('users/{id}', [UserApiController::class, 'read']);
+Route::put('users/{id}', [UserApiController::class, 'update']);
+Route::delete('users/{id}', [UserApiController::class, 'delete']);