Создание генератора статических сайтов с помощью Deno

Легко используйте TypeScript и Markdown с Deno для создания генератора статических сайтов

Создание статического веб-контента является абсолютной необходимостью. Если вы когда-либо занимались веб-разработкой за последние 10 лет, вы, скорее всего, сталкивались с генераторами статических сайтов, такими как Jekyll или Gatsby. Даже системы CMS, такие как Wordpress, довольно много выполняют генерацию статического контента.

Благодаря современному JavaScript вам не нужно быть мастером программирования, чтобы создавать причудливые генераторы статических сайтов. На самом деле, вы можете добиться очень многого, используя очень мало кода. Это то, что мы собираемся сделать в этой записи блога, и мы собираемся использовать удивительную новую технологию под названием Deno.

Кроме того, мы будем использовать TypeScript, НО ПОДОЖДИТЕ! для этого руководства не требуются предварительные навыки работы с TypeScript. Если вы хорошо разбираетесь в JavaScript, все будет в порядке. На самом деле, вы можете использовать это, чтобы окунуться в TypeScript, если вы еще этого не сделали.

Что такое Дено?

Я не буду вдаваться в подробности о Deno, но вы можете думать о нем как о духовном преемнике Node.js, который также создан первоначальным создателем узла.

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

Одна огромная разница между deno и node заключается в отсутствии менеджера пакетов. Вместо того, чтобы указывать пакеты или сторонние зависимости в отдельном файле, пакеты deno указываются в коде с операторами импорта на основе URL, например:

import { parse } from 'https://deno.land/[email protected]/datetime/mod.ts'

При запуске вашей программы эти зависимости проверяются, устанавливаются и кэшируются.

В целях безопасности вы используете флаги для определения уровня прав доступа к файлам и доступа к сети, доступного для сценариев при запуске с помощью deno, например:

deno run --allow-read mod.ts

Установка

Примечание. Этот шаг можно пропустить, если у вас уже установлен deno.

Чтобы установить deno в первый раз, используйте любой из следующих вариантов:

MacOS или Linux (оболочка)

curl -fsSL https://deno.land/x/install/install.sh | sh

MacOS (варенье)

brew install deno

Окна

iwr https://deno.land/x/install/install.ps1 -useb | iex

Примечание. В оставшейся части руководства будут использоваться команды оболочки, предназначенные для MacOS/Linux, однако все инструкции по написанию кода будут одинаковыми для Windows.

Что мы будем генерировать

Нашей целью будет написать скрипт deno на TypeScript, который будет принимать один файл .md в качестве входных данных и генерировать следующее:

  • Макет веб-сайта с панелью навигации, областью контента и нижним колонтитулом.
  • Папки и index.html файлов, которые соответствуют структуре веб-сайта (1 уровень глубины)
  • Действительные значения href во всех навигационных ссылках
  • Сгенерированная таблица стилей, связанная на каждой странице
  • Фавикон эмодзи в формате SVG, ссылка на который есть на каждой странице.

Думайте о веб-сайтах программно

Если вы все еще работаете над основами своего веб-сайта, генератор статических сайтов — отличный проект, который поможет укрепить эти навыки. Как только вы закончите это, вы увидите, что небо — это предел. Создание собственной функционирующей CMS, автоматически сгенерированной документации или автоматизация обслуживания маркетингового сайта не за горами.

Давайте создадим наш генератор сайтов Deno Markdown

Примечание. Весь исходный код этой записи блога доступен по адресу github.com/nafeu/deno-md-site.

Начните с создания новой папки проекта и файла main.ts:

mkdir deno-md-site
cd deno-md-site
touch main.ts

Откройте main.ts в выбранном вами редакторе и пока добавьте следующие комментарии:

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

Шаг 0. Получите аргументы CLI

Когда мы запустим наш скрипт, мы укажем путь к файлу .md (имя файла) и путь сборки, где будет создан наш веб-сайт, чтобы получить доступ к этим аргументам командной строки, мы можем использовать Deno.args следующим образом:

