てくてく

カテゴリ「プログラム4件]

小説をサイトに載せるとき気付いたんです。
「中身ほとんど一緒なんだからぱっと置き換えたら楽なんじゃない?」
最近PHPでパーツの共通化してとても便利ではあったのですが、小説本文をペッと貼ったらパッとhtml形式で書き出してくれたら楽できるなって気付いたんです。
ということでそんな小説をhtmlに書き出してくれる置き換えスクリプトを作りました!
今回は配布ありです。

配布
ダウンロードはこちら⇨novel 2 template
免責事項をご確認の上、ご利用くださいませ。

デモはこちらより確認できます。

内容物
  • index.html
  • readme.txt
  • script.js
  • style.css


確認環境はGoogle Chromeのみです。またローカルでの使用を想定しております。

使い方
あらかじめ、以下のものを用意してください。
  • 小説本文
  • 小説を載せたいテンプレート(html、phpどちらでも可)以降テンプレート表記


novel 2 template
index.htmlを開く(ダブルクリック)と規定ブラウザでページが開きます。
各項目を設定します。

◯タイトルタグ
<title>に書きたい内容。
置き換えの必要がない場合は空欄で大丈夫です。

◯cssのURL
ファイルを配置する場所からcssまでのパス。
文字を置き換えるだけなので、相対パス・絶対パスどちらでもOKです。
置き換えの必要がない場合は空欄で大丈夫です。

◯作品タイトル
小説のタイトル。
置き換えの必要がない場合は空欄で大丈夫です。

◯本文
小説本文。通常だとタグに囲まれずそのまま置き換えられます。
置き換えの必要がない場合は空欄で大丈夫です。
下記の機能があります。
・改行タグを入れる
 - 改行済みテキストの場合、改行タグを挿入します。デフォルトでチェックが入っています。
・空行が◯行以上ならpタグで囲む
 - 空行の行数を指定すると、その間のテキストをpタグで囲みます。
・ルビ表記をルビタグに差し替える
 - 特定のルビ表記をルビタグに変更します。対応しているルビ表記は後述です。
・pタグとpタグの間に水平タグを挟む
 - pタグとpタグの間に水平タグを入れます。これは管理人が欲しくて追加したオマケ機能です。

対応しているルビ表記は以下のとおりです。
| 漢字 《かんじ》
#漢字__かんじ__#
[RB:漢字,かんじ]
[[rb: 漢字 > かんじ ]]
[漢字"かんじ"]

◯JavaScriptのURL
JavaScriptのファイルまでのパス。
cssのURL同様、ファイルを配置する場所からJavaScriptファイルまでのパスをいれてください。
置き換えの必要がない場合は空欄で大丈夫です。

◯項目追加
項目名を入れ追加ボタンを押すと、項目を追加できます。
項目名はわかりやすくするためのものなので、未定義のままでも動きます。
追加した項目は削除可能です。追加・削除した項目はナンバリングが再設定されます。

◯ファイルから読み込み
テンプレートファイルがあればこちらから読み込むのが早いです。
読み込むとテンプレート欄に内容が表示されます。

◯テンプレート
ファイルから読み込まない場合、テンプレート欄にテンプレートを記入してください。

◯置き換え
設定した項目をテンプレートに反映させます。
テンプレート欄になにもないとアラートが表示されます。

◯置き換え結果
置き換えた内容をこのエリアに表示します。
内容をコピーしてご利用ください。
整形されていないので、整形したい場合は外部ツールやソフトをご利用くださいませ。

◯ダウンロード
ファイル名(デフォルトはoutput)を記入しhtmlかphpかを選びダウンロードボタンを押すと、置き換え結果の内容を選択した拡張子でダウンロードできます。
なお、サーバーに送信はされませんのでご安心ください。
置き換え結果欄になにもないと、アラートが表示されます。

テンプレートファイルの書き方
novel 2 templateの各項目には[item数字]が書かれています。
この[item数字]を書いた場所が、設定した内容と置き換わる仕組みです。
例1:
タイトルタグフォーム:小説をhtmlに置き換えるスクリプト作ってみたっ
テンプレート:<title>[item0]</title>
書き出されるもの:<title>小説をhtmlに置き換えるスクリプト作ってみたっ</title>

例2:
cssのURLフォーム:../css/style.css
テンプレート:<link rel="stylesheet" href="[item1]" type="text/css" id="style">
書き出されるもの:<link rel="stylesheet" href="../css/style.css" type="text/css" id="style">

例3:
追加した項目[item5]:明日は明日の風が吹く
テンプレート:<p>[item5]</p>
書き出されるもの:<p>明日は明日の風が吹く</p>

