Transitioning Markdown Tables to Formatted Text in Contentful by Embedding Inline Entries

Looking for a solution to migrate long Markdown articles into a Rich Text field? While the documentation suggests linking the content to a Markdown field in an existing entry, I'm struggling with tables. The problem arises when trying to create a new entry type for tables on the fly and reference it within the Rich Text content. The deriveLinkedEntries() method seems restricted to placing references in specific fields, but inline references can exist multiple times.

Manually creating tables in the Rich Text field works, but I need a way to automate this process using a migration script.

My current struggle is reflected in the console output for a table:

{
    type: 'table',
    align: [ null, 'center', 'right' ],
    children: [
        { type: 'tableRow', children: [Array], position: [Position] },
        { type: 'tableRow', children: [Array], position: [Position] }
    ],
    position: Position {
        start: { line: 14, column: 1, offset: 970 },
        end: { line: 25, column: 58, offset: 1660 },
        indent: [
            1, 1
        ]
    }
}

Here's an excerpt of my code (simplified for clarity):

const {richTextFromMarkdown} = require('@contentful/rich-text-from-markdown')
        
module.exports = function(migration) {
    
    migration.transformEntries({
        contentType: 'article',
        from: ['content'],
        to: ['contentV2'],
        transformEntryForLocale: async function(fromFields, currentLocale)
        {
            let copy        = fromFields.content[currentLocale]

            const content = await richTextFromMarkdown(copy,
                (node) => {
                    let ret = null
                    let didSomething = true

                    node.deriveLinkedEntries()

                    switch (node.type) {
                        case 'image':
                            // ...
                            break;
                        case 'html':
                            // ...
                            break;
                        default:
                            didSomething = false
                    }

                    if (false === didSomething) {
                        console.log(node)
                    }

                    return ret
                }
            )

            return {
                contentV2: {
                    nodeType: 'document',
                    content: content.content,
                    data: {}
                },
            }
        }
    })
}

Answer №1

After dealing with some challenges and getting a good night's rest, I finally discovered the solution:

const { richTextFromMarkdown } = require('@contentful/rich-text-from-markdown')
const { createClient } = require('contentful-management')
        
module.exports = function(migration) 
{
    const managementClient = createClient({ accessToken: context.accessToken })
    const space            = await managementClient.getSpace(context.spaceId)
    const environment      = await space.getEnvironment(config.activeEnvironmentId)

     /**
     * Creates a hash for a given string
     * @param string
     */
    function createHash(string)
    {
        let hash = 0,
            i,
            chr

        if (string.length === 0) {
            return hash
        }

        for (i = 0; i < string.length; i++)
        {
            chr   = string.charCodeAt(i);
            hash  = ((hash << 5) - hash) + chr;
            hash |= 0 // Convert to 32bit integer
        }

        return hash
    }

    /**
     * Extracts table content from MarkDown and converts it into an entry with a MarkDown field
     * Entry is created if it doesn't already exist
     * @param table
     * @param copy
     */
    async function linkTableEntry(table, copy)
    {
        // extract MarkDown for the table from the entered text
        const pos      = table.position
        const strLen   = pos.end.offset - pos.start.offset
        const tableStr = copy.substr(pos.start.offset, strLen)
        const id       = createHash(tableStr) // avoid creating duplicate tables
        let   entry

        try {
            entry = await environment.getEntry(id)
        } catch (e) { }

        // create new entry since it doesn't exist yet
        if ('undefined' === typeof entry) {
            entry = await createTableEntry(tableStr, id)
        }

        return {
            nodeType: 'embedded-entry-block',
            content: [],
            data: {
                target: {
                    sys: {
                        type: 'Link',
                        linkType: 'Entry',
                        id: entry.sys.id
                    }
                }
            }
        }
    }

    /**
     * Creates a new Entry element with a string representing the table in MarkDown format
     * The table header is used as the title for the Entry
     * @param tablestring
     * @param id
     */
    async function createTableEntry(tablestring, id)
    {
         // create entry title from table header
        const title = tablestring
                        .match(/^.+|\n/g)[0]     
                        .split('|')
                        .filter(Boolean)         
                        .join(', ')
                        .replace(/\t/g, '')      
                        .replace(/[ ]+,/g, ', ') 
                        .replace(/[ ]+/g, ' ')   
                        .trim()

        let entry = await environment.createEntryWithId('table', id, {
            fields: {
                title: {
                    'de-DE': title
                },
                content: {
                    'de-DE': tablestring
                    }
                }
        })

        return await entry.publish()
    }

    migration.transformEntries({
        contentType: 'article',
        from: ['content'],
        to: ['contentV2'],
        transformEntryForLocale: async function(fromFields, currentLocale)
        {
            let copy = fromFields.content[currentLocale]

            const content = await richTextFromMarkdown(copy,
                async (node) => {
                    let ret = null
                    let didSomething = true

                    node.deriveLinkedEntries()

                    switch (node.type) {
                        case 'image':
                            // ...
                            break;
                        case 'html':
                            // ...
                            break;
                        case 'table':
                            ret = await linkTableEntry(node, copy)
                            break
                        default:
                            didSomething = false
                    }

                    if (false === didSomething) {
                        console.log(node)
                    }

                    return ret
                }
            )

            return {
                contentV2: {
                    nodeType: 'document',
                    content: content.content,
                    data: {}
                },
            }
        }
    })
}

