fix(Docs): fix doc rendering

This commit is contained in:
Jake Turner 2025-07-11 15:31:07 -07:00
parent 97655ef75d
commit 44b7bfee16
14 changed files with 199 additions and 58 deletions

View File

@ -28,5 +28,6 @@ ENV NODE_ENV=production
WORKDIR /app WORKDIR /app
COPY --from=production-deps /app/node_modules /app/node_modules COPY --from=production-deps /app/node_modules /app/node_modules
COPY --from=build /app/build /app COPY --from=build /app/build /app
COPY ./docs /app/docs
EXPOSE 8080 EXPOSE 8080
CMD ["node", "./bin/server.js"] CMD ["node", "./bin/server.js"]

View File

@ -9,15 +9,13 @@ export default class DocsController {
) { } ) { }
async list({ }: HttpContext) { async list({ }: HttpContext) {
const docs = await this.docsService.getDocs(); return await this.docsService.getDocs();
return { articles: docs };
} }
async show({ params, inertia }: HttpContext) { async show({ params, inertia }: HttpContext) {
const content = await this.docsService.parseFile(`${params.slug}.md`); const content = await this.docsService.parseFile(params.slug);
return inertia.render('docs/show', { return inertia.render('docs/show', {
content, content,
title: "Documentation"
}); });
} }
} }

View File