カスタマイズ
MITライセンスで配布しておりますので、ご自身のテンプレートに合うように好きに改変していただいて構いません!
カスタマイズ向けの説明は同梱のreadme.txtに記載されています。

最後に
自分用に試しに作ってみたものですが、割といい感じにできたと思います。
カスタマイズ次第では項目追加も不要になると思いますので、自分だけの変換ツールを作ってみてください!

関連記事
簡易ログイン作ってみた!
【JavaScript】名前変換機能を作ってみた!
今は便利な名前変換機能(単語変換機能)スクリプトを配布してくださっている方がいらっしゃいます。
私が自分のサイトに実装した当時、求めている機能が微妙になかったため「作るっきゃねえ!」の精神で自作しました。

デモ
こちらのデモページから動作確認ができます。

仕様
①名字・名前・名字ひらがな・名前ひらがなを変換
②名字と名前はユニークなものを前提としている
③ひらがなは[RB:吃 > ども]りなどで利用
④小説ページごとにデフォルト名が異なっていても対応可能
⑤Cookieを利用
⑥変換フォームのプレースホルダーには変換した名前を入れておく。

ユニークなものとは、重複しない独特なものを意味しています。本文で使われない単語のことです。
例えば、名前が「空」だと「青空の下に広がる海。」など本文に「空」の文字が入っていると仕様上その文字も変換されてしまいます。

基本私が小説を書くときは名前変換を前提としておらず、名前を決めて執筆しています。
htmlに持っていくときに名前の部分にタグ付けがいちいち面倒だな……と思い、JavaScriptの文字列変換を利用して書き換える方向にしました。
ただひらがなはなかなか難しかったため、ひらがなだけタグをつけています。
(私の小説ではそこまで多用しなかったので妥協しました)

ファイル構成
index.html
contents.html
css/style.css
js.name-change.js
今回はjQueryを使いません。
Cookie処理はかわりにjs-cookieを利用しました。

JavaScriptの読み込み
名前変換のフォームがあるページと、名前変換処理をするページで読み込みます。
<!-- js-cookie -->
<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js"></script>
<!-- script -->
<script src="js/name-change.js"></script>

js-cookieのCDNはこちらからコピペしています。
Cookie操作をしやすいように利用しています。

名前変換フォーム
今回index.htmlに書いてる名前変換のフォームです。
なお、デザインに関するクラスは省いています。
<form name="d_name">
    <input type="text" placeholder="名字" name="lastname">
    <input type="text" placeholder="名前" name="firstname"><br>
    <input type="text" placeholder="みょうじ" name="lastname_hira">
    <input type="text" placeholder="なまえ" name="firstname_hira"><br>
    <button type="button" onclick="setName()">変換</button>
    <button type="button" onclick="resetName()">リセット</button>
</form>
<div id="setName"></div>

onclickの処理はjs/name-change.jsファイルに記述します。
<div id="setName"></div>は変換ボタンやリセットボタンを押した後の結果を表示するためのものです。

style.css
上記<div id="setName"></div>の設定です。
#setName {
    display: none;
    /* 下記はデザイン用 */
    margin: 15px 0;
    padding: 15px;
    border: 1px solid transparent;
    border-radius: 3px;
    margin: 0 0 10px;
    padding: 8px;
}

#setName.change,#setName.reset {
    display: block;
}

#setName.change::after {
    content: "名前を変換しました。";
}

#setName.reset::after {
    content: "名前をリセットしました。";
}

/* 下記はデザイン用 */
#setName.change,#setName.reset {
    border-color: #9ad4de;
    background-color: rgba(154, 212, 222, 0.25);
}


名前変換ありの本文
記述はこのようにします。
<div id="d-name-default"  data-last="デフォルト名字" data-first="デフォルト名前" data-last-hira="でふぉるとみょうじ" data-first-hira="でふぉるとなまえ"></div>
<div id="d-name-nvl">
    <!-- 本文 -- >
</div>

一つずつ解説していきます。

<div id="d-name-default" data-last="デフォルト名字" data-first="デフォルト名前" data-last-hira="でふぉるとみょうじ" data-first-hira="でふぉるとなまえ"></div>
タグはdivにしていますがなんでもいいです。こちらは
<script src="js/name-change.js"></script>
より上に記述してあればどこに記述しても構いません。今回は本文の前に記述しています。
id="d-name-default"でJavaScriptから取得できるようにします。
data-last=""はデフォルトの名字を入れます。省略可能です。
data-first=""はデフォルトの名前を入れます。省略可能です。
data-last-hira=""はデフォルトの名字のひらがな(読み方)を入れます。省略可能です。
data-first-hira=""はデフォルトの名前のひらがな(読み方)を入れます。省略可能です。
この省略可能は、本文で使用しない場合に記述しなくていいようにするものです。

