Skip to main content

Custom Metadata & Templating

Codex allows you to store and display custom metadata for your series beyond the standard fields (title, genre, tags, etc.). This guide covers how to use custom metadata and configure its display using Handlebars templates.

Overview

Custom metadata is a flexible JSON object that can contain any data you want to associate with a series. It's displayed on the series detail page using a configurable Handlebars template that renders to Markdown.

Templates have access to two data sources:

  • customMetadata - Your custom JSON data stored on the series
  • metadata - Built-in series metadata (title, genres, publisher, ratings, etc.)

Use cases:

  • Track reading progress and personal ratings
  • Link to external databases (MyAnimeList, AniList, etc.)
  • Store collection details (edition, condition, purchase info)
  • Add personal notes and reviews
  • Track technical information (resolution, audio, subtitles)

Custom Metadata on Series

Storing Custom Metadata

Custom metadata is stored as a JSON object on each series. You can set it via the API or through the series metadata editor.

JSON Structure

Custom metadata can be any valid JSON object:

{
"status": "In Progress",
"rating": 8.5,
"priority": 5,
"startedDate": "2024-01-15",
"notes": "Great series, highly recommended!",
"links": [
{ "name": "MyAnimeList", "url": "https://myanimelist.net/manga/1" },
{ "name": "AniList", "url": "https://anilist.co/manga/1" }
],
"tags": ["favorite", "action", "must-read"]
}

API Endpoints

Update custom metadata via the API:

# Update series custom metadata
PATCH /api/v1/series/{id}
Content-Type: application/json

{
"customMetadata": {
"status": "Completed",
"rating": 9
}
}

Display Templates

Custom metadata is rendered on the series detail page using a Handlebars template. The template is configured in Settings > Server Settings > Custom Metadata Template.

How Templates Work

  1. Your custom metadata is passed to the template as customMetadata
  2. Built-in series metadata is passed as metadata
  3. The template is rendered using Handlebars syntax
  4. The output (Markdown) is displayed using styled components

Default Template

The default template displays genres from built-in metadata and custom fields as a bullet list:

{{#if metadata.genres}}
**Genres:** {{join metadata.genres " • "}}
{{/if}}

{{#if customMetadata}}
## Additional Information

{{#each customMetadata}}
- **{{@key}}**: {{this}}
{{/each}}
{{/if}}

Configuring Templates

  1. Navigate to Settings > Server Settings
  2. Find the Custom Metadata Template section
  3. Edit the template or select a pre-built example
  4. Use the live preview to test your changes
  5. Save your changes

Custom Metadata Settings

Custom Metadata Template Editor

Handlebars Syntax

Templates use Handlebars syntax with additional custom helpers.

Basic Syntax

{{!-- Output a value --}}
{{customMetadata.title}}

{{!-- Conditional block --}}
{{#if customMetadata.rating}}
Rating: {{customMetadata.rating}}/10
{{/if}}

{{!-- Iterate over arrays --}}
{{#each customMetadata.tags}}
- {{this}}
{{/each}}

{{!-- Iterate over objects --}}
{{#each customMetadata}}
- **{{@key}}**: {{this}}
{{/each}}

Available Helpers

Codex provides these custom helpers:

HelperDescriptionUsage
formatDateFormat a date string{{formatDate value "MMM d, yyyy"}}
ifEqualsCheck if two values are equal{{#ifEquals value1 value2}}...{{/ifEquals}}
ifNotEqualsCheck if two values are not equal{{#ifNotEquals value1 value2}}...{{/ifNotEquals}}
jsonOutput JSON representation{{json value}}
truncateTruncate string to length{{truncate value 100 "..."}}
lowercaseConvert to lowercase{{lowercase value}}
uppercaseConvert to uppercase{{uppercase value}}
capitalizeCapitalize first letter{{capitalize value}}
firstGet first N items of array{{#first items 3}}...{{/first}}
joinJoin array with separator{{join array ", "}}
existsCheck if value exists{{#exists value}}...{{/exists}}
lengthGet length of array/string{{length array}}
gtGreater than comparison{{#gt value1 value2}}...{{/gt}}
ltLess than comparison{{#lt value1 value2}}...{{/lt}}
andLogical AND{{#and cond1 cond2}}...{{/and}}
orLogical OR{{#or cond1 cond2}}...{{/or}}
lookupDynamic property access{{lookup object key}}
defaultProvide default value{{default value "fallback"}}

Helper Examples

Date Formatting:

{{#if customMetadata.startedDate}}
Started: {{formatDate customMetadata.startedDate "MMMM d, yyyy"}}
{{/if}}

Date formats use date-fns format strings:

  • yyyy-MM-dd → 2024-01-15
  • MMM d, yyyy → Jan 15, 2024
  • MMMM d, yyyy → January 15, 2024
  • MMMM d, yyyy 'at' h:mm a → January 15, 2024 at 2:30 PM

Conditional Display:

{{#gt customMetadata.rating 8}}
🔥 Highly Rated!
{{else}}
{{#gt customMetadata.rating 5}}
👍 Worth Reading
{{else}}
🤔 Mixed Reviews
{{/gt}}
{{/gt}}

Array Operations:

{{!-- Join array items --}}
**Tags:** {{join customMetadata.tags ", "}}

{{!-- Show only first 3 items --}}
{{#first customMetadata.characters 3}}
- {{this.name}}: {{this.role}}
{{/first}}

{{!-- Show count --}}
Total: {{length customMetadata.items}} items

Default Values:

Status: {{default customMetadata.status "Not started"}}
Rating: {{default customMetadata.rating "—"}}/10

Built-in Metadata Fields

In addition to customMetadata, templates have access to the series' built-in metadata via the metadata object. This allows you to combine your custom tracking data with standard series information.

Available Fields

FieldTypeDescription
metadata.titlestringSeries title
metadata.summarystringSeries description/summary
metadata.publisherstringPublisher name
metadata.imprintstringPublisher imprint
metadata.yearnumberPublication year
metadata.statusstringSeries status (e.g., "ongoing", "completed")
metadata.totalBookCountnumberTotal number of books in the series
metadata.ageRatingnumberAge rating (e.g., 13, 18)
metadata.languagestringPrimary language
metadata.genresstring[]List of genres
metadata.tagsstring[]List of tags
metadata.externalRatingsarrayExternal ratings (source, rating, votes)
metadata.externalLinksarrayExternal links (source, url)
metadata.alternateTitlesarrayAlternate titles (title, label)

Metadata Examples

Display genres and publisher:

{{#if metadata.genres}}
**Genres:** {{join metadata.genres " • "}}
{{/if}}

{{#if metadata.publisher}}
**Publisher:** {{metadata.publisher}}{{#if metadata.year}} ({{metadata.year}}){{/if}}
{{/if}}

Show series status with capitalization:

{{#if metadata.status}}
**Status:** {{capitalize metadata.status}}
{{/if}}

Display external ratings:

{{#if metadata.externalRatings}}
### Community Ratings
{{#each metadata.externalRatings}}
- **{{this.source}}**: {{this.rating}}{{#if this.votes}} ({{this.votes}} votes){{/if}}
{{/each}}
{{/if}}

Combine custom and built-in metadata:

{{#if metadata}}
## {{metadata.title}}

{{#if metadata.summary}}
{{metadata.summary}}
{{/if}}

{{#if metadata.genres}}
**Genres:** {{join metadata.genres " • "}}
{{/if}}
{{/if}}

{{#if customMetadata}}
---
## My Progress

**Status:** {{default customMetadata.status "Not started"}}
{{#if customMetadata.currentVolume}}
**Currently on:** Volume {{customMetadata.currentVolume}}{{#if metadata.totalBookCount}} of {{metadata.totalBookCount}}{{/if}}
{{/if}}
{{/if}}

Supported Markdown

The template output is rendered as Markdown with support for:

Headings

# Large Heading
## Section Heading
### Subsection

Lists

- Item 1
- Item 2
- Item 3

List items with **label**: value pattern are styled as key-value rows:

- **Status**: Completed
- **Rating**: 9/10
- **Priority**: High

Tables

| Column 1 | Column 2 | Column 3 |
|----------|----------|----------|
| Value 1 | Value 2 | Value 3 |
| Value 4 | Value 5 | Value 6 |
[Link Text](https://example.com)

External links (starting with http) open in a new tab.

Code

Inline code:

File ID: `ABC123`

Code blocks:

```
Resolution: 1920x1080
Audio: Japanese 5.1
```

Blockquotes

> This is a blockquote for important notes or warnings.

Text Formatting

**Bold text**
*Italic text*
~~Strikethrough text~~

Horizontal Rules

---

Example Templates

Codex includes several pre-built templates to get you started:

Simple List

Basic key-value display with optional genres from built-in metadata:

{{#if metadata.genres}}
**Genres:** {{join metadata.genres " • "}}
{{/if}}

{{#if customMetadata}}
## Additional Information

{{#each customMetadata}}
- **{{@key}}**: {{this}}
{{/each}}
{{/if}}

Reading List

Track reading progress and ratings:

{{#if customMetadata}}
## Reading Info

{{#if customMetadata.status}}
**Status:** {{customMetadata.status}}
{{/if}}

{{#if customMetadata.rating}}
**My Rating:** {{customMetadata.rating}}/10
{{/if}}

{{#if customMetadata.startedDate}}
**Started:** {{formatDate customMetadata.startedDate "MMM d, yyyy"}}
{{/if}}

{{#if customMetadata.completedDate}}
**Completed:** {{formatDate customMetadata.completedDate "MMM d, yyyy"}}
{{/if}}

{{#if customMetadata.notes}}
### Notes
{{customMetadata.notes}}
{{/if}}
{{/if}}

Link to external databases:

{{#if customMetadata}}
{{#if customMetadata.links}}
## External Links

{{#each customMetadata.links}}
- [{{this.name}}]({{this.url}})
{{/each}}
{{/if}}

{{#if customMetadata.ids}}
### Database IDs
{{#each customMetadata.ids}}
- **{{@key}}**: `{{this}}`
{{/each}}
{{/if}}
{{/if}}

Collection Info

Track physical collection details:

{{#if customMetadata}}
## Collection Details

{{#if customMetadata.format}}
**Format:** {{customMetadata.format}}
{{/if}}

{{#if customMetadata.edition}}
**Edition:** {{customMetadata.edition}}
{{/if}}

{{#if customMetadata.condition}}
**Condition:** {{customMetadata.condition}}
{{/if}}

{{#if customMetadata.purchaseDate}}
**Purchased:** {{formatDate customMetadata.purchaseDate "MMM d, yyyy"}}
{{/if}}

{{#if customMetadata.location}}
**Location:** {{customMetadata.location}}
{{/if}}
{{/if}}

With Tables

Display data in tables:

{{#if customMetadata}}
{{#and customMetadata.status customMetadata.rating}}
## Reading Status

| Status | Rating | Priority |
|--------|--------|----------|
| {{default customMetadata.status "Not started"}} | {{default customMetadata.rating "—"}}/10 | {{default customMetadata.priority "—"}} |

{{/and}}
{{/if}}

Series Info (Built-in Metadata)

Display only built-in series metadata:

{{#if metadata}}
## Series Info

{{#if metadata.publisher}}
**Publisher:** {{metadata.publisher}}{{#if metadata.imprint}} ({{metadata.imprint}}){{/if}}
{{/if}}

{{#if metadata.year}}
**Year:** {{metadata.year}}
{{/if}}

{{#if metadata.status}}
**Status:** {{capitalize metadata.status}}
{{/if}}

{{#if metadata.genres}}
### Genres
{{join metadata.genres " • "}}
{{/if}}

{{#if metadata.externalRatings}}
### Ratings
{{#each metadata.externalRatings}}
- **{{this.source}}**: {{this.rating}}{{#if this.votes}} ({{this.votes}} votes){{/if}}
{{/each}}
{{/if}}

{{#if metadata.externalLinks}}
### Links
{{#each metadata.externalLinks}}
- [{{this.source}}]({{this.url}})
{{/each}}
{{/if}}
{{/if}}

Complete Overview (Combined)

Combine custom tracking data with built-in series metadata:

{{#if metadata}}
## {{metadata.title}}

{{#if metadata.summary}}
{{metadata.summary}}
{{/if}}

{{#and metadata.publisher metadata.year}}
*Published by {{metadata.publisher}} in {{metadata.year}}*
{{/and}}
{{/if}}

{{#if customMetadata}}
---

## My Progress

{{#if customMetadata.status}}
**Status:** {{customMetadata.status}}
{{/if}}

{{#if customMetadata.rating}}
**My Rating:** {{customMetadata.rating}}/10
{{/if}}

{{#if customMetadata.currentVolume}}
**Currently on:** Volume {{customMetadata.currentVolume}}{{#if metadata.totalBookCount}} of {{metadata.totalBookCount}}{{/if}}
{{/if}}

{{#if customMetadata.notes}}
### Notes
{{customMetadata.notes}}
{{/if}}
{{/if}}

{{#if metadata}}
{{#if metadata.genres}}
---
**Genres:** {{join metadata.genres " • "}}
{{/if}}
{{/if}}

Best Practices

Template Design

  1. Use conditional blocks: Always wrap sections in {{#if}} to handle missing data gracefully
  2. Provide defaults: Use {{default value "fallback"}} for optional fields
  3. Keep it readable: Use headings and sections to organize information
  4. Test with sample data: Use the live preview in settings to test your template

Data Structure

  1. Use consistent keys: Stick to a naming convention (camelCase recommended)
  2. Use ISO dates: Store dates as ISO strings (e.g., 2024-01-15) for reliable formatting
  3. Group related data: Use nested objects for related information
  4. Use arrays for lists: Store multiple items as arrays for easy iteration

Performance

  1. Keep templates simple: Complex nested loops may slow rendering
  2. Limit output size: Templates have a 100KB output limit
  3. Use efficient helpers: Prefer exists over if for null checks

Troubleshooting

Template Errors

If your template shows an error:

  1. Check for unclosed blocks ({{#if}} needs {{/if}})
  2. Verify helper names are spelled correctly
  3. Check for mismatched quotes in strings
  4. Use the live preview to see error messages

Data Not Displaying

If custom metadata isn't showing:

  1. Verify the series has custom metadata set
  2. Check that your template handles the data structure correctly
  3. Ensure conditional blocks match your data (e.g., customMetadata.field vs customMetadata.nested.field)

Styling Issues

If the output doesn't look right:

  1. Verify your Markdown syntax is correct
  2. Check that tables have proper header rows
  3. Ensure code blocks use triple backticks

Next Steps