建站手记

背景

本站点策划期间正值公司股改完成,无限接近“新三板”挂牌。新的公司形象需要一个新的展示窗口,所以官网改版提上了宣传工作日程。

随着三位得力助手的快速成长,个人工作压力得到极大限度的缓解,得以抽身回归技术探索工作,最终,受不了董秘大人的威逼利诱,此项工作最终于2016年6月29日奠基动工。

技术选型

得益于十年技术生涯,练就了“浑身刀,没把利”,虽然了解的技术屈指不可数,可惜没一项精通。幸运的是妈妈挑了个好季节把我生下来,正是董秘大人说的“处女座的强迫症”让我暂时免于失业,所产出的寥寥数项小打小闹的软件作品还勉强敢承认是自己所为。

长时间的耳濡目染,对建站相关技术也有一定的了解,此处罗列一下本次建站所用到的主要前后端技术,也顺道向这些开源技术的程序猿、攻城狮们表示感谢。

WordPress

两年前就曾经为WordPress的大名所贯耳

WordPress是一个注重美学、易用性和网络标准的个人信息发布平台。WordPress虽为免费的开源软件,但其价值无法用金钱来衡量。

WordPress的图形设计在性能上易于操作、易于浏览;在外观上优雅大方、风格清新、色彩诱人。

使用WordPress可以搭建功能强大的网络信息发布平台,但更多的是应用于个性化的博客。针对博客的应用,WordPress能让您省却对后台技术的担心,集中精力做好网站的内容。

以上内容摘录自WordPress中文网站

WordPress给人的形象首先是一个博客平台,当时曾策划过在公司内部用她来搭建一个技术博客站点,用于鼓励大家分享技术和经验,因各方面的原因最终搁浅了。当年也只停留在“部署成功”的状态,受更重要的工作内容牵绊,也没有好好摸索一下如何用起来以及怎样更好地用起来。但是“美观、易用”确实没有让人失望。

或许WordPress的野心不仅仅在于“个人信息发布”,当前,WordPress官网的介绍已经变化为

WordPress is web software you can use to create a beautiful website, blog, or app. We like to say that WordPress is both free and priceless at the same time.

以上内容摘录自WordPress官方网站

她已经摇身一变,成了一个建站系统。最为一个开源软件,WordPress的进取是多数技术宅所喜闻乐见的,窃以为虽然未为开源社区做过什么贡献,但是选用一项开源产品也是对她的认同和支持,也容易受其感染地认为“能力越大,责任就越大”。

纵使不愿意承认,但我确实是一个很固执的人。有了WordPress,自然其他CMS就入不了法眼,最后义无反顾地选择了她作为建站系统。

在我部署测试环境时WordPress处于4.5.2版本阶段,为了省时省力,直接在WordPress中文网站上下载了集成中文的软件包,到生产环境部署的时候,刚好遇上版本升级,也选择了新版本4.5.3。

ScrollReveal

提供元素随页面滚动初次被显示时的动画效果。

Easy scroll animations for web and mobile browsers.

Superslides

提供图片满页轮播功能。

Superslides is a full screen, hardware accelerated slider for jQuery. I wasn’t happy with the state of full screen sliders, so naturally I built my own.

FlexSlider

另一个响应式轮播组件

An awesome, fully responsive jQuery slider plugin.

Magnific-Popup

弹出层效果组件

Fast, light and responsive lightbox plugin, for jQuery and Zepto.js.

Bootstrap

全宇宙范围内应用最广的前端框架

Bootstrap is the most popular HTML, CSS, and JS framework for developing responsive, mobile first projects on the web.

主题的选择

部署完WordPress,首要的任务当然是选一款比较贴合心意的主题,由于董秘大人没有明确的指示,这个过程只能靠个人喜好了。经过一轮预览、测试、筛选,与我审美高度共鸣的有“Zerif Lite – by ThemeIsle”和“Sydney – by aThemes”两款。

Zerif内置了一个完整的首页,自定义功能也非常丰富,但自带插件很多,使用起来显得比较臃肿。所以最后被放弃了,但其首页的设计给了我太多的参考,所以后来基于Sydney定义首页时,前端效果和自定义项的设计抄袭了很多Zerif的内容。

