WordPressの単一投稿ページや固定ページでは、<BODY>タグのクラスに投稿IDが出力されます。
単一投稿ページでは、class="single single-post postid-13 single-format-standard~"、固定ページでは、class="page page-id-68 page-template-default~"という形式で自動でクラス指定されるわけです1

そして、アーカイブページのように複数の投稿をリスト化して表示するページの一つのリスト(テーマにより差はあるが、大抵は<ARTICLE>タグ)毎に、投稿IDがID属性とクラスに含まれています。
例えば、<article id="post-40" class="post-40 status-publish format-standard hentry"> という形式です。これはアーカイブページをはじめ、単一投稿ページや固定ページのメインコンテンツの記事部分も同様です。

基本的にこのデフォルトのタグ構造・クラス形式を継承するWordPressテーマであれば、単一投稿ページや固定ページが表示された時にその投稿IDをCookie等に保存することで、記事の既読判定を行うことが出来るようになります。
つまり、単一の投稿を表示した時点でその投稿IDをCookieの既読リストに保存して、アーカイブページや記事一覧系のコンテンツを表示するところでCookieから既読リストを読み出して照らし合わせることで、記事毎に未読・既読の表示を出し分けられるわけです。

では実践してみます。実際には出力されたHTMLを解析してのDOM処理となるため、WordPressテーマ等のPHPは全く触らずにJavaScriptオンリーで実現可能です。

まず、現在表示されているページの投稿IDを取得する関数を準備します。

function getPostID() {
    // BODYタグのclass属性から投稿IDを取得する
    var post_id = null;
    var classList = $('body').attr('class').split(/s+/);
    for (i=0; i<classList.length; i++) {
        if (classList[i].indexOf('postid-') != -1) {
            var post_id = Number(classList[i].replace('postid-', ''));
            break;
        } else if (classList[i].indexOf('page-id-') != -1) {
            var post_id = Number(classList[i].replace('page-id-', ''));
            break;
        }
    }
    return post_id;
}

次に、Cookieのセッタとゲッタを定義します。

function setCookie(ck_name, ck_value, expiredays) {
    // SetCookie
    var path = '/';
    var extime = new Date().getTime();
    var cltime = new Date(extime + (60*60*24*1000*expiredays));
    var exdate = cltime.toUTCString();
    var pre_data = getCookie(ck_name);
    var tmp_data = pre_data.split(',');
    tmp_data.push(ck_value);
    var fix_data = tmp_data.filter(function (x, i, self) { return self.indexOf(x) === i; });
    var s = '';
    s += ck_name + '=' + escape(fix_data.join(','));
    s += '; path=' + path;
    s += expiredays ? '; expires=' + exdate + '; ' : '; ';
    document.cookie = s;
}
function getCookie(ck_name) {
    // GetCookie
    var st = '', ed = '', res = '';
    if (document.cookie.length > 0) {
    st = document.cookie.indexOf(ck_name + '=');
    if (st != -1) {
        st = st + ck_name.length + 1;
        ed = document.cookie.indexOf(';', st);
        if (ed == -1) 
            ed = document.cookie.length;
            res = unescape(document.cookie.substring(st, ed));
        }
    }
    return res;
}

以上が共通処理です。本項以下の処理を実行するために必要になります。
そして、単一投稿ページや固定ページなどの既読を判定させたい記事ページに下記のスクリプトを追加します。

function checkAlreadyRead() {
    // 自ページのARTICLEタグの投稿IDを取得してCookieの既読済みリストと照らし合わせる
    var match_already_read = false;
    $('article').each(function() {
        var pid = null;
        var classList = $(this).attr('class').split(/s+/);
        for (i=0; i<classList.length; i++) {
            if (classList[i].indexOf('post-') != -1) {
                var pid = Number(classList[i].replace('post-', ''));
                break;
            }
        }
        if (pid != '') {
            var data = getCookie('already_read').split(',');
            var post_ids = data.filter(function(x, i, self) { return self.indexOf(x) === i; });
            post_ids.forEach(function() {
                if (Number(arguments[0]) == pid) {
                    match_already_read = true;
                }
            });
            // 既読済みARTICLEのclassにreadクラスを追加する
            var add_class = match_already_read ? 'read' : '';
            $(this).addClass(add_class);
        }
    });
    return match_already_read;
}
var is_already_read = checkAlreadyRead();
$(window).scroll(function() {
    // ヘッダーナビゲーションの高さ(例では60px)より下にスクロールしたら既読とする
    if (!is_already_read) {
        if ($(this).scrollTop() > 60) {
            setCookie('already_read', getPostID(), 365);
            is_already_read = true;
        }
    }
});
setTimeout(function() {
    // このページへの滞在時間が2秒以上になったら既読とする
    if (!is_already_read) {
        setCookie('already_read', getPostID(), 365);
        is_already_read = true;
    }
}, 2000);

上記サンプルでは、そのページにて特定位置より下にスクロールするか、ページでの滞在時間が一定時間を超えた時のいずれかの条件で、既読フラグをONにし、Cookieに既読済みリスト「already_read」としてそのページの投稿IDを保存期間を365日(1年間)でセットしています2

上記サンプルでは変数is_already_readを既読判定フラグとして定義してスイッチングしているだけで、特にこのフラグを使っての後続処理は行っていません。
例えば、既読判定フラグがTRUEになったら特定の処理をする関数を準備しておけば、下記のように呼び出すことができます。

// 呼び出し元
    if (!is_already_read) {
        setCookie('already_read', getPostID(), 365);
        is_already_read = true;
        setAlreadyReadIcon();
    }

function setAlreadyReadIcon(){
    // 既読クラス「read」を持つ要素の子要素に既読アイコンを追加する
    $(document).find('.read').each(function() {
        if (!$(this).children().is('.already-read-icon')) {
            $(this).append('<div class="already-read-icon"><i class="fa fa-eye"></i></div>');
        }
    });
}

上記はあくまで一例ですが、本サイトのTOPページなどでは、既読の記事だったら記事ブロックの左下に既読アイコン(目のアイコン)を表示している処理を行っています。

※ 本項のスクリプトを動かすにはjQueryが必要です。
※ 既読リストはCookieに保存してあるため、ブラウザにてCookieを削除すると既読履歴がクリアされてしまいますのでご注意を。


  1. アーカイブページなどの<BODY>タグクラスにはclass="archive post-type-archive logged-in~"のように投稿IDは含まれません。 
  2. 既読済みリストが更新されるたびに、保存期間はその時点から1年間に更新されます。