本站是如何建立并完善的

on

|

views

and

comments

这是本站的第一篇文章,记录本站是如何建立的,也许对打算建立个人网站的小伙伴也有一些参考。本站是基于阿里云购买的服务器与域名建立,使用Newspaper主题设计的。本文除了对这两点进行记录,还说明了如何解决网站无法发送邮件、无法渲染Latex公式、设置https访问的问题。

本文封面使用的图片是pixiv插画师邦乔彦的《夏梦》,本站是在暑假建成的,以此封面来给本站未来发布精良美观的内容一个美好的寓意。

配置环境

要建立网站发布内容就像你要打一个游戏,首先要买电脑,然后安装软件,才能开始打游戏一样,做好配置环境的工作你才能谈设计网站制作博客。

购买服务器和域名

首先需要购买服务器和域名,服务器是一台云服务公司的虚拟主机,你远程对它进行操作,它具有公网IP,可以被公网访问(理论上来说个人电脑也可以作为服务器做网站,但是你一般是在局域网里,需要申请到公网IP才能被公网访问);域名是你的IP地址的一个别名,你在购买服务器后,它会有一个IP地址,IP地址可以理解为这个服务器的身份证号码,是一串x, x, x, x的数字,其中每个x取值范围0-255,但是你一般不会用这个去用这个来指定访问一个网站,就像你不会用身份证号码来记人,一般用名字,你一般会用www.baidu.com来访问百度而不是它的IP地址。我是在阿里云购买服务器和域名的,服务器在阿里云特惠服务器购买的,99元/年;域名在阿里云域名注册购买的,域名一般.com这种正式的后缀比较贵,前缀如果比较正式也会比较贵,你可以看有哪些后缀,然后想一个你希望的前缀,这就组成了一个网址,如果这个网址没有被别人占有的话你就可以购买了,我购买的是qimuchuan.life域名,六七十块钱一年,之前还买过一个一块一年的js-ye.top。

安装WordPress镜像并登录WordPress

当你购买好服务器之后就要在服务器里安装WordPress以及它依赖的一些软件了。题主尝试过跟着网络上的某些博客配置环境,但是都出现了各种各样的bug;也有使用宝塔面板的,但是我个人不是怎么想用。后来发现了阿里的文档里有直接安装镜像就能搭好个人WordPress的,文档地址:快速搭建WordPress。里面有3种搭建WordPress方案,我选择的就是第2种。文档十分详细,会教你如何安装WordPress镜像,然后获取WordPress账号密码以及数据库密码,如何登录wordpress进入后台面板,最下面还说了需要备案域名(不然在国内无法通过域名解析来访问你的网站,并且备案过程1-2周,需要耐心等待),提供了备案的说明文档链接,以及备案后如何把你的网站从IP地址访问变成域名访问。当你做完这些之后,基础工作就完成啦!

网站基础

恭喜你已经成功登录WordPress了,你一定迫不及待想要开始发布自己的第一篇博客了吧。No, no, no. 如果你对自己的网站有一些外观与质量上的要求的话,还是要完成一些工作的。

设计WordPress

登录Wordpress之后,你会看到一篇《世界,你好!》的文章以及白茫茫的主题,如何让自己的网站美观甚至炫酷呢,你可以选用一个好看的主题,在已经做好的基础上按照自己需求进行改动。登录后台面板,选择外观-主题-添加主题,你可以浏览主题,挑选你自己喜欢的,或者上网阅读推荐WordPress主题的博客,看看自己有没有适合你的,看中了某个主题直接在浏览主题界面搜索名称即可。一个主题下会有多套模板,一般都有免费套餐,当然你如果要更多模板更多功能就得加钱。

外观-主题-添加主题
Newpaper主题有很多模板

我选择了Newspaper,其中的Art Blog模板是我在看微信公众号里面对WordPress主题做推荐的文章里看到的,看到的第一眼我就觉得这是我的梦中情板,就花钱买下来了。我之前也用过其他主题,Newspaper主题一个最大的优点就是他的功能很齐全,你完全可以使用主题自带的工具来设计出好看的页面。而这些工具的操作方法我也没有看教程,你可以自己多尝试来探索它的工具可以实现什么样的效果然后你就慢慢地可以知道你想要达到什么效果以及如何达到了。在Newpaper设计网站也没有什么难度,并且其他主题就不适用Newpaper的相关内容了,所以在这里不过多阐述。

网站无法发邮件怎么办

如果你对网站仅仅是想发布自己的文章,不需要搞评论的话,你大可记好自己的账号密码(因为如果你忘记了就要重置密码,而重置需要网站发一封邮件给你的邮箱),不必管自己的网站无法发邮件。如果你需要在网站里设置多名用户,想要开启评论并且评论必须是注册了的订阅者发表的话,你就需要你的网站可以成功发邮件了。你会发现当你想要更改自己的邮箱时,它会提示你验证了邮箱之后更改才生效,而你没有收到邮件。这时你需要使用SMTP服务,对国内用户来说,比较方便的是网易邮箱和QQ邮箱。在这里我使用的是QQ邮箱,因为网易邮箱只能用手机号注册一个,而QQ邮箱注册了一个QQ号就有一个邮箱(我使用了一个QQ邮箱负责网站发邮件,另外一个QQ邮箱作为网站管理员的邮箱,这样就不会让我自己用的网易邮箱被消息轰炸了)。

具体流程如下:在WordPress安装WP Mail SMTP插件,然后登录QQ邮箱申请SMTP服务,点击邮箱头像-账号安全-安全设置-POP3/IMAP/SMTP/Exchange/CardDAV 服务,点击“生成授权码”,把授权码拷贝到一个地方一会儿要用;然后进入WordPress后台的WP Main SMTP进行信息填写,邮箱名称是你的邮箱名,密码就是刚才的授权码,QQ邮箱的SMTP主机是smtp.qq.com,SSL方式加密对应的端口是465,TLS对应的是587(我这里踩坑了,把它们对错了,搞得我一直失败,后面发现点击一个加密方式会自动填写端口才发现我对应错了)。最后验证一下成功就说明你的网站可以发邮件了。接下来网站可以支持读者通过邮箱注册为订阅者了。

WP Mail SMTP插件
填写内容

用户头像的问题

刚开始建站的时候我是遇到了头像无法显示的问题,因为WordPress它是直接连接到一个什么网站来获取头像。所以我一开始的解决方法是使用插件Simple Local Avatars,这样用户都可以从本地上传头像图片到服务器。但是运营一年我发现,绝大部分用户都是注册完不设置自己头像的,并且如果用户愿意,他可以上传很多次头像,而这个插件把上传的头像都会显示在后台的媒体库里。如果上传很多次头像图片,会让我的媒体库很乱,并且之前的图片如果不人力删除就一直占着空间。所以我使用脚本,先做一个默认头像图片,根据颜色变化可以变出36种图片,每一个用户都会有一个默认头像图片。而每一个用户都会根据注册之后变不了的用户名计算字符串哈希值,对应唯一一个头像图片名,这样就可以解决上传很多次头像图片前面的一直占着的问题了。下面是使用插件Code Snippets做的脚本代码。

/**
 * 全体注册用户单文件头像系统。
 *
 * 功能:
 * - 适用于所有 WordPress 注册用户,不限制角色;
 * - 每个账号始终只有一个固定 PNG 头像文件;
 * - 文件名由登录用户名的 SHA-256 哈希确定;
 * - 没有上传头像时,生成 36 色之一的默认头像;
 * - 上传新头像时直接覆盖同一个文件;
 * - 恢复默认头像时重新生成彩色默认头像并覆盖;
 * - 不创建媒体附件,不进入媒体库;
 * - 删除账号时自动删除对应头像文件;
 * - 支持后台个人资料页面;
 * - 支持前台短代码:[qmc_avatar_form]
 */


/* =========================================================
 * 一、基础设置
 * =========================================================
 */

/*
 * 默认透明头像在 uploads 目录中的相对路径。
 *
 * 对应网址:
 * https://qimuchuan.life/wp-content/uploads/2026/07/默认头像.png
 */
if ( ! defined( 'QMCFA_TEMPLATE_RELATIVE_PATH' ) ) {
	define(
		'QMCFA_TEMPLATE_RELATIVE_PATH',
		'2026/07/默认头像.png'
	);
}


/*
 * 用户头像保存目录:
 *
 * wp-content/uploads/qmc-avatars/
 */
if ( ! defined( 'QMCFA_DIRECTORY_NAME' ) ) {
	define(
		'QMCFA_DIRECTORY_NAME',
		'qmc-avatars'
	);
}


/*
 * 所有头像统一处理为 256 × 256 PNG。
 */
if ( ! defined( 'QMCFA_IMAGE_SIZE' ) ) {
	define(
		'QMCFA_IMAGE_SIZE',
		256
	);
}


/*
 * 上传文件最大 2 MB。
 */
if ( ! defined( 'QMCFA_MAX_FILE_SIZE' ) ) {
	define(
		'QMCFA_MAX_FILE_SIZE',
		2 * MB_IN_BYTES
	);
}


/*
 * 上传原图最大边长。
 */
if ( ! defined( 'QMCFA_MAX_DIMENSION' ) ) {
	define(
		'QMCFA_MAX_DIMENSION',
		4096
	);
}


/*
 * 记录用户当前头像类型。
 *
 * 可选值:
 * default
 * custom
 */
if ( ! defined( 'QMCFA_MODE_META_KEY' ) ) {
	define(
		'QMCFA_MODE_META_KEY',
		'_qmc_file_avatar_mode'
	);
}


/*
 * 旧头像代码曾使用的媒体附件字段。
 *
 * 如果该字段仍存在,新系统在用户保存头像时,
 * 会尝试安全清理对应的旧媒体附件。
 */
if ( ! defined( 'QMCFA_LEGACY_META_KEY' ) ) {
	define(
		'QMCFA_LEGACY_META_KEY',
		'_qmc_avatar_attachment_id'
	);
}


/* =========================================================
 * 二、用户与路径辅助函数
 * =========================================================
 */

/**
 * 判断是否为有效的 WordPress 注册用户。
 *
 * 管理员、编辑、作者、投稿者、订阅者等
 * 所有有效注册用户都返回 true。
 */
function qmcfa_is_member( $user ) {

	if ( ! $user instanceof WP_User ) {
		return false;
	}

	return (
		$user->exists()
		&& (int) $user->ID > 0
	);
}


/**
 * 根据 WordPress 头像函数可能收到的参数解析用户。
 */
function qmcfa_resolve_user( $id_or_email ) {

	if ( $id_or_email instanceof WP_User ) {
		return $id_or_email;
	}


	if ( $id_or_email instanceof WP_Comment ) {

		if ( (int) $id_or_email->user_id > 0 ) {
			return get_user_by(
				'id',
				(int) $id_or_email->user_id
			);
		}

		if (
			is_email(
				$id_or_email->comment_author_email
			)
		) {
			return get_user_by(
				'email',
				$id_or_email->comment_author_email
			);
		}

		return false;
	}


	if ( $id_or_email instanceof WP_Post ) {
		return get_user_by(
			'id',
			(int) $id_or_email->post_author
		);
	}


	if ( is_numeric( $id_or_email ) ) {
		return get_user_by(
			'id',
			absint( $id_or_email )
		);
	}


	if (
		is_string( $id_or_email )
		&& is_email( $id_or_email )
	) {
		return get_user_by(
			'email',
			$id_or_email
		);
	}


	if ( is_object( $id_or_email ) ) {

		if ( ! empty( $id_or_email->user_id ) ) {
			return get_user_by(
				'id',
				absint( $id_or_email->user_id )
			);
		}

		if ( ! empty( $id_or_email->ID ) ) {
			return get_user_by(
				'id',
				absint( $id_or_email->ID )
			);
		}
	}

	return false;
}


/**
 * 规范化登录用户名。
 *
 * 该算法与旧版保持一致,
 * 所以现有头像文件名不会改变。
 */
function qmcfa_normalize_login( $user_login ) {

	$user_login = (string) $user_login;

	if ( function_exists( 'mb_strtolower' ) ) {
		return mb_strtolower(
			$user_login,
			'UTF-8'
		);
	}

	return strtolower( $user_login );
}


/**
 * 根据登录用户名生成固定文件索引。
 */
function qmcfa_get_login_hash( $user_login ) {

	return hash(
		'sha256',
		'qmc-single-avatar:'
			. qmcfa_normalize_login(
				$user_login
			)
	);
}


/**
 * 对网址相对路径中的每一级进行编码。
 */
function qmcfa_encode_relative_url( $relative_path ) {

	$parts = explode(
		'/',
		str_replace(
			'\\',
			'/',
			$relative_path
		)
	);

	$parts = array_map(
		'rawurlencode',
		$parts
	);

	return implode(
		'/',
		$parts
	);
}


/**
 * 获取指定用户名对应的头像路径与网址。
 */
