Google の検索順位判定ロジックに変更が施されたかもとか、そんなこと

突然奈落の底へ落ちる【Google大変動2018年8月】何も言えなくて…夏」の件。

ブログでの広告収入なんて興味ないので、書きたいことを書き散らかしてるだけのブログをいくつか持ってる。
年に数回くらいしか開かない Search Console を見てみた。
その中でも書き込み量が、まあまああるふたつの流入の様子。
グラフの右側 \frac{1}{3} くらいが 2018年8月の分。

はてなブログの方

FC2の方

はてなブログの方は、人力検索がつまらなくなったこともあって、無駄に長い投稿が最近 増えてきたかなあという気はする程度で、真面目に更新してるとは言えない。
FC2の方は、ほんとに倉庫というレベルで、一ヶ月放置すると出てくる広告を消す程度にしか更新してない。


確かに、2018年8月に入ってから、検索順位の判定に何か変わったのかも、という気がしなくもない程度にはうっすらと傾向が変わってる。
何かしらの判定が厳しくなったのかしらということを想像してるらしいけれど、はてなブログの方は、(ほんのりだけど)むしろ増えてるので、一律に厳しくなったのではないような気もする。
2018年8月は、どちらのブログにも記事を投稿してないから、過去記事のクロール結果、もしくは、クロール済みの結果の判定に何かあったのかも。


広告収入を期待してる人は大変だね :-p
# 一般論にするには、PV 少なすぎるな(ケラケラ

人力検索と Hatena::Let とアドオンと

まずは、

Hatena::Let のコードがさみしいので、Syntax Highlight しようと思ったの。
よく見るところだから、Bookmarklet じゃなくてユーザスクリプト(アドオン)なの。

クラッチパッドで確認したよ


(() => {
    document.body.appendChild(Object.assign(document.createElement("link"), {
        rel: "stylesheet",
        href: "https://highlightjs.org/static/demo/styles/zenburn.css",
    }));
    document.body.appendChild(Object.assign(document.createElement("script"), {
        src: "//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js",
        onload: () => {
            document.querySelectorAll('pre.code-raw > code').forEach(e => {
                hljs.highlightBlock(e);
            });
        },
    }));
})();

そんなに難しいことしてない。

アドオンだと動かないんだ

ブラウザツールボックスのコンソールには、こんなエラーが出てる。


ReferenceError: hljs is not defined

すぐに思い出したよ(うそ。ちょっと「あれ?」ってなった)。
chrome extension について。 ページ内から、contentscripts.js … - 人力検索はてな

Although content scripts don't by default get access to objects created by page scripts,

Content scripts - Mozilla | MDN

コンテンツスクリプトの window と、ページスクリプトの window は、同じじゃない。
だから、ページスクリプトにある window.hljs は、コンテンツスクリプトでは使えない。

window.postMessage と window.addEventListener を使うんだった


(() => {
    const d_ = document;
    d_.body.appendChild(Object.assign(d_.createElement("link"), {
        rel: "stylesheet",
        href: "//highlightjs.org/static/demo/styles/zenburn.css",
    }));

    d_.body.appendChild(Object.assign(d_.createElement("script"), {
        src: "//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js",
        onload: () => {
            window.postMessage({
                command: "do_highlight",
            }, "*");
        },
    }));

    d_.body.appendChild(Object.assign(d_.createElement("script"), {
        innerHTML: `
            window.addEventListener("message", ev => {
                if (ev.source == window && ev.data && ev.data.command == "do_highlight") {
                    document.querySelectorAll('pre.code-raw > code').forEach(e => {
                        hljs.highlightBlock(e);
                    });
                }
            });
        `,
    }));
})();

きっと普通のやり方は、

質問に答える人とは?

「回答が自分のところに返ってくる」ってことも、間々あったりする :-)

質問に答える人とは? 単なる善意だけでは成り立っているとは思… - 人力検索はてな



(おしまい)

Google Maps API → OpenLayers API (V3以降)

Google Maps APIOpenLayers API に書き換えるとこうなる、という話です。

