少し前まで、この手の登録サイトが数知れず存在したけど今ではあまり見られなくなった。その内容は、サイトの管理者に自身のサイト情報を送り、審査登録された後、カテゴリ別に分けてアクセス順や人気順などランク表示されて、登録者はSEO対策やHPの紹介などに利用していた。

そんな訳で、基本的な登録サイト作成してみた。機能の追加やセキュリティ対策など構築すると利便性が高まるかも知れません。以下に、基本的な仕様と各コードを公開します。

基本仕様
1.前提条件 登録者希望者はサイトのホームページから登録依頼をして、管理者からの返信で指定されたIDを入力すればサイトの登録可能となる。現在の仕様は、ID登録は自身の好きなIDで登録できる仕様。
2.お好きなバナー画像登録(JPEG・PNG・GIF等)サイズは適度の大きさですが、登録表示は画像縮小されます
3.サイトのタイトル(12文字以内)
4.サイトの紹介(100文字以内)
5.サイトのURL(アドレス ドメイン基本仕様)
6.ID入力(4桁英数字)
7.ID入力(4桁英数字 登録者削除機能)
8.管理者側サイト(個別削除、全体削除、登録者のID、タイトル表示)
ご利用について
このシステムはコードを公開していますので、機能追加・修正・削除・デザイン変更等、ご自由にアレンジして頂いて構いません。また、コードをそのままコピー&ペーストして動作不具合が発生しても作成者は責任を負う事は致しません。
※ほぼ原形のままご使用された場合はどこか小さく片隅にMOMOPLANと書いて頂くと嬉しいです。

IDはXになっていますが実際は英数字4桁です
ファイルの階層
site_registration/
├── index.html
├── script.js
├── style.css
├── savedata.php
├── loadcsv.php
├── deleteEntry.php
├── uploads/
├── data.csv
├── admin/ ← 🔒 管理ページ専用アクセス制限フォルダ
 ├── admin.html
 ├── admin.js
 ├── admin.css
 ├── deleteAll.php
ファイルパスの修正(管理ページ専用)
<link rel="stylesheet" href="admin/admin.css">
<script src="admin/admin.js"></script>
admin.js の fetch() リクエストのパス修正
fetch("admin/deleteAll.php")
BASIC認証を設定(Apache)
AuthType Basic
AuthName "管理者専用ページ"
AuthUserFile /path/to/.htpasswd
Require valid-user
ファイルへジャンプ
index.html script.js style.css savedata.php loadcsv.php deleteEntry.php admin.html admin.js deleteAll.php auth.php admin.css
クライアント側
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>サイト登録</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>サイト登録フォーム</h1>
    <form id="registrationForm" enctype="multipart/form-data">
        <label>バナー画像<input type="file" id="banner" name="banner" accept="image/*"></label><br>
        <label>サイトタイトル(12文字以内)<input type="text" id="title" name="title" maxlength="12"></label><br>
        <label>サイト紹介(100文字以内)<input type="text" id="description" name="description" maxlength="100"></label><br>
        <label>サイトURL: <input type="url" id="url" name="url"></label><br>    
        <label>ID(英数字4文字)<input type="text" id="siteId" name="siteId" maxlength="4" pattern="[A-Za-z0-9]{4}"></label>         
            <button type="submit">登録</button>
            <div style="display: flex; align-items: center;"></div>
            <span id="formMessage" style="margin-left: 10px;"></span> <!-- メッセージをボタンの横に配置 -->
        </div>    
    </form>
    <form id="deleteForm">
        <label>ID(登録したサイトを削除します)<input type="text" id="deleteId" name="deleteId" maxlength="4" pattern="[A-Za-z0-9]{4}"></label>
        <button type="submit" class="delbutton">削除</button>
        <span id="deleteMessage"></span> <!-- メッセージを表示する要素 -->
    </form>
    <img src="mlmain.png" id="img-howto">
    <center><h2>【素材登録一覧】</h2></center>
    <table id="siteTable">
        <thead>
            <tr>
                <th>登録順</th><th>バナー</th><th>タイトル</th><th>紹介</th><th>URL</th>
            </tr>
        </thead>
        <tbody></tbody>
    </table>
    <script src="script.js"></script>
