1. У Medium уже есть огромная аудитория, а это значит, что новые люди узнают о вашем содержании, которые иначе не нашли бы вас. Вам также не нужно бояться того, что Google снизит рейтинг вашего блога в поисковых системах из-за дублирования контента, потому что мы можем установить каноническую ссылку, единственный источник правды, в Medium, чтобы она указывала обратно на оригинальный пост на собственном сайте. Таким образом, размещение ваших сообщений на обеих платформах не является недостатком для SEO.
  2. Местный редактор среднего размера - отстой. Редактор даже не позволяет напрямую писать в Markdown. Несмотря на то, что Medium поддерживает Markdown (как мы увидим позже), нет переключателя для переключения между представлениями WYSIWYG / Markdown / HTML. Большинство знакомых мне разработчиков предпочитают писать в Markdown вместо того, чтобы вставлять ссылки / изображения через панель инструментов. Используя подход кросс-публикации, мы можем написать Markdown в нашей знакомой среде - для меня это VS Code + gatsbyjs для перезагрузки в реальном времени.
  3. Публикация на Medium программным способом проще с точки зрения рабочего процесса. Раньше я пробовал использовать функцию Medium Import Story непосредственно в публикации на моем сайте, которая часто нарушала форматирование при чтении HTML, или для импорта Markdown с помощью таких инструментов, как markdowntomedium, которые работают хорошо, но они загрязняют вашу учетную запись GitHub (частными) сущностями и устанавливают неправильный канонический URL-адрес для сущности GitHub. Теперь, с gatsbyjs или любым другим генератором статических сайтов, мой рабочий процесс выглядит так:
  • Напишите сообщение в Markdown
  • Отправить на GitHub = ›Netlify git-hook запускает и развертывает мой сайт
  • Я запускаю свой скрипт кросс-публикации: npm run crosspost, который публикует новое сообщение на Medium

Если это вас убедило, вот как настроить кросс-публикацию на Medium.

Настраивать

Все, что вам нужно, это nodejs версии 8 или выше и сообщение, написанное на Markdown. Сообщение Markdown должно иметь frontmatter, содержащий теги title, slug и medium,. Например, этот пост выглядит так:

---
title: How to cross-post to Medium
slug: how-to-crosspost-to-medium
medium:
- programming
- javascript
- medium
---

I host my blog ...

Создание учетных данных для скриптов

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

1. Создайте приложение.

В настройках канала нажмите Управление приложениями, Новое приложение и введите сведения о приложении, как вам удобно:

Name: Cross-Post script
Description: 
Callback URLs: http://example.com/callback/medium // We don't need this, but still it's mandatory

Нажмите "Сохранить" и запишите идентификатор клиента и секрет клиента только что созданного приложения.

2. Создайте токен интеграции.

В настройках среды перейдите в Токены интеграции, заполните описание, нажмите Получить токен интеграции и запишите токен.

Скрипт кросс-поста

А вот и самое интересное - мы готовы создать скрипт node.js. Давайте посмотрим на схему того, что должен делать наш скрипт:

  1. Прочтите файл Markdown, проанализируйте его в абстрактном синтаксическом дереве (MAST) уценки.
  2. Получите теги title, slug и medium из фронтматера.
  3. Перепишите все относительные изображения и URL-адреса ссылок на абсолютные URL-адреса, предшествующие нашему веб-сайту.
  4. Вставить пользовательскую обратную ссылку в нижний колонтитул на наш исходный пост
  5. Используйте Medium API, чтобы создать сообщение с измененным содержанием сообщения markdown, правильной канонической ссылкой и тегами.

Здесь я рассмотрю основные идеи, полностью функциональный код можно увидеть на GitHub.

Разбор файла уценки

Мы будем использовать remark с его многочисленными плагинами для анализа файла Markdown. remark работает, создавая цепочку плагинов. Таким образом, мы начинаем с содержимого файла, а выходные данные каждого плагина затем передаются в качестве входных данных для следующего плагина в цепочке. Мы будем использовать плагин parse для анализа файла уценки, затем обработаем возвращенный MAST для анализа узла yaml frontmatter и, наконец, снова воспользуемся stringify для воссоздания содержимого уценки из MAST.

