UNPKG

contentful-hugo

Version:

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.

1,484 lines (1,446 loc) 43.9 kB
'use strict'; const Limiter = require('async-limiter'); const path = require('path'); const node_fs = require('node:fs'); const yaml = require('js-yaml'); const c12 = require('c12'); const fs = require('fs-extra'); const contentful = require('contentful'); const richTextPlainTextRenderer = require('@contentful/rich-text-plain-text-renderer'); const richTextTypes = require('@contentful/rich-text-types'); const richTextHtmlRenderer = require('@contentful/rich-text-html-renderer'); const YAML = require('json-to-pretty-yaml'); const fs$1 = require('fs'); const url = require('url'); const promises = require('node:fs/promises'); var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null; function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; } function _interopNamespaceCompat(e) { if (e && typeof e === 'object' && 'default' in e) return e; const n = Object.create(null); if (e) { for (const k in e) { n[k] = e[k]; } } n.default = e; return n; } const Limiter__default = /*#__PURE__*/_interopDefaultCompat(Limiter); const path__default = /*#__PURE__*/_interopDefaultCompat(path); const yaml__default = /*#__PURE__*/_interopDefaultCompat(yaml); const c12__namespace = /*#__PURE__*/_interopNamespaceCompat(c12); const fs__default = /*#__PURE__*/_interopDefaultCompat(fs); const contentful__namespace = /*#__PURE__*/_interopNamespaceCompat(contentful); const YAML__namespace = /*#__PURE__*/_interopNamespaceCompat(YAML); const fs__default$1 = /*#__PURE__*/_interopDefaultCompat(fs$1); const removeLeadingAndTrailingSlashes = (string) => string.replace(/^\/+|\/+$/g, ""); const isMultilineString = (string) => { const array = string.split(` `); if (array.length && array.length > 1) { return true; } return false; }; const specialEntities = { "&quot;": '"', "&apos;": "'", "&amp;": "&", "&lt;": "<", "&gt;": ">", "&circ;": "^", "&nbsp;": " " }; const replaceSpecialEntities = (string) => { const checks = Object.keys(specialEntities); let finalString = string; for (const check of checks) { const regex = new RegExp(`${check}`, "g"); const replacementValue = specialEntities[check]; if (typeof replacementValue === "string") { finalString = finalString.replace(regex, replacementValue); } } return finalString; }; const characterIsWhiteSpace = (char) => /\s/.test(char); const leadingSpaces = (string, count = 0) => { if (characterIsWhiteSpace(string.charAt(0))) { return leadingSpaces(string.slice(1), count + 1); } let removedSpaces = ""; for (let i = 0; i < count; i++) { removedSpaces += " "; } return { exists: count > 0, newString: string, removedSpaces, count }; }; const trailingSpaces = (string, count = 0) => { const str = string.replace(/\s/g, " "); if (characterIsWhiteSpace(str.charAt(str.length - 1))) { return trailingSpaces(str.slice(0, -1), count + 1); } let removedSpaces = ""; for (let i = 0; i < count; i++) { removedSpaces += " "; } return { exists: count > 0, newString: str, removedSpaces, count }; }; const endsWith = (str, ext) => new RegExp(`${ext}$`).test(str); const replaceBackslashesWithForwardSlashes = (input) => input.replace(/\\/g, "/"); const determineFileType = (fileName) => { const splitStr = fileName.split("."); const fileExtension = splitStr[splitStr.length - 1]; switch (fileExtension) { case "js": return "javascript"; case "ts": return "typescript"; case "yaml": case "yml": return "yaml"; default: return null; } }; const isValidFileExtension = (extension) => ["md", "yaml", "yml", "json"].some( (ext) => endsWith(extension || "md", ext) ); const isTypeConfig = (input) => { const mappedInput = input; if (!Array.isArray(mappedInput)) { return false; } for (const item of mappedInput) { if (typeof item.id !== "string") { return false; } if (typeof item.directory !== "string") { return false; } } return true; }; const isContentfulHugoConfig = (input) => { const mappedInput = input; const { contentful, singleTypes, repeatableTypes } = mappedInput; if (!contentful) { return false; } if (typeof contentful.space !== "string" || typeof contentful.environment !== "string" || typeof contentful.token !== "string") { return false; } if (!isTypeConfig(singleTypes)) { return false; } if (!isTypeConfig(repeatableTypes)) { return false; } return true; }; const checkContentfulSettings = (config) => { let { contentful } = config; if (!contentful) { contentful = {}; } const space = contentful.space || process.env.CONTENTFUL_SPACE || ""; const token = contentful.token || process.env.CONTENTFUL_TOKEN || ""; const previewToken = contentful.previewToken || process.env.CONTENTFUL_PREVIEW_TOKEN || ""; const environment = contentful.environment || "master"; const newConfig = { locales: config.locales || [], contentful: { space, token, previewToken, environment, pageSize: contentful.pageSize }, singleTypes: config.singleTypes || [], repeatableTypes: config.repeatableTypes || [], staticContent: config.staticContent || [] }; return newConfig; }; const loadJavascriptConfigFile = async (filePath) => { let { config } = await c12__namespace.loadConfig({ configFile: filePath }); if (config && typeof config === "object") { config = checkContentfulSettings(config); if (isContentfulHugoConfig(config)) { return config; } } return null; }; const loadYamlConfigFile = (filePath) => { const file = node_fs.readFileSync(filePath); let configObject = yaml__default.load(file.toString()); if (configObject && typeof configObject === "object") { configObject = checkContentfulSettings(configObject); if (isContentfulHugoConfig(configObject)) { return configObject; } } return null; }; const loadFile = async (rootDir = ".", fileName = "") => { const filePath = path__default.resolve(rootDir, fileName); if (node_fs.existsSync(filePath)) { const fileType = determineFileType(fileName); if (fileType === "javascript" || fileType === "typescript") { return loadJavascriptConfigFile(filePath); } if (fileType === "yaml") { return loadYamlConfigFile(filePath); } } return null; }; const getResolveEntryConfigs = (data) => { if (Array.isArray(data)) { return data; } if (typeof data === "object") { const configs = []; Object.keys(data).forEach((key) => { configs.push({ field: key, resolveTo: data[key] }); }); return configs; } return []; }; const getOverrideConfigs = (data) => { if (Array.isArray(data)) { return data; } if (typeof data === "object") { const configs = []; Object.keys(data).forEach((key) => { configs.push({ field: key, options: data[key] }); }); return configs; } return []; }; const loadConfig = async (rootDir = ".", fileName = "") => { rootDir = path__default.resolve(rootDir); if (fileName) { const result = await loadFile(rootDir, fileName); if (result) { const config = checkContentfulSettings(result); return config; } throw new Error(`${fileName} does not exist or it is empty.`); } const defaultConfigNames = [ "contentful-hugo.config.ts", "contentful-hugo.config.js", "contentful-hugo.config.yaml", "contentful-hugo.yaml", "contentful-settings.yaml" ]; const tasks = []; const configMap = {}; for (const name of defaultConfigNames) { const file = loadFile(rootDir, name).then((result) => { if (result) { const conf = checkContentfulSettings(result); configMap[name] = conf; return; } configMap[name] = null; }); tasks.push(file); } await Promise.all(tasks); for (const filename of defaultConfigNames) { if (configMap[filename]) { console.log(`Using config at ${filename}`); return configMap[filename]; } } return false; }; const defineConfig = (config) => config; const getEntryFields = (entry) => { let obj = {}; if (entry.sys) { obj = { id: entry.sys.id, // contentType is missing if an deleted entry get's referenced contentType: entry.sys.contentType?.sys.id }; } return obj; }; const parseField = (field) => { if (typeof field === "object" && // eslint-disable-next-line @typescript-eslint/no-explicit-any (field?.["en-US"] || field?.["en-us"])) { return field?.["en-US"] ?? field?.["en-us"]; } return field; }; const getAssetFields = (contentfulObject) => { const frontMatter = { assetType: parseField(contentfulObject.fields.file)?.contentType || "", url: parseField(contentfulObject.fields.file)?.url || "", title: parseField(contentfulObject.fields.title) || "", description: parseField(contentfulObject.fields.description) || "", size: parseField(contentfulObject.fields.file)?.details.size, width: parseField(contentfulObject.fields.file)?.details.image?.width, height: parseField(contentfulObject.fields.file)?.details.image?.height }; return frontMatter; }; const mapEntry = (target) => ({ id: target.sys.id, // contentType doesn't exist if the entry is "missing or inaccessible" contentType: target.sys.contentType?.sys?.id }); const mapAsset = (target) => { const { title, description, file } = target.fields; const { url, details, fileName, contentType } = parseField(file) ?? { url: "", details: { size: 0 }, fileName: "", contentType: "" }; const asset = { title: typeof title === "string" ? title : title?.["en-US"] ?? "", description: typeof description === "string" ? description : description?.["en-US"] ?? "", url, fileName, assetType: contentType, size: details.size, width: details.image?.width ?? null, height: details.image?.height ?? null }; return asset; }; const optionsRenderNode = (parentContentType = "") => ({ [richTextTypes.BLOCKS.HEADING_1]: (node, next) => `# ${next(node.content)} `, [richTextTypes.BLOCKS.HEADING_2]: (node, next) => `## ${next(node.content)} `, [richTextTypes.BLOCKS.HEADING_3]: (node, next) => `### ${next(node.content)} `, [richTextTypes.BLOCKS.HEADING_4]: (node, next) => `#### ${next(node.content)} `, [richTextTypes.BLOCKS.HEADING_5]: (node, next) => `##### ${next(node.content)} `, [richTextTypes.BLOCKS.HEADING_6]: (node, next) => `###### ${next(node.content)} `, [richTextTypes.BLOCKS.PARAGRAPH]: (node, next) => `${next(node.content)} `, [richTextTypes.BLOCKS.QUOTE]: (node, next) => { const string = next(node.content); const lines = string.split(` `); let finalString = ""; for (let i = 0; i < lines.length; i++) { const line = lines[i]; finalString += `> ${line} `; } const removeExtraSpace = finalString.substr(0, finalString.length - 6); return `${removeExtraSpace} `; }, [richTextTypes.BLOCKS.OL_LIST]: (node, next) => { let string = ``; for (let i = 0; i < node.content.length; i++) { const item = node.content[i]; string += `${i + 1}. ${next(item.content)}`; } string = string.replace(/\n\n/g, ` `); return `${string} `; }, [richTextTypes.BLOCKS.UL_LIST]: (node, next) => { let string = ``; for (let i = 0; i < node.content.length; i++) { const item = node.content[i]; string += `- ${next(item.content)}`; } string = string.replace(/\n\n/g, ` `); return `${string} `; }, [richTextTypes.BLOCKS.HR]: (_node, _next) => `--- `, [richTextTypes.BLOCKS.EMBEDDED_ASSET]: (node, _next) => { const { title, description, url, fileName, assetType, size, width, height } = mapAsset(node.data.target); const handleQuotes = (string) => { const regex = new RegExp(/"/, "g"); return string.replace(regex, '\\"'); }; return `{{< contentful-hugo/embedded-asset title="${handleQuotes( title )}" description="${handleQuotes(description || "") || ""}" url="${url || ""}" filename="${fileName || ""}" assetType="${assetType || ""}" size="${size || ""}" width="${width || ""}" height="${height || ""}" parentContentType="${parentContentType || ""}" >}} `; }, // eslint-disable-next-line @typescript-eslint/no-unused-vars [richTextTypes.BLOCKS.EMBEDDED_ENTRY]: (node, next) => { const { id, contentType } = mapEntry(node.data.target); return `{{< contentful-hugo/embedded-entry id="${id}" contentType="${contentType}" parentContentType="${parentContentType || ""}" >}} `; }, [richTextTypes.INLINES.HYPERLINK]: (node, next) => `[${next(node.content)}](${node.data.uri})`, [richTextTypes.INLINES.ASSET_HYPERLINK]: (node, next) => { const { title, description, url, fileName, assetType, size, width, height } = mapAsset(node.data.target); return `{{< contentful-hugo/asset-hyperlink title="${title}" description="${description || ""}" url="${url || ""}" filename="${fileName || ""}" assetType="${assetType || ""}" size="${size || ""}" width="${width || ""}" height="${height || ""}" parentContentType="${parentContentType || ""}" >}}${next( node.content )}{{< /contentful-hugo/asset-hyperlink >}}`; }, [richTextTypes.INLINES.ENTRY_HYPERLINK]: (node, next) => { const { id, contentType } = mapEntry(node.data.target); return `{{< contentful-hugo/entry-hyperlink id="${id}" contentType="${contentType}" parentContentType="${parentContentType || ""}" >}}${next(node.content)}{{< /contentful-hugo/entry-hyperlink >}}`; }, // eslint-disable-next-line @typescript-eslint/no-unused-vars [richTextTypes.INLINES.EMBEDDED_ENTRY]: (node, next) => { const { id, contentType } = mapEntry(node.data.target); return `{{< contentful-hugo/inline-entry id="${id}" contentType="${contentType}" parentContentType="${parentContentType || ""}" >}}`; } }); const sanitizedMarkOutput = (input, markWrapper) => { const leading = leadingSpaces(input); const trailing = trailingSpaces(leading.newString); return `${leading.removedSpaces}${markWrapper}${trailing.newString}${markWrapper}${trailing.removedSpaces}`; }; const options = (parentContentType = "") => ({ renderMark: { [richTextTypes.MARKS.BOLD]: (text) => sanitizedMarkOutput(text, "**"), [richTextTypes.MARKS.ITALIC]: (text) => sanitizedMarkOutput(text, "_"), [richTextTypes.MARKS.CODE]: (text) => { if (isMultilineString(text)) { return `\`\`\` ${text} \`\`\``; } return `\`${text}\``; } }, renderNode: optionsRenderNode(parentContentType) }); const richTextToMarkdown = (document, contentType) => { const string = richTextHtmlRenderer.documentToHtmlString(document, options(contentType)); return ` ${replaceSpecialEntities(string)}`; }; let isQuietMode = false; let isVerboseMode = false; var LogTypes = /* @__PURE__ */ ((LogTypes2) => { LogTypes2[LogTypes2["log"] = 0] = "log"; LogTypes2[LogTypes2["info"] = 1] = "info"; LogTypes2[LogTypes2["warn"] = 2] = "warn"; return LogTypes2; })(LogTypes || {}); const log = (message, type = 0 /* log */, category = 0 /* default */) => { const emitLog = () => { switch (type) { case 1 /* info */: console.info(message); break; case 2 /* warn */: console.warn(message); break; default: console.log(message); break; } }; if (isQuietMode) { return; } if (category === 1 /* verbose */ && !isVerboseMode) { return; } emitLog(); }; const initLogger = (quietMode = false, verboseMode = false) => { isQuietMode = quietMode; isVerboseMode = verboseMode; return log; }; 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 { 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)) { 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; }; const getCustomFields = (appendFields, entry) => { const fields = {}; if (typeof appendFields === "object") { Object.keys(appendFields).forEach((key) => { const fieldVal = appendFields[key]; switch (typeof fieldVal) { case "function": fields[key] = fieldVal(entry); break; default: fields[key] = fieldVal; break; } }); } return fields; }; const mapArrayField = (fieldContent) => { if (!fieldContent.length) { return []; } const array = []; for (let i = 0; i < fieldContent.length; i++) { const arrayNode = fieldContent[i]; switch (typeof arrayNode) { case "object": if (arrayNode?.sys) { switch (arrayNode.sys.type) { case "Asset": array.push(getAssetFields(arrayNode)); break; case "Entry": array.push(getEntryFields(arrayNode)); break; default: array.push(arrayNode); break; } } else { array.push(arrayNode); } break; default: array.push(arrayNode); break; } } return array; }; const mapReferenceField = (fieldContent) => { switch (fieldContent.sys.type) { case "Asset": return getAssetFields(fieldContent); case "Entry": return getEntryFields(fieldContent); default: return fieldContent; } }; const mapRichTextField = (fieldContent) => { const richText = []; const fieldPlainText = richTextPlainTextRenderer.documentToPlainTextString(fieldContent); const nodes = fieldContent.content; if (nodes && nodes.length) { for (let i = 0; i < nodes.length; i++) { richText.push(richTextNodes(nodes[i])); } } return { richText, plainText: fieldPlainText }; }; const shouldResolve = (fieldName, resolve = []) => { for (const item of resolve) { if (fieldName === item.field) { return item; } } return false; }; const shouldOverride = (fieldName, overrides = []) => { for (const item of overrides) { if (fieldName === item.field) { return item; } } return false; }; const resolveEntry = (entry = {}, resolvesToString = "") => { const props = resolvesToString.split("."); let value = entry; for (const prop of props) { value = value[prop]; } return value; }; const isDateField = (input) => { const requiredSymbols = ["-", ":", "T"]; if (typeof input !== "string") { return false; } for (const symbol of requiredSymbols) { if (!input.includes(symbol)) { return false; } } const year = input.split("-")[0]; if (Number.isNaN(Number(year))) { return false; } if (typeof input === "string") { const date = Date.parse(input); if (Number.isNaN(date)) { return false; } return true; } return false; }; const resolveField = (fieldContent, resolvesToString = "") => { if (!resolvesToString || typeof fieldContent !== "object") { return null; } if (Array.isArray(fieldContent)) { const fieldValue = []; for (const entry of fieldContent) { fieldValue.push(resolveEntry(entry, resolvesToString)); } return fieldValue; } return resolveEntry(fieldContent, resolvesToString); }; const mapFields = (entry, isHeadless, type, mainContentField, resolveList, overrides, customFields = {}) => { const frontMatter = {}; if (isHeadless) { frontMatter.headless = true; } if (type) { frontMatter.type = type; } frontMatter.sys = { id: entry.sys.id, updatedAt: entry.sys.createdAt, createdAt: entry.sys.updatedAt, revision: entry.sys.revision, space: entry.sys.space?.sys.id, contentType: entry.sys.contentType.sys.id }; frontMatter.date = entry.sys.createdAt; for (const field of Object.keys(entry.fields)) { const fieldContent = parseField(entry.fields[field]); let fieldName = field; const fieldOverride = shouldOverride(field, overrides); if (fieldOverride && fieldOverride.options?.fieldName) { fieldName = fieldOverride.options.fieldName; } if (fieldOverride && fieldOverride.options?.valueTransformer) { frontMatter[fieldName] = fieldOverride.options.valueTransformer( entry.fields[field] ); continue; } const fieldResolver = shouldResolve(field, resolveList); if (fieldResolver) { frontMatter[fieldName] = resolveField( fieldContent, fieldResolver.resolveTo ); continue; } if (field === mainContentField) { continue; } else if (field === "date") { const d = fieldContent; if (d.length > 10) { frontMatter.date = new Date(d).toISOString(); } else { frontMatter.date = d; } continue; } switch (typeof fieldContent) { case "object": if ("sys" in fieldContent) { frontMatter[fieldName] = mapReferenceField( fieldContent ); } else if ("nodeType" in fieldContent) { frontMatter[fieldName] = mapRichTextField(fieldContent).richText; frontMatter[`${field}_plaintext`] = mapRichTextField(fieldContent).plainText; } else { frontMatter[fieldName] = mapArrayField( Array.isArray(fieldContent) ? fieldContent : [] ); } break; default: if (isDateField(fieldContent) && fieldContent.length > 10) { frontMatter[fieldName] = new Date( fieldContent ).toISOString(); break; } frontMatter[fieldName] = fieldContent; break; } } const fieldsToAppend = getCustomFields(customFields, entry); Object.keys(fieldsToAppend).forEach((key) => { frontMatter[key] = fieldsToAppend[key]; }); return frontMatter; }; const getMainContent = (entry, fieldName) => { const mainContentField = parseField(entry.fields[fieldName]); if (typeof mainContentField === "object" && "nodeType" in mainContentField && mainContentField.nodeType === "document") { return richTextToMarkdown( mainContentField, entry.sys.contentType.sys.id ); } if (mainContentField) { return ` ${mainContentField}`; } return null; }; const parseDirectoryPath = (directory, locale) => { const dir = removeLeadingAndTrailingSlashes(directory); if (locale && (dir.includes("[locale]") || dir.includes("[ locale ]"))) { const dirParts = dir.split("/"); const newDirParts = []; for (const part of dirParts) { if (part === "[locale]" || part === "[ locale ]") { newDirParts.push(locale.toLowerCase()); } else { newDirParts.push(part); } } return { path: newDirParts.join("/"), includesLocale: true }; } return { path: dir, includesLocale: false }; }; const determineFilePath = (contentSettings, entryId) => { const { fileExtension, fileName, isSingle, isHeadless, isTaxonomy, locale } = contentSettings; const { path, includesLocale } = parseDirectoryPath( contentSettings.directory, locale.mapTo ); let fName = entryId; if (fileName && isSingle) { fName = fileName; } else if (fileName) { fName = fileName; } const ext = locale.mapTo && !includesLocale ? `${locale.mapTo.toLowerCase()}.${fileExtension}` : fileExtension; if (isHeadless && !isSingle) { return `./${path}/${fName}/index.${ext}`; } if (isTaxonomy) { return `./${path}/${fName}/_index.${ext}`; } if (isSingle) { return `./${path}/${fName}.${ext}`; } return `./${path}/${fName}.${ext}`; }; const createDirectoryForFile = async (contentSettings, entryId) => { const { fileName, isSingle, isHeadless, isTaxonomy } = contentSettings; const directory = parseDirectoryPath( contentSettings.directory, contentSettings.locale.mapTo ).path; const fName = fileName || entryId; if (isHeadless && !isSingle) { await fs__default.ensureDir(`./${directory}/${fName}`); } else if (isTaxonomy) { await fs__default.ensureDir(`./${directory}/${fName}`); } else { await fs__default.ensureDir(`./${directory}`); } }; const cleanPreviousDynamicLocation = async (contentSettings, entryId) => { const settings = { ...contentSettings }; settings.fileName = ""; const tmpPath = determineFilePath(settings, entryId); const tmpPathFinal = tmpPath.replace("./", "./.contentful-hugo/"); if (await fs__default.pathExists(tmpPathFinal)) { const path = (await fs__default.readFile(tmpPathFinal)).toString(); await fs__default.remove(path); if (path.includes("/index.md")) { await fs__default.remove(path.replace("/index.md", "")); } if (path.includes("/_index.md")) { await fs__default.remove(path.replace("/_index.md", "")); } } }; const logDynamicLocation = async (contentSettings, entryId, filePath) => { const settings = { ...contentSettings }; settings.fileName = ""; const tmpPath = determineFilePath(settings, entryId); const tmpPathFinal = tmpPath.replace("./", "./.contentful-hugo/"); await fs__default.ensureFile(tmpPathFinal); await fs__default.writeFile(tmpPathFinal, filePath); }; const determineDynamicLocation = async (filePath) => { const path = filePath.replace("./", "./.contentful-hugo/"); if (await fs__default.pathExists(path)) { const newPath = (await fs__default.readFile(path)).toString(); return newPath; } return filePath; }; const setFileContent = (frontMatter, fileExtension, mainContent) => { let fileContent = ""; switch (fileExtension) { case "yaml": case "yml": fileContent += YAML__namespace.stringify(frontMatter); break; case "json": fileContent += JSON.stringify(frontMatter); break; default: fileContent += `--- `; fileContent += YAML__namespace.stringify(frontMatter); fileContent += `--- `; if (mainContent) { fileContent += mainContent; } break; } return fileContent; }; const createFile = async (contentSettings, entryId, frontMatter, mainContent) => { const { fileExtension, isHeadless, isTaxonomy, isSingle } = contentSettings; if (isHeadless && isTaxonomy) { throw new Error( "A content type cannot have both isHeadless and isTaxonomy set to true" ); } const fileContent = setFileContent( frontMatter, fileExtension || null, mainContent ); const hasDynamicFilename = typeof contentSettings.fileName === "string" && !isSingle; if (hasDynamicFilename) { await cleanPreviousDynamicLocation(contentSettings, entryId); } await createDirectoryForFile(contentSettings, entryId); const filePath = determineFilePath(contentSettings, entryId); await fs__default.writeFile(filePath, fileContent).catch((error) => { if (error) { log(error); } }); if (hasDynamicFilename) { await logDynamicLocation(contentSettings, entryId, filePath); } }; const overrideFileName = (nameStr, entry, locale) => { const strParts = nameStr.split("."); if (typeof entry !== "object") { return ""; } let result = entry; for (const part of strParts) { if (result[part]) { result = result[part]; } else if (result[locale]) { result = result[locale]; } } if (typeof result === "string") { return result; } return null; }; const processEntry = (item, contentSettings) => { const { isHeadless, type, mainContent, resolveEntries, overrides, customFields } = contentSettings; const frontMatter = mapFields( item, isHeadless, type, mainContent, resolveEntries, overrides, customFields ); const content = contentSettings.mainContent ? getMainContent(item, contentSettings.mainContent) : ""; const settings = { ...contentSettings }; if (!settings.isSingle && typeof settings.fileName === "string") { const newFileName = overrideFileName( settings.fileName, item, contentSettings.locale.code ); if (newFileName && typeof newFileName === "string") { settings.fileName = newFileName; } else { settings.fileName = item.sys.id; } } return createFile(settings, item.sys.id, frontMatter, content); }; const prepDirectory = async (settings) => { const parsedDir = parseDirectoryPath( settings.directory, settings.locale.mapTo ); const newDir = parsedDir.path; await fs__default.ensureDir(newDir); if (settings.isHeadless && !settings.isSingle) { const listPageFrontMatter = `--- # this is a work-around to prevent hugo from rendering a list page url: / --- `; if (settings.locale && settings.locale.mapTo) { await fs__default.writeFile( `${newDir}/_index.${settings.locale.mapTo.toLowerCase()}.md`, listPageFrontMatter ); } else { await fs__default.writeFile(`${newDir}/_index.md`, listPageFrontMatter); } } }; const getContentType = async (limit, skip, contentSettings, contentfulSettings, previewMode = false, itemsPulled = 0, directoryPrepped = false) => { if (!directoryPrepped) { await prepDirectory(contentSettings); } const { token, previewToken, space, environment } = contentfulSettings; if (previewMode && !previewToken) { throw new Error( "Environment variable CONTENTFUL_PREVIEW_TOKEN not set" ); } else if (!previewMode && !token) { throw new Error("Environment variable CONTENTFUL_TOKEN not set"); } let accessToken = token; if (previewMode) { accessToken = previewToken || token || ""; } const options = { space, host: previewMode ? "preview.contentful.com" : "cdn.contentful.com", accessToken, environment }; const client = contentful__namespace.createClient(options); if (!contentSettings.fileExtension) { contentSettings.fileExtension = "md"; } const query = { content_type: contentSettings.typeId, limit, skip, // eslint-disable-next-line @typescript-eslint/no-explicit-any order: "sys.updatedAt" }; if (contentSettings.filters) { const { filters } = contentSettings; const ignoreKeys = ["content_type", "limit", "skip"]; Object.keys(filters).forEach((key) => { if (!ignoreKeys.includes(key)) { query[key] = filters[key]; } }); } if (contentSettings.locale && contentSettings.locale.code) { query.locale = contentSettings.locale.code; } return client.getEntries(query).then(async (data) => { let itemCount; if (itemsPulled) { itemCount = itemsPulled; } else { itemCount = 0; } const tasks = []; for (let i = 0; i < data.items.length; i++) { const item = data.items[i]; tasks.push(processEntry(item, contentSettings)); itemCount++; } await Promise.all(tasks); if (data.total > data.limit && !contentSettings.isSingle) { const newSkip = skip + limit; return getContentType( limit, newSkip, contentSettings, contentfulSettings, previewMode, itemCount, true ); } return { totalItems: itemCount, typeId: contentSettings.typeId }; }); }; const resultMessage = (typeId, totalItems, locale = "") => { let grammarStuff; if (Number(totalItems) === 1) { grammarStuff = "item"; } else { grammarStuff = "items"; } if (locale) { return ` ${typeId} (${locale}) - ${totalItems} ${grammarStuff}`; } return ` ${typeId} - ${totalItems} ${grammarStuff}`; }; const __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('shared/contentful-hugo.BWF3yQhL.cjs', document.baseURI).href))); const __dirname$1 = path__default.dirname(__filename$1); const codeNames = [ "asset-hyperlink.html", "embedded-asset.html", "embedded-entry.html", "entry-hyperlink.html", "inline-entry.html" ]; const shortcodes = {}; for (const name of codeNames) { const noExtension = name.replace(".html", ""); const camelCaseName = noExtension.replace( /-([a-z])/g, (g) => g[1].toUpperCase() ); shortcodes[camelCaseName] = { filename: name, template: fs__default$1.readFileSync(path.resolve(__dirname$1, `./assets/shortcodes/${name}`)).toString() }; } const wait = (milli = 1e3) => new Promise((resolve) => { setTimeout(() => resolve(), milli); }); const generateConfig = async (filepath) => { log(`creating ./contentful-hugo.config.ts`); const configContent = `// go to https://github.com/ModiiMedia/contentful-hugo#configuration for configuration instructions import { defineConfig } from 'contentful-hugo'; export default defineConfig({ locales: [], // uses default locale if left empty singleTypes: [], repeatableTypes: [], });`; await fs__default.writeFile(filepath, configContent); log("config file created\n"); }; const checkForConfig = async () => { log(`checking for config...`); await wait(1e3); const config = await loadConfig(); const filepath = "./contentful-hugo.config.ts"; if (!config) { return generateConfig(filepath); } const { singleTypes, repeatableTypes } = config; if (singleTypes || repeatableTypes) { log(`config already exists `); return null; } return generateConfig(filepath); }; const regex = /<<([^%>]+)?>>/g; const getVariablesFromTemplate = (template) => { const params = template.match(regex); return params; }; const templateVariableValues = { "<<BACKGROUND_COLOR>>": "rgb(255, 231, 231)", "<<TEXT_COLOR>>": "red", "<<BORDER_COLOR>>": "red" }; const replaceVariablesWithValues = (template) => { const values = templateVariableValues; const params = getVariablesFromTemplate(template); let newTemplate = template; if (params && params.length) { for (const param of params) { newTemplate = newTemplate.replace(param, values[param]); } } return newTemplate; }; const addShortcodes = async (override = false) => { log("adding shortcodes for rich text..."); await wait(1e3); const directory = "./layouts/shortcodes/contentful-hugo"; await fs__default.ensureDir(directory); const handleShortCode = async (key) => { const { filename, template } = shortcodes[key]; const finalTemplate = replaceVariablesWithValues(template); const filepath = `${directory}/${filename}`; if (await fs__default.pathExists(filepath) && !override) { log(`${filepath} already exists`); } else { await fs__default.writeFile(filepath, finalTemplate); log(`created ${filepath}`); } }; const tasks = []; Object.keys(shortcodes).forEach(async (key) => { tasks.push(handleShortCode(key)); }); await Promise.all(tasks).then(() => { log("\n"); }); return null; }; const initializeDirectory = async (override = false) => { await checkForConfig(); await addShortcodes(override); }; const cleanInputAndOutput = (inputDir, outputDir) => { const input = removeLeadingAndTrailingSlashes(inputDir.replace("./", "")); const output = removeLeadingAndTrailingSlashes(outputDir.replace("./", "")); return { input, output }; }; const copyFileToOutputDirectory = async (filePath, inputDir, outputDir) => { const { input, output } = cleanInputAndOutput(inputDir, outputDir); const newFilePath = filePath.replace(`${input}/`, `${output}/`); await fs__default.ensureFile(newFilePath); return promises.copyFile(filePath, newFilePath); }; const deleteFileFromOutputDirectory = (filePath, inputDir, outputDir) => { const { input, output } = cleanInputAndOutput(inputDir, outputDir); const newFilePath = filePath.replace(`${input}/`, `${output}/`); return promises.unlink(newFilePath); }; const copyInputDirectoryToOutputDirectory = async (inputDir, outputDir) => { await fs__default.ensureDir(inputDir); const srcPath = path__default.resolve(inputDir); const outPath = path__default.resolve(outputDir); return fs__default.copy(srcPath, outPath); }; const copyStaticContent = async (config) => { if (!config.staticContent || !config.staticContent.length) { return; } const tasks = []; for (const item of config.staticContent) { const { inputDir, outputDir } = item; if (!inputDir) { throw new Error( `staticContent item config is missing required field "inputDir"` ); } if (!outputDir) { throw new Error( `staticContent item config is missing required field "outputDir"` ); } tasks.push(copyInputDirectoryToOutputDirectory(inputDir, outputDir)); } await Promise.all(tasks); }; const fetchType = (limit, skip, settings, contentfulSettings, preview = false) => getContentType(limit, skip, settings, contentfulSettings, preview).then((result) => { log( resultMessage( result.typeId, result.totalItems, settings.locale.code ) ); }).catch((error) => { log(error); if (typeof error === "string") { throw new Error(error); } const { sys } = error; if (sys && sys.id && sys.id === "InvalidQuery") { log( ` -------------------------- ${settings.typeId} - ERROR ${error.message} ${JSON.stringify( error.details )}) --------------------------`, LogTypes.warn ); } else { throw new Error(`${JSON.stringify(error)}`); } }); const configCheck = (config) => { const { space, token, environment } = config.contentful; const missingParams = []; const missingEnvVars = []; if (!space) { missingParams.push("contentful.space"); missingEnvVars.push("CONTENTFUL_SPACE"); } if (!token) { missingParams.push("contentful.token"); missingEnvVars.push("CONTENTFUL_TOKEN"); } if (!environment) { missingParams.push("contentful.environment"); missingEnvVars.push("CONTENTFUL_PREVIEW_TOKEN"); } if (missingParams.length > 0) { const errorMessage = `Missing [${missingParams.join(", ")}]. Update your config or set the following environment variables: [${missingEnvVars.join(", ")}]`; throw new Error(errorMessage); } return null; }; const fetchDataFromContentful = async (config, previewMode = false, waitTime = 0) => { const isPreview = previewMode; const deliveryMode = previewMode ? "Preview Data" : "Published Data"; configCheck(config); if (waitTime && typeof waitTime === "number") { log(`waiting ${waitTime}ms...`, LogTypes.warn); await new Promise((resolve) => { setTimeout(() => { resolve(null); }, waitTime); }); } log( ` --------------------------------------------- Pulling ${deliveryMode} from Contentful... --------------------------------------------- ` ); const jobs = []; const addJob = (item, isSingle) => { let settings; if (isSingle) { settings = { typeId: item.id, directory: item.directory, locale: { code: "", mapTo: "" }, fileExtension: item.fileExtension, fileName: item.fileName, mainContent: item.mainContent, isSingle: true, type: item.type, resolveEntries: getResolveEntryConfigs(item.resolveEntries), overrides: getOverrideConfigs(item.overrides), filters: item.filters, customFields: item.customFields || {} }; } else { settings = { typeId: item.id, locale: { code: "", mapTo: "" }, directory: item.directory, isHeadless: item.isHeadless, fileExtension: item.fileExtension, mainContent: item.mainContent, type: item.type, isTaxonomy: item.isTaxonomy, resolveEntries: getResolveEntryConfigs(item.resolveEntries), overrides: getOverrideConfigs(item.overrides), filters: item.filters, fileName: item.fileName, customFields: item.customFields || {} }; } if (isValidFileExtension(settings.fileExtension)) { const pageSize = config.contentful.pageSize ? config.contentful.pageSize : 1e3; if (config.locales.length && !item.ignoreLocales) { for (const locale of config.locales) { const newSettings = { ...settings }; if (typeof locale === "string") { newSettings.locale = { code: locale, mapTo: locale }; } else { newSettings.locale = locale; } const job = { limit: isSingle ? 1 : pageSize, skip: 0, contentSettings: newSettings, isPreview }; jobs.push(job); } } else { const job = { limit: isSingle ? 1 : pageSize, skip: 0, contentSettings: settings, isPreview }; jobs.push(job); } } else { log( ` ERROR: extension "${settings.fileExtension}" not supported`, LogTypes.warn ); } }; const repeatables = config.repeatableTypes; if (repeatables) { for (let i = 0; i < repeatables.length; i++) { addJob(repeatables[i], false); } } const singles = config.singleTypes; if (singles) { for (let i = 0; i < singles.length; i++) { addJob(singles[i], true); } } return new Promise((resolve) => { const t = new Limiter__default({ concurrency: 2 }); for (const job of jobs) { t.push((cb) => { fetchType( job.limit, job.skip, job.contentSettings, config.contentful, job.isPreview ).then(() => { cb(); }); }); } t.onDone(() => { log(` --------------------------------------------- `); resolve(); }); }); }; exports.cleanInputAndOutput = cleanInputAndOutput; exports.copyFileToOutputDirectory = copyFileToOutputDirectory; exports.copyStaticContent = copyStaticContent; exports.defineConfig = defineConfig; exports.deleteFileFromOutputDirectory = deleteFileFromOutputDirectory; exports.determineDynamicLocation = determineDynamicLocation; exports.determineFilePath = determineFilePath; exports.fetchDataFromContentful = fetchDataFromContentful; exports.getContentType = getContentType; exports.getOverrideConfigs = getOverrideConfigs; exports.getResolveEntryConfigs = getResolveEntryConfigs; exports.initLogger = initLogger; exports.initializeDirectory = initializeDirectory; exports.loadConfig = loadConfig; exports.log = log; exports.removeLeadingAndTrailingSlashes = removeLeadingAndTrailingSlashes; exports.replaceBackslashesWithForwardSlashes = replaceBackslashesWithForwardSlashes;