とほほのDjango入門 (チュートリアル)

トップ > Django入門 > チュートリアル

目次

Django とは

注意

インストール

Shell
# CentOS 7
# yum -y install python3
# pip3 install django==3.2

# Rocky Linux 8
# dnf -y install python3
# pip3 install django==3.2

# Ubuntu 22.04
# apt -y install python3 python3-pip
# pip3 install django==3.2

# Ubuntu 22.04
# apt -y install python3 python3-pip sqlite3
# pip3 install django==4.2
# pip3 install tzdata

SQLite3をバージョンアップする

RHEL, CentOS 系で SQLite を使用する際「django.core.exceptions.ImproperlyConfigured: SQLite X.X.X or later is required (found X.X.X).」の様なエラーが出る場合は SQLite を最新版にバージョンアップします。

Shell
# yum install -y wget gcc make
# wget --no-check-certificate https://www.sqlite.org/2022/sqlite-autoconf-3390400.tar.gz
# tar zxvf ./sqlite-autoconf-3390400.tar.gz
# cd ./sqlite-autoconf-3390400
# ./configure --prefix=/usr/local
# make
# make install
# cd ..
# mv /usr/bin/sqlite3 /usr/bin/sqlite3_old
# ln -s /usr/local/bin/sqlite3 /usr/bin/sqlite3
# echo 'export LD_LIBRARY_PATH="/usr/local/lib"' >> ~/.bashrc
# source ~/.bashrc

プロジェクトを作成する

まず、Django プロジェクトを作成します。Django では、コンフィグディレクトリの名前もプロジェクト名と同じになってしまうため、一度、config という名前でプロジェクトを作成し、その後、ディレクトリ名を変更するのがおすすめです。

Shell
$ django-admin startproject config
$ mv ./config ./myproj
$ cd ./myproj

下記のファイルが作成されます。

Files
./manage.py
./config
./config/__init__.py
./config/asgi.py
./config/settings.py
./config/urls.py
./config/wsgi.py

簡易サーバを起動する

外部のクライアントから接続すると 「Invalid HTTP_HOST header: '192.168.xx.xx'. You may need to add '192.168.56.102' to ALLOWED_HOSTS.」 といったエラーとなることがあります。./config/settings.py に接続を許可するホストの情報を設定する必要があります。開発時はとりあえず、すべて ('*') を指定していてもよいですが、本番移行の際にはセキュリティ確保のため、HTTP の Host ヘッダで送信されてくる IPアドレスや URL を指定してください。

./config/settings.py
ALLOWED_HOSTS = ['*']

開発用の簡易サーバを起動します。LISTEN する IPアドレスとポート番号を指定することができます。

Shell
$ python3 manage.py runserver 0.0.0.0:80

80番ポート番号が解放されていない場合は、例えば下記の様にして開放する必要があります。

Shell
# yum -y install firewalld
# systemctl enable firewalld
# systemctl start firewalld
# firewall-cmd --add-port 80/tcp --permanent
# firewall-cmd --reload

ブラウザから http://{{SERVER}}/ にアクセスすることで、Django のサンプルページが表示されれば成功です。

アプリケーションを作成する

books アプリケーションを作成します。

Shell
$ python3 manage.py startapp books

下記のファイルが作成されます。

Files
./books/__init__.py
./books/admin.py
./books/apps.py
./books/models.py
./books/tests.py
./books/views.py
./books/migrations/__init__.py

./books/urls.py を新規に作成し、http://{{SERVER}}/books/ にアクセスすれば view.py の list_books() を呼び出すように指定します。app_name には Books アプリケーションの名前空間を指定します。

./books/urls.py
from django.urls import path
from . import views

app_name = 'books';
urlpatterns = [
    path('', views.list_books, name='list_books'),
]

./books/views.py ビューに list_books() を実装します。

./books/views.py
from django.http import HttpResponse

def list_books(request):
    return HttpResponse("Hello world!")

./config/settings.py の INSTALLED_APPS に ./books/apps.py に定義されたクラスを登録します。

