← на главную
ПостыТеги

Here we static again...


В самом начале я планировал реализовать этот блог как SPA, но использовать тяжелую артилерию в виде React, Angular, etc. не хотелось, были мысли вообще использовать только Vanilla JS и не тянуть никаких зависимостей. Но по мере того как код и мои требования к нему стали разрастаться, я понял, что изначально загнал себя в ненужные рамки. Первое, о чем я задумался - без сборщика точно не обойтись - минимизация кода, стилей и разметки уменьшает бандл, ускоряет загрузку и как итог улучшает UX. Ставим Webpack. Раз раз уж есть Webpack, то почему бы не писать на ECMAScript? Ставим babel. А стили? Кто сейчас пишет чистый css? Правильно, используем SASS.

Далее после написания роутера, разметки страниц микроформатами, я задумался о том, что хорошо бы сделать пререндер - поисковые боты, да и парсеры микроразметки без этого работать не будут. Ставим prerender-spa-plugin.


Ну вот всё, работает, пререндер отрабатывает, страницы парсятся. Настало время делать, то для чего блог и предназначен - транслирование мыслей в читамый формат. Снова проблема. Нет, не с транслированием, а с публикацией. Иногда идея возникает на ходу, под рукой только смартфон, а писать HTML на нем все еще одно мучение. Тут тоже есть выход - использовать Markdown. Снова накручиваем webpack, теперь мои посты хранятся в виде md, на основе которых showdown генерирует HTML. Далее формируется запись вида:

{
  "type": "post",
  "properties": {
    "name": "IndieWeb",
    "published": "2019-12-03T23:12:42.958Z",
    "content": "<h1>Hello world!</h1>"
  }
}
Формат в котором я хранил посты

Такой json подгружался при переходе на страницу и, с помощью string template из ecmascript, рендерился в браузере.

Попутно я написал скрипт автогенерации фид ленты, который брал список файлов, лежащих в директории с контентом, и составлял JSON вида:

{
  "feedArray": [
    {
      "type": "feed",
      "navigate": "feed/3",
      "name": "SEO-friendly SPA 🤔",
      "published": "2019-12-09T23:41:42.958Z"
    },
    {
      "type": "feed",
      "navigate": "feed/2",
      "name": "Vanilla JS роутер за 40 <del>секунд</del> строк",
      "published": "2019-12-05T02:41:42.958Z"
    },
    {
      "type": "feed",
      "navigate": "feed/1",
      "name": "IndieWeb",
      "published": "2019-12-03T23:12:42.958Z"
    },
    {
      "type": "feed",
      "navigate": "feed/0",
      "name": "The very beginning...",
      "published": "2019-12-01T19:01:42.958Z"
    }
  ]
}
Формат feed-ленты

И тут я задумался, что, если, предположим, потребуется пагинация (точно потребуется), теги или более продвинутая навигация? Мой подход потребует слишком много времени на реализацию. Решил пока оставить свои мысли как у Тима Маринина вышел пост об 11ty. Тут я понял - это то, что мне нужно.

11ty или Eleventy - генератор статических сайтов, написан на Javascript Заком Летермэном (@zachleat).

The possum is Eleventy’s mascot
А еще у них крутой маскот!
Самое прекрасное в 11ty - он не заставляет тебя выбирать, что использовать в качестве шаблонов. Более того, он не ограничивает в выборе только одного движка шаблонов. Вот часть списка поддерживаемых шаблонов (я остановился на nunjucks):
  • HTML *.html
  • Markdown *.md
  • Nunjucks *.njk
  • Mustache *.mustache
  • Pug *.pug
Полный список поддерживаемых движков можно найти тут
11ty расширяем, гибок, и потому как не является фреймворком, он не тащит с собой огромный бандл javascript'a. Вы можете вообще не использовать js, концентрируясь на контенте и шаблонах.

Посмотреть как это все работает изнутри можно на странице проекта в github (на момент написания заметки там уже почти 4к ⭐️).

Here we go again
Ну вы поняли...

На переезд я потратил около пару-тройку вечеров, попутно делая рефакторинг верстки, улучшая разметку микроформатов. Также пришлось написать webpack конфиг, так как готового и устраиваюшего меня я не нашел. Оставлю его здесь, в надежде, что он пригодится не только мне.

const path = require('path');

module.exports = {
  entry: {
    app: path.join(__dirname, '../src/_includes/javascript/app.js')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader'
        }
      }
    ]
  }
};
webpack.config.js
const path = require('path');
const merge = require('webpack-merge');
const webpack = require('webpack');
const CommonConfig = require('./webpack.config');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const RemoveServiceWorkerPlugin = require('webpack-remove-serviceworker-plugin');
const PostcssLoader = require('./postcss.config');

module.exports = merge(CommonConfig, {
  mode: 'development',
  output: {
    filename: 'app.js',
    path: path.resolve('_site/assets'),
    publicPath: '/_site/assets/',
    hotUpdateChunkFilename: '_hot/hot-update.js',
    hotUpdateMainFilename: '_hot/hot-update.json'
  },
  devtool: 'inline-source-map',
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new MiniCssExtractPlugin(),
    new RemoveServiceWorkerPlugin()
  ],
  module: {
    rules: [
      {
        test: /\.js$/,
        enforce: 'pre',
        exclude: /node_modules/
      },
      {
        test: /\.(css|scss)$/,
        use: [
          'css-hot-loader',
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: { sourceMap: true, importLoaders: 1 }
          },
          PostcssLoader,
          'sass-loader'
        ].filter(Boolean)
      }
    ]
  }
});

