Перейти к содержанию

Из Markdown в HTML: первая сборка

Когда текст статьи и оформление страницы находятся в одном HTML-файле, даже небольшое изменение общего хедера приходится повторять на многих страницах. При обновлении ProfessorWeb мы разделили тексты, шаблоны и готовые файлы сайта. Статьи теперь хранятся в Markdown, а браузер получает собранный HTML. Подробности переноса описаны в кейсе о сохранении старых URL.

Начнём серию «Статический сайт из Markdown» с одной страницы. Вы создадите исходник статьи, напишете небольшой сборщик и увидите, как изменение Markdown попадает в HTML после повторного запуска. Для примеров нужны базовые знания HTML и Python. Учебный проект будет постепенно получать возможности, которые мы использовали в ProfessorWeb.

Как устроена статическая сборка

У статического сайта готовый документ существует до запроса посетителя. Сервер читает HTML-файл и отдаёт его браузеру. В нашей модели Python запускается при подготовке сайта, а не для формирования ответа на каждое открытие статьи.

Исходный Markdown — удобная форма хранения содержания. В нём можно обозначить абзац, заголовок, список или фрагмент кода, не повторяя HTML-теги вокруг каждого элемента. Однако браузеру для показа нашей страницы нужен HTML. Между исходником и результатом появляется сборка: программа читает текст, преобразует его в разметку и помещает в полный документ.

content/index.md → преобразование Markdown → HTML-документ → dist/index.html

Папка content содержит редактируемые исходники, а dist — результат. Такое разделение есть и в ProfessorWeb. На хостинг загружается готовый сайт; Markdown, инструменты и резервные архивы остаются в рабочем проекте.

Пока сборщик будет знать только об одной странице. Этого достаточно, чтобы проверить весь путь от текста до документа. Заголовок вкладки и общую HTML-оболочку сначала зададим прямо в программе, а в следующих уроках вынесем в метаданные и шаблоны.

Исходный Markdown и окружение

Используем Python 3.11 или новее и библиотеку Python-Markdown. Первый пример проверен на Python 3.14.5 с Python-Markdown 3.11. Команды ниже приведены для macOS и Linux; выполняйте их в новой рабочей папке, отдельно от проекта действующего сайта:

mkdir markdown-site
cd markdown-site
python3 -m venv .venv

Последняя команда создаёт виртуальное окружение — отдельное место для библиотек этого проекта. После этого создайте рядом файл requirements.txt с одной строкой:

Markdown==3.11

Установите указанную библиотеку:

.venv/bin/python -m pip install -r requirements.txt

Здесь Markdown — имя пакета для установки; в Python он импортируется как markdown. Виртуальное окружение не требуется отдельно активировать: мы обращаемся к его интерпретатору по пути .venv/bin/python.

Теперь создайте папку content и файл content/index.md. Поместите в него весь следующий текст:

## Первый материал

Текст статьи хранится в **Markdown**, а браузер получает HTML.

- Исходник: `content/index.md`
- Результат: `dist/index.html`

### Небольшой пример

```python
print("Профессор Веб")
```

Два символа # обозначают заголовок второго уровня, двойные звёздочки — выделение, дефисы — элементы списка. Обратные кавычки выделяют имя файла внутри предложения. Тройные обратные кавычки ограничивают блок кода; слово python указывает его язык.

Обратите внимание: в исходнике нет заголовка первого уровня. Основное название страницы появится в HTML-оболочке. Так же новые статьи ProfessorWeb получают H1 из своих метаданных и не повторяют его в теле Markdown.

Сборщик одной страницы

Создайте build.py рядом с requirements.txt. Ниже приведён полный файл:

from pathlib import Path

import markdown

ROOT = Path(__file__).resolve().parent

source = (ROOT / "content" / "index.md").read_text(encoding="utf-8")
body = markdown.markdown(
    source,
    extensions=["fenced_code"],
    output_format="html",
)

