Существует проблема: У сайта в IPFS нет возможности использовать серверные скрипты для формирования страницы. Если использовать генерацию страниц перед загрузкой то добавив новый пункт меню в каждую страницу мы изменим хеш этих страниц. Так что всю сборку страниц нужно производить силами браузера.


Обычно формируют содержание страниц при помощи JavaScript. Это знакомая технология но у неё есть свои недостатки.


Я буду использовать XSLT. Это древняя технология шаблонов которая давно встроена в браузеры но мало кто ей пользуется. Возможно потому что шаблоны заставляют писать много текста и из за путаницы с пространствами имён и множества ошибок без внятного объяснения. Также не смотря на то что есть уже XSLT 3.0 в браузерах по прежнему доступен только XSLT 1.0.


XSLT работает так:


  1. Пользователь открывает в браузере XML документ.
  2. В заголовке XML документ содержит ссылку на XSLT шаблон.
    <?xml-stylesheet href="xslt/запись.xslt" type="text/xsl" ?>
  3. Шаблон в браузере на основе XML документа и других данных формирует xHTML документ.
  4. Браузер отображает полученный xHTML документ.

Привязав множество страниц к одному шаблону можно менять отображаемый xHTML документ не меняя XML документы. Таким образом при смене дизайна не будет меняться хеш XML документов а значит старые их копии будут источниками для новых в IPFS.


Для поисковиков в данном способе тоже есть плюсы. Они ограничиваются обработкой XML документа получая только уникальный контент страницы без элементов навигации и остальных блоков которые повторяются на каждой странице.


image



В XML можно использовать любые теги. Главное не нарушать синтаксис XML который немного строже чем в HTML:


  1. Все теги должны быть закрыты.
    Но есть возможность использовать самозакрытые теги <br />.
  2. На верхнем уровне может быть только один тег. Остальные теги должны быть заключены в него.
    <корень>
        <потомок />
        <ещё-один />
        <и-ещё-один />
    </корень>
  3. Текст может быть только внутри тегов.
    <корень>
        Здесь текст
    </корень>

новая запись.xml


Наша задача сделать простой и понятный для пользователя блог который он сможет самостоятельно пополнять с помощью простого текстового редактора.


XML шаблон блого-записи для заполнения пользователем:
Файл: 'новая запись.xml'


<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="xslt/запись.xslt" type="text/xsl" ?>
<запись>
    <заголовок></заголовок>
    <текст>
        <срез></срез>
    </текст>
</запись>
<!-- Здесь можно добавить небольшую справку как этим пользоваться -->

По названию тегов уже понятно что где писать не зная английского. Вопрос может вызвать только тег срез. Его содержимое отправится в meta тег description. А в дальнейшем может использоваться для отображения в ленте.


В тексте мы можем использовать xHTML теги(те же HTML теги) для стилизации текста.


Пример заполнения:


Файл: 'Lorem Ipsum.xml'
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="xslt/запись.xslt" type="text/xsl" ?>
<запись>
    <заголовок>Lorem Ipsum</заголовок>
    <текст>
<h1>Lorem ipsum dolor sit amet</h1>

<срез>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras feugiat orci non orci tincidunt molestie. Donec eget porta metus. Aenean malesuada nunc lacus, a sollicitudin neque fermentum eu. <b style="color: green">Vestibulum egestas viverra urna</b>, sit amet mollis ipsum commodo imperdiet. Quisque sit amet est eu nibh commodo pharetra.</срез> Donec fermentum nisi nec lorem auctor imperdiet. Morbi ultricies at magna ullamcorper malesuada. In eget arcu dapibus, vehicula nunc vel, convallis erat. Cras accumsan lacus vel tellus gravida, eu gravida turpis congue. In ultrices pellentesque odio at hendrerit.

<h1>Cras nec lorem facilisis</h1>