var vfile = require('to-vfile')
const frontmatterPlugin = require('remark-frontmatter')
const Remark = require(`remark`)
const parse = require('remark-parse')
const stringify = require('remark-stringify')

const transformPostFromPath = async (filePath) => {
  try {
    return new Promise((resolve, reject) => {
      new Remark()
        .data(`settings`, {
          commonmark: true,
          footnotes: true,
          pedantic: true,
        })
        // creates the MAST from markdown
        .use(parse)
        // to create a markdown yaml node in the MAST
        .use(frontmatterPlugin)
        // converts the MAST back to markdown
        .use(stringify)
        // apply it to the file
        .process(vfile.readSync(filePath), function(err, vfile) {
          if (err) return reject(err)
          const returnValue = {
            // this will be the markdown content
            content: String(vfile),
          }
          return resolve(returnValue)
        })
    })
  } catch (ex) {
    console.log(ex)
  }
}

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

Получение главного

Чтобы прочитать переднюю часть и преобразовать ее в объект javascript, мы напишем наш первый remark плагин. Он проходит проверку MAST для yaml узлов и создает на их основе объект javascript.

Плагины Remark - это просто функции, которые сами возвращают transformer функцию.

const yaml = require('js-yaml')
const Remark = require(`remark`)
const parse = require('remark-parse')
const frontmatterPlugin = require('remark-frontmatter')
const stringify = require('remark-stringify')
const visit = require('unist-util-visit')

async function getFrontmatter(filePath) {
  let frontmatter
  function frontmatterToJs() {
    return function transformer(tree) {
      visit(tree, `yaml`, node => {
        frontmatter = yaml.load(node.value)
      })
    }
  }
  return new Promise((resolve, reject) => {
    new Remark()
      .data(`settings`, {
        commonmark: true,
        footnotes: true,
        pedantic: true,
      })
      .use(parse)
      .use(frontmatterPlugin)
      .use(frontmatterToJs)
      .process(vfile.readSync(filePath), function(err) {
        if (err) return reject(err)
        if (!frontmatter)
          return reject(new Error('No frontmatter found in markdown-AST'))
        console.log(`Found frontmatter ...`)
        return resolve(frontmatter)
      })
  })
}

Затем наша transformPostFromPath функция может использовать frontmatter для извлечения slug:

const url = require('url')