Sydney给人的感觉非常清爽,也提供了董秘大人钦点的图片轮播功能,个人最满意的是Sydney提供的图片轮播是基于Superslides的全屏轮播,对于内容捉襟见肘的站点来说,是把空虚填满的一把利器。比较坑的是Sydney提供的响应式导航菜单布局偏凌乱,效果也并非我的菜,这里的内容是被改得最狠的。

对主题的主要修改

导航菜单

导航菜单在横向1024px分辨率下仍处于折叠状态,这是我不能接受的,而且折叠状态下的左右对齐效果也很粗糙,所以这里是对原主题风格修改得最大刀阔斧的地方。

1. 初始保持为绝对定位

由于公司LOGO配色的限制,导航菜单需要在初始状态保持绝对定位,并以半透白色为背景色,可以利用全屏轮播图片(对首页)或页顶静态图片(非首页)来衬托。

2. 修改响应式定义

横向1024px下显示为展开状态,并简化导航栏布局,对齐左边的LOGO与右边的菜单文字(或折叠状态下的菜单按钮)的水平位置。

定义首页

首页以分段布局设计,全屏轮播置顶,“产品与服务”、“经典案例”、“最新动态”、“联系方式”四段跟随其后,最后是在页脚提供站点导航以及公司落款、备案号等信息,在此设计思想的基础上,尽量提供丰富的自定义功能。

页脚内容

据董秘家领导介绍,页脚提供导航区域有助于搜索引擎对站点的优化,其实也是目前大多数站点的设计潮流,所以此处也紧随潮流追了一把。Sydney主题本身也提供也页脚小工具区域,但是最多只有4列,效果比较空虚。所以做了一些修改,可以提供最多7列导航菜单,并把列宽定义从Bootstrap的“col-md-x”切换为大屏幕(横向1024px及以上)显示为6列,小屏幕则同行显示3列。

页面最底部的署名和备案号展示区域则偷懒直接提供HTML代码输入的方式作为自定义选项。

对核心代码的修改

在建站调试过程遇到一些问题,未能简单通过主题或插件的方式解决,最后不得以,对WordPress的核心代码做了一些修改,此处也记录下来,一旦以后更新了WordPress,可能需要找到相应的地方作类似的修改。

1. mediaelement-and-player.min.js

文件路径:wordpress/wp-includes/js/mediaelement/mediaelement-and-player.min.js

WordPress内置的mediaelement组件在高DPI分辨率的屏幕下嵌入到iframe中运行时,全屏会出现闪退现象,此处代码段:

b.isInIframe&&setTimeout(function d(){if(b.isNativeFullScreen){var c=window.devicePixelRatio||1,e=.002,f=c*a(window).width(),g=screen.width,h=c*f;Math.abs(g-f)>Math.abs(g-h)&&(f=h);var i=Math.abs(g-f),j=g*e;i>j?b.exitFullScreen():setTimeout(d,500)}},1e3);

被修改为:

b.isInIframe&&setTimeout(function d(){if(b.isNativeFullScreen){var c=window.devicePixelRatio||1,e=.002,f=a(window).width(),g=screen.width,h=c*f;Math.abs(g-f)>Math.abs(g-h)&&(f=h);var i=Math.abs(g-f),j=g*e;i>j?b.exitFullScreen():setTimeout(d,500)}},1e3);

删除了f=c*a(window).width()中的c*两个字符,测试可以解决问题,但是否会引起其他兼容性问题尚未得知,未仔细研读完整代码。

2. script-loader.php(不再需要)

文件路径:wordpress/wp-includes/script-loader.php

WordPress在仪表盘(站点管理后台)中从Google服务器引入了“Open Sans”的CSS定义,其地址为:

https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,300,400,600&subset=$subsets

由于众所周知的原因,会造成相关页面缓慢,从网络获知可以使用360提供的替代服务:

http://fonts.useso.com/css?family=Open+Sans:300italic,400italic,600italic,300,400,600&subset=$subsets

