Из 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("Профессор Веб")
</code></pre>
Markdown-признаки превратились в HTML-теги. В исходном HTML кавычка записана как "; в браузере она отображается обычной кавычкой. При этом показанный 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. В следующем уроке добавим к тому же исходнику метаданные, чтобы задавать название и описание статьи без изменения программы.