# Сергей Глазов — full-stack веб-разработчик Nuxt, Laravel - Полный экспорт контента > Full-stack веб-разработчик Nuxt, Laravel ## Сайт https://sglazov.ru ## О сайте Этот файл содержит полный контент всех опубликованных заметок блога. Это полезно для AI-моделей и LLM, которые хотят получить полную базу знаний. Для карты сайта без полного контента см.: https://sglazov.ru/llms.txt --- # Полный контент ## Скрипт для вёрстки статей из Google Docs в чистый HTML c картинками для WordPress **URL**: https://sglazov.ru/notes/google-docs-to-html/ **Дата**: 2025-08-02T00:00:00.000Z **Теги**: Google Docs, Apps Script, WordPress, HTML, JS **Описание**: В Google Docs экспорт в HTML через меню «Файл» → «Сохранить как» → «Веб-страница (HTML, ZIP-архив)» — вообще не работает. Точнее работает, но через жопу.
Задача:
На Apps Script написать скрипт для конвертации документа из Google Docs в чистый HTML с правильным именованием и хронологическим порядком картинок

В Google Docs экспорт в HTML через меню «Файл» → «Сохранить как» → «Веб-страница (HTML, ZIP-архив)» — вообще не работает. Точнее работает, но через жопу. Результат импорта практически использовать нельзя. На выходе будет архив, в котором кривой html-файл с десятичным кодом в unicode для всего текста, куча мусорных тегов <span> с инлайн-атрибутами и рандомными CSS-классами, странные общие глобальные стили, и, что самое удивительное, хронологический порядок картинок и их именование будет совершенно случайным.

В июле 2024 в Google Docs добавили экспорт в Markdown и в md-формате хронология и имена картинок — правильные! При этом картинки собираются в этом же файле в base64, что использовать сложно и рождает череду промежуточных этапов при простой казалось бы задаче «сверстать пост в блог на WordPress».

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

Все доступные найденные инструменты — не работали. Надежда была только на прямую конвертацию средствами Apps Script. Вариант через проект в Apps Script где-то внутри уже Google Drive меня не устраивал: создание, его настройка, раздача прав доступа под каждый документ на исполнение и сложности с правами на расшаренные документы напугали объёмом и передачей знаний по проекту. Инструкцию из двух десятков шагов не каждый пройдёт до финала.

Найден был более простой вариант: запустить отдельный сценарий в Apps Script на одном конкретном документе. Этот сценарий никуда ничего не передаёт и работает только с аккаунтом активного юзера. Минусы сценария: каждый запуск нужно проходить через череду модалок разрешений самого гугла.

Чего хотелось от скрипта и почему

Чистая и семантичная вёрстка

Если документ в самом Google Docs разметили правильно (что редакторы делают чаще всего), то его экспорт в HTML будет чистым по структуре: заголовки всех уровней будут заголовками, абзацы — абзацами, нумерованные и маркированные списки — списками. Если ссылка внешняя — взять и сразу добавить ей атрибут rel="nofollow". Ну и поисковые роботы всё это уважают, что для контент-проектов важно.

Работа с графикой и работа с медиафайлами в WordPress

Как процесс вёрстки поста для WordPress устроен в целом упрощённо без деталей про «а как» для каждого этапа: отдельно верстается разметка, отдельно загружается пачка файлов для этого поста и используется в разметке.

И вот эти промежуточные шаги, особенно если в посте картинок так с десятка два, чтобы каждую правильно именовать, например, добавив префикс после имени, которое повторяет slug поста, занимали кучу фокуса и времени. И это всё с учётом в разметке того, что WordPress будет хранить файлы по пути типа /wp-content/uploads/2025/08/

Ожидаемый результат: при запуске скрипта задаётся базовый набор параметров среди которых slug поста, например, ya-metrika. Алгоритм перебора картинок возьмёт его, добавит суффикс с порядковым номером типа ya-metrika_24 и сложит все картинки в архив.

Из архива все картинки загружаются в медиа библиотеку при создании нового поста в WordPress и все пути правильно работают.

Результат работы в едином артефакте

На выходе — один zip-архив с файлом разметки и всеми картинками из документа с правильным именованием. И поскольку мы в пределах Google Docs и Apps Script, то результат выполнения можно отправить на почту, с которой и ведётся работа с документом.

Автоматическое оглавление документа

Скорее бонусный, но приятный пункт. В самих Google Docs в панели слева есть структура документа. Почему бы её не вытащить также в кастомную разметку? Вместе с этой задачей добавляется новая задача: всем заголовкам надо добавить id, а чтобы его добавить нужна какая-то штука для транслитерации в slug-формат, чтобы из текста заголовка «Как создать и настроить счетчик посещений» получился id kak-sozdat-i-nastroit-schetchik-poseshchenij. Приставкой можно ещё id поста добавить, например, для уникальности.

Кому всё это надо

Контент-менеджерам, редакциям, верстальщикам — упростить рутину, автоматизировать самое нудное, ускорить процесс вёрстки и выпуска контента.

Как это работает

  1. Открыть документ в Google Docs;
  2. В меню «Расширения» → выбрать пункт «Apps Script»:
  3. В открывшийся редактор скопировать всё содержимое скрипта и настроить параметры;
  4. Нажать на иконку сохранения и после → «Выполнить»:
  5. Пройти все этапы подтверждения;
  6. Получить письмо на почту с ZIP-архивом.

Смотрите документацию с пошаговой инструкций прохождения этапа подтверждения.

Реальные результаты

Этот скрипт — настоящая находка для контент-команд, редакторов и верстальщиков! Он значительно ускоряет процесс подготовки статей, автоматизируя рутинные задачи и минимизируя ошибки при переносе текста из Google Docs в HTML.

Этот инструмент серьезно упрощает жизнь контент-специалистам. Если раньше верстка статьи занимала 20-30 минут, то теперь — буквально пару кликов. Рекомендую всем, кто работает с большими объемами текстов и хочет оптимизировать процесс публикации.

Фичи и заморочки

Купить Apps Script для вёрстки статей из Google Docs в WordPress

2 500 ₽
Куда отправить скрипт, инструкцию и чек

Нажимая на кнопку «Заплатить 2 500 ₽», вы соглашаетесь , , а ещё с тем, что вы великолепны.

--- ## Скрипт для проброса UTM-меток на внутренние и внешние ссылки **URL**: https://sglazov.ru/notes/utm-forward/ **Дата**: 2025-04-05T00:00:00.000Z **Теги**: UTM, JS **Описание**: Прокидываем UTM метки со страницы на страницу в рамках домена
Что происходит:
Написал клёвый скрипт для работы с UTM и простой задачей прокинуть полученные метки из источника перехода (реклама, например, или поисковая выдача) на все ссылки на странице.

utm-forward.js — это простой и эффективный скрипт на ванильном JS без зависимостей для автоматического проброса UTM-меток на внутренние и внешние ссылки вашего сайта. Для любых CMS, конструкторов сайтов или систем обучения (LMS).

Проблема и решение

Проблема: Когда пользователь переходит по ссылке с UTM-метками на ваш сайт, а затем кликает по внутренней ссылке, все UTM-параметры теряются. Это приводит к неточной аналитике и невозможности понять, какие маркетинговые каналы действительно приносят конверсии.

Решение: utm-forward.js автоматически добавляет UTM-метки ко всем внутренним и внешним ссылкам на вашем сайте, сохраняя информацию об источнике трафика на протяжении всего пути пользователя.

Кому подходит utm-forward.js?