page = f"""<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Статический сайт из Markdown</title>
</head>
<body>
  <main>
    <h1>Статический сайт из Markdown</h1>
    {body}
  </main>
</body>
</html>
"""

dist = ROOT / "dist"
dist.mkdir(exist_ok=True)
(dist / "index.html").write_text(page, encoding="utf-8")

print("Создан dist/index.html")

Сначала программа определяет папку, в которой расположен сам build.py. Поэтому пути исходника и результата считаются относительно программы, а не произвольной текущей папки терминала. read_text читает исходник; явная кодировка UTF-8 нужна для русского текста.

Функция markdown.markdown принимает текст и возвращает HTML-фрагмент. Расширение fenced_code добавляет обработку блоков с тройными обратными кавычками. Это стандартные возможности библиотеки; варианты вызова описаны в документации Python-Markdown.

Полученный фрагмент сам по себе не содержит head и body. Мы помещаем его в полный документ с помощью f-строки: вместо {body} подставляется преобразованное содержание. Затем mkdir создаёт dist, если такой папки ещё нет, а write_text записывает готовую страницу. При следующем запуске тот же файл будет перезаписан.

Запустите сборку из папки markdown-site:

.venv/bin/python build.py

Программа сообщит:

Создан dist/index.html

Сообщение подтверждает, что программа дошла до конца; теперь проверим сам результат. Откройте dist/index.html в редакторе. Внутри документа будет такой фрагмент:

<h2>Первый материал</h2>
<p>Текст статьи хранится в <strong>Markdown</strong>, а браузер получает HTML.</p>
<ul>
<li>Исходник: <code>content/index.md</code></li>
<li>Результат: <code>dist/index.html</code></li>
</ul>
<h3>Небольшой пример</h3>
<pre><code class="language-python">print(&quot;Профессор Веб&quot;)
</code></pre>

Markdown-признаки превратились в HTML-теги. В исходном HTML кавычка записана как &quot;; в браузере она отображается обычной кавычкой. При этом показанный print остался текстом примера: сборщик его не выполняет. Класс language-python обозначает язык, но сам по себе ещё не добавляет цветную подсветку.

Для этой страницы достаточно открыть готовый файл в браузере. Можно также запустить локальный HTTP-просмотр:

.venv/bin/python -m http.server 8000 --bind 127.0.0.1 --directory dist

Перейдите на http://127.0.0.1:8000/. Сервер показывает содержимое dist; в браузере появятся название, раздел «Первый материал», список и код. Параметр --bind ограничивает прослушивание локальным адресом. Этот сервер предназначен для проверки на компьютере; параметры и ограничения описаны в документации Python. Остановить его можно сочетанием Ctrl+C.

Изменение исходника и повторная сборка

Изменим тот же пример. В content/index.md замените только первый абзац:

Текст статьи хранится в **Markdown**. Чтобы обновить страницу, измените исходник и повторите сборку.

Сохраните исходник и обновите вкладку браузера без запуска build.py. Текст страницы останется прежним. Это важное свойство нашей схемы: сервер читает dist/index.html, а не content/index.md. Он не знает, что Markdown изменился.

Теперь выполните в другом окне терминала, из той же папки markdown-site:

.venv/bin/python build.py

Обновите страницу ещё раз. В ней появится новое предложение о повторной сборке. Заголовки, список и пример кода останутся прежними: мы изменили один абзац в знакомом исходнике.

Не исправляйте результат в dist вместо исходного текста. Такая правка пропадёт при следующей сборке, потому что программа заново сформирует файл. Для самостоятельной проверки добавьте в Markdown ещё один пункт списка и повторите запуск: в HTML должен появиться соответствующий li.

У нас получилась первая рабочая цепочка: редактируем content/index.md, запускаем build.py, проверяем dist/index.html. В следующем уроке добавим к тому же исходнику метаданные, чтобы задавать название и описание статьи без изменения программы.