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
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
},
"explorer.fileNesting.enabled": true,
"files.exclude": {
"**/node_modules": true
},
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export default defineConfig({
items: [
{ text: 'Introduction', link: 'introduction' },
{ text: 'Quick Start', link: 'quick-start' },
{ text: 'Migration to 1.0 Alpha', link: 'migration' },
{ text: 'Define Collections', link: 'define-collections' },
{ text: 'Using Collections', link: 'using-collections' },
{ text: 'Velite Schemas', link: 'velite-schemas' }
Expand Down
97 changes: 79 additions & 18 deletions docs/guide/custom-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,20 @@ import { defineSchema, s } from 'velite'
export const title = defineSchema(() => s.string().min(1).max(100))

// for validating email
export const email = defineSchema(() => s.string().email({ message: 'Invalid email address' }))
export const email = defineSchema(() => s.email({ message: 'Invalid email address' }))

// custom validation logic
export const hello = defineSchema(() =>
s.string().refine(value => {
if (value !== 'hello') {
return 'Value must be "hello"'
// custom validation logic using refine
export const hello = defineSchema(() => s.string().refine(value => value === 'hello', 'Value must be "hello"'))

// custom validation logic using superRefine (for more control)
export const customValidation = defineSchema(() =>
s.string().superRefine((value, ctx) => {
if (value.length < 5) {
ctx.addIssue({ code: 'custom', message: 'Value must be at least 5 characters' })
}
if (!value.includes('@')) {
ctx.addIssue({ code: 'custom', message: 'Value must contain @ symbol' })
}
return true
})
)
```
Expand All @@ -40,10 +45,28 @@ Refer to [Zod documentation](https://zod.dev) for more information about Zod.
```ts
import { defineSchema, s } from 'velite'

// for transforming title
// for transforming title (simple transform)
export const title = defineSchema(() => s.string().transform(value => value.toUpperCase()))

// ...
// for transforming with error handling (using ctx.addIssue)
export const safeTransform = defineSchema(() =>
s.string().transform((value, ctx) => {
try {
return value.toUpperCase()
} catch (err) {
ctx.addIssue({ fatal: true, code: 'custom', message: 'Transform failed' })
return value
}
})
)

// async transform (zod 4 supports async transforms)
export const asyncTransform = defineSchema(() =>
s.string().transform(async (value, ctx) => {
// async operations...
return processedValue
})
)
```

### Example
Expand All @@ -59,7 +82,7 @@ import type { Image } from 'velite'
* Remote Image with metadata schema
*/
export const remoteImage = () =>
s.string().transform<Image>(async (value, { addIssue }) => {
s.string().transform<Image>(async (value, ctx) => {
try {
const response = await fetch(value)
const blob = await response.blob()
Expand All @@ -69,7 +92,7 @@ export const remoteImage = () =>
return { src: value, ...metadata }
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
addIssue({ fatal: true, code: 'custom', message })
ctx.addIssue({ fatal: true, code: 'custom', message })
return null as never
}
})
Expand All @@ -78,22 +101,60 @@ export const remoteImage = () =>
## Schema Context

> [!TIP]
> Considering that Velite's scenario often needs to obtain metadata information about the current file in the schema, Velite does not use the original Zod package. Instead, it uses a custom Zod package that provides a `meta` member in the schema context.
> In Zod 4, the context object (`ctx`) in `refine`, `superRefine`, and `transform` provides an `addIssue()` method for adding validation errors. Velite extends this context to provide access to file metadata through `context()` function.

### Using Context API

```ts
import { defineSchema, s } from 'velite'
import { context, defineSchema, s } from 'velite'

// convert a nonexistent field
// Access file context in transform
export const path = defineSchema(() =>
s.custom<string>().transform((value, ctx) => {
if (ctx.meta.path) {
return ctx.meta.path
s.custom<string>().transform(value => {
// Use context() to access current file information
const { file, config } = context()

if (value == null) {
return file.path
}
return value
})
)
```

### Context API Reference

The `context()` function returns an object with:

- `config`: The resolved Velite configuration
- `file`: The current [`VeliteFile`](../reference/types.md#velitefile) being processed

### Error Handling in Transforms

```ts
import { defineSchema, s } from 'velite'

export const safeTransform = defineSchema(() =>
s.string().transform(async (value, ctx) => {
try {
// async operation
const result = await processValue(value)
return result
} catch (err) {
// Add error issue using Zod 4 API
ctx.addIssue({
fatal: true, // Set to true to stop processing
code: 'custom', // Error code
message: err.message // Error message
})
return null as never // Type assertion for TypeScript
}
})
)
```

### Reference

the type of `meta` is `ZodMeta`, which extends [`VeliteFile`](../reference/types.md#velitefile).
- `context()` returns `{ config: Config, file: VeliteFile }`
- `ctx.addIssue()` accepts `{ fatal?: boolean, code: string, message: string }`
- See [`VeliteFile`](../reference/types.md#velitefile) for file metadata structure
36 changes: 25 additions & 11 deletions docs/guide/define-collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,35 +156,49 @@ const posts = defineCollection({

### Transform Context Metadata

The `transform()` function can receive a second argument, which is the context object. This is useful for adding computed fields to the content items in a collection.
The `transform()` function can receive a second argument, which is the context object (in Zod 4). To access file metadata, use the `context()` function from Velite.

```js
import { context, s } from 'velite'

const posts = defineCollection({
schema: s
.object({
// fields
})
.transform((data, { meta }) => ({
...data,
// computed fields
path: meta.path // or parse to filename based slug
}))
.transform(data => {
const { file } = context()
return {
...data,
// computed fields
path: file.path, // or parse to filename based slug
basename: file.basename
}
})
})
```

the type of `meta` is `ZodMeta`, which extends [`VeliteFile`](../reference/types.md#velitefile). for more information, see [Custom Schema](custom-schema.md).
For more information about accessing file context, see [Custom Schema](custom-schema.md).

## Content Body

Velite's built-in loader keeps content's raw body in `meta.content`, and the plain text body in `meta.plain`.
Velite's built-in loader keeps content's raw body in `file.content`, and the plain text body in `file.plain`.

To add them as a field, you can use a custom schema.
To add them as a field, you can use a custom schema with the `context()` function.

```js
import { context, s } from 'velite'

const posts = defineCollection({
schema: s.object({
content: s.custom().transform((data, { meta }) => meta.content),
plain: s.custom().transform((data, { meta }) => meta.plain)
content: s.custom().transform(() => {
const { file } = context()
return file.content
}),
plain: s.custom().transform(() => {
const { file } = context()
return file.plain
})
})
})
```
Expand Down
27 changes: 12 additions & 15 deletions docs/guide/last-modified.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,15 @@ Create a timestamp schema based on file stat.

```ts
import { stat } from 'fs/promises'
import { defineSchema } from 'velite'
import { context, defineSchema } from 'velite'

const timestamp = defineSchema(() =>
s
.custom<string | undefined>(i => i === undefined || typeof i === 'string')
.transform<string>(async (value, { meta, addIssue }) => {
if (value != null) {
addIssue({ fatal: false, code: 'custom', message: '`s.timestamp()` schema will resolve the file modified timestamp' })
}

const stats = await stat(meta.path)
.custom<string>(i => typeof i === 'string')
.optional()
.transform<string>(async () => {
const { file } = context()
const stats = await stat(file.path)
return stats.mtime.toISOString()
})
)
Expand All @@ -45,18 +43,17 @@ const posts = defineCollection({
```ts
import { exec } from 'child_process'
import { promisify } from 'util'
import { defineSchema } from 'velite'
import { context, defineSchema } from 'velite'

const execAsync = promisify(exec)

const timestamp = defineSchema(() =>
s
.custom<string | undefined>(i => i === undefined || typeof i === 'string')
.transform<string>(async (value, { meta, addIssue }) => {
if (value != null) {
addIssue({ fatal: false, code: 'custom', message: '`s.timestamp()` schema will resolve the value from `git log -1 --format=%cd`' })
}
const { stdout } = await execAsync(`git log -1 --format=%cd ${meta.path}`)
.custom<string>(i => typeof i === 'string')
.optional()
.transform<string>(async () => {
const { file } = context()
const { stdout } = await execAsync(`git log -1 --format=%cd ${file.path}`)
return new Date(stdout || Date.now()).toISOString()
})
)
Expand Down
Loading