id="d-name-nvl"の中に小説本文を流し込みます。これは上記で設定したデフォルト名と変換フォームで設定した名前を置き換えるためのものです。このidがないと変換されないので注意です。

ひらがなのタグ
名字や名前のひらがな部分にはタグを追加します。
デモ版の場合はこのようにしてます。
「俺は<em>愛上陵</em>!”<em><span class="last-hira">あいうえ</span></em><em><span class="first-hira">おか</span></em>”って読むんだぜ!珍しいだろ?」<br>
 ──<em><span class="last-hira">あ……いう……</span></em>──<br>
「ん?いまなにか聞こえたか?」<br>
 ──<em><span class="last-hira">あい……うえ……</span></em><em><span class="first-hira">お……か</span></em>──<br>
「やっぱ誰かに呼ばれてる!」

名字のひらがなを
<span class="last-hira"></span>
名前のひらがなを
<span class="first-hira"></span>
で囲みます。
このとき間に三点リーダ(…)や句読点が入っても大丈夫です。
もしひらがなの部分に別のタグ(強調タグやルビタグなど)を使いたい場合は、spanタグの外側に設置します。

name-change.js
処理はこちらです。
/* 名前設定 */
function setName() {
    const formEl = document.forms.d_name;
    let change = false;
    change = setItem(formEl.lastname,'lastname');
    change = setItem(formEl.firstname,'firstname');
    change = setItem(formEl.lastname_hira,'lastname_hira');
    change = setItem(formEl.firstname_hira,'firstname_hira');
    if (!change) { return; }
    document.getElementById("setName").classList.remove("reset");
    document.getElementById("setName").classList.add("change");
    /* 同じページ内に変換箇所がある場合 */
    changeName();
}

/* 項目別 */
function setItem(elName,cookieId){
    const name = elName ? elName.value : '';
    if(name === null || name === '') return false;
    Cookies.set(cookieId, name);
    return true;
}

/* 名前変換時にテキストエリアを変換した名前に保持 */
function keepName() {
    const formEl = document.forms.d_name;
    if (!formEl) { return; }
    keepItem(formEl.lastname,'lastname');
    keepItem(formEl.firstname,'firstname');
    keepItem(formEl.lastname_hira,'lastname_hira');
    keepItem(formEl.firstname_hira,'firstname_hira');
    document.getElementById("setName").classList.remove("reset");
    document.getElementById("setName").classList.remove("change");
}

/* 項目別 */
function keepItem(elName,cookieId){
    let input = elName ?? null;
    if(input === null) return;
    const name = Cookies.get(cookieId) ?? input.placeholder;
    if(input.placeholder !== name){
        input.value = name;
    }
}

/* 名前リセット */
function resetName() {
    const formEl = document.forms.d_name;
    let reset = resetItem(formEl.lastname,'lastname');
    reset = resetItem(formEl.firstname,'firstname');
    reset = resetItem(formEl.lastname_hira,'lastname_hira');
    reset = resetItem(formEl.firstname_hira,'firstname_hira');
    if(reset){
        ajaxReload(); // 同一ページに変換箇所がある場合
        document.getElementById("setName").classList.remove("change");
        document.getElementById("setName").classList.add("reset");
    }
}

/* 項目別 */
function resetItem(elName,coolieId){
    Cookies.remove(coolieId);
    if (!elName) return false;
    else elName.value = '';
    return true;
}

/* 名前変換 */
function changeName() {
    let d_name_nvl = document.getElementById('d-name-nvl');
    if (d_name_nvl === null) { return; }
    let str = changeItem('lastname', 'data-last', d_name_nvl.innerHTML);
    str = changeItem('firstname', 'data-first', str);
    d_name_nvl.innerHTML = str;
    changeHiragana('lastname_hira', 'data-last-hira', 'last-hira');
    changeHiragana('firstname_hira', 'data-first-hira', 'first-hira');
}

/* 項目別 */
function changeItem(cookieId,attrName,str){
    const data_d = document.getElementById('d-name-default').getAttribute(attrName);
    const name = Cookies.get(cookieId);
    if(name && data_d) return str.replace(new RegExp(data_d,'g'),name);
    return str;
}