Cras nec lorem facilisis justo porttitor scelerisque id id ipsum. Duis mattis, sem eu sollicitudin pharetra, diam augue ullamcorper nisl, id dictum turpis ex in nisl. Aliquam venenatis egestas urna eget porttitor. Suspendisse potenti. Curabitur fermentum fermentum nulla, a lobortis ipsum laoreet eget. Duis finibus lacus id tempor elementum. <i>Interdum et malesuada fames ac ante ipsum primis in faucibus</i>. Curabitur tempus eget turpis ut tincidunt. In quis massa et risus tempor luctus quis eu enim. Duis finibus sem sit amet elit scelerisque molestie. Suspendisse laoreet, leo ut accumsan dapibus, nisi tellus dignissim lorem, eu auctor tellus diam vel nisi. Donec sem tortor, suscipit a leo maximus, aliquet dapibus nisl. Integer sodales, dui ut vulputate pharetra, ipsum neque tempor ipsum, sed porta tortor quam a nisl.

Sed consequat purus sed porttitor pulvinar. Mauris sollicitudin non sem a malesuada. Vestibulum non velit finibus, elementum purus nec, aliquam lacus. Etiam leo enim, venenatis id luctus ut, ornare sit amet ex. Nunc vel fringilla ante. Cras lobortis scelerisque pulvinar. Ut lectus augue, viverra quis molestie eu, maximus nec leo. <u>Sed facilisis odio ut malesuada mollis</u>. Pellentesque enim lacus, luctus nec velit quis, scelerisque tincidunt magna. Proin laoreet, erat eget porttitor bibendum, odio augue suscipit purus, id commodo magna nibh sed est. Ut dignissim aliquet sem, id suscipit sapien mollis volutpat. Vivamus ac fringilla est, at sagittis diam.
    </текст>
</запись>

запись.xhtml


Обычно формирование всего xHTML запихивают в XSLT шаблоны. Они от этого со временем будут разрастаться и усложняться. Нечего верстальщику делать в XSLT шаблонах.


Для верстальщика у нас будет xHTML страница в которой он может пометить классами элементы в которые надо вставить содержимое одноимённых тегов из XML.


Файл: 'xhtml/запись.xhtml'


<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <!-- если элемент <заголовок> будет пуст то в title будет этот текст -->
        <title class="заголовок">Простой блог на XML</title>
        <meta  class="срез"/>
        <!-- поскольку стиль маленький он включён в HTML -->
        <style>
            /* делаем использование xHTML необязательным в тексте */

            .текст{
                white-space: pre-wrap;
            }

            /* чтобы небыло больших отступов инлайним заголовки */

              .текст h1, .текст h2
            , .текст h3, .текст h4
            , .текст h5, .текст h6 {
                display: inline;
            }
        </style>
    </head>
    <body>
        <!-- надеюсь по именам классов понятно что будет в этих элементах -->
        <ul   class="навигация" />
        <h1   class="заголовок" />
        <span class="текст">Элемент <b style="color: red"><![CDATA[<текст></текст>]]></b> не содержит текста. Заполните его.</span>
    </body>
</html>

запись.xslt


Теперь надо связать эти два файла(XML и xHTML) при помощи XSLT.


Шаблон читает файл 'запись.xhtml' и заменяет содержимое тегов по классам на содержимое тегов с соответствующими именами в XML. Также он меняет некоторые теги в XML и привязывает к пространству имён xHTML.


Файл: 'xslt/запись.xslt'