但测试结果也很不稳定,最后决定先下载样式表内容,保存为本地CSS文件:

wordpress/wp-includes/css/open-sans-font.css

再行静态引入,故将script-loader.php文件内function wp_default_styles下的以下行:

$open_sans_font_url = "https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,300,400,600&subset=$subsets";

替换为

$open_sans_font_url = "/wp-includes/css/open-sans-font.css";

3. post.php(已更新)

文件路径:wordpress/wp-includes/post.php

WordPress预置的文章置顶功能仅用于在站点首页显示,在分类目录中的列表不会将置顶文件排序到最前,仍然采用基于文章发表时间的倒序排列。几经波折,利用取巧的方法实现了完整的置顶功能支持,详情此处留个悬念暂且不表,对post.php文件的修改是为了在置顶/取消置顶的代码中加入Hook机制,然后在插件中安装钩子,来添加文章被置顶/取消置顶后的附加操作。

此文件修改了两处内容,第一处为function stick_post,添加行

do_action('stick_post', $post_id);

变成:

function stick_post( $post_id ) {
	$stickies = get_option('sticky_posts');

	if ( !is_array($stickies) )
		$stickies = array($post_id);

	if ( ! in_array($post_id, $stickies) )
		$stickies[] = $post_id;

	update_option('sticky_posts', $stickies);
  do_action('stick_post', $post_id);
}

第二处为function unstick_post,添加行

do_action('unstick_post', $post_id);

变成:

function unstick_post( $post_id ) {
	$stickies = get_option('sticky_posts');

	if ( !is_array($stickies) )
		return;

	if ( ! in_array($post_id, $stickies) )
		return;

	$offset = array_search($post_id, $stickies);
	if ( false === $offset )
		return;

	array_splice($stickies, $offset, 1);

	update_option('sticky_posts', $stickies);
  do_action('unstick_post', $post_id);
}

新的方法:
只需要修改function sanitize_post_field,在函数起始处添加新行:

 $value = apply_filters('pre_sanitize_post_field', $value, $field, $post_id, $context);

变成:

function sanitize_post_field( $field, $value, $post_id, $context = 'display' ) {
	$value = apply_filters('pre_sanitize_post_field', $value, $field, $post_id, $context);
	
	$int_fields = array('ID', 'post_parent', 'menu_order');
	if ( in_array($field, $int_fields) )
		$value = (int) $value;
	...
}

4. class-wp-posts-list-table.php(不再需要)

文件路径:wordpress/wp-admin/includes/class-wp-posts-list-table.php
同样是为了实现完整的置顶功能,需要修改此文件来让仪表板的“所有文章”页面正确显示已置顶文章的真实发表时间。(也许你已经能猜出来了,在下所谓“完整的置顶功能”实际上是通过修改文章发表时间来让置顶文章始终排在前面的,这也带来了不小的副作用,此处继续留悬念)。

修改此文件内的function single_row,替换以下行:

$post = get_post( $post );

$post = get_post( $post, OBJECT, 'edit' );

 

以上为对WordPress核心代码中的4个文件修改情况记录,希望能供以后的站长们参考。

插件开发

WordPress提供的插件技术配合Hook机制可以实现很多订制功能,此次建站用这把牛刀小试了一下,写了几个插件,不求专业不求大全,仍然贯彻懒人思维“够用即可”。

1. Relative Media URL Support

按WordPress的设计,文章写作时插入的图片等媒体文件会引用绝对URL,此插件用于在添加媒体文件到文章时,自动将URL转换为相对地址,方便日后如果需要将站点迁移到其他地方或更换域名时,不会出现媒体地址错误的情况。

/*
Plugin Name: Relative Media URL Support
Version: 0.0.1
Author: Billy Poon
Author Email: panqb@uctrl.cn
License: Free
*/


function filter_media_send_to_editor($html, $id, $attachment) {
 $array=null;
 $len=preg_match('/"(http:\/\/\S+)"/i',$html,$array);
 //var_dump($len, $html, $array);
 if ($len>0) {
  $home_url=home_url();
  //var_dump($home_url);
  foreach($array as $i) {
   if (strpos($i,$home_url) === 0) {
    $nval=wp_make_link_relative($i);
    $html=str_replace($i, $nval, $html);
   }
  }
 }
 
 return $html;
}

