Подготовить сайт к запуску

This commit is contained in:
2026-05-17 19:09:30 +03:00
parent ebea196828
commit dcdc040918
47 changed files with 2456 additions and 0 deletions
+583
View File
@@ -0,0 +1,583 @@
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 = {
'вирус-самопроизвольный-запуск-брауз': `
<h2>Проверяем, что проблема решена</h2>
<p>После удаления задания перезагрузите компьютер и оставьте систему включенной на тот промежуток времени, через который раньше открывался браузер. Если окно больше не появляется, значит источник найден правильно. Если запуск повторяется, проверьте не только библиотеку планировщика, но и вкладку автозагрузки, ключи Run в реестре и расширения браузера.</p>
<ul>
<li>В планировщике смотрите не только имя задания, но и действие: путь к браузеру и URL в аргументах.</li>
<li>Удаляйте только понятные вредоносные задания. Системные задачи Microsoft лучше сначала отключить, а не стирать.</li>
<li>После очистки обновите антивирус и выполните полную проверку, потому что задание могло быть лишь следом установщика.</li>
</ul>
<p>В итоге рецепт простой: найти периодический запуск, проверить действие задания, удалить его и убедиться, что источник не восстанавливается после перезагрузки.</p>`,
'использование-jquery-в-webpack': `
<h2>Итоговая схема подключения</h2>
<p>Для старого проекта jQuery в Webpack лучше подключать явно: установить пакет, импортировать его в точке входа и, если плагинам нужна глобальная переменная, добавить ProvidePlugin. Тогда один и тот же экземпляр библиотеки будет использоваться и вашим кодом, и плагинами.</p>
<pre><code>const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery',
}),
],
};</code></pre>
<p>Если плагин всё равно не видит jQuery, проверьте порядок импортов и отсутствие второй копии библиотеки в bundle. На этом обычно все проблемы заканчиваются.</p>`,
'компиляция-64-x-разрядных-программ-на-dmd-по': `
<h2>Короткая памятка</h2>
<p>Чтобы не возвращаться к этой настройке каждый раз, держите отдельный build-скрипт для 64-битной сборки и явно указывайте архитектуру, компилятор и путь к библиотекам. Главная мысль статьи: не смешивать 32-битные OMF-библиотеки и 64-битную сборку.</p>
<pre><code>dmd -m64 main.d -ofapp.exe
dub build --arch=x86_64</code></pre>
<p>Если линковщик ругается на формат библиотек, сначала проверьте разрядность каждой внешней зависимости. В большинстве случаев ошибка не в D-коде, а в том, что рядом оказалась библиотека другого формата.</p>`,
'новый-движок-ctfe': `
<h2>Как применять выводы на практике</h2>
<p>CTFE полезен там, где результат можно посчитать один раз во время компиляции: таблицы констант, парсинг небольших DSL, генерация однотипного кода. Но не стоит превращать компиляцию в полноценный рантайм. Чем проще входные данные и чем меньше побочных эффектов, тем легче будет сопровождать проект.</p>
<p>Практическое правило такое: если вычисление зависит только от литералов и конфигурации сборки, переносите его в CTFE. Если нужен ввод-вывод, сеть или состояние окружения, оставьте это обычному коду.</p>`,
'особенности-vibe-d': `
<h2>Когда выбирать vibe.d</h2>
<p>vibe.d хорошо подходит для небольших и средних веб-сервисов на D, где важны неблокирующий ввод-вывод, простая маршрутизация и единая экосистема DUB. Начать можно с минимального HTTP-сервера, затем добавить Diet-шаблоны, MongoDB или Redis и вынести повторяющиеся части в отдельные модули.</p>
<pre><code>shared static this()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
listenHTTP(settings, &handleRequest);
}</code></pre>
<p>Если проект требует большого количества готовых интеграций, заранее проверьте наличие пакетов в DUB. Если же нужна компактная серверная часть на D, vibe.d остаётся прямым и понятным выбором.</p>`,
'ошибка-php-ssl-certificate-error-unable-to-get-local-issuer-certificate': `
<h2>Финальная проверка настройки</h2>
<p>После подключения файла сертификатов перезапустите веб-сервер или PHP-FPM и выполните тестовый HTTPS-запрос из того же окружения, где возникала ошибка. Важно проверять не браузер, а именно PHP: CLI и веб-сервер могут использовать разные php.ini.</p>
<pre><code>php -i | grep cafile
php -r "var_dump(file_get_contents('https://example.com') !== false);"</code></pre>
<p>Не отключайте проверку SSL через CURLOPT_SSL_VERIFYPEER = false как постоянное решение. Это скрывает проблему и делает соединение уязвимым. Правильный путь — актуальный CA bundle и явно указанная директива openssl.cafile.</p>`,
'о-tilix-и-d-интервью-с-геральдом-нанном': `
<h2>Что вынести из интервью</h2>
<p>Главный практический вывод прост: D может быть не только системным языком, но и удобным языком для прикладных desktop-инструментов. Tilix показывает, что связка D, GtkD и аккуратного следования HIG способна дать зрелое приложение, которым пользуются каждый день.</p>
<p>Если хочется повторить такой путь, начинайте не с большого терминала, а с маленькой GTK-утилиты: окно, настройки, несколько действий, сборка через DUB. Так быстрее станет понятно, насколько привычки из Java, Delphi или PHP мешают или помогают писать на D.</p>`,
'пишем-аналог-функции-php-preg_match_all-на-языке-прог': `
<h2>Как довести класс до рабочего состояния</h2>
<p>После реализации флагов обязательно добавьте тесты на три случая: нет совпадений, одно совпадение с группами и несколько совпадений с позициями. Именно на позициях чаще всего появляются ошибки, потому что PHP возвращает смещения в исходной строке, а не в подстроке после очередного поиска.</p>
<pre><code>assert(matchAll(r"(\w+)=(\d+)", "a=1 b=22").length == 2);
assert(matchAll(r"z+", "abc").empty);</code></pre>
<p>Дальше можно расширять API постепенно: сначала PREG_PATTERN_ORDER, затем PREG_SET_ORDER, и только после этого режим с OFFSET_CAPTURE.</p>`,
'bitrix-api-создание-добавление-торгового-пре': `
<h2>Что проверить после создания предложения</h2>
<p>После добавления торгового предложения проверьте три связи: привязку к товару, цены и остатки. В Bitrix предложение может успешно создаться, но не появиться в публичной части, если не заполнены обязательные свойства SKU или не обновлены индексы.</p>
<ul>
<li>Убедитесь, что установлен CML2_LINK или соответствующее свойство связи.</li>
<li>После записи цены вызовите обновление элемента и очистите кеш каталога.</li>
<li>Если используются склады, отдельно сохраните количество через API складского учёта.</li>
</ul>
<p>Такой порядок закрывает типичную проблему: элемент есть в админке, но покупатель его не видит.</p>`,
'bitrix-api-функция-для-генерации-кода-элемент': `
<h2>Как использовать функцию безопасно</h2>
<p>Транслитерация имени удобна, но код элемента должен оставаться уникальным. Поэтому после генерации проверяйте существующие символьные коды в инфоблоке и при совпадении добавляйте числовой суффикс.</p>
<pre><code>telefon-samsung
telefon-samsung-2
telefon-samsung-3</code></pre>
<p>Такой простой контроль избавляет от случайной перезаписи URL и от ситуаций, когда два товара получают один и тот же человекочитаемый адрес.</p>`,
'bitrix-api-add-foto-editor': `
<h2>Завершение интеграции</h2>
<p>После вывода компонента важно не забыть обработать результат формы: сохранить ID файла, проверить тип загруженного файла и права пользователя. Сам редактор решает задачу интерфейса, но безопасность и привязка к сущности остаются на стороне вашего кода.</p>
<p>Если редактор не открывается, проверьте подключение модулей main и fileman, права на загрузку и наличие административных JS-расширений на странице. После этого компонент работает стабильно и может использоваться не только в админке, но и в собственных формах.</p>`,
'dconf-2017-под-капотом-мусорщика-ди-дмитрий-ол': `
<h2>Практический вывод</h2>
<p>Статья важна не только для разработчиков компилятора. Она показывает, почему аллокатор и сборщик мусора нельзя оценивать одной фразой «GC медленный». Нужно смотреть на классы размеров, поиск пулов, стоимость маркировки и то, сколько работы выполняется во время остановки мира.</p>
<p>Для прикладного D-кода вывод такой: держите горячие циклы предсказуемыми, не создавайте лишние временные объекты в tight-loop и измеряйте паузы. Если участок действительно критичен, D позволяет локально отключить GC или заменить аллокации заранее подготовленными буферами.</p>`,
'firefox-увеличение-ожидания-загрузки-timeout-стр': `
<h2>Когда лучше не увеличивать timeout</h2>
<p>Увеличение ожидания помогает при разовых административных операциях, но не должно маскировать медленный сайт. Если обычная страница требует минуты, правильнее вынести задачу в фон: очередь, cron, прогресс-бар и отдельная проверка статуса.</p>
<p>Для phpMyAdmin, импорта и парсинга больших файлов увеличение timeout допустимо. Для публичного пользовательского сценария лучше исправлять серверную часть, иначе браузер станет ждать дольше, но пользователь всё равно получит плохой интерфейс.</p>`,
};
const articleAdditions = {
...additions,
'вирус-самопроизвольный-запуск-брауз': `
<h2>Проверяем, что проблема решена</h2>
<p>После удаления задания перезагрузите компьютер и оставьте систему включённой на тот промежуток времени, через который раньше открывался браузер. Если окно больше не появляется, значит источник найден правильно. Если запуск повторяется, проверяем не только библиотеку планировщика, но и автозагрузку, ключи Run в реестре и расширения браузера.</p>
<p>В планировщике заданий важно смотреть не только название задания. Название злоумышленник может сделать вполне безобидным: Update Service, Browser Check, Windows Helper. Смысл находится в действии. Если в действии указан путь к браузеру, а после него стоит адрес сайта, то перед нами как раз тот самый случай.</p>
<ul>
<li>Откройте свойства подозрительного задания и посмотрите вкладку «Действия».</li>
<li>Проверьте путь к программе: firefox.exe, chrome.exe, browser.exe или cmd.exe с последующим запуском браузера.</li>
<li>Проверьте аргументы: если там указан URL, рекламный домен или короткая ссылка, задание можно отключать.</li>
<li>Сначала отключите задание, перезагрузите компьютер и проверьте поведение. После проверки его уже можно удалить.</li>
</ul>
<p>Если задача появляется снова, значит в системе остался установщик или служба, которая её восстанавливает. В таком случае дополнительно смотрим «Приложения и возможности», папки автозагрузки пользователя, ключи <em>HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run</em> и <em>HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Run</em>. После очистки нужно обновить антивирусные базы и выполнить полную проверку.</p>
<p>Итог простой: в этом случае лечится не браузер, а причина его запуска. Находим периодическое задание, проверяем действие, отключаем, убеждаемся, что оно не восстановилось, и только после этого удаляем. Так проблема закрывается полностью, а не до следующей перезагрузки.</p>`,
'использование-jquery-в-webpack': `
<h2>Итоговая схема подключения</h2>
<p>Для старого проекта jQuery в Webpack лучше подключать явно: установить пакет, импортировать его в точке входа и отдельно решить вопрос с глобальными переменными. ProvidePlugin удобен, когда в модулях встречаются свободные идентификаторы <em>$</em> или <em>jQuery</em>. Но если старый плагин лезет именно в <em>window.jQuery</em>, одного ProvidePlugin может быть мало — нужно положить jQuery в <em>window</em> самостоятельно.</p>
<pre><code>import $ from 'jquery';
window.$ = $;
window.jQuery = $;</code></pre>
<p>После этого можно добавить ProvidePlugin, чтобы не писать импорт в каждом файле:</p>
<pre><code>const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
}),
],
};</code></pre>
<p>Порядок подключения имеет значение. Сначала в entry-файле импортируем jQuery и кладём его в <em>window</em>, потом импортируем старые плагины, которым нужен глобальный объект. Если плагин подключается через <em>require</em>, делаем это после установки <em>window.jQuery</em>.</p>
<pre><code>import './jquery-global';
import 'inputmask/dist/jquery.inputmask';
import './app';</code></pre>
<p>Проверка простая: в браузерной консоли должны существовать <em>window.$</em> и <em>window.jQuery</em>, а в собранном bundle не должно быть двух разных копий jQuery. Если проект новый, лучше не тащить jQuery без необходимости. Если проект старый и плагины уже написаны под jQuery, такая схема делает зависимость явной и предсказуемой.</p>`,
'компиляция-64-x-разрядных-программ-на-dmd-по': `
<h2>Короткая памятка для современных и старых сборок</h2>
<p>Главная мысль остаётся той же: нельзя смешивать 32-битные OMF-библиотеки и 64-битную сборку. В старых установках DMD под Windows это часто упиралось в ручную настройку <em>sc.ini</em>, Windows SDK и Visual Studio linker. В более новых установках официальный установщик DMD обычно делает большую часть этой настройки сам, а для нормальной работы достаточно иметь Visual Studio Build Tools или установленную Visual Studio с C++ tooling.</p>
<p>Для простой 64-битной сборки используем явную архитектуру:</p>
<pre><code>dmd -m64 main.d -of=app.exe
dub build --arch=x86_64</code></pre>
<p>Если используется старая 32-битная сборка, смотрим, какой формат объектных файлов нужен проекту. Старый OMF и MS-COFF несовместимы между собой. Для современных библиотек Windows чаще нужен MS-COFF, а для 64-битной сборки это фактически обычный путь.</p>
<ul>
<li>Проверьте, что внешние <em>.lib</em> файлы собраны под ту же архитектуру.</li>
<li>Запускайте сборку из Developer Command Prompt или x64 Native Tools Command Prompt, если компоновщик не находится.</li>
<li>Если DMD не видит linker, проверьте переменные окружения и секцию <em>Environment64</em> в <em>sc.ini</em>.</li>
<li>Если проект собирается через DUB, фиксируйте архитектуру в команде или конфигурации сборки.</li>
</ul>
<p>Типичная ошибка выглядит так: код компилируется, а линковка падает на внешней библиотеке. В большинстве случаев виноват не D-код, а то, что рядом лежит библиотека другого формата или другой разрядности. Сначала проверяем toolchain и зависимости, и только потом ищем ошибку в исходниках.</p>`,
'новый-движок-ctfe': `
<h2>Как применять выводы на практике</h2>
<p>CTFE полезен там, где результат можно посчитать один раз во время компиляции: таблицы констант, парсинг небольших DSL, генерация однотипного кода, предварительная подготовка строк и проверка инвариантов. Но не стоит превращать компиляцию в полноценный runtime. Чем проще входные данные и чем меньше побочных эффектов, тем легче будет сопровождать проект.</p>
<pre><code>enum table = buildLookupTable();
static assert(table.length == 256);</code></pre>
<p>Хороший кандидат для CTFE — функция, которая зависит только от литералов, типов и конфигурации сборки. Плохой кандидат — код, которому нужен ввод-вывод, сеть, текущее время, состояние окружения или большая внешняя база данных. Такой код лучше оставить в runtime или вынести в отдельный генератор, который запускается до сборки.</p>
<p>Для контроля качества CTFE-кода полезно держать рядом <em>static assert</em>. Он сразу показывает, что вычисление действительно произошло на этапе компиляции и вернуло ожидаемый результат. Если compile-time функция стала слишком сложной, её стоит разбить на маленькие чистые функции: так компилятору проще, и человеку проще понять, что происходит.</p>
<p>Практический вывод такой: CTFE в D — мощный инструмент, но сила его не в магии, а в переносе предсказуемых вычислений из runtime в compile-time. Используем его для констант, генерации и проверок. Не используем его как замену обычной программе.</p>`,
'особенности-vibe-d': `
<h2>Когда выбирать vibe.d</h2>
<p>vibe.d хорошо подходит для небольших и средних веб-сервисов на D, где важны неблокирующий ввод-вывод, простая маршрутизация и единая экосистема DUB. Начать можно с минимального HTTP-сервера, затем добавить Diet-шаблоны, MongoDB или Redis и вынести повторяющиеся части в отдельные модули.</p>
<pre><code>shared static this()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
listenHTTP(settings, &handleRequest);
}</code></pre>
<p>При проектировании сервиса главное не забывать, что удобный синтаксис не отменяет асинхронную модель. Долгие CPU-задачи нельзя выполнять прямо в обработчике запроса, иначе они будут мешать обработке ввода-вывода. Для таких задач лучше использовать отдельные worker-потоки, очередь или заранее подготовленные данные.</p>
<ul>
<li>HTTP и WebSocket оставляем в event loop vibe.d.</li>
<li>Тяжёлые вычисления выносим из обработчика запроса.</li>
<li>Доступ к базе оборачиваем в небольшой слой, чтобы не размазывать запросы по маршрутам.</li>
<li>Diet-шаблоны держим простыми: шаблон должен отображать данные, а не выполнять бизнес-логику.</li>
</ul>
<p>Если проект требует большого количества готовых интеграций, заранее проверьте наличие пакетов в DUB и состояние нужных драйверов. Если же нужна компактная серверная часть на D, vibe.d остаётся прямым и понятным выбором: один язык, быстрый нативный код, нормальная маршрутизация, шаблоны и асинхронный ввод-вывод.</p>`,
'ошибка-php-ssl-certificate-error-unable-to-get-local-issuer-certificate': `
<h2>Финальная проверка настройки</h2>
<p>После подключения файла сертификатов перезапустите веб-сервер или PHP-FPM и выполните тестовый HTTPS-запрос из того же окружения, где возникала ошибка. Важно проверять не браузер, а именно PHP: CLI и веб-сервер могут использовать разные <em>php.ini</em>.</p>
<pre><code>php --ini
php -i | grep -E "curl.cainfo|openssl.cafile"
php -r "var_dump(file_get_contents('https://example.com') !== false);"</code></pre>
<p>Для cURL указывается <em>curl.cainfo</em>, для потоков OpenSSL — <em>openssl.cafile</em>. На Windows путь лучше писать полностью и без относительных директорий. Если используется не глобальный <em>php.ini</em>, а отдельная настройка клиента, можно передать CA bundle прямо в cURL:</p>
<pre><code>curl_setopt($ch, CURLOPT_CAINFO, 'C:\\php\\extras\\ssl\\cacert.pem');</code></pre>
<p>Не отключайте проверку SSL через <em>CURLOPT_SSL_VERIFYPEER = false</em> как постоянное решение. Это скрывает проблему и делает соединение уязвимым. Такая настройка допустима только для короткой диагностики в локальной среде, и после проверки её нужно вернуть обратно.</p>
<p>Если CA bundle указан верно, но ошибка осталась, проверьте цепочку сертификатов на стороне удалённого сервера. Иногда браузер открывает сайт, потому что умеет достраивать цепочку, а PHP/cURL получает неполную цепочку и честно падает. В таком случае исправлять нужно сертификаты сервера, а не PHP-код.</p>
<p>Правильный итог: актуальный <em>cacert.pem</em>, явно указанные <em>curl.cainfo</em> и <em>openssl.cafile</em>, перезапуск PHP и тест именно из того окружения, где работает приложение.</p>`,
'о-tilix-и-d-интервью-с-геральдом-нанном': `
<h2>Что вынести из интервью</h2>
<p>Главный практический вывод прост: D может быть не только системным языком, но и удобным языком для прикладных desktop-инструментов. Tilix показывает, что связка D, GtkD и аккуратного следования GNOME HIG способна дать зрелое приложение, которым пользуются каждый день.</p>
<p>В интервью хорошо видно, что выбор языка был не религиозным, а практическим. Автору не хотелось писать GUI на C или C++, Python оказался неудобен для крупного статически проверяемого приложения, а D дал знакомую после Java модель, нативную скорость и нормальные GTK-привязки. Это важный критерий выбора технологии: не «самый модный язык», а язык, на котором конкретный автор может быстро и спокойно делать продукт.</p>
<p>Если хочется повторить такой путь, начинать стоит не с большого терминала, а с маленькой GTK-утилиты: окно, настройки, несколько действий, сборка через DUB, упаковка и обновления. На таком проекте сразу проявятся реальные вопросы: работа с GtkD, структура проекта, взаимодействие с C-библиотеками, обработка событий и паузы GC.</p>
<ul>
<li>Для GUI-кода держите бизнес-логику отдельно от виджетов.</li>
<li>Не бойтесь GC, но измеряйте горячие участки и большие циклы.</li>
<li>Если нужен C API, сначала сделайте маленький прототип вызова.</li>
<li>Сначала исправляйте ошибки, потом добавляйте функции — именно так Tilix сохранил управляемый backlog.</li>
</ul>
<p>Именно поэтому интервью полезно не только поклонникам D. Оно показывает нормальный инженерный путь: выбрать задачу, подобрать инструмент, сделать небольшую работающую версию, а затем постепенно доводить приложение до зрелого состояния.</p>`,
'пишем-аналог-функции-php-preg_match_all-на-языке-прог': `
<h2>Как довести класс до рабочего состояния</h2>
<p>После реализации флагов обязательно добавьте тесты на три случая: нет совпадений, одно совпадение с группами и несколько совпадений с позициями. Именно на позициях чаще всего появляются ошибки, потому что PHP возвращает смещения в исходной строке, а не в подстроке после очередного поиска.</p>
<pre><code>assert(matchAll(r"(\w+)=(\d+)", "a=1 b=22").length == 2);
assert(matchAll(r"z+", "abc").empty);</code></pre>
<p>В D стоит явно решить, какой формат результата нужен. PHP поддерживает несколько режимов: <em>PREG_PATTERN_ORDER</em>, <em>PREG_SET_ORDER</em> и вариант с <em>PREG_OFFSET_CAPTURE</em>. Если пытаться сделать всё сразу, код быстро станет запутанным. Лучше сначала реализовать простой режим, затем режим группировки по совпадениям, и только после этого добавить позиции.</p>
<pre><code>struct MatchPart
{
string text;
ptrdiff_t offset;
}
alias MatchSet = MatchPart[][];</code></pre>
<p>Отдельно проверьте Unicode. Если шаблон и строка содержат кириллицу, важно понимать, что именно возвращается в offset: позиция в байтах или позиция в символах. PHP для <em>PREG_OFFSET_CAPTURE</em> возвращает байтовое смещение. Если в D вы хотите повторить поведение PHP, нужно документировать именно байтовый offset, иначе результаты будут отличаться.</p>
<p>Финальный класс должен иметь маленькую поверхность API: метод без offset, метод с offset и понятный enum для порядка результата. Тогда аналог <em>preg_match_all</em> будет не просто копией PHP-функции, а удобным D-инструментом, который предсказуемо работает в тестах.</p>`,
'bitrix-api-создание-добавление-торгового-пре': `
<h2>Что проверить после создания предложения</h2>
<p>После добавления торгового предложения проверьте три связи: привязку к товару, цены и остатки. В Bitrix предложение может успешно создаться, но не появиться в публичной части, если не заполнены обязательные свойства SKU, не сохранён товарный каталог или не обновлены кеши.</p>
<ul>
<li>Убедитесь, что модуль <em>iblock</em> подключён перед созданием элемента.</li>
<li>Если задаются цена и складской остаток, подключите модуль <em>catalog</em>.</li>
<li>Проверьте свойство связи с товаром, чаще всего это <em>CML2_LINK</em> или настроенное в конкретном каталоге свойство SKU.</li>
<li>После записи цены и остатков очистите кеш компонента каталога.</li>
</ul>
<p>Минимальная последовательность такая: создать элемент торгового предложения в инфоблоке SKU, записать связь с товаром, сохранить параметры товара, установить цену, установить количество. Если хотя бы один шаг пропущен, предложение может быть видно в админке, но не попадёт в публичный каталог.</p>
<pre><code>CModule::IncludeModule('iblock');
CModule::IncludeModule('catalog');
$offerId = $el->Add($fields);
CCatalogProduct::Add([
'ID' => $offerId,
'QUANTITY' => 10,
]);
CPrice::SetBasePrice($offerId, 990, 'RUB');</code></pre>
<p>На боевом проекте обязательно логируйте результат <em>Add</em> и текст ошибки <em>LAST_ERROR</em>. Bitrix часто молча возвращает <em>false</em>, а настоящая причина находится именно там: обязательное поле, неправильный инфоблок, неверный код свойства или недостаточные права.</p>`,
'bitrix-api-функция-для-генерации-кода-элемент': `
<h2>Как использовать функцию безопасно</h2>
<p>Транслитерация имени удобна, но код элемента должен оставаться уникальным. Поэтому после генерации проверяйте существующие символьные коды в инфоблоке и при совпадении добавляйте числовой суффикс. Иначе два товара с похожим названием могут получить один URL.</p>
<pre><code>telefon-samsung
telefon-samsung-2
telefon-samsung-3</code></pre>
<p>Также стоит сразу нормализовать результат: привести к нижнему регистру, заменить пробелы на дефис, убрать повторяющиеся дефисы и обрезать дефис в начале или конце строки. Это мелочь, но именно такие мелочи потом портят адреса страниц.</p>
<pre><code>$code = CUtil::translit($name, 'ru', [
'replace_space' => '-',
'replace_other' => '-',
'change_case' => 'L',
]);
$code = trim(preg_replace('/-+/', '-', $code), '-');</code></pre>
<p>Проверку уникальности лучше делать в том же инфоблоке, где будет создан элемент. Если сайт многоязычный или есть разделы с одинаковыми товарами, заранее решите правило: уникальность на весь инфоблок или только внутри раздела. Для SEO обычно проще и надёжнее уникальность на весь инфоблок.</p>
<p>В итоге функция должна не просто транслитерировать строку, а возвращать готовый символьный код: чистый, нижнего регистра, без мусора и без совпадений с уже существующими элементами.</p>`,
'bitrix-api-add-foto-editor': `
<h2>Завершение интеграции</h2>
<p>После вывода компонента важно не забыть обработать результат формы: сохранить ID файла, проверить тип загруженного файла и права пользователя. Сам редактор решает задачу интерфейса, но безопасность и привязка к сущности остаются на стороне вашего кода.</p>
<p>В параметрах компонента отдельно проверьте <em>allowUpload</em>, <em>medialib</em>, <em>fileDialog</em>, <em>cloud</em>, <em>delete</em>, <em>edit</em> и <em>maxCount</em>. Не все параметры будут иметь эффект в любой установке Bitrix: часть зависит от подключённых модулей, прав пользователя и контекста страницы.</p>
<pre><code>$fileId = (int)$_POST['picture'];
if ($fileId > 0) {
$file = CFile::GetFileArray($fileId);
if ($file && str_starts_with($file['CONTENT_TYPE'], 'image/')) {
// сохраняем ID картинки в своей сущности
}
}</code></pre>
<p>Если редактор не открывается, проверьте подключение модулей <em>main</em> и <em>fileman</em>, права на загрузку, административные JS-расширения и наличие сессии пользователя. В публичной части сайта проблема часто оказывается не в <em>FileInput</em>, а в том, что на странице не подключены нужные скрипты Bitrix.</p>
<p>Рабочая схема такая: выводим FileInput, разрешаем только картинки, ограничиваем количество, после отправки формы валидируем ID файла и сохраняем его в свою сущность. Тогда встроенный редактор становится нормальной частью формы, а не просто красивой кнопкой загрузки.</p>`,
'dconf-2017-под-капотом-мусорщика-ди-дмитрий-ол': `
<h2>Практический вывод</h2>
<p>Статья важна не только для разработчиков компилятора. Она показывает, почему аллокатор и сборщик мусора нельзя оценивать одной фразой «GC медленный». Нужно смотреть на классы размеров, поиск пулов, стоимость маркировки и то, сколько работы выполняется во время остановки мира.</p>
<p>Для прикладного D-кода вывод такой: держите горячие циклы предсказуемыми, не создавайте лишние временные объекты в tight-loop и измеряйте паузы. Если участок действительно критичен, D позволяет локально управлять GC и заменить поток мелких аллокаций заранее подготовленными буферами.</p>
<pre><code>import core.memory : GC;
GC.disable();
scope(exit) GC.enable();
// горячий участок без лишних временных объектов</code></pre>
<p>Отключать GC нужно аккуратно. Это не волшебная оптимизация, а договор: пока сборщик выключен, программа не должна бесконтрольно накапливать мусор. Поэтому такой приём подходит для короткого измеримого участка, а не для всего приложения.</p>
<ul>
<li>Сначала измеряем: время, количество аллокаций, паузы.</li>
<li>Потом убираем лишние временные объекты и повторные выделения.</li>
<li>Только затем используем ручное управление GC, если оно действительно помогает.</li>
<li>После оптимизации оставляем комментарий, почему здесь нельзя писать «просто как обычно».</li>
</ul>
<p>Главный смысл доклада в том, что производительность памяти — это архитектура. Если в программе много мелких объектов, непредсказуемых ссылок и больших пауз, проблема редко решается одной настройкой. Нужно менять модель данных, жизненный цикл объектов и место, где выделяется память.</p>`,
'firefox-увеличение-ожидания-загрузки-timeout-стр': `
<h2>Когда лучше не увеличивать timeout</h2>
<p>Увеличение ожидания помогает при разовых административных операциях, но не должно маскировать медленный сайт. Если обычная страница требует минуты, правильнее вынести задачу в фон: очередь, cron, прогресс-бар и отдельная проверка статуса.</p>
<p>Важно не путать разные таймауты. Ожидание HTTP-ответа — это одно, долго выполняющийся JavaScript на странице — другое, а таймауты прокси или веб-сервера — третье. Если Firefox ждёт дольше, но nginx, Apache, PHP-FPM или upstream уже оборвал соединение, настройка браузера не поможет.</p>
<ul>
<li>Для долгого ответа сервера проверяйте HTTP timeout в браузере и серверные таймауты.</li>
<li>Для долгого JavaScript смотрите предупреждения о зависшем скрипте и настройки выполнения JS.</li>
<li>Для импорта больших файлов лучше использовать фоновую задачу и страницу прогресса.</li>
<li>Для phpMyAdmin и разовых административных операций увеличение ожидания допустимо.</li>
</ul>
<p>Если параметра в <em>about:config</em> нет, его можно создать вручную только тогда, когда вы понимаете, какая версия Firefox и какой именно timeout вам нужен. После завершения работы лучше вернуть настройку к обычному значению, чтобы браузер не висел слишком долго на реально недоступных сайтах.</p>
<p>Итог: увеличивать timeout можно как временный инструмент администратора. Для пользовательского сценария правильнее исправлять серверную часть, потому что долгий белый экран остаётся плохим интерфейсом даже тогда, когда браузер согласен ждать.</p>`,
};
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;');
}
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 id="more-\d+"><\/span>/g, '')
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<style[\s\S]*?<\/style>/gi, '');
content = content.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_, code) => {
const text = decodeHtml(code.replace(/<[^>]+>/g, ''));
return `<pre><code>${escapeHtml(text.trim())}</code></pre>`;
});
content = content.replace(/<img\b[^>]*>/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 `<figure class="article-media"><img src="${imageSrc}" alt="${escapeHtml(alt)}" /><figcaption>${escapeHtml(defaultAlt)}</figcaption></figure>`;
});
content = content
.replace(/\s+(srcset|sizes|width|height|class)=["'][^"']*["']/gi, '')
.replace(/<p>\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 `<text ${attrs}>${escapeHtml(body)}</text>`;
}
function rect(x, y, width, height, fill, stroke = '#263241', radius = 10) {
return `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="${radius}" fill="${fill}" stroke="${stroke}" stroke-width="2"/>`;
}
function baseSvg(width, height, title, subtitle, accent, body) {
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeHtml(title)}">
<rect width="${width}" height="${height}" fill="#f7f4ed"/>
<rect x="24" y="24" width="${width - 48}" height="${height - 48}" rx="18" fill="#ffffff" stroke="#242424" stroke-width="2"/>
<rect x="24" y="24" width="${width - 48}" height="14" rx="7" fill="${accent}"/>
${text(56, 82, title, { size: 30, weight: 700, fill: '#1f2933' })}
${subtitle ? text(56, 116, subtitle, { size: 16, fill: '#52606d' }) : ''}
${body}
</svg>`;
}
function diagramAsset(title, subtitle, accent, labels) {
const items = labels.map((item, index) => {
const x = 64 + index * 178;
const arrow = index < labels.length - 1 ? `<path d="M${x + 132} 248h44" stroke="#687789" stroke-width="4" stroke-linecap="round"/><path d="M${x + 168} 238l16 10-16 10" fill="none" stroke="#687789" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>` : '';
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)}
<circle cx="92" cy="164" r="6" fill="#ef4444"/><circle cx="114" cy="164" r="6" fill="#f59e0b"/><circle cx="136" cy="164" r="6" fill="#22c55e"/>
${code}
`);
}
function featureCard(title, subtitle, accent, rows) {
const body = rows.map((row, index) => {
const y = 168 + index * 58;
return `<circle cx="90" cy="${y - 8}" r="14" fill="${accent}"/><path d="M83 ${y - 8}l5 6 11-14" fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>${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)}
<rect x="66" y="130" width="908" height="44" rx="12" fill="#343b46"/>
${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) => `<rect x="${164 + j * 58}" y="${y + 7}" width="${38 + (j % 3) * 5}" height="18" rx="3" fill="${j % 2 ? '#14b8a6' : '#0f766e'}"/>`).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) => `<rect x="${108 + i * 38}" y="323" width="24" height="32" rx="4" fill="${i < 11 ? '#fb923c' : '#cbd5e1'}"/>`).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)}<path d="M160 494l132-150 102 110 70-76 116 116z" fill="#93c5fd"/><circle cx="484" cy="286" r="46" fill="#fde68a"/>${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`);