虽然已经摸鱼很久了,这篇文章最后还是决定补一下,免得日后网站维护的时候自己全忘了。另一方面,虽然关于 WordPress 和 Sakurairo 主题的指导在网络上有很多,文档也很详细,但是结合 Obsidian 等本地管理的参考文章还是相对较少,里面还是有不少坑。在这里也记录一下。
合适的建站方式?
既然会看到这里,相信你也是有建立个人网站的想法的。首先需要明确的是,使用 WordPress 模板的方式是最适合你的吗?
我从开始尝试建立个人网站到写下这篇文章,花费了大约一年多的时间进行探索。我尝试过以下方式:
- 不是建立个人网站,而是在社交平台发布自己的文章。我现在还会在知乎上写一些想法。好处是完全没有网站运行成本,并且可以和很多人讨论。坏处是没有什么可定制的空间,
也不够骚。 - 使用 GitHub Pages 或者 cnblog 这样的个人网站。cnblog 在用上一个好的模板时,看上去还是不错的。好处是只有很少的网站运营成本,只需要把前期的设置做好,后期维护就没什么成本了。不需要购买服务器,也不需要域名。坏处是可定制的空间很小。当然,如果是前端大师,去查看并翻改网页模板的代码也是可以的。我自己试过,读这些代码很痛苦,代价很大。如果只是想要做一个发布技术文章的网站,这还是挺适合的。
- 完全手搓。我确实尝试过。我曾经花了一个暑假的时间,用 Vue 搓出了一整个网站,并且放在了阿里云的服务器上。这样做的好处是完全可定制,几乎完全可以按照自己的需求来搓,坏处就是维护的代价非常大。
所以,最后我在闲着没事的时候发现了 WordPress + Sakurairo 这个组合,我认为它提供了对我来说足够的定制空间,同时维护成本在我可接受范围之内,提供的功能也满足我所有的需求。
WordPress 安装踩坑
如果选择手动安装 WordPress(而不是从宝塔面板或者其他安装工具),那么你一定见到过了所谓的五步安装法。简单来说,这五步就是把 WordPress 解压并放在服务器的某个目录下,然后在浏览器中用域名加路径的方式访问下面的index.php。
诶,怎么这样就可以从浏览器里面访问到index.php?当然是不行的!但是所有手动安装的教程都没有提到这一点,不知道是我太蠢了还是什么问题。在使用浏览器之前,必须先使用 Nginx 或者其他类似的软件配置好对文件的访问,并安装&启动 php 的运行环境。对 Nginx 来说,最核心的是:
location / {
try_files $uri $uri/ /index.php?$args;
}
具体怎么写可以询问 GPT。
插件&FTP 路径
在安装好 WordPress 之后,安装插件时,需要开启 FTP 服务才能安装。FTP 服务怎么开启这里就不提了,但是如果你的 WordPress 路径并不是在/下,e.g. 使用 Nginx 时它很可能在/usr/share/nginx/html下,此时可能会发生 FTP 找不到路径之类的报错。此时需要修改wp-config.php:
define('FTP_BASE', '/usr/share/nginx/html');
define('FTP_CONTENT_DIR', '/usr/share/nginx/html/wp-content');
define('FTP_PLUGIN_DIR', '/usr/share/nginx/html/wp-content/plugins');
define('FTP_THEMES_DIR', '/usr/share/nginx/html/wp-content/themes');
define('FS_METHOD', 'direct');
顺带一提,很多配置都可以在这里修改。比如,你一时兴起决定换一个二级域名给你的网站使用,除了要修改 Nginx 的匹配规则以外,还需要修改:
define('WP_HOME', 'https://blog.chensy.moe/';
define('WP_SITEURL', 'https://blog.chensy.moe/';
WordPress 安装大概的雷点是这些。如果还有什么问题的话,重启一下 Nginx 能解决大部分的问题。
WordPress Rest API 踩坑
WordPress 自带的编辑器实际上已经不错了,但是和本地编辑器相比还是不太丝滑。目前我的选择是在本地用 Obsidian 的仓库管理所有写过的东西。Obsidian 的仓库可以通过 Remotely Save 插件使用 Onedrive 在不同的设备之间同步,同时选择性地将一些文章通过 Rest API 上传到 WordPress。即使能够建立这样的流程,为了适配 WordPress 的渲染,最好让自己的写作习惯尽量贴近标准的 Markdown,太多的 Obsidian 扩展格式处理起来会很麻烦。
Obsidian 有一个 WordPress 的插件,支持通过 xmlrpc/Rest API 向 WordPress 提交文章。这个插件还能用,但是已经很久没有维护了,并且有一些 bug。事实上手写一个脚本也很简单,所以最后还是用了手写的方式。
Image Service
当然,在解决如何向 WordPress 上传文章之前,需要先解决图片的问题。这个问题 obsidian-wordpress 插件也没有解决,所以肯定是要手动解决的。解决方式无非就是把图片同步到图床,然后用一个脚本替换所有的图片链接。我选择了直接用当前的服务器提供图片服务,因为在安装了 WordPress 之后,Nginx 实际上已经能够提供这样的服务:
location / {
try_files $uri $uri/ /index.php?$args;
}
对,还是这段配置。在对应的 location 下放一个储存图片的文件夹,把图片同步过去就可以了,不需要做任何额外的配置。至于同步,我选择的是使用 rsync 手动同步。理论上 Syncthing 能够实现自动同步,但是它在公网上的表现似乎很差,所以最后也没有这么做。
Authentication
最后,就是把文章上传到 WordPress,此时要进行用户的登录授权,但这也是事实上最大的坑。首先,虽然 Rest API 默认是开放的,但是 Authentication 功能是由插件提供的,不是自带的。如果在没有安装相关插件的情况下,使用GET以外的方法访问 Rest API,会得到rest_cannot_create的回复。插件中,下载量最大的一个应该是miniOrange,但是这个插件对于 Sakurairo 主题的用户来说并不适用。主要的原因是 Sakurairo 主题中自定义了一些 API,而 miniOrange 插件默认会给所有的接口都加上授权,这会导致未登录的用户无法加载出一些展示图片。miniOrange 提供了自定义哪些接口不需要授权保护的功能,但这是收费的,并且贵得很。所以,对于这个主题的用户以及其他使用自定义接口的用户,这个插件并不合适。
我目前使用的是JWT Authentication插件,它只保护默认的 API,因此和目前的需求刚好合适。使用方法在插件的说明页里面有。
之前提到 obsidian-wordpress 插件,这个插件也是要配合 WordPress 中的授权插件使用的,尽管插件的说明中完全没有提到。这个插件目前的问题是只支持把文章推送为 posts 类型,不能推送成说说等其他类型。事实上它提供了这个选项,但是完全不起作用,github 上也有人提到了这个issue,但是作者应该没有要解决的意思。实际上,posts 的接口是/wp/v2/posts,说说的接口是wp/v2/shuoshuo,仅此而已。(没找到文档,是猜出来的)
下面是提交一篇文章的完整的脚本。提交成功后,脚本向 markdown 中写入一段前缀 yaml,在 Obsidian 中称为 properties,用于记录文章在网站上的 id。如果这篇文章被第二次提交,则从 yaml 中找到它的 id,从而自动执行更新文章的操作。
顺带一提,python 中使用最广泛的 markdown 解析库python-markdown和 Obsidian 的格式有些相性不合。个人建议使用mistune。
import requests
import base64
import yaml
import re
import argparse
import os
import mistune
# 全局变量
url_base = 'https://blog.chensy.moe/'
username = 'xxx'
password = 'xxx'
token = None
def upload_or_update_post(title, content, post_id=None, post_type='posts'):
# 构建认证信息
# credentials = f"{username}:{password}"
# token = base64.b64encode(credentials.encode()).decode()
# headers = {'Authorization': f'Basic {token}'}
headers = {'Authorization': f'Bearer {token}'}
# 构建请求数据
data = {
"title": title,
"content": content,
"status": "draft"
}
if post_id:
url = f"{url_base}/wp-json/wp/v2/{post_type}/{post_id}"
else:
url = f"{url_base}/wp-json/wp/v2/{post_type}"
response = requests.post(url, headers=headers, json=data)
if str(response.status_code).startswith('2'):
response_data = response.json()
return {
"id": response_data.get("id"),
"date": response_data.get("date"),
"modified": response_data.get("modified"),
"type": response_data.get("type")
}, response.status_code
else:
return {"error": "Failed to create or update post", "response": response.json()}, response.status_code
def read_markdown_with_front_matter(file_path):
# 读取文件内容
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
# 正则表达式匹配front matter
front_matter_match = re.match(r'^---\s*([\w\W]*?)\s*---\s*(.*)$', content, re.DOTALL)
if front_matter_match:
front_matter_yaml = front_matter_match.group(1).strip()
body_markdown = front_matter_match.group(2).strip()
# 解析front matter
front_matter = yaml.safe_load(front_matter_yaml)
else:
front_matter = None
body_markdown = content.strip()
# 将markdown正文转换为html
body_html = mistune.html(body_markdown)
return front_matter, body_markdown, body_html
def commit_markdown(file_path, post_type, debug=False):
front_matter, body_markdown, body_html = read_markdown_with_front_matter(file_path)
file_name = os.path.basename(file_path)
title = os.path.splitext(file_name)[0]
if front_matter is None:
post_id = None
else:
post_id = front_matter.get('id')
if post_id is None:
print('No id available in front matter')
return
metadata, status_code = upload_or_update_post(title, body_html, post_id=post_id, post_type=post_type)
if not str(status_code).startswith('2'):
print(metadata)
return
# Write the front matter back to the file
updated_content = '---\n' + yaml.dump(metadata, allow_unicode=True) + '---\n' + body_markdown
new_file_path = file_path.replace('.md', '_updated.md') if debug else file_path
with open(new_file_path, 'w', encoding='utf-8') as file:
file.write(updated_content)
print('commit success')
def jwt_init():
global token
auth_data = {
"username": username,
"password": password
}
url = f"{url_base}/wp-json/jwt-auth/v1/token"
response = requests.post(url, json=auth_data)
if not str(response.status_code).startswith('2'):
print('authentication failed')
return
token = response.json().get("token")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Read a Markdown file with optional YAML front matter and convert the body to HTML.")
parser.add_argument('file_path', type=str, help='Path to the Markdown file.')
parser.add_argument('--type', type=str, default='posts', help='Type of the post (default: posts).')
parser.add_argument('--debug', action='store_true', help='Enable debug mode to write output to a new file.')
args = parser.parse_args()
jwt_init()
commit_markdown(args.file_path, args.type, args.debug)

Comments NOTHING