/* ひらがな対応 */
function changeHiragana(cookieId,attrName,className){
    const data_d = document.getElementById('d-name-default').getAttribute(attrName);
    const name = Cookies.get(cookieId);
    if(!(name && data_d)) return;
    let arr = document.getElementsByClassName(className);
    for(i = 0;i<arr.length;i++){
        let el = arr[i];

        let defHira = data_d.split('');
        let nameHira = name.split('');
        let str = el.innerText.split('');
        let txt ='';
        let nameCount = 0;
        for (k = 0; k < str.length; k++) {
            if(!str[k].match(/^[ぁ-んー ]+$/) || defHira.length <= nameCount){
                txt += str[k];
            }else{
                let replace;
                if(nameCount < nameHira.length){
                    replace = nameHira[nameCount];
                    txt += str[k].replace(defHira[nameCount], replace);
                    nameCount++;
                }else if(nameHira.length < defHira.length){
                    // 設定された名前のほうが短ければ以降はつけない
                    break;
                }
            }
        }
       
        // 不足分追加
        if(defHira.length < nameHira.length){
            let substr = name.substr(nameCount);
            txt += substr;
        }

        el.innerText = txt;
    }
}

/* 同じページに変換がある場合 */
function ajaxReload(){
    let d_name_nvl = document.getElementById('d-name-nvl');
    if (d_name_nvl) {
        let url = location.href + "?date="+new Date().getTime();
        let ajax = new XMLHttpRequest;
        ajax.open("GET",url,true);
        ajax.onload = function(){
            var res = ajax.responseText;
            var parse = new DOMParser().parseFromString(res,"text/html");
            d_name_nvl.innerHTML = parse.getElementById('d-name-nvl').innerHTML;
        };
        ajax.send(null);
    }
}

/* 読み込み直後 */
window.onload = function () {
    /* 入力フォームの設定 */
    keepName();
    /* 名前変換 */
    changeName();
};

同一ページに変換箇所がある場合の処理も追記しておきました。
もしない場合はその部分は削除して大丈夫です。
名前を変換する場合は文字を置き換え、リセットする場合は名前変換のあるタグ(d_name_nvl)の中だけをajaxをつかってリロードしています。
一部のみリロードについてはjavascriptでページを一部だけ更新する方法を参考にしました。
ajaxでもどってくる内容はテキストデータなので、htmlにパースして利用しています。テキストのパースは【JavaScript】DOMParserで文字列をHTMLElement・Nodeに変換するを参考にしました。

最後に
実装したのは割と前で記事にしようとしてなかなかできていなかったので、ようやく記事にできて嬉しいです。
「もっと汎用性がほしい!」「もっと使いやすい方がいい!」「全ページ同じにしたい!」と言った場合は、すでに配布されている素晴らしいスクリプトがありますのでそちらを使っていただくといいかもしれないです。
私の場合「名前にいちいちタグしたくない」「デフォルト名が小説によって違う」と自分で用意したほうが良さそうだったので用意してみました。
同じように自作しようとしている方の参考になれば幸いですっ

それと記事に関係ないですが、Twitter(現X)アカウントを作成しました!
Twitter(現X)アカウント てくてく@個人サイト向け情報共有サイト
よければフォローして下さいませ♪

参考
js-cookie
CDN js-cookie
javascriptでページを一部だけ更新する方法
JavaScript】DOMParserで文字列をHTMLElement・Nodeに変換する
DOMParser の parseFromString() メソッドの使い方
個人サイトに連載系の小説を載せるとき、前ページや次ページのリンクをフッターに貼るのですが毎回毎回設定するの、ちょっと面倒になってきちゃったんですよね。
PHPのパーツ共通化利用してなんかいい感じにできないかな~と模索して、無事実装できたのでご紹介します。
なお、高速化などは考えておりませんのであしからず!
【2023/09/26追記】一部プログラムに不足があったので修正しました。

この記事で最終的にできるコト
前ページ/次ページのリンクを自動設定。

今回の確認環境
  • Windows10
  • Google Chrome
  • XAMPP v3.3.0(PHP8.2)
  • ロリポップサーバー(PHP8.2LiteSpeed版)

※XAMPP使用の場合、VirtualHost利用を前提として執筆しています。
VirtualHostは前記事(ローカルでもドメイン使用!?XAMPPのVirtualHostとはっ)を参照くださいませ。

今回のディレクトリ構成
サイトフォルダ
├novels
|├001.php
|├002.php
|├003.php
|└004.php
├parts
|├foot.php
|└head.php
└index.php

上記の中で前ページ/次ページを入れるのはnovelsフォルダ内のみです。
しかしフォルダによってfooterの中身を変えるのすら面倒なので、すべて同じfooterを使います。
処理の都合上、novelsフォルダ内は桁を揃えて連番にしてあります。

各ページ
今回パーツの共通化を前提としています。
よって各ページは下記のようにしております。

head.php
<!DOCTYPE html>
<html>
<head>
    <!-- 省略(headタグの中身) -->
</head>
<body>
    <header>
        <!-- 省略(headerタグの中身) -->
    </header>