<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
  version="1.0"
  xmlns="http://www.w3.org/1999/xhtml"
  xmlns:xhtml="http://www.w3.org/1999/xhtml"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <!--
        Шаблон для навигации я вынес в отдельный файл 'навигация.xslt'
        Пути в шаблоне идут относительно файла шаблона
        Chrome не любит кириллицу в URL поэтому кодируем URL
    -->
    <xsl:include href="%D0%BD%D0%B0%D0%B2%D0%B8%D0%B3%D0%B0%D1%86%D0%B8%D1%8F.xslt" />

    <!-- на выходе у нас будет xHTML а это тот же XML -->
    <xsl:output method="xml" encoding="UTF-8"/>

    <!-- сохраняем ссылку на узел с данными  -->
    <xsl:variable name="запись" select="запись" />

    <!-- сразу уходим в документ 'запись.xhtml' -->
    <xsl:template match="/">
        <!-- mode не даёт нам зациклиться и напоминает какой документ обрабатывает каждый шаблон -->
        <xsl:apply-templates mode="xhtml" select="document('../xhtml/%D0%B7%D0%B0%D0%BF%D0%B8%D1%81%D1%8C.xhtml')" />
    </xsl:template>

    <!-- копируем всё для чего нет шаблона ниже -->
    <xsl:template mode="xhtml" match="node()|@*">
        <xsl:copy>
            <xsl:apply-templates mode="xhtml" select="node()|@*"/>
        </xsl:copy>
    </xsl:template>

    <!-- на этом этапе мы видим только содержимое страницы 'запись.xhtml' -->

    <!-- добавляем содержимое элемента 'срез' если оно есть в элементы с классом 'срез' -->
    <xsl:template mode="xhtml" match="*[contains(@class, 'срез')][normalize-space($запись/текст//срез)]">
        <xsl:copy>
            <xsl:copy-of select="@*"/>
            <xsl:apply-templates mode="xml" select="$запись/текст//срез/node()" />
        </xsl:copy>
    </xsl:template>

    <!-- добавляем в meta тег содержимое элемента 'срез' если оно есть -->
    <xsl:template mode="xhtml" match="xhtml:meta[contains(@class, 'срез')][normalize-space($запись/текст//срез)]">
        <meta name="description" content="{$запись/текст//срез}" />
    </xsl:template>

    <!-- добавляем на страницу навигацию по записям -->
    <xsl:template mode="xhtml" match="*[contains(@class, 'навигация')]">
        <!-- вызываем шаблон по имени из файла 'навигация.xsl' -->
        <xsl:call-template name="навигация" />
    </xsl:template>

    <!-- добавляем содержимое элемента 'заголовок' если оно есть в элементы с классом 'заголовок' -->
    <xsl:template mode="xhtml" match="*[contains(@class, 'заголовок')][normalize-space($запись/заголовок)]">
        <xsl:copy>
            <xsl:copy-of select="@*"/>
            <xsl:value-of select="$запись/заголовок" />
        </xsl:copy>
    </xsl:template>

    <!-- добавляем содержимое элемента 'текст' если оно есть в элементы с классом 'текст' -->
    <xsl:template mode="xhtml" match="*[contains(@class, 'текст')][normalize-space($запись/текст)]">
        <xsl:copy>
            <xsl:copy-of select="@*"/>
            <xsl:apply-templates mode="xml" select="$запись/текст/node()" />
        </xsl:copy>
    </xsl:template>

    <!-- 
        теперь нам нужно поправить элементы исходного xml 

        копируем элементы у которых задано пространство имён
        (это может быть svg изображение) и атрибуты
     -->
    <xsl:template mode="xml" match="*[namespace-uri()]|@*">
        <xsl:copy>
            <xsl:apply-templates mode="xml" select="node()|@*" />
        </xsl:copy>
    </xsl:template>

    <!-- зададим пространство имён элементам без него -->
    <xsl:template mode="xml" match="*">
        <!--
            Создаём элемент с тем же именем. У нового элемента 
            пространство имён уже заданно по умолчанию
        -->
        <xsl:element name="{name()}">
            <xsl:apply-templates mode="xml" select="node()|@*" />
        </xsl:element>
    </xsl:template>

    <!-- оставляем от элемента 'срез' только его содержимое -->
    <xsl:template mode="xml" match="срез">
        <xsl:apply-templates mode="xml" select="node()|@*" />
    </xsl:template>

    <!-- даём возможность пользователю полноценно использовать xHTML заголовки --> 
    <xsl:template mode="xml" match="h1|h2|h3|h4|h5">
        <!-- 
            h1 у нас уже используется для элемента 'заголовок' 
            поэтому увеличиваем индекс xHTML заголовков пользователя
        -->
        <xsl:element name="h{substring(name(), 2, 1) + 1}">
            <!-- раз уж мы полезли в заголовки то добавим им id для навигации по записи -->
            <xsl:attribute name="id">
                <xsl:value-of select="." />
            </xsl:attribute>
            <xsl:apply-templates mode="xml" select="node()|@*" />
        </xsl:element>
    </xsl:template>