add_filter('media_send_to_editor', 'filter_media_send_to_editor', 99, 3);

2. Post Meta Script Support

为了支持在文章中加载额外的CSS或JS代码,制作了Post Meta Script Support插件:

<?php
/*
Plugin Name: Post Meta Script Support
Version: 0.0.1
Author: Billy Poon
Author Email: panqb@uctrl.cn
License: Free
*/

function filter_enqueue_scripts_from_post_meta() {
 global $post;

 if (is_single() || is_page()) {
  $array=@get_post_meta($post->ID, 'pmss.uctrl.cn', false);

  if (!is_array($array)) return;

  foreach($array as $i) {
   if (empty($i)) continue;
   if (!($v=@json_decode($i))) {
    if (preg_match('/.+((\.js)|(\.css))$/i', $i)) {
     $pos = strrpos($i, '/');
     if (false === $pos) {
      $id = $i;
     } else {
      $id = substr($i, $pos+1);
     }
     if (empty($id)) continue;

     $v = new stdClass();
     $v->id = str_replace('.','-',$id);
     $v->url = $i;
    }
   }
   if (!$v) continue;

   $iscss=true;
   if (isset($v->type)) {
    if ($v->type=='js') $iscss=false;
   } else if (isset($v->url)) {
    if (strtolower(substr($v->url,-3)) == '.js') $iscss=false;
   }

   if (isset($v->url)) {
    if ($iscss) {
     wp_enqueue_style($v->id, $v->url, $v->deps);
    } else {
     wp_enqueue_script($v->id, $v->url, $v->deps);
    }
   } else if (isset($v->source)) {
    if ($iscss) {
     wp_add_inline_style($v->id, $v->source);
    } else {
     wp_add_inline_script($v->id, $v->source);
    }
   }
  }
 }
}

add_action( 'wp_enqueue_scripts', 'filter_enqueue_scripts_from_post_meta',99 );

本插件的使用方法是在文章编辑页面添加名为“pmss.uctrl.cn”的自定义栏目,其值为指向CSS或JS文件的URL即可,完整的用法请自行阅读代码。

3. Sticky Posts Fully Support(已更新)

前文提到,WordPress提供的文章置顶功能只在首页才好用,分类目录中的文章列表仍然顽固地根据发表时间倒序排列。为了实现完整的文章置顶功能,有两种解决方案,其一是修改WordPress的文章搜索逻辑,将置顶文章排在第一页的顶端,但这方案涉及的核心代码改动面积太大,耗时耗力也非常高,故被放弃;另一种方案是在文章置顶时将发表时间修改为一个非常大的值,使得排序时能排在前面,显示时再把时间转换回真正的发表时间,使得其可以正确显示,由于当前版本的PHP能处理的最大时间值是“2038-01-19 03:14:07”,此方案带来的副作用是缩短了文章发表时间的支持期限。

建站时综合各方面情况的考虑,网站设计寿命为2014年初至2025年底,2026~2037年的发表时间用于表示置顶文章,即发表时间为2014年的文章在数据库中实际存放的发表时间为2026年的同一天,这样就能保证置顶文章在列表中处于靠前位置。如果文章的发表时间被设置为2013年或以前,或者2025年或以后,则会被自动前推或后移到有效的时间段。

<?php
/*
Plugin Name: Sticky Posts Fully Support
Version: 0.0.1
Author: Billy Poon
Author Email: panqb@uctrl.cn
License: Free
*/

/*
Firstly, you need to modify two native files:

1. wordpress/wp-includes/post.php
1) function stick_post, append the following line:
do_action('stick_post', $post_id);
2) function unstick_post, append the following line:
do_action('unstick_post', $post_id);

2. wordpress/wp-admin/includes/class-wp-posts-list-table.php
function single_row, replace the following line:
$post = get_post( $post );
by:
$post = get_post( $post, OBJECT, 'edit' );

Secondly, if you want to hide the fake post date (+7 years)
in you post meta, just modify the template in your theme files,
make the modified date displaying instead, or simply, just tell
"sticked".

Now enable this plugin and enjoy your "Sticky Posts Fully Support".
*/

