Polymer를 사용하여 github page 꾸미기

2017-07-17 17:40 KST

최근에 AngularReact의 사용 하면서 이전의 jQuery와 그 외 다수의 플러그인을 사용할 때보다 많이 빠른 것을 느끼면서 지금 사용하고 있는 이 블로그도 빠른 웹을 위해 변경할 필요가 있다고 느껴서 Angular와 React는 이미 사용해봤으므로 Polymer를 사용하여 블로그를 변경해보았습니다. 간단한 소개와 Polymer를 Github Page에 적용 시 문제점 등을 공유해보려 합니다.

언젠간 Polymer 사용하기라는 제목으로 Polymer에 대한 글을 자세히 쓰겠죠..

Polymer

Polymer는 사용자 정의 재사용 가능한 HTML 요소를 만들고이를 사용하여 효율적이고 유지 보수가 가능한 앱을 제작하는 데 도움이되는 JavaScript 라이브러리입니다.

구글 번역이 저보다 더 뛰어나군요.
어쨋든 React와 Angular하고 비슷하게? 재사용 가능한 요소를 만들고 사용할 수 있게 도움을 주는 JavaScript 라이브러리 입니다.

필요한 Component가 있으면 WebComponent에서 검색 후 bower install로 설치해서 사용하면 됩니다.

Github Page에서 Polymer

