Skip to content

Commit

Permalink
Merge pull request #237 from zazuko/mc-multiple-entries
Browse files Browse the repository at this point in the history
Support multiple entries
  • Loading branch information
ludovicm67 authored Dec 12, 2023
2 parents bb27c61 + 704baae commit c268cae
Show file tree
Hide file tree
Showing 6 changed files with 456 additions and 96 deletions.
37 changes: 37 additions & 0 deletions .changeset/olive-otters-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
"@zazuko/trifid-markdown-content": major
---

**BREAKING CHANGE:**
The plugin configuration changed in order to support multiple namespaces directly.

The following configuration:

```yaml
middlewares:
# […] your other middlewares
markdown-content:
module: "@zazuko/trifid-markdown-content"
order: 80
config:
namespace: root-content
directory: file:content
mountPath: /
```
Can be migrated in an easy way by moving all configuration entries (except the namespace) into the config.entries[namespaceValue] key, like this:
```yaml
middlewares:
# […] your other middlewares
markdown-content:
module: "@zazuko/trifid-markdown-content"
order: 80
config:
entries:
root-content:
directory: file:content
mountPath: /
```
See more details and options in the `README.md` file of the plugin.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

86 changes: 67 additions & 19 deletions packages/markdown-content/README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# Markdown support for Trifid

You can use this plugin to serve markdown files as content for Trifid.
You can use this plugin to serve Markdown files as content for Trifid.

This works well in case you have a lot of Markdown files and want to generates HTML pages out of them.
This works well in case you have a lot of Markdown files and want to generate HTML pages out of them.

It supports the following features:

- multilangual content
- multilingual content
- default language fallback
- custom templates
- multiple namespaces

A namespace in that regard is a dedicated instance of that plugin.
A single Trifid instance can host multiple namespaces.
A namespace in that regard is a dedicated set of content to serve.
A single Trifid instance can host multiple namespaces, and each of them can be configured in an independent way.

## Quick start

Expand All @@ -27,13 +27,16 @@ And then add in the `config.yaml` file the following part:
```yaml
middlewares:
# […] your other middlewares
spex:
markdown-content:
module: "@zazuko/trifid-markdown-content"
order: 80
config:
directory: file:content/custom
mountPath: /content/
namespace: custom-content
defaults:
autoLink: true
entries:
custom-content:
directory: file:content/custom
mountPath: /content/
```
This will create a new `custom-content` namespace that will serve the content located in the `content/custom` directory.
Expand All @@ -43,20 +46,64 @@ The content will be available with the `/content/` prefix.

The following options are supported:

- `directory`: The directory where the content is located. This should be a local directory (required).
- `mountPath`: The path where the content should be mounted. This should be a path that is not used by other middlewares (required).
- `namespace`: The namespace of the content. This is used to separate the content from other namespaces (default: `default`).
- `entries`: The list of namespaces to create. Keys should be the namespace names, and values should be the configuration for the namespace (required).
- `defaults`: The default configuration for all namespaces (optional). This is useful if you want to have the same configuration for all namespaces. You can still override the configuration for a specific namespace.

### Namespace options

You can define them in the `defaults` section or in specific entries in the `entries` section.
Those options are all optional.

- `idPrefix`: The prefix to use for the generated IDs for headings (default: `markdown-content-`).
- `classes`: The classes to add to the generated HTML (default: `{}`). Keys should be the CSS selectors, values should be the classes to add.
- `classes`: The classes to add to the generated HTML (default: `{}`). Keys should be the CSS selectors, and values should be the classes to add.
- `autoLink`: If `true`, will automatically add links to headings (default: `true`).
- `template`: Path to an alternative template (default: `views/content.hbs`).

### Namespace configuration

The following options are required for each namespace:

- `directory`: The directory where the content is located. This should be a local directory (required).
- `mountPath`: The path where the content should be mounted. This should be a path that is not used by other middlewares (required).

### Example

This is a more complete example on how this plugin can be used:

```yaml
middlewares:
# […] your other middlewares
markdown-content:
module: "@zazuko/trifid-markdown-content"
order: 80
config:
defaults:
idPrefix: markdown-content-
classes:
h1: custom-h1
h2: custom-h2
h3: custom-h3
h4: custom-h4
h5: custom-h5
h6: custom-h6
autoLink: true
template: file:views/content.hbs
entries:
root-content:
directory: file:content/root
mountPath: /
about-content:
directory: file:content/about
mountPath: /about/
autoLink: false # override the default value
```

## Content

The content is expected to be in Markdown format.
It should be stored in the configured directory.
Each file should be in a dedicated directory.
Each directory can contains multiple files:
Each directory can contain multiple files:

- `en.md`: The content in English
- `fr.md`: The content in French
Expand All @@ -74,7 +121,7 @@ Imagine you want to serve the two following Markdown files:
- `about.md`
- `contact.md`

First create a `content` directory.
First, create a `content` directory.
Then create a `about` directory inside the `content` directory.
Inside the `about` directory, move the `about.md` file and rename it to `default.md`.

Expand All @@ -86,13 +133,14 @@ If you want to create the pages directly at the root of your Trifid instance, yo
```yaml
middlewares:
# […] your other middlewares
spex:
markdown-content:
module: "@zazuko/trifid-markdown-content"
order: 80
config:
directory: file:content
mountPath: /
namespace: root-content
entries:
root-content:
directory: file:content
mountPath: /
```

That way, you will have two new endpoints:
Expand Down
77 changes: 42 additions & 35 deletions packages/markdown-content/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'

import addClasses from './addClasses.js'
import { defaultValue } from './utils.js'

const currentDir = dirname(fileURLToPath(import.meta.url))

Expand Down Expand Up @@ -176,47 +177,53 @@ const contentMiddleware = ({ logger, namespace, store }) => async (_req, res, ne

const factory = async (trifid) => {
const { config, logger, server, render } = trifid
const { namespace, directory, mountPath, idPrefix, classes, autoLink, template } = config

// check config
const configuredNamespace = namespace ?? 'default'
if (!directory || typeof directory !== 'string') {
throw new Error('\'directory\' should be a non-empty string')
}
const mountAtPath = mountPath || false

const configuredIdPrefix = idPrefix || 'markdown-content-'
const configuredClasses = classes || {}
const configuredAutolink = !!autoLink || autoLink === 'true'
const configuredTemplate = template || `${currentDir}/../views/content.hbs`
const entries = config?.entries || {}
const defaults = config?.defaults || {}

// Default configuration
const idPrefix = defaultValue('idPrefix', defaults, 'markdown-content-')
const classes = defaultValue('classes', defaults, {})
const autoLink = defaultValue('autoLink', defaults, true)
const template = defaultValue('template', defaults, `${currentDir}/../views/content.hbs`)

// Iterate over all configured entries
for (const [namespace, entry] of Object.entries(entries)) {
const directory = entry?.directory
const mountPath = entry?.mountPath || false

// check config
if (!directory || typeof directory !== 'string') {
throw new Error('\'directory\' should be a non-empty string')
}

const contentConfiguration = {
idPrefix: configuredIdPrefix,
classes: configuredClasses,
autoLink: configuredAutolink,
}
const contentConfiguration = {
idPrefix: defaultValue('idPrefix', entry, idPrefix),
classes: defaultValue('classes', entry, classes),
autoLink: defaultValue('autoLink', entry, autoLink),
}

const store = {}
const items = await getItems(directory)
const store = {}
const items = await getItems(directory)

for (const item of items) {
store[item.name] = await getContent(item.path, contentConfiguration)
}
for (const item of items) {
store[item.name] = await getContent(item.path, contentConfiguration)
}

// apply the middleware in all cases
server.use(contentMiddleware({ logger, namespace: configuredNamespace, store }))
// apply the middleware in all cases
server.use(contentMiddleware({ logger, namespace, store }))

// create a route for each entry
if (mountAtPath) {
const mountAtPathSlash = mountAtPath.endsWith('/') ? mountAtPath : `${mountAtPath}/`
// create a route for each entry
if (mountPath) {
const mountAtPathSlash = mountPath.endsWith('/') ? mountPath : `${mountPath}/`

for (const item of items) {
server.get(`${mountAtPathSlash}${item.name}`, async (_req, res, _next) => {
return res.send(await render(configuredTemplate, {
content: res.locals[LOCALS_PLUGIN_KEY][configuredNamespace][item.name] || '',
locals: res.locals,
}))
})
for (const item of items) {
server.get(`${mountAtPathSlash}${item.name}`, async (_req, res, _next) => {
return res.send(await render(defaultValue('template', entry, template), {
content: res.locals[LOCALS_PLUGIN_KEY][namespace][item.name] || '',
locals: res.locals,
}))
})
}
}
}

Expand Down
17 changes: 17 additions & 0 deletions packages/markdown-content/src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// @ts-check

/**
* Return the value for the specific key.
* If the value is not present, return the default value.
*
* @param {string} key Key to search for
* @param {Record<string, any>} values Values to search in
* @param {any} defaultValue Default value to return
* @returns {any} Value for the specific key or the default value
*/
export const defaultValue = (key, values, defaultValue) => {
if (values[key] === undefined) {
return defaultValue
}
return values[key]
}
Loading

0 comments on commit c268cae

Please sign in to comment.