function on_saved_post($post_id) {
 if (is_sticky($post_id)) {
  on_stick_post($post_id);
 }
}
add_action('save_post', 'on_saved_post');

function get_sticky_date($date, $for_sticky) {
 $pmin = 2014;
 $smax = 2037;
 if (($smax - $pmin)%2 == 0) --$smax;

 $range = ($smax - $pmin + 1) / 2;
 $pmax = $pmin + $range - 1;
 $smin = $pmax + 1;

 if (is_string($date)) $date = strtotime($date);
 if (!is_numeric($date)) $date = time();

 while(date('Y', $date) < $pmin) $date=strtotime('+1 years', $date);
 while(date('Y', $date) > $smax) $date=strtotime('-1 years', $date);

 $year = date('Y', $date);
 if ($for_sticky && $year < $smin) {
  $date = strtotime("+{$range} years", $date);
 }
 if (!$for_sticky && $year > $pmax) {
  $date = strtotime("-{$range} years", $date);
 }

 $local = date('Y-m-d H:i:s', $date);
 return array(
  $local,
  get_gmt_from_date($local),
 );
}

function on_stick_post($post_id) {
 global $wpdb;

 $row = $wpdb->get_row("select post_status,post_date from {$wpdb->posts} where ID={$post_id}", ARRAY_A);

 if (!$row || !in_array($row['post_status'], array('publish','future'))) return;

 $array = get_sticky_date($row['post_date'], true);

 $wpdb->update($wpdb->posts, array(
  'post_date' => $array[0],
  'post_date_gmt' => $array[1],
  'post_status' => 'publish',
 ), array( 'ID' => $post_id));

 wp_cache_delete($post_id, 'posts');
}

add_action('stick_post', 'on_stick_post');

function on_unstick_post($post_id) {
 global $wpdb;

 $row = $wpdb->get_row("select post_status,post_date from {$wpdb->posts} where ID={$post_id}", ARRAY_A);
 if (!$row || !in_array($row['post_status'], array('publish','future'))) return;

 $array = get_sticky_date($row['post_date'], false);

 $wpdb->update($wpdb->posts, array(
  'post_date' => $array[0],
  'post_date_gmt' => $array[1],
  'post_status' => 'publish',
 ), array( 'ID' => $post_id));

 wp_cache_delete($post_id, 'posts');
}

add_action('unstick_post', 'on_unstick_post');

function on_sanitize_post_date($value) {
 $array = get_sticky_date($value, false);
 return $array[0];
}

add_filter('edit_post_date', 'on_sanitize_post_date');
add_filter('post_date', 'on_sanitize_post_date');

function on_get_post_date($value, $d, $post) {
 $array = get_sticky_date($post->post_date, false);

 if ( '' == $d ) {
  $value = mysql2date( get_option( 'date_format' ), $array[0] );
 } else {
  $value = mysql2date( $d, $array[0] );
 }

 return $value;
}
add_filter('get_the_date', 'on_get_post_date', 10, 3);

更新为:

<?php
/*
Plugin Name: Sticky Posts Fully Support
Version: 0.0.2
Author: Billy Poon
Author Email: panqb@uctrl.cn
License: Free
*/


/*
Firstly, you need to modify this native file:

> wordpress/wp-includes/post.php
>> function sanitize_post_field, add the following line to the begining:
	$value = apply_filters('pre_sanitize_post_field', $value, $field, $post_id, $context);

Now enable this plugin and enjoy your "Sticky Posts Fully Support".
*/

function _spfs_on_save_post($post_id) {
	if (is_sticky($post_id)) {
		_spfs_on_stick_post($post_id);
	}
}
add_action('save_post', '_spfs_on_save_post');

function _spfs_on_update_option_sticky_posts($old_value, $value) {
	$sticks = array_diff($value, $old_value);
	$unsticks = array_diff($old_value, $value);

	foreach ($sticks as $i) _spfs_on_stick_post($i);
	foreach ($unsticks as $i) _spfs_on_unstick_post($i);
}