In summary, here's what is happening in this code:

  • I defined a new content model table with a title and a long text field content
  • I utilized the positional information provided by richTextFromMarkdown() to extract the MarkDown string for the table from the content
  • A simple hash is generated for the string to prevent duplicate table creation when rerunning the migration
  • If the table already exists, it is included inline; if not, ContentFul's managementClient is used to create a new one on-demand
  • By using the sys.id of this entry, we can insert a linked entry into the Rich Text field

I hope this explanation will be helpful to someone facing similar challenges with migrating tables from MarkDown to Rich Text in Contentful.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Unable to locate module 'fs'

Hey there, I'm encountering an issue where the simplest Typescript Node.js setup isn't working for me. The error message I'm getting is TS2307: Cannot find module 'fs'. You can check out the question on Stack Overflow here. I&apos ...

Contrasting EventEmitter and Output Decorators in Angular-Cli

Is there a reason why EventEmitter and Output Decorator are meant to be used in conjunction with each other? I'm having trouble distinguishing between the two. If they are intended to work together, wouldn't it make more sense to have just one d ...

Skip creating declarations for certain files

src/ user.ts department.ts In the scenario outlined above, where there are two files in the src directory (user.ts and department.ts), is there a way to exclude the generation of declaration files specifically for department.ts when running tsc wi ...

Tips for passing an array between components in Angular 2

My goal is to create a to-do list with multiple components. Initially, I have 2 components and plan to add more later. I will be sharing an array of tasks using the Tache class. Navbar Component import { Component } from '@angular/core'; impor ...

Is it possible to invoke a component's function within the click function of a Chartjs chart?

How do I trigger a function in my Component from an onclick event in my chart.js? export class ReportesComponent implements OnInit { constructor(public _router: Router, private data: ReporteService) {} this.chart = new Chart('myChart', { ...

What is the best way to reset the testing subject between test cases using Jest and TypeScript?

I'm currently utilizing typescript alongside jest for unit testing. My goal is to create a simple unit test, but it consistently fails no matter what I try. Below is the snippet of code in question: // initialize.ts let initialized = false; let secre ...

What is the most effective way to transform values into different values using TypeScript?

If I have a list of country codes and I want to display the corresponding country names in my component, how can I achieve this using props? interface MyComponentProps { countryCode: 'en' | 'de' | 'fr'; } const MyComponent: ...

What is the proper way to utilize the component prop when working with custom components?

I'm currently experimenting with using the Link component from react-router along with my customized button component. However, I seem to be encountering an issue that I can't quite figure out: <Button component={Link} to="/"> "This works" ...

Strategies for navigating dynamic references in Angular 2 components

I am working with elements inside an ngFor loop. Each element is given a reference like #f{{floor}}b. The variable floor is used for this reference. My goal is to pass these elements to a function. Here is the code snippet: <button #f{{floor}}b (click) ...

When defining a class property in TypeScript, you can make it optional by not providing

Is there a way to make a property on a Class optional without it being undefined? In the following example, note that the Class constructor takes a type of itself (this is intentional) class Test { foo: number; bar: string; baz?: string; construc ...

Unable to Load website with In-App-Browser

Hello there, I'm a newcomer to Ionic and I'm hoping for some guidance. My goal is to convert my website into an app, and after doing some research, it seems that utilizing the in-app-browser is the most suitable approach. constructor(public navC ...

Utilizing React MUI Autocomplete to Save Selected Items

Exploring the realms of React and TypeScript, I find myself puzzled by a task at hand. It involves storing user-selected options from an Autocomplete component and subsequently sending these values to an external API. Is there a recommended approach for ac ...

Sending a function along with arguments to props in TypeScript

Encountering a peculiar error while executing this code snippet (React+Typescript): // not functioning as expected <TestClass input={InputFunction} /> And similarly with the following code: // still causing issues <TestClass input={(props ...

Errors arose due to the deployment of TypeScript decorators

Using TypeScript in a brand new ASP.NET Core project has brought some challenges. We are actively incorporating decorators into our codebase. However, this integration is causing numerous errors to appear in the output of VS2015: Error TS1219 Experim ...

What is the best way to add .xlsx and .docx files to the Typescript compilation output?

For my server, I have decided to use Typescript with NodeJS. One of the challenges I encountered in my server logic is manipulating .xlsx and .docx files. Unfortunately, these file types are not included in the Typescript compilation output. These specifi ...

TypeScript has two variable types

I'm facing a challenge with a function parameter that can accept either a string or an array of strings. The issue arises when trying to pass this parameter to a toaster service, which only accepts the string type. As a result, when using join(' ...

Encountering an issue with setting up MikroORM with PostgreSQL due to a type

I'm currently working on setting up MikroORM with PostgreSQL, but I've encountered a strange error related to the type: Here is the code snippet: import { MikroORM, Options} from "@mikro-orm/core"; import { _prod_ } from "./consta ...

Issue encountered while trying to insert a new row into the mat-table

I need help with inserting a new row in mat-table using a button. I wrote a function for this, but when I click the button, I encounter an error CalculatoryBookingsComponent.html:62 ERROR Error: Cannot find control with path: 'rows -> 0'. Addi ...

Issues have been reported regarding the paramMap item consistently returning null when working with Angular 8 routing

I am encountering an issue with Angular 8 where I am trying to fetch some parameters or data from the route but consistently getting empty values. The component resides within a lazy-loaded module called 'message'. app-routing.module.ts: ... { ...

Understanding the status of HTTP requests or observing the updates of observables in Angular2/Typescript is essential

I've been working on an Angular2 and Typescript application where I'm utilizing Angular2's HTTP methods to retrieve data from a database within a service. The service is triggered inside a component's onInit() function and I'm able ...