6. HTMLテンプレート生成とレスポンスヘッダ
前章では、フォーム値を構造体に格納し、キー名でアクセスできるようにしました。
この章では、その値をHTMLに埋め込み、ブラウザに返す方法を扱います。CGIでは、レスポンスヘッダと本文を標準出力に書き出せば、Webページとして表示できます。
この章で学ぶこと
- CGIレスポンスのヘッダと本文の関係
- C言語でHTMLを出力する方法
- HTMLをテンプレートとして外部ファイルに分ける理由
- プレースホルダ置換の簡易実装
- HTML出力時のエスケープの必要性
レスポンスヘッダを出力する
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() しても動きます。しかし画面が少し複雑になると、次の問題が出てきます。
- Cコードの中にHTMLが大量に混ざる
- HTMLの修正だけでもCコードの再コンパイルが必要になる
- 同じヘッダやレイアウトを再利用しにくい
- 出力処理とアプリケーションロジックの境界が曖昧になる
そこで、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("&", out);
break;
case '<':
fputs("<", out);
break;
case '>':
fputs(">", out);
break;
case '"':
fputs(""", out);
break;
case '\'':
fputs("'", out);
break;
default:
fputc(*s, out);
break;
}
}
}
この関数は、HTML本文や属性値に文字列を出す前の基本対策です。文脈によって必要なエスケープは異なりますが、まず「入力値をそのままHTMLに出さない」ことを意識します。
置換用のデータ構造
テンプレートに埋め込む値を、キーと値のペアとして持ちます。
typedef struct {
const char *key;
const char *value;
} TemplateItem;
key には title や message を入れます。テンプレート側では {{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コード側では埋め込む値だけを用意できます。
この簡易テンプレートの限界
この章のテンプレート処理は、仕組みを理解するための簡易実装です。
- 条件分岐や繰り返しには対応していません。
- プレースホルダ名は64バイト未満を前提にしています。
- HTML以外の文脈に応じたエスケープは扱っていません。
- テンプレートファイルの読み込みエラー処理は最小限です。
それでも、HTMLとCロジックを分けるだけで、後のCMS実装はかなり見通しやすくなります。
小まとめ
- CGIでは、レスポンスヘッダの後にHTML本文を出力します。
- HTMLをCコードに直接埋め込みすぎると、保守しにくくなります。
- テンプレートファイルを使うと、表示と処理の責任を分けやすくなります。
- ユーザー入力をHTMLに出すときは、必ずエスケープを考えます。
次章では、MariaDBとODBCを使い、フォーム入力をデータベースに保存する準備を進めます。