function qmcfa_get_paths_for_login( $user_login ) {

	$uploads = wp_upload_dir(
		null,
		false
	);

	if ( ! empty( $uploads['error'] ) ) {
		return new WP_Error(
			'qmcfa_upload_directory_error',
			$uploads['error']
		);
	}


	$directory = trailingslashit(
		$uploads['basedir']
	) . QMCFA_DIRECTORY_NAME;


	if (
		! is_dir( $directory )
		&& ! wp_mkdir_p( $directory )
	) {
		return new WP_Error(
			'qmcfa_create_directory_failed',
			'无法创建用户头像目录。'
		);
	}


	/*
	 * 防止目录直接显示文件列表。
	 */
	$index_file = trailingslashit(
		$directory
	) . 'index.php';

	if ( ! file_exists( $index_file ) ) {
		@file_put_contents(
			$index_file,
			"<?php\n// Silence is golden.\n"
		);
	}


	$hash = qmcfa_get_login_hash(
		$user_login
	);

	$filename =
		'avatar-'
		. $hash
		. '.png';


	$avatar_path = trailingslashit(
		$directory
	) . $filename;


	$avatar_url = trailingslashit(
		$uploads['baseurl']
	) . QMCFA_DIRECTORY_NAME
		. '/'
		. rawurlencode( $filename );


	$template_path = path_join(
		$uploads['basedir'],
		QMCFA_TEMPLATE_RELATIVE_PATH
	);


	$template_url = trailingslashit(
		$uploads['baseurl']
	) . qmcfa_encode_relative_url(
		QMCFA_TEMPLATE_RELATIVE_PATH
	);


	return array(
		'directory'     => $directory,
		'filename'      => $filename,
		'path'          => $avatar_path,
		'url'           => $avatar_url,
		'template_path' => $template_path,
		'template_url'  => $template_url,
	);
}


/**
 * 获取指定用户对应的头像路径。
 */
function qmcfa_get_user_paths( $user ) {

	if ( ! qmcfa_is_member( $user ) ) {
		return new WP_Error(
			'qmcfa_invalid_user',
			'用户信息无效。'
		);
	}

	return qmcfa_get_paths_for_login(
		$user->user_login
	);
}


/**
 * 在头像目录中创建唯一临时文件路径。
 */
function qmcfa_create_temp_path( $directory ) {

	for ( $attempt = 0; $attempt < 10; $attempt++ ) {

		$filename =
			'.qmc-avatar-temp-'
			. wp_generate_password(
				20,
				false,
				false
			)
			. '.png';

		$path = trailingslashit(
			$directory
		) . $filename;

		if ( ! file_exists( $path ) ) {
			return $path;
		}
	}

	return false;
}


/* =========================================================
 * 三、36 种固定颜色
 * =========================================================
 */

/**
 * 根据登录用户名确定 0 至 35 的颜色编号。
 */
function qmcfa_get_color_index( $user_login ) {

	$hash = hash(
		'sha256',
		'qmc-avatar-color:'
			. qmcfa_normalize_login(
				$user_login
			)
	);

	$number = hexdec(
		substr(
			$hash,
			0,
			8
		)
	);

	return (int) ( $number % 36 );
}


/**
 * HSL 转 RGB 的辅助函数。
 */
function qmcfa_hue_component(
	$p,
	$q,
	$t
) {
	if ( $t < 0 ) {
		$t += 1;
	}

	if ( $t > 1 ) {
		$t -= 1;
	}

	if ( $t < 1 / 6 ) {
		return $p
			+ (
				$q - $p
			) * 6 * $t;
	}

	if ( $t < 1 / 2 ) {
		return $q;
	}

	if ( $t < 2 / 3 ) {
		return $p
			+ (
				$q - $p
			) * (
				2 / 3 - $t
			) * 6;
	}

	return $p;
}


/**
 * 把 HSL 颜色转换为 RGB。
 */
function qmcfa_hsl_to_rgb(
	$hue,
	$saturation,
	$lightness
) {
	$h = (
		(float) $hue % 360
	) / 360;

	$s = max(
		0,
		min(
			1,
			(float) $saturation
		)
	);

	$l = max(
		0,
		min(
			1,
			(float) $lightness
		)
	);


	if ( 0.0 === $s ) {
		$value = (int) round(
			$l * 255
		);

		return array(
			$value,
			$value,
			$value,
		);
	}


	$q = $l < 0.5
		? $l * ( 1 + $s )
		: $l + $s - $l * $s;

	$p = 2 * $l - $q;


	$r = qmcfa_hue_component(
		$p,
		$q,
		$h + 1 / 3
	);

	$g = qmcfa_hue_component(
		$p,
		$q,
		$h
	);

	$b = qmcfa_hue_component(
		$p,
		$q,
		$h - 1 / 3
	);


	return array(
		(int) round( $r * 255 ),
		(int) round( $g * 255 ),
		(int) round( $b * 255 ),
	);
}


/**
 * 获取用户固定的 RGB 颜色。
 *
 * 共 36 种色相,每种相隔 10 度。
 */
function qmcfa_get_user_rgb( $user_login ) {

	$index = qmcfa_get_color_index(
		$user_login
	);

	$hue = $index * 10;

	return qmcfa_hsl_to_rgb(
		$hue,
		0.82,
		0.45
	);
}


/* =========================================================
 * 四、原子覆盖头像文件
 * =========================================================
 */

/**
 * 使用准备好的临时文件安全替换正式头像。
 *
 * 新文件处理成功后才会替换旧文件。
 */
function qmcfa_atomic_replace(
	$prepared_path,
	$destination_path
) {
	if (
		! is_string( $prepared_path )
		|| ! file_exists( $prepared_path )
	) {
		return new WP_Error(
			'qmcfa_missing_prepared_file',
			'处理后的头像文件不存在。'
		);
	}


	$backup_path = '';

	if ( file_exists( $destination_path ) ) {

		$backup_path =
			$destination_path
			. '.backup-'
			. wp_generate_password(
				12,
				false,
				false
			);

		if (
			! @rename(
				$destination_path,
				$backup_path
			)
		) {
			return new WP_Error(
				'qmcfa_backup_failed',
				'无法备份原头像文件。'
			);
		}
	}


	if (
		! @rename(
			$prepared_path,
			$destination_path
		)
	) {
		if (
			$backup_path
			&& file_exists( $backup_path )
		) {
			@rename(
				$backup_path,
				$destination_path
			);
		}

		return new WP_Error(
			'qmcfa_replace_failed',
			'无法保存新的头像文件。'
		);
	}


	if (
		$backup_path
		&& file_exists( $backup_path )
	) {
		wp_delete_file(
			$backup_path
		);
	}


	$permissions = defined(
		'FS_CHMOD_FILE'
	)
		? FS_CHMOD_FILE
		: 0644;

	@chmod(
		$destination_path,
		$permissions
	);

	clearstatcache(
		true,
		$destination_path
	);

	return true;
}


/* =========================================================
 * 五、生成彩色默认头像
 * =========================================================
 */

/**
 * 使用 GD 生成彩色默认头像。
 */
function qmcfa_generate_default_with_gd(
	$template_path,
	$output_path,
	$rgb
) {
	if (
		! function_exists( 'imagecreatefrompng' )
		|| ! function_exists( 'imagepng' )
	) {
		return new WP_Error(
			'qmcfa_gd_unavailable',
			'服务器没有启用 GD 图片扩展。'
		);
	}


	$source = @imagecreatefrompng(
		$template_path
	);

	if ( ! $source ) {
		return new WP_Error(
			'qmcfa_template_load_failed',
			'无法读取默认透明头像。'
		);
	}


	$size = QMCFA_IMAGE_SIZE;

	$destination = imagecreatetruecolor(
		$size,
		$size
	);

	if ( ! $destination ) {
		imagedestroy( $source );

		return new WP_Error(
			'qmcfa_canvas_failed',
			'无法创建头像画布。'
		);
	}


	imagealphablending(
		$destination,
		false
	);

	imagesavealpha(
		$destination,
		true
	);


	$transparent = imagecolorallocatealpha(
		$destination,
		0,
		0,
		0,
		127
	);

	imagefill(
		$destination,
		0,
		0,
		$transparent
	);


	$source_width = imagesx(
		$source
	);

	$source_height = imagesy(
		$source
	);


	$copied = imagecopyresampled(
		$destination,
		$source,
		0,
		0,
		0,
		0,
		$size,
		$size,
		$source_width,
		$source_height
	);

	imagedestroy( $source );


	if ( ! $copied ) {
		imagedestroy( $destination );

		return new WP_Error(
			'qmcfa_resize_failed',
			'默认头像缩放失败。'
		);
	}


	$red   = (int) $rgb[0];
	$green = (int) $rgb[1];
	$blue  = (int) $rgb[2];

	/*
	 * 按透明度缓存颜色,
	 * 从而保留图案的抗锯齿边缘。
	 */
	$color_cache = array();


	for ( $y = 0; $y < $size; $y++ ) {

		for ( $x = 0; $x < $size; $x++ ) {

			$pixel = imagecolorat(
				$destination,
				$x,
				$y
			);

			$alpha = (
				$pixel >> 24
			) & 0x7F;


			if ( $alpha >= 127 ) {
				continue;
			}


			if (
				! isset(
					$color_cache[ $alpha ]
				)
			) {
				$color_cache[ $alpha ] =
					imagecolorallocatealpha(
						$destination,
						$red,
						$green,
						$blue,
						$alpha
					);
			}


			imagesetpixel(
				$destination,
				$x,
				$y,
				$color_cache[ $alpha ]
			);
		}
	}


	$saved = imagepng(
		$destination,
		$output_path,
		9
	);

	imagedestroy( $destination );


	if ( ! $saved ) {
		return new WP_Error(
			'qmcfa_png_save_failed',
			'彩色默认头像保存失败。'
		);
	}

	return true;
}


/**
 * 使用 Imagick 生成彩色默认头像。
 *
 * 仅在 GD 不可用时使用。
 */
function qmcfa_generate_default_with_imagick(
	$template_path,
	$output_path,
	$rgb
) {
	if ( ! class_exists( 'Imagick' ) ) {
		return new WP_Error(
			'qmcfa_imagick_unavailable',
			'服务器没有可用的图片处理扩展。'
		);
	}


	try {
		$image = new Imagick(
			$template_path
		);

		$image->setImageFormat(
			'png'
		);

		$image->setImageAlphaChannel(
			Imagick::ALPHACHANNEL_ACTIVATE
		);

		$image->cropThumbnailImage(
			QMCFA_IMAGE_SIZE,
			QMCFA_IMAGE_SIZE
		);


		$iterator = $image->getPixelIterator();

		foreach ( $iterator as $row ) {

			foreach ( $row as $pixel ) {

				$alpha = $pixel->getColorValue(
					Imagick::COLOR_ALPHA
				);

				if ( $alpha <= 0.001 ) {
					continue;
				}


				$pixel->setColor(
					sprintf(
						'rgb(%d,%d,%d)',
						(int) $rgb[0],
						(int) $rgb[1],
						(int) $rgb[2]
					)
				);

				$pixel->setColorValue(
					Imagick::COLOR_ALPHA,
					$alpha
				);
			}

			$iterator->syncIterator();
		}


		$image->setImageFormat(
			'png'
		);

		$saved = $image->writeImage(
			$output_path
		);

		$image->clear();
		$image->destroy();


		if ( ! $saved ) {
			return new WP_Error(
				'qmcfa_imagick_save_failed',
				'彩色默认头像保存失败。'
			);
		}

		return true;

	} catch ( Exception $exception ) {

		return new WP_Error(
			'qmcfa_imagick_exception',
			$exception->getMessage()
		);
	}
}


/**
 * 为指定注册用户生成彩色默认头像。
 */
function qmcfa_generate_default_avatar( $user_id ) {

	$user = get_userdata(
		absint( $user_id )
	);

	if ( ! qmcfa_is_member( $user ) ) {
		return new WP_Error(
			'qmcfa_invalid_member',
			'当前用户不是有效的注册用户。'
		);
	}


	$paths = qmcfa_get_user_paths(
		$user
	);

	if ( is_wp_error( $paths ) ) {
		return $paths;
	}


	if (
		! file_exists(
			$paths['template_path']
		)
	) {
		return new WP_Error(
			'qmcfa_missing_template',
			'没有找到默认头像 PNG:'
				. $paths['template_path']
		);
	}


	$temp_path = qmcfa_create_temp_path(
		$paths['directory']
	);

	if ( ! $temp_path ) {
		return new WP_Error(
			'qmcfa_temp_path_failed',
			'无法创建临时头像文件。'
		);
	}


	$rgb = qmcfa_get_user_rgb(
		$user->user_login
	);


	if (
		function_exists( 'imagecreatefrompng' )
		&& function_exists( 'imagepng' )
	) {
		$result =
			qmcfa_generate_default_with_gd(
				$paths['template_path'],
				$temp_path,
				$rgb
			);
	} else {
		$result =
			qmcfa_generate_default_with_imagick(
				$paths['template_path'],
				$temp_path,
				$rgb
			);
	}


	if ( is_wp_error( $result ) ) {

		if ( file_exists( $temp_path ) ) {
			wp_delete_file(
				$temp_path
			);
		}

		return $result;
	}


	$replaced = qmcfa_atomic_replace(
		$temp_path,
		$paths['path']
	);

	if ( is_wp_error( $replaced ) ) {

		if ( file_exists( $temp_path ) ) {
			wp_delete_file(
				$temp_path
			);
		}

		return $replaced;
	}


	update_user_meta(
		$user->ID,
		QMCFA_MODE_META_KEY,
		'default'
	);

	return true;
}


/* =========================================================
 * 六、处理用户上传头像
 * =========================================================
 */

/**
 * 判断表单是否实际上传了文件。
 */
function qmcfa_has_uploaded_file( $field_name ) {

	if (
		empty( $_FILES[ $field_name ] )
		|| empty(
			$_FILES[ $field_name ]['name']
		)
	) {
		return false;
	}

	if (
		isset(
			$_FILES[ $field_name ]['error']
		)
		&& UPLOAD_ERR_NO_FILE
			=== (int)
				$_FILES[ $field_name ]['error']
	) {
		return false;
	}

	return true;
}


/**
 * 清理旧头像代码产生的媒体附件。
 *
 * 只有能确认附件属于该用户时才删除。
 */