Всем: и без программистов и программирования всё можно настроить отслеживание UTM: нужно просто подключить скрипт на сайте или в конструкторе.

Как это работает?

  1. Пользователь переходит на ваш сайт по ссылке с UTM-метками
  2. utm-forward.js сохраняет эти метки в localStorage браузера
  3. Когда пользователь кликает по любой ссылке на сайте, скрипт автоматически добавляет сохраненные UTM-метки
  4. Аналитика показывает полный путь пользователя от первого клика до конверсии

Преимущества

Примеры использования

Реальные результаты

Скрипт решил проблему, которая отняла кучу времени. Примеры кода найденные на просторах интернета не работали. Реализовать решение через httacess не получилось. Скрипт легок в настройке и установке.

Фичи

Основные возможности

Заморочки

Купить JS скрипт проброса UTM-меток

990 ₽
Куда отправить скрипт, инструкцию и чек

Нажимая на кнопку «Заплатить 990 ₽», вы соглашаетесь , , а ещё с тем, что вы великолепны.

--- ## Стилизуй свой RSS **URL**: https://sglazov.ru/notes/rss-styling/ **Дата**: 2025-03-02T00:00:00.000Z **Теги**: RSS, XML **Описание**: Как стилизовать RSS-ленту: перевод статьи «Style your RSS feed», Darek Kay

RSS не умер. Это не мейнстрим, но это все еще процветающий протокол, особенно среди технарей. Многие не знают, что такое RSS-лента и как им пользоваться.

Большинство браузеров отображают RSS как сырые XML-файлы, что не помогает пользователям понять, в чём суть:

Пример необработанных XML-файлов RSS-лент

В этой статье я расскажу, как оформлять RSS-ленты и одновременно обучать читателей.

XSL(T) приходит на помощь

Вот как выглядит RSS-лента этого блога:

Пример необработанных XML-файлов RSS-лент

Чтобы стилизовать XML-файл в браузере, необходимо предоставить информацию о стилях. Это можно сделать, добавив инструкцию обработки xml-stylesheet в RSS-ленту:

<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:media="http://search.yahoo.com/mrss/">
  ...
</feed>

Атрибут href указывает URL-адрес допустимого XSL-файла. Вы можете ознакомиться с моим примером. Вот суть:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="3.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:atom="http://www.w3.org/2005/Atom">
  <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:template match="/">
  <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
    <head>
      <title>
        RSS Feed | <xsl:value-of select="/atom:feed/atom:title"/>
      </title>
      <link rel="stylesheet" href="/assets/styles.css"/>
    </head>
    <body>
      <p>
        This is an RSS feed. Visit
        <a href="https://aboutfeeds.com">About Feeds</a>
        to learn more and get started. It’s free.
      </p>
      <h1>Recent blog posts</h1>
      <xsl:for-each select="/atom:feed/atom:entry">
        <a>
          <xsl:attribute name="href">
            <xsl:value-of select="atom:link/@href"/>
          </xsl:attribute>
          <xsl:value-of select="atom:title"/>
        </a>
        <xsl:value-of select="atom:summary"/>
        Last updated:
        <xsl:value-of select="substring(atom:updated, 0, 11)" />
      </xsl:for-each>
    </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

Этот код вдохновлен pretty-feed-v3, но я адаптировал его под спецификацию Atom.

XSL-файл преобразует XML-ленту в валидный HTML-документ, который любой браузер может отобразить. Вы можете использовать любую информацию из XML-файла и поместить её в новую структуру разметки. Вы даже можете отобразить контент, который не является частью XML-ленты. Идеальный пример использования — добавить заметку о том, что такое RSS-ленты и как их использовать (как показано в примере выше).

Вы можете определить встроенные CSS-стили с помощью обычного элемента <style>, но также можно импортировать внешние CSS-файлы. Я импортировал основной CSS-файл своего блога, поэтому мне не пришлось писать новый код, чтобы RSS-лента соответствовала дизайну моего блога.

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

<!-- Full timestamp -->
<xsl:value-of select="atom:updated" />

<!-- Date only -->
<xsl:value-of select="substring(atom:updated, 0, 11)" />

Поддержка браузером XSL-трансформаций отличная; неподдерживаемые браузеры вернутся к поведению по умолчанию (прогрессивное улучшение).

Примеры

Вот несколько стилизованных RSS-лент, которые вы можете посмотреть для вдохновения:

XSL-файлы публичны, так что можно посмотреть, как эти всё реализовано.

Sitemap

XSL-файлы можно применять к любому XML-файлу. Ещё один пример использования стилизованных XML-файлов — это карта сайта. Хотя карты сайта предназначены для обработки роботами (например, краулерами), их можно стилизовать с минимальными усилиями. Посмотрите мою карту сайта и соответствующий XSL-файл в качестве примера. Вот суть:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9">
  <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:template match="/">
    <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
      <body>
        <h1>Sitemap</h1>
        <xsl:for-each select="/sitemap:urlset/sitemap:url">
          <a>
            <xsl:attribute name="href">
              <xsl:value-of select="sitemap:loc"/>
            </xsl:attribute>
            <xsl:value-of select="sitemap:loc"/>
          </a>
          Last updated:
          <xsl:value-of select="substring(sitemap:lastmod, 0, 11)" />
        </xsl:for-each>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

Предостережение

Что произойдёт, если пользователь захочет сохранить стилизованный XML-файл? Интересно, что ответ зависит от браузера. Chrome и Safari сохранят оригинальный XML-файл, а Firefox сохранит стилизованную HTML-страницу. Большинство RSS-ридеров ожидают URL-адрес ленты, поэтому это поведение не имеет значения. Однако я бы был осторожен с XML-файлами, которые предназначены для загрузки, например, OPML-файлами. Спасибо Robb Knight за это наблюдение.

Заключение

Я люблю RSS, и, согласно моей статистике, многие мои читатели тоже его любят. С учётом всех закрытых экосистем социальных сетей, я надеюсь, что RSS станет более популярным. Создавая собственный XSL-файл, вы можете сделать свою RSS-ленту привлекательной и добавить информацию о том, что такое RSS на самом деле.


Про этот сайт и что сделал я

Мне понравилась идея. Я захотел сделать не отдельную «красивую страницу» самого RSS, а как бы «вписать» RSS-страницу в общее оформление сайта — ну вот чтобы и шапка и подвал и вообще весь контекст.

Получилось вот так: https://sglazov.ru/notes/feed/

К сожалению, даты локализовать я не смог. И меня несколько смущает вот этот вид «2024-04-18», но это всё же лучше, чем просто xml-файл или отсутствие даты.

Мой файл rss-style.xsl повторяет разметку файла layout.njk всей темы этого сайта.

Единственный момент — понадобилось написать простую функцию, чтобы HTML-разметка там, где надо преобразовалась в xHTML — где-то закрывать теги и всё такое:

export default function html2xhtml(content) {
  return content.toString()
    .replace(/(<(img|source)[^>]*?) *\/?>/g, '$1 />')
    .replace('itemscope', 'itemscope=""')
    .replace('&copy;', '&#169;')
    .replace('&nbsp;', '&#160;')
}

В неё целиком я оборачиваю какие-то include-конструкции. Карта сайта сделана также.


Ещё всякие ссылки и посты про XSL(T)

--- ## История создания двух ссылок для добавления мероприятия в календарь **URL**: https://sglazov.ru/notes/add-to-calendar/ **Дата**: 2024-04-18T00:00:00.000Z **Теги**: WordPress, PHP, iCal, Google Calendar

