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
COPY --from=production-deps /app/node_modules /app/node_modules
COPY --from=build /app/build /app
COPY ./docs /app/docs
EXPOSE 8080
CMD ["node", "./bin/server.js"]

View File

@ -9,15 +9,13 @@ export default class DocsController {
) { }
async list({ }: HttpContext) {
const docs = await this.docsService.getDocs();
return { articles: docs };
return await this.docsService.getDocs();
}
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', {
content,
title: "Documentation"
});
}
}

View File

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

View File

@ -16,6 +16,11 @@ const driveConfig = defineConfig({
routeBasePath: '/storage',
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 { generateUUID } from '~/lib/util'
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 queryClient = new QueryClient()
@ -36,6 +37,7 @@ createInertiaApp({
<TransmitProvider baseUrl={window.location.origin} enableLogging={true}>
<ModalsProvider>
<App {...props} />
<ReactQueryDevtools initialIsOpen={false} />
</ModalsProvider>
</TransmitProvider>
</QueryClientProvider>

View File

@ -9,7 +9,7 @@ interface StyledSidebarProps {
items: Array<{
name: string
href: string
icon: React.ElementType
icon?: React.ElementType
current: boolean
}>
}
@ -25,7 +25,7 @@ const StyledSidebar: React.FC<StyledSidebarProps> = ({ title, items }) => {
const ListItem = (item: {
name: string
href: string
icon: React.ElementType
icon?: React.ElementType
current: boolean
}) => {
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'
)}
>
<item.icon aria-hidden="true" className="size-6 shrink-0" />
{item.icon && <item.icon aria-hidden="true" className="size-6 shrink-0" />}
{item.name}
</a>
</li>

View File

@ -1,35 +1,29 @@
import { Cog6ToothIcon, CommandLineIcon, FolderIcon } from '@heroicons/react/24/outline'
import { useEffect, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import StyledSidebar from '~/components/StyledSidebar'
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 }) {
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
useEffect(() => {
fetchDocs()
}, [])
const items = useMemo(() => {
if (isLoading || !data) return []
async function fetchDocs() {
try {
const data = await api.listDocs()
console.log('Fetched docs:', data)
setDocs(data)
} catch (error) {
console.error('Error fetching docs:', error)
}
}
return data.map((doc) => ({
name: doc.title,
href: `/docs/${doc.slug}`,
current: false,
}))
}, [data, isLoading])
return (
<div className="min-h-screen flex flex-row bg-stone-50/90">
<StyledSidebar title="Documentation" items={navigation} />
<StyledSidebar title="Documentation" items={items} />
{children}
</div>
)

View File

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

View File

@ -2,12 +2,11 @@ import { Head } from '@inertiajs/react'
import MarkdocRenderer from '~/components/MarkdocRenderer'
import DocsLayout from '~/layouts/DocsLayout'
export default function Show({ content, title }: { content: any; title: string }) {
export default function Show({ content }: { content: any; }) {
return (
<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">
<h1 className='font-semibold text-xl'>{title}</h1>
<MarkdocRenderer content={content} />
</div>
</DocsLayout>

View File

@ -28,6 +28,7 @@
"@tabler/icons-react": "^3.34.0",
"@tailwindcss/vite": "^4.1.10",
"@tanstack/react-query": "^5.81.5",
"@tanstack/react-query-devtools": "^5.83.0",
"@tanstack/react-virtual": "^3.13.12",
"@vinejs/vine": "^3.0.1",
"@vitejs/plugin-react": "^4.6.0",
@ -3668,9 +3669,19 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "5.81.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.81.5.tgz",
"integrity": "sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==",
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
"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",
"funding": {
"type": "github",
@ -3678,12 +3689,12 @@
}
},
"node_modules/@tanstack/react-query": {
"version": "5.81.5",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.81.5.tgz",
"integrity": "sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw==",
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz",
"integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.81.5"
"@tanstack/query-core": "5.83.0"
},
"funding": {
"type": "github",
@ -3693,6 +3704,23 @@
"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": {
"version": "3.13.12",
"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",
"@tailwindcss/vite": "^4.1.10",
"@tanstack/react-query": "^5.81.5",
"@tanstack/react-query-devtools": "^5.83.0",
"@tanstack/react-virtual": "^3.13.12",
"@vinejs/vine": "^3.0.1",
"@vitejs/plugin-react": "^4.6.0",

View File

@ -24,10 +24,10 @@ server.errorHandler(() => import('#exceptions/handler'))
*/
server.use([
() => import('#middleware/container_bindings_middleware'),
() => import('@adonisjs/static/static_middleware'),
() => import('@adonisjs/cors/cors_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');
};