const transformPostFromPath = async (filePath, transformerPlugin) => {
  try {
    const frontmatter = await getFrontmatter(filePath)
    const siteUrl = `https://cmichel.io`
    const { slug } = frontmatter
    const postUrl = url.resolve(siteUrl, `/${slug}`)

    return new Promise((resolve, reject) => {
      new Remark()
        // ...
          const returnValue = {
            content: String(vfile),
            frontmatter,
            postUrl,
            siteUrl,
            slug,
          }
        // ...
    }
  }
}

Преобразование относительных URL-адресов в абсолютные URL-адреса

Затем нам нужно написать плагин, который преобразует все относительные URL-адреса изображений и ссылок в абсолютные URL-адреса. Причина в том, что изображение, размещенное в нашем собственном блоге, встроено как

![Alt text](./cats.png)

не будет работать в Medium. Поэтому мы будем использовать slug, чтобы переписать MAST для images и links, чтобы иметь такую ​​структуру:

![Alt text](https://cmichel.io/how-to-crosspost-to-medium/cats.png)

Код плагина довольно прост, мы снова используем шаблон посетитель, чтобы пройти через MAST и переписать свойство url этих узлов.

const url = require('url')
const visit = require('unist-util-visit')

function urlIsRelative(url) {
  // catches http(s)://example.io but also //example.io
  const isAbsolute = new RegExp('^([a-z]+://|//)', 'i')
  return !isAbsolute.test(url)
}

function joinUrls(siteUrl, slug, relativeUrl) {
  const siteUrlWithSlug = url.resolve(siteUrl, `/${slug}`)
  return url.resolve(siteUrlWithSlug, `${relativeUrl}`)
}

function absoluteUrls(options) {
  const { siteUrl, slug } = options
  return transformer

  function replaceUrl(node) {
    if (!node.url || !urlIsRelative(node.url)) return
    const absoluteUrl = joinUrls(siteUrl, slug, node.url)
    console.log(`\tRewriting link "${node.url}" to "${absoluteUrl}" ...`)
    node.url = absoluteUrl
  }

  function transformer(tree) {
    console.log(`Rewriting image links ...`)
    visit(tree, 'image', replaceUrl)
    console.log(`Rewriting anchor links ...`)
    visit(tree, 'link', replaceUrl)
  }
}
// use the absoluteUrl plugin
const transformPostFromPath = async (filePath, transformerPlugin) => {
  // ...
    new Remark()
      .data(`settings`, {
        commonmark: true,
        footnotes: true,
        pedantic: true,
      })
      .use(parse)
      .use(frontmatterPlugin)
      // pass the siteUrl, slug as options
      .use(absoluteUrls, {
        siteUrl,
        slug,
        postUrl,
        frontmatter,
      })
      .use(stringify)
  // ...
}

Внедрение пользовательского нижнего колонтитула

Давайте добавим нижний колонтитул к сообщению со средней уценкой, ссылаясь на изначально опубликованное сообщение. Мы просто напишем плагин, который добавляет divider и paragraph к дочерним элементам MAST.

const url = require('url')

const createHorizontalRule = () => ({
  type: `thematicBreak`,
})

const createReferenceToOriginalPost = postUrl => ({
  type: `paragraph`,
  children: [
    {
      type: `text`,
      value: `Originally published at `,
    },
    {
      type: 'link',
      url: postUrl,
      children: [
        {
          type: 'text',
          value: 'cmichel.io',
        },
      ],
    },
  ],
})

const createClapImage = siteUrl => ({
  type: `image`,
  title: null,
  alt: 'Medium Clap',
  url: url.resolve(siteUrl, '/images/medium_clap.gif'),
})

function appendFooter(options) {
  const { siteUrl, postUrl } = options
  return transformer

  function transformer(tree) {
    tree.children = [
      ...tree.children,
      createHorizontalRule(),
      createReferenceToOriginalPost(postUrl),
      createClapImage(siteUrl),
    ]
  }
}

// use the plugin like this:
.use(appendFooter, {
  siteUrl,
  slug,
  postUrl,
  frontmatter,
})

Использование medium-sdk для публикации преобразованного сообщения

Теперь нам просто нужно вызвать функцию transformPostFromPath, которая последовательно выполняет все наши remark плагины и возвращает новый пост как markdown вместе с frontmatter.

const transformedPost = await transformPostFromPath(path)
const response = await client.createPost(transformedPost)

Реализация среднего клиента выглядит так:

require('dotenv').config()
const medium = require('medium-sdk')

const mediumClient = new medium.MediumClient({
  clientId: process.env.MEDIUM_CLIENT_ID,
  clientSecret: process.env.MEDIUM_CLIENT_SECRET,
})

mediumClient.setAccessToken(process.env.MEDIUM_ACCESS_TOKEN)

const client = {
  createPost({ content, frontmatter, postUrl }) {
    return new Promise((resolve, reject) => {
      mediumClient.getUser(function(err, user) {
        mediumClient.createPost(
          {
            userId: user.id,
            // markdown post
            content,
            title: frontmatter.title,
            canonicalUrl: postUrl,
            // tags for medium read out of frontmatter
            tags: frontmatter.medium,
            // format is Markdown
            contentFormat: medium.PostContentFormat.MARKDOWN,
            publishStatus: medium.PostPublishStatus.DRAFT,
          },
          function(err, post) {
            if (err) return reject(err)
            return resolve(post)
          }
        )
      })
    })
  },
}

module.exports = client

Вам необходимо установить правильные учетные данные, упомянутые в разделе настройки в .env файле.

Опять же, полный рабочий код можно увидеть на GitHub. После вызова этой функции у вас должна появиться новая история в вашем аккаунте Medium в виде черновика. 🎉

Улучшения

Написание этих сценариев автоматического пост-генератора вызывает привыкание. Вот еще несколько вещей, которые вы можете сделать:

  • Напишите плагин-реплику, который заменяет code узлов на суть Codepen / GitHub, чтобы включить подсветку синтаксиса в Medium.
  • Добавьте подпись / изображение в нижний колонтитул, рекламируя свои продукты.
  • Кросс-пост на другие платформы. Следующий пост будет о кросс-постинге в steemit.

Первоначально опубликовано на cmichel.io