function qmcfa_cleanup_legacy_attachment(
	$user_id
) {
	$user_id = absint( $user_id );

	$attachment_id = absint(
		get_user_meta(
			$user_id,
			QMCFA_LEGACY_META_KEY,
			true
		)
	);

	if ( ! $attachment_id ) {
		return;
	}


	$can_delete = false;

	if (
		'attachment'
		=== get_post_type(
			$attachment_id
		)
		&& wp_attachment_is_image(
			$attachment_id
		)
	) {
		$post_author = absint(
			get_post_field(
				'post_author',
				$attachment_id
			)
		);

		$owner_meta = absint(
			get_post_meta(
				$attachment_id,
				'_qmc_avatar_owner_user_id',
				true
			)
		);

		if (
			$post_author === $user_id
			|| $owner_meta === $user_id
		) {
			$can_delete = true;
		}
	}


	if ( $can_delete ) {
		wp_delete_attachment(
			$attachment_id,
			true
		);
	}


	delete_user_meta(
		$user_id,
		QMCFA_LEGACY_META_KEY
	);
}


/**
 * 验证、裁剪并保存用户上传的头像。
 */
function qmcfa_save_uploaded_avatar(
	$user_id,
	$field_name
) {
	$user = get_userdata(
		absint( $user_id )
	);

	if ( ! qmcfa_is_member( $user ) ) {
		return new WP_Error(
			'qmcfa_invalid_user_upload',
			'当前账号不能上传头像。'
		);
	}


	if (
		! qmcfa_has_uploaded_file(
			$field_name
		)
	) {
		return new WP_Error(
			'qmcfa_no_file',
			'没有选择头像文件。'
		);
	}


	$file = $_FILES[ $field_name ];


	if (
		isset( $file['error'] )
		&& UPLOAD_ERR_OK
			!== (int) $file['error']
	) {
		return new WP_Error(
			'qmcfa_upload_error',
			'头像上传失败,请重新选择文件。'
		);
	}


	if (
		empty( $file['size'] )
		|| (int) $file['size']
			> QMCFA_MAX_FILE_SIZE
	) {
		return new WP_Error(
			'qmcfa_file_too_large',
			'头像文件不能超过 2 MB。'
		);
	}


	if (
		empty( $file['tmp_name'] )
		|| ! is_uploaded_file(
			$file['tmp_name']
		)
	) {
		return new WP_Error(
			'qmcfa_invalid_upload',
			'没有检测到有效的上传文件。'
		);
	}


	$allowed_mimes = array(
		'jpg|jpeg|jpe' => 'image/jpeg',
		'png'          => 'image/png',
		'webp'         => 'image/webp',
	);


	$file_check = wp_check_filetype_and_ext(
		$file['tmp_name'],
		$file['name'],
		$allowed_mimes
	);


	$allowed_types = array(
		'image/jpeg',
		'image/png',
		'image/webp',
	);


	if (
		empty( $file_check['type'] )
		|| ! in_array(
			$file_check['type'],
			$allowed_types,
			true
		)
	) {
		return new WP_Error(
			'qmcfa_invalid_file_type',
			'只允许上传 JPG、PNG 或 WebP 图片。'
		);
	}


	$image_info = function_exists(
		'wp_getimagesize'
	)
		? wp_getimagesize(
			$file['tmp_name']
		)
		: @getimagesize(
			$file['tmp_name']
		);


	if ( ! is_array( $image_info ) ) {
		return new WP_Error(
			'qmcfa_not_an_image',
			'上传的文件不是有效图片。'
		);
	}


	$width = isset( $image_info[0] )
		? absint( $image_info[0] )
		: 0;

	$height = isset( $image_info[1] )
		? absint( $image_info[1] )
		: 0;


	if (
		! $width
		|| ! $height
		|| $width > QMCFA_MAX_DIMENSION
		|| $height > QMCFA_MAX_DIMENSION
	) {
		return new WP_Error(
			'qmcfa_invalid_dimensions',
			'图片宽度和高度必须小于 4096 像素。'
		);
	}


	$paths = qmcfa_get_user_paths(
		$user
	);

	if ( is_wp_error( $paths ) ) {
		return $paths;
	}


	$temp_path = qmcfa_create_temp_path(
		$paths['directory']
	);

	if ( ! $temp_path ) {
		return new WP_Error(
			'qmcfa_temp_file_failed',
			'无法创建临时头像文件。'
		);
	}


	$editor = wp_get_image_editor(
		$file['tmp_name']
	);

	if ( is_wp_error( $editor ) ) {
		return $editor;
	}


	/*
	 * 居中裁剪成统一正方形。
	 */
	$resized = $editor->resize(
		QMCFA_IMAGE_SIZE,
		QMCFA_IMAGE_SIZE,
		true
	);

	if ( is_wp_error( $resized ) ) {
		return $resized;
	}


	/*
	 * 最终统一保存成 PNG。
	 */
	$saved = $editor->save(
		$temp_path,
		'image/png'
	);


	if ( is_wp_error( $saved ) ) {

		if ( file_exists( $temp_path ) ) {
			wp_delete_file(
				$temp_path
			);
		}

		return $saved;
	}


	$prepared_path = isset(
		$saved['path']
	)
		? $saved['path']
		: $temp_path;


	if ( ! file_exists( $prepared_path ) ) {
		return new WP_Error(
			'qmcfa_missing_saved_image',
			'处理后的头像文件不存在。'
		);
	}


	$replaced = qmcfa_atomic_replace(
		$prepared_path,
		$paths['path']
	);


	if ( is_wp_error( $replaced ) ) {

		if ( file_exists( $prepared_path ) ) {
			wp_delete_file(
				$prepared_path
			);
		}

		return $replaced;
	}


	if (
		$temp_path !== $prepared_path
		&& file_exists( $temp_path )
	) {
		wp_delete_file(
			$temp_path
		);
	}


	update_user_meta(
		$user->ID,
		QMCFA_MODE_META_KEY,
		'custom'
	);


	qmcfa_cleanup_legacy_attachment(
		$user->ID
	);


	return true;
}


/**
 * 处理上传、恢复默认或不修改。
 */
function qmcfa_process_avatar_change(
	$user_id,
	$field_name,
	$restore_default
) {
	if (
		qmcfa_has_uploaded_file(
			$field_name
		)
	) {
		$result =
			qmcfa_save_uploaded_avatar(
				$user_id,
				$field_name
			);

		return is_wp_error( $result )
			? $result
			: 'updated';
	}


	if ( $restore_default ) {

		$result =
			qmcfa_generate_default_avatar(
				$user_id
			);

		if ( is_wp_error( $result ) ) {
			return $result;
		}


		qmcfa_cleanup_legacy_attachment(
			$user_id
		);

		return 'restored';
	}


	return 'unchanged';
}


/* =========================================================
 * 七、确保所有注册用户拥有头像文件
 * =========================================================
 */

/**
 * 如果头像文件不存在,自动生成彩色默认头像。
 */
function qmcfa_ensure_avatar_file( $user ) {

	if ( ! qmcfa_is_member( $user ) ) {
		return new WP_Error(
			'qmcfa_invalid_avatar_user',
			'头像用户无效。'
		);
	}


	$paths = qmcfa_get_user_paths(
		$user
	);

	if ( is_wp_error( $paths ) ) {
		return $paths;
	}


	if ( file_exists( $paths['path'] ) ) {
		return $paths;
	}


	$generated =
		qmcfa_generate_default_avatar(
			$user->ID
		);

	if ( is_wp_error( $generated ) ) {
		return $generated;
	}


	return qmcfa_get_user_paths(
		$user
	);
}


/* =========================================================
 * 八、接管 WordPress 头像地址
 * =========================================================
 */

add_filter(
	'pre_get_avatar_data',
	'qmcfa_replace_avatar_data',
	20,
	2
);

function qmcfa_replace_avatar_data(
	$args,
	$id_or_email
) {
	$user = qmcfa_resolve_user(
		$id_or_email
	);


	/*
	 * 只处理能够对应到注册账号的用户。
	 *
	 * 未登录访客评论仍保留 WordPress 原有头像逻辑。
	 */
	if ( ! qmcfa_is_member( $user ) ) {
		return $args;
	}


	$paths = qmcfa_ensure_avatar_file(
		$user
	);


	if ( is_wp_error( $paths ) ) {

		$fallback_paths =
			qmcfa_get_user_paths(
				$user
			);

		if (
			! is_wp_error(
				$fallback_paths
			)
		) {
			$args['url'] =
				$fallback_paths[
					'template_url'
				];

			$args['found_avatar'] = true;
		}

		return $args;
	}


	$version = file_exists(
		$paths['path']
	)
		? (int) filemtime(
			$paths['path']
		)
		: time();


	/*
	 * 文件地址不变,但 v 参数会随修改时间变化,
	 * 避免浏览器或 CDN 一直显示旧头像。
	 */
	$args['url'] = add_query_arg(
		'v',
		$version,
		set_url_scheme(
			$paths['url'],
			'https'
		)
	);

	$args['found_avatar'] = true;

	return $args;
}


/* =========================================================
 * 九、注册与角色变化时生成头像
 * =========================================================
 */

/**
 * 新账号注册后生成默认头像。
 */
add_action(
	'user_register',
	'qmcfa_generate_avatar_on_registration',
	20,
	1
);

function qmcfa_generate_avatar_on_registration(
	$user_id
) {
	$user = get_userdata(
		absint( $user_id )
	);

	if ( qmcfa_is_member( $user ) ) {
		qmcfa_generate_default_avatar(
			$user->ID
		);
	}
}


/**
 * 用户角色发生变化时检查头像文件。
 *
 * 不限制新角色是什么。
 */
add_action(
	'set_user_role',
	'qmcfa_generate_avatar_after_role_change',
	20,
	3
);

function qmcfa_generate_avatar_after_role_change(
	$user_id,
	$role,
	$old_roles
) {
	$user = get_userdata(
		absint( $user_id )
	);

	if ( ! qmcfa_is_member( $user ) ) {
		return;
	}


	$paths = qmcfa_get_user_paths(
		$user
	);

	if (
		! is_wp_error( $paths )
		&& ! file_exists(
			$paths['path']
		)
	) {
		qmcfa_generate_default_avatar(
			$user->ID
		);
	}
}


/* =========================================================
 * 十、后台个人资料头像表单
 * =========================================================
 */

/**
 * 允许个人资料表单上传文件。
 */
add_action(
	'user_edit_form_tag',
	'qmcfa_add_profile_form_enctype'
);

function qmcfa_add_profile_form_enctype() {

	echo ' enctype="multipart/form-data"';
}


add_action(
	'show_user_profile',
	'qmcfa_show_profile_fields',
	20
);

add_action(
	'edit_user_profile',
	'qmcfa_show_profile_fields',
	20
);

function qmcfa_show_profile_fields( $user ) {

	if ( ! qmcfa_is_member( $user ) ) {
		return;
	}


	$mode = get_user_meta(
		$user->ID,
		QMCFA_MODE_META_KEY,
		true
	);

	?>
	<h2>账号头像</h2>

	<table
		class="form-table"
		role="presentation"
	>

		<tr>
			<th scope="row">
				当前头像
			</th>

			<td>
				<div style="
					display:flex;
					align-items:center;
					gap:16px;
				">
					<?php
					echo get_avatar(
						$user->ID,
						96,
						'',
						$user->display_name
					);
					?>

					<p class="description">
						<?php
						if ( 'custom' === $mode ) {
							echo '当前正在使用你上传的头像。';
						} else {
							echo '当前正在使用系统分配的 36 色默认头像。';
						}
						?>
					</p>
				</div>
			</td>
		</tr>


		<tr>
			<th scope="row">
				<label for="qmcfa_avatar_file">
					上传新头像
				</label>
			</th>

			<td>
				<input
					type="file"
					id="qmcfa_avatar_file"
					name="qmcfa_avatar_file"
					accept="image/jpeg,image/png,image/webp"
				>

				<p class="description">
					支持 JPG、PNG 和 WebP,
					最大 2 MB。
					图片会居中裁剪成正方形。
					保存后直接覆盖旧头像,
					不会进入媒体库。
				</p>
			</td>
		</tr>


		<?php if ( 'custom' === $mode ) : ?>
			<tr>
				<th scope="row">
					恢复默认头像
				</th>

				<td>
					<label>
						<input
							type="checkbox"
							name="qmcfa_restore_default"
							value="1"
						>

						恢复系统分配的彩色默认头像
					</label>
				</td>
			</tr>
		<?php endif; ?>

	</table>

	<?php
	wp_nonce_field(
		'qmcfa_save_profile_'
			. $user->ID,
		'qmcfa_profile_nonce'
	);
}


/**
 * 保存后台个人资料中的头像。
 */
add_action(
	'personal_options_update',
	'qmcfa_save_profile_avatar'
);

add_action(
	'edit_user_profile_update',
	'qmcfa_save_profile_avatar'
);

function qmcfa_save_profile_avatar(
	$user_id
) {
	$user_id = absint( $user_id );


	if (
		! $user_id
		|| ! current_user_can(
			'edit_user',
			$user_id
		)
	) {
		return;
	}


	$user = get_userdata(
		$user_id
	);

	if ( ! qmcfa_is_member( $user ) ) {
		return;
	}


	if (
		empty(
			$_POST['qmcfa_profile_nonce']
		)
		|| ! wp_verify_nonce(
			sanitize_text_field(
				wp_unslash(
					$_POST[
						'qmcfa_profile_nonce'
					]
				)
			),
			'qmcfa_save_profile_'
				. $user_id
		)
	) {
		return;
	}


	$restore_default = ! empty(
		$_POST['qmcfa_restore_default']
	);


	$result = qmcfa_process_avatar_change(
		$user_id,
		'qmcfa_avatar_file',
		$restore_default
	);


	if ( is_wp_error( $result ) ) {
		wp_die(
			esc_html(
				$result->get_error_message()
			),
			'头像保存失败',
			array(
				'response'  => 400,
				'back_link' => true,
			)
		);
	}
}


