1. 前言

百度的熊掌号11月份就发布了,当我准备搞的时候已经黄花菜都凉了,百度移动端内容早就被无数站长的内容覆盖,为了迎合一下热点,本着提升点网站流量的目的,开始了 ghost 博客无比艰辛的改造过程。

2. 高兴的太早了

当注册审核通过之后,查看熊掌号后台,发现改造过程貌似非常简单,对于我这种前端门外汉来说除了校验工具的校验过程让人有点费解之外,其它的都比叫容易操作。

首先,粉丝关注改造。主要的目的是显示一个站长卡牌,让别人可以方便的关注。

关注图片

其次,结构化改造。主要的目的就是便于百度搜集网站的数据,并在移动端展示。

熊掌号页面

半个小时搞定后,在验证页面尝试,发现怎么都不成功,但是页面中确有一个奇葩的application/ld+json格式数据。看着能够打开关注的文章页面,所以也就没有在意,以为是百度识别了我添加的application/ld+json数据,并使用脚本自动生成下面的样子,我其实内心还觉得百度太牛叉了,能够自动识别文章标签,真准啊……

{
    "@context": "https://schema.org",
    "@type": "Article",
    "publisher": {
        "@type": "Organization",
        "name": "第二纪元",
        "logo": {
            "@type": "ImageObject",
            "url": "http://wangzhechao.com/favicon.ico",
            "width": 60,
            "height": 60
        }
    },
    "author": {
        "@type": "Person",
        "name": "W_Z_C",
        "image": {
            "@type": "ImageObject",
            "url": "//www.gravatar.com/avatar/0d4bf6032c133d0f1fa9d1506887491e?s=250&d=mm&r=x",
            "width": 250,
            "height": 250
        },
        "url": "http://wangzhechao.com/author/w_z_c/",
        "sameAs": [
            "http://wangzhechao.com"
        ]
    },
    "headline": "Jmeter 安装教程",
    "url": "http://wangzhechao.com/jmeter-an-zhuang-jiao-cheng/",
    "datePublished": "2017-12-14T14:04:56.000Z",
    "dateModified": "2017-12-14T14:12:58.000Z",
    "image": {
        "@type": "ImageObject",
        "url": "http://wangzhechao.com/content/images/2017/12/jmeter-2-1.png",
        "width": 700,
        "height": 385
    },
    "keywords": "安装教程, Jmeter, HTTP, HTTP测试, HTTP压力测试, Jmeter安装, 压力测试, 性能测试",
    "description": "这一阵工作中涉及到HTTP接口测试的需求,主要做一些简单的压力测试和性能测试。用nodejs手撸了一份,运行后发现离自己想要的还是有些差距的,上网搜索研究了一下相关的测试工具,接下来准备写几篇文章总结一下使用心得,本篇文章算是开篇,介绍一下Jmeter的安装使用。",
    "mainEntityOfPage": {
        "@type": "WebPage",
        "@id": "http://wangzhechao.com/"
    }
}

一直到我闲着蛋疼,查看了其他站长的改造后的效果,和我的完全不一样,然后潜心花费了几个小时研究怎样改造,接下来记录一下其中思考的过程,以儆效尤!

3. 苦逼的开始

第一步,因为开始以为那些数据是百度生成的,所以想要验证一下,将上次添加所有熊掌号相关的添加内容全部删除后,发现这个结构数据仍然存在,百度一下,发现这个结构不是百度原创,而是一种标准,名词叫做:JSON-LD,是一种基于JSON表示和传输互联数据(Linked Data)的方法。

受教育之后,自然想到ghost博客本身是支持JSON-LD的输出,谷歌之后,发现因为ghost博客支持google移动端页面,所以有AMP的支持,具体设置可以参考这篇文章

我突然感觉到找到组织了,赶快去博客后台,将amp插件禁用,保存,然后人就懵逼了。

applist

因为AMP这个插件应用默认根本没有开启

wtf

4. 禁用AMP之路

继续搜索,查找AMP相关,怎样禁用ghost博客的JSON-LD结构输出,只有禁止后才能添加百度熊掌号的结构。

还真让我搜到了一篇佳作:Google AMP is shit!

感觉又迎来了希望,立刻远程登录ssh,进入/core/server/config/index.js目录,vim 打开,如下:

var Nconf = require('nconf'),
    path = require('path'),
    _debug = require('ghost-ignition').debug._base,
    debug = _debug('ghost:config'),
    localUtils = require('./utils'),
    env = process.env.NODE_ENV || 'development',
    _private = {};

_private.loadNconf = function loadNconf(options) {
    debug('config start');
    options = options || {};

    var baseConfigPath = options.baseConfigPath || __dirname,
        customConfigPath = options.customConfigPath || process.cwd(),
        nconf = new Nconf.Provider();

    /**
     * no channel can override the overrides
     */
    nconf.file('overrides', path.join(baseConfigPath, 'overrides.json'));

    /**
     * command line arguments
     */
    nconf.argv();

    /**
     * env arguments
     */
    nconf.env({
        separator: '__'
    });

    nconf.file('custom-env', path.join(customConfigPath, 'config.' + env + '.json'));
    nconf.file('default-env', path.join(baseConfigPath, 'env', 'config.' + env + '.json'));
    nconf.file('defaults', path.join(baseConfigPath, 'defaults.json'));

    /**
     * transform all relative paths to absolute paths
     * transform sqlite filename path for Ghost-CLI
     */
    nconf.makePathsAbsolute = localUtils.makePathsAbsolute.bind(nconf);
    nconf.isPrivacyDisabled = localUtils.isPrivacyDisabled.bind(nconf);
    nconf.getContentPath = localUtils.getContentPath.bind(nconf);
    nconf.sanitizeDatabaseProperties = localUtils.sanitizeDatabaseProperties.bind(nconf);
    nconf.doesContentPathExist = localUtils.doesContentPathExist.bind(nconf);

    nconf.sanitizeDatabaseProperties();
    nconf.makePathsAbsolute(nconf.get('paths'), 'paths');
    nconf.makePathsAbsolute(nconf.get('database:connection'), 'database:connection');

    /**
     * Check if the URL in config has a protocol
     */
    nconf.checkUrlProtocol = localUtils.checkUrlProtocol.bind(nconf);
    nconf.checkUrlProtocol();

    /**
     * Ensure that the content path exists
     */
    nconf.doesContentPathExist();

    /**
     * values we have to set manual
     */
    nconf.set('env', env);

    // Wrap this in a check, because else nconf.get() is executed unnecessarily
    // To output this, use DEBUG=ghost:*,ghost-config
    if (_debug.enabled('ghost-config')) {
        debug(nconf.get());
    }

    debug('config end');
    return nconf;
};

module.exports = _private.loadNconf();
module.exports.loadNconf = _private.loadNconf;

请问AMP在哪里?

wtf

难道是ghost没事总升级,这篇文章的内容太久了?立刻git下载ghost源码,搜索所有amp单词相关信息,果然被我找到了类似的配置信息。

在config目录下的overrides.json配置文件:

    "apps": {
        "internal": [
            "private-blogging",
            "subscribers",
            "amp"
        ]
    },
    "routeKeywords": {
        "tag": "tag",
        "author": "author",
        "page": "page",
        "preview": "p",
        "private": "private",
        "subscribe": "subscribe",
        "amp": "amp",
        "primaryTagFallback": "all"
    },
    "slugs": {
        "reserved": ["admin", "app", "apps", "categories",
            "category", "dashboard", "feed", "ghost-admin", "login", "logout",
            "page", "pages", "post", "posts", "public", "register", "setup",
            "signin", "signout", "signup", "user", "users", "wp-admin", "wp-login"],
        "protected": ["ghost", "rss", "amp"]
    },

我立刻就被自己感动了,我实在是太聪明了,按照他的方法删除amp相关信息,重启ghost,我满心期待的刷新了网页……

不科学啊

一切仍然涛声依旧......

5. 老老实实读源码

是时候秀出自己的10年功力了,读源码!!!

多亏我以前研究过ghost源码,如今梅开二度,应该没有任何难度。老司机上路,然后我发现ghost博客开发者的成果了,和我上一次研究的代码貌似完全不一样啊……