</body>
</html>
JS
document.getElementById("registrationForm").addEventListener("submit", function(event) {
    event.preventDefault();
    /*let formData = new FormData(this);*/
    let title = document.getElementById("title").value;
    let description = document.getElementById("description").value;
    let url = document.getElementById("url").value;
    let siteId = document.getElementById("siteId").value;
    let lastSubmitTime = localStorage.getItem("lastSubmitTime");
    let banner = document.getElementById("banner").files[0];
    let messageElement = document.getElementById("formMessage");
    let currentTime = Date.now();

    if (lastSubmitTime && (currentTime - lastSubmitTime) < 5000) { // 5秒以内の連続登録禁止
        document.getElementById("formMessage").textContent = "❌ 短時間での連続登録は禁止されています。";
        document.getElementById("formMessage").style.color = "red";
        return;
    }

    localStorage.setItem("lastSubmitTime", currentTime);

    if (!title || !description || !url || !siteId || !banner) {
        messageElement.textContent = "❌ 全ての項目を入力してください。";
        messageElement.style.color = "red";
        return;
    }

    let formData = new FormData();
    formData.append("title", title);
    formData.append("description", description);
    formData.append("url", url);
    formData.append("siteId", siteId);
    formData.append("banner", banner);

    fetch("savedata.php", {
        method: "POST",
        body: formData
    }).then(response => response.text())
      .then(data => {
          console.log("受け取ったレスポンス:", data); // ✅ 確認用ログ

          messageElement.textContent = data; // ✅ まずメッセージを表示
          messageElement.style.color = data.includes("✅") ? "green" : "red"; // ✅ 成功なら緑、エラーなら赤

          // ✅ 成功時のみリロード
          if (data.includes("✅")) {
              setTimeout(() => {
                  location.reload();
              }, 2000); // 2秒後にリロード(ユーザーがメッセージを確認できるように)
          }
      }).catch(error => {
          console.error("通信エラー:", error);
          messageElement.textContent = "❌ サーバーとの通信エラーが発生しました。";
          messageElement.style.color = "red";
      });
});

function loadSites() {
    fetch("loadcsv.php")
        .then(response => response.json())
        .then(data => {
            const tbody = document.querySelector("#siteTable tbody");
            tbody.innerHTML = "";
            data.forEach(site => {
                const row = document.createElement("tr");
                row.innerHTML = `
                    <td>${site.id}</td>
                    <td>${site.bannerPath ? `<img src="${site.bannerPath}" width="120">` : "画像なし"}</td>
                    <td>${site.title}</td>
                    <td>${site.description}</td>
                    <td><a href="${site.url}" target="_blank">${site.url}</a></td>
                `;
                tbody.appendChild(row);
            });
        });
}

document.getElementById("deleteForm").addEventListener("submit", function(event) {
    event.preventDefault();
    let siteId = document.getElementById("deleteId").value;
    let messageElement = document.getElementById("deleteMessage");

    fetch("deleteEntry.php", {
        method: "POST",
        body: new URLSearchParams({ siteId })
    }).then(response => response.text())
      .then(data => {
        messageElement.textContent = data; // 結果メッセージをボタン横に表示
          let icon = data.includes("成功") ? "✅ " : "❌ ";
          messageElement.textContent = icon + data; // メッセージの先頭にアイコンを追加
          messageElement.style.color = data.includes("成功") ? "green" : "red"; // 成功なら緑、失敗なら赤
          loadSites(); // 削除後にテーブル更新
      });
});

document.addEventListener("DOMContentLoaded", loadSites);
CSS
body {
    font-family: Arial, sans-serif;
    background-image: url("s-brick008.gif");
}

label {
    gap: 100px; /* 各要素の間隔 */
    display: inline-block;
    width: 240px; /* ラベルの固定幅を設定 */
    text-align: left; /* 左揃え */
    color: rgb(10, 23, 99);
    text-shadow: 1px 1px 1px #5c8fd3;
}

input, textarea {
    width: 220px;
    padding: 8px;
    border-radius: 6px;
    border: 2px solid #382ebb;
    font-size: 16px;
}

input[type="file"] {
    width: auto; /* ファイル選択はデフォルトの幅 */
}