/* =========================================================
 * 十一、前台头像设置短代码
 * =========================================================
 */

/*
 * 使用方法:
 *
 * 在 WordPress 页面中加入:
 *
 * [qmc_avatar_form]
 */
add_shortcode(
	'qmc_avatar_form',
	'qmcfa_frontend_form_shortcode'
);

function qmcfa_frontend_form_shortcode() {

	if ( ! is_user_logged_in() ) {
		return '<p>请先登录后设置头像。</p>';
	}


	$user = wp_get_current_user();

	if ( ! qmcfa_is_member( $user ) ) {
		return '<p>当前账号不能使用此头像设置。</p>';
	}


	$mode = get_user_meta(
		$user->ID,
		QMCFA_MODE_META_KEY,
		true
	);


	$status = isset(
		$_GET['qmcfa_avatar_status']
	)
		? sanitize_key(
			wp_unslash(
				$_GET['qmcfa_avatar_status']
			)
		)
		: '';


	$messages = array(
		'updated' =>
			'头像已经更新。',

		'restored' =>
			'已恢复系统分配的彩色默认头像。',

		'unchanged' =>
			'头像没有发生变化。',

		'error' =>
			'头像处理失败,请重新尝试。',
	);


	$error_message = '';

	if ( 'error' === $status ) {

		$error_message = get_transient(
			'qmcfa_avatar_error_'
				. $user->ID
		);

		delete_transient(
			'qmcfa_avatar_error_'
				. $user->ID
		);
	}


	ob_start();


	if ( isset( $messages[ $status ] ) ) {
		?>
		<div
			class="qmcfa-avatar-message<?php
				echo 'error' === $status
					? ' is-error'
					: '';
			?>"
		>
			<?php
			echo esc_html(
				$error_message
					? $error_message
					: $messages[ $status ]
			);
			?>
		</div>
		<?php
	}

	?>
	<div class="qmcfa-avatar-form-wrap">

		<div class="qmcfa-avatar-current">

			<?php
			echo get_avatar(
				$user->ID,
				120,
				'',
				$user->display_name
			);
			?>

			<div>
				<strong>当前头像</strong>

				<p>
					<?php
					if ( 'custom' === $mode ) {
						echo '正在使用你上传的头像。';
					} else {
						echo '正在使用系统分配的 36 色默认头像。';
					}
					?>
				</p>
			</div>

		</div>


		<form
			method="post"
			enctype="multipart/form-data"
			action="<?php
				echo esc_url(
					admin_url(
						'admin-post.php'
					)
				);
			?>"
		>
			<input
				type="hidden"
				name="action"
				value="qmcfa_save_front_avatar"
			>

			<?php
			wp_nonce_field(
				'qmcfa_save_front_avatar_'
					. $user->ID,
				'qmcfa_front_nonce'
			);
			?>


			<p>
				<label for="qmcfa_front_file">
					<strong>上传新头像</strong>
				</label>
			</p>

			<p>
				<input
					type="file"
					id="qmcfa_front_file"
					name="qmcfa_front_file"
					accept="image/jpeg,image/png,image/webp"
				>
			</p>

			<p class="qmcfa-avatar-description">
				支持 JPG、PNG 和 WebP,
				最大 2 MB。
				图片会被居中裁剪成正方形,
				并直接覆盖当前头像文件。
				不会进入媒体库。
			</p>


			<?php if ( 'custom' === $mode ) : ?>
				<p>
					<label>
						<input
							type="checkbox"
							name="qmcfa_restore_default"
							value="1"
						>

						恢复系统分配的彩色默认头像
					</label>
				</p>
			<?php endif; ?>


			<p>
				<button type="submit">
					保存头像
				</button>
			</p>

		</form>

	</div>


	<style>
		.qmcfa-avatar-form-wrap {
			max-width: 620px;
		}

		.qmcfa-avatar-current {
			display: flex;
			align-items: center;
			gap: 18px;
			margin-bottom: 24px;
		}

		.qmcfa-avatar-current img {
			object-fit: cover;
		}

		.qmcfa-avatar-current p {
			margin: 6px 0 0;
		}

		.qmcfa-avatar-description {
			color: #666;
			font-size: 14px;
			line-height: 1.7;
		}

		.qmcfa-avatar-message {
			margin: 0 0 20px;
			padding: 11px 14px;

			color: #085d3a;
			background: #ecfdf3;

			border: 1px solid #abefc6;
			border-radius: 4px;
		}

		.qmcfa-avatar-message.is-error {
			color: #912018;
			background: #fee4e2;
			border-color: #fecdca;
		}
	</style>
	<?php

	return ob_get_clean();
}


/**
 * 处理前台头像保存。
 */
add_action(
	'admin_post_qmcfa_save_front_avatar',
	'qmcfa_handle_frontend_save'
);

function qmcfa_handle_frontend_save() {

	if ( ! is_user_logged_in() ) {
		wp_die(
			'请先登录。',
			'无法保存头像',
			array(
				'response' => 403,
			)
		);
	}


	$user = wp_get_current_user();

	if ( ! qmcfa_is_member( $user ) ) {
		wp_die(
			'当前账号不能执行此操作。',
			'无法保存头像',
			array(
				'response' => 403,
			)
		);
	}


	check_admin_referer(
		'qmcfa_save_front_avatar_'
			. $user->ID,
		'qmcfa_front_nonce'
	);


	$restore_default = ! empty(
		$_POST['qmcfa_restore_default']
	);


	$result = qmcfa_process_avatar_change(
		$user->ID,
		'qmcfa_front_file',
		$restore_default
	);


	if ( is_wp_error( $result ) ) {

		set_transient(
			'qmcfa_avatar_error_'
				. $user->ID,
			$result->get_error_message(),
			MINUTE_IN_SECONDS
		);

		$status = 'error';

	} else {
		$status = $result;
	}


	$redirect_url = wp_get_referer();

	if ( ! $redirect_url ) {
		$redirect_url = home_url( '/' );
	}


	$redirect_url = remove_query_arg(
		'qmcfa_avatar_status',
		$redirect_url
	);

	$redirect_url = add_query_arg(
		'qmcfa_avatar_status',
		$status,
		$redirect_url
	);


	wp_safe_redirect(
		$redirect_url
	);

	exit;
}


/* =========================================================
 * 十二、删除账号时清理头像文件
 * =========================================================
 */

/**
 * 根据登录用户名删除头像文件。
 */
function qmcfa_delete_avatar_for_login(
	$user_login
) {
	$paths = qmcfa_get_paths_for_login(
		$user_login
	);

	if ( is_wp_error( $paths ) ) {
		return;
	}


	if ( file_exists( $paths['path'] ) ) {
		wp_delete_file(
			$paths['path']
		);
	}


	/*
	 * 清理极少数意外遗留的备份文件。
	 */
	$backup_files = glob(
		$paths['path']
			. '.backup-*'
	);

	if ( is_array( $backup_files ) ) {

		foreach ( $backup_files as $backup_file ) {

			if ( is_file( $backup_file ) ) {
				wp_delete_file(
					$backup_file
				);
			}
		}
	}
}


/**
 * 普通 WordPress 删除用户。
 */
add_action(
	'delete_user',
	'qmcfa_cleanup_deleted_user',
	10,
	3
);

function qmcfa_cleanup_deleted_user(
	$user_id,
	$reassign,
	$user
) {
	if ( ! $user instanceof WP_User ) {
		$user = get_userdata(
			absint( $user_id )
		);
	}

	if ( ! $user ) {
		return;
	}


	qmcfa_delete_avatar_for_login(
		$user->user_login
	);

	qmcfa_cleanup_legacy_attachment(
		$user->ID
	);
}


/**
 * WordPress 多站点删除用户。
 */
add_action(
	'wpmu_delete_user',
	'qmcfa_cleanup_multisite_user',
	10,
	1
);

function qmcfa_cleanup_multisite_user(
	$user_id
) {
	$user = get_userdata(
		absint( $user_id )
	);

	if ( ! $user ) {
		return;
	}


	qmcfa_delete_avatar_for_login(
		$user->user_login
	);

	qmcfa_cleanup_legacy_attachment(
		$user->ID
	);
}

其他的一些小问题

上面三个问题都是跟博客网站运行直接重要的,还有一些不那么重要的问题。比如分类的顺序无法控制。这里我都是使用插件来解决的。对这个问题,我使用的插件分别是Category Order and Taxonomy Terms Order。多问AI大模型,这样的小问题一般都能用插件解决。这也是WordPress的优势之一,生态丰富,经过多年发展很多问题都有解决方案了。

博客文章

文章渲染LaTex公式失败怎么办

作为理工科学生,如果不能使用LaTex写文章那真是左右为难啊。WordPress并不支持书写公式,网络上有说使用插件来写LaTex的,有一些是渲染成图片然后嵌入文章里,那样做可能会导致后期文章多起来图片多会加载巨慢,所以这种做法被我Pass;还有就是直接渲染的,使用MathJax-LaTex插件渲染公式,但是安装之后我发现并没有渲染,后面参考了博客园里的一篇博客wordpress 显示数学公式(MathJax-LaTeX),发现这个插件是要连接一个服务器,把LaTex代码发送给服务器渲染后再发送回来,而在国内这个服务器连接是不好的,所以还是无法渲染。解决办法是安装本地的渲染器,

具体步骤如下:

我们首先需要知道WordPress安装在哪里。wp-config.php是WordPress的核心配置文件,找到它就等于找到了网站的根目录。在命令行中执行以下命令:

sudo find / -name "wp-config.php"

你将会得到类似下面这样的输出:

/data/wwwroot/wordpress/wp-config.php

这表明,您的WordPress网站根目录是 /data/wwwroot/wordpress。请记下这个路径,我们后续将用 /path/to/your/wordpress 来指代它。接下来,我们直接在服务器上完成MathJax的下载和部署。进入wp-content目录,这是存放所有自定义内容的标准目录。请将下面的路径替换为您在上一步找到的真实路径。

cd /path/to/your/wordpress/wp-content

下载MathJax库,我们使用wget命令下载一个稳定且兼容性极佳的2.7.9版本。

sudo wget https://github.com/mathjax/MathJax/archive/2.7.9.zip

如果提示unzip命令不存在,请先安装它(CentOS/RHEL: sudo yum install unzip;Debian/Ubuntu: sudo apt-get install unzip)。

重命名与清理,为了方便后续引用,我们将解压后的文件夹MathJax-2.7.9重命名为MathJax,并删除不再需要的压缩包。

sudo mv MathJax-2.7.9 MathJax
sudo rm 2.7.9.zip

至此,MathJax库已成功部署在/path/to/your/wordpress/wp-content/MathJax/

接下来设置正确的文件权限,这是确保Web服务器能成功读取MathJax文件的关键一步。查找Web服务器运行的用户,执行以下命令,查看您的Web服务器(如Nginx或Apache)是以哪个用户身份运行的。下面命令中的-R参数会递归地将权限应用到MathJax文件夹内的所有文件和子文件夹。

sudo chown -R www:www /path/to/your/wordpress/wp-content/MathJax

服务器端的工作已经完成。加下来直接将下面的代码添加到主题的functions.php文件中。该文件可以在WordPress后台外观-主题编辑文件器-主题文件找到。

function load_local_mathjax() {
    // 仅在单篇文章或页面中加载脚本,优化性能
    if ( is_single() || is_page() ) {
        // 使用WordPress函数自动生成本地MathJax文件的正确URL
        $mathjax_url = content_url( '/MathJax/MathJax.js?config=TeX-AMS-MML_HTMLorMML' );

        // 输出用于加载MathJax的<script>标签
        echo '<script type="text/javascript" async src="' . esc_url( $mathjax_url ) . '"></script>';

        // 输出MathJax的详细配置,这是渲染成功的核心
        echo <<<EOT
<script type="text/x-mathjax-config">
  MathJax.Hub.Config({
    // 配置公式的定界符,如 $...$ 和 $$...$$
    tex2jax: {
      inlineMath: [ ['\$','\$'], ['\\(','\\)'] ],
      displayMath: [ ['\$\$','\$\$'], ['\\[','\\]'] ],
      processEscapes: true
    },
    // 强制加载AMS扩展,以支持矩阵、分段函数等高级环境
    TeX: {
      extensions: ["AMSmath.js", "AMSsymbols.js", "noErrors.js", "noUndefined.js"]
    }
  });
</script>
EOT;
    }
}

// 将我们的函数挂载到WordPress的头部加载动作上
add_action('wp_head', 'load_local_mathjax');

接下来你就能尝试写LaTex代码看看是否渲染成功了。

我测试了很多LaTex公式,发现其他都渲染好了,但是对于那种要跨行的LaTex公式(比如分段函数,矩阵)会失败,只能显示为一行。其原因是多行公式渲染失败的具体原因是 WordPress 的富文本编辑器(如古腾堡)会自动添加<p>标签或转义换行符,导致 MathJax 无法正确解析多行 LaTex 语法。我没有找到能够直接解决的方法,最后的方案是把这一段的代码插入到WordPress的自定义HTML的块中,这个可以让AI帮你写,这样做就可以成功渲染出多行的数学公式了。

普通的一行公式可以正常渲染
矩阵、分段函数这种要多行才能展示的公式只能渲染成一行。

这里给两个例子。

第一个是矩阵,HTML源码如下:

$$
A = \begin{pmatrix}
1 & 2 & 3 \\
4 & 5 & 6 \\
7 & 8 & 9
\end{pmatrix}
$$

效果如下:

$$ A = \begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{pmatrix} $$

接下来是分段函数的代码:

$$
|x| = \begin{cases}
x, & \text{if } x \ge 0 \
-x, & \text{if } x < 0
\end{cases}
$$

效果如下:

$$ |x| = \begin{cases} x, & \text{if } x \ge 0 \\ -x, & \text{if } x < 0 \end{cases} $$

现在可以认为渲染LaTex公式没有问题了。

代码块高亮

当文章内要插入代码时,一个漂亮的代码块对文章的美观就很重要。站主使用 Code Block Pro 这款插件,可以对多种语言的代码进行高亮,并且右上角可以一键复制,十分方便。

脚注

当你有一天想要写一篇像科研论文那样,严谨的参考了多篇权威资料后产出的文章,就需要脚注了。就是某句话右上角有一个数字小标,点击一下跳转到文章末尾参考文献的那种。这种情况下当然是希望有一个管理脚注的插件了,题主使用的是 Easy Footnotes 这款插件,用代码来设置脚注,文章渲染的时候可以帮你把多个地方同一份资料归为一个脚注并且自动编号,十分方便。

Mermaid画图

优质的文章是缺少不了图的,尤其是流程图这样的。使用本地软件绘制再上传当然也是一种好方法,但是如果可以在线编辑就很棒了。可以使用 MerPress 这个插件,在编写文章时插入一个 MerPress 块,输入Mermaid代码,就可以渲染出Mermaid图了。

评论区

距离建站完成已经一年多,我翻着这个博客网站,想到虽然现在读者稀少,几乎看不到什么评论,但是我还是希望可以好好打造这个博客网站,这个博客网站作为记录我自己想法的同时,也可以成为同道中人交流的一个媒介。所以我对评论区进行了一些改进。这里要推荐一个插件,结合AI非常好用:Code Snippets。只要跟AI说自己的需求,帮自己写好脚本看看效果反馈回去就可以做好了。

评论格式

我一开始主要是想要有一些评论可能需要数学公式,后来我想到也许还需要斜体、下划线、颜色甚至代码块这些的,所以我专门写脚本来做这些东西。

由于一般情况下WordPress是不会像正文一样让评论文本也有丰富的渲染格式的,一些html代码普通订阅者写了也会被过滤掉很多特性,所以先要写一个脚本允许普通订阅者写评论使用这些渲染格式。

/**
 * 允许已登录的订阅者在评论中使用更多文字格式。
 *
 * 支持:
 * - 文字颜色、底色及部分安全内联样式
 * - 粗体、斜体、删除线、下划线、高亮
 * - 多行代码块和代码语言标记
 * - 有序列表、无序列表
 * - 上标、下标、键盘按键
 * - 引用、链接
 * - details / summary 折叠内容
 *
 * 注意:
 * - 只对已登录且角色为 subscriber 的用户生效
 * - 不允许 script、iframe、事件属性等危险内容
 */
add_filter(
	'wp_kses_allowed_html',
	'my_site_allow_subscriber_comment_html',
	10,
	2
);

function my_site_allow_subscriber_comment_html(
	$allowed_tags,
	$context
) {
	/*
	 * WordPress 在保存普通评论前,
	 * 使用 pre_comment_content 上下文过滤 HTML。
	 */
	if ( 'pre_comment_content' !== $context ) {
		return $allowed_tags;
	}

	/*
	 * 只对已登录的订阅者开放。
	 */
	$user = wp_get_current_user();

	if (
		! $user->exists()
		|| ! in_array(
			'subscriber',
			(array) $user->roles,
			true
		)
	) {
		return $allowed_tags;
	}

	/*
	 * 额外允许的 HTML 标签和属性。
	 *
	 * 标签名、属性名必须全部小写。
	 */
	$extra_tags = array(

		/*
		 * 通用文字容器:
		 *
		 * <span style="color:red;">
		 * <span style="background-color:yellow;">
		 */
		'span' => array(
			'style' => true,
			'class' => true,
			'title' => true,
		),

		/*
		 * 高亮文字:
		 *
		 * <mark>重点内容</mark>
		 */
		'mark' => array(
			'style' => true,
			'class' => true,
			'title' => true,
		),

		/*
		 * 下划线:
		 *
		 * <u>下划线内容</u>
		 */
		'u' => array(
			'style' => true,
			'class' => true,
			'title' => true,
		),

		/*
		 * 粗体。
		 */
		'strong' => array(
			'style' => true,
			'class' => true,
			'title' => true,
		),

		'b' => array(
			'style' => true,
			'class' => true,
			'title' => true,
		),

		/*
		 * 斜体。
		 */
		'em' => array(
			'style' => true,
			'class' => true,
			'title' => true,
		),

		'i' => array(
			'style' => true,
			'class' => true,
			'title' => true,
		),

		/*
		 * 删除线。
		 */
		'del' => array(
			'style' => true,
			'class' => true,
			'title' => true,
		),

		's' => array(
			'style' => true,
			'class' => true,
			'title' => true,
		),

		/*
		 * 段落和换行。
		 */
		'p' => array(
			'style' => true,
			'class' => true,
		),

		'br' => array(
			'class' => true,
		),

		/*
		 * 引用。
		 */
		'blockquote' => array(
			'style' => true,
			'class' => true,
			'title' => true,
		),

		/*
		 * 多行代码块。
		 *
		 * data-language 用于显示语言名称;
		 * class="language-javascript"
		 * 用于后续语法高亮。
		 */
		'pre' => array(
			'style'         => true,
			'class'         => true,
			'title'         => true,
			'data-language' => true,
		),

		'code' => array(
			'style' => true,
			'class' => true,
			'title' => true,
		),

		/*
		 * 无序列表。
		 */
		'ul' => array(
			'style' => true,
			'class' => true,
		),

		/*
		 * 有序列表。
		 *
		 * start:指定起始数字
		 * reversed:倒序
		 * type:数字或字母类型
		 */
		'ol' => array(
			'style'    => true,
			'class'    => true,
			'start'    => true,
			'reversed' => true,
			'type'     => true,
		),

		/*
		 * 列表项目。
		 */
		'li' => array(
			'style' => true,
			'class' => true,
			'value' => true,
		),

		/*
		 * 下标、上标。
		 */
		'sub' => array(
			'style' => true,
			'class' => true,
		),

		'sup' => array(
			'style' => true,
			'class' => true,
		),

		/*
		 * 键盘按键。
		 */
		'kbd' => array(
			'style' => true,
			'class' => true,
			'title' => true,
		),

		/*
		 * 可折叠内容。
		 */
		'details' => array(
			'style' => true,
			'class' => true,
			'open'  => true,
		),

		'summary' => array(
			'style' => true,
			'class' => true,
			'title' => true,
		),

		/*
		 * 链接。
		 */
		'a' => array(
			'href'   => true,
			'title'  => true,
			'target' => true,
			'rel'    => true,
			'style'  => true,
			'class'  => true,
		),
	);

	/*
	 * 把新增属性合并到 WordPress 原有白名单,
	 * 避免覆盖标签原本允许的属性。
	 */
	foreach ( $extra_tags as $tag => $attributes ) {
		if ( ! isset( $allowed_tags[ $tag ] ) ) {
			$allowed_tags[ $tag ] = array();
		}

		$allowed_tags[ $tag ] = array_merge(
			$allowed_tags[ $tag ],
			$attributes
		);
	}

	return $allowed_tags;
}

接下来是关于代码框的大小、底色、高亮的专门的设置。

/**
 * 评论代码块:统一正式评论与 Preview 的样式,
 * 并加载 Prism 语法高亮。
 *
 * 效果:
 * - 代码框根据最长一行代码自动调整宽度;
 * - 不受同一条评论中长文字、列表或公式影响;
 * - 多个代码框仍然从上到下排列;
 * - 代码过长时最多占满评论区域;
 * - 超出宽度的代码在框内横向滚动;
 * - 正式评论和 Preview 保持一致;
 * - 支持 Prism 语言组件自动加载。
 *
 * 使用方法:
 *
 * <pre class="comment-code" data-language="JavaScript">
 * <code class="language-javascript">...</code>
 * </pre>
 */


/* =========================================================
 * 一、加载 Prism 核心和语言自动加载器
 * =========================================================
 */

add_action(
	'wp_enqueue_scripts',
	'qmc_enqueue_comment_prism',
	120
);

function qmc_enqueue_comment_prism() {

	if ( ! is_singular() ) {
		return;
	}

	$version = '1.30.0';

	$base_url =
		'https://cdn.jsdelivr.net/npm/prismjs@'
		. $version
		. '/';

	/*
	 * Prism 核心。
	 */
	wp_enqueue_script(
		'qmc-prism-core',
		$base_url
			. 'components/prism-core.min.js',
		array(),
		$version,
		true
	);

	/*
	 * Prism 语言自动加载器。
	 *
	 * 页面中出现:
	 *
	 * language-python
	 * language-java
	 * language-cpp
	 * language-matlab
	 *
	 * 等类名时,会自动加载对应语言组件。
	 */
	wp_enqueue_script(
		'qmc-prism-autoloader',
		$base_url
			. 'plugins/autoloader/'
			. 'prism-autoloader.min.js',
		array(
			'qmc-prism-core',
		),
		$version,
		true
	);

	$components_url =
		$base_url . 'components/';

	$javascript =
		'(function () {'

		. 'if (!window.Prism) {'
		. 'return;'
		. '}'

		/*
		 * 设置自动加载器的语言组件目录。
		 */
		. 'if ('
		. 'Prism.plugins '
		. '&& Prism.plugins.autoloader'
		. ') {'

		. 'Prism.plugins.autoloader.languages_path = '
		. wp_json_encode( $components_url )
		. ';'

		. '}'

		/*
		 * 对指定区域中的评论代码执行高亮。
		 */
		. 'function highlightCommentCode(root) {'

		. 'var container = root || document;'

		. 'container.querySelectorAll('
		. '"pre.comment-code '
		. 'code[class*=\"language-\"]"'
		. ').forEach(function (code) {'

		/*
		 * 避免同一个代码块同时重复执行。
		 */
		. 'if ('
		. 'code.dataset.qmcHighlighting === "1"'
		. ') {'
		. 'return;'
		. '}'

		. 'code.dataset.qmcHighlighting = "1";'

		. 'Prism.highlightElement('
		. 'code,'
		. 'false,'
		. 'function () {'
		. 'delete code.dataset.qmcHighlighting;'
		. '}'
		. ');'

		. '});'

		. '}'

		/*
		 * 页面加载后处理正式发布的评论。
		 */
		. 'if (document.readyState === "loading") {'

		. 'document.addEventListener('
		. '"DOMContentLoaded",'
		. 'function () {'
		. 'highlightCommentCode(document);'
		. '}'
		. ');'

		. '} else {'

		. 'highlightCommentCode(document);'

		. '}'

		/*
		 * 提供给动态 Preview 功能调用。
		 */
		. 'window.qmcHighlightCommentCode = '
		. 'highlightCommentCode;'

		. '})();';

	wp_add_inline_script(
		'qmc-prism-autoloader',
		$javascript,
		'after'
	);
}


/* =========================================================
 * 二、正式评论与 Preview 的代码样式
 * =========================================================
 */

add_action(
	'wp_head',
	'qmc_output_comment_code_styles',
	999
);