./config/settings.py
INSTALLED_APPS = [
    'books.apps.BooksConfig',
    'django.contrib.admin',
    'django.contrib.auth',

./config/urls.py に、http://{{SERVER}}/books/ が要求されたら、./books/urls.py を参照するように指定します。

./config/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('books/', include('books.urls')),
]

ブラウザで http://{{SERVER}}/books/ にアクセスして Hello world! が表示されれば成功です。

アプリケーションディレクトリを集約する

Django の標準では BASE_DIR(./) 直下にアプリケーションディレクトリが乱雑に並んでしまうため、アプリケーションを集約して格納するための ./apps ディレクトリを用意します。

Shell
$ mkdir ./apps

./config/settings.py に ./apps ディレクトリを登録します。

./config/settings.py
import os
import sys
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))

アプリケーションディレクトリを ./apps 配下に移動します。

Shell
$ mv ./books ./apps

ブラウザで http://{{SERVER}}/books/ にアクセスして Hello world! が表示されれば成功です。

モデルを作成する

本(Book)を管理するモデルを作成します。本(Book)は、管理番号(book_id)、タイトル(title)、著者(author)の属性を持つものとします。

./apps/books/models.py
from django.db import models

class Book(models.Model):
    book_id = models.CharField(max_length=32)
    title = models.CharField(max_length=256)
    author = models.CharField(max_length=256)

    def __str__(self):
        return self.title

DB に対するマイグレーションファイルを作成し、マイグレーションを実行します。これにより、モデルで定義したテーブルやカラムが自動的に作成されます。テーブルやカラムを変更して再度マイグレーションを行うことで、テーブル追加やカラム追加がマイグレーションされます。

Shell
$ python3 manage.py makemigrations
$ python3 manage.py migrate

作成されたテーブルは次のようにして確認することができます。

Shell
$ python3 manage.py dbshell
sqlite> .tables
auth_group                  books_book
    :                            :
sqlite> .schema books_book
CREATE TABLE IF NOT EXISTS "books_book" (...);
sqlite> select * from books_book;    // まだ何も表示されない
sqlite> (Ctrl-D)

管理者サイトを使用する

Django は簡易的な管理サイト機能を標準で装備しています。管理者サイトからモデルで定義した DB に対して簡単な追加・一覧・更新・削除を行うことができます。まず、管理者ユーザを作成します。

Shell
$ python3 manage.py createsuperuser
Username (leave blank to use 'root'): admin
Email address: admin@example.com
Password: ***password***
Password (again): ***password***
Superuser created successfully.

Books アプリケーションの Book モデルを管理者サイトで管理できるようにします。

./apps/books/admin.py
from django.contrib import admin
from .models import Book

admin.site.register(Book)

ブラウザから http://{{SERVER}}/admin/ にアクセスすることで、上記で作成した管理者ユーザで管理者サイトにログインすることができます。Groups と Users のみ表示され、Books が表示されない場合は、「manage.py runserver」を再起動してみてください。管理者サイトから、Book の情報を何冊が登録してみましょう。

テンプレートを使用する

HTMLテンプレートを使用して Book の一覧をテーブル表示してみます。まず、テンプレートディレクトリを作成します。

Shell
$ mkdir ./templates

./config/settings.py に ./templates を登録します。

./config/settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
           :
    },
]

Books のためのテンプレートディレクトリを作成します。

Shell
$ mkdir ./templates/books

./templates/books の下に list_books.html テンプレートファイルを作成します。

./templates/books/list_books.html
<table>
  <thead>
    <tr>
      <th>Book ID</th>
      <th>Title</th>
      <th>Author</th>
      <th>Action</th>
    </tr>
  </thead>
  <tbody>
    {% if books %}
      {% for book in books %}
        <tr>
          <td><a href="/books/{{ book.book_id }}">{{ book.book_id }}</a></td>
          <td>{{ book.title }}</td>
          <td>{{ book.author }}</td>
          <td><a href="/books/{{ book.book_id }}/edit">[Edit]</a></td>
        </tr>
      {% endfor %}
    {% else %}
      <tr>
        <td colspan=4>No books.</td>
      </tr>
    {% endif %}
  </tbody>