button {
    padding: 4px 16px;
    font-size: 1rem;
    text-align: center;
    color: #fff;
    text-shadow: 1px 1px 1px #000;
    border-radius: 10px;
    background-color: rgb(118, 37, 250);
    background-image: linear-gradient(
        to top left,
    rgba(0, 0, 0, 0.2),
    rgba(0, 0, 0, 0.2) 30%,
    rgba(0, 0, 0, 0)       
    );
}

button:hover {
    background-color: #d32f2f;
}

.warning {
    color: #d32f2f; /* 赤系 */
    font-weight: bold;
    margin-top: 10px;
}

.success {
    color: #4CAF50; /* 緑系 */
    font-weight: bold;
    margin-top: 10px;
}

table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 20px;
    font-size: 16px;
    text-align: left;
}

thead {
    background-color: #4067e7; /* 緑系のヘッダー */
    color: white;
    font-weight: bold;
    border: 1px solid #010513; /* 罫線の色 */
    text-align: center;
}

th, td {
    border: 1px solid #010513; /* 罫線の色 */
    padding: 10px;
}

tbody tr:nth-child(even) {
    background-color: #f2f2f2; /* 偶数行の背景色 */
}

tbody tr:hover {
    background-color: #ddd; /* ホバー時の背景色 */
}

.warning {
    color: red;
    font-weight: bold;
    margin-left: 10px;
}

.form-group {
    grid-template-columns: 150px 1fr; /* ラベルと入力フィールドの幅設定 */
}

#banner{
    font-weight: bold;
    border-radius: 10px;
    color: rgb(255, 255, 255);
    background-color: rgb(118, 37, 250);
}

#img-howto{
    position: absolute;
    left:  400px;
    top: 114px;
  }
  
#deleteMessage {
    margin-left: 10px;
    font-weight: bold;
}

#formMessage {
    font-weight: bold;
    margin-left: 10px; /* ボタンの横に配置 */
}

.delbutton{
    background-color: rgb(209, 32, 106);   
}
PHP(savedata.php)
<?php
session_start();
header("Content-Type: text/plain"); // ✅ 明示的にテキストレスポンスを設定

if ($_SERVER["REQUEST_METHOD"] == "POST") {
    $title = trim($_POST["title"]);
    $description = trim($_POST["description"]);
    $url = trim($_POST["url"]);
    $siteId = trim($_POST["siteId"]);

    // ✅ 既存IDのチェック(登録前に確認)
    $file = fopen("./data.csv", "r"); //ファイルパスは環境に応じて変えて下さい
    while (($record = fgetcsv($file)) !== false) {
        if ($record && isset($record[0]) && trim($record[0]) === $siteId) {
            fclose($file); 
            exit("❌ このIDは既に登録されています。");
        }
    }
    
    fclose($file);

    // ✅ 画像アップロード処理のチェック(画像のパスを正しく取得)
    $bannerPath = "画像なし"; // デフォルト値
    if (isset($_FILES["banner"]) && $_FILES["banner"]["error"] === UPLOAD_ERR_OK) {
        $uploadDir = "uploads/";
        $fileName = uniqid() . "_" . basename($_FILES["banner"]["name"]);
        $uploadFile = $uploadDir . $fileName;

        if (move_uploaded_file($_FILES["banner"]["tmp_name"], $uploadFile)) {
            $bannerPath = $uploadFile; // ✅ 画像のパスをセット
        }
    }

    $file = fopen("./data.csv", "a");
    fputcsv($file, [$siteId, $title, $description, $url, $bannerPath]); // ✅ 画像パスを正しく保存
    fclose($file);
    
    exit("✅ 登録が完了しました!");
}
?>
PHP(loadcsv.php)
<?php
header("Content-Type: application/json");

$file = fopen("./data.csv", "r");
$sites = [];
$counter = 1; // 連番用

while (($data = fgetcsv($file)) !== false) {
    $sites[] = [
        "id" => $counter++,
        "siteId" => $data[0],
        "title" => $data[1],
        "description" => $data[2],
        "url" => $data[3],
        "bannerPath" => $data[4] ? $data[4] : "" // 空なら未登録
    ];
}