function qmc_output_comment_code_styles() {

	if ( ! is_singular() ) {
		return;
	}

	?>
	<style id="qmc-comment-code-styles">

		/*
		 * 正式评论和 Preview 共用的代码框样式。
		 *
		 * display: block:
		 * 确保多个代码框始终从上到下排列,
		 * 不会左右并排。
		 *
		 * width: max-content:
		 * 宽度根据代码中最长的一行计算,
		 * 不会受到同一条评论中长文字的影响。
		 *
		 * max-width: 100%:
		 * 代码太长时最多占满评论区域,
		 * 多出的内容在代码框内部横向滚动。
		 */
		pre.comment-code,
		.qmc-preview-content pre.comment-code {
			position: relative !important;

			display: block !important;
			clear: both !important;

			width: max-content !important;
			max-width: 100% !important;
			min-width: 0 !important;

			inline-size: max-content !important;
			max-inline-size: 100% !important;

			margin-top: 1.15em !important;
			margin-right: auto !important;
			margin-bottom: 1.15em !important;
			margin-left: 0 !important;

			padding:
				47px
				18px
				18px !important;

			overflow-x: auto !important;
			overflow-y: hidden !important;

			box-sizing: border-box !important;

			color: #e6edf3 !important;
			background: #0d1117 !important;

			border:
				1px solid
				#30363d !important;

			border-radius: 8px !important;

			box-shadow:
				0 4px 14px
				rgba(
					0,
					0,
					0,
					0.12
				) !important;

			white-space: pre !important;

			overflow-wrap: normal !important;
			word-break: normal !important;
			word-wrap: normal !important;

			tab-size: 4;

			font-family:
				ui-monospace,
				SFMono-Regular,
				Menlo,
				Monaco,
				Consolas,
				"Liberation Mono",
				"Courier New",
				monospace !important;

			font-size: 14px !important;
			font-weight: 400 !important;
			line-height: 1.65 !important;

			text-align: left !important;
			text-shadow: none !important;

			vertical-align: top !important;
		}


		/*
		 * 代码框顶部的语言栏。
		 *
		 * data-language="Python"
		 * 会在顶部显示 Python。
		 */
		pre.comment-code::before,
		.qmc-preview-content
			pre.comment-code::before {
			position: absolute;

			top: 0;
			right: 0;
			left: 0;

			display: block;

			min-height: 32px;

			padding:
				8px
				14px;

			box-sizing: border-box;

			content: attr(data-language);

			color: #8b949e;
			background: #161b22;

			border-bottom:
				1px solid
					#30363d;

			border-radius:
				8px
				8px
				0
				0;

			font-family:
				-apple-system,
				BlinkMacSystemFont,
				"Segoe UI",
				sans-serif;

			font-size: 12px;
			font-weight: 600;
			line-height: 16px;

			letter-spacing: 0.04em;

			white-space: nowrap;
		}


		/*
		 * 没有填写 data-language 时,
		 * 不显示顶部语言栏。
		 */
		pre.comment-code:not(
			[data-language]
		),
		pre.comment-code[
			data-language=""
		],
		.qmc-preview-content
			pre.comment-code:not(
				[data-language]
			),
		.qmc-preview-content
			pre.comment-code[
				data-language=""
			] {
			padding-top:
				18px !important;
		}

		pre.comment-code:not(
			[data-language]
		)::before,
		pre.comment-code[
			data-language=""
		]::before,
		.qmc-preview-content
			pre.comment-code:not(
				[data-language]
			)::before,
		.qmc-preview-content
			pre.comment-code[
				data-language=""
			]::before {
			display: none;
		}


		/*
		 * 清除主题为 code 元素添加的灰色背景、
		 * 内边距、边框及宽度限制。
		 *
		 * code 也按自身内容计算宽度,
		 * 不能使用 min-width: 100%,
		 * 否则会再次把代码框撑满。
		 */
		pre.comment-code > code,
		pre.comment-code >
			code[class*="language-"],
		.qmc-preview-content
			pre.comment-code > code,
		.qmc-preview-content
			pre.comment-code >
			code[class*="language-"] {
			display: block !important;

			width: max-content !important;
			max-width: none !important;
			min-width: 0 !important;

			inline-size: max-content !important;

			margin: 0 !important;
			padding: 0 !important;

			color: inherit !important;
			background: transparent !important;

			border: 0 !important;
			border-radius: 0 !important;

			box-shadow: none !important;

			white-space: inherit !important;

			overflow-wrap: normal !important;
			word-break: normal !important;
			word-wrap: normal !important;

			font: inherit !important;
			line-height: inherit !important;

			text-align: left !important;
			text-shadow: none !important;
		}


		/*
		 * 防止主题给包含代码块的段落
		 * 设置居中或其他特殊布局。
		 */
		.comment-content
			p:has(
				> pre.comment-code
			),
		.td-comment-content
			p:has(
				> pre.comment-code
			),
		.qmc-preview-content
			p:has(
				> pre.comment-code
			) {
			display: block !important;

			width: 100% !important;

			margin-right: 0 !important;
			margin-left: 0 !important;

			text-align: left !important;
		}


		/* =================================================
		 * Prism 语法高亮颜色
		 * =================================================
		 */

		pre.comment-code .token.comment,
		pre.comment-code .token.prolog,
		pre.comment-code .token.doctype,
		pre.comment-code .token.cdata {
			color: #8b949e !important;
			font-style: italic;
		}

		pre.comment-code .token.punctuation {
			color: #c9d1d9 !important;
		}

		pre.comment-code .token.namespace {
			opacity: 0.8;
		}

		pre.comment-code .token.property,
		pre.comment-code .token.tag,
		pre.comment-code .token.boolean,
		pre.comment-code .token.number,
		pre.comment-code .token.constant,
		pre.comment-code .token.symbol,
		pre.comment-code .token.deleted {
			color: #ff7b72 !important;
		}

		pre.comment-code .token.selector,
		pre.comment-code .token.attr-name,
		pre.comment-code .token.string,
		pre.comment-code .token.char,
		pre.comment-code .token.builtin,
		pre.comment-code .token.inserted {
			color: #a5d6ff !important;
		}

		pre.comment-code .token.operator,
		pre.comment-code .token.entity,
		pre.comment-code .token.url,
		pre.comment-code .token.variable {
			color: #d2a8ff !important;
		}

		pre.comment-code .token.atrule,
		pre.comment-code .token.attr-value,
		pre.comment-code .token.function,
		pre.comment-code .token.class-name {
			color: #d2a8ff !important;
		}

		pre.comment-code .token.keyword {
			color: #ff7b72 !important;
		}

		pre.comment-code .token.regex,
		pre.comment-code .token.important {
			color: #ffa657 !important;
		}

		pre.comment-code
			.token.function-variable {
			color: #d2a8ff !important;
		}

		pre.comment-code .token.parameter {
			color: #ffa657 !important;
		}

		pre.comment-code .token.bold {
			font-weight: 700;
		}

		pre.comment-code .token.italic {
			font-style: italic;
		}


		/*
		 * 手机端。
		 *
		 * 代码框仍然按内容调整宽度,
		 * 但不会超过屏幕可用范围。
		 */
		@media (max-width: 767px) {

			pre.comment-code,
			.qmc-preview-content
				pre.comment-code {
				width: max-content !important;
				max-width: 100% !important;

				inline-size:
					max-content !important;
				max-inline-size:
					100% !important;

				padding-right:
					14px !important;
				padding-left:
					14px !important;

				font-size:
					12.5px !important;
			}
		}

	</style>
	<?php
}

评论预览

由于有丰富的格式,评论者写评论的时候想要知道现在的文本渲染出来的效果怎么样就不能很确定,所以这个时候最好可以写的时候看到渲染后的样子。

/**
 * WordPress 评论预览按钮。
 *
 * 功能:
 * 1. 在原来的 Post Comment 按钮右侧添加 Preview;
 * 2. Preview 自动继承原提交按钮的样式;
 * 3. 点击后在按钮下方显示单一预览框;
 * 4. 不显示 Preview 标题栏或状态栏;
 * 5. 使用正式评论相同的 HTML 白名单;
 * 6. 支持 MathJax 2、MathJax 3/4;
 * 7. 页面已有 Prism 或 highlight.js 时自动高亮代码。
 */


/* =========================================================
 * 一、处理预览 AJAX 请求
 * =========================================================
 */

add_action(
	'wp_ajax_qmc_preview_comment',
	'qmc_handle_comment_preview'
);

function qmc_handle_comment_preview() {

	check_ajax_referer(
		'qmc_comment_preview',
		'nonce'
	);

	if ( ! is_user_logged_in() ) {
		wp_send_json_error(
			array(
				'message' => '请先登录。',
			),
			403
		);
	}

	$content = isset( $_POST['content'] )
		? wp_unslash( $_POST['content'] )
		: '';

	if ( '' === trim( $content ) ) {
		wp_send_json_error(
			array(
				'message' => '请先输入评论内容。',
			),
			400
		);
	}

	/*
	 * 限制单次预览的内容长度。
	 * 这里设为约 50 KB。
	 */
	if ( strlen( $content ) > 50000 ) {
		wp_send_json_error(
			array(
				'message' => '评论内容过长,无法预览。',
			),
			400
		);
	}

	/*
	 * 使用正式提交评论时相同的 HTML 白名单。
	 *
	 * 之前添加的:
	 * wp_kses_allowed_html / pre_comment_content
	 * 过滤器也会在这里生效。
	 */
	$allowed_tags = wp_kses_allowed_html(
		'pre_comment_content'
	);

	$clean_content = wp_kses(
		$content,
		$allowed_tags
	);

	/*
	 * 修复未正确闭合的安全标签。
	 */
	$clean_content = force_balance_tags(
		$clean_content
	);

	/*
	 * 使用 WordPress 的 comment_text 显示过滤器,
	 * 尽量模拟评论正式发布后的显示效果。
	 */
	$preview_html = apply_filters(
		'comment_text',
		$clean_content,
		null,
		array(
			'qmc_preview' => true,
		)
	);

	wp_send_json_success(
		array(
			'html' => $preview_html,
		)
	);
}


/* =========================================================
 * 二、判断当前页面是否需要加载预览功能
 * =========================================================
 */

function qmc_should_load_comment_preview() {

	if ( is_admin() ) {
		return false;
	}

	if ( ! is_singular() ) {
		return false;
	}

	if ( ! comments_open() ) {
		return false;
	}

	if ( ! is_user_logged_in() ) {
		return false;
	}

	return true;
}


/* =========================================================
 * 三、输出预览框样式
 * =========================================================
 */

add_action(
	'wp_head',
	'qmc_output_comment_preview_css',
	99
);

function qmc_output_comment_preview_css() {

	if ( ! qmc_should_load_comment_preview() ) {
		return;
	}

	?>
	<style id="qmc-comment-preview-css">

		/*
		 * 原提交按钮和 Preview 按钮所在行。
		 */
		#commentform .qmc-submit-row {
			display: flex;
			flex-wrap: wrap;
			align-items: center;
			gap: 10px;
		}

		/*
		 * 避免主题给原按钮设置 100% 宽度,
		 * 导致两个按钮不能并排。
		 */
		#commentform .qmc-submit-row > input[type="submit"],
		#commentform .qmc-submit-row > button[type="submit"],
		#commentform .qmc-submit-row > .qmc-preview-button {
			width: auto !important;
			margin-right: 0 !important;
			margin-left: 0 !important;
		}

		/*
		 * Preview 按钮会复制 Post Comment 按钮。
		 */
		#commentform .qmc-preview-button {
			cursor: pointer;
		}

		#commentform .qmc-preview-button:hover {
			filter: brightness(0.96);
		}

		#commentform .qmc-preview-button:disabled {
			cursor: wait;
			opacity: 0.65;
		}


		/*
		 * 单一预览框。
		 *
		 * 不再包含顶部标题栏。
		 */
		.qmc-preview-panel {
			display: none;

			width: 100%;
			margin: 18px 0 22px;

			overflow: hidden;

			color: #222;
			background: #fff;

			border: 1px solid #dedede;
			border-radius: 4px;

			box-shadow:
				0 2px 10px rgba(0, 0, 0, 0.05);

			box-sizing: border-box;
		}

		.qmc-preview-panel.is-visible {
			display: block;
		}


		/*
		 * 预览内容主体。
		 *
		 * 它同时也是整个预览框内部唯一的区域。
		 */
		.qmc-preview-content {
			min-height: 90px;
			padding: 18px 20px;

			overflow-wrap: anywhere;

			font-size: inherit;
			line-height: 1.8;

			box-sizing: border-box;
		}

		.qmc-preview-content > :first-child {
			margin-top: 0;
		}

		.qmc-preview-content > :last-child {
			margin-bottom: 0;
		}


		/*
		 * 加载中、空内容和错误提示。
		 *
		 * 提示直接显示在预览框内部,
		 * 不再显示单独的标题或状态栏。
		 */
		.qmc-preview-content.is-message {
			display: flex;
			align-items: center;

			min-height: 90px;

			color: #777;
			font-size: 13px;
		}

		.qmc-preview-content.is-error {
			color: #b42318;
		}


		/*
		 * 恢复评论预览中的列表样式。
		 */
		.qmc-preview-content ul,
		.qmc-preview-content ol {
			margin: 0.9em 0 0.9em 1.8em;
			padding: 0;
		}

		.qmc-preview-content ul {
			list-style: disc outside;
		}

		.qmc-preview-content ol {
			list-style: decimal outside;
		}

		.qmc-preview-content li {
			margin: 0.3em 0;
			padding: 0;
		}


		/*
		 * 上标、下标。
		 */
		.qmc-preview-content sup,
		.qmc-preview-content sub {
			position: relative;

			font-size: 0.75em;
			line-height: 0;
			vertical-align: baseline;
		}

		.qmc-preview-content sup {
			top: -0.5em;
		}

		.qmc-preview-content sub {
			bottom: -0.25em;
		}


		/*
		 * 键盘按键。
		 */
		.qmc-preview-content kbd {
			display: inline-block;

			padding: 1px 6px;

			color: #333;
			background: #f7f7f7;

			border: 1px solid #c8c8c8;
			border-bottom-width: 2px;
			border-radius: 4px;

			font-family: inherit;
			font-size: 0.84em;
			line-height: 1.5;
		}


		/*
		 * 折叠内容。
		 */
		.qmc-preview-content details {
			margin: 1em 0;
			padding: 10px 13px;

			border: 1px solid #ddd;
			border-radius: 4px;
		}

		.qmc-preview-content summary {
			cursor: pointer;
			font-weight: 600;
		}


		/*
		 * 行内代码。
		 */
		.qmc-preview-content :not(pre) > code {
			padding: 0.12em 0.35em;

			color: #b42318;
			background: #f3f4f5;

			border-radius: 3px;

			font-family:
				ui-monospace,
				SFMono-Regular,
				Menlo,
				Monaco,
				Consolas,
				"Liberation Mono",
				"Courier New",
				monospace;

			font-size: 0.9em;
		}


		/*
		 * 多行代码块。
		 *
		 * 如果另外启用了评论 Prism 代码片段,
		 * 其更具体的样式会继续覆盖这里。
		 */
		.qmc-preview-content pre,
		.qmc-preview-content pre.comment-code {
			display: block;

			max-width: 100%;
			margin: 1.1em 0;
			padding: 16px 18px;

			overflow-x: auto;

			color: #e6edf3;
			background: #0d1117;

			border: 1px solid #30363d;
			border-radius: 7px;

			box-shadow: none;

			white-space: pre;
			tab-size: 4;

			font-family:
				ui-monospace,
				SFMono-Regular,
				Menlo,
				Monaco,
				Consolas,
				"Liberation Mono",
				"Courier New",
				monospace;

			font-size: 0.9em;
			line-height: 1.65;

			box-sizing: border-box;
		}

		.qmc-preview-content pre code {
			display: block;

			padding: 0;

			color: inherit;
			background: transparent;

			border: 0;
			border-radius: 0;

			white-space: inherit;
			font: inherit;
		}


		/*
		 * 手机端。
		 */
		@media (max-width: 767px) {

			#commentform .qmc-submit-row {
				gap: 8px;
			}

			.qmc-preview-content {
				padding: 14px;
			}

			.qmc-preview-content pre,
			.qmc-preview-content pre.comment-code {
				padding: 13px 14px;
				font-size: 0.83em;
			}
		}

	</style>
	<?php
}