add_action('update_option_sticky_posts', '_spfs_on_update_option_sticky_posts', 10, 2);

function get_sticky_date($date, $for_sticky) {
	$pmin = 2014;
	$smax = 2037;
	if (($smax - $pmin)%2 == 0) --$smax;
	
	$range = ($smax - $pmin + 1) / 2;
	$pmax = $pmin + $range - 1;
	$smin = $pmax + 1;

	if (is_string($date)) $date = strtotime($date);
	if (!is_numeric($date)) $date = time();
	
	while(date('Y', $date) < $pmin) $date=strtotime('+1 years', $date);
	while(date('Y', $date) > $smax) $date=strtotime('-1 years', $date);
	
	$year = date('Y', $date);
	if ($for_sticky && $year < $smin) {
		$date = strtotime("+{$range} years", $date);
	}
	if (!$for_sticky && $year > $pmax) {
		$date = strtotime("-{$range} years", $date);
	}

	$local = date('Y-m-d H:i:s', $date);
	return array(
		$local,
		get_gmt_from_date($local),
	);
}

function _spfs_on_stick_post($post_id) {
	global $wpdb;

	$row = $wpdb->get_row("select post_status,post_date from {$wpdb->posts} where ID={$post_id}", ARRAY_A);

	if (!$row || !in_array($row['post_status'], array('publish','future'))) return;
	
	$array = get_sticky_date($row['post_date'], true);
 
	$wpdb->update($wpdb->posts, array(
		'post_date'		 => $array[0],
		'post_date_gmt' => $array[1],
		'post_status'	 => 'publish',
		), array( 'ID' => $post_id));

	wp_cache_delete($post_id, 'posts');
}

function _spfs_on_unstick_post($post_id) {
	global $wpdb;
	
	$row = $wpdb->get_row("select post_status,post_date from {$wpdb->posts} where ID={$post_id}", ARRAY_A);
	if (!$row || !in_array($row['post_status'], array('publish','future'))) return;
	
	$array = get_sticky_date($row['post_date'], false);

	$wpdb->update($wpdb->posts, array(
		'post_date'		 => $array[0],
		'post_date_gmt' => $array[1],
		'post_status'	 => 'publish',
		), array( 'ID' => $post_id));

	wp_cache_delete($post_id, 'posts');
}

function _spfs_on_pre_sanitize_post_field($value, $field, $post_id, $context) {
	if ('post_date'==$field || 'post_date_gmt' == $field) {
		$array = get_sticky_date($value, false);
		if ($array) $value = $array[0];
	}

	return $value;
}

add_filter('pre_sanitize_post_field', '_spfs_on_pre_sanitize_post_field', 10, 4);

function _spfs_on_pre_post_link($permalink, $post, $leavename) {
	if (is_sticky($post->ID) && strpos($permalink, '%year%') !== false) {
		$array = get_sticky_date($post->post_date, true);
		$year = date('Y', $array[2]);

		return str_replace('%year%', $year, $permalink);
	}
	
	return $permalink;
}

add_filter('pre_post_link', '_spfs_on_pre_post_link', 1, 3);

后记

站点主体内容在2016年7月19日基本完工,本文的主体撰写也工作也做2016年7月20日完成。一方面当前站点给人的感觉还是比较粗糙,特别是文章阅读页面;另一方面董秘大人也是一位强迫症患者,还在不断地给我提难题,所以估计还需要不少功夫继续调整,如果后续工作有记录下来的必要会在文末继续追加。

–THE GORGEOUS SPLIT LINE–

彻底禁用Google Open Sans Font@2016-07-27

某时猛然醒悟,前文关于“script-loader.php”的修改其实是一种错误的解决方案,只保存CSS文件无法改变对字体文件的引用,访问相关页面时仍然需要从Google之类的远程服务器下载对应的字体文件,没有从根本上解决问题。以下链接介绍的方法可以彻底禁用这些字体:

WordPress 后台禁用Google Open Sans字体,加速网站