Github Page의 특정상 일반적인 Polymer 개발과는 조금 다르게 생각을 해야 했습니다.

  • Polymer의 기능으로 route를 변경했을 때 location이 변경된 상태(ex: https://jhbae200.github.io/home)에서 새로고침 시 404페이지로 가버리는 것
  • markdown으로 작성한 포스트의 연결
  • 다른 정적페이지..?
  • 일반 github.io 도메인으로는 h2 프로토콜이 아니므로 PWA(Progresive Web App)을 적용하지 못함
  • polymer build로 bundle, minify, polyfill 작업 후 나오는 index.html에서 jekyll parsing 오류
  • SEO 적용

일단 디렉토리의 구조는 이렇습니다.

.
├─images
│  └─manifest
├─paginate
├─src
│  ├─my-about
│  ├─my-app
│  ├─my-home
│  ├─my-pagination
│  └─my-post
├─test
├─_includes
├─_layouts
└─_posts

하나하나 답변을 달아보자면..

Polymer의 기능으로 route를 변경했을 때 location이 변경된 상태(ex: https://jhbae200.github.io/home)에서 새로고침 시 404페이지로 가버리는 것

app-location Component에서 use-hash-as-path 옵션으로 hashtag를 사용해서 페이지 이동을 하면 location이 변경되어도 index.html을 불러옵니다.
페이지 내에서 열심히 돌려도 github의 404페이지로 가지 않겠죠.ㅎㅎ

/src/my-app/index.html

---
---
<link rel="import" href="/bower_components/polymer/polymer-element.html">
<link rel="import" href="/bower_components/app-layout/app-drawer/app-drawer.html">
<link rel="import" href="/bower_components/app-layout/app-drawer-layout/app-drawer-layout.html">
<link rel="import" href="/bower_components/app-layout/app-header/app-header.html">
<link rel="import" href="/bower_components/app-layout/app-header-layout/app-header-layout.html">
<link rel="import" href="/bower_components/app-layout/app-scroll-effects/app-scroll-effects.html">
<link rel="import" href="/bower_components/app-layout/app-toolbar/app-toolbar.html">
<link rel="import" href="/bower_components/app-route/app-location.html">
<link rel="import" href="/bower_components/app-route/app-route.html">
<link rel="import" href="/bower_components/iron-pages/iron-pages.html">
<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
<link rel="import" href="/bower_components/iron-selector/iron-selector.html">
<link rel="import" href="/bower_components/paper-icon-button/paper-icon-button.html">

<link rel="import" href="/src/shared-styles.html">
<link rel="import" href="/src/my-icons.html">
<link rel="import" href="/src/my-search-bar.html">
<link rel="import" href="/src/my-snackbar.html">

<link rel="lazy-import" href="/src/my-home/index.html">
<link rel="lazy-import" href="/src/my-post/index.html">
<link rel="lazy-import" href="/src/my-about/index.html">
<link rel="lazy-import" href="/src/my-view404.html">
<link rel="import" href="/src/my-network-warning.html">

<dom-module id="my-app">
{% raw %}
    <template>
        <style include="shared-styles">
          /*
          something css code
          */
        </style>
        <app-location route="{{route}}" use-hash-as-path=""></app-location>
        <app-route
                route="{{route}}"
                pattern="!/:page"
                data="{{routeData}}"
                tail="{{subroute}}"></app-route>

        <app-drawer-layout fullbleed>
            <!-- Drawer content -->
            <app-drawer id="drawer" slot="drawer">
                <app-toolbar>Menu</app-toolbar>
                <iron-selector selected="[[routeData.page]]" attr-for-selected="name" class="drawer-list"
                               role="navigation">
                    <a name="home" href="#!/home/">
                        <iron-icon icon="my-icons:home"></iron-icon>
                        Home</a>
                    <a name="about" href="#!/about">
                        <iron-icon icon="my-icons:person"></iron-icon>
                        About</a>
                    <a name="feed" href="/feed.xml" target="_blank">
                        <iron-icon icon="my-icons:rss-feed"></iron-icon>
                        Rss Feed</a>
                    <hr>
                    {% endraw %}
                    {% if site.github_username %}
                    <a name="github" href="https://github.com/{{site.github_username}}">
                        <iron-icon icon="my-icons:github"></iron-icon>
                        Github</a>
                    {% endif %}
                    {% if site.email %}
                    <a name="email" href="mailto:{{site.email}}">
                        <iron-icon icon="my-icons:email"></iron-icon>
                        Email</a>
                    {% endif %}
                    {% raw %}
                    <hr>
                    <div class="copyright">© 2016, Jinhwan Bae</div>
                </iron-selector>
            </app-drawer>

            <!-- Main content -->
            <app-header-layout has-scrolling-region>

                <app-header slot="header" condenses reveals effects="waterfall">
                    <app-toolbar>
                        <paper-icon-button icon="my-icons:menu" drawer-toggle></paper-icon-button>
                        <div main-title>Jinhwan Blog</div>
                        <my-search-bar active="{{searchActive}}"></my-search-bar>
                    </app-toolbar>
                </app-header>

                <iron-pages
                        selected="[[page]]"
                        attr-for-selected="name"
                        fallback-selection="view404"
                        role="main"
                        selected-attribute="selected">
                    <my-home name="home" route="[[subroute]]"></my-home>
                    <my-post name="post" route="[[subroute]]"></my-post>
                    <my-about name="about"></my-about>
                    <my-view404 name="view404"></my-view404>
                    <my-network-warning name="network-warning"></my-network-warning>
                </iron-pages>
            </app-header-layout>
        </app-drawer-layout>
    </template>
    {% endraw %}
    <script>
        class MyApp extends Polymer.Element {

            static get is() {
                return 'my-app';
            }

            static get properties() {
                return {
                    page: {
                        type: String,
                        reflectToAttribute: true,
                        observer: '_pageChanged',
                    },
                    oldPage: {
                        type: String,
                    },
                    rootPattern: String,
                    routeData: Object,
                    subroute: String,
                    searchActive: {
                        type: Boolean,
                    },
                };
            }

            static get observers() {
                return [
                    '_routePageChanged(routeData.page)',
                ];
            }

            constructor() {
                super();
                window.performance && performance.mark && performance.mark('my-app.created');
            }

            ready() {
                super.ready();
                // listen for online/offline
                Polymer.RenderStatus.afterNextRender(this, () => {
                    window.addEventListener('online', (e)=>this._notifyNetworkStatus(e));
                    window.addEventListener('offline', (e)=>this._notifyNetworkStatus(e));
                });
            }

            _notifyNetworkStatus() {
                let oldOffline = this.offline;
                this.offline = !navigator.onLine;
                // Show the snackbar if the user is offline when starting a new session
                // or if the network status changed.
                if (this.offline || (!this.offline && oldOffline === true)) {
                    if (!this._networkSnackbar) {
                        this._networkSnackbar = document.createElement('my-snackbar');
                        this.root.appendChild(this._networkSnackbar);
                    }
                    this._networkSnackbar.innerHTML = this.offline ?
                        'You are offline' : 'You are online';
                    this._networkSnackbar.open();
                }
                if (this.offline === false) {
                    this.page = this.oldPage;
                }
            }

            _routePageChanged(page) {
                this.searchActive = false;
                if (page === undefined) {
                    this.set('route.path', '!/home/');
                    this.page = 'home';
                } else {
                    this.page = page || 'home';
                }
            }

            _pageChanged(page) {
                if (this.offline === true || this.page === 'network-warning') {
                    if (this.page !== 'network-warning') {
                        this.oldPage = this.page;
                    }
                    this.page = 'network-warning';
                    return;
                }
                let resolvedPageUrl = this.resolveUrl('/src/my-' + page + '/');
                Polymer.importHref(
                    resolvedPageUrl,
                    null,
                    this._showErrorPage.bind(this),
                    true);
            }


            _showErrorPage() {
                this.page = 'view404';
            }
        }

        window.customElements.define(MyApp.is, MyApp);
    </script>
</dom-module>

markdown으로 작성한 포스트의 연결

markdown으로 작성한 포스트의 layout을 json 형식으로 바꿔버렸습니다.
content에 워낙 다양한 내용이 있어서 그런지 자꾸 parsing 오류가 나길래 url_encode 해서 화면에 표시할 때 decode 했습니다.

/_layouts/post.html

---
---
{
    "title": "{{ page.title | escape }}",
    "date": "{{ page.date | date: '%Y-%m-%d %H:%M KST' }}",
    "modify": "{{ page.update | date: '%Y-%m-%d %H:%M KST' }}",
    "content": "{{ content | url_encode }}",
    "description": "{{ post.content | strip_html | truncatewords: 70 | url_encode }}",
    "url": "{{ page.url }}",
    "writer": "{{ page.writer | escape }}",
    "tags": ["{{ page.tags | join: '","' }}"]
}
그 후 my-post라는 Component를 만들어주고 app-routeiron-ajax를 사용하여 post를 가져왔습니다.

/src/my-post/index.html

<link rel="import" href="/bower_components/polymer/polymer-element.html">
<link rel="import" href="/bower_components/paper-card/paper-card.html">
<link rel="import" href="/bower_components/paper-styles/paper-styles.html">
<link rel="import" href="/bower_components/paper-button/paper-button.html">
<link rel="import" href="/bower_components/app-route/app-route.html">
<link rel="import" href="/bower_components/iron-ajax/iron-ajax.html">

<link rel="import" href="/src/shared-styles.html">

<dom-module id="my-post">
    <template>
        <style include="shared-styles">
            /*
            something css code
            */
        </style>
        <app-route
                route="{{route}}"
                pattern="/:year/:month/:day/:title/"></app-route>
        <iron-ajax auto url="{{page}}" handle-as="json"
                   last-response="{{response}}" on-error="_onError" on-response="_onSuccess"></iron-ajax>
        <paper-card>
            <div class="card-content">
                <div class="post-header"><h1>[[ response.title ]]</h1></div>
                <div class="post-info">
                    <span class="user">[[ response.writer ]]</span><br>
                    <span class="date">[[ response.date ]]</span><br>
                    <span class="modify">Last Update: [[ response.modify ]]</span>
                </div>
                <div id="post-content"></div>
            </div>
        </paper-card>
    </template>
    <script>
        class MyPost extends Polymer.Element {
            static get is() {
                return 'my-post';
            }

            static get properties() {
                return {
                    page: {
                        type: String,
                        reflectToAttribute: true,
                    },
                    route: Object,
                    response: Object,
                    selected: {
                        type: Boolean,
                        value: false,
                        observer: '_selectedChanged',
                    },
                };
            }

            static get observers() {
                return [
                    '_routePageChanged(route.path)',
                ];
            }

            _routePageChanged(page) {
                if (this.route.prefix !== '!/post') {
                    return;
                }
                this.page = '/post' + page;
            }

            _onError() {
                document.querySelector('my-app').page = 'view404';
            }

            _onSuccess() {
                if (this.response) {
                    let data = this.response.content.replace(/\+/g, '%20');
                    let html = decodeURIComponent(data);
                    this.$['post-content'].innerHTML = html;
                }
                this._selectedChanged();
            }

            _selectedChanged() {
                if (this.response) {
                    document.querySelector('title').innerHTML = this.response.title;
                    document.querySelector('meta[name="description"]').content = this.response.description;
                    document.querySelector('meta[property="og:title"]').content = this.response.title;
                    document.querySelector('meta[property="og:description"]').content = this.response.description;
                    document.querySelector('meta[property="og:url"]').content = '{{ site.url }}{{ site.baseurl }}/#!' + this.response.url;
                }
            }
        }

        window.customElements.define(MyPost.is, MyPost);
    </script>
</dom-module>

다른 정적페이지 연결

다른 정적페이지의 연결은 다른 정적페이지의 layout을 WebComponent로 만들면 됩니다. 저는 about페이지 하나만 필요하므로 about으로 고정되어있지만 template을 이용해서 각각에 Component로 만들 수는 있습니다.

/_layouts/about.html

---
---
<link rel="import" href="/bower_components/polymer/polymer-element.html">
<link rel="import" href="/bower_components/paper-card/paper-card.html">
<link rel="import" href="/bower_components/paper-styles/paper-styles.html">
<link rel="import" href="/bower_components/paper-button/paper-button.html">

<link rel="import" href="/src/shared-styles.html">

<dom-module id="my-about">
    <template>
        <style include="shared-styles">
            :host {
                display: block;

                padding: 10px;
            }

            paper-card {
                width: 100%;
            }

            .post-info .date {
                color: var(--paper-grey-600);
            }
        </style>
        <paper-card>
            <div class="card-content">
                <div class="post-header"><h1>{{ page.title }}</h1></div>
                <div class="post-info">
                    <span class="date">Last Update: {{ page.update | date: '%Y-%m-%d %H:%M KST' }}</span><br>
                </div>
                <div id="post-content">{{ content }}</div>
            </div>
        </paper-card>
    </template>
    <script>
        class MyAbout extends Polymer.Element {
            static get is() {
                return 'my-about';
            }

            static get properties() {
                return {
                    selected: {
                        type: Boolean,
                        value: false,
                        observer: '_selectedChanged',
                    },
                };
            }

            _selectedChanged() {
                let title = '{{ page.title }} | {{ site.title }}';
                let description = `{{ site.description }}`;
                document.querySelector('title').innerHTML = title;
                document.querySelector('meta[name="description"]').content = description;
                document.querySelector('meta[property="og:title"]').content = title;
                document.querySelector('meta[property="og:description"]').content = description;
                document.querySelector('meta[property="og:url"]').content = '{{ site.url }}{{ site.baseurl }}/#!/about';
            }
        }

        window.customElements.define(MyAbout.is, MyAbout);
    </script>
</dom-module>

PWA 적용

github.io를 그대로 사용한다면 github.io가 h2를 지원해주길 우주에 바랄 바랄 수밖에 없습니다.
CloudFlare를 사용하여 Github Page에 도메인을 연결하면 된다고는 합니다. 아직 이 블로그에는 적용 전이기 때문에 만약 적용하게 된다면 다시 포스팅이 되겠죠.. 참고하실 사이트입니다.
https://blog.cloudflare.com/secure-and-fast-github-pages-with-cloudflare/

polymer build작업 후 jekyll parsing 오류

polymer로 다 작성 후 polymer bulid로 빌드하고 빌드된 파일을 기반으로 app을 실행해보니 polyfill에서 {% 또는 {{을 포함하고 있어서 그런지 Jekyll에서 parsing error가 발생합니다. glup으로 수동 빌드를 만들어 사용하였습니다.

가능한 polymer cli의 build를 따라가고 싶었으므로 polymer cli의 소스를 이용했습니다.
먼저 상단의 jekyll의 머리말을 뗴어내고 minify, bundling, compile등의 모든 작업을 거친 뒤 다시 jekyll의 머리말을 붙인다음
jekyll에서 parsing 오류가 나지 않도록 파일들을 검사하여 <html> 앞에 {% raw %}를 붙이고 이후 만나는 {% raw %}앞에 {% endraw %}를 붙였습니다.

lib/jekyllSplitter.js

/**
 * Created by Jinhwan on 2017-07-19.
 */

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __asyncValues = (this && this.__asyncIterator) || function (o) {
    if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
    var m = o[Symbol.asyncIterator];
    return m ? m.call(o) : typeof __values === "function" ? __values(o) : o[Symbol.iterator]();
};
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
    if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
    var g = generator.apply(thisArg, _arguments || []), i, q = [];
    return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i;
    function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }
    function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
    function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r);  }
    function fulfill(value) { resume("next", value); }
    function reject(value) { resume("throw", value); }
    function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
};
Object.defineProperty(exports, "__esModule", { value: true });

const streams_1 = require('polymer-build/lib/streams');
const osPath = require("path");

class JekyllSplitter {
    constructor() {
        this._jekyllTagMap = new Map();
    }

    split() {
        return new JekyllSplit(this);
    }

    rejoin() {
        return new JekyllJoin(this);
    }


    getJekyllTag(key) {
        return this._jekyllTagMap.get(key);
    }

    setJekyllTag(key, value) {
        this._jekyllTagMap.set(key, value);
    }
}
exports.JekyllSplitter = JekyllSplitter;

class JekyllSplit extends streams_1.AsyncTransformStream {
    constructor(splitter) {
        super({ objectMode: true });
        this._state = splitter;
    }
    _transformIter(files) {
        return __asyncGenerator(this, arguments, function* _transformIter_1() {
            try {
                for (var files_1 = __asyncValues(files), files_1_1; files_1_1 = yield __await(files_1.next()), !files_1_1.done;) {
                    const file = yield __await(files_1_1.value);
                    const filePath = osPath.normalize(file.path);
                    if (!(file.contents && filePath.endsWith('.html'))) {
                        yield file;
                        continue;
                    }
                    let contents = yield __await(streams_1.getFileContents(file));
                    const jekyllTag = getJekyllTag(contents);
                    this._state.setJekyllTag(file.path, jekyllTag);
                    if (jekyllTag) {
                        contents = contents.substring(jekyllTag.length);
                    }
                    file.contents = new Buffer(contents);

                    yield file;
                }
            }
            catch (e_1_1) { e_1 = { error: e_1_1 }; }
            finally {
                try {
                    if (files_1_1 && !files_1_1.done && (_a = files_1.return)) yield __await(_a.call(files_1));
                }
                finally { if (e_1) throw e_1.error; }
            }
            var e_1, _a;
        });
    }
}

class JekyllJoin extends streams_1.AsyncTransformStream {
    constructor(splitter) {
        super({ objectMode: true });
        this._state = splitter;
    }
    _transformIter(files) {
        return __asyncGenerator(this, arguments, function* _transformIter_1() {
            try {
                for (var files_1 = __asyncValues(files), files_1_1; files_1_1 = yield __await(files_1.next()), !files_1_1.done;) {
                    const file = yield __await(files_1_1.value);
                    const filePath = osPath.normalize(file.path);
                    if (!(file.contents && filePath.endsWith('.html'))) {
                        yield file;
                        continue;
                    }
                    let contents = yield __await(streams_1.getFileContents(file));
                    const jekyllTag = this._state.getJekyllTag(file.path);
                    if (jekyllTag) {
                        contents = jekyllTag + '\n' + contents;
                        file.contents = new Buffer(contents);
                    }
                    yield file;
                }
            }
            catch (e_1_1) { e_1 = { error: e_1_1 }; }
            finally {
                try {
                    if (files_1_1 && !files_1_1.done && (_a = files_1.return)) yield __await(_a.call(files_1));
                }
                finally { if (e_1) throw e_1.error; }
            }
            var e_1, _a;
        });
    }
}

const jekyllTagRegex = /^---(\n|\r|\n\r|\r\n)(\w*\:.*.(\n|\r|\n\r|\r\n))*---/;
exports.jekyllTagRegex = jekyllTagRegex;

function getJekyllTag(html) {
    const jekyllTag = html.match(jekyllTagRegex);
    if (jekyllTag) {
        return jekyllTag[0];
    }
    return null;
}

exports.getJekyllTag = getJekyllTag;

: lib/appendRaw.js

/**
 * Created by Jinhwan on 2017-07-19.
 */

const stream_1 = require("stream");

const bundleRegex = /<html>/;
const rawRegex = /{%( )?raw( )?%}/;
const jekyllTagRegex = require("./jekyllSplitter").jekyllTagRegex;

class AppendRawTransform extends stream_1.Transform {
    constructor(optimizerName, optimizer, optimizerOptions) {
        super({ objectMode: true });
        this.optimizer = optimizer;
        this.optimizerName = optimizerName;
        this.optimizerOptions = optimizerOptions || {};
    }
    _transform(file, _encoding, callback) {
        if (file.contents) {
            try {
                let contents = file.contents.toString();
                if (contents.search(jekyllTagRegex) === -1 || contents.search(rawRegex) === -1) {
                    callback(null, file);
                    return;
                }
                contents = contents.replace(rawRegex, '{% endraw %}{% raw %}').replace(bundleRegex, '{% raw %}<html>');
                file.contents = new Buffer(contents);
            }
            catch (error) {
                logger.warn(`${this.optimizerName}: Unable to optimize ${file.path}`, { err: error.message || error });
            }
        }
        callback(null, file);
    }
}
exports.AppendRawTransform = AppendRawTransform;

: src/guipfile.js

/**
 * Created by Jinhwan on 2017-07-18.
 */
'use strict';

const del = require('del');
const gulp = require('gulp');
const mergeStream = require('merge-stream');
const polymerBuild = require('polymer-build');

const swPrecacheConfig = require('./sw-precache-config.js');
const polymerJson = require('./polymer.json');
const options = polymerJson.builds[0];
const optimizeOptions = {
    css: options.css,
    js: options.js,
    html: options.html
};
const polymerProject = new polymerBuild.PolymerProject(polymerJson);
const buildDirectory = 'build';
const getOptimizeStreams = require('../lib/optimize-stream').getOptimizeStreams;
const JekyllSplitter = require('../lib/jekyllSplitter').JekyllSplitter;
const AppendRawTransform = require("../lib/appendRaw").AppendRawTransform;

/**
 * Waits for the given ReadableStream
 */
function waitFor(stream) {
    return new Promise((resolve, reject) => {
        stream.on('end', resolve);
        stream.on('error', reject);
    });
}

function pipeStreams(streams) {
    return Array.prototype.concat.apply([], streams)
        .reduce((a, b) => {
            return a.pipe(b);
        });
}

function build() {
    return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars

        // Okay, so first thing we do is clear the build directory
        console.log(`Deleting ${buildDirectory} directory...`);
        del([buildDirectory])
            .then(() => {
                const stream = gulp.src(options.passingPattern, {base: '.'}).pipe(gulp.dest(buildDirectory));
                return waitFor(stream);
            })
            .then(() => {
                // Let's start by getting your source files. These are all the files
                // in your `src/` directory, or those that match your polymer.json
                // "sources"  property if you provided one.
                const sourcesStream = polymerBuild.forkStream(polymerProject.sources());
                const depsStream = polymerBuild.forkStream(polymerProject.dependencies());
                const htmlSplitter = new polymerBuild.HtmlSplitter();
                const jekyllSplitter = new JekyllSplitter();

                let buildStream = pipeStreams([
                    mergeStream(sourcesStream, depsStream),
                    jekyllSplitter.split(),
                    htmlSplitter.split(),
                    getOptimizeStreams(optimizeOptions),
                    htmlSplitter.rejoin(),
                ])
                    .once('data', () => {
                        console.log('building...');
                    });


                const compiledToES5 = !!(optimizeOptions.js && optimizeOptions.js.compile);
                if (compiledToES5) {
                    buildStream = buildStream.pipe(polymerProject.addBabelHelpersInEntrypoint())
                        .pipe(polymerProject.addCustomElementsEs5Adapter());
                }

                const bundled = !!(options.bundle);
                if (bundled && typeof options.bundle === 'object') {
                    buildStream = buildStream.pipe(polymerProject.bundler(options.bundle));
                } else if (bundled) {
                    buildStream = buildStream.pipe(polymerProject.bundler());
                }

                if (options.addPushManifest) {
                    buildStream = buildStream.pipe(polymerProject.addPushManifest());
                }

                buildStream = buildStream.pipe(jekyllSplitter.rejoin());
                buildStream = buildStream.pipe(new AppendRawTransform());

                buildStream = buildStream.pipe(gulp.dest(buildDirectory));
                return waitFor(buildStream);
            })
            .then(() => {
                if (options.addServiceWorker) {
                    // Okay, now let's generate the Service Worker
                    console.log('Generating the Service Worker...');
                    return polymerBuild.addServiceWorker({
                        project: polymerProject,
                        buildRoot: buildDirectory,
                        bundled: !!(options.bundle),
                        //TODO: 2017-07-18 polymer.json 따라가지 않음.
                        swPrecacheConfig: swPrecacheConfig
                    });
                }
            })
            .then(() => {
                // You did it!
                console.log('Build complete!');
                resolve();
            });
    });
}