/* =========================================================
 * 四、输出前端 JavaScript
 * =========================================================
 */

add_action(
	'wp_footer',
	'qmc_output_comment_preview_javascript',
	99
);

function qmc_output_comment_preview_javascript() {

	if ( ! qmc_should_load_comment_preview() ) {
		return;
	}

	$config = array(
		'ajaxUrl' => admin_url(
			'admin-ajax.php'
		),

		'action' => 'qmc_preview_comment',

		'nonce' => wp_create_nonce(
			'qmc_comment_preview'
		),

		'buttonText' => 'Preview',

		'loadingText' =>
			'Rendering…',

		'emptyText' =>
			'Please enter a comment first.',

		'errorText' =>
			'Preview failed. Please try again.',
	);

	?>
	<script id="qmc-comment-preview-js">
	(function () {
		'use strict';

		var config =
			<?php echo wp_json_encode( $config ); ?>;


		/*
		 * 复制原按钮的主要计算样式。
		 */
		function copyButtonAppearance(
			source,
			target
		) {
			var computed =
				window.getComputedStyle(source);

			var properties = [
				'backgroundColor',
				'backgroundImage',
				'color',
				'borderTopWidth',
				'borderRightWidth',
				'borderBottomWidth',
				'borderLeftWidth',
				'borderTopStyle',
				'borderRightStyle',
				'borderBottomStyle',
				'borderLeftStyle',
				'borderTopColor',
				'borderRightColor',
				'borderBottomColor',
				'borderLeftColor',
				'borderRadius',
				'paddingTop',
				'paddingRight',
				'paddingBottom',
				'paddingLeft',
				'fontFamily',
				'fontSize',
				'fontWeight',
				'fontStyle',
				'lineHeight',
				'letterSpacing',
				'textTransform',
				'textDecoration',
				'boxShadow',
				'minHeight'
			];

			properties.forEach(
				function (property) {
					target.style[property] =
						computed[property];
				}
			);
		}


		/*
		 * 执行代码高亮。
		 */
		function highlightCode(container) {

			/*
			 * 优先使用之前评论高亮片段
			 * 暴露的专用函数。
			 */
			if (
				typeof window
					.qmcHighlightCommentCode
				=== 'function'
			) {
				window.qmcHighlightCommentCode(
					container
				);

				return;
			}

			if (
				window.Prism
				&& typeof window.Prism
					.highlightAllUnder
					=== 'function'
			) {
				window.Prism.highlightAllUnder(
					container
				);

				return;
			}

			if (
				window.hljs
				&& typeof window.hljs
					.highlightElement
					=== 'function'
			) {
				container
					.querySelectorAll('pre code')
					.forEach(
						function (codeElement) {
							window.hljs
								.highlightElement(
									codeElement
								);
						}
					);
			}
		}


		/*
		 * 重新渲染动态加入的数学公式。
		 *
		 * 同时兼容:
		 * MathJax 2
		 * MathJax 3
		 * MathJax 4
		 */
		function typesetMath(container) {

			if (!window.MathJax) {
				return;
			}

			/*
			 * MathJax 2。
			 */
			if (
				window.MathJax.Hub
				&& typeof window.MathJax.Hub
					.Queue === 'function'
			) {
				window.MathJax.Hub.Queue(
					[
						'Typeset',
						window.MathJax.Hub,
						container
					]
				);

				return;
			}

			/*
			 * MathJax 3 / 4。
			 */
			if (
				typeof window.MathJax
					.typesetPromise === 'function'
			) {
				if (
					typeof window.MathJax
						.typesetClear === 'function'
				) {
					window.MathJax.typesetClear(
						[container]
					);
				}

				window.MathJax
					.typesetPromise([container])
					.catch(function () {
						/*
						 * 公式失败不阻止其他预览。
						 */
					});
			}
		}


		function initializePreview() {

			var form = document.querySelector(
				'#commentform'
			);

			if (!form) {
				return;
			}

			var textarea = form.querySelector(
				'textarea[name="comment"]'
			);

			if (!textarea) {
				return;
			}

			/*
			 * 优先寻找 WordPress 标准 #submit。
			 */
			var submitButton = form.querySelector(
				'#submit'
			);

			if (!submitButton) {
				submitButton = form.querySelector(
					'input[type="submit"],'
					+ ' button[type="submit"]'
				);
			}

			if (!submitButton) {
				return;
			}

			if (
				form.dataset.qmcPreviewInitialized
			) {
				return;
			}

			form.dataset.qmcPreviewInitialized = '1';


			/*
			 * 找到提交按钮外围容器。
			 */
			var submitRow = submitButton.closest(
				'.form-submit'
			);

			if (!submitRow) {
				submitRow =
					submitButton.parentElement;
			}

			if (!submitRow) {
				return;
			}

			submitRow.classList.add(
				'qmc-submit-row'
			);


			/*
			 * 复制原 Post Comment 按钮。
			 */
			var previewButton =
				submitButton.cloneNode(true);

			previewButton.removeAttribute('id');
			previewButton.removeAttribute('name');
			previewButton.removeAttribute(
				'formaction'
			);
			previewButton.removeAttribute(
				'onclick'
			);

			previewButton.type = 'button';

			previewButton.classList.add(
				'qmc-preview-button'
			);

			if (
				previewButton.tagName
					.toLowerCase()
				=== 'input'
			) {
				previewButton.value =
					config.buttonText;
			} else {
				previewButton.textContent =
					config.buttonText;
			}

			previewButton.setAttribute(
				'aria-expanded',
				'false'
			);

			previewButton.setAttribute(
				'aria-controls',
				'qmc-comment-preview-panel'
			);

			copyButtonAppearance(
				submitButton,
				previewButton
			);

			submitRow.appendChild(
				previewButton
			);


			/*
			 * 创建单一预览框。
			 *
			 * 不再创建:
			 * qmc-preview-header
			 * qmc-preview-title
			 * qmc-preview-status
			 */
			var panel =
				document.createElement('div');

			panel.id =
				'qmc-comment-preview-panel';

			panel.className =
				'qmc-preview-panel';

			var previewContent =
				document.createElement('div');

			previewContent.id =
				'qmc-comment-preview-content';

			previewContent.className =
				'qmc-preview-content';

			previewContent.setAttribute(
				'aria-live',
				'polite'
			);

			panel.appendChild(
				previewContent
			);

			submitRow.insertAdjacentElement(
				'afterend',
				panel
			);


			function showPanel() {

				panel.classList.add(
					'is-visible'
				);

				previewButton.setAttribute(
					'aria-expanded',
					'true'
				);
			}


			/*
			 * 在单一预览框内部显示状态信息。
			 */
			function showMessage(
				message,
				isError
			) {
				previewContent.innerHTML = '';

				previewContent.textContent =
					message || '';

				previewContent.classList.add(
					'is-message'
				);

				previewContent.classList.toggle(
					'is-error',
					Boolean(isError)
				);
			}


			function clearMessageState() {

				previewContent.classList.remove(
					'is-message',
					'is-error'
				);
			}


			function requestPreview() {

				var content = textarea.value;

				showPanel();

				if (!content.trim()) {

					showMessage(
						config.emptyText,
						true
					);

					textarea.focus();

					return;
				}

				showMessage(
					config.loadingText,
					false
				);

				previewButton.disabled = true;

				var body =
					new URLSearchParams();

				body.set(
					'action',
					config.action
				);

				body.set(
					'nonce',
					config.nonce
				);

				body.set(
					'content',
					content
				);

				fetch(
					config.ajaxUrl,
					{
						method: 'POST',

						credentials:
							'same-origin',

						headers: {
							'Content-Type':
								'application/'
								+ 'x-www-form-urlencoded;'
								+ ' charset=UTF-8'
						},

						body: body.toString()
					}
				)
					.then(function (response) {

						return response
							.json()
							.then(function (data) {
								return {
									ok:
										response.ok,
									status:
										response.status,
									data:
										data
								};
							});
					})
					.then(function (result) {

						var response =
							result.data;

						if (
							!result.ok
							|| !response
							|| !response.success
							|| !response.data
							|| typeof response.data
								.html !== 'string'
						) {
							var message =
								response
								&& response.data
								&& response.data
									.message
									? response.data
										.message
									: config.errorText;

							throw new Error(
								message
							);
						}

						clearMessageState();

						previewContent.innerHTML =
							response.data.html;

						highlightCode(
							previewContent
						);

						typesetMath(
							previewContent
						);
					})
					.catch(function (error) {

						showMessage(
							error.message
								|| config.errorText,
							true
						);
					})
					.finally(function () {

						previewButton.disabled =
							false;
					});
			}


			previewButton.addEventListener(
				'click',
				requestPreview
			);


			/*
			 * 输入内容改变时不再显示顶部状态文字。
			 *
			 * 已有预览会暂时保留;
			 * 再次点击 Preview 后刷新。
			 */
			textarea.addEventListener(
				'input',
				function () {
					/*
					 * 不自动刷新,也不产生额外提示栏。
					 */
				}
			);


			/*
			 * Ctrl + Enter 或 Command + Enter:
			 * 快速预览。
			 */
			textarea.addEventListener(
				'keydown',
				function (event) {

					if (
						(
							event.ctrlKey
							|| event.metaKey
						)
						&& event.key === 'Enter'
					) {
						event.preventDefault();

						requestPreview();
					}
				}
			);
		}


		if (document.readyState === 'loading') {

			document.addEventListener(
				'DOMContentLoaded',
				initializePreview
			);

		} else {

			initializePreview();
		}

	})();
	</script>
	<?php
}

评论撤回

这个是对上一个预览的补充。评论都是需要经过审核的,订阅者发布评论之后,还没有通过审核之前,自己可以看到这条新评论,其他人还看不到。这个时候评论者已经可以看到真正发不出去之后渲染出来是什么效果,或者有没有自己写错的地方。如果有想要改的,一般情况下WordPress没有给订阅者评论修改的权利了。这个时候我觉得在审核之前最好评论者有撤回的权利。所以我添加了这个功能。

/**
 * 允许订阅者撤回自己尚未审核通过的评论。
 *
 * 功能:
 * - Delete 显示在 Reply 旁边;
 * - Delete 使用与 Reply 相同的红色小字样式;
 * - Reply 与 Delete 之间保留固定间距;
 * - 只有评论作者本人可以看到和操作;
 * - 只有待审核评论可以撤回;
 * - 审核通过后 Delete 自动消失;
 * - 评论移入回收站,不会永久删除;
 * - 删除后返回评论区,不显示绿色提示框。
 */


/* =========================================================
 * 一、在 Reply 旁边添加 Delete 链接
 * =========================================================
 */

add_filter(
	'comment_reply_link',
	'qmc_add_pending_comment_delete_link',
	20,
	4
);

function qmc_add_pending_comment_delete_link(
	$reply_link,
	$args,
	$comment,
	$post
) {
	/*
	 * 必须是已登录用户。
	 */
	if ( ! is_user_logged_in() ) {
		return $reply_link;
	}

	/*
	 * 必须是有效的评论对象。
	 */
	if ( ! $comment instanceof WP_Comment ) {
		return $reply_link;
	}

	$user = wp_get_current_user();

	/*
	 * 只允许订阅者角色使用。
	 */
	if (
		! in_array(
			'subscriber',
			(array) $user->roles,
			true
		)
	) {
		return $reply_link;
	}

	/*
	 * 只处理待审核评论。
	 *
	 * comment_approved:
	 * 0 = 待审核
	 * 1 = 已审核
	 */
	if ( '0' !== (string) $comment->comment_approved ) {
		return $reply_link;
	}

	/*
	 * 只能撤回当前账号本人发表的评论。
	 */
	if (
		(int) $comment->user_id
		!== get_current_user_id()
	) {
		return $reply_link;
	}

	$comment_id = (int) $comment->comment_ID;

	$nonce = wp_create_nonce(
		'qmc_withdraw_comment_' . $comment_id
	);

	/*
	 * Delete 使用和 Reply 相同的
	 * comment-reply-link 类名,
	 * 因此会继承主题的红色小字样式。
	 *
	 * 真正的 POST 表单由 JavaScript 点击后动态创建。
	 */
	$delete_ui =
		'<span class="qmc-pending-delete-wrap">'

		. '<span'
		. ' class="qmc-pending-delete-gap"'
		. ' aria-hidden="true"'
		. '></span>'

		. '<a'
		. ' href="#"'
		. ' class="comment-reply-link'
		. ' qmc-pending-delete-link"'

		. ' data-comment-id="'
		. esc_attr( $comment_id )
		. '"'

		. ' data-nonce="'
		. esc_attr( $nonce )
		. '"'

		. ' data-confirm="'
		. esc_attr(
			'确定撤回这条待审核评论吗?'
		)
		. '"'

		. '>Delete</a>'

		. '</span>';

	return $reply_link . $delete_ui;
}


/* =========================================================
 * 二、处理撤回请求
 * =========================================================
 */

add_action(
	'admin_post_qmc_withdraw_pending_comment',
	'qmc_handle_pending_comment_withdrawal'
);