</xsl:stylesheet>


Навигация нужна для перемещения между записями блога. К каждой записи мы добавляем список ссылок на другие страницы.


журнал.xml


У нас будет XML файл в котором просто перечислены все записи:


Файл: 'журнал.xml'


<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="xslt/журнал.xslt" type="text/xsl" ?>
<журнал>
    <запись>новая запись</запись>
</журнал>
<!-- Здесь можно добавить небольшую справку как этим пользоваться -->

Содержимое тега 'запись' это имя xml файла записи без расширения.


Пример заполнения:


<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="xslt/журнал.xslt" type="text/xsl" ?>
<журнал>
    <запись>Lorem Ipsum 1</запись>
    <запись>Lorem Ipsum 2</запись>
</журнал>


Теперь пишем простенький шаблон который из файла 'журнал.xml' сформирует xHTML список со ссылками на другие страницы.


Файл: 'xslt/навигация.xslt'


<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
  version="1.0"
  xmlns="http://www.w3.org/1999/xhtml"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <!-- сюда мы приходим из шаблона 'запись.xsl' -->
    <xsl:template name="навигация">
        <ul>
            <!-- копируем атрибуты -->
            <xsl:copy-of select="@*" />

            <!-- сразу выбираем из документа 'журнал.xml' нужные узлы -->
            <xsl:apply-templates mode="навигация" select="document('../%D0%B6%D1%83%D1%80%D0%BD%D0%B0%D0%BB.xml')/журнал/запись" />
        </ul>
    </xsl:template>

    <!-- делаем список ссылок на другие записи -->
    <xsl:template mode="навигация" match="запись">
        <li>
            <a href="{text()}.xml">
                <xsl:value-of select="text()" />
            </a>
        </li>
    </xsl:template>

</xsl:stylesheet>

На этом этапе страницы блога готовы к заполнению и просмотру.


перенаправление


Теперь нам нужно посетителей перенаправить на последнюю запись в журнале.


index.html


К сожалению в IPFS пока не возможно сделать страницей поумолчанию что то кроме index.html. Поэтому сделаем в ней простое перенаправление на XML страницу.


Задача этого файла перенаправить пользователя на страницу 'журнал.xml'.


Файл: 'index.html'


<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>Перенаправление</title>
        <!-- автоматическое перенаправление на страницу 'журнал.xml' -->
        <!-- Firefox не любит кириллицу в параметре url поэтому имя закодировано-->
        <meta http-equiv="refresh" content="0; url=%D0%B6%D1%83%D1%80%D0%BD%D0%B0%D0%BB.xml" />
    </head>
    <body>
        <!-- на случай если автоматическое перенаправление не сработает -->
        Перенаправление на:
        "<a class="перенаправление" href="журнал.xml">журнал</a>"
    </body>
</html>

Это HTML оборотень. Он правильный XML и HTML одновременно. Это нам пригодится дальше.


журнал.xslt


'журнал.xml' при открытии в браузере вызывает шаблон 'журнал.xslt'. Тот в свою очередь берёт первую запись из 'журнал.xml' и переиспользуя 'index.html' перенаправляет на первую запись.


Файл: 'xslt/журнал.xslt'


