100dolphins/postmark

从Markdown文件同步WP内容

安装: 0

依赖项: 0

建议者: 0

安全性: 0

星标: 0

关注者: 0

分支: 7

类型:wp-cli-package

dev-master 2023-08-17 10:47 UTC

This package is auto-updated.

Last update: 2024-09-17 13:11:53 UTC


README

静态网站生成器允许您使用受版本控制的Markdown文件树来制作网站,但提供的主题和动态功能不多。WordPress有很多主题和动态功能,但会将内容锁定在数据库中嵌入的HTML中,您无法使用自己的编辑器或对内容进行版本控制。

那么为什么不可以 结合 这两者呢?

Postmark是一个wp-cli命令,它接受Markdown文件(或整个文件树)并在WordPress中创建或更新帖子、页面和其他数据库对象。一些关键特性包括:

  • 无需更改web服务器配置:文件可以从服务器上的任何目录同步,并且不需要由web服务器可写或可读。(但它们需要由运行wp-cli命令的用户可读!)
  • 文件同步使用GUID来识别数据库中的帖子或页面,因此相同的文件可以应用于多个WordPress网站(例如,dev/staging/prod,或品牌网站之间的通用页面),并且移动或重命名文件会更改其在WP中的父项或别名,而不是创建新的页面或帖子。
  • 文件可以包含YAML前端内容来设置所有标准WordPress页面/帖子属性
  • 允许自定义帖子类型,并且插件可以使用动作和过滤器在同步过程中支持自定义字段或其他WP数据。
  • 插件的特殊页面(如结账或“我的账户”)和主题的自定义CSS可以使用选项引用进行同步,并且可以使用选项HTML值同步插件的HTML选项
  • 原型和模板:除了它们的WP帖子类型外,文件还可以有原型,从中继承属性和可选的Twig模板,该模板可以使用来自文档前端内容的额外数据生成静态内容。
  • 只有当输入文件(或其原型/模板文件)的大小、时间戳、名称、位置或内容发生变化时,才会更新帖子或页面(除非您使用--force
  • 与几乎任何文件监视工具(如entr、gulp、modd、reflex、guard等)配合使用效果极佳,可以在保存编辑后的帖子后立即更新,即使您的工具无法传递更改的文件名,也只需更新实际更改的文件。
  • Markdown使用league/commonmark进行转换,具有表格属性扩展,您还可以通过过滤器添加其他扩展。Markdown内容还可以包括短代码。(尽管您可能需要使用反斜杠转义相邻的左方括号,以防止它们被视为Markdown脚注链接。单独一行的短代码开闭器将放置在任何段落、div、表格等之外,它们可能中断、在前面或跟随。)
  • 支持父帖子或页面(文件上的最近index.md成为其父项,递归)
  • 别名默认为文件名(或index.md的目录名),除非有其他指定
  • 帖子/页面标题默认为Markdown主体的第一行,如果它是标题的话。

Postmark在哲学上与imposer类似,即同步始终是单向的(从文件系统到数据库),但不会覆盖由输入文件指定的任何数据库内容。因此,帖子或页面中不在Markdown或YAML(例如评论)中的任何部分都不会受到同步的影响。

(Postmark也与imposer类似,因为它是由mantle预安装的。Mantle项目包括一个可选的文件监控守护进程,该守护进程会检测项目content/目录中Markdown文件的变化,并将它们自动同步到您的数据库。)

目录

概述

安装和使用

Postmark可以通过以下方式安装:

wp package install dirtsimple/postmark

Postmark提供的两个主要命令是:

  • wp postmark sync <file>... [--force] [--skip-create] [--porcelain]
  • wp postmark tree <dir>... [--force] [--skip-create] [--porcelain]

sync命令创建或更新与给定的.md文件匹配的帖子或页面,而tree命令处理命名目录(及其所有子目录)中的所有.md文件。--porcelain选项使得输出除了同步的Wordpress帖子/页面/选项ID之外都是静默的。(这意味着您可以通过将文件名传递给wp postmark sync --porcelain来获取文件的Wordpress帖子ID或选项ID。)

默认情况下,除非自上次成功同步以来.md文件已更改(或已移动/重命名),否则帖子或页面不会更新,但--force选项会覆盖此限制,并同步所有指定的文件或目录,无论是否更改。(如果您添加或删除影响帖子转换或格式化的插件,这可能会很有用。)如果由于错误而未完全同步文件,则在运行类似命令的下一次尝试中,它将被再次尝试,即使不使用--force

要同步Markdown文件与Wordpress,文件的元数据必须包括ID字段中的全局唯一标识符。如果此值不存在,Postmark将自动添加它,除非您使用--skip-create选项(在这种情况下,您将收到错误消息)。

为了添加一个 ID,Postmark 必须能够写入相关的文件和目录(以在更改期间保存文件的备份副本),因此如果 wp-cli 用户没有这些权限,你应该使用 --skip-create。 (此外,Postmark 假设你的前文格式是这样的,即在顶部的 YAML 中添加 ID: 行不会创建语法错误,即顶级 YAML 不是用 {} 或其他内容包裹。)

Postmark 提供的其他命令包括

  • wp export [<spec>...] [--dir=<output-dir>] [--porcelain] [--allow-none] -- 将指定的帖子、页面或其他资源导出为 markdown 文件。 (有关更多信息,请参阅下面的 导出帖子、页面和其他资源。)
  • wp update [<file>...] [--porcelain] [--allow-none] -- 将数据库中的所选状态信息导出为与指定 markdown 文档相邻的 .pmx.yml 文件,这样就可以通过 WordPress GUI 配置复杂的属性(如页面构建器数据),但仍然作为文本文件进行版本控制。 (有关更多信息,请参阅下面关于 更新导出文档 的部分。)
  • wp postmark uuid [<file>...] -- 为每个尚未有 ID: 字段的 文件 添加一个 ID: 字段。如果没有给出文件,它将输出到标准输出的新 UUID,适合用作新 .md 文件的 ID:。 (有关更多信息,请参阅下面的 ID: 字段。)

文件格式和目录布局

Postmark 期望看到具有 .md 扩展名且具有 YAML 前文的非空 markdown 文件。如果文件名为 index.md,它将成为该目录或任何子目录(不包含其自己的 index.md)中其他文件的 WordPress 页面/帖子父级。如果 YAML 字段中没有覆盖,帖子或页面的默认别名是它的文件名,去掉 .md 扩展名。如果文件名是 index.md,则使用包含目录的名称。

当同步任何单个 .md 文件时,Postmark 会向上搜索,直到找到一个匹配的 index.md 文件或找到“项目根”目录。 (项目根是包含 .git.hg.svn_postmark.postmark 子目录的任何目录。)找到的第一个此类 index.md 成为当前 .md 文件的父页面或帖子。 (如果它尚未存在于 WordPress 数据库中,则将其同步,递归向上搜索更多父级,直到每个父 index.md 都存在并成为相应子帖子或页面的父级。)

Postmark 输入文件不需要放置在您的 WordPress 目录下,甚至不需要通过您的 web 服务器访问。出于安全考虑,它们不应由您的 web 服务器 写入,并且甚至不需要由运行 wp postmark 命令的用户 读取。您也不必将所有 markdown 文件放置在单个树中: postmark syncpostmark tree 命令分别接受多个文件和目录名称。

Postmark 输入文件使用标准的 YAML(v1.2)前文,由 --- 在前后界定,如下所示

---
ID: urn:uuid:1e30ea5f-17fe-422a-9c24-cb591eb2d72d
Draft: true
---
## Content Goes Here

If no `Title` was given, the above heading is stripped from the post body and used instead.
But if a `Title` *was* given, the heading remains in place.

内容使用 league/commonmark 从 Markdown 转换为 HTML,格式化过程可以通过 markdown 格式化的动作和过滤器 进行扩展。

ID:字段

所有前言字段都是可选的,除了ID:,它必须包含一个全局唯一标识符,最好是uuid的形式。(您可以使用wp postmark uuid生成合适的值。)默认情况下,Postmark会自动将字段添加到新的markdown文件中,除非您使用--skip-create或Postmark无法写入文件或目录。

这个标识符的目的是让Postmark能够匹配Wordpress数据库中已存在的页面或帖子,或者知道需要创建一个带有该标识符的新帖子。(帖子ID不足以实现这个目的,因为它们在不同的Wordpress安装中可能会有所不同,而且Wordpress内部生成的基于URL的“guids”在迁移过程中通常会发生变化。)

前端内容字段

除了必需的ID:字段外,您还可以包括以下任何或所有可选字段,以在Wordpress中设置相应的数据。任何未包含的字段,或者值为空或缺失的字段,都不会从Wordpress的当前值中进行更改。

Title: # if missing, it's parsed from the first heading if the content starts with one
Slug:  # if missing, will be obtained from file/directory name

Category: something          # this can be a comma-delimited string, or a YAML list
Tags:     bar, baz           # this can be a comma-delimited string, or a YAML list
Author:   foo@example.com    # user id is looked up by email address or user login

Excerpt: |  # You can set a custom excerpt, which can contain markdown
  Some *amazing* blurb that makes you want to read this post!

Date: 2017-12-11 13:41 PST        # Dates can be anything recognized by PHP, and
Updated: April 30, 2018 3:46pm    # use Wordpress's timezone if no zone is given

Status:    # string, Wordpress `post_status`
Draft:     # unquoted true/false/yes/no -- if true or yes, overrides Status to `draft`
Template:  # string, Worpdress `page_template`
WP-Type:   # string, Wordpress `post_type`, defaults to 'post'

WP-Terms:  # Worpdress `tax_input` -- a map from taxonomy names to terms:
  some-taxonomy: term1, term2   # terms can be a string 
  other-taxonomy:               # or a YAML list
    - term1
    - term2

Comments:   # string, 'open' or 'closed', sets Wordpress `comment_status`
Password:   # string, sets Wordpress `post_password`
Weight:     # integer, sets Wordpress `menu_order`
Pings:      # string, 'open' or 'closed', sets Wordpress `ping_status`
MIME-Type:  # string, sets Wordpress `post_mime_type`

Post-Meta:  # array of meta keys -> meta values; only the given values are changed
  a_custom_field: "Good stuff!"
  _some_hidden_field: 42
  delete_me: null  # setting to null deletes the meta key

Set-Options:  # an option path or array of option paths; each will be set to the post's db ID
  - edd_settings/purchase_history_page  # e.g., make this page the "my account" page for both EDD
  - lifterlms_myaccount_page_id         # and LifterLMS.  See "Working With Options" for more info

HTML:  # override markdown conversion for specific fields
  Excerpt: "<p>This is html</p>"   # a string means, "use this HTML instead of what's in the field"
  body: true                       # non-false non-string means, "field is HTML, not markdown"

请注意,Postmark只会验证或转换这些字段中的一小部分。大多数字段都是以原样传递给Wordpress的,如果您使用了无效的值,可能会出现问题。(例如,如果您分配了一个实际上未安装的自定义帖子类型,或者一个帖子类型不支持的状态。)然而,在大多数情况下,您只需将值更改为有效值并重新同步文件即可修复此类问题。

Wordpress插件或wp-cli包可以通过注册操作和过滤器来添加额外的字段(或更改现有字段的处理)。

与选项一起工作

许多Wordpress插件在其设置中引用了特殊页面(如购物车、结账、“我的账户”等),作为帖子ID。Wordpress本身有一个设置主页(即page_on_front)的选项。有时有一些需要HTML但您希望能够将其表达为markdown的选项,可能是在版本控制的文件中。

Postmark提供了三种方法来集成此类Wordpress选项。

  • 您可以通过将选项路径放入文档的Set-Options:前言字段中,将一个或多个Wordpress选项(或其部分)设置为数据库ID,在同步时将文档的数据库ID设置为Wordpress选项。例如,Set-Options: page_on_front将使该文档成为Wordpress主页。
  • 您可以使用文档的ID:作为urn:x-option-id: URL来就地更新可能已经存在的、由插件提供的帖子或页面。
  • 您可以使用urn:x-option-value: URL作为文档的ID:,将选项(或其部分)设置为由文档生成的HTML。

(此外,前两种集成方法实际上可以结合使用:您可以使用urn:x-option-id: URL作为文档的ID:就地更新插件提供的默认页面,然后使用文档的Set-Options:字段将其他选项指向同一页面。)

无论您在某个文档中采用哪种方法,要使用选项,您将使用选项路径。选项路径是一个以Wordpress选项名称开始的以/分隔的路径。任何第一个路径段之后的所有路径段被视为数组键,用于遍历选项内的子项,并且如果路径段包含除字母数字、-_之外的内容,则所有路径段都必须进行URL编码。(例如,路径foo/bar%2fbaz引用的是foo选项的bar/baz键。)

对于 Set-Options: 字段,您只需要放置一个选项路径(或它们的数组)来更新相关选项或其部分。对于 ID: 字段,您需要将路径前缀为 urn:x-option-id:urn:x-option-value:,以区分选项中存储的是帖子 ID,还是将 HTML 存储在选项中。

选项引用

当插件创建一个存储帖子 ID 的选项的默认页面时,您可以通过将您的 markdown 文档的 ID: 设置为 urn:x-option-id: URL 来就地更新该页面,例如:

ID: "urn:x-option-id:edd_settings/purchase_page"  # use this page as the EDD checkout page

具有作为 IDurn:x-option-id: URL 的帖子将与正常帖子略有不同同步。与往常一样,如果存在具有给定 GUID 的帖子,markdown 文件将同步到该帖子。之后,将编辑指定的选项以反映该帖子的 Wordpress post_id。(在上面的示例中,edd_settings 选项下的 purchase_page 键将被创建或更改)。

但是,如果没有存在具有给定 GUID 的帖子,将检查选项值(例如,edd_settings 选项下的 purchase_page 键)以获取有效的帖子 ID。如果该值存在并且引用了一个现有的帖子,则 现有帖子的 GUID 将被更改,并由同步覆盖其内容。(这允许您在插件首次激活时替换由插件生成的默认页面的内容,而无需手动干预或创建重复帖子。)

以下是一些您可能有用的选项引用示例

AffiliateWP

  • urn:x-option-id:affwp_settings/affiliates_page

Easy Digital Downloads

  • urn:x-option-id:edd_settings/failure_page
  • urn:x-option-id:edd_settings/purchase_page
  • urn:x-option-id:edd_settings/purchase_history_page
  • urn:x-option-id:edd_settings/success_page

LifterLMS

  • urn:x-option-id:lifterlms_checkout_page_id
  • urn:x-option-id:lifterlms_memberships_page_id
  • urn:x-option-id:lifterlms_myaccount_page_id
  • urn:x-option-id:lifterlms_shop_page_id
  • urn:x-option-id:lifterlms_terms_page_id

WooCommerce

  • urn:x-option-id:woocommerce_cart_page_id
  • urn:x-option-id:woocommerce_checkout_page_id

(注意:即使对于列出的插件,这个列表也可能远远不够全面。此外,由于上述插件的最新版本可能会添加、重命名或删除任何这些设置,因此您应在生产环境中更新插件之前,始终对非生产数据库进行同步测试。)

自定义CSS

WordPress 将主题的定制 CSS 存储在类型为 custom_css 的帖子中,并在选项中保存帖子 ID。因此,您可以使用 ID:urn:x-option-id:theme_mods_THEME/custom_css_post_id 的主题同步 markdown 文件,其中 THEME 是主题的别名。

与其他 选项引用 一样,如果存在具有现有帖子 post id 的现有选项值,则将使用 markdown 文件的 内容更新该帖子。或者,如果该值未设置(或默认为 -1),则将创建新帖子,并将选项值更新为指向新帖子。

因此,要为 Hestia 主题定义自定义 CSS,您会创建一个这样的 markdown 文件:

---
ID:       urn:x-option-id:theme_mods_hestia/custom_css_post_id
WP-Type:  custom_css
Title:    hestia
Slug:     hestia
Comments: closed
Pings:    closed
Status:   publish
---

```css

/* CSS Content Goes Here */

```

CSS 代码块封装是可选的:如果在类型为 custom_css 的帖子中找到,它将自动删除。(这样做是为了让您能够利用 markdown 编辑器的任何 CSS 特定编辑或突出显示功能。)您可以使用反引号或波浪线(~)进行封装,只要至少有三个,开闭括号长度相同且未缩进,并且开括号第一行上的第一个单词是 css(小写)。

选项HTML值

一些 WordPress 插件具有包含 HTML 内容的选项,您可能更愿意使用 Markdown 编写并维护在版本控制下。您可以使用每个文档的 ID: 中的 urn:x-option-value: URL 将文件同步到这些设置,例如:

ID: "urn:x-option-value:edd_settings/purchase_receipt"

具有上述ID:的帖子将通过将文档主体转换为HTML进行同步,然后将结果保存到edd_settings选项的purchase_receipt键中。大多数其他前端内容都被忽略(除了用于原型和模板的内容外),并且不会实际创建帖子,因此在此过程中只会调用Markdown格式化选项同步钩子,命令输出将列出ID:而不是WordPress的数字帖子ID。

由于选项没有元字段,选项的同步时间戳保存在一个(非自动加载)选项中,即postmark_option_cache,从而避免了未更改的文档的不必要更新。(然而,删除此选项是安全的,因为唯一的影响将是有效地--force任何具有选项值的ID:的文档的下一个重同步。)

原型和模板

在某些情况下,您可能有许多具有常见字段值或结构的文档。您可以通过创建原型来保持项目DRY(即不重复自己)。例如,如果您有许多包含一个或多个视频和一些介绍性文字的“视频”页面,您可以创建如下文件

---
Prototype: video
Videos:
 - title: First Video
   url: https://youtube.com?view=example
 - title: Second Video
   url: https://vimeo.com/something
---
# Example Videos

Dude, check these out!

然后,在相同的目录或父目录中创建一个包含video.type.yml文件的_postmark/.postmark/目录,该文件包含常见的属性

WP-Type: post
Draft: false
Category: videos
Author: me@example.com

以及一个包含正文文本的video.type.twig文件,这是一个Twig模板

{{ body }}

{% for video in Videos %}
## {{ video.title }}
[video src="{{ video.url }}"]
{% endfor %}

然后,任何在其前端内容中包含Prototype: video的文档都将具有指定的帖子类型、分类和作者,并且通过在正文后添加Videos:中列出的任何项目来进行格式化。

或者,如果您只想通过一个文件来指定类型,可以将属性和模板合并为一个单独的video.type.md文件,将属性放在前端内容中,如果有,将Twig模板(如果有的话)放在正文。与custom_css帖子类似,正文可以选择性地用带有twig语言的语言包裹的代码块包围,如果您想在Markdown编辑器中利用twig特定的编辑或突出显示支持的话。

如果存在与.type.yml和/或.type.twig一起的.type.md文件,则.type.yml中的属性将覆盖.type.md中的属性,而.type.twig中的模板将包裹.type.md模板的输出。

模板处理

Twig模板(在.type.twig.type.md中)用于生成Markdown(不是HTML),可能还包含WordPress短代码。模板在同步时间静态处理,不是在WordPress页面生成期间,并且只能访问正在同步的文档中的数据。提供给模板的“变量”是前端内容的属性,加上正文文本的body

模板可以使用完整的Twig语法,包括宏、extends标签和include()函数,这意味着您可以将其他模板文件放在您的_postmark.postmark目录中,然后将其用作部分或基础模板,类似于其他静态网站生成器。例如,我们可以在上面的例子中做如下操作

{% from "macros.twig" import video_block %}
{{ body }}

{% for video in Videos %}
{{ video_block(video) }}
{% endfor %}

在我们的_postmark.postmark目录中包含一个macros.twig

{% macro video_block(video) %}
## {{ video.title }}
[video src="{{ video.url }}"]
{% endmacro %}

继承和模板重用

支持有限的原型继承形式:如果一个原型有一个包含 Prototype:.type.md 文件,那么这个原型的属性将被视为 .type.md 的默认值。(递归地,如果第二个原型自身也有 Prototype:)。只继承属性,不继承模板,因为将模板应用于模板通常没有用。如果您需要在多个原型之间共享模板,请将其放入一个单独的 .twig 文件中,然后使用 Twig 的 include()(或 extendsimport)在每个需要它的位置应用它。

更改检测

为了使同步尽可能快,Postmark 会将导入文档的信息缓存到 WordPress 数据库中,并且只有在文档(或其原型文件)实际上发生变化时才会更新数据库。

Postmark 缓存的信息包括文档的前置内容和正文内容的哈希值,在继承原型并应用模板之后。这确保了如果文档或其原型文件发生变化,数据库将使用新的结果进行更新。

然而,这个过程不能自动检测对插件、动作、过滤器等进行的更改,这些更改会影响文档如何渲染为 HTML 或哪些数据将被插入到数据库中。除非您使用 --force 强制同步所有文档,或者您在您的前置内容中添加额外的字段,否则这些更改通常不会被捕获。

例如,您可以在您的原型中添加一个 Prototype-Version 字段,然后更改此字段的值以触发使用该原型的所有文档的更改。

当然,如果您正在创建 Postmark 扩展(例如,在 wp-cli 包、插件、主题或 Imposer 状态模块中),这不会帮到您。假设您甚至知道要编辑什么,您也不能编辑您用户的原型文件。

但您可以在导入时“编辑”您用户的 前置内容,使用 postmark load wp-post 动作。例如

add_action('postmark load wp-post', function($doc) {
    if ( $doc->has('EDD') ) $doc['EDD-Importer-Version'] = '4.1';
}, 10, 1);

add_action('postmark_metadata', function($postinfo, $doc) {
	if ( ! $doc->get('EDD') ) return;
    // ...  code to import various things to $postinfo
}, 10, 2);

在这个例子中,postmark load wp-post 处理器在加载包含 EDD 字段的文档时添加了一个额外的 EDD-Importer-Version 字段。这意味着如果 EDD 字段的导入语义发生变化,则可以更改版本,然后任何具有 EDD 字段的文档都将被视为“已更改”,因为它们的上次同步。这样,仅升级插件(或包、状态模块、主题等)就会自动使受影响的文档的缓存失效。

(顺便说一下,这种类型的版本化仅适用于修改 HTML 格式或需要访问 postinfo 对象的扩展。如果一个扩展只是提供语法糖或重新映射字段,并且可以从 postmark load wp-post 动作中完成所有操作,那么重新映射的字段就已经是文档哈希的一部分了,因此任何映射过程的变化都会自动更改受影响的任何文档的哈希值。)

Imposer集成

Postmark 为与 imposer 的可选集成提供了一个 状态模块:只需将如下 shell 块添加到您的 imposer-project.md,或任何需要将 markdown 内容作为其状态一部分的状态模块

require "dirtsimple/postmark"      # load the API

# Use postmark-module for prepackaged .md files that should be read-only and have
# ID: values already:

postmark-module "$__DIR__/stuff"   # sync `stuff/` next to this file, with --skip-create option

# Or use postmark-content for writable directories where you might be adding new .md
# files without an existing ID:

postmark-content "my-content"      # sync `my-content/` at the project root

您实际上可以使用此方法分发包含 markdown 内容的 wp-cli 包,并且它们将被自动加载到使用它们的服务器上。

注意:当调用时,postmark-modulepostmark-content 函数不会立即执行同步。相反,它们将目录信息记录到 imposer JSON 规范对象中,以便在 imposer apply 的任务运行阶段进行后续解析。(有关详细信息,请参阅 imposer 文档。)

导出帖子、页面和其他资源

为了便于与特殊帖子类型和其他数据库资源一起工作,Postmark 提供了一个 wp postmark export 命令,该命令根据提供的帖子 ID、GUID、URL 或印版引用列表(例如 @my-appt-type:id:285)在指定目录中创建 markdown 文件。任何具有已注册导出功能的资源类型都可以导出。

每个导出文件都根据其别名(即其 post_name)命名,可能以一个 - 和一个数字结尾。如果已存在具有相同别名的文件,则会检查它是否具有相同的 GUID -- 如果是,则覆盖该文件,否则增加数字并检查下一个候选文件。

(例如,如果有十个具有唯一 GUID 的帖子正在导出,且 post_namefoo,它们将最终在 foo.mdfoo-1.md、直至 foo-9.md 中,并且重复导出任何十个帖子都将使用之前使用的相同文件名,前提是没有删除任何帖子并且 post_name 没有改变。)

目前,帖子内容以原样导出到 markdown 文件中,没有尝试将 HTML 转换回 markdown。此外,可能包含大量默认值或空字段到 YAML 前端元数据中。因此,导出的帖子必须手动编辑以解决这些问题。或者,您可以在导出过程中使用 导出操作和过滤器 转换或清理内容。(例如,删除不应放在版本控制下的元字段。)

由于帖子摘要和正文在数据库中以 HTML 存储的,导出的文档将相关的 HTML: 前端元数据字段设置为 true,以便如果文件以原样导入,则内容不会重新解析为 markdown。如果您确实将内容转换为 markdown,应从 HTML: 映射中删除相关条目,或者将其重命名为 Export-HTML:,如果您将在 WordPress 中编辑内容并使用 postmark update 保存它。

另外请注意:帖子的父级、菜单顺序和 MIME 类型目前 包含在其导出文件中,因为菜单顺序和 MIME 类型仅用于菜单项和附件,而 Postmark 使用目录位置确定帖子的父级(如果有)。(帖子的 _thumbnail_id 元数据也被排除,因为它引用的整数 ID 可能在不同数据库中有所不同。)

更新导出的文档

许多 WordPress 插件和主题为帖子提供额外的设置或数据,这些设置或数据难以通过前端元数据手动指定。例如,页面构建器、访问限制工具、特定页面的主题选项等。

为了在支持版本控制和开发/生产部署的同时帮助处理这些功能,Postmark 提供了 postmark update 命令。该命令将更新的帖子属性导出到与原始 markdown 文档相邻的 .pmx.yml 文件中,这些属性在同步期间自动合并到帖子中。使用单独的文件是为了避免在主文档的前端元数据中丢失注释、空格等,并且任何在前端元数据中指定的字段都会覆盖 .pmx.yml 文件中的字段。

为了导出帖子元数据字段,它们必须在文档的 Export-Meta: 前端元数据中列出(或从原型中继承)。例如,将以下内容放入文档的前端元数据中,将允许使用 Elementor 做出的更改通过 postmark update 保存,并通过 postmark syncpostmark tree 应用到相同的或另一个数据库。

Export-Meta:
  # On `postmark update`, export primary Elementor fields
  _elementor_edit_mode:
  _elementor_template_type:
  _elementor_version:
  _elementor_data:

Post-Meta:
  # Forcibly delete the CSS cache on import, so it won't be stale
  _elementor_css: null

Export-HTML:  # Use HTML from Elementor instead of the markdown body
  body:

请注意,为了安全使用此功能,您需要充分理解您插件提供的各种元数据字段所代表的含义,以免在部署时损坏数据。

例如,Elementor 包含一个 _elementor_css 字段,在导入时应该始终删除,以确保正确的 CSS;而 LifterLMS 有一个 _llms_num_reviews 字段,不应该导入以避免丢失数据。一些插件也可能基于其他字段生成值,并且通常只在通过 UI 更新帖子时设置它们,而不是通过命令行。所以在提交和部署您的 .pmx.yml 文件之前请务必小心。

设置 Export-Meta 字段的简单方法是将现有的帖子或页面导出,然后编辑导出的文件,将 Post-Meta: 字段重命名为 Export-Meta:。然后,删除不应通过导入重置的字段以及您希望保留的字段值。一旦您对需要的字段有了清晰的认识,您可能希望将它们添加到一个原型中,这样就不需要将它们复制到多个文件中。单个文档可以添加任何额外的字段,或者通过设置值为 false 来抑制字段的导出。例如,以下设置将防止 _some_field 在当前文档中被导出,即使原型中列出了导出。

Export-Meta:
  _some_field: false

对于 Export-Meta 中的每个非 false 条目,在 .pmx.yml 导出文件的 Post-Meta 中都有一个对应的条目:要么是该元数据字段的值,要么如果帖子缺少该字段,则是 null。这意味着在导入时,将明确删除这些缺失的字段,从而删除数据库中的任何悬空值。这对于使用基于元数据字段的呈现或缺失来决定事情(而不仅仅是其内容)的插件尤为重要。

更新导出的HTML

如果您将通过 WordPress GUI(例如,使用 Gutenberg 或页面构建器)编辑或生成帖子的内容或摘要,您应该在您的文档中添加一个 Export-HTML: 字段,列出要导出的字段,并从文档中(以及如果存在的话,从 HTML: 字段)删除相应的字段。例如,如果您想创建一个新文档,其内容主要通过 Gutenberg 编辑器进行编辑,您可以创建一个新的空文档,如下所示

---
Title: An Example
WP-Type: page

Export-HTML:
  body:
  Excerpt:
---

在此文档导入到 WordPress 页面后,您可以使用 postmark update 导出正文和摘要的 HTML。

(相反,如果您已经创建了 WordPress 中的文档,您可以使用 postmark export 创建初始的 markdown 文件,但您将需要编辑它并将 HTML: 字段重命名为 Export-HTML:,删除正文文本和 Excerpt:,然后对该文件运行 postmark update 以替换相应的 .pmx.yml 文件中的旧正文和摘要。)

更新其他字段

除了元数据和 HTML 字段之外,您还可以允许在执行 postmark update 时从 WordPress GUI 更新其他字段:只需将这些字段添加到 Export-Fields:。例如,以下前置信息将导致 Updated:Tags: 字段在更新期间导出到 .pmx.yml 文件中。

---
Export-Fields:
  Updated:
  Tags:
---

但请记住:由 postmark update 导出的字段只是 默认值:您必须从主 .md 文件中删除相应的字段,以便导出的数据可以导入。

操作和过滤器

Postmark 的所有操作和过滤器都可以从插件、主题、wp-cli 包或 imposer 状态模块注册。因为 Postmark 基于 imposer 构建,所以您可以将 imposer_tasks 动作钩子用于注册其他动作和过滤器——这意味着您可以将您的 imposer 或 postmark 特定的钩子放在一个单独的 PHP 文件中,然后通过 require_once 那个文件,例如。

add_action('imposer_tasks', function() {
    require_once(__DIR__ . '/includes/cli-hooks.php');
});

然后,您可以在includes/cli-hooks.php中放置任何代码以注册邮戳操作或过滤器,并且只有当运行wp postmarkimposer时,该文件才会被加载。

Markdown格式化

Markdown格式化由以下过滤器控制

  • apply_filters('postmark_formatter_config', array $cfg, Environment $env) -- 该过滤器在每次命令调用时被调用一次,以初始化League/Commonmark的环境配置。过滤器可以向Environment对象添加Markdown扩展、解析器、处理器或渲染器,或返回修改后的$cfg数组。除了标准配置元素外,$cfg还包含一个映射扩展或解析器类名到参数数组的extensions数组(或null)。使用给定的参数数组实例化这些扩展类,并将它们添加到$env。可以通过将扩展在extensions数组中的值设置为false来禁用扩展。

    当前默认扩展包括

  • apply_filters('postmark_markdown', string $markdown, Document $doc, $fieldName) -- 该过滤器可以在将文档内容(或其任何前导字段)转换为HTML之前修改文档的markdown内容。$fieldName"body",如果$markdown来自$doc->body;否则,它是正在转换的前导字段名称。(例如"摘要",或任何由插件添加的自定义字段。)

  • apply_filters('postmark_html', string $html, Document $doc, $fieldName) -- 该过滤器可以在将文档内容(或其任何前导字段)转换为HTML后立即修改其HTML内容。与postmark_markdown过滤器一样,$fieldName"body"或前导字段名称。

请注意,postmark_markdownpostmark_html可能会多次调用或根本不调用,因为它们在调用$doc->html(...)时运行。如果在Postmark有机会之前,同步过滤器或操作设置了$postinfo['post_content']$postinfo['post_excerpt'],除非过滤器或操作使用$doc->html(...)进行转换,否则这些过滤器不会调用。

另外请注意,如果您正在向任何这些过滤器添加钩子,那么您也应该在相关的postmark load过滤器期间向文档添加格式化版本信息,以便在您的扩展被添加、删除或更新时,任何受影响的文档都将被视为“更改”并重新同步。

文档对象

许多过滤器和操作接收dirtsimple\Postmark\Document对象作为参数。这些对象提供了以下API

  • 文档的元字段可以作为公共可写对象属性访问。(例如,$doc->Foo 返回元字段 Foo)。无效的 PHP 属性名可以使用例如 $doc->{'Some-Field'} 的方式访问。缺失或空的字段返回 null;如果字段缺失时希望有不同的默认值,可以使用 $doc->get('Somename', 'default-value')
  • $doc->body 是文档的 Markdown 文本,且是一个可写属性。
  • $doc->html($propName='body') 将命名属性从 Markdown 转换为 HTML(触发 postmark_markdownpostmark_html 过滤器)。
  • $doc->has("field") 如果文档的元字段中有 "field",则返回 true。
  • $doc->get("field", $default=null) 返回来自元字段的 "field" 内容,或者如果没有找到则返回 $default
  • $doc->select(['field'=>callback, ...]) 如果匹配的字段存在,则调用每个 callback,该字段的值作为参数传递。返回值是一个包含仅存在字段键的数组,其值是调用回调的结果。如果回调实际上不可调用(例如 true),则该值将作为输出数组中的值返回。如果回调是一个关联数组,则递归处理,例如 $doc->select(['EDD'=>['Price'=>$cb]]) 只有当存在具有 Price 子字段的 EDD 元字段时,才会调用 $cb

postmark load resource-kind

每次从磁盘加载文档时,都会运行 do_action("postmark load $kind", Document $doc),以便在计算文档哈希之前修改文档(例如其 Post-Meta)。在此操作期间对文档所做的任何更改都会影响哈希计算,因此这是进行简单语法糖或字段重映射的理想位置。

但是,如果您正在编写需要进行复杂计算或访问数据库的扩展,则可能需要使用不同的钩子,并在此操作期间添加版本字段(例如 MyPlugin-Version-Info),以确保在您的算法更改时文档重新同步。同样,如果您的扩展正在更改 Markdown 格式化方式,则应添加版本字段以确保添加、删除或更新您的扩展将强制受影响的文档重新同步。

默认的 资源类型wp-post,这意味着文档将被映射到 WordPress 文章,或者如果文档有一个 x-option-value URL,则默认资源类型是 wp-option-html。其他资源类型可以通过使用 postmark_resource_kinds 动作 注册,并通过 Resource-Kind: 元字段分配给文档(直接在文档中或通过原型)。

postmark_resource_kinds

Postmark 不仅用于导入文章和选项值。原则上,它可以用于导入其他内置 WordPress 对象(如用户或分类)或由插件定义的专用对象(如存储在自定义数据库表中的 Gravity Forms)。

为了确定要导入的对象类型,Postmark 会查看文档的 Resource-Kind 字段,默认为 wp-post(除非使用 x-option-value: URL 的 ID:,在这种情况下,默认类型为 wp-option-html)。但是,您可以使用文档的元字段或其原型覆盖这些默认值,只要插件已注册该资源类型的导入处理程序。

要添加其他资源类型,扩展可以注册 postmark_resource_kinds 动作的钩子,该钩子将接收一个将类型名称映射到 "类型定义" 对象的类似数组对象。注册此动作的处理程序可以使用如 setImporter()setExporter()setEtagQuery() 等配置方法配置类型。例如,下面的代码注册了 my_plugin-item 资源类型的导入器和导出器。

add_action('postmark_resource_types', function($kinds) {
    $kinds['my_plugin-item']->setImporter('my_plugin_import_item_from_doc');
    $kinds['my_plugin-item']->setExporter('my_plugin_export_item_to_doc');
});

function my_plugin_import_item_from_doc($doc) {
    # import $doc into database, saving $doc->etag() with it for caching purposes,
    # then return a database ID or WP_Error
}

function my_plugin_export_item_to_doc($md, $id, $dir, $doc=null) {
    # Export an database object whose ID is $id by setting values on the
    # MarkdownFile in $md, returning a slug that will be used to generate
    # the export filename.  Return `false` if $id isn't found, or a WP_Error
    # to signal other error conditions.  If $doc is non-null, the export is
    # an update to an existing document, and $doc can be used to trim or
    # filter the output fields accordingly (e.g. the way posts use `Export-Meta:`).
}

每当一个文档具有 Resource-Kind:my_plugin-item 时,将调用 my_plugin_import_from_doc() 函数来执行导入,取代 Postmark 内置的同步处理。

为了避免对未更改的文件进行不必要的数据库更新,Postmark 为文档内容计算一个 etag(可以通过 $doc->etag() 方法获取)。导入程序应在导入时将此值保存到数据库中,并在同步开始时注册一个处理程序来检索这些 etag。

例如,假设我们想要为 WordPress 用户创建一个导入程序,但不想在导入文件(或文件集)未更改时更新用户数据。我们需要注册一个导入程序和一个 etag 查询,例如

function demo_user_importer($doc) {
    # Create or update a user
    if ( $user_id = email_exists($doc['Email']) ) {
        $user_id = wp_update_user(...);  # ...with appropriate data
    } else {
        $user_id = wp_insert_user(...);  # ...with appropriate data
    }

    # Return error if insert or update failed
    if ( is_wp_error($user_id) ) return $user_id;

    # save $doc->etag() for caching
    update_user_meta($user_id, '_postmark_cache', wp_slash($doc->etag()));
    return $user_id;
}

add_action('postmark_resource_types', function($kinds) {
    global $wpdb;
    $kinds['wp-user']->setImporter('demo_user_importer');
    $kinds['wp-user']->setEtagQuery(
        "SELECT user_id, meta_value FROM $wpdb->usermeta WHERE meta_key='_postmark_cache'"
    );
});

在上面的示例中,使用一个 _postmark_cache 元数据字段来存储 etag,并通过简单的 SQL 查询获取它。使用的查询必须以该顺序返回数据库 ID 和 etag,作为每个适当类型的项目的第一个两个字段。

当然,在某些情况下,无法直接查询数据库以获取必要的信息。例如,Gravity Forms 不使用类似 WordPress 的元数据为其表单,因此包含的 Gravity Forms 示例扩展 使用 setEtagOption('gform_postmark_etag') 而不是 setEtagQuery() 来配置资源类型的 etag 处理。这告诉 Postmark 从指定的选项自动保存和加载 etag。

这种方法的缺点是,您需要编写代码来从选项中删除已删除的 ID,当关联的数据库项被删除时。(在上面的用户示例中不需要这样做,因为删除用户会自动删除关联的元数据字段。)

最后,如果您既不能使用数据库查询也不能使用选项来处理您的资源类型,则可以使用 setEtagCallback($callback) 来注册一个函数,该函数将不带参数调用,并应返回一个数组,将您资源类型的数据库 ID 映射到其关联的 etag。(与 setEtagQuery() 一样,您的导入函数负责将 etag 存储在数据库中。)

帖子同步操作

在帖子同步过程中,文档将构建一个 $postinfo 数组,并将其传递给 wp_insert_postwp_update_post。 (Postmark 只设置尚未由操作或过滤器设置的值,因此您可以通过先设置一个值来防止它这样做。)

例如,Postmark 在调用 postmark_metadata 动作后,但在调用 postmark_content 动作之前计算 post_content。这意味着您可以通过从 postmark_before_sync 动作或 postmark_metadata 动作中设置 post_content 来防止 Postmark 进行其自己的 Markdown-to-HTML 转换。

注意:$postinfo 实际上不是一个 PHP 数组 - 它是一个 PHP ArrayObject 子类,具有一些额外的功能,如 get($key, $default=null)has($key) 等。但您仍然可以将其视为常规数组来设置、获取或删除项目。您可以在 dirtsimple\imposer\Bag 类 中查看有关大多数其他可用方法的详细信息,但还有一些可能对您有帮助的额外方法。

  • $postinfo->id() 返回正在更新的现有帖子的帖子 ID,或者如果帖子是新帖子,则返回 null。
  • $postinfo->set_meta($key, $val) - 执行 update_post_meta,将 $key 设置为 $val$key 可以是一个字符串或一个数组:如果它是一个数组,则被视为元字段内的子项的路径,类似于 wp-cli wp post meta patch insert 命令的键路径,但父数组会自动创建。
  • $postinfo->delete_meta($key) -- 删除指定的元数据键,或者如果 $key 是一个数组,它将被视为元字段中要删除的子项的路径,就像 wp-cli wp post meta patch delete 命令的键路径。

以下操作在同步过程中运行(对于文章,不是选项),按以下顺序

postmark_before_sync

do_action('postmark_before_sync', Document $doc, PostModel $postinfo) 允许在同步之前修改文档(例如 Post-Meta 字段)或其他操作。此操作可以设置 $postinfo 对象中的 WordPress 文章字段(例如 post_authorpost_type),以防止 Postmark 对这些字段进行默认翻译。(此时对象大多为空,因此从中读取并不十分有用。)将 $postinfo->wp_error 设置为 WP_Error 实例将强制同步以给定错误终止。

postmark_metadata

do_action('postmark_metadata', PostModel $postinfo, Document $doc) 允许您修改将传递给 wp_insert_postwp_update_post$postinfo。此钩子可用于根据前端内容覆盖或扩展基于 WordPress 字段的计算。

当此操作运行时,$postinfo 将初始化为 Postmark 从前端内容计算出的任何 WordPress 字段值,或者通过 postmark_before_sync 动作在 $postinfo 中设置的值。然而,它目前不包含 post_contentpost_excerpt,除非之前有操作或过滤器设置。

为此操作注册的函数可以将 post_contentpost_excerpt 设置在 $postinfo 中,以防止 Postmark 执行此操作。它们还可以将 $postinfo['wp_error'] 设置为 WP_Error 对象,以错误终止同步过程。

如果处理此操作注册的所有函数后,post_contentpost_titlepost_excerptpost_status 仍然为空,Postmark 将通过将文档体从 Markdown 转换为 HTML,以及/或根据需要提取标题和摘要,提供默认值。

postmark_content

do_action('postmark_content', $postinfo, Document $doc)postmark_metadata 操作类似,但 markdown 转换和标题/摘要提取已在需要时完成。

postmark_after_sync

do_action('postmark_after_sync', Document $doc, WP_Post $rawPost) 允许在文档和/或生成的文章上运行文章同步后的操作。$rawPost 是一个 raw-过滤的 WordPress WP_Post 对象,反映了现在同步的文章。这可以用于处理需要知道文章 ID 的前端内容字段(例如,向自定义表添加数据)。

选项同步操作

postmark_before_sync_option

do_action('postmark_before_sync_option', Document $doc, array $optpath) 在处理同步到 选项 HTML 值 的文档之前运行。没有 $postinfo,因为没有创建或更新文章。但是,此过滤器仍然可以访问或修改文档的任何其他属性,例如,在更新选项之前以某种方式预处理体。为了方便起见,$optpath 包含正在同步的选项的路径,例如 ['edd_settings', 'purchase_receipt']

postmark_after_sync_option

do_action('postmark_after_sync_option', Document $doc, array $optpath)postmark_before_sync_option 类似,但它在更新 HTML 版本的文档体中的选项值之后运行。此钩子的典型用途是从文档的前端内容更新其他选项,例如。

use dirtsimple\Postmark\Option;

add_action('postmark_after_sync_option', function($doc, $optpath){
    if ( $optpath === ['edd_settings', 'purchase_receipt'] ) {
        if ( $doc->has('Title') )  Option::patch(['edd_settings', 'purchase_subject'], $doc->Title);
        if ( $doc->has('Header') ) Option::patch(['edd_settings', 'purchase_heading'], $doc->Header);
    }
}, 10, 2);

上面的示例代码将在同步任何 ID 为 urn:x-option-value:edd_settings/purchase_receipt 的文档之后运行,检查文档的标题和/或标题字段,然后使用它们设置相关的 EDD 选项。

导出操作和过滤器

在每个以下操作和过滤器中,$md 参数是一个 dirtsimple\Postmark\MarkdownFile 对象,其 body 是正在导出的文章的 post_content,其其他属性将作为文档开头的 YAML 前端内容导出。这两个操作和过滤器都可以根据需要读取、设置或取消设置这些属性,从而改变将写入输出文件的属性。

以下钩子按执行顺序列出

postmark_export_meta

do_action('postmark_export_meta', $postmeta, MarkdownFile $md, WP_Post $post) 允许您修改 $postmeta 的内容(如果其中还有内容,最终将填充导出文档的 Post-Meta: 字段)。您可以使用此功能取消设置在输出中无用的元值,或从它们设置文档字段。(例如,postmark 本身从 $postmeta['_wp_page_template'] 设置 $md->Template,然后从 $postmeta 中取消设置。)

$postmeta 对象是一个 Bag(具有额外方法的 ArrayObject 子类,见 额外方法),它支持正常的数组操作,如 $postmeta['foo']="bar",以及如 has()get()select() 之类的功能。

postmark_export_meta_$key

do_action("postmark_export_meta_$key", $meta_val, MarkdownFile $md, WP_Post $post) 在运行 postmark_export_meta 钩子后,对 $postmeta 中仍存在的每个元字段执行。 $meta_val 是字段的值。

如果有任何钩子注册为此操作,相应的 $key 将从导出文档的 Post-Meta: 字段中移除。(这意味着您可以使用 add_action("postmark_export_meta_somekey", "__return_true"); 作为抑制元键导出的简单方法(例如,包含不应保存在导出文件中的动态状态的元键。)

postmark_export

do_action('postmark_export', MarkdownFile $md, WP_Post $post) 在输出文件写入之前,每次导出都会调用一次,传递一个完全填充的 MarkdownFile 对象。您可以使用此功能根据需要添加、更改或删除字段。(例如,为存储在其他表中的数据的自定义帖子类型添加字段。)

postmark_export_slug

apply_filters('postmark_export_slug', string $slug, MarkdownFile $md, WP_Post $post, $dir) 过滤用于生成导出文件名的 slug。 $dir 是输出目录,可以是空字符串(表示当前目录)或带有尾随 / 的目录名。 $slug 的初始值来自 $md->Slug,在 postmark_export 钩子有机会更改它之后。

postmark update wp-post

do_action('postmark export wp-post', Bag $export, MarkdownFile $md, WP_Post $post, Document $doc)postmark update 命令在更新现有文档($doc)时从相应的 $post 运行。 $md 变量包含如果执行正常导出将会写入的 MarkdownFile 对象,而 $export 是将内容写入 .pmx.yml 文件的 ArrayObject。为此操作注册的回调可以修改 $export 以更改要写入的数据。

虽然大多数为此操作注册的回调只需要前两个参数,但可以使用 $doc 参数来查找标志以决定如何进行导出。例如,您可以使用 $doc->has('Export-Foo') 来决定是否导出某些数据,类似于内置的 Export-HTMLExport-Meta 字段。在 $doc 中找到标志后,然后从 $md(或 $post)复制相关数据到 $export 上的字段。

其他过滤器

postmark_author_email

apply_filters('postmark_author_email', string $author, Document $doc) 过滤 Author: 前面的字段(如果存在),以提取用于查找帖子作者数据库 ID 的电子邮件或登录名。此过滤器接受字符串,并应返回电子邮件、登录或 WP_Error 对象。如果字符串已经是有效的电子邮件或登录,则过滤器应返回它不变。

仅当前面有 Author: 字段并且 postmark_before_sync 没有设置 post_author 时,才会调用此过滤器。内部,postmark 将此结果视为 Imposer @wp-user 引用键,因此从技术上讲,您可以返回 Imposer 识别为 @wp-user 引用的任何内容,而无需特定键类型。

postmark_excluded_types

apply_filters('postmark_excluded_types', array $post_types) 用于过滤一个数组,其中包含了不会被 postmark 导入的文章类型。默认情况下,该数组包含 ['revision', 'edd_log', 'edd_payment', 'shop_order', 'shop_subscription'],用于排除修订和 EDD/WooCommerce 订单。如果您正在使用任何具有自定义文章类型的插件,这些类型可能增长到数千条,但不需要导入,将这些文章类型添加到列表中将有助于保持 postmark 启动速度快,通过减少需要从数据库中加载的 GUID 数量。

项目状态/路线图

该项目仍处于早期开发阶段:测试不存在,CLI 输出的国际化支持不完整。我希望未来包含的功能包括

  • 一种标记摘要提取分界点的方法(最好是通过链接从摘要到目标页面的断点进行定位)
  • 一种处理图片/附件的方法
  • 将相对文件链接转换为绝对 URL 的链接翻译

请查看完整的 路线图/待办事项