啥都别说了,冷静一下的我,灵机一动抱着尝试的态度所搜了一下application/ld+json关键字。

    return getMetaData(dataRoot, dataRoot)
        .then(function handleMetaData(metaData) {
            debug('end fetch');

...
                if (!_.includes(context, 'paged') && useStructuredData) {
                    head.push('');
                    head.push.apply(head, finaliseStructuredData(metaData));
                    head.push('');

                    if (metaData.schema) {
                        head.push('<script type="application/ld+json">\n' +
                            JSON.stringify(metaData.schema, null, '    ') +
                            '\n    </script>\n');
                    }
                }

...
};

喜从天降啊,仔细研究,发现具体信息是从getMetaData函数返回,搜索发现具体的schema信息是从getSchema函数返回的。

return Promise.props(getImageDimensions(metaData)).then(function () {
    metaData.structuredData = getStructuredData(metaData);
    metaData.schema = getSchema(metaData, data);

    return metaData;
}).catch(function (err) {
    common.logging.error(err);
    return metaData;
});

继续搜索,前方遇见schema大军,各种schema数据。

function getSchema(metaData, data) {
    if (!config.isPrivacyDisabled('useStructuredData')) {
        var context = data.context ? data.context : null;
        if (_.includes(context, 'post') || _.includes(context, 'page') || _.includes(context, 'amp')) {
            return getPostSchema(metaData, data);
        } else if (_.includes(context, 'home')) {
            return getHomeSchema(metaData);
        } else if (_.includes(context, 'tag')) {
            return getTagSchema(metaData, data);
        } else if (_.includes(context, 'author')) {
            return getAuthorSchema(metaData, data);
        }
    }
    return null;
}

包括post、home、tag、author等等,每个都会生成一种schema数据,格式就是JSON-LD结构,本来想把这了直接改成百度想要的格式,但是后来仔细一看,发现配置中有个isPrivacyDisabled函数,该函数返回true,则getSchema函数返回null,页面中肯定不会再有JSON-LD的结构了。

查看该函数:

exports.isPrivacyDisabled = function isPrivacyDisabled(privacyFlag) {
    if (!this.get('privacy')) {
        return false;
    }

    // CASE: disable all privacy features
    if (this.get('privacy').useTinfoil === true) {
        // CASE: you can still enable single features
        if (this.get('privacy')[privacyFlag] === true) {
            return false;
        }

        return true;
    }

    return this.get('privacy')[privacyFlag] === false;
};

也就是说配置中会有一个privacy的配置项,该配置项中的useStructuredData字段如果为false,则不会输出JSON-LD的结构。

总结一下,如果想删除页面中的结构数据,只需要在配置中添加下面的配置即可。

privacy: {
  useStructuredData: false
}

立刻加上,圆满完成任务。google搜索ghost privacy useStructuredData,发现ghost官方文档提到了这个配置。

这回再次证明:研究帮助文档的重要性

ghost自带的JSON-LD数据已经消除,是时候开始真正的熊掌号页面改造了。

6. 熊掌号页面改造

6.1 粉丝页面改造

ghost博客的后台其实已经增加了头部数据添加框,可以直接在其中加入粉丝改造相关的内容。

blog_header

如果你不喜欢,也可以手动在default.hbs模板文件中增加。本人添加的具体位置如下:

...

{{ghost_head}}

    <script src="//msite.baidu.com/sdk/c.js?appid=xxxxxxx"></script>

</head>
<body class="{{body_class}}">

    <div class="site-wrapper">

        {{!-- All the main content gets inserted here, index.hbs, post.hbs, etc --}}
        {{{body}}}


        <script>cambrian.render('tail')</script>
        
...

6.2 结构化改造

因为添加改造的内容是与文章数据相关,所以这里将改造的数据添加到了post.hbs模板文件中,具体可以放到disqus评论上方(默认disqus评论是注释的)。

<link rel="canonical" href='{{url absolute="true"}}'/>

<script type="application/ld+json">
    {
        "@context": "https://ziyuan.baidu.com/contexts/cambrian.jsonld",
        "@id": "{{url absolute='true'}}",
        "appid": "xxxxxxxx",
        "title": "{{title}}",
        "images": ["{{img_url feature_image absolute='true'}}"],
        "description": "{{excerpt characters='120'}}",
        "pubDate": "{{date published_at format='YYYY-MM-DDTHH:mm:ss'}}",
        "upDate": "{{date updated_at format='YYYY-MM-DDTHH:mm:ss'}}"                
    }
</script>

这里面的图像列表我选择了ghost每篇文章的特色图片,你也可以换成别的图片内容,具体需要查看ghost的主题帮助文档。

最后可以去在线校验工具输入链接和文章的最终html代码进行验证。

验证成功

7. 总结

啥问题就怕刨根问底儿……