foot.php
<!-- ここに処理を書きます。 -->
<footer>
    <nav>
        <ul>
            <!-- 前/次のリンク差し込み -->
            <li><a href="#">pagetop</a></li>
        </ul>
    </nav>
</footer>
</body>
</html>

※最低限のみ載せたのでscript読み込みなど色々省いています。

共通化したパーツを読み込む各ページ
index.phpや001.phpなどです。
<?php include_once($_SERVER['DOCUMENT_ROOT']."/parts/head.php"); ?>
<!-- 省略(bodyタグでheaderより下、footerより上の中身) -->
<?php include_once($_SERVER['DOCUMENT_ROOT']."/parts/foot.php"); ?>



追記する処理と解説
foot.phpに追記していきます。
<!-- ▼▼ここから追記▼▼ -->
<?php
if(basename(getcwd()) === "novels"){
    $array = glob("*");
    $current = pathinfo($_SERVER['REQUEST_URI'], PATHINFO_BASENAME);
    $is_prev = false;
    for($i = 0 ; $i < count($array) ; $i++ ){
        $filename = pathinfo($array[$i], PATHINFO_BASENAME);
        if($current === $filename){
            $is_prev = true;
            continue;
        }
        if(!$is_prev){
            $prev = $filename;
            continue;
        }
    /* 2023/09/26 修正部分 */
        $next = $filename;
        break;
    }
}
?>
<!-- ▲▲ここまで追記▲▲ -->
<footer>
    <nav>
        <ul>
            <!-- ▽▽ここから追記▽▽ -->
            <?php echo isset($prev) ? "<li class='prev'><a href='{$prev}'></a></li>" :"";
            echo isset($next) ? "<li class='next'><a href='{$next}'></a></li>" :""; ?>
            <!-- △△ここまで追記△△ -->
            <li><a href="#">pagetop</a></li>
        </ul>
    </nav>
</footer>
</body>
</html>


それでは一つずつ見ていきます。

if(basename(getcwd()) === "novels")
こちらの条件分で、現在開かれているファイルがnovelディレクトリ以下のものであれば処理をするようにします。
getcwd()は現在のディレクトリを出してくれる関数で、今回のディレクトリ構成だと「サイトフォルダ/novels」まで出してくれます。
ルートからはいらないので、basename()を使いディレクトリが「novels」であるかどうか確認するようにします。

$array = glob("*");
glob()は指定したパス以下のファイルをすべて取得してくれる便利な関数です。
今回はnovelsディレクトリ以下のファイルで呼び出されていることを前提に、現在のディレクトリの中身のみ取得します。*(アスタリスク)で現在のディレクトリの中身をすべて取得してくれます。

$current = pathinfo($_SERVER['REQUEST_URI'], PATHINFO_BASENAME);
現在のファイル名を取得します。
pathinfo()は指定したパスからファイル名や拡張子などを取得してくれます。
$_SERVER['REQUEST_URI']はルートからファイルまでのパスを示しています。
PATHINFO_BASENAMEを第二引数に設定することで、ファイル名+拡張子のみを取得するようにします。
これにより$currentには「001.php」など現在開かれているファイル名のみが取得できます。
なお「.htaccess」などで拡張子を隠す処理をしている場合は、PATHINFO_BASENAMEPATHINFO_FILENAMEにするとファイル名のみで拡張子は抜いてくれます。

$is_prev = false;
for($i = 0 ; $i < count($array) ; $i++ ){
    $filename = pathinfo($array[$i], PATHINFO_BASENAME);
    if($current === $filename){
        $is_prev = true;
        continue;
    }
    if(!$is_prev){
        $prev = $filename;
        continue;
    }
  /* 2023/09/26 修正部分 */
    $next = $filename;
    break;
}


$is_prevは前ページがあるかどうかです。先にfalseで初期化しておきます。
ループで取得したファイルの数だけ処理を回します。
$filenameでファイル名+拡張子を取得し、その後は現在ファイル($current)と一致するかどうか判定します。
if($current === $filename):一致したら前ページの設定は済んでいるとして$is_prevtrueにしてから次のループへ移動します。
if(!$is_prev):一致していない且つ前ページの設定が済んでいなければ$prevにファイル名を設定し次のループへ移動します。
if ($i !== count($array) - 1):一致しない且つ前ページの設定は済んでいる、且つファイルが最後でない場合は次ページを設定してループを抜けます。
【2023/09/26修正】ページ数が3ページしか無いとき、2ページ目から3ページ目へのリンクが表示されない条件文だったので修正しました。
修正前は「最後のファイルでなければ次ページを設定」としていましたが、次ページを設定した時点でループを抜けるようにしました。