以前、こんな投稿をしたのですが、OpenLayers のバージョンは V2 です。
そのときは既に V3 が出ていましたし、この記事を書いている時点では、OpenLayersV5 が出ようか、という状況です。
というわけで、そのときの記事を OpenLayers V3 以降に対応するコードでリライトしました。
# コードは、記事を書いている時点の最新版 V4.6.5 で確認しました

では、ここから本題です。


id:ykhpno1 さんの、この質問。

その前までの一連の質問で、Google Maps API を使ってある程度の形になっていたのだけれど、アクセス数の制限があったりするから OpenStreet Map に変更したいという お話。
Google Maps API のここは、OpenLayers API ではこう書く、みたいな感じページが意外と見つからなかったので、備忘を兼ねて。

長いよ

  • まずは、ソース
  • Google Maps API vs. OpenLayers API (V3 以降)
    • (A) 地図の作成
    • (B) 地図の作成タイミング
    • (C) 地図を表示する要素との紐づけ
    • (D) 中心の位置
    • (E) スケールバー
    • (F) 良い感じにズーム
    • (G) ズームの最大値を抑制
    • (H) マーカーの作成
    • (I) マーカー画像の指定
    • (J) マーカーのツールチップ
    • (K) 吹き出しの作成
    • (L) マーカーをクリックしたときに、吹き出しの表示をトグル
    • (M) 吹き出しがマーカーを覆い隠す
    • (N) 地図のスタイル
    • (O) マーカーのクラスタリング
  • よく見かけるサンプルとの違い
  • 以前の OpenLayers V2 との比較の記事のコードから変更したところ
  • OpenLayers 2 と OpenLayers 3 以降
    • ol.Feature や ol.geom.* は、GeoJSON のオブジェクトモデルなんだ
    • 地図は、基本的に canvas に描かれる
    • Marker (ol.Feature) も、canvas に描かれるので、イベント処理は ol.Map のイベントを処理する
    • 吹き出しは、自分で DOM を用意して、ol.Overlay と紐づける
    • オブジェクトの入れ子の階層が深いけど、javascript は型チェックがないので、間違えたクラスを渡したときのエラーが分かりにくい
    • ol.style.Style のインスタンスは、キャッシュした方が良さそう
  • その他
    • 公式
    • リンク
    • 元ネタとか
    • Fiddle
続きを読む

Proxy 切り替えアドオン @Firefox 59

Firefox 57 では、PAC を変更することしかできなかった WebExtension API
Firefox 59 で、ようやくプロクシの設定を切り替える手段ができた。

ぼくは、「プロクシなし」と「システム設定」を切り替えたいだけなので、こんな感じのアドオンで十分。

  • ファイル構成
  • manifest.json
  • background.js
  • スクリーンショット
  • 強いて言えば
  • FF 60.0b16 で動かなくなってる (2018-5-1 追記)
  • 実装が変わった (2018-5-3 追記)
    • manifest.json (FF 60.0b16 以降)
    • background.js (FF 60.0b16 以降)
続きを読む

offsetLeft の罠

こちらのさいとう先生の画像を切り取ろうとしたときのこと。