webpack.dev.js
const Merge = require('webpack-merge');
const CommonConfig = require('./webpack.config');
const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const PostcssLoader = require('./postcss.config');

module.exports = Merge(CommonConfig, {
  mode: 'production',
  output: {
    filename: 'app.js',
    path: path.resolve('_site/assets'),
    publicPath: '/_site/assets/'
  },
  plugins: [
    new webpack.LoaderOptionsPlugin({
      minimize: true,
      debug: false
    }),
    new MiniCssExtractPlugin({ filename: 'app.css' })
  ],
  module: {
    rules: [
      {
        test: /\.js$/,
        enforce: 'pre',
        exclude: /node_modules/
      },
      {
        test: /\.(css|scss)$/,
        use: [
          'css-hot-loader',
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              sourceMap: true,
              importLoaders: 1
            }
          },
          PostcssLoader,
          'sass-loader'
        ].filter(Boolean)
      }
    ]
  }
});
webpack.prod.js
const postcssImport = require('postcss-import');
const postcssPresetEnv = require('postcss-preset-env');
const postcssReporter = require('postcss-reporter');
const postcssMqPacker = require('css-mqpacker');
const postcssNano = require('cssnano');

module.exports = {
  loader: 'postcss-loader',
  options: {
    plugins: [
      postcssImport(),
      postcssPresetEnv(),
      postcssMqPacker(),
      postcssReporter(),
      postcssNano({
        preset: [
          'default', { discardComments: { removeAll: true } }
        ]
      })
    ]
  }
};
postcss.config.js

Структура директорий на данный момент следующая

╭─smurygin@macbook ~/Projects/smurygin.com ‹master*›
╰─$ tree -v -C --charset utf-8

├── README.md
├── configs
│   ├── eleventy.config.js
│   ├── postcss.config.js
│   ├── webpack.config.js
│   ├── webpack.dev.js
│   └── webpack.prod.js
├── package-lock.json
├── package.json
├── scripts
│   ├── clean.js
│   └── post.js
└── src
    ├── _11ty
    │   ├── getFeedList.js
    │   └── getTagList.js
    ├── _data
    │   └── metadata.json
    ├── _includes
    │   ├── components
    │   │   ├── card.njk
    │   │   ├── footer.njk
    │   │   ├── header.njk
    │   │   ├── nextprev.njk
    │   │   ├── pagination.njk
    │   │   ├── posts-home.njk
    │   │   ├── posts.njk
    │   │   └── tags.njk
    │   ├── javascript
    │   │   ├── app.js
    │   │   ├── http-client.js
    │   │   ├── lazy-load-images.js
    │   │   ├── render-mentions.js
    │   │   ├── external-links.js
    │   │   └── theme-picker.js
    │   ├── layouts
    │   │   ├── comment.njk
    │   │   ├── default.njk
    │   │   ├── home.njk
    │   │   ├── like.njk
    │   │   └── post.njk
    │   └── stylesheets
    │       ├── __mixins.scss
    │       ├── _base.scss
    │       ├── _card.scss
    │       ├── _feed.scss
    │       ├── _footer.scss
    │       ├── _header.scss
    │       ├── _highlight-github.scss
    │       ├── _homepage.scss
    │       ├── _mentions.scss
    │       ├── _navigation.scss
    │       ├── _pagination.scss
    │       ├── _post.scss
    │       ├── _reset.scss
    │       ├── _tags.scss
    │       ├── _theme.scss
    │       └── app.scss
    ├── _redirects
    ├── assets
    │   ├── apple-touch-icon.png
    │   ├── cache-polyfill.js
    │   ├── favicon.ico
    │   ├── highlight.js
    │   └── images
    │       ├── 2019-12-18-regexp
    │       │   ├── 1.jpg
    │       │   └── 1.min.jpg
    │       ├── 2019-12-18-the-very-beginning
    │       │   └── 1.svg
    │       ├── 2019-12-21-here-we-static-again
    │       │   ├── 1.min.png
    │       │   ├── 1.png
    │       │   ├── 2.jpg
    │       │   └── 2.min.jpg
    │       ├── android-chrome-192x192.png
    │       ├── android-chrome-512x512.png
    │       ├── avatar.jpg
    │       └── meotyda-logo.svg
    ├── comments
    │   └── comment.json
    ├── like
    │   ├── 0.md
    │   └── like.json
    ├── manifest.json
    ├── pages
    │   ├── 404.md
    │   ├── archive.njk
    │   ├── feed.njk
    │   ├── home.njk
    │   ├── humans.txt.njk
    │   ├── offline.njk
    │   ├── robots.txt.njk
    │   ├── sitemap.xml.njk
    │   ├── sw.js.njk
    │   ├── tags-list.njk
    │   └── tags.njk
    └── posts
        ├── 2019-12-18-indieweb.md
        ├── 2019-12-18-regexp.md
        ├── 2019-12-18-the-very-beginning.md
        ├── 2019-12-18-vanilla-js-router-in-40-lines.md
        ├── 2019-12-21-here-we-static-again.md
        └── posts.json

Согласно IndieWeb wiki, я разделяю контент на категории, в зависимости от его смысловой нагрузки и содержания. В данный момент реализованы отдельные сущности для постов (article), ответов/комментариев (reply) и лайков (like).


Далее у меня в планах расширить количество типов публикаций, добавить публикации типа photo, bookmark, game play.

Пока никто не ответил 🥺. Если у тебя есть ответ — пришли мне вебменшен!