/* Step 0: Grab CLI arguments */
const filename = Deno.args[FIRST_ITEM_INDEX];
const buildPath = Deno.args[SECOND_ITEM_INDEX] || './build';

Вы заметите, что мы использовали здесь две константы, FIRST_ITEM_INDEX и SECOND_ITEM_INDEX, мы еще не объявили их, так что давайте начнем и сделаем это в нашем разделе констант следующим образом:

/* Section: Constants */
const FIRST_ITEM_INDEX = 0;
const SECOND_ITEM_INDEX = 1;

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

Теперь, если мы протестируем наш скрипт, мы должны увидеть следующее:

$ deno run main.ts
Please specify .md file

И с двумя аргументами мы получаем:

$ deno run main.ts testFile testDir
Building site with 'testFile' into 'testDir'

Определение нашего решения

Прежде чем мы продолжим, есть пара вещей, которые мы хотим обдумать. Какие значения по умолчанию мы должны учитывать? Мы пропустили некоторые зависимости? Как мы можем использовать types, чтобы четко определить проблему, которую мы собираемся решить?

Давайте добавим следующее в наш раздел Interfaces and Globals:

Здесь мы говорим, что хотим, чтобы наш скрипт сосредоточился на типе объекта Page, который будет иметь:

  • строка path на страницу (подумайте о ссылке на страницу на веб-сайте, а не о пути в нашей файловой системе)
  • строка name, которая будет заголовком нашей страницы
  • строка html, которая будет включать весь HTML для этой страницы

Мы хотим рассмотреть дополнительный объект Layout, который может хранить информацию о чем-то вроде многократно используемого нижнего колонтитула. Наконец, мы определяем массив pages, в котором хранятся объекты Page, а также создается экземпляр нашего объекта Layout.

Теперь, когда мы это настроили, мы импортируем пару дополнительных зависимостей. Мы будем импортировать Marked из https://deno.land/x/markdown и ensureFileSync из https://deno.land/std. Это поможет нам проанализировать уценку, а также создать/сохранить пути к файлам и каталогам соответственно.

Мы можем импортировать их так:

Шаг 1. Разберите метаданные и компоненты из файла уценки

Создайте новый TextDecoder с опцией кодировки utf-8 и прочитайте содержимое файла, который мы указали при запуске нашего скрипта:

Теперь у нас есть доступ к содержимому нашего файла, но подождите… что вообще находится внутри этого файла? Давайте сделаем шаг назад и примем некоторые решения о том, как мы хотим создавать шаблоны с использованием уценки.

Принятие решения о механике шаблона

Давайте сохраним наши правила простыми:

  • используйте YAML Front Matter, чтобы объявить заголовок веб-сайта, необязательные стили css и необязательный значок смайликов.
  • используйте тройные плюсы (+++) для разделения страниц и компонентов макета
  • используйте формат /[PAGE_PATH]:[PAGE_TITLE] под тройными плюсами для обозначения пути и заголовка страницы соответственно
  • используйте обычную уценку для содержимого страницы в области ниже объявлений пути/заголовка страницы
  • обязательная домашняя страница в формате /home:Home

Мы можем создать пример файла .md с touch example.md и заполнить его:

---
title: Deno Markdown Site
styles: >
  body { color: #22a6b3; }
favicon: 🦕
---
/home:Home

# Home

Hello world!

+++
/about:About

# About

Built for learning.

+++
layout:footer

deno-md-site

Теперь в нашем файле main.ts давайте разделим содержимое файла на +++, создадим константу с именем COMPONENT_DELIMITER:

/* Section: Constants */
const FIRST_ITEM_INDEX = 0;
const SECOND_ITEM_INDEX = 1;
const COMPONENT_DELIMITER = '+++';

Мы будем использовать эту константу с fileContent.split(...) в Step 1:

Если вы временно добавите console.log(components) и запустите скрипт, используя deno run --allow-read main.ts example.md, вы должны увидеть следующее:

Мы видим, что в этом массиве есть 3 элементов, домашняя страница с вводной частью (заголовок, стили и значок значка), страница с информацией и нижний колонтитул. компонент макета. Каждый из этих элементов имеет допустимую уценку, это очень важно для нас.

Давайте извлечем вступительную часть из первого элемента, используя библиотеку Marked:

В этом случае Marked.parse(...) принимает допустимую уценку, выполняет некоторую обработку, а затем возвращает некоторые значения. Из этих значений мы выбираем деконструировать только поле с именем meta, затем мы переименовываем это поле в frontMatter для лучшего чтения.

Давайте добавим console.log({ title, styles, favicon }), а затем запустим наш скрипт, используя:

deno run --allow-read --unstable main.ts example.md

Примечание. Мы используем флаги --allow-read, чтобы разрешить операции чтения в нашей файловой системе, и флаги --unstable, поскольку некоторые из стандартных библиотек Deno еще не на 100 % стабильны.

Мы должны увидеть:

Здорово! Теперь мы знаем, как разделить наш шаблонный файл уценки и извлечь полезную информацию. Мы можем перейти к созданию остальных страниц.

Шаг 2. Создание данных страницы из компонентов

Давайте используем Marked для создания HTML-разметки из нашей уценки, добавим следующее в Step 2:

Marked.parse(...) возвращает поле content, содержащее HTML, который мы здесь деструктурируем. Мы можем использовать временный оператор console.log(...), чтобы увидеть его результаты. Запускаем скрипт снова и у нас должно быть:

Потрясающий! Наконец-то у нас есть сгенерированная разметка для работы. Для каждого компонента вы можете видеть, что первый тег — это тег абзаца с путем и заголовком страницы. Давайте используем регулярные выражения, чтобы вытащить их.

Использование простых регулярных выражений для извлечения разметки

Примечание. Я не эксперт по регулярным выражениям и обычно использую regex101.com для создания шаблонов.

Учитывая образец текста

<p>/home:Home</p>
<h1 id="home">Home</h1>
<p>Hello world!</p>

Мы хотим вытащить /home и Home из первого тега. Точно так же мы хотим вытащить layout и footer из

<p>layout:footer</p>
<p>deno-md-site</p>

Мы можем думать об этих значениях как о наших Значениях типов компонентов, и для их извлечения мы можем использовать следующий шаблон:

Это можно разбить следующим образом:

  • соответствует первому экземпляру <tag>[GROUP_1]\:[GROUP_2]</tag>
  • <\S> и <\/\S> будут соответствовать открывающему и закрывающему тегу
  • (.*?)\:(.*?) будет соответствовать ЛЮБЫМ значениям от <tag> до :, а затем от : до </tag> соответственно в группы захвата
  • первая группа захвата (.*?) дает тип компонента, который может быть
  • путь к странице, начинающийся с /
  • слово макет означает, что это компонент макета
  • вторая группа захвата (.*?) дает значение компонента, которое может быть
  • название страницы для пути к странице
  • определенный компонент макета (например, нижний колонтитул)
  • \S соответствует любому непробельному символу
  • использование флага глобального шаблона g означает, что мы будем сопоставлять весь текст, указанный

Добавим этот паттерн в наши константы

/* Section: Constants */
const FIRST_ITEM_INDEX = 0;
const SECOND_ITEM_INDEX = 1;
const COMPONENT_DELIMITER = '+++';
const COMPONENT_TYPE_PATTERN = /<\S>(.*?)\:(.*?)<\/\S>/g;

А затем давайте обновим Step 2 следующим образом:

Здесь мы берем этот html content и используем content.matchAll(COMPONENT_TYPE_PATTERN) для получения всей нашей matchedComponentType информации. Затем мы используем деструктурирование массива с помощью const [, path, name], чтобы игнорировать первый элемент (который будет нашим полным соответствием), а затем извлекаем path и name из соответствующих групп.

Поскольку мы регистрируем path и name, повторный запуск скрипта должен дать нам:

Теперь нам также нужен фактический HTML-контент для каждого из этих компонентов. Для начала давайте объявим еще один шаблон:

const HTML_CONTENT_PATTERN = /\n(.*)/gs;

Это гораздо более простая модель, которую можно разбить следующим образом:

  • \n(.*) соответствует всему, что идет после первого символа новой строки (\n)
  • (...) создает группу захвата
  • .* означает, что он будет соответствовать ЛЮБОЙ последовательности символов
  • использование флага глобального шаблона s означает, что символ новой строки также соответствует области действия .
  • использование флага глобального шаблона g означает, что мы будем сопоставлять весь текст, указанный

Итак, учитывая текст

<p>/home:Home</p>
<h1 id="home">Home</h1>
<p>Hello world!</p>

Мы хотим извлечь содержимое нашей HTML-страницы как

<h1 id="home">Home</h1>
<p>Hello world!</p>

Давайте обновим наши константы нашим новым шаблоном

/* Section: Constants */
const FIRST_ITEM_INDEX = 0;
const SECOND_ITEM_INDEX = 1;
const COMPONENT_DELIMITER = '+++';
const COMPONENT_TYPE_PATTERN = /<\S>(.*?)\:(.*?)<\/\S>/g;
const HTML_CONTENT_PATTERN = /\n(.*)/gs;

И обновите Step 2, чтобы он также использовал новый шаблон для извлечения HTML.

Мы также добавили html в оператор журнала, запуск скрипта должен дать нам:

Безнравственный! Теперь, когда у нас есть компоненты, мы можем сохранить их в соответствующих переменных layout и pages, которые мы объявили ранее в нашем разделе Interfaces and Globals. Добавьте наше последнее обновление в Step 2:

Объявите еще одну константу LAYOUT_PREFIX и добавьте ее так:

/* Section: Constants */
const FIRST_ITEM_INDEX = 0;
const SECOND_ITEM_INDEX = 1;
const COMPONENT_DELIMITER = '+++';
const COMPONENT_TYPE_PATTERN = /<\S>(.*?)\:(.*?)<\/\S>/g;
const HTML_CONTENT_PATTERN = /\n(.*)/gs;
const LAYOUT_PREFIX = 'layout';

Мы будем использовать LAYOUT_PREFIX, чтобы решить, является ли возвращенный path компонентом макета или нет, затем мы либо отобразим значения компонента в объект layout, либо поместим весь компонент в массив pages.

Если мы зарегистрируем layout и pages в этот момент и запустим скрипт, мы должны увидеть:

Довольно мило, да?

Шаг 3. Создайте шаблоны для HTML-контента

Теперь, когда у нас есть весь наш контент, нам нужно создать весь остальной HTML-код, необходимый для нашего сайта. Мы создадим набор вспомогательных функций шаблона:

Прежде чем мы начнем, добавим еще две константы HOME_PATH и STYLESHEET_PATH:

Это поможет нам двигаться вперед, давайте добавим вспомогательный метод isHomePath, который поможет нам решить, является ли обрабатываемый компонент домашней страницей:

Затем добавим хелпер, который принимает строку path, определяет, находимся ли мы на домашней странице, а затем возвращает окончательный путь к таблице стилей (мы скоро сгенерируем):

Теперь для создания шаблонов давайте добавим помощник шаблона для создания фавикона в виде svg:

Поскольку фавикон эмодзи является необязательным, если мы его получаем, мы отображаем его, если нет, по умолчанию используется 🦕. Теперь давайте создадим <div> для навигации следующим образом:

Это берет строку currentPath, перебирает все page, которые у нас есть в нашей переменной pages, и сопоставляет набор тегов <a>, где ONE будет иметь дополнительный класс selected, если currentPath и страница path совпадают. href также определяется тем, является ли страница домашней. Этот метод создает что-то вроде:

Мы также можем быстро объявить шаблон нижнего колонтитула следующим образом:

Это просто проверяет наше ранее объявленное сопоставление layout, чтобы увидеть, существует ли нижний колонтитул, и, если он существует, извлекает его HTML в div. В нашем сценарии это даст нам:

<div id="footer">
  <p>deno-md-site</p>
</div>

Давайте добавим оба этих помощника к Step 3, чтобы у нас было:

Теперь о нашем ОСНОВНОМ html-содержимом, которое является основной структурой самих файлов index.html. Давайте создадим еще один помощник шаблона с именем getHtmlByPage, который принимает тип Page в качестве входных данных:

Здесь вы можете видеть, что мы используем все ранее объявленные помощники и переменные, которые имеют отношение к нашей цели.

  • ${name} – это название текущей Страницы.
  • ${title} — это название всего нашего веб-сайта (т.е. Deno Markdown Site)
  • ${getStylesheetHref(path)} связывает нас с таблицей стилей (которую мы снова создадим позже)
  • ${getNavigation(path)} генерирует навигационный блок
  • ${html} — это HTML-код текущей страницы.

Наш последний раздел Step 3 должен выглядеть так:

Все потихоньку сходится. Последнее, что нам нужно сделать, это сгенерировать наши файлы index.html и связанные с ними папки.

Шаг 4. Создайте страницы в файлах .html с соответствующими путями

Этот шаг относительно прост, мы знаем, что каждый page в нашей переменной pages содержит весь наш HTML-контент, а также файл path. Например, путь может выглядеть как / или /about и т. д. На основе этих значений давайте определим пути вывода, которые будут фактическими путями файловой системы, куда записываются наши файлы index.html:

Помните, как мы взяли buildPath из аргументов CLI ранее в скрипте? Здесь мы можем сшить его вместе с путем к странице, чтобы получить наш выходной путь. Используя временный console.log(...), мы можем запустить скрипт и получим:

Давайте обновим Step 4, чтобы теперь эти файлы сохранялись в нашей файловой системе, чтобы убедиться, что мы включили правильный контент в файлы, мы используем наш помощник шаблона getHtmlByPage:

Здесь ensureFileSync гарантирует, что файл существует. Если указанный путь содержит несуществующие каталоги, эти каталоги создаются. После того, как мы гарантируем, что путь к файлу существует, Deno.writeFileSync использует getHtmlByPage с нашим выбранным объектом Страница и генерирует для него весь необходимый HTML-контент (в соответствии с нашими шаблонами).

Кроме того, поскольку мы сейчас записываем в нашу файловую систему, мы должны использовать флаг --allow-write, протестировать скрипт следующим образом:

Это должно создать каталог build со следующей структурой:

build
├── about
│   └── index.html
└── index.html

Шаг 5. Создайте дополнительные файлы активов

Наш последний шаг — создать дополнительные файлы активов и убедиться, что они также сохранены в соответствующем месте для нашей сборки. В этом примере у нас есть только два дополнительных ресурса: таблица стилей (styles.css) и значок (favicon.svg). Мы можем создать их так:

Теперь, когда мы запускаем наш скрипт, наша структура каталогов

build
├── about
│   └── index.html
├── favicon.svg
├── index.html
└── styles.css

Предварительный просмотр нашего сайта

Сначала давайте cd build зайдем в нашу папку сборки. У нас есть много вариантов запуска локального веб-сервера, выберите тот из следующих, который вам проще всего:

  • Питон 2: python -m SimpleHTTPServer 8001
  • Питон 3: python3 -m http.server 8001
  • PHP: php -S localhost:8001
  • Browsersync (пакет узла): npm install -g browser-sync; browser-sync --port 8001

Затем, если мы откроем localhost:8001 в нашем локальном браузере, у нас должно быть:

Вот и все, если вы хотите попробовать более сложный пример со стилями, посмотрите этот example-site.md, доступный по адресу github.com/nafeu/deno-md-site.

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