Интеграция Tilda и Django
Как сделать так, чтобы контент из Тильды легко выводился в шаблоне вашего сайта? В этом материале я покажу пример реализации этой задаче на Tilda, Django и кодом на Python.
У меня на работе появилась задача - нужно выкладывать на сайте статьи, сверстанные в конструкторе Tilda. Движок нашего сайта написан на Django и Python, материалы выкладываем через стандартную админку.
Первое решение, которое приходит на ум -- просто экспортнуть код из тильды в виде архива и загрузить к нам в папку на сервере.
Но у этого метода есть масса недостатков.
- Подвала нет
- Навигация отсутствует
- Редактор не может поправить контент
- Нигде в админке не отображается список этих статей
- Статья выглядит чужеродной и никак не встроенной в сайт.
- Переместить статью просто так не получится, нужно будет перемещать файлы
- Невозможно массово отредактировать что-то внутри этих статей
В общем, все недостатки, присущие статичным файлам, выложенным на сервере.
Цель этой статьи - показать вам метод лучше.
Метод лучше
Что, если мы сохраним у себя в базе данных хтмл-код статьи и будем выводить его в нашем шаблоне? С нашей шапкой и подвалом? Тогда у нас будет вроде бы как обычная статья, хранящаяся в базе данных, но сделанная на Тильде. Это решило бы все проблемы из списка выше.
Наша вьюшка выглядела бы примеро так:
def tilda_article(request, pk):
article = get_object_or_404(TildaArticle, pk=pk)
return render('tilda_article.html', article=article)
а шаблон так:
... header ...
{{ article.styles }}
{{ article.scripts }}
{{ article.tilda_content }}
... footer ...
Не будут ли при этом конфликтовать стили и скрипты, ведь все будет выводиться на одной странице?
Я провел несколько опытов и выяснил, что стили и скрипты Тильды не конфликтуют с окружающим контентом сайта. Тильда довольно назависима и хорошо выживает в чужеродной среде.
Пока неплохо. Но как это будет выглядеть со стороны администратора?
Сценарий использования
- Редактор заходит в стандартную админку джанги, создает новую Статью
- Выгружает из Тильды zip-архив, вставляет его специальное поле
- Сохраняет
- Джанга распаковывает архив в куда-то в
MEDIA_ROOT
, а контент статьи в виде готового хтмл-кода сохраняет в базу данных
Само собой при этом должны работать все рисунки и стили и скрипты.
Звучит просто?
На деле в процессе решения этой задачи возникают некоторые трудности. Чтобы их понять, нужно сначала рассмотреть, как устроен архив с экспортируемым контентом из Тильды.
Что в архиве Тильды?
Экспорт из Тильды - это файл с расширением .zip
следующего содержания:
$ zipinfo -1 project1556959.zip
/project1556959/
/project1556959/files/
/project1556959/images/
/project1556959/css/
/project1556959/js/
/project1556959/images/tild6635-6563-4934-b537-356439633464__plast.png
/project1556959/css/tilda-grid-3.0.min.css
/project1556959/css/tilda-blocks-2.12.css
/project1556959/css/tilda-animation-1.0.min.css
/project1556959/css/tilda-slds-1.4.min.css
/project1556959/css/tilda-zoom-2.0.min.css
/project1556959/js/jquery-1.10.2.min.js
/project1556959/js/tilda-scripts-2.8.min.js
/project1556959/js/tilda-blocks-2.7.js
/project1556959/js/lazyload-1.3.min.js
/project1556959/js/tilda-animation-1.0.min.js
/project1556959/js/tilda-slds-1.4.min.js
/project1556959/js/hammer.min.js
/project1556959/js/tilda-zoom-2.0.min.js
/project1556959/images/tild6635-6563-4934-b537-356439633464__-__resize__504x__plast.png
/project1556959/images/tild6635-6563-4934-b537-356439633464__-__empty__plast.png
/project1556959/images/tild6539-6531-4635-b262-633039613131__-__empty__mark.png
/project1556959/images/tild6539-6531-4635-b262-633039613131__mark.png
/project1556959/files/page6917313body.html
/project1556959/page6917313.html
/project1556959/htaccess
/project1556959/readme.txt
/project1556959/images/tildafavicon.ico
/project1556959/images/tildacopy.png
/project1556959/images/tildacopy_black.png
/project1556959/404.html
/project1556959/robots.txt
/project1556959/sitemap.xml
Основной файл с контентом страницы - page6917313.html
. Это полный хтмл со всеми заголовками и телом.
Стили и скрипты в нем прописаны c относительными путями:
<!-- Assets -->
<link rel="stylesheet" href="css/tilda-grid-3.0.min.css" type="text/css" media="all" />
<link rel="stylesheet" href="css/tilda-blocks-2.12.css?t=1498182256" type="text/css" media="all" />
<script src="js/jquery-1.10.2.min.js"></script>
<script src="js/tilda-scripts-2.8.min.js"></script>
<script src="js/tilda-blocks-2.6.js?t=1498182256"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/jquery.touchswipe.min.js"></script>
<script src="js/tilda-forms-1.0.min.js"></script>
<script src="js/lazyload-1.3.min.js">
Парсингом их можно выдрать из тела и сохранить. Это не проблема.
Трудность в том, что если наша статья выводится по адресу /articles/some/
, а архив распакован где-то в media/tilda/xxx
, то нам нужно поправить пути к ассетам на абсолютные.
Архитектура решения
Для реализации задуманного нам понадобится модель, которая будет хранить данные статьи и код, который будет заниматься распаковкой архива и сохранением нужных файлов в нужные места.
Сделаем модель:
class TildaArticle(models.Model):
tilda_content = models.TextField(u'Tilda code', blank=True)
styles = models.TextField(u'Стили', blank=True)
scripts = models.TextField(u'Скрипты', blank=True)
archive = models.FileField(u'Импорт из файла', blank=True, null=True,
upload_to=tilda_upload_to)
def import_archive(self):
"""Распаковать и импортировать загруженный в `archive` файл из Тильды"""
if self.archive:
pass # extract archive...
В поле archive
редактор будет загружать zip-файл, который нам нужно автоматически распаковать.
В styles
и scripts
будет храниться хтмл-код со ссылками на нужные для этой статьи ассеты. Данные для этих полей мы будем получать парсингом основного файла pageXXXX.html
.
Чтобы распаковывать файл сразу после загрузки в админке, повесим обработчик в save_model
:
@admin.register(TildaArticle)
class TildaArticleAdmin(BaseMaterialAdmin):
def save_model(self, request, obj, form, change):
# распаковка архива при импорте
archive_changed = 'archive' in form.changed_data
# запишет файл на диск
super(TildaArticleAdmin, self).save_model(request, obj, form, change)
if archive_changed and obj.archive:
obj.import_archive()
Теперь при загрузке нового файла в поле archive
будет автоматически вызываться код распаковки и парсинга в функции model.import_archive()
.
Давайте напишем его.
Архитектура распаковщика
Я решил сделать отдельный класс, который будет заниматься распаковкой архива и его обработкой.
Требования:
Распаковывать не все файлы
robots.txt
или htaccess
, которые по-умолчанию добавлены в каждый зип-архив, нам не нужны. В целом, нам нужны только файлы из папок images
, css
и js
.
Избавиться от папки project1234
в распакованных файлах
Из файлов нужно парсить данные
В файле pageXXX.html
есть куски кода, ссылающиеся на стили и классы. Их нужно выдрать, чтобы потом сохранить в модель.
Расширяемость
Класс должен быть расширяемым, чтобы другие программисты просто дописали нужные им конкретные реализации. Может быть кто-то и не станет парсить файлы.
При написании я вдохновился примером Django Syndication Framework
.
Вся работа по открытию, перебору и сохранению файлов спрятана где-то внутри базового класса, а мои юзер-логичные функции я просто прицепляю в нескольких местах, переписывая наследника, и фреймворк их вызывает в процессе обработки.
В основе лежит вот такой базовый класс.
class TildaArchive(object):
def __init__(self, path):
self.path = path
def content(self, zipinfo, f):
pass
def extract_path(self, zipinfo):
pass
def done(self):
pass
def process(self):
with zipfile.ZipFile(self.path) as zf:
for zipinfo in zf.infolist():
# парсинг контента
with zf.open(zipinfo) as f:
self.content(zipinfo, f)
# распаковка
with zf.open(zipinfo) as f:
save_as = self.extract_path(zipinfo)
if save_as:
self.save(f, save_as)
self.done()
def save(self, source, targetpath):
# Create all upper directories if necessary
upperdirs = os.path.dirname(targetpath)
if upperdirs and not os.path.exists(upperdirs):
os.makedirs(upperdirs)
with file(targetpath, "wb") as target:
shutil.copyfileobj(source, target)
return targetpath
Он открывает архив и перебирает файлы. Для каждого файла:
- содержимое передает на обработку в функцию
content
; - сохраняет файл по адресу, который вернет функция
extract_path
. А если она возвращает False, то файл пропускается.
После обработки всех файлов вызывается функция done
- это место для хука, который сохраняет все накопленные парсингом данные из разных файлов.
Вы можете работать с этим классом, унаследовавшись от него и написав свой код для функций content
, extract_path
и done
. Сейчас я покажу, как это сделал я, но сначала напишем пример использования класса из нашей модели TildaArticle
.
def import_archive(self):
"""Распаковать и импортировать загруженный в `archive` файл из Тильды"""
if self.archive:
archive = IrkruTildaArchive(self.archive, material=self)
archive.process()
И, собственно, моя реализация с парсингом:
class IrkruTildaArchive(TildaArchive):
def __init__(self, path, material):
super(IrkruTildaArchive, self).__init__(path)
self.material = material
self.styles = None
self.scripts = None
self.body = None
self.extract_root = material.tilda_extract_root
self.extract_url = material.tilda_extract_url
def content(self, zipinfo, f):
"""
Из html-файла парсит ссылки на стили и скрипты
"""
filename = self.strip_project(zipinfo.filename)
if re.match(r'page\d+.html', filename):
html = f.read()
self.styles, self.scripts = self.assets(html)
elif re.match(r'files/page\d+body.html', filename):
self.body = f.read().decode('utf-8')
def done(self):
"""
Вызывается после обработки всех файлов
"""
if self.styles:
self.material.styles = '\n'.join(self.styles)
if self.scripts:
self.material.scripts = '\n'.join(self.scripts)
if self.body:
self.material.tilda_content = self.body
self.material.save()
def extract_path(self, zipinfo):
filename = self.strip_project(zipinfo.filename)
if self.is_css(filename) or self.is_js(filename) or self.is_image(filename):
return os.path.join(self.extract_root, filename)
@staticmethod
def assets(html):
styles, scripts = None, None
link_pattern = re.compile(r'''<link[^>]+rel=["']stylesheet["'].+?>''')
styles = link_pattern.findall(html)
link_pattern = re.compile(r'''<script\s+src=["'].+?></script>''')
scripts = link_pattern.findall(html)
return styles, scripts
Я не включил описание нескольких вспомогательных методов, их код вы найдете в полной версии в конце статьи.
Шаблонные фильтры
Я решил сохранять пути к статик-файлам в базе данных как есть, без изменений. А уже при выводе заменять пути на абсолютные.
Для этого я использую дополнительные шаблонные теги:
# app/templatetags/tilda_tags.py
@register.simple_tag
def tilda_scripts(article):
return mark_safe(article.prepare_scripts())
@register.simple_tag
def tilda_styles(article):
return mark_safe(article.prepare_styles())
@register.simple_tag
def tilda_content(article):
return mark_safe(article.prepare_content())
Код в модели:
class TildaArticle(models.Model):
...
def prepare_content(self):
"""Возвращает готовый к выводу хтмл с поправленными путями к изображениям"""
result = self.tilda_content.replace('="images/', '="{}images/'.format(self.tilda_extract_url))
result = result.replace("url('images/", "url('{}images/".format(self.tilda_extract_url))
return result
def prepare_scripts(self):
return self.scripts.replace('src="js/', 'src="{}js/'.format(self.tilda_extract_url))
def prepare_styles(self):
return self.styles.replace('href="css/', 'href="{}css/'.format(self.tilda_extract_url))
И вывод в шаблоне:
{% load tilda_tags %}
<html>
<head>
{% tilda_styles article %}
{% tilda_scripts article %}
</head>
<body>
{% tilda_content article %}
</body>
Ссылки по теме
save_model: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#modeladmin-methods
Полный код примера
Я собрал небольшое приложение, полностью функционирующее и содержащее логику из статьи.