Ну казалось бы: две маленькие простые странные ссылки «Добавить в календарь» среди кучи интерактивных элементов на странице. При этом я получил ответы на вопросы типа «почему формат календарей устроен именно так?».

С чего всё началось

На самом-то деле, конечно, с глупости. На каком-то «сером» сайте-помойке, типа «скачать субтитры из видео ютуба», где тонна всякой хреновой рекламы и друных схем обмана наивных посетителей. Попал я на такой сайт с айфона.

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

Из любопытства и исследовательского взгляда — согласился. И эта херня подписала меня на ical-feed какого-то «новостного агентства», засрав мне весь календарь, блин. Создав отдельный типа событий в календаре и при этом подтягивая данные из своей ленты из внешнего источника. Забавная же хрень. Я тоже такую захотел.

Понравилось. Как идея того, что можно сделать подписку на ленту мероприятий. Как раз то, что нужно мне в проекте.

Контекст истории в проекте начинается там, где есть вот такой дизайн статус-шапки с информацией о событии:

Дизайн статус-шапки с информацией о мероприятии
Даты проведения и длительность, место проведения и/или формат (очное или онлайн) ну и кнопка-действие. В первом экране, отвечает сразу на все базовые вопросы

Аккуратно свёрстано и работает. Во время вёрстки придумал, что для очного формата элемент с адресом будет кликабельным и открывать Яндекс.Карты с прокинутым в поиск, адресом. Урлы Яндекс.Карты умеют получать адрес и искать просто из ссылки с параметром ?text=:

<!-- 
    Параметр `z=16` — это масштаб карты (zoom), 
    z=20, например покажет отдельное здание 
-->

<a href="https://maps.yandex.ru/?text=Санкт-Петербург, Тверская 10&z=16" 
   target="_blank" 
   title="Подскажем адрес на Яндекс.Картах">
  Санкт-Петербург, ...
</a>

Включил это для тач-устройств в первую очередь: опыт дружелюбнее. В большинстве случаев после тапа, сразу откроется приложение Яндекс.Карт на телефоне. Если приложения на устройстве нет, то откроется браузер с сайтом карт сразу с адресом.

Захотелось также сделать что-то приятно-незаметное (пока не кликнешь, не знаешь, что о тебе позаботились и сценарий продумали уже) и для элемента с датой. Самый очевидный вариант — добавлялка напоминания в календарь.

А как у других

Afisha.ru, russpass.ru, KudaGo и Яндекс Афиша

На самых популярных порталах добавление в календарь не встречается в интерфейсе вовсе. Скорее там везде один паттерн: призыв к добавлению будет после действия — записи или покупки.

Также, как и с билетами на самолёт, например. В письме с маршрутной квитанцией обязательно будут ссылки, типа «Добавить в Apple Wallet» и добавлялки в календари.

Так что актуальных примеров для вдохновения я там не нашёл. Поискал скрины и в своих письмах по истории покупок билетов, понятны стали самые популярные варианты календарей:

  1. Google Calendar,
  2. iCal (Apple),
  3. Outlook Calendar,
  4. Есть ещё частные случаи, где компания активно пушит свои продукты: Календарь Mail.ru, Яндекс Календарь и проч.

TimePad

Подглядел в TimePad. Если смотреть их стандартную «Афишу» — клик по блоку с датой скроллит к этажу выбора билетов. А вот идея с оформлением даты, как календарика — понравилась. Кнопок-действий для добавления в календарь на странице отдельного события нет (апрель, 2024). Но есть в письмах после регистрации или оплаты:

123
Ссылки в письме TimePad. С отдельной явной сноской о необходимости авторизации в Google-календаре.

А если смотреть на страницу отдельного организатора мероприятий, то в блоке даты есть ссылка «Добавить в календарь» с выпадайкой и выбором вариантов. Любопытно, что такого нет на других отдельных страницах.


Отдельная страница организатора TimePad и контрол на стрнице в сайтбаре с выпадайкой

Как хочу сделать я

Технические вводные у задачи: WordPress, PHP, свой шаблон и полный контроль над кодом, часть данных из админки доставать надо через поля Advanced Custom Fields (ACF).

Google Calendar

У продукта Google Calendar квадратная по пропорциям иконка и внутри текстом указывается актуальный день месяца. В favicon calendar.google.com тоже подставляется актуальный день, например, такое:

<link 
  rel="icon" 
  id="favicon" 
  type="image/x-icon" 
  href="https://calendar.google.com/googlecalendar/images/favicons_2020q4/calendar_27.ico"
>

Где «27» в имени файла calendar_27.ico — день месяца. Ещё очень интересна структура УРЛа. Что ж там такое ещё есть в favicons_2020q4.

Как добавить событие Google Calendar в календарь

Чтобы добавить какое-то событие в календарь — была (!) ссылка http://www.google.com/calendar/render? с обширным списком параметров для события. Почему-то из официальной документации хоть какая-то полезная информация была удалена и все всегда во всех вопросах ссылаются на вебархивную ссылку из 2012-го (!): Share your events with an individual, a group, or the whole world.

Интернет очень странная штука, конечно. На этой же архивной странице есть генератор ссылок. Работает и спустя 12 лет.

А современные официальные ссылки документации и раздела помощи гугла показывают что-то бесполезное.

Параметры описаны на странице Instructions for making Google Calendar event reminder buttons, тоже вебархивная ссылка. Там параметры обозначают термином «CGI parameters».

Базовый набор:

# тут всегда только TEMPLATE, так сказано в доке 
action=TEMPLATE
# название события
text="Публикация поста и ссылках на календари"
# местоположение
location="https://sglazov.ru"
# описание, можно простые HTML-теги
details="Я публикую пост, это торжественно!"
# дата, в Ymd\THis + `Z` после времени
dates=20240517T190000Z/20240517T200000Z

И всё это можно превратить в набор параметров для ссылки, правда ссылка-пример из этой самой [документации] — не работает, почему-то в параметрах запроса есть лишние символы точки с запятой: text;=, &details;= ну и т.п. А конструктор кнопок — работает, но выбор года там только до 2020.

Ну и актуальный УРЛ для добавления мероприятий и формирования полной ссылки — https://calendar.google.com/calendar/r/eventedit, параметры передаются гетом.

Беру и делаю

SVG-иконку для фона взял случайную, сам в фигме удалил фон. При вёрстке поставил блок с текстом по центру размеров иконки и подогнал визуально размер шрифта, чтобы две цифры даты не поломали габариты блока. Ещё какое-то время понадобилось уделить тому, чтобы выяснить, параметр &ctz=Europe/Moscow для часового пояса обязателен.

<?php
// переменные $dateStart, $dateEnd, $speakerName, $title, $formatOffline, $city объявлены выше, там ничего интересного

// форматирует даты
$calendarDataStart = date('Ymd\THis', strtotime( $dateStart )); 
$calendarDataEnd = date('Ymd\THis', strtotime( $dateEnd ));

$calendarDetails = "Преподаватель: {$speakerName}. Описание и программа: https://site.tld/events/" . get_the_id() . "/";

$calendarUrl  = 'https://calendar.google.com/calendar/r/eventedit';
$calendarUrl .= '?text=' . htmlspecialchars( $title ); // название мероприятия
$calendarUrl .= '&ctz=Europe/Moscow'; // часовой пояс обязательный
$calendarUrl .= '&dates=' . $calendarDataStart . '/' . $calendarDataEnd;
$calendarUrl .= '&details=' . htmlspecialchars($calendarDetails);