一番最初のファイル(001.php)には前ページがないので、ファイルが一致した時点で前ページ設定済みとみなし次のループへ行きます。そして次は002.phpファイルなので、次ページ設定の条件文に入り、次ページ設定をしてループを抜ける流れになります。
一番最後のファイル(004.php)であればループの回数とファイルの個数-1が一致して次ページを設定せずループを終了します。
(ファイルの個数は4つだけど配列では0から数えるので$iが3であれば最後のループということになります)

【2023/09/26修正】次ページが設定された時点でループを抜けるようにしたので、最後のページまで確認しないようにしました。
現在ページが最後のファイルと同じだった場合、if($current === $filename)を満たして次のループに行こうとしますがforの条件がページ数分回すことなのでループが終了します。
現在ページが最後のファイルでない且つ前ページの設定が済んでいる場合は、次ページを設定してループを抜けるように修正しました。


<?php echo isset($prev) ? "<li class='prev'><a href='{$prev}'></a></li>" :"";
echo isset($next) ? "<li class='next'><a href='{$next}'></a></li>" :""; ?>

isset()は値が設定されているかどうかの関数です。
NULLかどうかや空かどうかではなく、undefinedかどうかの違いです。
isset($prev)で前ページが設定されていたら"<li class='prev'><a href='{$prev}'></a></li>"で前ページのリンクを差し込みます。されていなければ何も表示しません。
同じようにisset($next)で次ページが設定されていたら"<li class='next'><a href='{$next}'></a></li>"で次ページのリンクを差し込みます。されていなければ何も表示しません。

処理まとめ
最終的にfoot.phpはこのようになります。
<?php
if(basename(getcwd()) === "novels"){
    $array = glob("*");
    $current = pathinfo($_SERVER['REQUEST_URI'], PATHINFO_BASENAME);
    $is_prev = false;
    for($i = 0 ; $i < count($array) ; $i++ ){
        $filename = pathinfo($array[$i], PATHINFO_BASENAME);
        if($current === $filename){
            $is_prev = true;
            continue;
        }
        if(!$is_prev){
            $prev = $filename;
            continue;
        }
        /* 2023/09/26 修正部分 */
        $next = $filename;
        break;
    }
}
?>
<footer>
    <nav>
        <ul>
            <?php echo isset($prev) ? "<li class='prev'><a href='{$prev}'></a></li>" :"";
            echo isset($next) ? "<li class='next'><a href='{$next}'></a></li>" :""; ?>
            <li><a href="#">pagetop</a></li>
        </ul>
    </nav>
</footer>
</body>
</html>


最後に
今回は高速化や桁を揃えていない連番を無視して実装してあります。
もし桁が不揃い場合はこちらの記事がわかりやすいので参考にしてみてください。
高速化については、個人サイトの範囲ならそこまで気にしなくていいかなとは思いますがglob()よりopendir()のほうが早い!というこちらの記事もあるので参考にしてみてください。
これで小説ページを追加するたびに前次リンクを追加しなくて済む!わーい!!と喜んでいるとーれでございました!

参考リンクまとめ
敬称略
php:pathinfo
二色人日記:【PHP】特定のディレクトリ内の連番が振られている画像を順番に出力する方法
あたまのなかがとっちらか~る:PHPでファイル一覧を取得するならglobよりopendirのほうが27倍早い
.htaccessやサーバー設定でBasic認証ができるわけですが、ブラウザからIDやパスワードを求められるとちょっとびっくりします。
昔そういう個人サイトを見たことがあるのですが、フォレストやnanoなどの認証ばかり触れていたせいでブラウザからの認証に「なんだこれ!怖い!」と避けていた記憶が蘇ります。
サイトを運営する立場になった今ならあれはそこまで恐れるものでもないとわかるのですが、せっかくならログイン周りもデザインしたいところ・・・
個人サイトで入り用だったのもあり、JavaScriptで簡単なログイン認証を自作してみました!
今回はログインに焦点を絞りましたが、流用すればログインに限らないパスワード認証にも使えると思います。

デモ
こちらのデモページから動作確認ができます。
仕様
①indexページでパスワード認証
成功:コンテンツページへ飛ぶ
失敗:「パスワードが違います」と表示

②ログインに成功したら、次回以降パスワードは自動入力
 今回はパスワード保存の期限を設定していないので、使用したライブラリのデフォルト期間(365日)はCookieで保存されます。

③ログインしていない状態でindex以外のページへいったら、内容を隠してログイン画面をポップアップ表示
成功:ポップアップが消えて中身が見える
失敗:画面はそのまま「パスワードが違います」と表示される

④ログインしていればindex以外のページへ直接いってもログイン画面が出ず中身を見れる

