import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const webRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); const sourceRoot = join(webRoot, '..', '.webarchive'); const illustrationsDir = join(webRoot, 'public', 'assets', 'illustrations'); const mediaDir = join(webRoot, 'public', 'assets', 'media'); const dataDir = join(webRoot, 'data'); const rawArticles = JSON.parse(await readFile(join(sourceRoot, 'extracted-articles.json'), 'utf8')); const categories = { 'вирус-самопроизвольный-запуск-брауз': ['Windows', 'Вирусы'], 'использование-jquery-в-webpack': ['JavaScript'], 'компиляция-64-x-разрядных-программ-на-dmd-по': ['DLang', 'DMD', 'Windows'], 'новый-движок-ctfe': ['DLang', 'Compile-time'], 'особенности-vibe-d': ['DLang', 'Vibe.d'], 'ошибка-php-ssl-certificate-error-unable-to-get-local-issuer-certificate': ['PHP', 'SSL'], 'о-tilix-и-d-интервью-с-геральдом-нанном': ['DLang', 'Интервью'], 'пишем-аналог-функции-php-preg_match_all-на-языке-прог': ['DLang', 'Регулярные выражения'], 'bitrix-api-создание-добавление-торгового-пре': ['Bitrix', 'PHP'], 'bitrix-api-функция-для-генерации-кода-элемент': ['Bitrix', 'PHP'], 'bitrix-api-add-foto-editor': ['Bitrix', 'PHP'], 'dconf-2017-под-капотом-мусорщика-ди-дмитрий-ол': ['DLang', 'GC'], 'firefox-увеличение-ожидания-загрузки-timeout-стр': ['Firefox', 'Настройки'], }; const covers = { 'вирус-самопроизвольный-запуск-брауз': '/assets/illustrations/cover-virus.svg', 'использование-jquery-в-webpack': '/assets/illustrations/jquery-webpack.svg', 'компиляция-64-x-разрядных-программ-на-dmd-по': '/assets/illustrations/dmd-win64.svg', 'новый-движок-ctfe': '/assets/illustrations/ctfe-engine.svg', 'особенности-vibe-d': '/assets/illustrations/vibe-cover.svg', 'ошибка-php-ssl-certificate-error-unable-to-get-local-issuer-certificate': '/assets/illustrations/php-ssl-cacert.svg', 'о-tilix-и-d-интервью-с-геральдом-нанном': '/assets/illustrations/tilix-cover.svg', 'пишем-аналог-функции-php-preg_match_all-на-языке-прог': '/assets/illustrations/preg-match-all-d.svg', 'bitrix-api-создание-добавление-торгового-пре': '/assets/illustrations/bitrix-offer-api.svg', 'bitrix-api-функция-для-генерации-кода-элемент': '/assets/illustrations/bitrix-translit-api.svg', 'bitrix-api-add-foto-editor': '/assets/illustrations/bitrix-photo-editor.svg', 'dconf-2017-под-капотом-мусорщика-ди-дмитрий-ол': '/assets/illustrations/dconf-gc-cover.svg', 'firefox-увеличение-ожидания-загрузки-timeout-стр': '/assets/illustrations/firefox-timeout.svg', }; const imageMap = [ ['2017-05-13_13-39-44', '/assets/illustrations/taskschd-run.svg', 'Окно запуска taskschd.msc'], ['2017-05-13_13-41-17', '/assets/illustrations/taskschd-action.svg', 'Подозрительное задание в планировщике Windows'], ['/2017/05/1.png', '/assets/illustrations/vibe-async.svg', 'Асинхронная модель vibe.d'], ['/2017/05/2.png', '/assets/illustrations/vibe-api.svg', 'Короткий API vibe.d'], ['/2017/05/3.png', '/assets/illustrations/vibe-web.svg', 'Веб-стек vibe.d'], ['/2017/05/4.png', '/assets/illustrations/vibe-db.svg', 'Драйверы баз данных vibe.d'], ['/2017/05/5.png', '/assets/illustrations/vibe-io.svg', 'Неблокирующий ввод-вывод'], ['/2017/05/6.png', '/assets/illustrations/vibe-dlang.svg', 'Язык D как основа vibe.d'], ['/2017/05/7.png', '/assets/illustrations/vibe-diet.svg', 'Шаблоны Diet'], ['tilix-screenshot-2', '/assets/illustrations/tilix-screenshot.svg', 'Боковая панель Tilix в действии'], ['/2017/06/1-300x225', '/assets/illustrations/gc-small-pools.svg', 'Пулы малых объектов в GC D'], ['/2017/06/1.jpg', '/assets/illustrations/gc-small-pools.svg', 'Пулы малых объектов в GC D'], ['/2017/06/2-200x300', '/assets/illustrations/gc-large-pool.svg', 'Пул больших объектов в GC D'], ['/2017/06/2.jpg', '/assets/illustrations/gc-large-pool.svg', 'Пул больших объектов в GC D'], ['bitrix-foto-editor-2', '/assets/illustrations/bitrix-photo-editor-ui.svg', 'Редактор картинок Bitrix'], ]; const additions = { 'вирус-самопроизвольный-запуск-брауз': `

Проверяем, что проблема решена

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

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

`, 'использование-jquery-в-webpack': `

Итоговая схема подключения

Для старого проекта jQuery в Webpack лучше подключать явно: установить пакет, импортировать его в точке входа и, если плагинам нужна глобальная переменная, добавить ProvidePlugin. Тогда один и тот же экземпляр библиотеки будет использоваться и вашим кодом, и плагинами.

const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
      'window.jQuery': 'jquery',
    }),
  ],
};

Если плагин всё равно не видит jQuery, проверьте порядок импортов и отсутствие второй копии библиотеки в bundle. На этом обычно все проблемы заканчиваются.

`, 'компиляция-64-x-разрядных-программ-на-dmd-по': `

Короткая памятка

Чтобы не возвращаться к этой настройке каждый раз, держите отдельный build-скрипт для 64-битной сборки и явно указывайте архитектуру, компилятор и путь к библиотекам. Главная мысль статьи: не смешивать 32-битные OMF-библиотеки и 64-битную сборку.

dmd -m64 main.d -ofapp.exe
dub build --arch=x86_64

Если линковщик ругается на формат библиотек, сначала проверьте разрядность каждой внешней зависимости. В большинстве случаев ошибка не в D-коде, а в том, что рядом оказалась библиотека другого формата.

`, 'новый-движок-ctfe': `

Как применять выводы на практике

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

Практическое правило такое: если вычисление зависит только от литералов и конфигурации сборки, переносите его в CTFE. Если нужен ввод-вывод, сеть или состояние окружения, оставьте это обычному коду.

`, 'особенности-vibe-d': `

Когда выбирать vibe.d

vibe.d хорошо подходит для небольших и средних веб-сервисов на D, где важны неблокирующий ввод-вывод, простая маршрутизация и единая экосистема DUB. Начать можно с минимального HTTP-сервера, затем добавить Diet-шаблоны, MongoDB или Redis и вынести повторяющиеся части в отдельные модули.

shared static this()
{
    auto settings = new HTTPServerSettings;
    settings.port = 8080;
    listenHTTP(settings, &handleRequest);
}

Если проект требует большого количества готовых интеграций, заранее проверьте наличие пакетов в DUB. Если же нужна компактная серверная часть на D, vibe.d остаётся прямым и понятным выбором.

`, 'ошибка-php-ssl-certificate-error-unable-to-get-local-issuer-certificate': `

Финальная проверка настройки

После подключения файла сертификатов перезапустите веб-сервер или PHP-FPM и выполните тестовый HTTPS-запрос из того же окружения, где возникала ошибка. Важно проверять не браузер, а именно PHP: CLI и веб-сервер могут использовать разные php.ini.

php -i | grep cafile
php -r "var_dump(file_get_contents('https://example.com') !== false);"

Не отключайте проверку SSL через CURLOPT_SSL_VERIFYPEER = false как постоянное решение. Это скрывает проблему и делает соединение уязвимым. Правильный путь — актуальный CA bundle и явно указанная директива openssl.cafile.

`, 'о-tilix-и-d-интервью-с-геральдом-нанном': `

Что вынести из интервью

Главный практический вывод прост: D может быть не только системным языком, но и удобным языком для прикладных desktop-инструментов. Tilix показывает, что связка D, GtkD и аккуратного следования HIG способна дать зрелое приложение, которым пользуются каждый день.

Если хочется повторить такой путь, начинайте не с большого терминала, а с маленькой GTK-утилиты: окно, настройки, несколько действий, сборка через DUB. Так быстрее станет понятно, насколько привычки из Java, Delphi или PHP мешают или помогают писать на D.

`, 'пишем-аналог-функции-php-preg_match_all-на-языке-прог': `

Как довести класс до рабочего состояния

После реализации флагов обязательно добавьте тесты на три случая: нет совпадений, одно совпадение с группами и несколько совпадений с позициями. Именно на позициях чаще всего появляются ошибки, потому что PHP возвращает смещения в исходной строке, а не в подстроке после очередного поиска.

assert(matchAll(r"(\w+)=(\d+)", "a=1 b=22").length == 2);
assert(matchAll(r"z+", "abc").empty);

Дальше можно расширять API постепенно: сначала PREG_PATTERN_ORDER, затем PREG_SET_ORDER, и только после этого режим с OFFSET_CAPTURE.

`, 'bitrix-api-создание-добавление-торгового-пре': `

Что проверить после создания предложения

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

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

`, 'bitrix-api-функция-для-генерации-кода-элемент': `

Как использовать функцию безопасно

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

telefon-samsung
telefon-samsung-2
telefon-samsung-3

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

`, 'bitrix-api-add-foto-editor': `

Завершение интеграции

После вывода компонента важно не забыть обработать результат формы: сохранить ID файла, проверить тип загруженного файла и права пользователя. Сам редактор решает задачу интерфейса, но безопасность и привязка к сущности остаются на стороне вашего кода.

Если редактор не открывается, проверьте подключение модулей main и fileman, права на загрузку и наличие административных JS-расширений на странице. После этого компонент работает стабильно и может использоваться не только в админке, но и в собственных формах.

`, 'dconf-2017-под-капотом-мусорщика-ди-дмитрий-ол': `

Практический вывод

Статья важна не только для разработчиков компилятора. Она показывает, почему аллокатор и сборщик мусора нельзя оценивать одной фразой «GC медленный». Нужно смотреть на классы размеров, поиск пулов, стоимость маркировки и то, сколько работы выполняется во время остановки мира.

Для прикладного D-кода вывод такой: держите горячие циклы предсказуемыми, не создавайте лишние временные объекты в tight-loop и измеряйте паузы. Если участок действительно критичен, D позволяет локально отключить GC или заменить аллокации заранее подготовленными буферами.

`, 'firefox-увеличение-ожидания-загрузки-timeout-стр': `

Когда лучше не увеличивать timeout

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

Для phpMyAdmin, импорта и парсинга больших файлов увеличение timeout допустимо. Для публичного пользовательского сценария лучше исправлять серверную часть, иначе браузер станет ждать дольше, но пользователь всё равно получит плохой интерфейс.

`, }; const articleAdditions = { ...additions, 'вирус-самопроизвольный-запуск-брауз': `

Проверяем, что проблема решена

После удаления задания перезагрузите компьютер и оставьте систему включённой на тот промежуток времени, через который раньше открывался браузер. Если окно больше не появляется, значит источник найден правильно. Если запуск повторяется, проверяем не только библиотеку планировщика, но и автозагрузку, ключи Run в реестре и расширения браузера.

В планировщике заданий важно смотреть не только название задания. Название злоумышленник может сделать вполне безобидным: Update Service, Browser Check, Windows Helper. Смысл находится в действии. Если в действии указан путь к браузеру, а после него стоит адрес сайта, то перед нами как раз тот самый случай.

Если задача появляется снова, значит в системе остался установщик или служба, которая её восстанавливает. В таком случае дополнительно смотрим «Приложения и возможности», папки автозагрузки пользователя, ключи HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run и HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Run. После очистки нужно обновить антивирусные базы и выполнить полную проверку.

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

`, 'использование-jquery-в-webpack': `

Итоговая схема подключения

Для старого проекта jQuery в Webpack лучше подключать явно: установить пакет, импортировать его в точке входа и отдельно решить вопрос с глобальными переменными. ProvidePlugin удобен, когда в модулях встречаются свободные идентификаторы $ или jQuery. Но если старый плагин лезет именно в window.jQuery, одного ProvidePlugin может быть мало — нужно положить jQuery в window самостоятельно.

import $ from 'jquery';

window.$ = $;
window.jQuery = $;

После этого можно добавить ProvidePlugin, чтобы не писать импорт в каждом файле:

const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
    }),
  ],
};

Порядок подключения имеет значение. Сначала в entry-файле импортируем jQuery и кладём его в window, потом импортируем старые плагины, которым нужен глобальный объект. Если плагин подключается через require, делаем это после установки window.jQuery.

import './jquery-global';
import 'inputmask/dist/jquery.inputmask';
import './app';

Проверка простая: в браузерной консоли должны существовать window.$ и window.jQuery, а в собранном bundle не должно быть двух разных копий jQuery. Если проект новый, лучше не тащить jQuery без необходимости. Если проект старый и плагины уже написаны под jQuery, такая схема делает зависимость явной и предсказуемой.

`, 'компиляция-64-x-разрядных-программ-на-dmd-по': `

Короткая памятка для современных и старых сборок

Главная мысль остаётся той же: нельзя смешивать 32-битные OMF-библиотеки и 64-битную сборку. В старых установках DMD под Windows это часто упиралось в ручную настройку sc.ini, Windows SDK и Visual Studio linker. В более новых установках официальный установщик DMD обычно делает большую часть этой настройки сам, а для нормальной работы достаточно иметь Visual Studio Build Tools или установленную Visual Studio с C++ tooling.

Для простой 64-битной сборки используем явную архитектуру:

dmd -m64 main.d -of=app.exe
dub build --arch=x86_64

Если используется старая 32-битная сборка, смотрим, какой формат объектных файлов нужен проекту. Старый OMF и MS-COFF несовместимы между собой. Для современных библиотек Windows чаще нужен MS-COFF, а для 64-битной сборки это фактически обычный путь.

Типичная ошибка выглядит так: код компилируется, а линковка падает на внешней библиотеке. В большинстве случаев виноват не D-код, а то, что рядом лежит библиотека другого формата или другой разрядности. Сначала проверяем toolchain и зависимости, и только потом ищем ошибку в исходниках.

`, 'новый-движок-ctfe': `

Как применять выводы на практике

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

enum table = buildLookupTable();

static assert(table.length == 256);

Хороший кандидат для CTFE — функция, которая зависит только от литералов, типов и конфигурации сборки. Плохой кандидат — код, которому нужен ввод-вывод, сеть, текущее время, состояние окружения или большая внешняя база данных. Такой код лучше оставить в runtime или вынести в отдельный генератор, который запускается до сборки.

Для контроля качества CTFE-кода полезно держать рядом static assert. Он сразу показывает, что вычисление действительно произошло на этапе компиляции и вернуло ожидаемый результат. Если compile-time функция стала слишком сложной, её стоит разбить на маленькие чистые функции: так компилятору проще, и человеку проще понять, что происходит.

Практический вывод такой: CTFE в D — мощный инструмент, но сила его не в магии, а в переносе предсказуемых вычислений из runtime в compile-time. Используем его для констант, генерации и проверок. Не используем его как замену обычной программе.

`, 'особенности-vibe-d': `

Когда выбирать vibe.d

vibe.d хорошо подходит для небольших и средних веб-сервисов на D, где важны неблокирующий ввод-вывод, простая маршрутизация и единая экосистема DUB. Начать можно с минимального HTTP-сервера, затем добавить Diet-шаблоны, MongoDB или Redis и вынести повторяющиеся части в отдельные модули.

shared static this()
{
    auto settings = new HTTPServerSettings;
    settings.port = 8080;
    listenHTTP(settings, &handleRequest);
}

При проектировании сервиса главное не забывать, что удобный синтаксис не отменяет асинхронную модель. Долгие CPU-задачи нельзя выполнять прямо в обработчике запроса, иначе они будут мешать обработке ввода-вывода. Для таких задач лучше использовать отдельные worker-потоки, очередь или заранее подготовленные данные.

Если проект требует большого количества готовых интеграций, заранее проверьте наличие пакетов в DUB и состояние нужных драйверов. Если же нужна компактная серверная часть на D, vibe.d остаётся прямым и понятным выбором: один язык, быстрый нативный код, нормальная маршрутизация, шаблоны и асинхронный ввод-вывод.

`, 'ошибка-php-ssl-certificate-error-unable-to-get-local-issuer-certificate': `

Финальная проверка настройки

После подключения файла сертификатов перезапустите веб-сервер или PHP-FPM и выполните тестовый HTTPS-запрос из того же окружения, где возникала ошибка. Важно проверять не браузер, а именно PHP: CLI и веб-сервер могут использовать разные php.ini.

php --ini
php -i | grep -E "curl.cainfo|openssl.cafile"
php -r "var_dump(file_get_contents('https://example.com') !== false);"

Для cURL указывается curl.cainfo, для потоков OpenSSL — openssl.cafile. На Windows путь лучше писать полностью и без относительных директорий. Если используется не глобальный php.ini, а отдельная настройка клиента, можно передать CA bundle прямо в cURL:

curl_setopt($ch, CURLOPT_CAINFO, 'C:\\php\\extras\\ssl\\cacert.pem');

Не отключайте проверку SSL через CURLOPT_SSL_VERIFYPEER = false как постоянное решение. Это скрывает проблему и делает соединение уязвимым. Такая настройка допустима только для короткой диагностики в локальной среде, и после проверки её нужно вернуть обратно.

Если CA bundle указан верно, но ошибка осталась, проверьте цепочку сертификатов на стороне удалённого сервера. Иногда браузер открывает сайт, потому что умеет достраивать цепочку, а PHP/cURL получает неполную цепочку и честно падает. В таком случае исправлять нужно сертификаты сервера, а не PHP-код.

Правильный итог: актуальный cacert.pem, явно указанные curl.cainfo и openssl.cafile, перезапуск PHP и тест именно из того окружения, где работает приложение.

`, 'о-tilix-и-d-интервью-с-геральдом-нанном': `

Что вынести из интервью

Главный практический вывод прост: D может быть не только системным языком, но и удобным языком для прикладных desktop-инструментов. Tilix показывает, что связка D, GtkD и аккуратного следования GNOME HIG способна дать зрелое приложение, которым пользуются каждый день.

В интервью хорошо видно, что выбор языка был не религиозным, а практическим. Автору не хотелось писать GUI на C или C++, Python оказался неудобен для крупного статически проверяемого приложения, а D дал знакомую после Java модель, нативную скорость и нормальные GTK-привязки. Это важный критерий выбора технологии: не «самый модный язык», а язык, на котором конкретный автор может быстро и спокойно делать продукт.

Если хочется повторить такой путь, начинать стоит не с большого терминала, а с маленькой GTK-утилиты: окно, настройки, несколько действий, сборка через DUB, упаковка и обновления. На таком проекте сразу проявятся реальные вопросы: работа с GtkD, структура проекта, взаимодействие с C-библиотеками, обработка событий и паузы GC.

Именно поэтому интервью полезно не только поклонникам D. Оно показывает нормальный инженерный путь: выбрать задачу, подобрать инструмент, сделать небольшую работающую версию, а затем постепенно доводить приложение до зрелого состояния.

`, 'пишем-аналог-функции-php-preg_match_all-на-языке-прог': `

Как довести класс до рабочего состояния

После реализации флагов обязательно добавьте тесты на три случая: нет совпадений, одно совпадение с группами и несколько совпадений с позициями. Именно на позициях чаще всего появляются ошибки, потому что PHP возвращает смещения в исходной строке, а не в подстроке после очередного поиска.

assert(matchAll(r"(\w+)=(\d+)", "a=1 b=22").length == 2);
assert(matchAll(r"z+", "abc").empty);

В D стоит явно решить, какой формат результата нужен. PHP поддерживает несколько режимов: PREG_PATTERN_ORDER, PREG_SET_ORDER и вариант с PREG_OFFSET_CAPTURE. Если пытаться сделать всё сразу, код быстро станет запутанным. Лучше сначала реализовать простой режим, затем режим группировки по совпадениям, и только после этого добавить позиции.

struct MatchPart
{
    string text;
    ptrdiff_t offset;
}

alias MatchSet = MatchPart[][];

Отдельно проверьте Unicode. Если шаблон и строка содержат кириллицу, важно понимать, что именно возвращается в offset: позиция в байтах или позиция в символах. PHP для PREG_OFFSET_CAPTURE возвращает байтовое смещение. Если в D вы хотите повторить поведение PHP, нужно документировать именно байтовый offset, иначе результаты будут отличаться.

Финальный класс должен иметь маленькую поверхность API: метод без offset, метод с offset и понятный enum для порядка результата. Тогда аналог preg_match_all будет не просто копией PHP-функции, а удобным D-инструментом, который предсказуемо работает в тестах.

`, 'bitrix-api-создание-добавление-торгового-пре': `

Что проверить после создания предложения

После добавления торгового предложения проверьте три связи: привязку к товару, цены и остатки. В Bitrix предложение может успешно создаться, но не появиться в публичной части, если не заполнены обязательные свойства SKU, не сохранён товарный каталог или не обновлены кеши.

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

CModule::IncludeModule('iblock');
CModule::IncludeModule('catalog');

$offerId = $el->Add($fields);
CCatalogProduct::Add([
    'ID' => $offerId,
    'QUANTITY' => 10,
]);
CPrice::SetBasePrice($offerId, 990, 'RUB');

На боевом проекте обязательно логируйте результат Add и текст ошибки LAST_ERROR. Bitrix часто молча возвращает false, а настоящая причина находится именно там: обязательное поле, неправильный инфоблок, неверный код свойства или недостаточные права.

`, 'bitrix-api-функция-для-генерации-кода-элемент': `

Как использовать функцию безопасно

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

telefon-samsung
telefon-samsung-2
telefon-samsung-3

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

$code = CUtil::translit($name, 'ru', [
    'replace_space' => '-',
    'replace_other' => '-',
    'change_case' => 'L',
]);

$code = trim(preg_replace('/-+/', '-', $code), '-');

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

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

`, 'bitrix-api-add-foto-editor': `

Завершение интеграции

После вывода компонента важно не забыть обработать результат формы: сохранить ID файла, проверить тип загруженного файла и права пользователя. Сам редактор решает задачу интерфейса, но безопасность и привязка к сущности остаются на стороне вашего кода.

В параметрах компонента отдельно проверьте allowUpload, medialib, fileDialog, cloud, delete, edit и maxCount. Не все параметры будут иметь эффект в любой установке Bitrix: часть зависит от подключённых модулей, прав пользователя и контекста страницы.

$fileId = (int)$_POST['picture'];

if ($fileId > 0) {
    $file = CFile::GetFileArray($fileId);
    if ($file && str_starts_with($file['CONTENT_TYPE'], 'image/')) {
        // сохраняем ID картинки в своей сущности
    }
}

Если редактор не открывается, проверьте подключение модулей main и fileman, права на загрузку, административные JS-расширения и наличие сессии пользователя. В публичной части сайта проблема часто оказывается не в FileInput, а в том, что на странице не подключены нужные скрипты Bitrix.

Рабочая схема такая: выводим FileInput, разрешаем только картинки, ограничиваем количество, после отправки формы валидируем ID файла и сохраняем его в свою сущность. Тогда встроенный редактор становится нормальной частью формы, а не просто красивой кнопкой загрузки.

`, 'dconf-2017-под-капотом-мусорщика-ди-дмитрий-ол': `

Практический вывод

Статья важна не только для разработчиков компилятора. Она показывает, почему аллокатор и сборщик мусора нельзя оценивать одной фразой «GC медленный». Нужно смотреть на классы размеров, поиск пулов, стоимость маркировки и то, сколько работы выполняется во время остановки мира.

Для прикладного D-кода вывод такой: держите горячие циклы предсказуемыми, не создавайте лишние временные объекты в tight-loop и измеряйте паузы. Если участок действительно критичен, D позволяет локально управлять GC и заменить поток мелких аллокаций заранее подготовленными буферами.

import core.memory : GC;

GC.disable();
scope(exit) GC.enable();

// горячий участок без лишних временных объектов

Отключать GC нужно аккуратно. Это не волшебная оптимизация, а договор: пока сборщик выключен, программа не должна бесконтрольно накапливать мусор. Поэтому такой приём подходит для короткого измеримого участка, а не для всего приложения.

Главный смысл доклада в том, что производительность памяти — это архитектура. Если в программе много мелких объектов, непредсказуемых ссылок и больших пауз, проблема редко решается одной настройкой. Нужно менять модель данных, жизненный цикл объектов и место, где выделяется память.

`, 'firefox-увеличение-ожидания-загрузки-timeout-стр': `

Когда лучше не увеличивать timeout

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

Важно не путать разные таймауты. Ожидание HTTP-ответа — это одно, долго выполняющийся JavaScript на странице — другое, а таймауты прокси или веб-сервера — третье. Если Firefox ждёт дольше, но nginx, Apache, PHP-FPM или upstream уже оборвал соединение, настройка браузера не поможет.

Если параметра в about:config нет, его можно создать вручную только тогда, когда вы понимаете, какая версия Firefox и какой именно timeout вам нужен. После завершения работы лучше вернуть настройку к обычному значению, чтобы браузер не висел слишком долго на реально недоступных сайтах.

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

`, }; function escapeHtml(value) { return String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"'); } function decodeHtml(value = '') { const named = { amp: '&', apos: "'", gt: '>', hellip: '...', laquo: '«', lt: '<', mdash: '—', nbsp: ' ', quot: '"', raquo: '»' }; return value .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16))) .replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number.parseInt(dec, 10))) .replace(/&([a-z]+);/gi, (_, name) => named[name] ?? `&${name};`); } function stripTags(value = '') { return decodeHtml(value.replace(/<[^>]+>/g, ' ')).replace(/\s+/g, ' ').trim(); } function normalizeContent(html) { let content = html .replace(/<\/span>/g, '') .replace(//gi, '') .replace(//gi, ''); content = content.replace(/]*>([\s\S]*?)<\/pre>/gi, (_, code) => { const text = decodeHtml(code.replace(/<[^>]+>/g, '')); return `
${escapeHtml(text.trim())}
`; }); content = content.replace(/]*>/gi, (tag) => { const src = decodeHtml(tag.match(/\bsrc=["']([^"']+)["']/i)?.[1] ?? ''); const found = imageMap.find(([needle]) => src.includes(needle)); if (!found) return ''; const [, imageSrc, defaultAlt] = found; const originalAlt = decodeHtml(tag.match(/\balt=["']([^"']*)["']/i)?.[1] ?? ''); const alt = originalAlt || defaultAlt; return `
${escapeHtml(alt)}
${escapeHtml(defaultAlt)}
`; }); content = content .replace(/\s+(srcset|sizes|width|height|class)=["'][^"']*["']/gi, '') .replace(/

\s*<\/p>/g, '') .replace(/\n{3,}/g, '\n\n') .trim(); return content; } function text(x, y, body, options = {}) { const attrs = [ `x="${x}"`, `y="${y}"`, options.size ? `font-size="${options.size}"` : '', options.weight ? `font-weight="${options.weight}"` : '', options.fill ? `fill="${options.fill}"` : '', options.family ? `font-family="${options.family}"` : '', ].filter(Boolean).join(' '); return `${escapeHtml(body)}`; } function rect(x, y, width, height, fill, stroke = '#263241', radius = 10) { return ``; } function baseSvg(width, height, title, subtitle, accent, body) { return ` ${text(56, 82, title, { size: 30, weight: 700, fill: '#1f2933' })} ${subtitle ? text(56, 116, subtitle, { size: 16, fill: '#52606d' }) : ''} ${body} `; } function diagramAsset(title, subtitle, accent, labels) { const items = labels.map((item, index) => { const x = 64 + index * 178; const arrow = index < labels.length - 1 ? `` : ''; return `${rect(x, 190, 130, 96, '#f8fafc', '#d1d8e0', 12)}${text(x + 20, 229, item[0], { size: 17, weight: 700, fill: '#1f2933' })}${text(x + 20, 258, item[1], { size: 13, fill: '#52606d' })}${arrow}`; }).join(''); return baseSvg(860, 420, title, subtitle, accent, items); } function codeAsset(title, subtitle, accent, lines) { const code = lines.map((line, index) => text(84, 178 + index * 28, line, { size: 17, fill: '#d7e3f5', family: 'SFMono-Regular, Menlo, Consolas, monospace' })).join(''); return baseSvg(860, 460, title, subtitle, accent, ` ${rect(60, 140, 740, 248, '#111827', '#111827', 14)} ${code} `); } function featureCard(title, subtitle, accent, rows) { const body = rows.map((row, index) => { const y = 168 + index * 58; return `${text(124, y, row, { size: 20, fill: '#263241' })}`; }).join(''); return baseSvg(760, 420, title, subtitle, accent, body); } function taskScheduler(kind) { const isAction = kind === 'action'; return baseSvg(980, 520, isAction ? 'Планировщик заданий: действие' : 'Запуск планировщика заданий', isAction ? 'В аргументах браузера виден лишний URL' : 'Win + R, затем taskschd.msc', '#2f6fed', ` ${rect(70, 148, 840, 300, '#eef3fb', '#bfccdc', 8)} ${text(96, 186, isAction ? 'Библиотека планировщика заданий' : 'Выполнить', { size: 22, weight: 700, fill: '#1f2933' })} ${isAction ? ` ${rect(96, 212, 300, 192, '#fff', '#d1d8e0', 6)} ${text(118, 248, 'Update Service', { size: 17, fill: '#1f2933' })} ${text(118, 282, 'Browser Updater', { size: 17, fill: '#1f2933' })} ${text(118, 316, 'Mozilla Firefox Task', { size: 17, fill: '#b42318', weight: 700 })} ${rect(430, 212, 420, 192, '#fff', '#d1d8e0', 6)} ${text(456, 250, 'Программа:', { size: 16, fill: '#52606d' })} ${text(456, 282, 'firefox.exe', { size: 20, fill: '#1f2933', family: 'monospace' })} ${text(456, 326, 'Аргументы:', { size: 16, fill: '#52606d' })} ${text(456, 358, 'http://example-bad-site.test', { size: 20, fill: '#b42318', family: 'monospace' })} ` : ` ${text(110, 245, 'Открыть:', { size: 18, fill: '#52606d' })} ${rect(110, 268, 540, 54, '#fff', '#9aa8b8', 6)} ${text(132, 303, 'taskschd.msc', { size: 24, fill: '#1f2933', family: 'monospace' })} ${rect(670, 268, 128, 54, '#2f6fed', '#2f6fed', 6)} ${text(704, 303, 'OK', { size: 22, fill: '#fff', weight: 700 })} ${text(110, 372, 'Так открывается системный инструмент, где видны все периодические запуски.', { size: 17, fill: '#52606d' })} `} `); } function tilixScreenshot() { return baseSvg(1040, 600, 'Tilix', 'Тайлинговый терминал с боковой панелью сессий', '#7c3aed', ` ${rect(66, 130, 908, 390, '#1f2933', '#111827', 12)} ${text(100, 159, 'project: ~/src/tilix', { size: 16, fill: '#d7e3f5', family: 'monospace' })} ${rect(88, 198, 170, 286, '#111827', '#334155', 8)} ${text(112, 232, 'sessions', { size: 15, fill: '#94a3b8', family: 'monospace' })} ${text(112, 272, 'build', { size: 18, fill: '#f8fafc', family: 'monospace' })} ${text(112, 310, 'ssh prod', { size: 18, fill: '#f8fafc', family: 'monospace' })} ${text(112, 348, 'logs', { size: 18, fill: '#f8fafc', family: 'monospace' })} ${rect(286, 198, 308, 132, '#0f172a', '#475569', 8)} ${rect(616, 198, 308, 132, '#0f172a', '#475569', 8)} ${rect(286, 352, 638, 132, '#0f172a', '#475569', 8)} ${text(316, 238, '$ dub build', { size: 19, fill: '#a7f3d0', family: 'monospace' })} ${text(646, 238, '$ journalctl -f', { size: 19, fill: '#bfdbfe', family: 'monospace' })} ${text(316, 394, '$ grep -R GtkD source/', { size: 19, fill: '#fde68a', family: 'monospace' })} ${text(316, 430, 'ready', { size: 18, fill: '#e5e7eb', family: 'monospace' })} `); } function gcPools() { return baseSvg(900, 520, 'GC D: малые пулы', 'Классы размеров и страницы памяти', '#0f766e', ` ${[16, 32, 64, 128, 256, 512].map((size, i) => { const y = 150 + i * 48; return `${text(74, y + 28, `${size} B`, { size: 17, fill: '#1f2933', family: 'monospace' })}${rect(150, y, 620, 32, '#ecfeff', '#99f6e4', 4)}${Array.from({ length: 10 }, (_, j) => ``).join('')}`; }).join('')} ${text(74, 470, 'Каждая строка показывает отдельный класс размера и фрагментацию внутри страницы.', { size: 16, fill: '#52606d' })} `); } function gcLargePool() { return baseSvg(900, 520, 'GC D: большие объекты', 'Метаданные растут вместе с диапазоном страниц', '#c2410c', ` ${rect(86, 166, 728, 92, '#fff7ed', '#fdba74', 10)} ${text(112, 222, 'object > 100 MB', { size: 25, fill: '#9a3412', family: 'monospace', weight: 700 })} ${rect(86, 310, 728, 58, '#f8fafc', '#d1d8e0', 8)} ${Array.from({ length: 18 }, (_, i) => ``).join('')} ${text(86, 410, 'Один большой объект может оставить заметный хвост таблиц и свободных страниц.', { size: 17, fill: '#52606d' })} `); } const assetFiles = { 'cover-virus.svg': baseSvg(900, 480, 'Планировщик против рекламного запуска', 'Схема поиска вредного задания', '#2f6fed', `${rect(86, 168, 728, 178, '#eef3fb', '#bfccdc', 12)}${text(128, 230, 'taskschd.msc', { size: 38, weight: 700, fill: '#1f2933', family: 'monospace' })}${text(128, 286, 'firefox.exe + URL', { size: 28, fill: '#b42318', family: 'monospace' })}`), 'taskschd-run.svg': taskScheduler('run'), 'taskschd-action.svg': taskScheduler('action'), 'jquery-webpack.svg': codeAsset('jQuery в Webpack', 'Один экземпляр библиотеки для модулей и старых плагинов', '#2563eb', ['import $ from "jquery";', 'window.$ = window.jQuery = $;', '', 'new webpack.ProvidePlugin({', ' $: "jquery",', ' jQuery: "jquery"', '});']), 'dmd-win64.svg': diagramAsset('DMD x64 под Windows', 'Не смешиваем OMF и COFF библиотеки', '#7c2d12', [['D source', 'main.d'], ['dmd -m64', 'compiler'], ['COFF libs', 'x64'], ['EXE', 'win64']]), 'ctfe-engine.svg': diagramAsset('NewCTFE', 'Вычисления во время компиляции без лишней магии', '#7c3aed', [['AST', 'input'], ['CTFE VM', 'evaluate'], ['Result', 'constant'], ['Binary', 'ready']]), 'vibe-cover.svg': diagramAsset('vibe.d', 'Асинхронный веб-стек на языке D', '#059669', [['HTTP', 'server'], ['Fiber', 'async I/O'], ['Mongo/Redis', 'drivers'], ['Diet', 'views']]), 'vibe-async.svg': featureCard('Асинхронность', 'Задачи выглядят синхронно, I/O остаётся неблокирующим', '#059669', ['fiber на запрос', 'ожидание без блокировки', 'простая модель ошибок']), 'vibe-api.svg': featureCard('Короткий API', 'Минимум кода для типовых веб-задач', '#2563eb', ['маршруты рядом с логикой', 'простые настройки сервера', 'DUB-пакеты']), 'vibe-web.svg': featureCard('Веб-инструменты', 'HTTP, WebSocket, сессии и шаблоны', '#dc2626', ['статические файлы', 'JSON/BSON', 'SMTP и криптография']), 'vibe-db.svg': featureCard('Базы данных', 'MongoDB и Redis доступны из коробки', '#0f766e', ['асинхронные драйверы', 'простые запросы', 'расширение через DUB']), 'vibe-io.svg': featureCard('Неблокирующий I/O', 'libevent и системные API вместо ожидания потока', '#9333ea', ['сетевые операции', 'файловые операции', 'меньше простаивания']), 'vibe-dlang.svg': featureCard('Язык D', 'Скорость нативного кода и выразительность высокого уровня', '#b91c1c', ['типы и шаблоны', 'быстрая компиляция', 'удобная метапрограмма']), 'vibe-diet.svg': featureCard('Diet templates', 'HTML без лишнего синтаксического шума', '#0f172a', ['наследник Pug/Haml', 'динамические вставки', 'читаемые view']), 'php-ssl-cacert.svg': diagramAsset('PHP SSL CA bundle', 'Ошибка лечится сертификатами, а не отключением проверки', '#0f766e', [['PHP', 'request'], ['OpenSSL', 'verify'], ['CA file', 'cacert.pem'], ['HTTPS', 'ok']]), 'tilix-cover.svg': diagramAsset('Tilix + D', 'Desktop-приложение на GtkD', '#7c3aed', [['GtkD', 'UI'], ['D', 'logic'], ['VTE', 'terminal'], ['GNOME', 'HIG']]), 'tilix-screenshot.svg': tilixScreenshot(), 'preg-match-all-d.svg': codeAsset('preg_match_all на D', 'Регулярные выражения, группы и позиции совпадений', '#be123c', ['auto r = regex(r"(\\\\w+)=(\\\\d+)");', 'auto m = matchAll("a=1 b=22", r);', '', 'foreach (hit; m) {', ' writeln(hit.captures);', '}']), 'bitrix-offer-api.svg': codeAsset('Bitrix API: SKU', 'Создание торгового предложения и привязка к товару', '#1d4ed8', ['CModule::IncludeModule("iblock");', '$el = new CIBlockElement;', '$id = $el->Add($fields);', 'CPrice::SetBasePrice($id, 990);']), 'bitrix-translit-api.svg': codeAsset('Bitrix API: translit', 'Символьный код из имени элемента', '#0f766e', ['$code = CUtil::translit($name, "ru", [', ' "replace_space" => "-",', ' "replace_other" => "-",', ']);']), 'bitrix-photo-editor.svg': diagramAsset('Bitrix photo editor', 'Встроенный FileInput для загрузки и редактирования картинки', '#1d4ed8', [['FileInput', 'create'], ['upload', 'image'], ['edit', 'crop'], ['save', 'file ID']]), 'bitrix-photo-editor-ui.svg': baseSvg(1030, 720, 'Редактор картинок Bitrix', 'Читаемый интерфейс FileInput', '#1d4ed8', `${rect(70, 150, 890, 470, '#eef3fb', '#bfccdc', 10)}${rect(110, 200, 520, 360, '#ffffff', '#9aa8b8', 8)}${rect(668, 200, 238, 56, '#fff', '#d1d8e0', 8)}${text(690, 236, 'Обрезать', { size: 20, fill: '#1f2933' })}${rect(668, 278, 238, 56, '#fff', '#d1d8e0', 8)}${text(690, 314, 'Повернуть', { size: 20, fill: '#1f2933' })}${rect(668, 356, 238, 56, '#1d4ed8', '#1d4ed8', 8)}${text(710, 392, 'Сохранить', { size: 20, fill: '#fff', weight: 700 })}`), 'dconf-gc-cover.svg': diagramAsset('D GC under the hood', 'Пулы, страницы, маркировка и очистка', '#0f766e', [['Pool', 'memory'], ['Mark', 'scan'], ['Sweep', 'free'], ['Reuse', 'alloc']]), 'gc-small-pools.svg': gcPools(), 'gc-large-pool.svg': gcLargePool(), 'firefox-timeout.svg': baseSvg(860, 420, 'Firefox timeout', 'Когда браузеру нужно ждать дольше', '#ea580c', `${rect(80, 170, 700, 80, '#fff7ed', '#fed7aa', 12)}${text(112, 222, 'network.http.response.timeout = 600', { size: 28, fill: '#9a3412', family: 'monospace', weight: 700 })}${rect(80, 286, 520, 26, '#fed7aa', '#fed7aa', 13)}${rect(80, 286, 390, 26, '#ea580c', '#ea580c', 13)}${text(80, 352, 'Для административных операций, а не для маскировки медленного сайта.', { size: 18, fill: '#52606d' })}`), }; await mkdir(illustrationsDir, { recursive: true }); await mkdir(mediaDir, { recursive: true }); await mkdir(dataDir, { recursive: true }); for (const [name, svg] of Object.entries(assetFiles)) { await writeFile(join(illustrationsDir, name), svg); } for (const name of ['cropped-WallpapersMania.nnm_.ru_vol141-087.jpg', 'vibe.d.png', 'logo.png', 'bitrix-foto-editor-1.jpg', 'Без-имени.png', '-имени-e1495740674880.png', 'i-880x660.jpg']) { try { await copyFile(join(sourceRoot, 'assets', name), join(mediaDir, name)); } catch { // Generated illustrations are enough for the site; copied media is optional. } } const articles = rawArticles.filter((article) => article.slug !== 'привет-мир').map((article) => { const content = normalizeContent(article.contentHtml); const addition = articleAdditions[article.slug]?.trim() ?? ''; const contentHtml = [content, addition].filter(Boolean).join('\n'); return { slug: article.slug, title: article.title, date: article.date, author: 'DarkRiDDeR', categories: categories[article.slug] ?? [], cover: covers[article.slug], excerpt: article.paragraphs.join(' ').replace(/\s+/g, ' ').slice(0, 260), contentHtml, readingMinutes: Math.max(1, Math.round(stripTags(contentHtml).split(/\s+/).length / 180)), }; }).sort((a, b) => new Date(b.date) - new Date(a.date)); await writeFile(join(dataDir, 'articles.json'), `${JSON.stringify(articles, null, 2)}\n`); console.log(`Generated ${articles.length} articles and ${Object.keys(assetFiles).length} illustration assets`);