contentful-hugo
Advanced tools
Comparing version 1.5.4 to 1.6.0
492
index.js
require('dotenv').config(); | ||
const contentful = require('contentful'); | ||
const yaml = require('js-yaml'); | ||
const fs = require('fs'); | ||
const mkdirp = require('mkdirp'); | ||
const yargs = require('yargs'); | ||
const { loadConfig } = require('./src/config'); | ||
const { removeLeadingAndTrailingSlashes } = require('./src/strings'); | ||
yargs.options({ | ||
preview: { type: 'boolean', default: false, alias: 'P' }, | ||
preview: { type: 'boolean', default: false, alias: 'P' }, | ||
init: { type: 'boolean', default: false }, | ||
wait: { type: 'number', default: 0, alias: 'W' }, | ||
config: { type: 'string', default: null, alias: 'C' }, | ||
}); | ||
const argv = yargs.argv; | ||
const richTextToPlain = require('@contentful/rich-text-plain-text-renderer') | ||
.documentToPlainTextString; | ||
const getAssetFields = require('./src/getAssetFields'); | ||
const getEntryFields = require('./src/getEntryFields'); | ||
const richTextNodes = require('./src/richTextNodes'); | ||
const createFile = require('./src/createFile'); | ||
const processEntry = require('./src/processEntry'); | ||
const checkIfFinished = require('./src/checkIfFinished'); | ||
const initializeDirectory = require('./src/initializeDirectory'); | ||
@@ -25,100 +24,105 @@ // counter variables | ||
if ( | ||
process.env.CONTENTFUL_SPACE && | ||
(process.env.CONTENTFUL_TOKEN || process.env.CONTENTFUL_PREVIEW_TOKEN) | ||
if (argv.init) { | ||
initializeDirectory(); | ||
} else if ( | ||
process.env.CONTENTFUL_SPACE && | ||
(process.env.CONTENTFUL_TOKEN || process.env.CONTENTFUL_PREVIEW_TOKEN) | ||
) { | ||
initialize(); | ||
loadConfig('.', argv.config).then(config => { | ||
initialize(config); | ||
}); | ||
} else { | ||
console.log( | ||
`\nERROR: Environment variables not yet set.\n\nThis module requires the following environmental variables to be set before running:\nCONTENTFUL_SPACE, CONTENTFUL_TOKEN, CONTENTFUL_PREVIEW_TOKEN (optional)\n\nYou can set them using the command line or place them in a .env file.\n` | ||
); | ||
console.log( | ||
`\nERROR: Environment variables not yet set.\n\nThis module requires the following environmental variables to be set before running:\nCONTENTFUL_SPACE, CONTENTFUL_TOKEN, CONTENTFUL_PREVIEW_TOKEN (optional)\n\nYou can set them using the command line or place them in a .env file.\n` | ||
); | ||
} | ||
// getting settings from config file | ||
function initialize() { | ||
const configFile = 'contentful-settings.yaml'; | ||
// check if configFile exist and throw error if it doesn't | ||
const deliveryMode = argv.preview ? 'Preview Data' : 'Published Data'; | ||
if (fs.existsSync(configFile)) { | ||
console.log( | ||
`\n---------------------------------------------\n Pulling ${deliveryMode} from Contentful...\n---------------------------------------------\n` | ||
); | ||
try { | ||
const config = yaml.safeLoad( | ||
fs.readFileSync('contentful-settings.yaml') | ||
); | ||
// loop through repeatable content types | ||
const types = config.repeatableTypes; | ||
if (types) { | ||
totalContentTypes += types.length; | ||
for (let i = 0; i < types.length; i++) { | ||
// object to pass settings into the function | ||
const contentSettings = { | ||
typeId: types[i].id, | ||
directory: types[i].directory, | ||
isHeadless: types[i].isHeadless, | ||
fileExtension: types[i].fileExtension, | ||
titleField: types[i].title, | ||
dateField: types[i].dateField, | ||
mainContent: types[i].mainContent, | ||
type: types[i].type, | ||
}; | ||
// check file extension settings | ||
switch (contentSettings.fileExtension) { | ||
case 'md': | ||
case 'yaml': | ||
case 'yml': | ||
case undefined: | ||
case null: | ||
getContentType(1000, 0, contentSettings); | ||
break; | ||
default: | ||
console.log( | ||
` ERROR: extension "${contentSettings.fileExtension}" not supported` | ||
); | ||
break; | ||
} | ||
} | ||
} | ||
// loop through single content types | ||
const singles = config.singleTypes; | ||
if (singles) { | ||
totalContentTypes += singles.length; | ||
for (let i = 0; i < singles.length; i++) { | ||
const single = singles[i]; | ||
const contentSettings = { | ||
typeId: single.id, | ||
directory: single.directory, | ||
fileExtension: single.fileExtension, | ||
fileName: single.fileName, | ||
titleField: single.title, | ||
dateField: single.dateField, | ||
mainContent: single.mainContent, | ||
isSingle: true, | ||
type: single.type, | ||
}; | ||
switch (contentSettings.fileExtension) { | ||
case 'md': | ||
case 'yaml': | ||
case 'yml': | ||
case null: | ||
case undefined: | ||
getContentType(1, 0, contentSettings); | ||
break; | ||
default: | ||
console.log( | ||
` ERROR: extension "${contentSettings.fileExtension}" not supported` | ||
); | ||
break; | ||
} | ||
} | ||
} | ||
} catch (e) { | ||
console.log(e); | ||
} | ||
} else { | ||
console.log( | ||
`\nConfiguration file not found. Create a file called "contentful-settings.yaml" to get started.\nVisit https://github.com/ModiiMedia/contentful-hugo for configuration instructions\n` | ||
); | ||
} | ||
async function initialize(config = null) { | ||
// check if configFile exist and throw error if it doesn't | ||
const deliveryMode = argv.preview ? 'Preview Data' : 'Published Data'; | ||
if (config) { | ||
const waitTime = argv.wait; | ||
if (waitTime && typeof waitTime === 'number') { | ||
console.log(`waiting ${waitTime}ms...`); | ||
await new Promise((resolve, reject) => { | ||
setTimeout(() => { | ||
resolve(); | ||
}, waitTime); | ||
}); | ||
} | ||
console.log( | ||
`\n---------------------------------------------\n Pulling ${deliveryMode} from Contentful...\n---------------------------------------------\n` | ||
); | ||
// loop through repeatable content types | ||
const types = config.repeatableTypes; | ||
if (types) { | ||
totalContentTypes += types.length; | ||
for (let i = 0; i < types.length; i++) { | ||
// object to pass settings into the function | ||
const contentSettings = { | ||
typeId: types[i].id, | ||
directory: types[i].directory, | ||
isHeadless: types[i].isHeadless, | ||
fileExtension: types[i].fileExtension, | ||
titleField: types[i].title, | ||
dateField: types[i].dateField, | ||
mainContent: types[i].mainContent, | ||
type: types[i].type, | ||
}; | ||
// check file extension settings | ||
switch (contentSettings.fileExtension) { | ||
case 'md': | ||
case 'yaml': | ||
case 'yml': | ||
case undefined: | ||
case null: | ||
getContentType(1000, 0, contentSettings); | ||
break; | ||
default: | ||
console.log( | ||
` ERROR: extension "${contentSettings.fileExtension}" not supported` | ||
); | ||
break; | ||
} | ||
} | ||
} | ||
// loop through single content types | ||
const singles = config.singleTypes; | ||
if (singles) { | ||
totalContentTypes += singles.length; | ||
for (let i = 0; i < singles.length; i++) { | ||
const single = singles[i]; | ||
const contentSettings = { | ||
typeId: single.id, | ||
directory: single.directory, | ||
fileExtension: single.fileExtension, | ||
fileName: single.fileName, | ||
titleField: single.title, | ||
dateField: single.dateField, | ||
mainContent: single.mainContent, | ||
isSingle: true, | ||
type: single.type, | ||
}; | ||
switch (contentSettings.fileExtension) { | ||
case 'md': | ||
case 'yaml': | ||
case 'yml': | ||
case null: | ||
case undefined: | ||
getContentType(1, 0, contentSettings); | ||
break; | ||
default: | ||
console.log( | ||
` ERROR: extension "${contentSettings.fileExtension}" not supported` | ||
); | ||
break; | ||
} | ||
} | ||
} | ||
} else { | ||
console.log( | ||
`\nConfiguration file not found. Run "contentful-hugo --init" to get started.\nFor more detailed instructions visit https://github.com/ModiiMedia/contentful-hugo\n` | ||
); | ||
} | ||
} | ||
@@ -129,208 +133,88 @@ | ||
function getContentType(limit, skip, contentSettings, itemsPulled) { | ||
let previewMode = false; | ||
if (argv.preview) { | ||
previewMode = true; | ||
} | ||
if (previewMode && !process.env.CONTENTFUL_PREVIEW_TOKEN) { | ||
throw new Error( | ||
'Environment variable CONTENTFUL_PREVIEW_TOKEN not set' | ||
); | ||
} else if (!previewMode && !process.env.CONTENTFUL_TOKEN) { | ||
throw new Error('Environment variable CONTENTFUL_TOKEN not set'); | ||
} | ||
const options = { | ||
space: process.env.CONTENTFUL_SPACE, | ||
host: previewMode ? 'preview.contentful.com' : 'cdn.contentful.com', | ||
accessToken: previewMode | ||
? process.env.CONTENTFUL_PREVIEW_TOKEN | ||
: process.env.CONTENTFUL_TOKEN, | ||
}; | ||
const client = contentful.createClient(options); | ||
let previewMode = false; | ||
if (argv.preview) { | ||
previewMode = true; | ||
} | ||
if (previewMode && !process.env.CONTENTFUL_PREVIEW_TOKEN) { | ||
throw new Error( | ||
'Environment variable CONTENTFUL_PREVIEW_TOKEN not set' | ||
); | ||
} else if (!previewMode && !process.env.CONTENTFUL_TOKEN) { | ||
throw new Error('Environment variable CONTENTFUL_TOKEN not set'); | ||
} | ||
const options = { | ||
space: process.env.CONTENTFUL_SPACE, | ||
host: previewMode ? 'preview.contentful.com' : 'cdn.contentful.com', | ||
accessToken: previewMode | ||
? process.env.CONTENTFUL_PREVIEW_TOKEN | ||
: process.env.CONTENTFUL_TOKEN, | ||
}; | ||
const client = contentful.createClient(options); | ||
// check for file extension default to markdown | ||
if (!contentSettings.fileExtension) { | ||
contentSettings.fileExtension = 'md'; | ||
} | ||
// check for file extension default to markdown | ||
if (!contentSettings.fileExtension) { | ||
contentSettings.fileExtension = 'md'; | ||
} | ||
client | ||
.getEntries({ | ||
content_type: contentSettings.typeId, | ||
limit: limit, | ||
skip: skip, | ||
order: 'sys.updatedAt', | ||
}) | ||
.then(data => { | ||
// variable for counting number of items pulled | ||
let itemCount; | ||
if (itemsPulled) { | ||
itemCount = itemsPulled; | ||
} else { | ||
itemCount = 0; | ||
} | ||
// create directory for file | ||
mkdirp.sync(`.${contentSettings.directory}`); | ||
client | ||
.getEntries({ | ||
content_type: contentSettings.typeId, | ||
limit: limit, | ||
skip: skip, | ||
order: 'sys.updatedAt', | ||
}) | ||
.then(data => { | ||
// variable for counting number of items pulled | ||
let itemCount; | ||
if (itemsPulled) { | ||
itemCount = itemsPulled; | ||
} else { | ||
itemCount = 0; | ||
} | ||
// create directory for file | ||
mkdirp.sync( | ||
`./${removeLeadingAndTrailingSlashes( | ||
contentSettings.directory | ||
)}` | ||
); | ||
for (let i = 0; i < data.items.length; i++) { | ||
const item = data.items[i]; | ||
const frontMatter = {}; | ||
if (contentSettings.isHeadless) { | ||
frontMatter.headless = true; | ||
mkdirp.sync(`.${contentSettings.directory + item.sys.id}`); | ||
} | ||
if (contentSettings.type) { | ||
frontMatter.type = contentSettings.type; | ||
} | ||
frontMatter.updated = item.sys.updatedAt; | ||
frontMatter.createdAt = item.sys.createdAt; | ||
frontMatter.date = item.sys.createdAt; | ||
for (const field of Object.keys(item.fields)) { | ||
if (field === contentSettings.mainContent) { | ||
// skips to prevent duplicating mainContent in frontmatter | ||
continue; | ||
} else if (field === 'date') { | ||
// convert dates with time to ISO String so Hugo can properly Parse | ||
const d = item.fields[field]; | ||
if (d.length > 10) { | ||
frontMatter.date = new Date(d).toISOString(); | ||
} else { | ||
frontMatter.date = d; | ||
} | ||
continue; | ||
} | ||
const fieldContent = item.fields[field]; | ||
switch (typeof fieldContent) { | ||
case 'object': | ||
if ('sys' in fieldContent) { | ||
frontMatter[field] = {}; | ||
switch (fieldContent.sys.type) { | ||
case 'Asset': | ||
frontMatter[field] = getAssetFields( | ||
fieldContent | ||
); | ||
break; | ||
case 'Entry': | ||
frontMatter[field] = getEntryFields( | ||
fieldContent | ||
); | ||
break; | ||
default: | ||
frontMatter[field] = fieldContent; | ||
break; | ||
} | ||
} | ||
// rich text (see rich text function) | ||
else if ('nodeType' in fieldContent) { | ||
frontMatter[field] = []; | ||
frontMatter[ | ||
`${field}_plaintext` | ||
] = richTextToPlain(fieldContent); | ||
const nodes = fieldContent.content; | ||
for (let i = 0; i < nodes.length; i++) { | ||
frontMatter[field].push( | ||
richTextNodes(nodes[i]) | ||
); | ||
} | ||
} | ||
// arrays | ||
else { | ||
if (!fieldContent.length) { | ||
frontMatter[field] = fieldContent; | ||
} else { | ||
frontMatter[field] = []; | ||
for ( | ||
let i = 0; | ||
i < fieldContent.length; | ||
i++ | ||
) { | ||
const arrayNode = fieldContent[i]; | ||
switch (typeof arrayNode) { | ||
case 'object': { | ||
let arrayObject = {}; | ||
switch (arrayNode.sys.type) { | ||
case 'Asset': | ||
arrayObject = getAssetFields( | ||
arrayNode | ||
); | ||
frontMatter[field].push( | ||
arrayObject | ||
); | ||
break; | ||
case 'Entry': | ||
arrayObject = getEntryFields( | ||
arrayNode | ||
); | ||
frontMatter[field].push( | ||
arrayObject | ||
); | ||
break; | ||
default: | ||
frontMatter[field].push( | ||
arrayNode | ||
); | ||
break; | ||
} | ||
break; | ||
} | ||
default: | ||
frontMatter[field].push( | ||
arrayNode | ||
); | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
break; | ||
default: | ||
frontMatter[field] = item.fields[field]; | ||
break; | ||
} | ||
} | ||
let mainContent = null; | ||
if (item.fields[contentSettings.mainContent]) { | ||
mainContent = `${item.fields[contentSettings.mainContent]}`; | ||
} | ||
for (let i = 0; i < data.items.length; i++) { | ||
const item = data.items[i]; | ||
processEntry(item, contentSettings); | ||
itemCount++; | ||
} | ||
createFile( | ||
contentSettings, | ||
item.sys.id, | ||
frontMatter, | ||
mainContent | ||
); | ||
itemCount++; | ||
} | ||
// check total number of items against number of items pulled in API | ||
if (data.total > data.limit) { | ||
// run function again if there are still more items to get | ||
const newSkip = skip + limit; | ||
getContentType(limit, newSkip, contentSettings, itemCount); | ||
} else { | ||
let grammarStuff; | ||
if (Number(data.total) === 1) { | ||
grammarStuff = 'item'; | ||
} else { | ||
grammarStuff = 'items'; | ||
} | ||
console.log( | ||
` ${contentSettings.typeId} - ${itemCount} ${grammarStuff}` | ||
); | ||
typesExtracted++; | ||
if (checkIfFinished(typesExtracted, totalContentTypes)) { | ||
console.log( | ||
`\n---------------------------------------------\n` | ||
); | ||
} | ||
} | ||
}) | ||
.catch(error => { | ||
const response = error.response; | ||
if (response) { | ||
console.log( | ||
` --------------------------\n ${contentSettings.typeId} - ERROR ${response.status} ${response.statusText}\n (Note: ${response.data.message})\n --------------------------` | ||
); | ||
} else { | ||
console.log(error); | ||
} | ||
}); | ||
// check total number of items against number of items pulled in API | ||
if (data.total > data.limit) { | ||
// run function again if there are still more items to get | ||
const newSkip = skip + limit; | ||
getContentType(limit, newSkip, contentSettings, itemCount); | ||
} else { | ||
let grammarStuff; | ||
if (Number(data.total) === 1) { | ||
grammarStuff = 'item'; | ||
} else { | ||
grammarStuff = 'items'; | ||
} | ||
console.log( | ||
` ${contentSettings.typeId} - ${itemCount} ${grammarStuff}` | ||
); | ||
typesExtracted++; | ||
if (checkIfFinished(typesExtracted, totalContentTypes)) { | ||
console.log( | ||
`\n---------------------------------------------\n` | ||
); | ||
} | ||
} | ||
}) | ||
.catch(error => { | ||
const response = error.response; | ||
if (response) { | ||
console.log( | ||
` --------------------------\n ${contentSettings.typeId} - ERROR ${response.status} ${response.statusText}\n (Note: ${response.data.message})\n --------------------------` | ||
); | ||
} else { | ||
console.log(error); | ||
} | ||
}); | ||
} |
126
package.json
{ | ||
"name": "contentful-hugo", | ||
"version": "1.5.4", | ||
"description": "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.", | ||
"main": "index.js", | ||
"repository": { | ||
"typ": "git", | ||
"url": "https://github.com/ModiiMedia/contentful-hugo" | ||
}, | ||
"keywords": [ | ||
"hugo", | ||
"contentful", | ||
"blog", | ||
"markdown", | ||
"yaml", | ||
"ssg", | ||
"website", | ||
"static-site-generator", | ||
"jamstack", | ||
"frontmatter", | ||
"static-site" | ||
], | ||
"author": { | ||
"name": "Joshua Sosso", | ||
"email": "josh@modiimedia.com", | ||
"url": "https://www.modiimedia.com" | ||
}, | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1", | ||
"lint": "eslint .", | ||
"lint:fix": "eslint . --fix" | ||
}, | ||
"license": "ISC", | ||
"dependencies": { | ||
"@contentful/rich-text-plain-text-renderer": "^13.4.0", | ||
"contentful": "^7.14.0", | ||
"dotenv": "^7.0.0", | ||
"js-yaml": "^3.13.1", | ||
"json-to-pretty-yaml": "^1.2.2", | ||
"mkdirp": "^0.5.3", | ||
"yargs": "^15.3.1" | ||
}, | ||
"bin": { | ||
"contentful-hugo": "./cli.js" | ||
}, | ||
"files": [ | ||
"cli.js", | ||
"images", | ||
"readme.md", | ||
"index.js", | ||
"src" | ||
], | ||
"devDependencies": { | ||
"eslint": "^6.8.0", | ||
"eslint-config-prettier": "^6.10.0", | ||
"eslint-config-standard": "^14.1.0", | ||
"eslint-plugin-import": "^2.20.1", | ||
"eslint-plugin-node": "^10.0.0", | ||
"eslint-plugin-promise": "^4.2.1", | ||
"eslint-plugin-standard": "^4.0.1", | ||
"prettier": "^1.19.1" | ||
} | ||
"name": "contentful-hugo", | ||
"version": "1.6.0", | ||
"description": "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.", | ||
"main": "index.js", | ||
"repository": { | ||
"typ": "git", | ||
"url": "https://github.com/ModiiMedia/contentful-hugo" | ||
}, | ||
"keywords": [ | ||
"hugo", | ||
"contentful", | ||
"blog", | ||
"markdown", | ||
"yaml", | ||
"ssg", | ||
"website", | ||
"static-site-generator", | ||
"jamstack", | ||
"frontmatter", | ||
"static-site" | ||
], | ||
"author": { | ||
"name": "Joshua Sosso", | ||
"email": "josh@modiimedia.com", | ||
"url": "https://www.modiimedia.com" | ||
}, | ||
"scripts": { | ||
"test": "jest --coverage", | ||
"lint": "eslint .", | ||
"lint:fix": "eslint . --fix", | ||
"postinstall": "node postinstall.js" | ||
}, | ||
"license": "ISC", | ||
"dependencies": { | ||
"@contentful/rich-text-html-renderer": "^13.4.0", | ||
"@contentful/rich-text-plain-text-renderer": "^13.4.0", | ||
"@contentful/rich-text-types": "^14.1.0", | ||
"contentful": "^7.14.6", | ||
"dotenv": "^7.0.0", | ||
"js-yaml": "^3.14.0", | ||
"json-to-pretty-yaml": "^1.2.2", | ||
"mkdirp": "^0.5.5", | ||
"yargs": "^15.4.1" | ||
}, | ||
"bin": { | ||
"contentful-hugo": "./cli.js" | ||
}, | ||
"files": [ | ||
"cli.js", | ||
"images", | ||
"readme.md", | ||
"index.js", | ||
"src" | ||
], | ||
"devDependencies": { | ||
"eslint": "^6.8.0", | ||
"eslint-config-prettier": "^6.11.0", | ||
"eslint-config-standard": "^14.1.1", | ||
"eslint-plugin-import": "^2.22.0", | ||
"eslint-plugin-node": "^10.0.0", | ||
"eslint-plugin-promise": "^4.2.1", | ||
"eslint-plugin-standard": "^4.0.1", | ||
"jest": "^26.2.2", | ||
"prettier": "^1.19.1" | ||
} | ||
} |
131
readme.md
@@ -14,2 +14,4 @@ # Contentful Hugo | ||
- [Expected Output](#Expected-Output) | ||
- [Standard Fields](#Default-Date-and-Time-Fields) | ||
- [Richtext Fields](#Rich-Text-As-Main-Content) | ||
- [Known Issues](#Known-Issues) | ||
@@ -44,2 +46,6 @@ | ||
```powershell | ||
## initialize the directory | ||
contentful-hugo --init | ||
## fetch from contentful | ||
contentful-hugo | ||
@@ -51,10 +57,14 @@ ``` | ||
```powershell | ||
npx contentful-hugo --init | ||
npx contentful-hugo | ||
``` | ||
### Optional Flags | ||
### Flags | ||
| flag | aliases | description | | ||
| --------- | ------- | ---------------------------------------------------------------------------------------- | | ||
| --preview | -P | runs in preview mode, which pulls both published and unpublished entries from Contentful | | ||
| 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 | Specific the path to a config file. | | ||
@@ -67,2 +77,8 @@ #### Preview Mode Example | ||
#### Multiple Flags Example | ||
```powershell | ||
contentful-hugo --wait=2000 --preview --config="my_custom_config.js" | ||
``` | ||
### Example Package.json | ||
@@ -128,7 +144,57 @@ | ||
In order to pull the data you want you will need to create a **contentful-settings.yaml** file in the root of your repository. | ||
In order to pull the data you want 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. | ||
Example **contentful-settings.yaml** file (see below for complete configuration options) | ||
- `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) | ||
#### Example Javascript Config | ||
```javascript | ||
// contentful-hugo.config.js | ||
module.exports = { | ||
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', | ||
}, | ||
{ | ||
id: 'seoFields', | ||
isHeadless: true, | ||
directory: 'content/seo-fields', | ||
}, | ||
{ | ||
id: 'reviews', | ||
directory: 'content/reviews', | ||
mainContent: 'reviewBody', | ||
}, | ||
], | ||
}; | ||
``` | ||
#### Example YAML Config | ||
```yaml | ||
# contentful-hugo.config.yaml | ||
singleTypes: | ||
@@ -138,3 +204,3 @@ # fetches only the most recently updated entry in a particular content type | ||
- id: homepage | ||
directory: /content/ | ||
directory: content | ||
fileName: _index | ||
@@ -145,3 +211,3 @@ fileExtension: md | ||
- id: siteSettings | ||
directory: /data/ | ||
directory: data | ||
fileName: settings | ||
@@ -155,3 +221,3 @@ fileExtension: yaml | ||
- id: posts | ||
directory: /content/posts/ | ||
directory: content/posts | ||
fileExtension: md | ||
@@ -162,6 +228,6 @@ mainContent: content | ||
isHeadless: true | ||
directory: /content/seo-fields/ | ||
directory: content/seo-fields | ||
- id: reviews | ||
directory: /content/reviews/ | ||
directory: content/reviews | ||
mainContent: reviewBody | ||
@@ -171,6 +237,6 @@ | ||
isHeadless: true | ||
directory: /content/staff/ | ||
directory: content/staff | ||
``` | ||
**Configuration Options** | ||
#### **Config File Options** | ||
@@ -180,3 +246,3 @@ | field | required | description | | ||
| id | required | contentful content type ID goes here | | ||
| directory | required | directory where you want the file(s) to be generated (leading and trailing slashes required for the time being) | | ||
| directory | required | directory where you want the file(s) to be generated | | ||
| fileName | required (single types only) | name of the file generated | | ||
@@ -220,4 +286,4 @@ | fileExtension | optional | can be "md", "yml", or "yaml" (defaults to "md") | | ||
<img | ||
src="{{ .Params.assetFieldName.url }}" | ||
width="{{ .Params.assetFieldName.width }}" | ||
src="{{ .Params.assetFieldName.url }}" | ||
width="{{ .Params.assetFieldName.width }}" | ||
/> | ||
@@ -270,4 +336,33 @@ ``` | ||
### Rich Text Fields | ||
### Rich Text As Main Content | ||
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. | ||
```md | ||
<!-- 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: | ||
- contentful-hugo/asset-hyperlink.html | ||
- contentful-hugo/embedded-asset.html | ||
- contentful-hugo/embedded-entry.html | ||
- contentful-hugo/entry-hyperlink.html | ||
- contentful-hugo/inline-entry.html | ||
By default the richtext short codes will show a notification for an unconfigured item. | ||
![Unconfigured Embedded Entry Block](images/unconfigured-embedded-entry-block.jpg) | ||
You can customize them by navigating to `layouts/shortcodes/contentful-hugo/{shortcode-name}.html` | ||
### Rich Text In FrontMatter | ||
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. | ||
@@ -318,2 +413,2 @@ | ||
- **Date & Time Field w/o Timezone**: Date fields that include time but do not have a specified timezone will have a timezone set based on whatever machine the script is run on. So using a date field in contentful with this setting could lead to unexpected results when formatting dates. Date fields that don't include time (ex: YYYY-MM-DD) are not effected by this. | ||
- **Fetching Data Before Contentful CDN Updates**: Sometimes when triggering a build from a webhook, it won't always get the latest data. This is because it sometimes takes a couple seconds for the latest data to get distrubuted across Contentful's CDN. If you run into this issue it might be worth it to create a "wait function" just to delay fetching the data by a couple seconds. You could include it in the script you use contentful-hugo by doing something like the following `"node wait.js && contentful-hugo"` | ||
- **Fetching Data Before Contentful CDN Updates**: Sometimes when triggering a build from a webhook, it won't always get the latest data. This is because it sometimes takes a couple seconds for the latest data to get distrubuted across Contentful's CDN. If you run into this issue add teh the `--wait` flag to your script. Here's an example where we wait an additional 6 seconds `contentful-hugo --wait=6000`. |
const checkIfFinished = (num, totalContentTypes) => { | ||
if (num === totalContentTypes) { | ||
return true; | ||
} else { | ||
return false; | ||
} | ||
if (num === totalContentTypes) { | ||
return true; | ||
} else { | ||
return false; | ||
} | ||
}; | ||
module.exports = checkIfFinished; |
const YAML = require('json-to-pretty-yaml'); | ||
const fs = require('fs'); | ||
const { removeLeadingAndTrailingSlashes } = require('./strings'); | ||
const mkdirp = require('mkdirp'); | ||
module.exports = (contentSettings, entryId, frontMatter, mainContent) => { | ||
let fileContent = ''; | ||
if ( | ||
contentSettings.fileExtension === 'md' || | ||
contentSettings.fileExtension === null || | ||
contentSettings.fileExtension === undefined | ||
) { | ||
fileContent += `---\n`; | ||
} | ||
/** | ||
* | ||
* @param {Object} contentSettings - Content settings object | ||
* @param {String} entryId - The id of the Contentful entry | ||
* @param {Object} frontMatter - Object containing all the data for frontmatter | ||
* @param {String} mainContent - String data for the main content that will appear below the frontmatter | ||
*/ | ||
const createFile = (contentSettings, entryId, frontMatter, mainContent) => { | ||
let fileContent = ''; | ||
if ( | ||
contentSettings.fileExtension === 'md' || | ||
contentSettings.fileExtension === null || | ||
contentSettings.fileExtension === undefined | ||
) { | ||
fileContent += `---\n`; | ||
} | ||
// add current item to filecontent | ||
fileContent += YAML.stringify(frontMatter); | ||
if ( | ||
contentSettings.fileExtension !== 'yaml' || | ||
contentSettings.fileExtension !== 'yml' | ||
) { | ||
fileContent += `---\n`; | ||
} | ||
// add current item to filecontent | ||
fileContent += YAML.stringify(frontMatter); | ||
if ( | ||
contentSettings.fileExtension !== 'yaml' || | ||
contentSettings.fileExtension !== 'yml' | ||
) { | ||
fileContent += `---\n`; | ||
} | ||
// if set add the main content below the front matter | ||
if (mainContent) { | ||
fileContent += mainContent; | ||
} | ||
// if set add the main content below the front matter | ||
if (mainContent) { | ||
fileContent += mainContent; | ||
} | ||
// create file | ||
let filePath = ''; | ||
if (contentSettings.isHeadless) { | ||
filePath = `.${contentSettings.directory}/${entryId}/index.${contentSettings.fileExtension}`; | ||
} else if (contentSettings.isSingle) { | ||
filePath = `.${contentSettings.directory}/${contentSettings.fileName}.${contentSettings.fileExtension}`; | ||
} else { | ||
filePath = `.${contentSettings.directory}${entryId}.${contentSettings.fileExtension}`; | ||
} | ||
return fs.writeFile(filePath, fileContent, error => { | ||
if (error) { | ||
console.log(error); | ||
} | ||
}); | ||
// create file | ||
let filePath = ''; | ||
const directory = removeLeadingAndTrailingSlashes( | ||
contentSettings.directory | ||
); | ||
const { fileExtension, fileName, isSingle, isHeadless } = contentSettings; | ||
if (isHeadless) { | ||
mkdirp.sync(`./${directory}/${entryId}`); | ||
filePath = `./${directory}/${entryId}/index.${fileExtension}`; | ||
} else if (isSingle) { | ||
filePath = `./${directory}/${fileName}.${fileExtension}`; | ||
} else { | ||
filePath = `./${directory}/${entryId}.${fileExtension}`; | ||
} | ||
return fs.writeFile(filePath, fileContent, error => { | ||
if (error) { | ||
console.log(error); | ||
} | ||
}); | ||
}; | ||
module.exports = createFile; |
@@ -1,20 +0,28 @@ | ||
module.exports = contentfulObject => { | ||
const frontMatter = {}; | ||
let assetType = ''; | ||
if (contentfulObject.fields.file) { | ||
assetType = contentfulObject.fields.file.contentType; | ||
} | ||
frontMatter.assetType = assetType; | ||
frontMatter.url = contentfulObject.fields.file.url; | ||
frontMatter.title = contentfulObject.fields.title; | ||
frontMatter.description = contentfulObject.fields.description; | ||
/** | ||
* | ||
* @param {Object} contentfulObject | ||
* @param {Object} contentfulObject.sys | ||
* @param {Object} contentfulObject.fields | ||
*/ | ||
const getAssetFields = contentfulObject => { | ||
const frontMatter = {}; | ||
let assetType = ''; | ||
if (contentfulObject.fields.file) { | ||
assetType = contentfulObject.fields.file.contentType; | ||
} | ||
frontMatter.assetType = assetType; | ||
frontMatter.url = contentfulObject.fields.file.url; | ||
frontMatter.title = contentfulObject.fields.title; | ||
frontMatter.description = contentfulObject.fields.description; | ||
// get specific details depending on the asset type | ||
const details = contentfulObject.fields.file.details; | ||
if (assetType.includes('image')) { | ||
// image height and width | ||
frontMatter.width = details.image.width; | ||
frontMatter.height = details.image.height; | ||
} | ||
return frontMatter; | ||
// get specific details depending on the asset type | ||
const details = contentfulObject.fields.file.details; | ||
if (assetType.includes('image')) { | ||
// image height and width | ||
frontMatter.width = details.image.width; | ||
frontMatter.height = details.image.height; | ||
} | ||
return frontMatter; | ||
}; | ||
module.exports = getAssetFields; |
module.exports = entry => { | ||
let obj = {}; | ||
if (entry.sys) { | ||
obj = { | ||
id: entry.sys.id, | ||
contentType: entry.sys.contentType.sys.id, | ||
}; | ||
} | ||
return obj; | ||
let obj = {}; | ||
if (entry.sys) { | ||
obj = { | ||
id: entry.sys.id, | ||
contentType: entry.sys.contentType.sys.id, | ||
}; | ||
} | ||
return obj; | ||
}; |
const getEntryFields = require('./getEntryFields'); | ||
const getAssetFields = require('./getAssetFields'); | ||
const mapDataNode = node => { | ||
const { target } = node; | ||
if (target) { | ||
if (target.sys) { | ||
switch (target.sys.type) { | ||
case 'Entry': | ||
return getEntryFields(target); | ||
case 'Asset': | ||
return getAssetFields(target); | ||
} | ||
} else { | ||
console.log(node); | ||
} | ||
} | ||
return node; | ||
}; | ||
const mapContentNode = node => { | ||
const contentArr = []; | ||
for (const item of node) { | ||
contentArr.push(richTextNodes(item)); | ||
} | ||
return contentArr; | ||
}; | ||
const mapMarks = node => { | ||
const markArr = []; | ||
for (const item of node) { | ||
markArr.push(item.type); | ||
} | ||
return markArr; | ||
}; | ||
const richTextNodes = node => { | ||
const fieldContent = {}; | ||
for (const field of Object.keys(node)) { | ||
switch (field) { | ||
case 'data': { | ||
const t = node[field].target; | ||
if (t) { | ||
if (t.sys) { | ||
switch (t.sys.type) { | ||
case 'Entry': | ||
fieldContent[field] = getEntryFields(t); | ||
break; | ||
case 'Asset': | ||
fieldContent[field] = getAssetFields(t); | ||
break; | ||
} | ||
} else { | ||
console.log(node[field]); | ||
} | ||
} else { | ||
fieldContent[field] = node[field]; | ||
} | ||
break; | ||
} | ||
case 'content': { | ||
const contentArr = []; | ||
for (const item of node[field]) { | ||
contentArr.push(richTextNodes(item)); | ||
} | ||
fieldContent[field] = contentArr; | ||
break; | ||
} | ||
case 'marks': { | ||
const markArr = []; | ||
for (const item of node[field]) { | ||
markArr.push(item.type); | ||
} | ||
fieldContent[field] = markArr; | ||
break; | ||
} | ||
default: | ||
fieldContent[field] = node[field]; | ||
break; | ||
} | ||
} | ||
return fieldContent; | ||
const fieldContent = {}; | ||
for (const field of Object.keys(node)) { | ||
const subNode = node[field]; | ||
switch (field) { | ||
case 'data': { | ||
fieldContent[field] = mapDataNode(subNode); | ||
break; | ||
} | ||
case 'content': { | ||
fieldContent[field] = mapContentNode(subNode); | ||
break; | ||
} | ||
case 'marks': { | ||
fieldContent[field] = mapMarks(subNode); | ||
break; | ||
} | ||
default: | ||
fieldContent[field] = node[field]; | ||
break; | ||
} | ||
} | ||
return fieldContent; | ||
}; | ||
module.exports = richTextNodes; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Install scripts
Supply chain riskInstall scripts are run when the package is installed. The majority of malware in npm is hidden in install scripts.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
146606
30
1588
1
401
9
9
1
14
1
+ Added@contentful/rich-text-html-renderer@13.4.0(transitive)
+ Added@contentful/rich-text-types@14.1.2(transitive)
+ Addedescape-html@1.0.3(transitive)
Updatedcontentful@^7.14.6
Updatedjs-yaml@^3.14.0
Updatedmkdirp@^0.5.5
Updatedyargs@^15.4.1