⑤コードからパスワードが読まれないようハッシュ化
暗号化:暗号化されたデータを元のデータに戻せるタイプ
ハッシュ化:暗号化されたデータは元のデータに戻せない

こんな感じですね。
こちらの作り方を解説していきます~

準備
  • jQuery…jquery-cookieのために必要です
  • jquery-cookie…jQuery用いたCookie操作用
  • jsSHA…パスワードのハッシュ化用

ハッシュ化はjsSHAを使います。
「Newest Release / Download」のGitHubにリンクがあるので、そこからDLページへ飛びます。
Source code(zip)かSource code(tar.gz)のどちらかを選んでDLしてください。
いろいろなファイルが入っていますが、必要なのは「jsSHA-3.3.0/jsSHA-3.3.0/dist」にあるsha256.jsファイルだけです。

ファイル構成
index.html
contents.html
css/style.css
js/
├login.js
└3rd/sha256.js
さきほどDLしたsha256.jsをこの位置に入れます。
自分が作ったものと区別できるよう3rdフォルダに入れていますが、パスが通っていればどこでもOKです。
login.jsにログインの処理を書いていきます。

JavaScriptの読み込み
まずJavaScript周りの読み込みしておきましょう。
ログイン認証したいページやログインしていなければ中を見せたくないページに各種読み込んでいきます。
今回だとindex.htmlとcontents.htmlですね。
htmlファイルのbodyの閉じタグ直前に下記を記述します。
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.4.min.js" integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8=" crossorigin="anonymous"></script>
<!-- 3rd -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.js"></script>
<script src="js/3rd/sha256.js"></script>
<!-- script -->
<script src="js/login.js"></script>

jQueryのCDNはこちらをコピペしてます。
今回はjQuery Core 3.6.4 minifiedを使いました。
配布プログラムを利用していてjQueryを入れてるよ!ということであればそちらを使うのが良いかと。

jquery-cookieのCDNはこちらの最新版を使ってます。
今回はVersion1.4.1を使いました。URLが2つありますが下の方を使ってます(どちらでも大丈夫です)。

sha256.jsは準備項目でDLしたファイルです。
login.jsはログイン周りの処理を書くファイルです。

index.html
ログイン周りに必要な部分だけ抜粋します。
<div id="loginResult">パスワードが違います</div>
<div>
    <input type="password" placeholder="password" id="loginPass">
    <input type="button" value="送信" id="login">
</div>

loginResultにはパスワードが異なるときの文章を記述します。
inputdivタグで囲っていますが、これはformにするとエンターキーでsubmitになってしまうことからdivにしてます。そのあたりの処理(エンターキーで認証処理を実行)を書けるようであればformでもOKかと!

そしてパスワードを入力するのが
<input type="password" placeholder="password" id="loginPass">
の部分です。type="password"で伏せ字にしてますがtype="text"でもOK。

パスワード認証の処理は
<input type="button" value="送信" id="login">
のクリックで実行するようにします。

contents.html
今回はわかりやすいようにcontents.htmlの名前になっていますが、ログインしないと見れないファイルに記述する内容です。
まずログインするまでぼかしておきたいタグにclass="passcheck"というクラスを追加します。
bodyに追加するとログイン用のポップアップもぼかしてしまうのでbodyに入れないようにします。
例えば
<body>
    <header class="passcheck">
        <!-- ヘッダーの記述 -->
    </header>

    <main class="passcheck">
        <!-- ここにコンテンツの記述 -->
    </main>
    
    <footer class="passcheck">
       <!-- ここにフッターの記述 -->
    </footer>

    <!-- 各種scriptの読み込み(省略) -->
</body>

といった感じです。
ぼかさなくていいなってものがあればクラスを外してください。

次にログイン用のポップアップを記述します。
<div class="overlay">
    <div class="popup-window">
        <h2>Login</h2>
        <div>
            <div id="loginResult">パスワードが違います</div>
            <div>
                <input type="password" placeholder="password" id="loginPass">
                <input type="button" value="送信" id="login">
            </div>
            <a href="index.html">indexへ</a>
        </div>
    </div>
</div>

index.htmlへ戻るリンクはindexページへ戻れるように貼ってます。不要なら削除してOK。
必ずpasscheckの外側に記述してください
demoだと下記のようにmainタグとfooterタグの間に記述してます。
<main class="passcheck">
    <!-- ここにコンテンツの記述 -->
</main>

<!-- ログイン用ポップアップ -->
<div class="overlay">
    <div class="popup-window">
        <h2>Login</h2>
        <div>
            <div id="loginResult">パスワードが違います</div>
            <div>
                <input type="password" placeholder="password" id="loginPass">
                <input type="button" value="送信" id="login">
            </div>
            <a href="index.html">indexへ</a>
        </div>
    </div>