// если событие в оффлайне и в админке указан город 
if ( $formatOffline && $city ) 
{
  $calendarAddress = $city;
  
  // добавляем параметр `location` с адресом к урлу календаря
  $calendarUrl .= '&location=' . htmlspecialchars($calendarAddress);
}
?>

<li class="dropdown-menu__item">
  <a class="add-to-calendar-link"
     href="<?= $calendarUrl; ?>"
     title="Календарь откроется в новом окне"
     target="_blank">
    <div class="add-to-calendar-link__symbol">
      <svg role="img"
           width="22"
           height="22">
        <use xlink:href="#i_google-calendar-blank"></use>
      </svg><!-- /.w-icon__symbol -->
      <div class="add-to-calendar-link__day">
        <?= date('j', strtotime( $dateStart ) ); ?><!-- день месяца в стилизованной иконке календаря -->
      </div><!-- /.add-to-calendar-link__day -->
    </div><!-- /.add-to-calendar-link__symbol -->
    <div class="add-to-calendar-link__text">
      Google Календарь
    </div><!-- /.add-to-calendar-link__text -->
  </a><!-- /.add-to-calendar-link -->
</li><!-- /.dropdown-menu__item -->

И всё прекрасно заработало.

Ссылка для добавления в iCal — календарь Apple iCloud

Стоит упомянуть, что «i» в названии формата iCalendar, созданного в 1998/2009, не имеет отношение к манере Apple называть свои продукты. В случае с iCalendar — это Internet Calendaring and Scheduling Core Object Specification (iCalendar).

Формат представляет собой набор параметров вида ключ-значение. В этой реализации все параметры орут и написаны прописными. В 199* так было модно? Файл на десятка два событий, визуально превращается в мешанину из капса, цифр и неожиданных двоеточий.

Пример структуры для отдельного события:

BEGIN:VCALENDAR
PRODID:-//Timepad Ltd.//NONSGML Timepad//RU
VERSION:2.0
METHOD:PUBLISH

BEGIN:VEVENT
UID:3414132849538
DTSTART:20240517T190000
DTEND:20240517T200000
DTSTAMP:20240415T000102
SUMMARY:SPB Frontend Митап #36
DESCRIPTION:[...] традиционная встреча веб-разработчиков в Санкт-Петербурге [...]
LOCATION:Санкт-Петербург, Цветочная улица, дом 19
END:VEVENT

END:VCALENDAR

Формат состоит из компонент-секций BEGIN:-END:. Версия является обязательной и всегда указывается вторая. Сомневаюсь, что она будет когда-то меняться, но поскольку есть совсем старинная первая, то указывается вторая.

Вместо TITLE использовали вариант SUMMARY, ну ок. METHOD:PUBLISH — это про событие, которое явно случится.

Отдельных VEVENT внутри *.ics может быть произвольное количество, не только одно событие.

Как всё работает и как должно работать у меня, тезисно:

  1. Магией создаётся/генерируется налету файл id.ics, где id — уникальный ID мероприятия, ну можно префикс ещё добавить;
  2. К этому «файлу» можно обратиться по уникальной ссылке;
  3. В момент обращения — скачивается *.ics;
  4. Файл при открытии запускает календарь, например, на айфоне;
  5. Или пользователь его открывает сам после скачивания (нууу, такоооое, но именно так оно задумано и работает).

Ещё я хотел отдельную ленту всех мероприятий в формате iCal. Придумал, что можно сделать кастомную RSS-ленту в WordPress, которая может принимать на вход один ID параметром, типа ?event=<?= get_the_id(); ?> и формировать файл для отдельного мероприятия. Ну и без параметра урл вида /feed/ical/ будет отдавать всегда актуальный общий список мероприятий в формате iCalendar. Два в одном типа.

Кастомную RSS-ленту можно собрать через WordPress-хук add_feed(). Писал про этот хук на Хабре в 2019-м: Кастомная авторассылка в MailChimp из RSS-ленты.

Скачать файл сразу поможет и сам браузер, но лучше ему помочь и указать заголовок Content-Disposition: attachment; при ответе.

Создание RSS-ленты

Файл feed-ical.php:

// Добавляет новую ленту `/feed/ical`
add_action('init', 'iCalendarFeed');
function iCalendarFeed()
{
  add_feed('ical', 'iCalendarFeedTemplate');
}

// Шаблон ленты
function iCalendarFeedTemplate()
{
  get_template_part('feed', 'ical');
}

Подключить в файле функций темы functions.php:

// лента для Apple Календаря
require get_template_directory() . '/core/custom/feed-ical.php';

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

Кнопка-действие

<li class="dropdown-menu__item">
  <a class="add-to-calendar-link" 
     href="<?= get_feed_link('ical'); ?>?event=<?= get_the_id(); ?>"
     title="Файл события скачается"
     target="_blank"
     download="event-<?= get_the_id(); ?>.ics">
    <div class="add-to-calendar-link__symbol">
      <div class="apple-calendar">
        <div class="apple-calendar__week">
          <?= date_i18n('D', strtotime( $dateStart ) ); ?>
        </div><!-- /.apple-calendar__week -->
        <div class="apple-calendar__date">
          <?= date('j', strtotime( $dateStart ) ); ?>
        </div><!-- /.apple-calendar__date -->
      </div><!-- /.apple-calendar -->
    </div><!-- /.add-to-calendar-link__symbol -->
    <div class="add-to-calendar-link__text">
      iCal (Календарь Apple)
    </div><!-- /.add-to-google-calendar__text -->
  </a><!-- /.add-to-calendar-link -->
</li><!-- /.dropdown-menu__item -->

WordPress-функция date_i18n() позволяет из коробки работать с локализацией дат без магии. Ну и формат D выведет сокращённый двухбуквенный вариант дня недели. Как в иконке календаря на айфоне. А на макос иконка позывает месяц + день.

Шаблон iCal-ленты

<?php
// имя файла
$file = urlencode( "event-" . date('Y-m-d') . ".ics" );

// это — календарь, в UTF-8
header('Content-type: text/calendar; charset=utf-8');
// и файл надо скачать
header("Content-Description: File Transfer");
// имя этому файлу объявлено
header("Content-Disposition: attachment; filename={$file}");
// помогает всегда отдавать только свежее, игнорируя кэш
header("Expires: 0");


// если `event=ID`, то карточку собираем для него 
if ( isset($_GET['event']) && $_GET['event'] ) 
{
  $getEvent = $_GET['event'];
}

$args = array(
  'p' => $getEvent,        // ID мероприятия из запроса
  'post_type' => 'events', // мероприятия
  'posts_per_page' => -1,  // все
);
$getEvents = new WP_Query($args);

// в стандартном цикле WP перебираем всё, что нужно
if ( $getEvents->have_posts() ) : 
?>
BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//RU
NAME:БРЕНД
X-WR-CALDESC:Краткое описание календаря
TZID:Europe/Moscow
<?php
// в цикле собирает отдельный компонент `BEGIN:VEVENT-END:VEVENT`
while ( $getEvents->have_posts() ) :
  // ... тут из acf/wp достаются нужные поля
  ob_start(); ?>
  BEGIN:VEVENT
  SUMMARY:<?= $title; ?>
  UID:<?= uniqid(); ?>
  DTSTAMP:<?= $timestamp; ?>
  DTSTART:<?= $dateStart; ?>
  DTEND:<?= $dateEnd; ?>
  LOCATION:<?= escapeString($location); ?>
  DESCRIPTION:<?= escapeString($description); ?>
  END:VEVENT
