Часть 2: Использование полноформатных текстовых полей

Добро пожаловать во вторую часть! Если вы пропустили часть 1, вы можете прочитать ее, чтобы получить более полное представление о том, чего мы хотели достичь. В противном случае продолжайте читать, чтобы узнать о долгожданном окончании нашего путешествия!

Содержательные поля форматированного текста

Поле с форматированным текстом на Contentful - это самый модный тип поля, который вы можете получить на их платформе. Он предоставляет приятный пользовательский интерфейс для добавления дополнительного форматирования к тексту. Он также позволяет вам встраивать другие типы контента либо в строку, либо в виде блока прямо в поле, если вы обрабатываете его отрисовку в своей кодовой базе.

Мы хотели перейти непосредственно к полю с форматированным текстом, но из-за его сложной структуры мы решили подтолкнуть нашу уценку до поля уценки и использовать библиотеку Rich-text-from-markdown Contentful для преобразования нашей уценки в форматированный текст, а затем вставьте его в поле с форматированным текстом.

Время загрузки!

Пришло время, наконец, загрузить наши блоги на Contentful. Для этого мы использовали Contentful Management API через их жемчужину управления содержимым. Вот что мы сделали:

require 'contentful/management'
require 'csv'
blogs = CSV.read("./results/blog_posts_with_tags.csv", headers: true)
# To prevent the creation of duplicates, we created the content model entries for each of the tags, campuses and authors and stored their IDs here so that we can do a simple lookup to see if we already created one
TAG_IDS = {
  "Example tag" => 'tagId'
  # more tag ids omitted for brevity
}
CAMPUS_IDS = {
  "Example campus": "campusId"
}
AUTHOR_IDS = {
  "Example author": "authorId"
}
# Contentful client
client = Contentful::Management::Client.new('CONTENTFUL_API_KEY', raise_errors: true)
# To find the correct environment, we search for it based on the client stored in the variable above
environment = client.environments('CONTENTFUL_SPACE_ID').find('CONTENTFUL_ENV_ID')
# Gets content types that will be used to find or create entries of these types
blog_type = environment.content_types.find('blogPost')
person_type = environment.content_types.find('person')
campus_type = environment.content_types.find('campus')
tag_type = environment.content_types.find('tags')
blogs.each do |blog|
  begin
    tags_arr = []
# There are a finite amount of campuses and all of them are in the CAMPUS_IDS hash above
if (CAMPUS_IDS[blog['campus']])
      campus_entry =
environment.entries.find(CAMPUS_IDS[blog['campus']])
end
# For the tags, the logic is a bit more involved because if there is no tag present, we want to use a default tag, (we made it a required field for our blogs and if we didn't provide a tag, things would break). However, if it exists we either find it using our hash of tags above or create a new one.
if (!blog['tag'])
      tag_entry = environment.entries.find(TAG_IDS['Default Blog Tag'])
      tags_arr << tag_entry
    else
      slug = blog['tag'].downcase.gsub(' ', '-')
if (TAG_IDS[blog['tag']])
        puts 'found tag'
        tag_entry = environment.entries.find(TAG_IDS[blog['tag']])
        tags_arr << tag_entry
      else
        puts 'creating tag from blog'
        tag_entry = tag_type.entries.create(name: blog['tag'], slug: slug)
        TAG_IDS[blog['tag']] = tag_entry.id
        tags_arr << tag_entry
      end
    end
# Find or create author
if (AUTHOR_IDS[blog['author']])
      author_entry = environment.entries.find(AUTHOR_IDS['Flatiron School'])
    else
      author_entry = person_type.entries.create(name: blog['author'], jobTitle: 'Blog Post Author')
      AUTHOR_IDS[blog['author']] = author_entry.id
    end
# Creates the blog post
entry = blog_type.entries.create(
      title: blog['title'],
      publishedAt: DateTime.parse(blog['publishedAt']),
      markdown: blog['content'],
      slug: blog['slug'],
    )