</table>

./apps/books/views.py ファイルを下記の様に修正します。

./apps/books/views.py
from django.http import HttpResponse
from django.template import loader
from .models import Book

def list_books(request):
    books = Book.objects.all()
    context = {
        'title': 'List Books',
        'books': books,
    }
    template = loader.get_template('books/list_books.html')
    return HttpResponse(template.render(context, request))

http://{{SERVER}}/books/ にアクセスして、管理者サイトで登録した本の一覧が表示されれば成功です。

テンプレートで共通なレイアウトページを参照する

{% block %} {% extends %} を用いて、複数のテンプレートで共有するレイアウトを作成することができます。子テンプレート(list_books.html)に content という名前のブロックを定義し、親テンプレート(layout.html)がこれを読み込みます。まず、 ./templates/layout.html を新規作成します。

./templates/layout.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
</head>
<body>
  <h1>{{ title }}</h1>
  {% block content %}{% endblock %}
</body>
</html>

テンプレートファイルに下記を追記します。

./templates/books/list_books.html
{% extends 'layout.html' %}
{% block content %}
<table>
  :
</table>
{% endblock %}

http://{{SERVER}}/books/ にアクセスして、「List Books」のタイトルが表示されれば成功です。

スタティックファイルを読み込む

CSS や JavaScript などのスタティックファイルを格納するスタティックディレクトリ(./static)を作成します。

Shell
$ mkdir -p ./static/css ./static/js ./static/img

./static を ./config/settings.py の末尾に登録します。

./config/settings.py
    :
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
)

./static/css/style.css ファイルを作成します。