<?php endwhile; ?>
END:VCALENDAR
<?php
  ob_end_clean();
  return ob_get_contents();
  endif;
?>

Стандарты и спецификации

В 1996-м году написали спеку vCalendar, где «v» — virtual. vCalendar — формат данных для представления и обмена информацией календаря. В той же спеке было и первое упоминание формата vCard — формата данных для карточки контакта. vCard частично работает до сих и формат 1996 поддерживается в рамках обратной совместимости.

Формат vCalendar позже превратился в iCalendar — vCalendar: The Basis for Cross-Platform Scheduling. В 1998 году появилась спека RFC 2445.

В 2009-м, через два года после выхода первого iPhone, первую спеку прокачали для применения уже в разросшемся вебе — RFC 5545, она и является актуальной. Ещё её правили в 2016-м и в целом, спека — живая.

В 2014 году сделали мапинг формата iCalendar в json-формат в спеке RFC 7265 — jCal. Перед ним в период популярности XML была спека RFC 6321 — xCal, но формат уж слишком многословный получился.

Ещё всякие ссылки про календари и спецификации

--- ## Электрические письма **URL**: https://sglazov.ru/notes/emails/ **Дата**: 2023-06-05T00:00:00.000Z **Теги**: email, CSS **Описание**: Что происходит с вёрсткой html-писем и немного истории

В 1971 году было написано и отправлено первое электронное письмо. Общедоступный WWW и то, во что он превратился создан в 1991-1996 годах. В Россию Рунет пришёл в апреле 1994 года.

Черновик HTML 3.0 опубликован в марте 1995 года. Ну и вот начинается то, куда мы пришли. Есть активное развитие каждой из ключевых технологий и веб пополз даже в другие стороны, захватывая железные части: можно получать доступ к микрофону или геопозиции, акселерометру, сканеру отпечатка пальцев для подтверждения операций и много чего ещё интересного. Тут всё хорошо, и пока не об этом хочется поговорить.

Первые письма с минимальными возможностями технологий тех лет (HTML 4.01 и CSS2) начали использовать только в конце 90-х. В 1997 году появляется почтовый веб-клиент с использованием актуальных веб-технологий. Ну, Hotmail вы точно должны знать — он был первым. Через год после запуска его купит Microsoft и вслед за этим запустит свой почтовый клиент — Outlook. Outlook жил в Windows. Это очень повлияло на его популярность. Но доставило много боли разработчикам. Всё как с Internet Explorer. Кстати. В июне 1999 Microsoft выпускает Outlook 2000 в котором письма рендерит движок их собственного браузера Internet Explorer 6 (эх). В январе 2007 Outlook перешёл с движка Internet Explorer на движок своего другого продукта — MS Word. Сразу после перехода много CSS-свойств в письмах перестали нормально поддерживаться. Совпадение?

Среди первой критики HTML-писем были такие пункты: «съедают» пропускную способность интернет-канала, долго рендерятся. Пост с этой критикой — целая история. И актуальные для тех лет проблемы.

Разработчикам оставалось только бороться и изобретать костыли для решения задач: функциональных, оформительных или кросс-платформенных. Вот самые интересные вариант: встраивание ленты твиттера прямо в письмо, работающая форма заявки, чекбоксы (!) и на их основе слайдеры и блоки-спойлеры.

Например, Gmail (и веб-клиент и приложение) обрежут письмо, превышающее 102 Кб. Отдельно сразу нужно заметить, что делает он это также тогда, когда несколько писем были отправлены с одной и той же темой. 102 Кб — это только про разметку и HTML-код.

Человечество уже столкнулось с каждой проблемой и нашло какое-то решение.


Всякие ссылки про письма

--- ## WordPress и домены-синонимы (алиасы) **URL**: https://sglazov.ru/notes/wordpress-domains/ **Дата**: 2022-07-12T00:00:00.000Z **Теги**: WordPress, PHP
Задача:
сделать так, чтобы один сайт на WordPress с одной БД открывался по множеству разных доменов и не сойти с ума.

Ситуация такая: есть сайт. Внутри — WordPress и свой мир. У сайта есть команда разработки. Для каждого разработчика придумано своё окружение и работы ведутся на dev-домене. dev0.*.tld, dev2.*.tld, ну и пока не закончатся разработчики или цифры.

Ещё есть обычные зеркала сайта — маркетинг придумал домен в другой доменной зоне, глобальные, региональные и какие угодно. И при этом всё это живёт на одной БД, ну, да. Не спрашивайте. Так бывает. Реальность достаточна сурова и объясняется контекстом (который до конца никому не понятен) или фразой типа: «так устроена жизнь».

И вот я совсем не понимаю, зачем в WordPress в базе данных в пугающе многих местах, захардкожен домен сайта. При варианте разработки «сделал локально → перенёс на удалённый сервер», человечество изобрело для этого костыля другие костыли или просто в лоб заменяют SQL-запросом все совпадения домена. Потрясающе.

Константы WP_SITEURL и WP_HOME

Решение (трюк?) с переопределением констант WP_SITEURL и WP_HOME работают не так, как нужно: они переопределяются, но не изменяются. И чаще всего вы получите при обращении к dev0.*.tld запрос ресурсов, статики и всего, с абсолютным урлом основного домена. На dev-окружениях такое и бесит и недопустимо.

// wp-config.php

define( 'WP_HOME',    'https://' . $_SERVER['HTTP_HOST'] );
define( 'WP_SITEURL', 'https://' . $_SERVER['HTTP_HOST'] );

«Умная переопределялка» для хоста через $_SERVER['HTTP_HOST'] будет вести себя точно также из-за абсолютных ссылок в БД.

/**
 * Вторая попытка определения хоста для дев- и прод-доменов
 */
switch ( strtolower( $_SERVER['HTTP_HOST'] ) )
{
  case 'dev.7site.tld':
    define('WP_SITEURL', 'https://dev.7site.tld');
    define('WP_HOME',    'https://dev.7site.tld');
    break;
    
  case '7site.tld':
    define('WP_SITEURL', 'https://7site.tld');
    define('WP_HOME',    'https://7site.tld');
    break;

  default:
    define('WP_SITEURL', 'https://site.tld');
    define('WP_HOME',    'https://site.tld');
    break;
}

Разумеется, при этом менять что-то в самой БД — нельзя. При этом хочется. А нельзя.

Кажется, логичным: заменять то, что нужно и там, где надо (дорогостоящая операция по производительности). Так уже работает, например, для медиафайлов на dev-доменах: медиа загружены на основной домен, менеджер при тестировании смотрит окружение разработчика, но урл для картинки будет заменён на урл боевого домена:

<img
  class="gallery__img"
  alt="<?= $item['alt']; ?>"
  draggable="false"
  src="<?= str_replace( 'dev1.', '', $item['src'] ); ?>">

Что же делать

Выкинуть из кода все переопределения констант.

Создать файл wp-domains.php в корне. Внутри описать магию с ob_start + str_replace.

<?php
/**
 * https://github.com/MihanEntalpo/WordpressGitTools/blob/master/wp-domains.php
 */

//Проверим, не запускался ли данный скрипт ранее:
if (defined("WP_DOMAINS_INITIALIZED")) return;
const WP_DOMAINS_INITIALIZED = 1;

//Домены на котором может работать сайт, локальные и удалённые,
$domains_enabled = array(
  "site.tld",
  "7site.tld",
  "dev.7site.tld"
);