</div>

<footer class="passcheck">
    <!-- ここにフッターの記述 -->
</footer>


css
#loginResult#loginResult.error.overlay.popup-windowを記述していきます。

ログイン失敗表示
#loginResult{
    display: none;
    /* 下記はデザイン用 */
    padding: 10px;
    margin: 10px auto;
    width: 80%;
    border: 1px solid transparent;
}

#loginResult.error{
    display: block;
    /* 下記はデザイン用 */
    border-color: #de9a9a;
    background: rgba(222, 154, 154, 0.25);
}

デフォルトは非表示で、errorクラスを追加したら表示するようにします。

ポップアップ
.overlay {
    display: none;
    position: fixed;
    z-index: 9999;
    width: 100%;
    height: 100vh;
    top: 0;
    left: 0;
    background: rgba(0, 0, 0, .5)
}

.popup-window{
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    /* 下記はデザイン用 */
    width: 90vw;
    max-width: 380px;
    padding: 20px 0;
    background: #3a3a3a;
}

デフォルトは非表示です。表示処理をJavaScriptで行います。

そしてぼかし用のクラスです。
.blur {
    filter: blur(10px);
}


login.js
ここが要ですね!
まずスクリプトを書いていく前にパスワードを用意します。
SHA256に変換してくれるサイトでSHA256変換したパスワードを用意します。
わかりやすさからこちらのサイトで用意しました。出力形式は小文字です。
今回使うライブラリのjsSHAページにあるデモを使ってもいいでしょう。
その場合はHasing Demoの欄に以下の設定で出力します。
Input Text: パスワードにしたい文字列を入力
Input Type: TEXT
SHA Variant: SHA-256
Number of 1
Rounds: Output Type: HEX
Output Hashに出力された文字列がハッシュ化されたパスワードです。

login.jsに記述するコード全文はこちらです。
$(function(){
    const PASS_HASH = 'さきほど出力したハッシュ化されたパスワード';

    /* index.htmlでない且つCookieのハッシュ化パスワードが異なっていたら
       passcheckクラスにぼかしをつける+ポップアップを表示 */
    if(!location.pathname.includes('index.html') && $.cookie('loginhash') != PASS_HASH){
        $('.passcheck').addClass('blur');
        $('.overlay').show();
    }
    
    /* Cookieにログインパスワードが残っていたら自動入力 */
    $('#loginPass').val($.cookie('loginpass'));

    /* ログインボタンをクリックしたら認証処理 */
    $('#login').click(function() {
        const pass = $('#loginPass').val();
        const shaObj = new jsSHA('SHA-256','TEXT');
        shaObj.update(pass);
        const hash = shaObj.getHash('HEX');
        if (PASS_HASH === hash) {
            /* 入力されたパスワードが合っていたら、Cookieに保存 */
            $.cookie('loginpass',pass);
            $.cookie('loginhash',hash);
            if(location.pathname.includes('index.html')){
                /* index.htmlにいたらコンテンツページへ飛ぶ */
                window.location.href = 'contents.html';
            }else{
                /* index.htmlでなければ、ぼかしを消してポップアップを非表示 */
                $('.passcheck').removeClass('blur');
                $('.overlay').hide();
            }
        }else{
            /* パスワードが不一致なら「パスワードが違います」のエラー表示 */
            $('#loginResult').addClass('error');
        }
    });
})

.htaccessで拡張子を表示しない設定をしている場合location.pathname.includes('index.html')の拡張子も消してください。
セキュリティ的にはまだまだかと思いますが、パッと見でパスワードがわからないくらいにはなってるかなと思います。
もっとしっかりやるならパスワード管理用のファイルを用意したほうがいいのですが、そこまでガチガチじゃないくていいかなというのと、ファイル読み込みなどの処理が面倒だったので省きました。

最後に
phpで作ってみようかな~と思ったのですが、ちょっとまだ勉強不足で時間がかかりそうだったのでJavaScriptで作りました。
ゆくゆくはきちんと中身が見られないようパスワード用のファイルを用意したり、パスワードの設定そのものをできるようにしたいところ・・・
とりあえずなものではあるものの、割と悪くないんじゃないかな?と個人的に思ってます。
上手く出来ると楽しい~~!

参考
【超シンプル】ポップアップをHTMLとCSSだけで実装する

DASHBOARD

つぶやき

アカウント作成しました!
Twitter(現X)

全文検索

カレンダー

日付一覧を表示
2023年9月
12
3456789
10111213141516
17181920212223
24252627282930

Thanks !

Link

Contact

目次