gulp.task('build', build);

이후 package.json의 script에 build항목을 추가했습니다. "build": "gulp build --gulpfile src/gulpfile.js"

SEO 적용

Poylmer는 JavaScript기반의 라이브러리이기 때문에 JavaScript를 실행하지 않는 검색 엔진은 최적화가 불가능 합니다. server-side-rendering이 지원된다면 앞으론 가능성이 있겠으나 정적페이지인 Github Page에선.. (절래절래)

그래도 구글은 JavaScript를 실행하기 때문에 SEO에 필요한 몇 가지를 Component가 보여질 때 바꿔주면 됩니다.
iron-pages를 사용하고 있으니 iron-pages의 Attribute로 selected-attribute="selected"를 사용하여 각각 Component에서 Attribute Event를 잡을 수 있도록 합니다. 해당 Component에 properties에

selected: {
    type: Boolean,
    value: false,
    observer: '_selectedChanged',
},

를 추가 하고 _selectedChanged 함수에 title과 meta tag들의 값들을 수정합니다.

_selectedChanged() {
    if (this.response) {
        document.querySelector('title').innerHTML = this.response.title;
        document.querySelector('meta[name="description"]').content = this.response.description;
        document.querySelector('meta[property="og:title"]').content = this.response.title;
        document.querySelector('meta[property="og:description"]').content = this.response.description;
        document.querySelector('meta[property="og:url"]').content = 'https://jhbae.in/#!' + this.response.url;
    }
}

iron-ajaxlast-response으로 response를 지정해서 this.response를 사용 중입니다. response에서 가져온 정보를 기반으로 title과 meta tag값을 수정하면 일단은 페이지 노출은 될 수 있겠죠.. 이후로 sitemap, rss-feed를 만들어 검색엔진에 제출하면 검색엔진은 더 정확하게 크롤링을 할 수 있을겁니다.

의욕만 된다면 PWA적용 후 블로그 글을 읽다 중간에 나갔을 때 마지막 블로그 글 위치로 이동할 수 있게 하는 기능이라든지 검색 기능, 새 글 등록 시 Notification등을 구현하고 싶은데 맘이 안 따라주는 것 같습니다. ㅎㅎㅎㅎㅎ
천천히 블로그를 업데이트할 테니 이 블로그에 사용한 build전의 소스는 polymer-jekyll에 있습니다.