function qmc_handle_pending_comment_withdrawal() {

	/*
	 * 必须登录。
	 */
	if ( ! is_user_logged_in() ) {
		wp_die(
			'请先登录。',
			'无法撤回评论',
			array(
				'response' => 403,
			)
		);
	}

	$user = wp_get_current_user();

	/*
	 * 只允许订阅者角色使用。
	 */
	if (
		! in_array(
			'subscriber',
			(array) $user->roles,
			true
		)
	) {
		wp_die(
			'当前账号不能执行此操作。',
			'无法撤回评论',
			array(
				'response' => 403,
			)
		);
	}

	/*
	 * 获取评论编号。
	 */
	$comment_id = isset( $_POST['comment_id'] )
		? absint( $_POST['comment_id'] )
		: 0;

	if ( ! $comment_id ) {
		wp_die(
			'评论编号无效。',
			'无法撤回评论',
			array(
				'response' => 400,
			)
		);
	}

	/*
	 * 验证安全令牌。
	 */
	check_admin_referer(
		'qmc_withdraw_comment_' . $comment_id
	);

	$comment = get_comment( $comment_id );

	if ( ! $comment ) {
		wp_die(
			'没有找到这条评论。',
			'无法撤回评论',
			array(
				'response' => 404,
			)
		);
	}

	/*
	 * 只能撤回自己的评论。
	 */
	if (
		(int) $comment->user_id
		!== get_current_user_id()
	) {
		wp_die(
			'你不能撤回其他用户的评论。',
			'无权操作',
			array(
				'response' => 403,
			)
		);
	}

	/*
	 * 服务器端再次确认评论仍然待审核。
	 *
	 * 即使管理员刚刚通过了评论,
	 * 用户旧页面上的 Delete 也无法删除它。
	 */
	if ( '0' !== (string) $comment->comment_approved ) {
		wp_die(
			'这条评论已经通过审核,不能自行撤回。',
			'无法撤回评论',
			array(
				'response' => 403,
			)
		);
	}

	$post_id = (int) $comment->comment_post_ID;

	/*
	 * 将评论移入回收站。
	 */
	if ( ! wp_trash_comment( $comment_id ) ) {
		wp_die(
			'撤回失败,请稍后重试。',
			'无法撤回评论',
			array(
				'response' => 500,
			)
		);
	}

	/*
	 * 删除成功后直接返回文章评论区。
	 *
	 * 不添加查询参数,
	 * 也不显示绿色成功提示框。
	 */
	$redirect_url =
		get_permalink( $post_id )
		. '#comments';

	wp_safe_redirect(
		$redirect_url
	);

	exit;
}


/* =========================================================
 * 三、处理 Delete 链接点击
 * =========================================================
 */

add_action(
	'wp_footer',
	'qmc_pending_comment_delete_script',
	999
);

function qmc_pending_comment_delete_script() {

	if (
		! is_singular()
		|| ! is_user_logged_in()
	) {
		return;
	}

	$admin_post_url = admin_url(
		'admin-post.php'
	);

	?>
	<script id="qmc-pending-comment-delete-js">
	(function () {
		'use strict';

		var adminPostUrl =
			<?php echo wp_json_encode( $admin_post_url ); ?>;

		document.addEventListener(
			'click',
			function (event) {

				var deleteLink = event.target.closest(
					'.qmc-pending-delete-link'
				);

				if (!deleteLink) {
					return;
				}

				event.preventDefault();

				/*
				 * 防止正在提交时重复点击。
				 */
				if (
					deleteLink.getAttribute(
						'aria-disabled'
					) === 'true'
				) {
					return;
				}

				var confirmMessage =
					deleteLink.getAttribute(
						'data-confirm'
					)
					|| '确定撤回这条评论吗?';

				if (!window.confirm(confirmMessage)) {
					return;
				}

				var commentId =
					deleteLink.getAttribute(
						'data-comment-id'
					);

				var nonce =
					deleteLink.getAttribute(
						'data-nonce'
					);

				if (!commentId || !nonce) {
					return;
				}

				deleteLink.setAttribute(
					'aria-disabled',
					'true'
				);

				deleteLink.style.pointerEvents =
					'none';

				/*
				 * 动态创建 POST 表单。
				 *
				 * 避免在 Reply 的行内 HTML 中
				 * 嵌套 form,兼容性更好。
				 */
				var form =
					document.createElement('form');

				form.method = 'post';
				form.action = adminPostUrl;
				form.style.display = 'none';


				var actionInput =
					document.createElement('input');

				actionInput.type = 'hidden';
				actionInput.name = 'action';
				actionInput.value =
					'qmc_withdraw_pending_comment';


				var commentInput =
					document.createElement('input');

				commentInput.type = 'hidden';
				commentInput.name = 'comment_id';
				commentInput.value = commentId;


				var nonceInput =
					document.createElement('input');

				nonceInput.type = 'hidden';
				nonceInput.name = '_wpnonce';
				nonceInput.value = nonce;


				form.appendChild(actionInput);
				form.appendChild(commentInput);
				form.appendChild(nonceInput);

				document.body.appendChild(form);

				form.submit();
			}
		);
	})();
	</script>
	<?php
}


/* =========================================================
 * 四、Delete 链接和固定间距样式
 * =========================================================
 */

add_action(
	'wp_head',
	'qmc_pending_comment_delete_styles',
	999
);

function qmc_pending_comment_delete_styles() {

	if ( ! is_singular() ) {
		return;
	}

	?>
	<style id="qmc-pending-comment-delete-css">

		/*
		 * Delete 区域与 Reply 保持在同一行。
		 */
		.reply .qmc-pending-delete-wrap,
		.qmc-pending-delete-wrap {
			display: inline !important;

			margin: 0 !important;
			padding: 0 !important;

			border: 0 !important;
		}


		/*
		 * Reply 和 Delete 之间的固定间距。
		 *
		 * 需要调整距离时,
		 * 同时修改下面三个 8px。
		 */
		.reply .qmc-pending-delete-gap,
		.qmc-pending-delete-gap {
			display: inline-block !important;

			width: 8px !important;
			min-width: 8px !important;
			max-width: 8px !important;

			height: 1px !important;

			margin: 0 !important;
			padding: 0 !important;

			vertical-align: baseline !important;

			pointer-events: none !important;
		}


		/*
		 * Delete 继承 Reply 的主题样式。
		 *
		 * 清除可能附加的按钮外观。
		 */
		.reply .qmc-pending-delete-link,
		.qmc-pending-delete-link {
			display: inline !important;

			margin: 0 !important;
			padding: 0 !important;

			background: none !important;

			border: 0 !important;
			border-radius: 0 !important;

			box-shadow: none !important;

			vertical-align: baseline !important;

			cursor: pointer !important;
		}


		/*
		 * Delete 正在提交时稍微降低透明度。
		 */
		.qmc-pending-delete-link[
			aria-disabled="true"
		] {
			opacity: 0.65;
			cursor: default !important;
		}

	</style>
	<?php
}

评论者展示

我希望我的博客网站如果某一篇文章吸引到了很多志趣相投的读者,进行了好的讨论,如果一个读者想要了解另一个读者,该怎么样呢?订阅者的注册邮箱我觉得是隐私,还是不要公布的好。所以我设置,如果订阅者在后台写了自己的主页网站,那么Ta发表评论后在评论区别人就可以点击Ta的名称,跳转到Ta的个人主页。这篇文章就可以成为一个有益于交流的地方了。

/**
 * QMC 评论者名称链接到个人网站
 *
 * 效果:
 * 1. 不给评论头像添加链接;
 * 2. 注册用户填写了“个人网站”时,评论名称链接到该网站;
 * 3. 没有填写个人网站时,名称只是普通文字;
 * 4. 不跳转到 WordPress 作者页;
 * 5. 个人网站在新标签页打开;
 * 6. 对管理员、编辑、作者、订阅者等所有角色一视同仁;
 * 7. 已有评论会自动读取用户当前填写的个人网站。
 */

add_filter(
	'get_comment_author_link',
	'qmc_comment_author_link_to_personal_website',
	99,
	3
);

function qmc_comment_author_link_to_personal_website(
	$original_link,
	$author_name,
	$comment_id
) {

	/*
	 * 不改变 WordPress 后台评论管理页面。
	 */
	if ( is_admin() ) {
		return $original_link;
	}

	$comment = get_comment( $comment_id );

	if ( ! $comment instanceof WP_Comment ) {
		return esc_html(
			wp_strip_all_tags( $author_name )
		);
	}

	/*
	 * 获取安全的评论者显示名称。
	 */
	$display_name = trim(
		wp_strip_all_tags( $author_name )
	);

	if ( '' === $display_name ) {
		$display_name = 'Anonymous';
	}

	$website = '';

	/*
	 * 注册用户:
	 * 动态读取用户资料中的“网站”字段。
	 *
	 * 因此用户以后修改个人网址时,
	 * 以前发表的评论也会自动使用新网址。
	 */
	if ( ! empty( $comment->user_id ) ) {

		$user = get_userdata(
			(int) $comment->user_id
		);

		if ( $user instanceof WP_User ) {
			$website = trim(
				(string) $user->user_url
			);
		}

	/*
	 * 非注册访客:
	 * 使用发表评论时填写的网站。
	 */
	} else {
		$website = trim(
			(string) $comment->comment_author_url
		);
	}

	/*
	 * 没有填写网址时,只返回普通文字。
	 *
	 * 不会出现:
	 * - 手型鼠标;
	 * - 红色悬停效果;
	 * - 作者页链接。
	 */
	if (
		'' === $website ||
		'http://' === strtolower( $website ) ||
		'https://' === strtolower( $website )
	) {
		return esc_html( $display_name );
	}

	/*
	 * 用户只填写 example.com 时,
	 * 自动补充 https://。
	 */
	if (
		! preg_match(
			'#^https?://#i',
			$website
		)
	) {
		$website = 'https://' . $website;
	}

	/*
	 * 只允许安全的 HTTP 和 HTTPS 链接。
	 */
	$website = esc_url(
		$website,
		array(
			'http',
			'https',
		)
	);

	/*
	 * 无效网址仍然显示为普通文字。
	 */
	if ( '' === $website ) {
		return esc_html( $display_name );
	}

	/*
	 * 有个人网站时才生成链接。
	 * 主题原有的红色悬停和手型鼠标会自然生效。
	 */
	return sprintf(
		'<a class="url qmc-comment-personal-website" href="%1$s" target="_blank" rel="noopener noreferrer nofollow ugc" aria-label="%2$s">%3$s</a>',
		esc_url( $website ),
		esc_attr(
			sprintf(
				'访问 %s 的个人网站,在新标签页打开',
				$display_name
			)
		),
		esc_html( $display_name )
	);
}

运营维护

安全访问

那么现在已经设计好了网站的排布,也解决了读者订阅、渲染公式这些问题,就可以开始写文章啦。当你的域名备案成功、并且把WordPress设置为域名访问的时候,你会发现浏览器会提示“不安全”。这是因为你的WordPress访问方式为http,要消除“不安全”提示需要安装SSL证书并且改为https访问。阿里云是有免费领取SSL证书服务的,并且有安装SSL证书的相关文档,详情见:WordPress环境上安装SSL证书

我在实现https访问的过程中感觉这篇文档对我来说还不够,我是让AI读了一下这个文档,然后我让它说需要使用什么命令得到我的服务器里的某些信息,然后它给我命令,我复制到服务器里执行,把结果给AI看它来思考下一步干什么。当时是遇到了几个困难,我也不懂具体是什么原理,大概就是Nginx的config文件设置http和https,WordPress的config文件需要修改SSL证书的文件地址,有什么bug就让AI来解决,然后折腾了几下之后我的网站就可以使用https安全访问了。

配置文件都写好后我就只需要在证书文件过期之前替换成新的就可以了。现在我是白嫖阿里云的SSL证书,这里是每年可以申请20张证书,每张证书申请后有效期3个月,我需要每次都下载好新的文件然后放到指定位置。这虽然也没啥但是我还是觉得太麻烦了,我写了一个每一个小时执行的脚本,它会看root下有没有新的证书文件,如果有它就会处理覆盖旧文件,这样每三个月我下载好新的证书文件直接上传到那个位置就好了,剩下的工作脚本会自己完成。不过如果你不在意这个“不安全”标志,也可以不管(好像没有安全证书的时候Edge浏览器如果不指定http访问它不会加载头像图片,不知道啥原因)。感觉https访问维护起来还是很轻松的。

存储空间

由于服务器只有30G,因此为了可以长久运行WordPress网站,要关注存储空间的实用情况。我主要有两个措施:第一就是对于站内需要引用的pdf等文件,在Github上面专门开一个仓库进行存储,站内只要用链接指引过去就行了;还有一个就是尽量使用 .jpg 这样格式的图片上传,一个 .png 这样格式的图片动不动就几 MB,这样的图片多了占用大量不必要的存储不说,访客加载网课也会很卡顿。不过毕竟每次上传图片本地转化成 .jpg 也十分麻烦,这怎么办呢?当然是用插件了,我使用的是 EWWW Image Optimizer 这款插件,它在上传 .png会转化成 .jpg,这时候随便上传图片了,插件帮你自动压缩。

总结

由于这篇站主没有足够精力以及经验来完成一个保姆式的教学工作,因此对于建站小白来说,仅看本文是远不够的,还是需要多参考互联网中的博客,多问AI大模型,多尝试。希望这篇文章能帮助到你。如果你在建站过程中遇到任何问题,或是发现本文有错误,欢迎在评论区留言交流!

Tags

【转载】机器人工程师学习计划

这篇文章是机器人领域大佬杨硕在2016-2018写成的机器人学习指南,很多人都说这一篇文章让他们受益良多,走上了机器人的道路。虽然距离今天这篇学习指南已经发布快十年了,也许有些建议已经不适用于今天,...

欧拉角

本文将对欧拉角的基本概念和几何直观进行介绍。 本文封面源于维基百科。 数学概念 万向锁 笔者学习欧拉角时对万向锁现象很奇怪,一个物体一直都有三个旋转轴,怎么会出现旋转...