あれ、ずれてる...
offsetParent をたどって行けば、良いんじゃなかったっけ (´・ω・`)

    let e = ...
    let x = 0, y = 0;
    do {
        x += e.offsetLeft;
        y += e.offsetTop;
    } while (e = e.offsetParent);

    console.log(x, y);

(468, 1455) と計算されるけど、Firefox のものさしツールで測ってみると (514, 1454)。
46px も足りてない。


# ↓に たどり着くまで、かなりの日数が経ってます
画像の offsetParent の DIV には、border-left が指定されてる。

.NA_articleFigure {
    position: relative;
    border-left: 46px solid #fff;
}

ああ、border の分を足してあげなきゃダメなんだ(border で位置を調整するんじゃねえよ


ぴったり。

汎用的にやるためには、box-sizing も考慮してあげなきゃいけない。
こんな感じか。

    let e = ...
    let x = 0, y = 0;
    do {
        x += e.offsetLeft;
        y += e.offsetTop;
        if (e.offsetParent) {
            let {borderTopWidth, borderLeftWidth, boxSizing} =
						window.getComputedStyle(e.offsetParent);
            if (boxSizing != "border-box") {
                x += parseFloat(borderLeftWidth);
                y += parseFloat(borderTopWidth);
            }
        }
    } while (e = e.offsetParent);

    console.log(x, y);

→ (514, 1455)




今どきは、getBoundingClientRect() 使おうね、って話でした。

    let e = ...
    let {x, y} = e.getBoundingClientRect();
    x += document.documentElement.scrollLeft;
    y += document.documentElement.scrollTop;

    console.log(x, y);

→ (513.75 1455.8) : なんだ、この小数点……

Promise vs async/await

背景というか、状況というか

Firefox の addon で、コンテンツプロセスでスクリプトをロードしたり、コードを実行したりする browser.tabs.executeScript() を続けて実行したい、という場面。

  • ライブラリを使いたいんだけど、一部がエラーになる
  • ライブラリに機能を追加したい
  • でも、(何となく)ライブラリはいじらずにおきたい
  • コンテンツプロセスでは、前回の状態が残っているので、ライブラリのロードは一回にしたい

「一部がエラーになる」というのは、

    convertStringEncoding: function(str) {
        // for subscript loader
        return decodeURIComponent(escape(str || ''));
    },

この関数がこんなふうに呼ばれてて、

SketchSwitch.Buttons.RedPen = function(sketch) { this.sketch = sketch };
SketchSwitch.Buttons.RedPen.prototype = SketchSwitch.Utils.extend({
    shortcut: null,
    icon: 'data:image/png;base64,iVBOR...',
    name: SketchSwitch.Utils.convertStringEncoding('赤ペン'),

URIError: malformed URI sequence というエラーが出ちゃう。

どうやら、XPCOM で、設定ファイルで非ASCII 文字を扱うときの あるある らしく、件のコードは UTF-8UTF-16 コンバータとして機能するらしい。

そのライブラリは、XPCOM のアドオンだったのだけれど、文字列リテラルに必要な処理だったのかどうかは謎だし、WebExtension では必要ない(というか、エラーになる)。
対応としては、convertStringEncoding() を書き換えれば良いだけなんだけど、

  • ライブラリをいじらずにおきたい
  • ロードするときに呼ばれてる処理なので、後から置き換えるという手が通じない

ということがあって、ライブラリをロードする前に decodeURIComponent() を書き換えて、ロードが終わった後に戻す(ある意味、荒業
# 素直に、ライブラリをいじった方が良いんじゃないか(という気はしてる

で、こんな感じの処理の流れになる。

  1. ライブラリがロード済みかどうかを確認
  2. ライブラリがロードされてなかったら
    1. decodeURIComponent の実装を保存して、書き換える
    2. ライブラリをロードする
    3. decodeURIComponent の実装を戻す
    4. ライブラリの機能追加/変更のスクリプトをロードする
  3. ライブラリを使う

前置きが長くなりました。

で、実際のコード

Promise を使って書いたコード。

function show_sketch_menu(tab) {
    // https://github.com/mdn/webextensions-examples/blob/master/context-menu-copy-link-with-types/background.js
    let loaded_first = false;
    browser.tabs.executeScript(tab.id, {
        code: "typeof SketchSwitch === 'function'",
    }).then(result => {
        if (!result || result[0] !== true) {    // SketchSwitch is not defined
            loaded_first = true;
            // for malformed URI --- SketchSwitch.Utils.convertStringEncoding()
            return browser.tabs.executeScript(tab.id, {
                code: `
                    original__decodeURIComponent = decodeURIComponent;
                    decodeURIComponent = s => unescape(s);
                    true;   // Script '<anonymous code>' result is non-structured-clonable data
                `,
            });
        }
    }).then(result => {
        if (loaded_first) {
            return browser.tabs.executeScript(tab.id, {
                file: "/content/SketchSwitch.js",
            });
        }
    }).then(_ => {
        if (loaded_first) {
            return browser.tabs.executeScript(tab.id, {
                code: `
                    decodeURIComponent = original__decodeURIComponent;
                    true;
                `,
            });
        }
    }).then(_ => {
        if (loaded_first) {
            return browser.tabs.executeScript(tab.id, {
                file: "/content/sketch-patch.js",
            });
        }
    }).then(_ => {
        return browser.tabs.executeScript(tab.id, {
            code: `
                (_ => {
                    const canvas = document.getElementById("__sketch_switch_canvas__");
                    if (canvas) {
                        // Sketch Menu is shown.
                        return;
                    }

                    const sketch = new SketchSwitch(window, {});
                    sketch.show();
                })();
            `,
        });
    }).catch(error => {
        console.error(error.message || error);
        console.dir(error);
    });
}

こちらが async/await を使って、書き直したコード。

async function show_sketch_menu(tab) {
    try {
        // https://github.com/mdn/webextensions-examples/blob/master/context-menu-copy-link-with-types/background.js
        const result = await browser.tabs.executeScript(tab.id, {
            code: "typeof SketchSwitch === 'function'",
        });
        if (!result || result[0] !== true) {    // SketchSwitch is not defined
            // for malformed URI --- SketchSwitch.Utils.convertStringEncoding()
            await browser.tabs.executeScript(tab.id, {
                code: `
                    original__decodeURIComponent = decodeURIComponent;
                    decodeURIComponent = s => unescape(s);
                    true;   // Script '<anonymous code>' result is non-structured-clonable data
                `,
            });
            await browser.tabs.executeScript(tab.id, {
                file: "/content/SketchSwitch.js",
            });
            await browser.tabs.executeScript(tab.id, {
                code: `
                    decodeURIComponent = original__decodeURIComponent;
                    true;
                `,
            });
            await browser.tabs.executeScript(tab.id, {
                file: "/content/sketch-patch.js",
            });
        }
        await browser.tabs.executeScript(tab.id, {
            code: `
                (_ => {
                    const canvas = document.getElementById("__sketch_switch_canvas__");
                    if (canvas) {
                        // Sketch Menu is shown.
                        return;
                    }

                    const sketch = new SketchSwitch(window, {});
                    sketch.show();
                })();
            `,
        });
    } catch(error) {
        console.error(error.message || error);
        console.dir(error);
    }
}

diff を取ったものがこちら

思ったこと

こんなハイクをした後に書きました。
Promise を返してくれる API だったら、async/await は、気持ちすっきりするかなあ、という感じ。
行数もインデントも減るし。

書きながら思ったけど、Promise の方は、最初の一発目だけを、もうひとつの Promise でくくってあげれば、余計なフラグを持つ必要はなくなるのかも(試してない

    }).then(result => {
        return new Promise((resolve, reject) => {
            if (!result || result[0] !== true) {    // SketchSwitch is not defined
                // for malformed URI --- SketchSwitch.Utils.convertStringEncoding()
                browser.tabs.executeScript(tab.id, {
                    code: `
                        original__decodeURIComponent = decodeURIComponent;
                        decodeURIComponent = s => unescape(s);
                        true;   // Script '<anonymous code>' result is non-structured-clonable data
                    `,
                })
                .then(_ => {
                    return browser.tabs.executeScript(tab.id, {
                        file: "/content/SketchSwitch.js",
                    });
                }).then(_ => {
                    ...
                }).then(_ => {
                    resolve();
                }).catch(_ => {
                    reject();
                }
            }
        };
    }).then(_ => {
        ...

余計な変数は必要なくなるけど、インデントは深くなる。
自分で Promise 書いちゃうと、すっきりしないなあという感じはする。


こんなハイクのやり取りも。
http://h.hatena.ne.jp/noromanba/316607276733271244
http://h.hatena.ne.jp/noromanba/315956334695317249



おしまい。

ブックマークツールバーのアイコンを変更する @Firefox57

人呼んで "Firefox Quantum" では、アドオンが全面的に WebExtension に移行して、ブラウザの見た目をいじるようなアドオンが作れなくなってしまいました。
でも、アドオンではできなくなっちゃったけど、手作業で何とかブックマークツールバーFavicon は変えられます。
人力検索こんなのこんなので回答したやつのまとめというかリライトです


まず、ユースケースから。


次に、予備知識。

続きを読む