Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
# @nuxtus/generator
# @resultcrafter/nuxtus-generator

Takes a Directus collection object and creates a Nuxt page or pages.
Fork of [@nuxtus/generator](https://github.com/nuxtus/generator) with upgraded page templates.

For more details visit [nuxtus.com](https://nuxtus.com) or [read the documentation](https://docs.nuxtus.com)
Takes a Directus collection object and creates styled Nuxt pages with Tailwind CSS, smart field detection, and UUID routing.

## What's Updated (from @nuxtus/generator)

- **Styled templates**: All three page templates (list, detail, singleton) now produce Tailwind-styled output with `max-w-3xl` centered layout
- **Smart field detection**: Templates automatically detect common fields (`title` > `name` > `id`, `excerpt` > `description`, `date_created`, `content`)
- **UUID routing**: Detail pages use UUID-based routing (`/collection/:id`)
- **Prose styles**: HTML content rendered with scoped prose CSS (headings, lists, blockquotes, code blocks, images)
- **Not-found handling**: Detail page shows "Not found" with back navigation when item doesn't exist
- **Unique async keys**: Each template uses unique `useAsyncData` keys (`{collection}-list`, `{collection}-detail`, `{collection}-singleton`)
- **Singleton fix**: Uses `$readItems()` instead of `$readSingleton()` for SDK v21 compatibility

## Usage

```bash
$ npm install @nuxtus/generator
```bash
$ npm install @resultcrafter/nuxtus-generator
```

Then in your application:

```typescript
import { createPage } from "@nuxtus/generator"
import { createPage } from "@resultcrafter/nuxtus-generator"

createPage("collection-name", isSingleton)
```

## Templates

- `index.njk.vue` — Collection listing with styled cards, links, dates, and excerpts
- `individual.njk.vue` — Item detail with back nav, title, date, prose content, and not-found fallback
- `singleton.njk.vue` — Singleton page with title, description, and prose content
7,699 changes: 7,695 additions & 4 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nuxtus/generator",
"version": "1.9.4",
"name": "@resultcrafter/nuxtus-generator",
"version": "1.9.5",
"description": "Package responsible for generating a Nuxt page from Directus collection.",
"main": "dist/generator.es.js",
"module": "dist/generator.es.js",
Expand All @@ -16,7 +16,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/nuxtus/generator.git"
"url": "git+https://github.com/resultcrafter/resultcrafter-nuxtus-generator.git"
},
"publishConfig": {
"access": "public",
Expand All @@ -30,9 +30,9 @@
"author": "Craig Harman",
"license": "MIT",
"bugs": {
"url": "https://github.com/nuxtus/generator/issues"
"url": "https://github.com/resultcrafter/resultcrafter-nuxtus-generator/issues"
},
"homepage": "https://github.com/nuxtus/generator#readme",
"homepage": "https://github.com/resultcrafter/resultcrafter-nuxtus-generator#readme",
"devDependencies": {
"@babel/core": "7.29.0",
"@babel/preset-env": "7.29.2",
Expand Down
46 changes: 40 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
AuthenticationClient,
DirectusClient,
RestClient,
StaticTokenClient,
authentication,
createDirectus,
readCollections,
Expand All @@ -19,7 +20,8 @@ export type Schema = {} // TODO: Not sure we actually will every use the Schema

export type Directus = DirectusClient<Schema> &
AuthenticationClient<Schema> &
RestClient<Schema>
RestClient<Schema> &
StaticTokenClient<Schema>

export default class Generator {
chalk = Chalk
Expand Down Expand Up @@ -53,11 +55,18 @@ export default class Generator {
)
.with(rest())
.with(authentication())

this.login()
}

public async login(): Promise<void> {
const staticTokenValue = process.env.NUXTUS_DIRECTUS_STATIC_TOKEN
if (staticTokenValue) {
this.directus = createDirectus(
process.env.DIRECTUS_URL || "http://localhost:8055"
)
.with(rest())
.with(staticToken(staticTokenValue))
return
}
await login(this.directus, this.chalk)
}

Expand All @@ -78,14 +87,39 @@ export default class Generator {
}

public async getCollections(): Promise<unknown> {
// TODO: THis return type is not unknown!
await this.login() // Need to be logged in as admin to get all collections
return this.directus.request(readCollections())
await this.login()
const result: any = await this.directus.request(readCollections())
return Array.isArray(result) ? result : (result.data || result)
}

public async generateStaticToken(): Promise<string> {
await this.login()
const token = nanoid()
const accessToken = await this.directus.getToken()
const meResp = await fetch(
process.env.DIRECTUS_URL + "/users/me",
{ headers: { Authorization: "Bearer " + accessToken } }
)
const meData = await meResp.json()
const userId = meData.data.id
const resp = await fetch(
process.env.DIRECTUS_URL + "/users/" + userId,
{
method: "PATCH",
headers: {
Authorization: "Bearer " + accessToken,
"Content-Type": "application/json",
},
body: JSON.stringify({ token }),
}
)
if (!resp.ok) {
const err: any = await resp.json()
throw new Error(
"Failed to register static token: " +
(err.errors?.[0]?.message || resp.statusText)
)
}
this.directus.setToken(token)
return token
}
Expand Down
8 changes: 4 additions & 4 deletions src/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,16 @@ export function createPage(
localChalk: typeof chalk | undefined = undefined
): void {
let templateFolder = path.join(__dirname, "templates")
if (!fs.existsSync(templateFolder)) {
if (!fs.existsSync(templateFolder)) {
templateFolder = path.join(
process.cwd(),
"node_modules",
"@nuxtus",
"generator",
"@resultcrafter",
"nuxtus-generator",
"dist",
"templates"
)
}
}

const env: nunjucks.Environment = nunjucks.configure(templateFolder, {
tags: {
Expand Down
40 changes: 27 additions & 13 deletions src/templates/index.njk.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,37 @@ import type { components } from "../../interfaces/nuxtus"
type {$ collection | camelcase $} = components["schemas"]["Items{$ collection | camelcase $}"]
const { $directus, $readItems, $checkError } = useNuxtApp()

const query: Query<components, {$ collection | camelcase $}> = {
// Add your filters and query customisations here
}
const query: Query<components, {$ collection | camelcase $}> = {}

const { data, error } = useAsyncData <{$ collection | camelcase $}[] | null> ('{$ collection $}', () => {
const { data, error } = useAsyncData<{$ collection | camelcase $}[] | null>('{$ collection $}-list', () => {
return $directus.request($readItems('{$ collection $}', query))
})
$checkError(error)

function formatDate(date: string | undefined): string {
if (!date) return ''
return new Date(date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
}
</script>

<template>
<ul v-if="data === null || data.length > 0">
<li v-for="{$ collection | lower $} in data" :key=" {$ collection | lower $}.id ">
<NuxtLink :to=" `/{$ collection $}/${{$ collection | lower $}.id}`">
{{ {$ collection | lower $}.id }}
</NuxtLink>
</li>
</ul>
<p v-else>No {$ collection $} found.</p>
</template>
<div class="max-w-3xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">{$ collection | camelcase $}</h1>
<div v-if="data && data.length > 0" class="space-y-8">
<article v-for="item in data" :key="item.id" class="border-b border-gray-200 pb-8">
<NuxtLink :to="`/{$ collection $}/${item.id}`" class="group">
<h2 class="text-xl font-semibold group-hover:text-blue-600 transition-colors">
<template v-if="item.title">{{ item.title }}</template>
<template v-else-if="item.name">{{ item.name }}</template>
<template v-else>{{ item.id }}</template>
</h2>
</NuxtLink>
<p v-if="item.date_created" class="text-sm text-gray-500 mt-1">{{ formatDate(item.date_created) }}</p>
<p v-if="item.excerpt" class="text-gray-600 mt-2">{{ item.excerpt }}</p>
<p v-else-if="item.description" class="text-gray-600 mt-2">{{ item.description }}</p>
</article>
</div>
<p v-else-if="data && data.length === 0" class="text-gray-500">No {$ collection $} found.</p>
<p v-else>Loading...</p>
</div>
</template>
52 changes: 43 additions & 9 deletions src/templates/individual.njk.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,53 @@ type {$ collection | camelcase $} = components["schemas"]["Items{$ collection |
const route = useRoute()
const { $directus, $readItem, $checkError } = useNuxtApp()

const query: Query<components, {$ collection | camelcase $}> = {
// Add your filters and query customisations here
}
const query: Query<components, {$ collection | camelcase $}> = {}

const { data: {$ collection $}, error } = useAsyncData <{$ collection | camelcase $} | null> ('{$ collection $}', () => {
return $directus.request($readItem('{$ collection $}', route.params.id, query))
const { data, error } = useAsyncData<{$ collection | camelcase $} | null>('{$ collection $}-detail', () => {
return $directus.request($readItem('{$ collection $}', route.params.id as string, query))
})
$checkError(error)

function formatDate(date: string | undefined): string {
if (!date) return ''
return new Date(date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
}
</script>

<template>
<div>
<h1>{$ collection | camelcase $}</h1>
<div>{{ {$ collection $} }}</div>
<div class="max-w-3xl mx-auto px-4 py-8">
<NuxtLink :to="`/{$ collection $}`" class="text-blue-600 hover:underline text-sm">&larr; Back to {$ collection $}</NuxtLink>

<template v-if="data">
<h1 class="text-3xl font-bold mt-4 mb-2">
<template v-if="data.title">{{ data.title }}</template>
<template v-else-if="data.name">{{ data.name }}</template>
<template v-else>{{ data.id }}</template>
</h1>
<p v-if="data.date_created" class="text-sm text-gray-500 mb-6">{{ formatDate(data.date_created) }}</p>
<p v-if="data.description" class="text-gray-600 mb-6">{{ data.description }}</p>
<div v-if="data.content" class="prose max-w-none" v-html="data.content"></div>
</template>

<template v-else-if="!error">
<h1 class="text-3xl font-bold mt-4">Not found</h1>
<p class="text-gray-500 mt-2">This item could not be found.</p>
<NuxtLink :to="`/{$ collection $}`" class="text-blue-600 hover:underline text-sm mt-4 inline-block">&larr; Back to {$ collection $}</NuxtLink>
</template>
</div>
</template>
</template>

<style scoped>
.prose :deep(h1) { font-size: 1.875rem; font-weight: 700; margin-top: 2rem; margin-bottom: 1rem; }
.prose :deep(h2) { font-size: 1.5rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.75rem; }
.prose :deep(h3) { font-size: 1.25rem; font-weight: 600; margin-top: 1.25rem; margin-bottom: 0.5rem; }
.prose :deep(p) { margin-bottom: 1rem; line-height: 1.75; }
.prose :deep(ul) { list-style-type: disc; padding-left: 1.5rem; margin-bottom: 1rem; }
.prose :deep(ol) { list-style-type: decimal; padding-left: 1.5rem; margin-bottom: 1rem; }
.prose :deep(a) { color: #2563eb; text-decoration: underline; }
.prose :deep(blockquote) { border-left: 4px solid #e5e7eb; padding-left: 1rem; font-style: italic; color: #6b7280; margin-bottom: 1rem; }
.prose :deep(img) { max-width: 100%; height: auto; border-radius: 0.5rem; margin: 1.5rem 0; }
.prose :deep(code) { background: #f3f4f6; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875rem; }
.prose :deep(pre) { background: #1f2937; color: #e5e7eb; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; margin-bottom: 1rem; }
.prose :deep(pre code) { background: transparent; padding: 0; color: inherit; }
</style>
41 changes: 32 additions & 9 deletions src/templates/singleton.njk.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,42 @@
import type { Query } from "@directus/sdk"
import type { components } from "../../interfaces/nuxtus"
type {$ collection | camelcase $} = components["schemas"]["Items{$ collection | camelcase $}"]
const { $directus, $readSingleton, $checkError } = useNuxtApp()
const { $directus, $readItems, $checkError } = useNuxtApp()

const query: Query<components, {$ collection | camelcase $}> = {
// Add your filters and query customisations here
}
const query: Query<components, {$ collection | camelcase $}> = {}

const { data: {$ collection $}, error } = useAsyncData <{$ collection | camelcase $} | null> ('{$ collection $}', () => {
return $directus.request($readSingleton('{$ collection $}', query))
const { data: {$ collection $}, error } = useAsyncData<{$ collection | camelcase $} | null>('{$ collection $}-singleton', () => {
return $directus.request($readItems('{$ collection $}', query))
})
$checkError(error)
</script>

<template>
<h1>{$ collection | camelcase $}</h1>
{{ {$ collection $} }}
</template>
<div class="max-w-3xl mx-auto px-4 py-8">
<template v-if="{$ collection $}">
<h1 class="text-3xl font-bold mb-4">
<template v-if="${$ collection $}.title">{{ {$ collection $}.title }}</template>
<template v-else-if="${$ collection $}.name">{{ {$ collection $}.name }}</template>
<template v-else>{$ collection | camelcase $}</template>
</h1>
<p v-if="${$ collection $}.description" class="text-gray-600 mb-6">{{ {$ collection $}.description }}</p>
<div v-if="${$ collection $}.content" class="prose max-w-none" v-html="{$ collection $}.content"></div>
</template>
<p v-else>Loading...</p>
</div>
</template>

<style scoped>
.prose :deep(h1) { font-size: 1.875rem; font-weight: 700; margin-top: 2rem; margin-bottom: 1rem; }
.prose :deep(h2) { font-size: 1.5rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.75rem; }
.prose :deep(h3) { font-size: 1.25rem; font-weight: 600; margin-top: 1.25rem; margin-bottom: 0.5rem; }
.prose :deep(p) { margin-bottom: 1rem; line-height: 1.75; }
.prose :deep(ul) { list-style-type: disc; padding-left: 1.5rem; margin-bottom: 1rem; }
.prose :deep(ol) { list-style-type: decimal; padding-left: 1.5rem; margin-bottom: 1rem; }
.prose :deep(a) { color: #2563eb; text-decoration: underline; }
.prose :deep(blockquote) { border-left: 4px solid #e5e7eb; padding-left: 1rem; font-style: italic; color: #6b7280; margin-bottom: 1rem; }
.prose :deep(img) { max-width: 100%; height: auto; border-radius: 0.5rem; margin: 1.5rem 0; }
.prose :deep(code) { background: #f3f4f6; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875rem; }
.prose :deep(pre) { background: #1f2937; color: #e5e7eb; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; margin-bottom: 1rem; }
.prose :deep(pre code) { background: transparent; padding: 0; color: inherit; }
</style>