fclose($file);
echo json_encode($sites);
?>
PHP(deleteEntry.php)
<?php
if ($_SERVER["REQUEST_METHOD"] == "POST") {
    $siteId = $_POST["siteId"];

    if (!$siteId || strlen($siteId) !== 4) {
        echo "無効なID";
        exit;
    }

    $file = fopen("data.csv", "r");
    $tempFile = fopen("data_temp.csv", "w");
    $deleted = false;

    while (($data = fgetcsv($file)) !== false) {
        // IDが一致しない場合は書き戻す
        if ($data[0] !== $siteId) {
            fputcsv($tempFile, $data);
        } else {
            $deleted = true;
        }
    }

    fclose($file);
    fclose($tempFile);

    if ($deleted) {
        rename("data_temp.csv", "data.csv");
        echo "✅ 削除が完了しました!";
    } else {
        unlink("data_temp.csv");
        echo "指定したIDのデータが存在しません。";
    }
}
?>
サーバー側
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>管理者ページ</title>
    <link rel="stylesheet" href="admin.css">
</head>
<body>
    <h1>管理者ページ</h1>
    <table id="adminTable">
        <thead>
            <tr>
                <th>連番</th><th>ID</th><th>タイトル</th><th>操作</th>
            </tr>
        </thead>
        <tbody></tbody>
    </table>
    <!-- 全削除ボタン -->
    <button id="deleteAllButton">全削除</button>
    <span id="deleteAllMessage"></span> <!-- メッセージ表示 -->

    <script src="admin.js"></script>
</body>
</html>
JS(admin.js)
document.addEventListener("DOMContentLoaded", function() {
    loadAdminData();
});

function loadAdminData() {
    fetch("loadcsv.php")
        .then(response => response.json())
        .then(data => {
            const tbody = document.querySelector("#adminTable tbody");
            tbody.innerHTML = "";
            data.forEach(site => {
                const row = document.createElement("tr");
                row.innerHTML = `
                    <td>${site.id}</td>
                    <td>${site.siteId}</td>
                    <td>${site.title}</td>
                    <td>
                        <button onclick="deleteEntry('${site.siteId}')">削除</button>
                        <span class="deleteMessage" id="msg_${site.siteId}"></span>
                    </td>
                `;
                tbody.appendChild(row);
            });
        });
}

function deleteEntry(siteId) {
    fetch("deleteEntry.php", {
        method: "POST",
        body: new URLSearchParams({ siteId })
    }).then(response => response.text())
      .then(data => {
          let messageElement = document.getElementById(`msg_${siteId}`);
          let icon = data.includes("成功") ? "✅ " : "❌ ";
          messageElement.textContent = icon + data;
          messageElement.style.color = data.includes("成功") ? "green" : "red";
          loadAdminData();
      });
}

document.getElementById("deleteAllButton").addEventListener("click", function() {
    fetch("deleteAll.php", { method: "POST" })
        .then(response => response.text())
        .then(data => {
            document.getElementById("deleteAllMessage").textContent = data;
            document.getElementById("deleteAllMessage").style.color = "red";
            loadAdminData();
        });
});
JS(deleteAll.php)
<?php
if ($_SERVER["REQUEST_METHOD"] == "POST") {
    file_put_contents("./data.csv", ""); // CSVの内容を空にする
    echo "❌ すべてのデータを削除しました。";
}
?>
JS(auth.php)
<?php
session_start();
$adminPassword = "your_secure_password";

if ($_SERVER["REQUEST_METHOD"] == "POST") {
    $password = $_POST["password"];

    if ($password === $adminPassword) {
        $_SESSION["admin_logged_in"] = true;
        echo "✅ ログイン成功";
        exit();
    } else {
        echo "❌ パスワードが間違っています";
        exit();
    }
}
?>
CSS(admin.css)
body {
    font-family: Arial, sans-serif;
    background-color: #dfdfdf;
    text-align: center;
}

table {
    width: 80%;
    margin: 20px auto;
    border-collapse: collapse;
    background-color: white;
}

th {
    background-color: #007bff;
    color: white;
    padding: 10px;
}

td {
    border: 1px solid #ddd;
    padding: 2px;
}

tr {
    height: 18px; /* お好みの高さに調整 */
}

button {
    background-color: #d9534f;
    color: white;
    border: none;
    padding: 8px 12px;
    cursor: pointer;
    border-radius: 10px;
}

button:hover {
    background-color: #c9302c;
}

.deleteMessage {
    margin-left: 10px;
    font-weight: bold;
    color: red;
}

以上です・・・・デモサイト