Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
contentful-hugo
Advanced tools
Node module that pulls data from Contentful and turns it into markdown files for Hugo. Can be used with other Static Site Generators, but has some Hugo specific features.
This is a simple Node.js CLI tool that pulls data from Contentful CMS and turns it into Markdown or YAML files for use with a static site generator. It can be used with any static site generator that uses Markdown with YAML frontmatter, but it has some features that are specific to Hugo. It also includes a simple Express server that can can recieve webhooks from Contentful to retrigger get and delete commands (useful when running a preview environment).
Install Node.js
with NPM
npm install contentful-hugo
with Yarn
yarn add contentful-hugo
Complete configuration then run the following command(s) in the terminal
## initialize the directory
contentful-hugo --init
## fetch content from contentful
contentful-hugo [flags]
npx contentful-hugo --init
npx contentful-hugo [flags]
flag | aliases | description |
---|---|---|
--init | Initialize the directory. Generates a config file and default shortcodes for Contentful rich text fields | |
--preview | -P | Runs in preview mode, which pulls both published and unpublished entries from Contentful |
--wait | -W | Wait for the specified number of milliseconds before pulling data from Contentful. |
--config | -C | Specify the path to a config file. |
--server | -S | Run in server mode to recieve webhooks from Contentful (BETA) |
--port | Specify port for server mode (Default 1414) | |
--clean | Delete any directories specified in singleTypes and repeatableTypes | |
--help | Show help | |
--version | Show version number |
contentful-hugo --wait=2000 --preview --config="my_custom_config.js"
# or
contentful-hugo --wait 2000 --preview --config my_custom_config.js
{
"name": "my-hugo-project",
"scripts": {
"dev": "contentful-hugo --preview && hugo server",
"build": "contentful-hugo && hugo --minify"
}
}
In this example when you run npm run dev
it will first use contentful-hugo to pull Contentful data then start hugo server. In the same way when you do the command npm run build
it will first use contentful-hugo to pull Contentful data then run hugo --minify
to build a minified version of your hugo site.
Trying to use this package before completing configuration will return an error in the console
Error: There is an error in your config file, or it does't exits.
Check your config for errors or run "contentful-hugo --init" to create a config file.
By default this library will look for the following environment variables. You can also override these values with the config file. (See config)
.env File:
To declare these environment variables create a .env
file in the root directory of your project.
CONTENTFUL_SPACE = '<space-id>'
CONTENTFUL_TOKEN = '<content-accessToken>'
# optional but required for preview mode
CONTENTFUL_PREVIEW_TOKEN = '<preview-accessToken>'
You can also declare the environment variables in the command line
Powershell:
$env:CONTENTFUL_SPACE="<contentful_space_id>"
$env:CONTENTFUL_TOKEN="<contentful_acessToken>"
$env:CONTENTFUL_PREVIEW_TOKEN="<contentful_preview_accessToken>"
Bash:
export CONTENTFUL_SPACE="<contentful_space_id>"
export CONTENTFUL_TOKEN="<contentful_accessToken>"
export CONTENTFUL_PREVIEW_TOKEN="<contentful_preview_accessToken>"
Before getting started, you will need to create a config file in the root of your repository. Contentful-hugo by default will search for the following files as a config.
contentful-hugo.config.js
contentful-hugo.config.yaml
contentful-hugo.yaml
contentful-settings.yaml
You can also specify a custom config file using the --config
flag. (Javascript or YAML config files are the only currently accepted filetypes)
// contentful-hugo.config.js
module.exports = {
// fetches from default locale if left blank
locales: ['en-US', 'fr-FR'],
contentful: {
// defaults to CONTENTFUL_SPACE env variable
space: 'space-id',
// defaults to CONTENTFUL_TOKEN env variable
token: 'content-deliver-token',
// defaults to CONTENTFUL_PREVIEW_TOKEN env variable
previewToken: 'content-preview-token',
// defaults to "master"
environment: 'master',
},
singleTypes: [
{
id: 'homepage',
directory: 'content',
fileName: '_index',
fileExtension: 'md',
},
{
id: 'siteSettings',
directory: 'data',
fileName: 'settings',
fileExtension: 'yaml',
},
],
repeatableTypes: [
{
id: 'posts',
directory: 'content/posts',
fileExtension: 'md',
mainContent: 'content',
resolveEntries: [
{
field: 'categories',
resolveTo: 'fields.slug',
},
{
field: 'author',
resolveTo: 'fields.name',
},
{
field: 'relatedPosts',
resolveTo: 'sys.id',
},
],
},
{
id: 'seoFields',
isHeadless: true,
directory: 'content/seo-fields',
customFields: {
// these fields will be added to the frontmatter
myCustomField: 'myCustomFieldVal',
myOtherCustomField: (entry) => {
return entry.fields.whatever;
},
},
},
{
id: 'reviews',
directory: 'content/reviews',
mainContent: 'reviewBody',
},
{
id: 'category',
directory: 'content/categories',
isTaxonomy: true, // Experimental Feature
},
],
staticContent: [
{
inputDir: 'static_content',
ouputDir: 'content',
},
],
};
# contentful-hugo.config.yaml
locales: # fetches from default locale if left blank
- en-US
- fr-FR
contentful:
space: 'space-id' # defaults to CONTENTFUL_SPACE env variable
token: 'content-deliver-token' # defaults to CONTENTFUL_TOKEN env variable
previewToken: 'content-preview-token' # defaults to CONTENTFUL_PREVIEW_TOKEN env variable
environment: 'master' # defaults to "master"
singleTypes:
# fetches only the most recently updated entry in a particular content type
# Generated file will be named after the fileName setting
- id: homepage
directory: content
fileName: _index
fileExtension: md
- id: siteSettings
directory: data
fileName: settings
fileExtension: yaml
repeatableTypes:
# fetches all the entries of a content type and places them in a directory.
# Generated files will be named after their Entry ID in Contentful.
- id: posts
directory: content/posts
fileExtension: md
mainContent: content
resolveEntries: # resolves a reference or asset field to a specific property
- field: categories
resolveTo: fields.slug
- field: author
resolveTo: fields.name
- field: relatedPosts
resolveTo: sys.id
- id: seoFields
isHeadless: true
directory: content/seo-fields
customFields:
# will be added to the frontmatter
myCustomFields: 'myCustomFieldValue'
- id: reviews
directory: content/reviews
mainContent: reviewBody
- id: staff
isHeadless: true
directory: content/staff
- id: category
directory: content/categories
isTaxonomy: true # Experimental Feature
field | required | description |
---|---|---|
space | optional | Contentful Space ID (Defaults to CONTENTFUL_SPACE environment variable if not set) |
token | optional | Content delivery token (Defaults to CONTENTFUL_TOKEN environment variable if not set) |
previewToken | optional | Content preview token (Defaults to CONTENTFUL_PREVIEW_TOKEN environment variable if not set) |
environment | optional | Contentful environment ID (Defaults to "master" if not set) |
field | required | description |
---|---|---|
id | required | Contentful content type ID |
directory | required | Directory where you want the file(s) to be generated |
fileName | required | Name of the file generated |
fileExtension | optional | Can be "md", "yml", or "yaml" (defaults to "md") |
mainContent | optional | Field ID for field you want to be the main Markdown content. (Can be a markdown, richtext, or string field) |
type | optional | Manually set value for "type" field in the frontmatter (see hugo docs) |
resolveEntries | optional | Resolve the specified reference fields and/or asset fields to one of it's properties specified with the resolveTo parameter |
overrides | optional | Do custom overrides for field values or field names |
filters | optional | Accepts an object of Contentful search parameters to filter results. See Contentful docs |
ignoreLocales | optional | Ignore localization settings and only pull from the default locale (defaults to false) |
customFields | optional | Accepts an object of fields and values. The values can be a standard static value or a function that accepts the Contentful entry as a parameter and returns a value |
field | required | description |
---|---|---|
id | required | Contentful content type ID |
directory | required | Directory where you want the files to be generated |
fileName | optional | Entry property that will dicatate the filename. (By default this will be sys.id ) |
fileExtension | optional | Can be "md", "yml", or "yaml" (defaults to "md") |
isHeadless | optional | Turns all entries in a content type into headless leaf bundles (see hugo docs). Cannot be set to true when isTaxonomy is set to true. |
isTaxonomy (Experimental) | optional | Organize entries in file structure allowing for custom taxonomy metadata (see hugo docs). Cannot be set to true when isHeadless is set to true. |
mainContent | optional | Field ID for field you want to be the main markdown content. (Can be a markdown, richtext, or string field) |
type | optional | Manually set value for "type" field in the frontmatter (see hugo docs) |
resolveEntries | optional | Resolve the specified reference fields and/or asset fields to one of it's properties specified with the resolveTo parameter |
overrides | optional | Do custom overrides for field values or field names |
filters | optional | Accepts an object of Contentful search parameters to filter results. See Contentful docs |
ignoreLocales | optional | Ignore localization settings and only pull from the default locale (defaults to false) |
customFields | optional | Accepts an object of fields and values. The values can be a standard static value or a function that accepts the Contentful entry as a parameter and returns a value |
The config also has a locales
field that allows you to specify what locales you want to pull from. This field can take an array of strings, an array of objects, or a combination.
By default locale specific file extensions will be used for multiple translations.
// produce en-us.md and fr-fr.md files
module.exports = {
locales: ['en-US', 'fr-FR'];
// rest of config
}
// produce en.md and fr.md files
module.exports = {
locales: [
{
code: 'en-US',
mapTo: 'en'
},
{
code: 'fr-FR',
mapTo: 'fr'
}
]
// rest of config
}
// produce en-us.md files and fr.md files
module.exports = {
locales: [
'en-US',
{
code: 'fr-FR',
mapTo: 'fr'
}
]
// rest of config
}
After configuring locales in Contentful Hugo you will need to update your Hugo config to account for these locales. Consult the Hugo docs for more details.
# config.toml
[languages]
[languages.en-us]
#language settings
[languages.fr-fr]
#language settings
There are sometimes cases where you will want to place content in a directory based on it's locale rather than using a file extension based translation. In order to do this you simple include [locale]
inside your directory file path.
When using locale specific directories the locale specific file extensions (i.e. en.md
or fr.md
) get dropped
module.exports = {
locales: ['en', 'fr']
singleTypes: [
{
id: 'settings',
fileName: 'settings',
fileExtension: 'yaml',
directory: 'data/[locale]'
/*
produces:
- data/en/settings.yaml
- data/fr/settings.yaml
*/
}
]
repeatableTypes: [
{
id: 'post',
directory: 'content/[locale]/post',
/*
produces:
- content/en/post/[entryId].md
- content/fr/post/[entryId].md
*/
},
],
};
The recommended setup for Contentful Hugo is to have your content (usually ./content
) and data (usually ./data
) directories ignored in version control. This is because contentful-hugo will generate these directories at build time. However, this creates trouble for instances where you have pages that are not managed in Contentful and aren't generated at build time by another source.
To deal with this problem Contentful-Hugo has a staticContent
parameter. This paramter accepts an input directory (inputDir
) that can be commited to git, and an output directory (outputDir
) which would be your standard content or data directory. All items in the inputDir will get copied into the outputDir at build time and will retain their folder structure.abs
For example in the config below ./static_content/posts/my-post.md
will get copied to ./content/posts/my-post.md
, and ./static_data/global-settings.yaml
will be copied to ./data/global-settings.yaml
.
module.exports = {
// rest of config
staticContent: [
{
// all items in ./static_content will be copied to ./content
inputDir: 'static_content',
outputDir: 'content',
},
{
// all items in ./static_data will be copied to ./data
inputDir: 'static_data',
outputDir: 'data',
},
],
};
Contentful-Hugo will also watch for file changes in the inputDir's while running in server mode.
Here is an example of dynamically change the token
, previewToken
, and environment
options depending on any arbitrary condition.
// contentful-hugo.config.js
require('dotenv').config(); // assuming you have "dotenv" in your dependencies
const myMasterToken = process.env.CONTENTFUL_MASTER_TOKEN;
const myMasterPreviewToken = process.env.CONTENTFUL_MASTER_PREVIEW_TOKEN;
const myStagingToken = process.env.CONTENTFUL_STAGING_TOKEN;
const myStagingPreviewToken = process.env.CONTENTFUL_STAGING_PREVIEW_TOKEN;
// set some condition
const isStaging = true || false;
module.exports = {
contentful: {
space: 'my-space-id',
token: isStaging ? myStagingToken : myMasterToken,
preview: isStaging ? myStagingPreviewToken : myMasterPreviewToken,
environment: isStaging ? 'staging' : 'master',
},
// rest of config
};
// contentful-hugo.config.js
module.exports = {
repeatableTypes: [
{
id: "trips",
directory: "content/trips"
overrides: [{
field: "url",
options: {
// change the url field name to "slug" in frontmatter
fieldName: "slug"
}
},
{
field: "distanceInKilometers",
options: {
// rename "distanceInKilometers" to "distanceInMiles"
fieldName: "distanceInMiles",
// convert distance to miles and output the result in frontmatter
valueTransformer: (val) => {
if(typeof val === 'number') {
return val * 0.621371
}
return 0
}
}
}]
}
]
}
For JS config files you can import a ContentfulHugoConfig
type which will enable autocomplete in text editors that support Typescript typings. (Tested in Visual Studio Code.)
/**
* @type {import('contentful-hugo').ContentfulHugoConfig}
*/
module.exports = {
// rest of config
};
Example .gitignore
setup
# general stuff
.env
node_modules
public
resources
# Contenful Hugo stuff
# temp folder that contentful uses to track files
.contentful-hugo
# since content and data is coming from Contentful
# usually you'll want to ignore those directories
content
data
Files will be generated in the directory specified in the config file. Front matter will be in YAML format. Files of single types will be named after fileName specified in the config file. Files of repeatable types will be named after their entry ID in Contenful, which makes it easy to link files together.
The following fields will always appear in your frontmatter:
date: # defaults to sys.createdAt unless you have a field with the id "date" then it get's overwritten
sys:
id: # the entry id
updatedAt: # the last time this entry was updated in Contentful
createdAt: # when the entry was created in Contentful
revision: # the revision number
space: # the space id
contentType: # the content type id
# the following fields are depreciated and will be removed in a future version
# migrate to using the sys.updatedAt and sys.createdAt iterations
updated: # the last time the entry was updated in Contentful
createdAt: # when the entry was created in Contentful
Assets like images and videos come with some extra information that makes it easy to implement things like alt text or layouts that rely on knowing the image dimensions. The fields are as follows:
assetFieldName:
assetType: # indicates the asset type such as "image" "video" "audio" ect.
url: # url of the asset
title: # title of the asset written in Contentful
description: # description of the asset written in Contentful
width: # width of the asset (images only)
height: # height of the asset (images only )
If you're using Hugo you can access the information like below:
<img
src="{{ .Params.assetFieldName.url }}"
width="{{ .Params.assetFieldName.width }}"
/>
This same information will also appear in asset arrays like a gallery:
myGallery:
- assetType: 'image/jpg'
url: '//link-to-image.jpg'
title: 'Image 1'
description: 'Image 1 Description'
width: 500
height: 500
- assetType: 'image/jpg'
url: '//link-to-image-2.jpg'
title: 'Image 2'
description: 'Image 2 Description'
width: 1920
height: 1080
Linked entries will include fields for it's id and it's content type id.
linkedEntry:
id: <contentful-entry-id>
typeId: <content-type-ID>
#example with array of linked entries
relatedArticles:
- id: '41UFfIhszbS1kh95bomMj7'
typeId: 'articles'
- id: '85UFfIhsacS1kh71bpqMj7'
typeId: 'articles'
All files are named after their entry id in Contentful making it easy to retrieve it using .Site.GetPage
in Hugo
// if you have access to the "Page" object
{{ with .Site.GetPage "<path-to-file>/<entry-id>" }}
{{ .Title }}
{{ end }}
// if you don't have access to the "Page" object
// for example in a nested partial
{{ with site.GetPage "<path-to-file>/<entry-id>" }}
{{ .Title }}
{{ end }}
Relevant Documentation:
A rich text field that is set as the "mainContent" for a content type will be rendered as markdown for Hugo.
Dynamic content such as embedded-entry-blocks
are rendered as shortcodes with parameters included that can be used to fetch the necessary data.
<!-- example embedded entry -->
<!-- you can use the id, contentType, and parentContentType parameters to fetch the desired data -->
{{< contentful-hugo/embedded-entry id="nTLo2ffSJJp5QrnrO5IU9" contentType="gallery" parentContentType="post" >}}
Before fetching rich text data make sure you have run contentful-hugo --init
so that you will have all the rich text shortcodes. Once you have these shortcodes you can extend and modify them to suit your needs.
The list of rich text short codes includes:
By default the richtext short codes will show a notification for an unconfigured item.
You can customize them by navigating to layouts/shortcodes/contentful-hugo/{shortcode-name}.html
A Rich text field will produce nested arrays mirroring the JSON structure that they have in the API. Each node will need to be looped through and produce HTML depending on the nodeType field.
richTextField:
- nodeType: 'paragraph'
data: {}
content:
- data: {}
marks: []
value: 'This is a simple paragraph.'
nodeType: 'text'
- nodeType: 'paragraph'
data: {}
content:
- data: {}
marks: []
value: 'This is a paragraph with '
nodeType: 'text'
- data: {}
marks:
- type: 'italic'
value: 'italicized text.'
nodeType: 'text'
- nodeType: 'embedded-asset-block'
data:
assetType: 'image/jpeg'
url: '//images.ctfassets.net/some-image-url.jpg'
title: 'Image title will appear here'
description: 'Image description will appear here'
width: 1920
height: 1080
content: []
In addition a plaintext version of the field will be generated using the field ID appended with "_plaintext". This allows you to quickly fetch the text by itself without any of the other data. A simple use case would be using the plaintext output to automatically generate a meta description for a webpage.
richTextField_plaintext: 'This is a simple paragraph. This is a paragraph with italicized text.'
The resolve entries option let's you specify a property from a referenced entry or asset to resolve that field value to. For example say you have a category
content type that is referenced in posts
. Normally contentful-hugo will give the following result
category:
id: some-entry-id
contentType: category
While this makes it easy to find the category, this format does not allow you to use Hugo's built in taxonomy features. With the resolveEntries
parameter you can remedy this.
// from the config file
module.exports = {
repeatableTypes: [
{
id: 'post',
directory: 'content/posts',
resolveEntries: [
{
field: 'category',
resolveTo: 'fields.slug',
},
],
},
],
};
Now the category field will only display the slug as the value.
category: my-category-slug
The resolve entries feature works with both reference fields and asset fields, as well as multiple reference and multiple asset fields.
Overrides can be used to modify field names and field values.
Here's a simple example of changing a field name from "url" to "videoUrl"
repeatableTypes: [
{
id: 'youtubeVideo',
directory: 'content/_youtubeVideo',
isHeadless: true,
overrides: [
{
field: 'url',
options: {
// set new field name in frontmatter
fieldName: 'videoUrl',
},
},
],
},
];
overrides
also has a valueTransformer
options that allows you to manipulate the field data that will appear in frontmatter. valueTransformer
takes a method that has the field value as a parameter and then returns the final result that will appear in the frontmatter. (Be aware that since valueTransformer
must be a method this option will only work in javascript config files)
Here's an example where we change the field name from "url" to "videoId" and then we use the valueTransformer
to extract the video id from the url and then place it in the frontmatter.
repeatableTypes: [
{
id: 'youtubeVideo',
directory: 'content/_youtubeVideo',
isHeadless: true,
overrides: [
{
field: 'url',
options: {
fieldName: 'videoId',
// "value" is whatever value is currently saved in the field.
// in this case it's a url for a youtube video
valueTransformer: (value) => {
const url = new URL(value);
// extract the video id from the url and return it
return url.searchParams.get('v');
},
},
},
],
},
];
When using the valueTransformer
option on fields that contain arrays make sure to loop through the value when manipulating it.
repeatabledTypes: [
{
id: 'post',
directory: 'content/posts',
overrides: [
{
// the author field is a multi-reference field
field: 'authors',
options: {
valueTransformer: (authorRefs) => {
const authors = [];
for (const ref of authorRefs) {
// get the name, photo, and bio of the author
// and add it to the array
authors.push({
name: ref.fields.name,
photo: ref.fields.photo.fields.file.url,
bio: ref.fields.bio,
});
}
return authors;
},
},
},
],
},
];
Now the authors
field will look like this:
authors:
- name: Some Name
photo: //images.cfassets.net/path-to-photo.jpg
bio: some bio text
- name: Some other name
photo: //images.cfassets.net/path-to-photo.jpg
bio: some other bio text
As you can see this can be used to produce similar results to the resolveEntries
parameter, but resolveEntries
can only return one property while with overrides you can do whatever you want with the field values.
You can use to filters
option to enter search parameters allowing you to filter entries based on some of their properties. For more info on Contentful search parameters visit their docs.
Be aware that the following search parameters will be ignored content_type
, skip
, order
, limit
module.exports = {
singleTypes: [
// get a homepage with a specific entryId
{
id: 'homepage',
directory: 'content',
fileName: '_index',
filters: {
'sys.id': 'my-homepace-id'
}
}
]
repeatableTypes: [
// only get events that start after 01/01/2020
{
id: 'events',
directory: 'content/events',
filters: {
'fields.startDate[gte]': '2020-01-01T00:00:00Z',
},
},
// get posts where author is "John Doe" and contains the tag "flowers"
{
id: 'posts',
directory: 'content/posts',
filters: {
'fields.author': 'John Doe',
'fields.tags': 'flowers'
},
},
];
}
You can use the customFields
parameter to add additional fields to your entries. The config for custom fields can be a static value or a method that accepts a Contentful entry as a parameter and returns a value.
Let's say we have an author content type with the following fields:
Here's an example config:
module.exports = {
// rest of config
repeatableTypes: [
{
id: 'author',
directory: 'content/authors',
customFields: {
// both "myCustomField" and "fullName"
// will be appended to the frontmatter for author entries
myCustomField: 'myCustomFieldValue',
fullName: (entry) => {
const { firstName, lastName } = entry.fields;
return `${firstName} ${lastName}`;
},
},
},
],
};
Here's what that config will result in
---
firstName: 'John'
lastName: 'Doe'
slug: 'john-doe'
myCustomField: 'myCustomFieldValue' # custom field
fullName: 'John Doe' # custom field
---
You could also use this for Hugo specific fields like Build Options
// prevent a content type from appearing in list pages
{
customFields: {
_build: {
render: 'alway',
list: 'never',
publishResources: true
}
}
}
// prevent a content type from rendering a single page
{
customFields: {
_build: {
render: 'never',
list: 'always',
publishResources: true
}
}
}
These are some known issues.
--wait
flag to your script. Here's an example where we wait an additional 6 seconds contentful-hugo --wait=6000
.hugo server --disableFastRender
FAQs
Node module that pulls data from Contentful and turns it into markdown files for Hugo. Can be used with other Static Site Generators, but has some Hugo specific features.
The npm package contentful-hugo receives a total of 76 weekly downloads. As such, contentful-hugo popularity was classified as not popular.
We found that contentful-hugo demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.