//Добавим в массив $_SERVER все необходимые для проверки ключи,
//которых там может не быть
$_SERVER = array_replace(
  array(
    "HTTPS" => "", "HTTP_HTTPS" => "", "REQUEST_SCHEME" => "", "SERVER_PORT" => ""
  ),
  $_SERVER
);

//Проверим, используется ли https?
$https =
  (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')
    || (!empty($_SERVER['HTTP_HTTPS']) && $_SERVER['HTTP_HTTPS'] != 'off')
    || $_SERVER['REQUEST_SCHEME'] == 'https'
    || $_SERVER['SERVER_PORT'] == 443;


//Составим адрес хоста
$site_addr = ($https ? "https://" : "http://") . $_SERVER['HTTP_HOST'] . "/";

//Заменим адрес хоста на тот, что мы вычислили
if (!defined("WP_HOME")) define("WP_HOME", $site_addr);
if (!defined("WP_SITEURL")) define("WP_SITEURL", $site_addr);


//Добавим обработчик выдачи сайта, который будет заменять во всех ссылках
//домены на текущий домен
ob_start(function ($data) use ($domains_enabled, $https)
{
  $current_host = $_SERVER['HTTP_HOST'];
  $replace = array();
  $scheme = $https ? "https" : "http";
  foreach ($domains_enabled as $domain) {
    if ($current_host == $domain) continue;
    $replace["http://$domain/"] = "$scheme://$current_host/";
    $replace["https://$domain"] = "$scheme://$current_host";
    $replace["//$domain/"] = "//$current_host/";
    $replace["//$domain"] = "//$current_host";
  }
  return str_replace(array_keys($replace), array_values($replace), $data);
});

Подключить файл wp-domains.php в файлы wp-config.php и wp-load.php.

require_once __DIR__ . '/wp-domains.php';

Проверено на хостинге timeweb.ru, четырёх доменах-синонимов и трёх поддоменах.

--- ## Хочешь чего-то узнать — включи любопытство **URL**: https://sglazov.ru/notes/curious/ **Дата**: 2022-06-30T00:00:00.000Z **Описание**: Расковырять, узнать побольше, понять, как устроено всё

И не выключай никогда, слыш.

Вот детям такое объяснять даже не надо. А взрослым — почему-то это всё вообще не очевидно. Они всего боятся вокруг. Даже дерево в парке потрогать. Даже себя иногда боятся.

Когда появляется какая-то новая штука — хочется расковырять, узнать побольше, понять, как устроена. И как вообще чего оно там.

Даёшь людям законченную единицу смысла в работе. Изолировано и так, чтобы они ничего не сломали, а если и сломали — это даже плюс. Можно что-то сделать лучше, чем было. А случившаяся поломка бы не задела другие части системы. От админки и до оплат — критичное всё, в общем. А остальное — ломай, изучай, понимай.

А через три месяца узнаёшь, что люди используют три очевидные кнопки и переключатель, а на пункт «развернуть», где ещё есть шесть кнопок даже курсор наводить бояться. Объективного ответа на «почему?» — на находится.

Вспомнил историю на контрасте. Моя тётя. Много годиков. Любопытство к этому возрасту даже проветриваться должно и заменяться поучительной мудростью (неуместной). Учитель математики у кучи избалованных непоседливых детей. И тут вот пандемия, онлайн-уроки, Zoom и его аналоги, вся вот эта херня.

Она попросила мне ей позвонить, сама создала комнаты-конференции, сама изучила, как кикнуть из звонка кого-либо, как стереть с демонстрации экрана нарисованную письку или если кто-то в крестики-нолики играет, или слово «хуй» написал сердечками-символами. Я оставил звонок без звука и её с любопытством. За час она перезванивала трижды и за полтора часа знала, как устроено всё со стороны организатора и как вести себя (куда нажать) в нужной ей ситуации.

Уважуха за любопытство.

И почему так не у всех — я так и не понимаю.

--- ## Исполнитель (сотрудник) — не ксерокс **URL**: https://sglazov.ru/notes/your-opinion/ **Дата**: 2022-06-09T00:00:00.000Z **Описание**: У человека должно быть своё мнение. Обязано быть

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

У человека должно быть своё мнение. Обязано быть.

И свой взгляд на то, что он делает. Даже если всё ТЗ описали (так не бывает) прям педантично.

Предусмотреть все детали и все ситуации во всех мультивалентных и позаботится о нескольких шагах наперёд. Менеджер или кто-то угодно в цепочке дел может что-то проебать или просто не знать. Ну а ты-то знаешь. Добавь своего видения.

Всегда был убеждён, кто каждый человек заинтересован делать что-то хорошее в первую очередь для себя.

Разгрузить своё время, позаботиться о себе так, чтобы результат был прекрасным.

Круто работать с теми, кому не всё равно.

--- ## Laravel на виртуальном хостинге (shared hosting) TimeWeb **URL**: https://sglazov.ru/notes/laravel-in-shared-timeweb/ **Дата**: 2021-11-25T00:00:00.000Z **Теги**: Laravel, PHP **Описание**: Поставить Composer, указать правильную версию PHP, создать симлинк на public_html и добавить конфиг для GitHub Actions
Задача:
поднять Laravel на хостинге TimeWeb и вкрутить CI/CD через GitHub Actions.

Так случилось, что хостинг клиента — виртуальный (shared) на TimeWeb. Тариф ничем не выделяется.

Внутри есть SSH, БД, вообще всё, что нужно и задача в целом, реальная и решаемая.

В терминале версия PHP5.6:

client@vh2**:~/site.tld$ php -v
PHP 5.6.37 (cli) (built: Jul 24 2018 00:54:11)

А нужна минимум PHP7.3. В настройках каждого сайта в личном кабинете хостера можно указать требуемую версию PHP, но вот терминалу на это плевать.

Установка Composer и Laravel

В терминале можно указать путь к исполняемому пакету с нужной версией PHP. Laravel ставится через Composer, его надо скачать и положить рядом.

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
...
php -r "unlink('composer-setup.php');"

В корне появился composer.phar. Ставим Laravel, выбирая нужную версию PHP:

/opt/php73/bin/php composer.phar create-project laravel/laravel

Устанавливается без каких-либо проблем. Важно знать, что к artisan обращаться надо также:

/opt/php73/bin/php artisan optimize

У проектов в таймвебе паблик-директория — ./public_html. А Laravel торчит наружу из ./public в корне файловой структуры. Чтобы это просто завести на таймвебе — можно обойтись симлинком:

ln -s ~/site.tld/public ~/site.tld/public_html

GitHub Actions

В Marketplace GitHub Actions есть уже куча пакетов для Laravel. Моя задача была простой и я обошёлся rsync-deployments:

# .github/workflows/timeweb-deploy.yml

name: Deployment site.tld

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    if: "!contains(github.event.head_commit.message, 'skip ci')"

    steps:
      - name: Загрузка актуальной кодовой базы
        uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: "14"
      - name: Создаёт .env-файл
        run: echo "$" > ".env"
      # Install Сomposer
      - name: Установка Сomposer-зависимостей
        run: composer install --optimize-autoloader --no-dev
      # Artisan cli commands
      - name: Трудится, artisan
        run: |
          php artisan clear-compiled
          php artisan optimize
          php artisan migrate
      # Install NPM
      - name: Установка NPM-зависимостей и сборка статики
        run: |
          npm ci
          npm run prod
      # Deploy
      - name: Публикация проекта site.tld
        uses: burnett01/rsync-deployments@4.1
        with:
          switches: -az --delete --exclude-from="rsync-excludes.txt"
          path: .
          remote_path: $
          remote_host: $
          remote_user: $
          remote_key: $

Все параметры, как и .env-конфиг для production-окружения, будут взяты из Actions secrets.

Ссылки на справочный центр таймвеба

--- ## Убирать модификаторы скрытия, а не добавлять модификаторы показа **URL**: https://sglazov.ru/notes/remove-not-add-for-hide/ **Дата**: 2021-09-10T00:00:00.000Z **Теги**: CSS **Описание**: Разметка — чище. Из разметки сразу понятно базовое состояние «всё скрыто». Проще дебажить

Вот представим. Вы верстаете. И есть штука, которая должна показаться/скрываться по клику на какой-либо элемент. Не модалка, а клик-подсказки или кнопка-меню.

Конструкция, упрощённо (но обязательно с aria-атрибутами!), такая:

<!-- контрол -->
<button class="ui-button"
        type="button"
        id="ui-menu"
        aria-haspopup="true"
        aria-expanded="false">
  Добавить в...
</button>

<!-- 
    нечто, что реагирует на действие 
    с контролом. и оно скрыто
-->
<ul class="ui-menu is-hidden"
    aria-labelledby="ui-menu">
  Меню с пунктами...
</ul>

И вот самое важное как раз в классе is-hidden.

Исходим всегда из того, что в начальном состоянии элемент всегда скрыт. А действие с контролом — его показывает.

Нужно удалять модификатор скрытия, а не добавлять модификатор показа.

$(document).ready(function()
{
  $('.ui-button')
    .on('click', function () 
  {
    $('.ui-menu')
      .removeClass('is-hidden');
  });
});

Разметка — чище. Из разметки сразу понятно базовое состояние «всё скрыто». Проще дебажить.

Стили также пишутся исходя из мысли, что элемент всегда виден, а модификатор — скрывает. С анимацией там или ещё с чем угодно.

.ui-menu {
  opacity: 1;
  visibility: visible;

  // скрывалка
  &.is-hidden {
    opacity: 0;
    visibility: hidden;
  }
}

Ещё всяких ссылок про то, как скрывать/показывать элементы

--- ## Как сделать автоматическую выгрузку транзакций из CloudPayments в CSV-файл **URL**: https://sglazov.ru/notes/cloudpayments-payments-list/ **Дата**: 2021-06-30T00:00:00.000Z **Теги**: PHP, CloudPayments, CSV

Что происходит

Есть прекрасный CloudPayments для приёма платежей. Он умеет в выгрузку списка транзакций из интерфейса. В разделе «Транзакции» (в меню слева) над списком есть кнопка «Экспорт транзакций», по клику вам отправят на почту *.csv-файл за месяц.

Это такой ручной вариант: надо авторизоваться в личном кабинете, запросить файл, забрать его из почты, куда-то там загрузить. Долго и сложно. И это нужно делать бухгалтерии, им итак несладко, а тут ещё это.

Хочется автоматического варианта. Чаще для автоматизации 1С (или любого интегратора) + CloudPayments.

Тезисно, идеальный вариант работы

  1. Есть csv-файл (он прост, с ним легко работать и его понимают все), надо его как-то создавать
  2. Файл создаётся по расписанию
  3. Кладётся куда-то на сервер
  4. Есть FTP-доступ на этот сервер
  5. Доступ указывается где-то в 1С
  6. Бухгалтерия получает актуальную информацию по транзакциям, всё происходит автоматически
  7. Все радуются, занимаются другими полезными делами (считают деньги?)

Как сделать автоматическую выгрузку транзакций из CloudPayments

Через API от CloudPayments, конечно. Документация прекрасна. То, что нам нужно описано тут: https://developers.cloudpayments.ru/#vygruzka-spiska-tranzaktsiy

/**
 * Выгрузка списка транзакций
 *
 * https://developers.cloudpayments.ru/#vygruzka-spiska-tranzaktsiy
 *
 * API Lib: https://github.com/flowwow/cloudpayments-php-client
 */
$today    = date('Y-m-d');
$publicId = '';
$pass     = '';
$apiClient = new \Flowwow\Cloudpayments\Library($publicId, $pass);

$response = $apiClient->getListPayment(
    new \Flowwow\Cloudpayments\Request\PaymentsList(
        '2021-06-05',
        'MSK'
    )
);

$list = $response->model;

$publicId и $pass — ваши «секреты» из настроек сайта в личном кабинете.

В общем-то, это всё. Спасибо за лёгкость библиотеке для работы с API. Люблю прям опенсорс за такое.

Вы получите json со списком транзакций за сутки.

Дальше нужно из этого ответа собрать csv-файл. По пути можно чего-то там отфильтровать (например, собрать формат только сумма + название платежа, всё остальное неинтересно). Так вот, собрать файл и его куда-то положить. Ещё настроить автозапуск скрипта, чтобы файл создавался каждый день.

--- ## Организация шрифтов в проекте **URL**: https://sglazov.ru/notes/fonts-in-project/ **Дата**: 2020-09-30T00:00:00.000Z **Теги**: CSS, Fonts **Описание**: Рассказываю о своей организации шрифтов: имена по начертаниям и немного магии в SCSS. И предзагрузка ещё

Чаше всего я использую в проектах стандартный набор системных шрифтов:

font-family: -apple-system, BlinkMacSystemFont,
            'Segoe UI', Roboto,
            'Helvetica Neue', Helvetica,
             Ubuntu,
             Cantarell,
             Arial, sans-serif,
            'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';

Один из них точно «попадёт» и текст на сайте будет выглядеть привычно для посетителя, как в его операционной системе.

А нестандартные шрифты чаще всего рутина и боль. Я стараюсь организовать свой процесс, рассказываю, как.

Скачать и удобно переименовать

Для начала нужно скачать нужные начертания и положить к себе так, чтобы потом ими было удобно манипулировать.

Google Fonts позволяет скачивать шрифты со страницы описания и отдаёт *.ttf-вариант. Вверху справа есть кнопка «Download family».

Далее каждую гарнитуру размещаю в папке с названием, соответствующим имени гарнитуры. Внутри папки — файлы с именем по начертанию. Стандартные имена файлов шрифтов с начертаниям сложны и многословны.

├── fonts
│   └── alegreya-sans
│       ├── ...
│       ├── AlegreyaSans-Regular.ttf       # → normal.ttf
│       ├── AlegreyaSans-MediumItalic.ttf  # → 500-italic.ttf
│       ├── AlegreyaSans-Medium.ttf        # → 500.ttf
│       ├── AlegreyaSans-Bold.ttf          # → bold.ttf
│       ├── AlegreyaSans-BlackItalic.ttf   # → 900-italic.ttf
│       ├── AlegreyaSans-Black.ttf         # → 900.ttf

Обычно bold и normal оставляю в буквенном варианте. И в CSS использую также буквенный вариант.

После переименования нужно сконвертировал «тяжёлый» *.ttf в лёгкие *.woff и *.woff2, которые являются архивами.

В редких случаях лучше использовать google-webfonts-helper — там есть все нужные и правильные варианты.

Генерация *.woff/2 из *.ttf-файлов

Раньше я пользовался разными онлайн-инструментами, но увидев консольный способ в докладе Вадима Макеева «_ ___ ______?», стал использовать только его:

# поставить пакет
brew tap bramstein/webfonttools
brew install woff2 sfnt2woff-zopfli

# перейти в папку со шрифтами
cd app/src/fonts/

# сконвертировать ttf
woff2_compress 500.ttf     # → 500.woff2
sfnt2woff-zopfli 500.ttf   # → 500.woff

Увы, инструменты @bramstein/webfonttools для генерации не умеют работать пакетно или по маске типа/имени файла. Каждый *.ttf-файл нужно указать в ручном режиме. Дважды.

Подключение в стилях: SCSS, карты и @each

Всё, что нужно для подключения шрифта в стилях — это имя шрифта и набор его начертаний. Именно это я и указываю в карте шрифтов.

Из имени шрифта получается имя директории со шрифтом — пробелы заменяются на дефисы и регистр меняется на строчные. Всё это происходит в функции slug(string).

$global-font-path : '/fonts/'; // где вообще все шрифты в проекте

// имя шрифтов и их начертания
$fonts: (
  "Alegreya Sans" : ( 300, normal, 500, bold ),
  "Fira Code"     : ( 100, normal, bold )
);

@mixin font-face($font, $weight) {

  // каждый стиль: обычный и курсив
  @each $style in (normal, italic) {

    $path : slug($font);

    // если курсив, то добавить суффикс
    $font-file: if( $style == italic, $weight + '-italic', $weight);

    // повторить этот блок
    @font-face {
      font-family: $font;
      font-display: swap; // это важно для отрисовки
      font-weight: $weight;
      font-style: $style;
      src:
        url('#{$global-font-path}#{$path}/#{$font-file}.woff2') format('woff2'),
        url('#{$global-font-path}#{$path}/#{$font-file}.woff') format('woff');
    }
  }
}

// для каждого шрифта и начертаний..
@each $font, $weight in $fonts {
  // при разном количестве указания начертаний меняется тип переменной в SCSS o_O
  // проверяем это и если > 1, list — проходим по всем в цикле...
  @if type-of($weight) == list {
    @each $weight in $weight {
      @include font-face($font, $weight);
    }
  // если =1, используем это значение
  } @else {
    @include font-face($font, $weight);
  }
}

Разумеется, подключаются только современные форматы. И используется font-display: swap;, чтобы шрифт отображался сразу.

Как только возникает необходимость подключить какой-то шрифт:

Предзагрузка шрифтов

Я предзагружаю файлы базовых начертаний только на главной странице через preload:

<link rel="preload"
      href="/fonts/alegreya-sans/normal.woff2"
      as="font"
      type="font/woff2"
      crossorigin
/>

<link rel="preload"
      href="/fonts/alegreya-sans/bold.woff2"
      as="font"
      type="font/woff2"
      crossorigin
/>

Не забывайте про атрибут crossorigin.


Подробнее о предзагрузке любых ресурсов — в посте Ивана Акулова: «Preload, prefetch and other <link> tags» (английский, или на русском на Хабре).

Выводы, тезисно

  1. Скачивать и конвертировать. Лучше с надёжных источников;
  2. Переименовывать по начертанию, хранить в папке с именем шрифта;
  3. Максимально упросить подключение в стилях. Я использовал SCSS Maps для указания шрифтов/начертаний, @mixin и @each для генерации нужных правил @font-face;
  4. Не забывать про предзагрузку;
  5. Часть работы можно автоматизировать на этапе сборки.

Ещё всякие ссылки про шрифты


Обновлено 22.09.2023: добавлена ссылка на WOFF Has Left the Building.

--- ## Как настроить динамический ретаргетинг ВКонтакте (про JavaScript тоже) **URL**: https://sglazov.ru/notes/vk-retargeting/ **Дата**: 2019-10-13T00:00:00.000Z **Теги**: VK, JS

Чё делать-то?

  1. Поставить пиксель на сайт. Просто скопируйте js-код и установите;
  2. Опишите события и вызовите их асинхронно, то есть гарантировано после того, как vk-пиксель инициализирован;

Как описывают события?

Всего событий сейчас 12 (актуально на октябрь 2018 года). Полный список можно найти по ссылкам-первоисточникам в разделе чуть ниже. У людей много вопросов появляется в духе, что именно за код, как его добавить, что делать, причём тут пример для Google Tag Manager и т.п.

Всё проще, чем кажется.

Вам важно знать id товара из прайс-листа. Его видно в меню «Ретаргетинг» рекламных кампаний в ВК.

В примерах ниже цифры 15901 — это и есть id прайс-листа. Замените на свой.

view_home

<script>
window.vkAsyncInit = function() { 
  VK.Retargeting.ProductEvent(15901, "view_home", {});
  console.log('VK pixel event send: view_home');
}
</script>

view_search

<script>
window.vkAsyncInit = function() { 
  VK.Retargeting.ProductEvent(15901, "view_search", {"search_string": "ПОИСКОВЫЙ ЗАПРОС"});
  console.log('VK pixel event send: view_search');
}
</script>

view_category

<script>
window.vkAsyncInit = function() { 
  VK.Retargeting.ProductEvent(15901, "view_category", "");
  console.log('VK pixel event send: view_category');
}
</script>

view_other

<script>
window.vkAsyncInit = function() { 
  VK.Retargeting.ProductEvent(15901, "view_other", {});
  console.log('VK pixel event send: view_other');
}
</script>

add_payment_info

<script>
window.vkAsyncInit = function() { 
  VK.Retargeting.ProductEvent(15901, "add_payment_info", {});
  console.log('VK pixel event send: add_payment_info');
}
</script>

purchase

<script>
window.vkAsyncInit = function() { 
  VK.Retargeting.ProductEvent(15901, "purchase", {});
  console.log('VK pixel event send: purchase');
}
</script>

add_payment_info

<script>
window.vkAsyncInit = function() { 
  VK.Retargeting.ProductEvent(15901, "add_payment_info", {});
  console.log('VK pixel event send: add_payment_info');
}
</script>

init_checkout

<script>
window.vkAsyncInit = function() { 
  VK.Retargeting.ProductEvent(15901, "init_checkout", {});
  console.log('VK pixel event send: init_checkout');
}
</script>

view_product

<script>
window.vkAsyncInit = function() { 
  VK.Retargeting.ProductEvent(15901, "view_product", {"products": [{"id": "ID ИЛИ АРТИКУЛ ПРОДУКТА"}]} );
  console.log('VK pixel event send: view_product');
}
</script>

add_to_cart

onclick="VK.Retargeting.ProductEvent(15901, "add_to_cart", {"products": [{"id": "ID ИЛИ АРТИКУЛ ПРОДУКТА"}]});return false;"

remove_from_cart

onclick="VK.Retargeting.ProductEvent(15901, "remove_from_cart", {"products": [{"id": "ID ИЛИ АРТИКУЛ ПРОДУКТА"}]});return false;"

Особенности

  1. Если вы используете шаблонизатор Smarty, то не забывайте оборачивать JS-код в шаблонах в теги {literal}...{/literal}

Ещё на эту тему 🔥 🔥 🔥 🔥 🔥

Куча сайтов в своих статьях, по сути, ссылается на эти материалы и/или пересказываю их, но не отвечают на вопрос «как?».

--- ## Дополнительные ресурсы - **RSS Feed**: https://sglazov.ru/notes/feed/ - **Sitemap**: https://sglazov.ru/sitemap.xml - **Robots.txt**: https://sglazov.ru/robots.txt - **Карта сайта для LLM**: https://sglazov.ru/llms.txt --- *Сгенерировано автоматически с сайта sglazov.ru. Последнее обновление: 2026-07-14T11:59:52.000Z*