# Associates tags, campus and author to blog post
entry.update(tags: tags_arr)
    entry.update(campus: campus_entry)
    entry.update(author: author_entry)
# Throttle the request so we don't get rate limit errors
sleep 0.15
# Print out the blogs that didn't successfully upload to Contentful
rescue => error
    puts '______________________________________'
    puts blog['id'], blog['status'], blog['slug']
    puts error
  end
end
puts 'DONE 🎉'

Удивительный. На данный момент все наши блоги находятся на Contentful, а их контент указан в поле уценки. Следующим шагом будет преобразование этих полей уценки в форматированный текст.

Преобразование в форматированный текст 🤑

Пора перейти на использование JavaScript! Мы использовали библиотеки Contentful contentful-migration и rich-text-from-markdown для преобразования разметки в форматированный текст. Библиотека contentful-migration обрабатывает фактическую передачу данных между полями, а библиотека rich-text-from-markdown обрабатывает преобразование самой уценки. Для части миграции с полным содержанием требуется лишь небольшая настройка:

// convert_markdown_to_rich_text.js
const runMigration = require('contentful-migration/built/bin/cli').runMigration
const dotenv = require('dotenv')
dotenv.config()
// We define some options that allow the library to find our space and environment on Contentful and then point it to where our migration lives in our file tree
const options = {
  filePath: 'data/migration-test.js',
  spaceId: process.env.CONTENTFUL_SPACE_ID,
  accessToken: process.env.CONTENTFUL_MANAGEMENT_API_KEY,
  environmentId: process.env.CONTENTFUL_ENVIRONMENT_ID,
  yes: true
}
// Runs the migration
runMigration({...options})
  .then(() => console.log('Migration Done!'))
  .catch((e) => console.error)

Теперь о том, для чего вы, вероятно, нажали на этот пост:

Вот сама миграция:

// migration-test.js
const { richTextFromMarkdown } = require('@contentful/rich-text-from-markdown')
const { createClient } = require('contentful-management')
// Our function takes in the migration for free from the runMigration function in config.js. We also get our space id, environment id and access token.
module.exports = async function(migration, { spaceId, accessToken, environmentId }) {
// We need to find our client, space and environment because, like we saw when we used the ruby gem above, to get to the environment which is where we create entries, we need our space and client first.
const client = await createClient({ accessToken: accessToken })
  const space = await client.getSpace(spaceId)
  const environment = await space.getEnvironment(environmentId)
// We call the transformEntries function on our migration to ask the library to find our blog post content model and for each one, take its markdown field, do something to it (defined below) and push that result into its content field. The shouldPublish attribute set to true also publishes it rather than leaving it as a draft.
migration.transformEntries({
    contentType: 'blogPost',
    from: ['markdown'],
    to: ['content'],
    shouldPublish: true,
// The transformEntryForLocale attribute's value is an anonymous function that is called with the value of the current field (fromFields) and that field's locale (currentLocale)
transformEntryForLocale: async function(fromFields, currentLocale) {
// If the currentLocale isn't 'en-US' or if the markdown field is empty we want to move on and process the next field rather than waste time trying to process something that isn't there
if (
        currentLocale !== 'en-US' ||
        fromFields.markdown === undefined
      ) {
        return
      }
// This is where more ✨magic✨ happens. Here we call on the powers of the rich-text-from-markdown library to convert the nodes of our markdown field into nodes that the rich text field can understand. If it comes across a node that it can't automatically parse, it's passed into the second argument of our richTextFromMarkdown function which then passes it into a switch statement that is able to determine what kind of node it is. In our case, code blocks and images were the ones we had to define manually.
const content = await    richTextFromMarkdown(fromFields.markdown['en-US'], async (node) => {
        switch (node.type){
          case 'code':
            return processCode(node)
          case 'image':
            return await processImage(environment, node)
        }
      })
// This is where the regular text nodes are handled
try {
        return {
          content: content
        }
} catch (error){
        console.error
      }
}
  })
}
// If the richTextFromMarkdown comes across a code block, the node is passed into this helper function that converts it to a format that the rich text field can understand
const processCode = async (node) => {
  return {
    nodeType: "blockquote",
    content: [
      {
        nodeType: "paragraph",
        data: {},
        content: [
          {
            nodeType: "text",
            value: node.value,
            marks: [],
            data: {}
          }
        ]
      }
    ],
    data: {}
  }
}
// If the richTextFromMarkdown comes across a image, the node is passed into this helper function that creates an asset in our Contentful environment, uploads and publishes that image and returns it in a format that the rich text field can understand
const processImage = async (environment, node) => {
  const title = node.url.split('/').pop()
  const ext = title.split('.').pop()
const asset = await environment.createAsset({
    fields: {
      title: {
        'en-US': `Blog post image: ${title}`
      },
      description: {
        'en-US': node.alt || `Blog post image: ${title}`
      },
      file: {
        'en-US': {
          contentType: `image/${ext}`,
          fileName: title,
          upload: node.url
        }
      }
    }
  }).catch(e => console.log('in create asset catch'))
asset.processForAllLocales()
return {
    nodeType: 'embedded-asset-block',
    content: [],
    data: {
      target: {
        sys: {
          type: 'Link',
          linkType: 'Asset',
          id: asset.sys.id
        }
      }
    }
  }
}

