6. HTMLテンプレート生成とレスポンスヘッダ

前章では、フォーム値を構造体に格納し、キー名でアクセスできるようにしました。

この章では、その値をHTMLに埋め込み、ブラウザに返す方法を扱います。CGIでは、レスポンスヘッダと本文を標準出力に書き出せば、Webページとして表示できます。


この章で学ぶこと


レスポンスヘッダを出力する

CGIプログラムは、本文を出力する前にレスポンスヘッダを出力します。HTMLを返す場合は、少なくとも Content-Type を指定します。

printf("Content-Type: text/html; charset=UTF-8\r\n\r\n");

ヘッダと本文の間には空行が必要です。上の例では、最後の \r\n\r\n が「ヘッダ行の終わり」と「空行」を表しています。

その後にHTML本文を出力します。

printf("<!DOCTYPE html>\n");
printf("<html lang=\"ja\">\n");
printf("<body>\n");
printf("<h1>Hello</h1>\n");
printf("</body>\n");
printf("</html>\n");

直接HTMLを出力する問題

小さなサンプルなら、Cコード内でHTMLを printf() しても動きます。しかし画面が少し複雑になると、次の問題が出てきます。

そこで、HTMLを外部ファイルに分け、動的に変えたい部分だけを置換する方法を考えます。


テンプレートファイルを用意する

たとえば、次のような template.html を用意します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>{{title}}</title>
</head>
<body>
  <h1>{{title}}</h1>
  <p>{{message}}</p>
</body>
</html>

{{title}}{{message}} がプレースホルダです。CGIプログラム側で、この部分を実際の値に置き換えます。


HTMLエスケープ

ユーザー入力をHTMLに埋め込む場合は、HTMLとして特別な意味を持つ文字をエスケープする必要があります。

たとえば、ユーザーが <script> のような文字列を入力した場合、それをそのままHTMLに出力すると、ブラウザがタグとして解釈してしまいます。

学習用の簡易関数として、次のようなHTMLエスケープを用意します。

#include <stdio.h>

void html_escape_print(FILE *out, const char *s) {
    for (; *s != '\0'; s++) {
        switch (*s) {
            case '&':
                fputs("&amp;", out);
                break;
            case '<':
                fputs("&lt;", out);
                break;
            case '>':
                fputs("&gt;", out);
                break;
            case '"':
                fputs("&quot;", out);
                break;
            case '\'':
                fputs("&#39;", out);
                break;
            default:
                fputc(*s, out);
                break;
        }
    }
}

この関数は、HTML本文や属性値に文字列を出す前の基本対策です。文脈によって必要なエスケープは異なりますが、まず「入力値をそのままHTMLに出さない」ことを意識します。


置換用のデータ構造

テンプレートに埋め込む値を、キーと値のペアとして持ちます。

typedef struct {
    const char *key;
    const char *value;
} TemplateItem;

key には titlemessage を入れます。テンプレート側では {{title}} のように書きます。


プレースホルダを出力時に置換する

次の実装は、テンプレートファイルを読みながら、{{key}} に一致した部分を値に置き換えて出力する簡易版です。

#include <stdio.h>
#include <string.h>

typedef struct {
    const char *key;
    const char *value;
} TemplateItem;

static const TemplateItem *find_template_item(
    const TemplateItem *items,
    int count,
    const char *key,
    size_t key_len
) {
    for (int i = 0; i < count; i++) {
        if (strlen(items[i].key) == key_len &&
            strncmp(items[i].key, key, key_len) == 0) {
            return &items[i];
        }
    }

    return NULL;
}

void render_template(
    const char *filename,
    const TemplateItem *items,
    int count,
    FILE *out
) {
    FILE *fp = fopen(filename, "r");
    if (fp == NULL) {
        fputs("<p>Template not found.</p>", out);
        return;
    }

    int ch;
    while ((ch = fgetc(fp)) != EOF) {
        if (ch != '{') {
            fputc(ch, out);
            continue;
        }

        int next = fgetc(fp);
        if (next != '{') {
            fputc(ch, out);
            if (next != EOF) {
                fputc(next, out);
            }
            continue;
        }

        char key[64];
        size_t len = 0;
        int closed = 0;

        while ((ch = fgetc(fp)) != EOF) {
            if (ch == '}') {
                int end = fgetc(fp);
                if (end == '}') {
                    closed = 1;
                    break;
                }

                if (len + 1 < sizeof(key)) {
                    key[len++] = (char)ch;
                }
                if (end != EOF && len + 1 < sizeof(key)) {
                    key[len++] = (char)end;
                }
                continue;
            }

            if (len + 1 < sizeof(key)) {
                key[len++] = (char)ch;
            }
        }

        key[len] = '\0';

        if (!closed) {
            fprintf(out, "{{%s", key);
            break;
        }

        const TemplateItem *item = find_template_item(items, count, key, len);
        if (item != NULL) {
            html_escape_print(out, item->value);
        } else {
            fprintf(out, "{{%s}}", key);
        }
    }

    fclose(fp);
}

この実装では、プレースホルダに対応する値を見つけた場合、html_escape_print() を通して出力します。見つからないプレースホルダはそのまま出力します。


使用例

#include <stdio.h>

int main(void) {
    TemplateItem items[] = {
        { "title", "Hello Page" },
        { "message", "こんにちは、CGI" }
    };

    printf("Content-Type: text/html; charset=UTF-8\r\n\r\n");
    render_template("template.html", items, 2, stdout);

    return 0;
}

このようにすると、HTMLファイルはHTMLファイルとして編集し、Cコード側では埋め込む値だけを用意できます。


この簡易テンプレートの限界

この章のテンプレート処理は、仕組みを理解するための簡易実装です。

それでも、HTMLとCロジックを分けるだけで、後のCMS実装はかなり見通しやすくなります。


小まとめ

次章では、MariaDBとODBCを使い、フォーム入力をデータベースに保存する準備を進めます。