1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

昔のWindows (1996) 用のサイトを、最新のWebサーバーで作ってみた

1
Last updated at Posted at 2026-02-05

はじめに

こんにちは。
みなさん、昨今WebデザインとかCSSアニメーションと言われてますが、「初心」忘れていませんか?
ということで、WWWが普及し始めたころ (1996) から30年の今日この頃、初心に立ち返るべく、当時のOSとWebブラウザ (WinNT4.0 + IE3) で見られて、かつ機能するという制約でアプリを作ってみました。

※Windows NT 4.0を知らない場合、最悪、Windows 95と読み替えてもらって大丈夫です。

で、なにを作ったの? + スクショ

「今日の数字」というアプリです。 当方、あまり企画センスはないもので
みんなで今日の象徴すると思う数値を登録して、登録が多い数値を「今日の数字」としてランキングにしよう!みたいなアプリです。
要は今年の漢字を毎日、数字でやるみたいなものです。
今日の漢字みたいなわかりやすさはゼロですが、どうなるんですかね

サイトはこちら→ 今日の数字 (http://yumezato-stt.f5.si/todays-no/index.php)
インターネットに接続された古いPCで、ぜひアクセスしてみてください!
そして適当でもいいので数字登録してください!

(HTTPなので正直安全ではないです。自己責任で...)

↓ これが実際にWindows NT 4.0 + IE3で動作しているスクリーンショットです。
VirtualBox_WindowsNT4VM_05_02_2026_22_19_27.png
このデザイン、正直今のCSSで作る方が難しそうです。

環境

さらっと環境をおさらいします

クライアント環境

Windows NT 4.0が動く実機は持っていないので、環境は仮想マシンを使って行いました。
VE : Oracle VirtualBox 7.1.10
OS : Windows NT 4.0 Service Pack 1 (Build 1381) PC-AT互換機
ブラウザ : Microsoft インターネット エクスプローラ Version 3.0 (4.70.1158)
VirtualBox_WindowsNT4VM_05_02_2026_22_21_13.png

サーバー環境

HTTPで通信できるサーバーであればなんでもいいので、サーバーは普通の環境で整えました。
フロントエンド・DBともにDockerで、PHP 8.4 + NGINX 1.29、DBはMySQL 8.4 LTSを動かしています。
もっと世界観にこだわるならApache httpdのほうがよかったんでしょうが、完全に気分です←

主に何が使えないの?

IE3は30年前のブラウザです。
まずCSSなんてものはありません (実際あるけど使えたものではありません)
JavaScriptも基本使えません (実際あるけど(ry )
使えるのは本当にHTML 3.2のみです。

コード解説

語るよりも先に、コードを解説したほうが速いと思うのでもうコードを載せます
全体像は見た方が分かりやすいと思うので読むことおすすめします
サクッと見たい方は右のチャプターからつまんでください

データベースの宣言

-- DDL

DELIMITER ;
CREATE DATABASE IF NOT EXISTS testinet;
USE testinet;
CREATE TABLE IF NOT EXISTS suujitable(
	id BIGINT PRIMARY KEY AUTO_INCREMENT,
    suuji BIGINT NOT NULL,
    registered_ts TIMESTAMP NOT NULL
);

CREATE OR REPLACE VIEW todayssuuji AS SELECT 
	RANK() OVER (ORDER BY COUNT(*) DESC) AS "順位",
	suuji AS "数字",
    COUNT(*) AS "票数"
FROM suujitable
WHERE registered_ts >= CONVERT_TZ( DATE( CONVERT_TZ( UTC_TIMESTAMP(), "UTC", "Asia/Tokyo" ) ), "Asia/Tokyo", "UTC")
GROUP BY `数字`
ORDER BY `票数` DESC,
	`数字` ASC;
    
DROP FUNCTION IF EXISTS regicheck;
DROP PROCEDURE IF EXISTS register;
DELIMITER //
CREATE FUNCTION regicheck (IN target TIMESTAMP) RETURNS TINYINT DETERMINISTIC
BEGIN
	RETURN target < CONVERT_TZ( DATE( CONVERT_TZ( UTC_TIMESTAMP(), "UTC", "Asia/Tokyo" ) ), "Asia/Tokyo", "UTC");
END
//
CREATE PROCEDURE register (IN val BIGINT) 
BEGIN
	INSERT INTO suujitable(suuji, registered_ts) VALUES(val, UTC_TIMESTAMP());
END
//
DELIMITER ;

index.php

コードの全文はこちら
<?php
  $mysqli = new mysqli($_ENV['MYSQL_HOST'], $_ENV['MYSQL_USER'], $_ENV['MYSQL_PASSWORD']);
  if ($mysqli->connect_error) {
      echo $mysqli->connect_error;
      exit();
  } else {
      $mysqli->set_charset("cp932");
  }
  $mysqli->select_db("testinet");
  //人数
  $result = $mysqli->query(<<<EOQ
      SELECT SUM(票数) AS "人数" FROM todayssuuji;
  EOQ);
  $row = $result->fetch_assoc();
  $ninzu = $row["人数"] + 0;
  //ランキング取得
  $result = $mysqli->query(<<<EOQ
      SELECT * FROM todayssuuji;
  EOQ);
  $ranking = [];
  while ($row = $result->fetch_assoc()) {
      array_push($ranking, 
          [
              $row["順位"], 
              $row["数字"], 
              $row["票数"]
          ]);
  }
  $mysqli->close();
?>
<html>
  <head>
      <title>今日の数字</title>
      <?php include_once("styles.php") ?>
  </head>
  <body bgcolor="#FFFFFF">
      <br>
      <br>
      <br>
      <center><h1>今日の数字</h1></center>
      <br>
      <center>現在<font size="5"><b><?php echo $ninzu ?></b></font>が今日の数字を登録しました。</center>
      <center>良ければ、あなたの中で「今日を象徴する数字」を登録してください。</center>
      <br>
      <center>
          <form method="post" action="/todays-no/register.php">
              数値を入力 : <input type="number" name="suuji" id="suuji" type="numeric" value="0">
              <input type="submit" value="登録!" />
          </form>
      </center>
      <br>
      <center><h2>現在のランキング</h2></center>
      <center>
          <?php
              foreach ($ranking as $rankinfo) {
                  switch($rankinfo[0]) {
                      case "1":
                          echo(<<<EOL
                              <font size="6">$rankinfo[0]位... <b>$rankinfo[1]</b></font> ($rankinfo[2]票)<br>
                          EOL);
                      break;
                      case "2":
                          echo(<<<EOL
                              <font size="5">$rankinfo[0]位... <b>$rankinfo[1]</b></font> ($rankinfo[2]票)<br>
                          EOL);
                      break;
                      case "3":
                          echo(<<<EOL
                              <font size="4">$rankinfo[0]位... <b>$rankinfo[1]</b></font> ($rankinfo[2]票)<br>
                          EOL);
                      break;
                      default:
                          echo(<<<EOL
                              $rankinfo[0]位... <b>$rankinfo[1]</b> ($rankinfo[2]票)<br>
                          EOL);
                      break;
                  }
                  
              }
          ?>
      </center>
  </body>
</html>

centerタグ, fontタグ, bタグ, bgcolor属性... 懐かしいですね! (世代じゃありませんが)
今ではfontもcenterも非推奨になってしまいました...
それではコードについて解説していきます。

タイトル

        <br>
        <br>
        <br>
        <center><h1>今日の数字</h1></center>
        <br>
        <center>現在<font size="5"><b><?php echo $ninzu ?></b></font>が今日の数字を登録しました。</center>
        <center>良ければ、あなたの中で「今日を象徴する数字」を登録してください。</center>

タイトルと最初の説明の部分です。
最初の怒涛の改行は、CSSに依存しない形で力業のマージンを作るためのモノです。
基本的に中央揃えにしたいので、改行以外ほとんどcenterで囲んでます。
image.png

「今日の数字」登録フォーム

        <center>良ければ、あなたの中で「今日を象徴する数字」を登録してください。</center>
        <br>
        <center>
            <form method="post" action="/todays-no/register.php">
                数値を入力 : <input type="number" name="suuji" id="suuji" type="numeric" value="0">
                <input type="submit" value="登録!" />
            </form>
        </center>

見ての通り、今日の数字を登録するフォームの本体です。
入力された「今日の数字」は、POSTメソッドでregister.php (後述) に送信されて「今日の数字データベース」へ登録されるようになっています。

"数値を入力 : "の部分はlabel forを使いたかったのですが、当然HTML 3.2にはないのでべた書きです。
(input type="number"も認識されずに普通のtextタイプとして表示されています。HTML 3.2時代にはなかったんでしょうかね?)
image.png

補足 : type=numberはHTML5からでした(笑)

ランキングの表示部分

        <center><h2>現在のランキング</h2></center>
        <center>
            <?php
                foreach ($ranking as $rankinfo) {
                    switch($rankinfo[0]) {
                        case "1":
                            echo(<<<EOL
                                <font size="6">$rankinfo[0]位... <b>$rankinfo[1]</b></font> ($rankinfo[2]票)<br>
                            EOL);
                        break;
                        case "2":
                            echo(<<<EOL
                                <font size="5">$rankinfo[0]位... <b>$rankinfo[1]</b></font> ($rankinfo[2]票)<br>
                            EOL);
                        break;
                        case "3":
                            echo(<<<EOL
                                <font size="4">$rankinfo[0]位... <b>$rankinfo[1]</b></font> ($rankinfo[2]票)<br>
                            EOL);
                        break;
                        default:
                            echo(<<<EOL
                                $rankinfo[0]位... <b>$rankinfo[1]</b> ($rankinfo[2]票)<br>
                            EOL);
                        break;
                    }
                    
                }
            ?>
        </center>

1位・2位・3位・その他の順位それぞれの場合で文字の大きさを変えるという処理を、switchで脳筋実装してます。
可読性は本当に地獄です。DRY原則も...😇
VirtualBox_WindowsNT4VM_05_02_2026_22_34_36.png

index.phpのこだわりと総括

動的な表示や機能も、とにかくサーバー側で完結するように書きました。
ランキング登録などの対話的な処理もHTTPだけあれば機能できるほどにサーバー完結なので、どんなひどいクライアントでもまともに機能するってわけです。
極論w3mやLynxのようなテキストブラウザーでも普通に動作可能です。
そう考えるとHTTPのすごさを実感しますね🤔

register.php

ここからが本番、地獄の裏の処理コードです。

コードの全文はこちらです (75行)
<?php
    date_default_timezone_set("UTC");

    $needtocheck = 0;
    $registable = 1;
    if (isset($_COOKIE["registered"])) {
        $needtocheck = 1;
        $registable = 0;
    }
    $mysqli = new mysqli($_ENV['MYSQL_HOST'], $_ENV['MYSQL_USER'], $_ENV['MYSQL_PASSWORD']);
    if ($mysqli->connect_error) {
        echo $mysqli->connect_error;
        exit();
    } else {
        $mysqli->set_charset("cp932");
    }
    $success = 0;
    $mysqli->select_db("testinet");
    $mysqli->begin_transaction();
    try
    {
        if ($needtocheck) {
            $stmt = $mysqli->prepare(<<<EOQ
                SELECT regicheck(?) AS "registable";
            EOQ);
            $stmt->bind_param("s", $_COOKIE["registered"]);
            if (!$stmt->execute()) {
                $stmt->close();
                echo($stmt->errno . $stmt->error);
                throw new Exception();
            }
            $result = $stmt->get_result();
            $registable = $result->fetch_assoc()["registable"];
            $stmt->close();
        }
        if ($registable) {
            $stmt = $mysqli->prepare(<<<EOQ
                CALL register(?);
            EOQ);
            $stmt->bind_param("i", $_POST["suuji"]);
            if (!$stmt->execute()) {
                $stmt->close();
                echo($stmt->errno . $stmt->error);
                throw new Exception();
            }
            $stmt->close();
            $mysqli->commit();
        }
        $success = 1;
    }
    catch (Exception $exp) {
        var_dump($exp);
        $mysqli->rollback();
    }
    if (!$success) {
        $mysqli->close();
        die("not success");
    }
    if (!$registable) {
        $mysqli->close();
        header("Location:/todays-no/already.php");
        return;
    }
    $result = $mysqli->query(<<<EOQ
        SELECT NOW() AS "NOW";
    EOQ);
    $nowrow = $result->fetch_assoc();
    $encoded_date = urlencode($nowrow["NOW"]);
    $expire_time = time() + 3600 * 24 * 2;
    $formatted_ext = date("D d M Y G:i:s", $expire_time)." GMT";
    
    header("Set-Cookie: registered=$encoded_date; Path=/todays-no; Expires=$formatted_ext;");
    header("Location: /todays-no/index.php");
    $mysqli->close();

本当にごちゃごちゃしていて嫌ですね、それではコードについて解説していきます。

重複登録の防止

一日一票以上登録できないようにします。

COOKIE "registered"に登録した日時が記録され、登録された日時が昨日以前なら登録可能、今日なら登録不可という処理を実装しています。
registeredクッキーの有効期限は3日に設定されています。 (後で苦戦するところ)

実装は主に二つに分かれていて、一次チェックと二次チェックが存在します

一次チェック

過去3日 (72h) 以内に投票したか? = Cookieがあるか

    //一次チェック
    $needtocheck = 0;
    $registable = 1;
    if (isset($_COOKIE["registered"])) {
        $needtocheck = 1;
        $registable = 0;
    }

\$needtocheckフラグと\$registableフラグがあります
それぞれ\$needtocheckフラグは、三日以内に登録した記録があるから、最後の登録日時についてチェックする必要があるかのフラグで、
\$registable フラグは登録可能フラグです。
$_COOKIE["registered"] が存在する=三日以内に登録した記録がある場合は、一時的に\$registered フラグをオフにし、二次チェックする必要があるというフラグを建てます。
存在しない場合はそのままチェック必要なし・登録可能として通過されます

二次チェック

最終登録が昨日以前かのチェック

    //二次チェック
    try {
      if ($needtocheck) {
            $stmt = $mysqli->prepare(<<<EOQ
                SELECT regicheck(?) AS "registable";
            EOQ);
            $stmt->bind_param("s", $_COOKIE["registered"]);
            if (!$stmt->execute()) {
                $stmt->close();
                echo($stmt->errno . $stmt->error);
                throw new Exception();
            }
            $result = $stmt->get_result();
            $registable = $result->fetch_assoc()["registable"];
            $stmt->close();
      }
      /* (省略) */
    } catch (Exception $exp) {
      /* (省略) */
    }

基本SQLサーバーにチェックは丸投げしています。
サーバーに定義した"regicheck"ファンクションへ前回投票日時を突っ込むことで、カラム"registable"に0 (登録不可) か1 (登録可能) が返却されるので、それをそのまま\$registable フラグに突っ込んでいます。
↓regicheckファンクション

CREATE FUNCTION regicheck (IN target TIMESTAMP) RETURNS TINYINT DETERMINISTIC
BEGIN
	RETURN target < CONVERT_TZ( DATE( CONVERT_TZ( UTC_TIMESTAMP(), "UTC", "Asia/Tokyo" ) ), "Asia/Tokyo", "UTC");
END
//

(受け取った日時が、日本の今日0:00より前かどうかの判定)

登録処理

チェックが完了し、登録可能ならデータベースへ登録します

       if ($registable) {
            $stmt = $mysqli->prepare(<<<EOQ
                CALL register(?);
            EOQ);
            $stmt->bind_param("i", $_POST["suuji"]);
            if (!$stmt->execute()) {
                $stmt->close();
                echo($stmt->errno . $stmt->error);
                throw new Exception();
            }
            $stmt->close();
            $mysqli->commit();
        }

登録処理自体はSQLサーバーに登録したregisterプロシージャへ登録したい値を渡すことで自動的に行われるようにしました。
↓registerプロシージャ

CREATE PROCEDURE register (IN val BIGINT) 
BEGIN
	INSERT INTO suujitable(suuji, registered_ts) VALUES(val, UTC_TIMESTAMP());
END
//

SQLサーバーの現在時刻 (UTC) を登録日時として、suujitableに値を挿入するだけのプロシージャです。
※わざわざプロシージャにする意味があったかは不明です。
SQL入門したばかりなので使いたかっただけなんてのは言えません←

登録記録の作成

登録した記録をクライアントに保存させる処理です

    $result = $mysqli->query(<<<EOQ
        SELECT NOW() AS "NOW";
    EOQ);
    $nowrow = $result->fetch_assoc();
    $encoded_date = urlencode($nowrow["NOW"]);
    $expire_time = time() + 3600 * 24 * 2;
    $formatted_ext = date("D d M Y G:i:s", $expire_time)." GMT";
    
    header("Set-Cookie: registered=$encoded_date; Path=/todays-no; Expires=$formatted_ext;");

ここ地味にめちゃくちゃ苦戦したところで、(2026年2月)現時点最新のPHP(8.4)だと、setcookie関数でのExpires指定は勝手にMax-Ageまでつけてくるんですよね。
今のブラウザならMax-Ageつけても解釈できるというか、寧ろMax-Age付きの方が正しいですが、IE3はそううまく行きません。
Max-Ageなんて当時ないので、Max-Ageがデータとして解釈されて、普通にデータが解釈されず消滅します。なのでMax-Ageなしで送ってあげる必要があるんですが、setcookieってオプションでそれ出来ないんですよね。

だからわざわざ自前でこんなのを書いてるんわけです。
2026年と1996年、30年間の壁ですね。

register.phpのこだわりと総括

Shift-JIS、コードページ932で完結するようにしました。
今なら絶対UTF-8一択ですが、UTF-8にした結果不具合が続出したのであきらめてすべてShift-JISにせざるを得ませんでした。
あと正直、何も問題は起きないだろうと高を括ってた矢先でCookieの壁に当たったときは内心焦りました。

完走した感想

正直、ページを作ること自体は断然今よりも楽しかったです。
わざわざいろいろなところでCSSを使う必要もないので、本当にWordやマークダウン感覚で適当に書くことができて手軽でした。
当初個人サイトが乱立していた理由として、HTMLが作りやすかったのは本当にあると思います。
初心は「作って、動くことが楽しい」というところにあったのかもしれません。
VirtualBox_WindowsNT4VM_05_02_2026_23_34_59.png

こんな記事を最後まで読んでくれてありがとうございました。
ぜひWindows 95やNT 4.0でサイトアクセスしてください!
Windows NT 4.0でも動く作ったサイト→ 今日の数字 (http://yumezato-stt.f5.si/todays-no/index.php)

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?