<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
  version="1.0"
  xmlns="http://www.w3.org/1999/xhtml"
  xmlns:xhtml="http://www.w3.org/1999/xhtml"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:output method="xml" encoding="UTF-8"/>

    <xsl:variable name="запись" select="журнал/запись/text()" />

    <xsl:template match="/">
        <!-- используем уже готовый образец страницы перенаправления -->
        <xsl:apply-templates mode="html" select="document('../index.html')" />
    </xsl:template>

    <!-- копируем элементы и атрибуты -->
    <xsl:template mode="html" match="node()|@*">
        <xsl:copy>
            <xsl:apply-templates mode="html" select="node()|@*" />
        </xsl:copy>
    </xsl:template>

    <!-- меняем необходимые элементы -->

    <!-- меняем адрес автоматического перенапрвления -->
    <xsl:template mode="html" match="xhtml:meta[@http-equiv='refresh']">
        <!--
             Хак
             1. Меняем базовый URL
                Таким образом мы избегаем необходимости кодирования URL
        -->
        <base href="{$запись}.xml" />

        <!-- 2. Обновляем страницу -->
        <meta http-equiv="refresh" content="0; url=#" />
    </xsl:template>

    <!-- меняем имя и адрес ссылки на странице перенаправления -->
    <xsl:template mode="html" match="xhtml:a[@class='перенаправление']">
        <a href="" class="{@class}">
            <xsl:value-of select="$запись" />
        </a>
    </xsl:template>

</xsl:stylesheet>

Шаблон 'журнал.xslt' можно переработать так что он будет отображать ленту записей вместо простого перенаправления.


добавляем новую запись


  1. В текстовом редакторе в файл 'журнал.xml' добавляем имя первой записи:


    <?xml version="1.0" encoding="UTF-8"?>
    <?xml-stylesheet href="xslt/журнал.xslt" type="text/xsl" ?>
    <журнал>
        <запись>моя первая запись</запись>
    </журнал>

  2. Теперь копируем файл 'новая запись.xml' с именем в моём случае 'моя первая запись.xml'


  3. Открываем копию в текстовом редакторе и заполняем:


    <?xml version="1.0" encoding="UTF-8"?>
    <?xml-stylesheet href="xslt/запись.xslt" type="text/xsl" ?>
    <запись>
        <заголовок>Моя первая запись</заголовок>
        <текст>
    Текст моей первой записи.
        </текст>
    </запись>


Первая запись готова.


локальное тестирование


Для возможности предпросмотра в браузере перед загрузкой в IPFS нужно настроить браузер.


Firefox


  1. создаём отдельный профиль
  2. на странице about:config установить пункт security.fileuri.strict_origin_policy в false

Chrome


  1. создаём отдельный профиль
  2. запускаем с дополнительным флагом --allow-file-access-from-files

Загрузка в IPFS


Перед загрузкой естественно надо заполнить блог.


Генерируем ID для блога


Нам нужно сгенерировать новый уникальный ключь который мы будем использовать для публикации в блог.


ipfs key gen -t rsa blog

В результате будет ID блога


загружаем


  1. Загружаем блог в ipfs


    ipfs add -r -s rabin --inline --raw-leaves --fscache <каталог блога>

    -r — загружаем каталог
    -s rabin — полезно для дедупликации больших jpeg, mp3 и других файлов в которых есть возможность менять метаданные.
    --inline — встраивает микро блоки в родительский блок.
    --raw-leaves — добавляет совместимость с загрузкой в режиме --nocopy который позволяет загружать большие файлы не копируя их в репозитарий.
    --fscache — если блок уже есть в репозитарии то он не будет копироваться в него заново.


  2. Привязываем хеш полученный на первом этапе к ID блога.


    ipfs name publish -k blog <хеш>


проверяем


В браузере открываем два адреса:


http://127.0.0.1:8080/ipfs/<хеш>
http://127.0.0.1:8080/ipns/<id блога>

И глобально:


http://gateway.ipfs.io/ipfs/<хеш>
http://gateway.ipfs.io/ipns/<id блога>

Соответственно по хешу будет доступна только определённая версия блога. А по ID самая новая загруженная пользователем версия.


Готово.


Заключение


Естественно дизайн и функциональность этого блога можно и нужно развивать под потребности пользователя. Я задал некоторую базу от которой можно развиваться дальше.


Ссылки


Хороший справочник по XSLT
Код на GitHub (снимок)