./static/css/style.css
table { border-collapse: collapse; margin-bottom: .5rem; }
table th, table td { border: 1px solid #ccc; padding: .1rem .3rem; }
table th { background-color: #ddd; }
button { line-height: 1.2rem; min-width: 6rem; }

レイアウトファイルから style.css を読み込ませます。

./templates/layout.html
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>

http://{{SERVER}}/books/ にアクセスして、表の枠線が表示されたら成功です。うまく表示されない場合は、「manage.py runserver」を再起動したり、ブラウザのキャッシュをクリアしてみましょう。

ヘッダやメニューバーを表示する

レイアウトファイルにヘッダやメニューバーを追加します。

./templates/layout.html
<body>
  <div class="header-block">
    @ Django Sample
  </div>
  <div class="menu-block">
    <a href="/books/">Book</a> /
    <a href="/settings/">Settings</a>
  </div>
  <div class="content-block">
    <h1>{{ title }}</h1>
    {% block content %}{% endblock %}
  </div>
</body>

ヘッダやメニューのためのスタイルを追記します。

./static/css/style.css
* { margin: 0; padding: 0; }
.header-block { background-color: #000; color: #fff; line-height: 2rem;
    font-weight: bold; padding: 0 .5rem; }
.menu-block { background-color: #ddd; line-height: 2rem; padding: 0 .5rem; }
.content-block { padding: 0 .5rem; }
table { border-collapse: collapse; margin-bottom: .5rem; }

http://{{SERVER}}/books/ にアクセスして、ヘッダやメニューが表示されれば成功です。

詳細画面

一覧画面と同様に詳細画面を作成します。まず、テンプレートを作成します。

./templates/books/detail_book.html
{% extends 'layout.html' %}
{% block content %}
<table>
  <tr><th>Book ID</th><td>{{ book.book_id }}</td></tr>
  <tr><th>Title</th><td>{{ book.title }}</td></tr>
  <tr><th>Author</th><td>{{ book.author }}</td></tr>
</table>
<div class="basic-block">
  <button onclick="location.href='/books/'">Return</button>
</div>
{% endblock %}

./apps/books/views.py に詳細画面用のビュー関数を追加します。

./apps/books/views.py
    :
def detail_book(request, book_id):
    try:
        book = Book.objects.get(book_id=book_id)
    except Book.DoesNotExist:
        book = None

    context = {
        'title': 'Detail Book',
        'book': book,
    }
    template = loader.get_template('books/detail_book.html')
    return HttpResponse(template.render(context, request))

ビューを urls.py に登録します。

./apps/books/urls.py
urlpatterns = [
    path('', views.list_books, name='list_books'),
    path('<str:book_id>', views.detail_book, name='detail_book'),
]

http://{{SERVER}}/books/ から Book ID のリンクをクリックして、詳細画面が表示されれば成功です。

編集画面

詳細画面と同様に編集画面を作成します。まず、テンプレートを作成します。{% url 名前 引数 %} は、./apps/*/urls.py の name="..." で指定した名前に対応するパス名を取得します。app_name で名前空間が指定されている場合は 'app_name:name' で指定します。{% if ... %} ... {% elif ... %} ... {% endif %} は条件式を記述します。

./templates/books/edit_book.html
{% extends 'layout.html' %}
{% block content %}
<form method="POST" action="{% url 'books:edit_book' book.book_id %}">
  {% csrf_token %}
  <input type="hidden" name="mode" value="{{ mode }}">
  <table>
    <tr>
      <th>Book ID</th>
      <td><input type="text" name="book_id" readonly value="{{ book.book_id }}"></td>
    </tr>
    <tr>
      <th>Title</th>
      <td><input type="text" name="title"
          {% if mode != 'input' %}readonly{% endif %} value="{{ book.title }}"></td>
    </tr>
    <tr>
      <th>Author</th>
      <td><input type="text" name="author"
          {% if mode != 'input' %}readonly{% endif %} value="{{ book.author }}"></td>
    </tr>
  </table>
  <div class="basic-block">
    {% if mode == 'input' %}
      <button type="button" onclick="location.href='{% url 'books:list_books' %}'">Return</button>
      <button type="submit">OK</button>
    {% elif mode == 'confirm' %}
      <button type="button" onclick="history.back()">Back</button>
      <button type="submit">OK</button>
    {% elif mode == 'result' %}
      <button type="button" onclick="location.href='{% url 'books:list_books' %}'">Return</button>
    {% endif %}
  </div>
</form>
{% endblock %}

ビューファイルに編集画面用のビューを追加します。

./apps/books/views.py
def edit_book_input(request, book_id):
    try:
        book = Book.objects.get(book_id=book_id)
    except Book.DoesNotExist:
        book = None

    context = {
        'title': 'Edit Book(input)',
        'mode': 'input',
        'book': book,
    }
    template = loader.get_template('books/edit_book.html')
    return HttpResponse(template.render(context, request))

def edit_book_confirm(request, book_id):
    book = Book()
    book.book_id = request.POST['book_id']
    book.title = request.POST['title']
    book.author = request.POST['author']

    context = {
        'title': 'Edit Book(confirm)',
        'mode': 'confirm',
        'warning_message': 'Are you sure you want to save?',
        'book': book,
    }
    template = loader.get_template('books/edit_book.html')
    return HttpResponse(template.render(context, request))

def edit_book_result(request, book_id):
    try:
        book = Book.objects.get(book_id=book_id)
        book.book_id = request.POST['book_id']
        book.title = request.POST['title']
        book.author = request.POST['author']
        book.save()
    except Book.DoesNotExist:
        book = None

    context = {
        'title': 'Edit Book(result)',
        'mode': 'result',
        'success_message': 'Success!',
        'book': book,
    }
    template = loader.get_template('books/edit_book.html')
    return HttpResponse(template.render(context, request))

def edit_book(request, book_id):
    if request.method == 'GET':
        return edit_book_input(request, book_id)
    elif request.method == 'POST':
        if request.POST['mode'] == 'input':
            return edit_book_confirm(request, book_id)
        if request.POST['mode'] == 'confirm':
            return edit_book_result(request, book_id)

ビューを urls.py に登録します。

./apps/books/urls.py
urlpatterns = [
    path('', views.list_books, name='list_books'),
    path('<str:book_id>', views.detail_book, name='detail_book'),
    path('<str:book_id>/edit', views.edit_book, name='edit_book'),
]

レイアウトにメッセージ表示欄を追加します。

./templates/layout.html
  <div class="content-block">
    <h1>{{ title }}</h1>
    {% if success_message %}
    <div class="msg-success">{{ success_message }}</div>
    {% endif %}
    {% if warning_message %}
    <div class="msg-warning">{{ warning_message }}</div>
    {% endif %}
    {% if error_message %}
    <div class="msg-error">{{ error_message }}</div>
    {% endif %}
    {% block content %}{% endblock %}
  </div>

style.css にスタイルを追加します。

./static/css/style.css
button { line-height: 1.2rem; min-width: 6rem; }
input[type="text"], select { border: 1px solid #ccc; height: 1.5rem;
    border-radius: .2rem; padding: 0 .3rem; width: 20rem; }
input[readonly] { border: 0; }
.msg-success { padding: .2rem; color: #080; border: 1px solid #9c9;
    background-color: #cfc; margin-bottom: .5rem; }
.msg-warning { padding: .2rem; color: #880; border: 1px solid #cc9;
    background-color: #ffc; margin-bottom: .5rem; }
.msg-error   { padding: .2rem; color: #800; border: 1px solid #c99;
    background-color: #fcc; margin-bottom: .5rem; }

http://{{SERVER}}/books/ から [Edit] のリンクをクリックして、編集操作ができれば成功です。

多言語対応

Webページを、多言語に対応させます。まず、gettext をインストールします。

Shell
# RHEL/CentOS系
# yum install -y gettext

./config/settings.py に下記の設定を行います。LocaleMiddleware は必ず SessionMiddleware と CommonMiddleware の間に記述してください。

./config/settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
       :
]

LANGUAGE_CODE = 'ja'

LOCALE_PATHS = (
    os.path.join(BASE_DIR, 'locale'),
)

TIME_ZONE = 'Asia/Tokyo'

テンプレートの冒頭で i18n を読み込み、翻訳対象の文字列を {% trans ... %} で囲みます。

./templates/books/list_books.html
{% extends 'layout.html' %}
{% load i18n %}
{% block content %}
<table>
  <thead>
    <tr>
      <th>{% trans 'Book ID' %}</th>
      <th>{% trans 'Title' %}</th>
      <th>{% trans 'Author' %}</th>
      <th>{% trans 'Action' %}</th>
    </tr>
  </thead>

プログラムの中で翻訳対象の文字列を _(...) で囲みます。

./apps/books/views.py
from django.utils.translation import gettext_lazy as _

def list_books(request):
    books = Book.objects.all()
    context = {
        'title': _('List Books'),
        'books': books,
    }

./locale フォルダを作成し、翻訳対象の辞書ファイルを作成します。

Shell
$ python3 manage.py makemessages -l ja

./locale/ja/LC_MESSAGES/django.po ファイルが作成されるので、これに、翻訳対象文字列の翻訳を記入します。

./locale/ja/LC_MESSAGES/django.po
#: apps/books/views.py:9
msgid "List Books"
msgstr "ブックの一覧"

#: templates/books/list_books.html:7
msgid "Book ID"
msgstr "ブックID"

django.po ファイルに fuzzy というコメントがある場合、他のメッセージと表記の揺れであるとみなされ無視されます。

#: templates/layout.html:14
#, fuzzy              ← 表記ゆれでない場合はこの行を削除する
#| msgid "Book ID"
msgid "Book"
msgstr "ブック"

下記のコマンドを実行して、メッセージをコンパイルし、./locale/ja/LC_MESSAGES/django.mo ファイルを作成します。

Shell
$ python3 manage.py compilemessages

http://{{SERVER}}/books/ にアクセスし、「List Books」の代わりに「ブックの一覧」と日本語が表示されれば成功です。うまく表示できない場合は、「manage.py runserver」を再起動したり、ブラウザキャッシュをクリアしてみましょう。

言語設定画面を用意する

言語設定画面を用意します。まずは、設定画面のためのアプリケーションとテンプレートディレクトリを作成します。

Shell
$ python3 manage.py startapp settings
$ mv ./settings ./apps
$ mkdir ./templates/settings

テンプレートファイルを作成します。

./templates/settings/settings.html
{% extends 'layout.html' %}
{% load i18n %}
{% block content %}
<form action="{% url 'set_language' %}" method="post">
  {% csrf_token %}
  <input name="next" type="hidden" value="{{ redirect_to }}">
  <select name="language">
    {% get_current_language as LANGUAGE_CODE %}
    {% get_available_languages as LANGUAGES %}
    {% get_language_info_list for LANGUAGES as languages %}
    {% for language in languages %}
      <option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
        {{ language.name_local }} ({{ language.code }})
      </option>
    {% endfor %}
  </select>
  <button>{% trans 'Set' %}</button>
</form>
{% endblock %}

views.py を作成します。

./apps/settings/views.py
from django.http import HttpResponse
from django.template import loader
from django.utils.translation import gettext_lazy as _

def settings(request):
    context = {
        'title': _('Settings'),
    }
    template = loader.get_template('settings/settings.html')
    return HttpResponse(template.render(context, request))

settings の urls.py を作成します。

./apps/settings/urls.py
from django.urls import path
from . import views

app_name = 'settings'
urlpatterns = [
    path('', views.settings,  name='settings'),
]

./config/urls.py に settings の urls.py を登録します。

./config/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('books/', include('books.urls')),
    path('i18n/', include('django.conf.urls.i18n')),
    path('settings/', include('settings.urls')),
]

選択肢として表示する言語を絞るには、./config/settings.py で LANGUAGES を設定してください。

./config/settings.py
import os
import sys
from django.utils.translation import gettext_lazy as _
   :
   :

LANGUAGE_CODE = 'ja'

LANGUAGES = [
    ('ja', _('Japanese')),
    ('en', _('English')),
]

Settings メニューから http://{{SERVER}}/settings/ にアクセスして、表示言語を切り替えることができれば成功です。

Apacheから起動する

Apache → mod_wsgi → Django の経路で処理できるようにします。

Shell
# CentOS 7
# yum -y install gcc httpd httpd-devel python3-devel
# pip3 install mod_wsgi

# Rocky Linux 8
# dnf -y install httpd python3-mod_wsgi

/etc/httpd/conf/httpd.conf を修正します。

/etc/httpd/conf/httpd.conf
ServerName www.example.com:80         # コメントを削除

/etc/httpd/conf.d/django.conf を作成します。

/etc/httpd/conf.d/django.conf
# CentOS 7 の場合は下記の1行を追加
# LoadModule wsgi_module /usr/local/lib64/python3.6/site-packages/mod_wsgi/server/mod_wsgi-py36.cpython-36m-x86_64-linux-gnu.so

WSGIScriptAlias / /opt/myproj/config/wsgi.py
WSGIPythonPath /opt/myproj
Alias /static/ /opt/myproj/static/

<Directory /opt/myproj>
  <Files wsgi.py>
    Require all granted
  </Files>
</Directory>

<Directory /opt/myproj/static>
    Require all granted
</Directory>

Apache を起動します。

Shell
# RHEL/CentOS系
# systemctl start httpd
# systemctl enable httpd

# Ubuntu系
$ sudo apachectl -D FORGROUND

MariaDBと接続する

必要なモジュールをインストールします。

Shell
# yum -y install mariadb mariadb-devel
# pip3 install mysqlclient

./config/setting.py に次のように設定します。

./config/settings.py
import pymysql
pymysql.install_as_MySQLdb()
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'django',        # データベース名
        'USER': 'xxxxxx',        # DBユーザ名
        'PASSWORD': 'xxxxxxxx',  # DBパスワード
        'HOST': '127.0.0.1',     # DBサーバアドレス
        'PORT': '3306',          # DBサーバポート
    }
}

Copyright (C) 2018-2023 杜甫々
初版:2018年3月4日 最終更新:2023年6月4日
http://www.tohoho-web.com/django/tutorial.html