СДЕЛАНО 🎉

Вот и все.

Это была тонна работы, проделанная методом проб и ошибок. Я исключил все кроличьи норы и просто включил то, что работает. При этом можно сделать несколько важных выводов:

Попросите коллег присмотреться раньше, чем позже. На этот проект у меня ушло около месяца. Длина была частично связана с количеством проб и ошибок, которые я перенес, и нехваткой ресурсов, доступных в Интернете. Однако я уверен, что его можно было бы сократить, если бы я попросил дополнительные глаза раньше, чем позже.

Иногда работа, которую вы выполняете как инженер, просто отстой. В этом проекте (мрачный пятничный полдень) были моменты, когда я чувствовал себя совершенно не вдохновленным и перегорелым. Тем не менее, я выдержал это, и теперь я могу оглянуться на этот опыт и понять, как много я извлек из него.

Рост выходит за рамки вашей зоны комфорта. Это, вероятно, очевидно для большинства людей, но это действительно звучало для меня в ходе работы над этим проектом.

Возьмите отпуск. Выгорание было настоящим после этого, поэтому я взял себе 1,5 недели отдыха, чтобы восстановить силы. К счастью, я работаю с командой очень понимающих инженеров и менеджеров, так что это вообще не было проблемой.

Подождите, есть еще

Вот бонусный сценарий, когда нам нужно было понизить рейтинг заголовков в целях доступности:

require 'contentful/management'
client = Contentful::Management::Client.new('CONTENTFUL_API_KEY', raise_errors: true)
environment = client.environments(CONTENTFUL_SPACE_ID).find('CONTENTFUL_ENV_ID')
entries = client.entries(CONTENTFUL_SPACE_ID, CONTENTFUL_ENV_ID).all(content_type: "blogPost", limit: 100)
while entries.next_page
  entries.each do |blog|
      puts blog.title
      blog.markdown = blog.markdown.gsub(/(^# )/, "### ")
      blog.markdown = blog.markdown.gsub(/(^## )/, "#### ")
      blog.save
  end
 
  entries = entries.next_page
end
puts 'DONE 🎉'

Спасибо за прочтение! Хотите работать в целеустремленной команде, которая любит стек JAM? Мы нанимаем!

Чтобы узнать больше о Flatiron School, посетите веб-сайт, подпишитесь на нас в Facebook и Twitter и посетите нас на предстоящих мероприятиях рядом с вами.

Flatiron School - гордый член семьи WeWork. Посетите наши родственные технологические блоги WeWork Technology и Making Meetup.