@ -1,17 +1,28 @@
import drive from '@adonisjs/drive/services/main';
import Markdoc from '@markdoc/markdoc'; import Markdoc from '@markdoc/markdoc';
import { readdir, readFile } from 'node:fs/promises'; import { streamToString } from '../../util/docs.js';
import { join } from 'node:path';
export class DocsService { export class DocsService {
async getDocs() { async getDocs() {
const docsPath = join(process.cwd(), '/docs'); const disk = drive.use('docs');
console.log(`Resolving docs path: ${docsPath}`); if (!disk) {
throw new Error('Docs disk not configured');
}
const files = await readdir(docsPath, { withFileTypes: true }); const contents = await disk.listAll('/');
const docs = files const files: Array<{ title: string; slug: string }> = [];
.filter(file => file.isFile() && file.name.endsWith('.md'))
.map(file => file.name); for (const item of contents.objects) {
return docs; if (item.isFile && item.name.endsWith('.md')) {
const cleaned = this.prettify(item.name);
files.push({
title: cleaned,
slug: item.name.replace(/\.md$/, '')
});
}
}
return files.sort((a, b) => a.title.localeCompare(b.title));
} }
parse(content: string) { parse(content: string) {
@ -26,13 +37,36 @@ export class DocsService {
return Markdoc.transform(ast, config); return Markdoc.transform(ast, config);
} }
async parseFile(filename: string) { async parseFile(_filename: string) {
const fullPath = join(process.cwd(), '/docs', filename); const disk = drive.use('docs');
console.log(`Resolving file path: ${fullPath}`); if (!disk) {
const content = await readFile(fullPath, 'utf-8') throw new Error('Docs disk not configured');
}
if (!_filename) {
throw new Error('Filename is required');
}
const filename = _filename.endsWith('.md') ? _filename : `${_filename}.md`;
const fileExists = await disk.exists(filename);
if (!fileExists) {
throw new Error(`File not found: ${filename}`);
}
const fileStream = await disk.getStream(filename);
if (!fileStream) {
throw new Error(`Failed to read file stream: ${filename}`);
}
const content = await streamToString(fileStream);
return this.parse(content); return this.parse(content);
} }
private prettify(filename: string) {
const cleaned = filename.replace(/_/g, ' ').replace(/\.md$/, '');
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
}
private getConfig() { private getConfig() {
return { return {
tags: { tags: {

View File

@ -16,6 +16,11 @@ const driveConfig = defineConfig({
routeBasePath: '/storage', routeBasePath: '/storage',
visibility: 'public', visibility: 'public',
}), }),
docs: services.fs({
location: app.makePath('docs'),
serveFiles: false, // Don't serve files directly - we handle this via routes/Inertia
visibility: 'public',
}),
}, },
}) })

View File

@ -1 +1,71 @@
# This is a markdown file! # Lorem Ipsum Markdown Showcase
---
## Introduction
This document serves as a comprehensive example of **Markdown's various formatting possibilities**, using the classic *Lorem Ipsum* text as its content. From basic text styling to lists, code blocks, and tables, you'll find a demonstration of common Markdown features here.
---
## Basic Text Formatting
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
* This text is **bold**.
* This text is *italic*.
* This text is ***bold and italic***.
* This text is ~~struck through~~.
* You can also use `backticks` for `inline code`.
---
## Headers
Markdown supports up to six levels of headers.
# Header 1
## Header 2
### Header 3
#### Header 4
##### Header 5
###### Header 6
---
## Lists
### Unordered List
* Lorem ipsum dolor sit amet.
* Consectetur adipiscing elit.
* Sed do eiusmod tempor.
* Incididunt ut labore et dolore magna.
* Aliqua ut enim ad minim veniam.
### Ordered List
1. Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
2. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
3. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
---
## Blockquotes
> "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
>
> — John Doe, *Lorem Ipsum Anthology*
---
## Code Blocks
```python
def fibonacci(n):
a, b = 0, 1
for i in range(n):
print(a, end=" ")
a, b = b, a + b
fibonacci(10)

View File

@ -9,6 +9,7 @@ import ModalsProvider from '~/providers/ModalProvider'
import { TransmitProvider } from 'react-adonis-transmit' import { TransmitProvider } from 'react-adonis-transmit'
import { generateUUID } from '~/lib/util' import { generateUUID } from '~/lib/util'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const appName = import.meta.env.VITE_APP_NAME || 'Project N.O.M.A.D.' const appName = import.meta.env.VITE_APP_NAME || 'Project N.O.M.A.D.'
const queryClient = new QueryClient() const queryClient = new QueryClient()
@ -36,6 +37,7 @@ createInertiaApp({
<TransmitProvider baseUrl={window.location.origin} enableLogging={true}> <TransmitProvider baseUrl={window.location.origin} enableLogging={true}>
<ModalsProvider> <ModalsProvider>
<App {...props} /> <App {...props} />
<ReactQueryDevtools initialIsOpen={false} />
</ModalsProvider> </ModalsProvider>
</TransmitProvider> </TransmitProvider>
</QueryClientProvider> </QueryClientProvider>

View File

@ -9,7 +9,7 @@ interface StyledSidebarProps {
items: Array<{ items: Array<{
name: string name: string
href: string href: string
icon: React.ElementType icon?: React.ElementType
current: boolean current: boolean
}> }>
} }
@ -25,7 +25,7 @@ const StyledSidebar: React.FC<StyledSidebarProps> = ({ title, items }) => {
const ListItem = (item: { const ListItem = (item: {
name: string name: string
href: string href: string
icon: React.ElementType icon?: React.ElementType
current: boolean current: boolean
}) => { }) => {
return ( return (
@ -39,7 +39,7 @@ const StyledSidebar: React.FC<StyledSidebarProps> = ({ title, items }) => {
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold' 'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold'
)} )}
> >
<item.icon aria-hidden="true" className="size-6 shrink-0" /> {item.icon && <item.icon aria-hidden="true" className="size-6 shrink-0" />}
{item.name} {item.name}
</a> </a>
</li> </li>

View File

@ -1,35 +1,29 @@
import { Cog6ToothIcon, CommandLineIcon, FolderIcon } from '@heroicons/react/24/outline' import { useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react' import { useMemo } from 'react'
import StyledSidebar from '~/components/StyledSidebar' import StyledSidebar from '~/components/StyledSidebar'
import api from '~/lib/api' import api from '~/lib/api'
const navigation = [
{ name: 'Apps', href: '/settings/apps', icon: CommandLineIcon, current: false },
{ name: 'ZIM Explorer', href: '/settings/zim', icon: FolderIcon, current: false },
{ name: 'System', href: '/settings/system', icon: Cog6ToothIcon, current: true },
]
export default function DocsLayout({ children }: { children: React.ReactNode }) { export default function DocsLayout({ children }: { children: React.ReactNode }) {
const [docs, setDocs] = useState<Array<{ title: string; slug: string }>>([]) const { data, isLoading } = useQuery<Array<{ title: string; slug: string }>>({
queryKey: ['docs'],
queryFn: () => api.listDocs(),
refetchOnWindowFocus: false,
staleTime: Infinity,
})
// Fetch docs when the component mounts const items = useMemo(() => {
useEffect(() => { if (isLoading || !data) return []
fetchDocs()
}, [])
async function fetchDocs() { return data.map((doc) => ({
try { name: doc.title,
const data = await api.listDocs() href: `/docs/${doc.slug}`,
console.log('Fetched docs:', data) current: false,
setDocs(data) }))
} catch (error) { }, [data, isLoading])
console.error('Error fetching docs:', error)
}
}
return ( return (
<div className="min-h-screen flex flex-row bg-stone-50/90"> <div className="min-h-screen flex flex-row bg-stone-50/90">
<StyledSidebar title="Documentation" items={navigation} /> <StyledSidebar title="Documentation" items={items} />
{children} {children}
</div> </div>
) )

View File

@ -15,8 +15,8 @@ class API {
async listDocs() { async listDocs() {
try { try {
const response = await this.client.get<{ articles: Array<{ title: string; slug: string }> }>("/docs/list"); const response = await this.client.get<Array<{ title: string; slug: string }>>("/docs/list");
return response.data.articles; return response.data;
} catch (error) { } catch (error) {
console.error("Error listing docs:", error); console.error("Error listing docs:", error);
throw error; throw error;

View File

@ -2,12 +2,11 @@ import { Head } from '@inertiajs/react'
import MarkdocRenderer from '~/components/MarkdocRenderer' import MarkdocRenderer from '~/components/MarkdocRenderer'
import DocsLayout from '~/layouts/DocsLayout' import DocsLayout from '~/layouts/DocsLayout'
export default function Show({ content, title }: { content: any; title: string }) { export default function Show({ content }: { content: any; }) {
return ( return (
<DocsLayout> <DocsLayout>
<Head title={`${title} | Documentation | Project N.O.M.A.D.`} /> <Head title={'Documentation | Project N.O.M.A.D.'} />
<div className="xl:pl-80 py-6"> <div className="xl:pl-80 py-6">
<h1 className='font-semibold text-xl'>{title}</h1>
<MarkdocRenderer content={content} /> <MarkdocRenderer content={content} />
</div> </div>
</DocsLayout> </DocsLayout>

View File

@ -28,6 +28,7 @@
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.34.0",
"@tailwindcss/vite": "^4.1.10", "@tailwindcss/vite": "^4.1.10",
"@tanstack/react-query": "^5.81.5", "@tanstack/react-query": "^5.81.5",
"@tanstack/react-query-devtools": "^5.83.0",
"@tanstack/react-virtual": "^3.13.12", "@tanstack/react-virtual": "^3.13.12",
"@vinejs/vine": "^3.0.1", "@vinejs/vine": "^3.0.1",
"@vitejs/plugin-react": "^4.6.0", "@vitejs/plugin-react": "^4.6.0",
@ -3668,9 +3669,19 @@
} }
}, },
"node_modules/@tanstack/query-core": { "node_modules/@tanstack/query-core": {
"version": "5.81.5", "version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.81.5.tgz", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
"integrity": "sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==", "integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-devtools": {
"version": "5.81.2",
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.81.2.tgz",
"integrity": "sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
@ -3678,12 +3689,12 @@
} }
}, },
"node_modules/@tanstack/react-query": { "node_modules/@tanstack/react-query": {
"version": "5.81.5", "version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.81.5.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz",
"integrity": "sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw==", "integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/query-core": "5.81.5" "@tanstack/query-core": "5.83.0"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@ -3693,6 +3704,23 @@
"react": "^18 || ^19" "react": "^18 || ^19"
} }
}, },
"node_modules/@tanstack/react-query-devtools": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.83.0.tgz",
"integrity": "sha512-yfp8Uqd3I1jgx8gl0lxbSSESu5y4MO2ThOPBnGNTYs0P+ZFu+E9g5IdOngyUGuo6Uz6Qa7p9TLdZEX3ntik2fQ==",
"license": "MIT",
"dependencies": {
"@tanstack/query-devtools": "5.81.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/react-query": "^5.83.0",
"react": "^18 || ^19"
}
},
"node_modules/@tanstack/react-virtual": { "node_modules/@tanstack/react-virtual": {
"version": "3.13.12", "version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",

View File

@ -74,6 +74,7 @@
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.34.0",
"@tailwindcss/vite": "^4.1.10", "@tailwindcss/vite": "^4.1.10",
"@tanstack/react-query": "^5.81.5", "@tanstack/react-query": "^5.81.5",
"@tanstack/react-query-devtools": "^5.83.0",
"@tanstack/react-virtual": "^3.13.12", "@tanstack/react-virtual": "^3.13.12",
"@vinejs/vine": "^3.0.1", "@vinejs/vine": "^3.0.1",
"@vitejs/plugin-react": "^4.6.0", "@vitejs/plugin-react": "^4.6.0",

View File

@ -24,10 +24,10 @@ server.errorHandler(() => import('#exceptions/handler'))
*/ */
server.use([ server.use([
() => import('#middleware/container_bindings_middleware'), () => import('#middleware/container_bindings_middleware'),
() => import('@adonisjs/static/static_middleware'),
() => import('@adonisjs/cors/cors_middleware'), () => import('@adonisjs/cors/cors_middleware'),
() => import('@adonisjs/vite/vite_middleware'), () => import('@adonisjs/vite/vite_middleware'),
() => import('@adonisjs/inertia/inertia_middleware') () => import('@adonisjs/inertia/inertia_middleware'),
() => import('@adonisjs/static/static_middleware')
]) ])
/** /**

9
admin/util/docs.ts Normal file
View File

@ -0,0 +1,9 @@
import { Readable } from 'stream';
export const streamToString = async (stream: Readable): Promise<string> => {
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(Buffer.from(chunk));
}
return Buffer.concat(chunks).toString('utf-8');
};