first commit

This commit is contained in:
Alex 2025-06-15 00:02:46 +03:00
commit e9449fb36b
87 changed files with 10630 additions and 0 deletions

View File

@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.toml]
indent_size = 4

46
webpunk-templates/.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# testing
/coverage
# production
/ssl
/build
/dist
/dist-ssr
/temp
dist.zip
*.local
# misc
.DS_Store
Thumbs.db
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
.env
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -0,0 +1,6 @@
# Lockfiles
package-lock.json
pnpm-lock.yaml
# vendor
vendor/

View File

@ -0,0 +1,14 @@
{
"trailingComma": "none",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"overrides": [
{
"files": ["*.css", "*.less", "*.sass", "*.scss"],
"options": {
"singleQuote": false
}
}
]
}

View File

@ -0,0 +1,264 @@
{
"rules": {
"order/order": ["custom-properties", "declarations"],
"order/properties-order": [
"content",
"counter-reset",
"counter-increment",
"all",
"position",
"top",
"right",
"bottom",
"left",
"inset",
"inset-block-start",
"inset-block-end",
"inset-inline-start",
"inset-inline-end",
"z-index",
"display",
"grid-template",
"grid-template-rows",
"grid-template-columns",
"grid-template-areas",
"grid-auto-rows",
"grid-auto-columns",
"grid-auto-flow",
"grid-area",
"grid-row",
"grid-column",
"grid-row-start",
"grid-row-end",
"grid-column-start",
"grid-column-end",
"flex",
"flex-grow",
"flex-shrink",
"flex-basis",
"flex-flow",
"flex-direction",
"flex-wrap",
"order",
"place-items",
"justify-content",
"justify-items",
"justify-self",
"align-content",
"align-items",
"align-self",
"grid-gap",
"gap",
"grid-row-gap",
"row-gap",
"grid-column-gap",
"column-gap",
"float",
"clear",
"box-sizing",
"writing-mode",
"width",
"height",
"max-width",
"max-height",
"min-width",
"min-height",
"aspect-ratio",
"inline-size",
"max-inline-size",
"min-inline-size",
"block-size",
"max-block-size",
"min-block-size",
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"overflow",
"overflow-x",
"overflow-y",
"font",
"font-family",
"font-size",
"line-height",
"font-weight",
"font-style",
"text-align",
"text-align-last",
"vertical-align",
"color",
"font-display",
"font-variant",
"font-size-adjust",
"font-stretch",
"font-effect",
"font-emphasize",
"font-emphasize-position",
"font-emphasize-style",
"font-smooth",
"letter-spacing",
"white-space",
"text-transform",
"text-decoration",
"text-emphasis",
"text-emphasis-color",
"text-emphasis-style",
"text-emphasis-position",
"text-indent",
"text-justify",
"text-outline",
"text-wrap",
"text-overflow",
"text-overflow-ellipsis",
"text-overflow-mode",
"text-orientation",
"text-shadow",
"word-wrap",
"word-break",
"word-spacing",
"overflow-wrap",
"tab-size",
"hyphens",
"direction",
"unicode-bidi",
"columns",
"column-count",
"column-fill",
"column-rule",
"column-rule-color",
"column-rule-style",
"column-rule-width",
"column-span",
"column-width",
"src",
"page-break-after",
"page-break-before",
"page-break-inside",
"list-style",
"list-style-position",
"list-style-type",
"list-style-image",
"table-layout",
"empty-cells",
"caption-side",
"background",
"background-color",
"background-image",
"background-repeat",
"background-size",
"background-position",
"background-position-x",
"background-position-y",
"background-clip",
"background-origin",
"background-attachment",
"background-blend-mode",
"box-decoration-break",
"object-fit",
"border",
"border-width",
"border-style",
"border-color",
"border-top",
"border-block-start",
"border-top-width",
"border-top-style",
"border-top-color",
"border-right",
"border-inline-end",
"border-right-width",
"border-right-style",
"border-right-color",
"border-bottom",
"border-block-end",
"border-bottom-width",
"border-bottom-style",
"border-bottom-color",
"border-left",
"border-inline-start",
"border-left-width",
"border-left-style",
"border-left-color",
"border-radius",
"border-top-left-radius",
"border-top-right-radius",
"border-bottom-right-radius",
"border-bottom-left-radius",
"border-image",
"border-image-source",
"border-image-slice",
"border-image-width",
"border-image-outset",
"border-image-repeat",
"border-collapse",
"border-spacing",
"outline",
"outline-width",
"outline-style",
"outline-color",
"outline-offset",
"box-shadow",
"visibility",
"cursor",
"mix-blend-mode",
"backdrop-filter",
"will-change",
"transform",
"transform-origin",
"transform-style",
"translate",
"rotate",
"scale",
"backface-visibility",
"opacity",
"filter",
"perspective",
"perspective-origin",
"transition",
"transition-delay",
"transition-timing-function",
"transition-duration",
"transition-property",
"animation",
"animation-name",
"animation-duration",
"animation-play-state",
"animation-timing-function",
"animation-delay",
"animation-iteration-count",
"animation-direction",
"animation-fill-mode",
"appearance",
"clip",
"clip-path",
"resize",
"user-select",
"nav-index",
"nav-up",
"nav-right",
"nav-down",
"nav-left",
"pointer-events",
"quotes",
"touch-action",
"zoom",
"fill",
"fill-rule",
"clip-rule",
"stroke",
"stroke-width"
]
}
}

View File

@ -0,0 +1 @@
vendor/

View File

@ -0,0 +1,64 @@
{
"extends": ["stylelint-config-standard-scss", "./.stylelint-order.json"],
"plugins": ["stylelint-prettier", "stylelint-order"],
"overrides": [
{
"files": ["**/*.scss"],
"customSyntax": "postcss-scss"
}
],
"rules": {
"at-rule-empty-line-before": [
"always",
{
"except": ["blockless-after-blockless", "inside-block"],
"ignore": ["after-comment"],
"ignoreAtRules": ["else"]
}
],
"at-rule-no-unknown": [
true,
{
"ignoreAtRules": [
"mixin",
"define-mixin",
"include",
"content",
"rules",
"extend",
"use",
"if",
"for"
]
}
],
"block-no-empty": true,
"color-hex-length": "long",
"declaration-no-important": true,
"declaration-empty-line-before": "never",
"declaration-block-no-redundant-longhand-properties": [
true,
{
"ignoreShorthands": ["columns", "grid-template", "flex-flow"]
}
],
"font-family-name-quotes": "always-unless-keyword",
"no-irregular-whitespace": null,
"no-descending-specificity": null,
"rule-empty-line-before": "never",
"selector-class-pattern": "",
"shorthand-property-no-redundant-values": null,
"scss/dollar-variable-colon-space-after": "at-least-one-space",
"scss/dollar-variable-empty-line-before": null,
"scss/double-slash-comment-empty-line-before": [
"always",
{
"except": ["inside-block"],
"ignore": ["between-comments"]
}
],
"scss/no-global-function-names": null,
"scss/comment-no-empty": null,
"scss/at-import-partial-extension": null
}
}

View File

@ -0,0 +1,19 @@
import globals from 'globals'
import pluginJs from '@eslint/js'
/** @type {import('eslint').Linter.Config[]} */
export default [
{
ignores: [
"**/settings.js", "**/ssl-manager.js", "**/settings.json", "**/*.mjs"
]
},
{
files: ['**/*.js'],
languageOptions: { sourceType: 'script' }
},
{
languageOptions: { globals: globals.browser }
},
pluginJs.configs.recommended
]

View File

@ -0,0 +1,856 @@
import autoprefixer from 'autoprefixer'
import concat from 'gulp-concat'
import csso from 'gulp-csso'
import { deleteAsync } from 'del'
import gulp from 'gulp'
import gulpWebp from 'gulp-webp'
import imagemin, { mozjpeg, optipng, svgo } from 'gulp-imagemin'
import imageminAvif from 'imagemin-avif'
import order from 'gulp-order'
import plumber from 'gulp-plumber'
import postcss from 'gulp-postcss'
import pug from 'gulp-pug'
import rename from 'gulp-rename'
import uglify from 'gulp-uglify-es'
import * as dartSass from 'sass'
import gulpSass from 'gulp-sass'
import sourcemap from 'gulp-sourcemaps'
import svgstore from 'gulp-svgstore'
import sync from 'browser-sync'
import { nanoid } from 'nanoid'
import postcssUrl from 'postcss-url'
import inject from 'gulp-inject'
import newer from 'gulp-newer'
import twig from 'gulp-twig'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { execSync } from 'child_process'
import dotenv from 'dotenv'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Загружаем переменные окружения из .env.local
const envPath = path.join(__dirname, '.env.local')
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath })
console.log('📝 Загружены настройки из .env.local')
} else {
console.log('⚠️ Файл .env.local не найден. Запустите: gulp configInit')
}
const baseUrl = '.'
const pathCss = '..'
const searchId = Date.now()
// Конфигурация для разных шаблонизаторов
const settings = {
templateEngine: process.env.TEMPLATE_ENGINE || 'pug', // 'pug' или 'twig'
dataPath: 'src/data', // путь к JSON файлам с данными
https: {
enabled: process.env.HTTPS === 'true',
keyPath: 'ssl/server.key',
certPath: 'ssl/server.crt'
},
server: {
port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000,
host: process.env.HOST || 'localhost',
open: process.env.OPEN_BROWSER === 'true',
notify: process.env.BROWSER_SYNC_NOTIFY === 'true'
}
}
// Выводим текущие настройки при запуске
console.log('⚙️ Текущие настройки:')
console.log(` Шаблонизатор: ${settings.templateEngine}`)
console.log(` HTTPS: ${settings.https.enabled ? 'включен' : 'выключен'}`)
console.log(` Порт: ${settings.server.port}`)
console.log(` Хост: ${settings.server.host}`)
console.log(` Автооткрытие браузера: ${settings.server.open ? 'да' : 'нет'}`)
console.log(` Уведомления: ${settings.server.notify ? 'да' : 'нет'}`)
// Функция для загрузки данных из JSON файлов
const loadData = () => {
const dataPath = path.join(__dirname, settings.dataPath)
let data = {}
if (fs.existsSync(dataPath)) {
const files = fs.readdirSync(dataPath).filter(file => file.endsWith('.json'))
files.forEach(file => {
const fileName = path.basename(file, '.json')
const filePath = path.join(dataPath, file)
try {
const fileContent = fs.readFileSync(filePath, 'utf8')
data[fileName] = JSON.parse(fileContent)
} catch (error) {
console.error(`Ошибка загрузки данных из ${file}:`, error.message)
}
})
} else {
console.log(`📁 Папка с данными не найдена: ${dataPath}`)
console.log('💡 Запустите: gulp data для создания примера данных')
}
return data
}
const getImageSrcset = (path, format, params = '') => {
return [1, 2]
.map((dpr) => `${baseUrl}${path}@${dpr}x.${format}${params} ${dpr}x`)
.join(', ')
}
const getImageMime = (format) => {
switch (format) {
case 'avif':
return 'image/avif'
case 'webp':
return 'image/webp'
case 'jpeg':
case 'jpg':
return 'image/jpeg'
case 'png':
return 'image/png'
default:
return null
}
}
const sass = gulpSass(dartSass)
const clean = async () => {
return await deleteAsync(['dist'])
}
const copy = () => {
return gulp
.src(
[
'src/fonts/**/*.{woff,woff2}',
'src/img/**/*.{webm,webp,avif,jpg,jpeg,png,svg}',
'src/favicon/**/*',
'src/favicon.ico',
'src/settings.js',
'src/urls.json',
'src/robots.txt'
],
{ base: 'src', encoding: false }
)
.pipe(gulp.dest('dist'))
}
const css = () => {
return gulp
.src('src/scss/index.scss')
.pipe(plumber())
.pipe(sourcemap.init())
.pipe(sass.sync().on('error', sass.logError))
.pipe(
postcss([
postcssUrl({
url: (asset) => {
if (asset.url.startsWith('/')) {
return `${pathCss}${asset.url}?v=${searchId}`
}
return asset.url
}
}),
autoprefixer({ remove: false })
])
)
.pipe(rename('style.css'))
.pipe(sourcemap.write('.'))
.pipe(gulp.dest('dist/css'))
.pipe(sync.stream())
.pipe(csso())
.pipe(rename('style.min.css'))
.pipe(gulp.dest('dist/css'))
}
const images = () => {
return gulp
.src('src/img/**/*.{png,jpg,jpeg,svg}', { base: 'src', encoding: false })
.pipe(newer('dist'))
.pipe(
imagemin(
[
optipng({ optimizationLevel: 5 }),
mozjpeg({ quality: 85, progressive: true }),
svgo({
plugins: [
{ name: 'removeViewBox', active: false },
{ name: 'removeXMLNS', active: false },
{ name: 'removeUnknownsAndDefaults', active: false }
]
})
],
{ silent: true }
)
)
.pipe(gulp.dest('dist'))
}
const webp = () => {
return gulp
.src('src/img/**/*.{png,jpg,jpeg}', { base: 'src', encoding: false })
.pipe(newer({ dest: 'dist', ext: '.webp' }))
.pipe(gulpWebp({ quality: 85 }))
.pipe(gulp.dest('dist'))
}
const avif = () => {
return gulp
.src('src/img/**/*.{png,jpg,jpeg}', { base: 'src', encoding: false })
.pipe(newer({ dest: 'dist', ext: '.avif' }))
.pipe(imagemin([imageminAvif({ quality: 60 })], { silent: true }))
.pipe(rename((path) => (path.extname = '.avif')))
.pipe(gulp.dest('dist'))
}
const processImages = gulp.parallel(images, webp, avif)
const sprite = () => {
return gulp
.src('src/icons/**/*.svg')
.pipe(svgstore({ inlineSvg: true }))
.pipe(rename('sprite.svg'))
.pipe(gulp.dest('dist/img'))
}
const injectSprite = () => {
const spritePath = 'dist/img/sprite.svg'
// Проверяем, существует ли файл спрайта
if (!fs.existsSync(spritePath)) {
console.log('⚠️ Спрайт не найден, пропускаем инъекцию')
return Promise.resolve()
}
return gulp
.src('dist/*.html')
.pipe(
inject(gulp.src(spritePath), {
transform: (_, file) => {
return file.contents.toString()
}
})
)
.pipe(gulp.dest('dist'))
}
// Инъекция спрайта только в конкретный файл
const injectSpriteToFile = (htmlFile) => {
const spritePath = 'dist/img/sprite.svg'
if (!fs.existsSync(spritePath) || !fs.existsSync(htmlFile)) {
return Promise.resolve()
}
return gulp
.src(htmlFile)
.pipe(
inject(gulp.src(spritePath), {
transform: (_, file) => {
return file.contents.toString()
}
})
)
.pipe(gulp.dest('dist'))
}
const jsCommon = () => {
return gulp
.src('src/js/common/*.js')
.pipe(
plumber({
errorHandler(err) {
console.error('JS Common Error:', err.toString())
this.emit('end')
}
})
)
.pipe(order(['_utils.js', '*.js']))
.pipe(sourcemap.init())
.pipe(concat('script.js'))
.pipe(sourcemap.write('.'))
.pipe(gulp.dest('dist/js'))
}
const jsCommonMin = () => {
return gulp
.src('src/js/common/*.js')
.pipe(
plumber({
errorHandler(err) {
console.error('JS Common Min Error:', err.toString())
this.emit('end')
}
})
)
.pipe(order(['_utils.js', '*.js']))
.pipe(concat('script.min.js'))
.pipe(uglify.default())
.pipe(gulp.dest('dist/js'))
}
const jsVendor = () => {
return gulp
.src('src/js/vendor/*.js')
.pipe(
plumber({
errorHandler(err) {
console.error('JS Vendor Error:', err.toString())
this.emit('end')
}
})
)
.pipe(concat('vendor.js'))
.pipe(gulp.dest('dist/js'))
}
const jsVendorMin = () => {
return gulp
.src('src/js/vendor/*.js')
.pipe(
plumber({
errorHandler(err) {
console.error('JS Vendor Min Error:', err.toString())
this.emit('end')
}
})
)
.pipe(concat('vendor.min.js'))
.pipe(uglify.default())
.pipe(gulp.dest('dist/js'))
}
const js = gulp.parallel(jsCommon, jsCommonMin, jsVendor, jsVendorMin)
// Простая обработка Pug шаблонов
const htmlPug = () => {
const data = loadData()
return gulp
.src('src/pug/pages/**/*.pug')
.pipe(plumber({
errorHandler(err) {
console.error('Pug Error:', err.toString())
this.emit('end')
}
}))
.pipe(
pug({
pretty: true,
basedir: 'src/pug',
locals: {
baseUrl,
searchId,
getSrcset: getImageSrcset,
getMime: getImageMime,
...data
}
})
)
.pipe(gulp.dest('dist'))
}
// Простая обработка Twig шаблонов
const htmlTwig = () => {
const data = loadData()
return gulp
.src('src/twig/pages/**/*.twig')
.pipe(plumber({
errorHandler(err) {
console.error('Twig Error:', err.toString())
this.emit('end')
}
}))
.pipe(
twig({
base: 'src/twig',
data: {
baseUrl,
searchId,
getSrcset: getImageSrcset,
getMime: getImageMime,
...data
},
functions: [
{
name: 'getSrcset',
func: getImageSrcset
},
{
name: 'getMime',
func: getImageMime
}
]
})
)
.pipe(gulp.dest('dist'))
}
// Pug с инкрементальной сборкой
const htmlPugIncremental = () => {
const data = loadData()
return gulp
.src('src/pug/pages/**/*.pug')
.pipe(plumber({
errorHandler(err) {
console.error('Pug Error:', err.toString())
this.emit('end')
}
}))
.pipe(newer({
dest: 'dist',
ext: '.html'
}))
.pipe(
pug({
pretty: true,
basedir: 'src/pug',
locals: {
baseUrl,
searchId,
getSrcset: getImageSrcset,
getMime: getImageMime,
...data
}
})
)
.pipe(gulp.dest('dist'))
.on('end', () => {
console.log('📄 Обработаны только измененные Pug файлы')
})
}
// Twig с инкрементальной сборкой
const htmlTwigIncremental = () => {
const data = loadData()
return gulp
.src('src/twig/pages/**/*.twig')
.pipe(plumber({
errorHandler(err) {
console.error('Twig Error:', err.toString())
this.emit('end')
}
}))
.pipe(newer({
dest: 'dist',
ext: '.html'
}))
.pipe(
twig({
base: 'src/twig',
data: {
baseUrl,
searchId,
getSrcset: getImageSrcset,
getMime: getImageMime,
...data
},
functions: [
{
name: 'getSrcset',
func: getImageSrcset
},
{
name: 'getMime',
func: getImageMime
}
]
})
)
.pipe(gulp.dest('dist'))
.on('end', () => {
console.log('📄 Обработаны только измененные Twig файлы')
})
}
// Универсальная функция для обработки HTML
const html = (incremental = false) => {
if (settings.templateEngine === 'twig') {
return incremental ? htmlTwigIncremental() : htmlTwig()
} else {
return incremental ? htmlPugIncremental() : htmlPug()
}
}
// Создание SSL сертификатов для разработки
const createSSLCerts = (done) => {
const sslDir = path.join(__dirname, 'ssl')
const keyPath = path.join(sslDir, 'server.key')
const certPath = path.join(sslDir, 'server.crt')
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
console.log('✅ SSL сертификаты уже существуют')
done()
return
}
if (!fs.existsSync(sslDir)) {
fs.mkdirSync(sslDir, { recursive: true })
}
try {
console.log('🔐 Создание SSL сертификатов для разработки...')
execSync(`openssl genrsa -out ${keyPath} 2048`, { stdio: 'pipe' })
const certCommand = `openssl req -new -x509 -key ${keyPath} -out ${certPath} -days 365 -subj "/C=RU/ST=Local/L=Local/O=Development/OU=Dev/CN=localhost"`
execSync(certCommand, { stdio: 'pipe' })
console.log('✅ SSL сертификаты успешно созданы')
console.log('⚠️ Это сертификаты только для разработки!')
} catch (error) {
console.error('❌ Ошибка создания SSL сертификатов:')
console.error('Убедитесь что OpenSSL установлен в системе')
}
done()
}
// Создание конфигурационного файла
const createHTTPSConfig = (done) => {
const configPath = path.join(__dirname, '.env.local')
if (!fs.existsSync(configPath)) {
const envContent = `# Конфигурация для разработки
# Этот файл автоматически загружается при запуске gulp
# ===== ОСНОВНЫЕ НАСТРОЙКИ =====
# Включить HTTPS (true/false)
# После изменения нужно перезапустить gulp
HTTPS=false
# Шаблонизатор (pug/twig)
# После изменения нужно перезапустить gulp
TEMPLATE_ENGINE=pug
# ===== НАСТРОЙКИ СЕРВЕРА =====
# Порт для разработки
PORT=3000
# Хост для разработки (localhost, 127.0.0.1, 0.0.0.0 для доступа извне)
HOST=localhost
# Автоматически открывать браузер при запуске (true/false)
OPEN_BROWSER=false
# Показывать уведомления BrowserSync (true/false)
BROWSER_SYNC_NOTIFY=false
`
fs.writeFileSync(configPath, envContent)
console.log('✅ Создан файл конфигурации: .env.local')
console.log('📝 Теперь вы можете настроить параметры в .env.local')
} else {
console.log(' Файл .env.local уже существует')
}
done()
}
// Функция для перезагрузки конфигурации
const reloadConfig = (done) => {
const envPath = path.join(__dirname, '.env.local')
if (fs.existsSync(envPath)) {
// Очищаем старые значения
delete process.env.TEMPLATE_ENGINE
delete process.env.HTTPS
delete process.env.PORT
delete process.env.HOST
delete process.env.OPEN_BROWSER
delete process.env.BROWSER_SYNC_NOTIFY
// Загружаем новые
dotenv.config({ path: envPath, override: true })
// Обновляем настройки
settings.templateEngine = process.env.TEMPLATE_ENGINE || 'pug'
settings.https.enabled = process.env.HTTPS === 'true'
settings.server.port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000
settings.server.host = process.env.HOST || 'localhost'
settings.server.open = process.env.OPEN_BROWSER === 'true'
settings.server.notify = process.env.BROWSER_SYNC_NOTIFY === 'true'
console.log('🔄 Конфигурация перезагружена!')
console.log('⚙️ Новые настройки:')
console.log(` Шаблонизатор: ${settings.templateEngine}`)
console.log(` HTTPS: ${settings.https.enabled ? 'включен' : 'выключен'}`)
console.log(` Порт: ${settings.server.port}`)
console.log(` Хост: ${settings.server.host}`)
console.log('⚠️ Для применения HTTPS и смены шаблонизатора нужен перезапуск!')
} else {
console.log('❌ Файл .env.local не найден')
}
done()
}
// Задача для создания примера структуры данных
const createDataExample = (done) => {
const dataDir = path.join(__dirname, 'src/data')
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true })
}
const exampleData = {
site: {
title: 'Мой сайт',
description: 'Описание сайта',
author: 'Автор'
},
navigation: [
{ title: 'Главная', url: '/' },
{ title: 'О нас', url: '/about' },
{ title: 'Контакты', url: '/contacts' }
]
}
fs.writeFileSync(
path.join(dataDir, 'global.json'),
JSON.stringify(exampleData, null, 2)
)
console.log('✅ Создан пример файла данных: src/data/global.json')
done()
}
const refresh = (done) => {
sync.reload()
done()
}
// Обработка конкретного Pug файла
const processSinglePugFile = (filePath) => {
const data = loadData()
const relativePath = path.relative('src/pug/pages', filePath)
const outputPath = path.join('dist', relativePath.replace('.pug', '.html'))
console.log(`📄 Обработка файла: ${relativePath}`)
return gulp
.src(filePath)
.pipe(plumber({
errorHandler(err) {
console.error('Pug Error:', err.toString())
this.emit('end')
}
}))
.pipe(
pug({
pretty: true,
basedir: 'src/pug',
locals: {
baseUrl,
searchId,
getSrcset: getImageSrcset,
getMime: getImageMime,
...data
}
})
)
.pipe(rename(path.basename(outputPath)))
.pipe(gulp.dest(path.dirname(outputPath)))
}
// Обработка конкретного Twig файла
const processSingleTwigFile = (filePath) => {
const data = loadData()
const relativePath = path.relative('src/twig/pages', filePath)
const outputPath = path.join('dist', relativePath.replace('.twig', '.html'))
console.log(`📄 Обработка файла: ${relativePath}`)
return gulp
.src(filePath)
.pipe(plumber({
errorHandler(err) {
console.error('Twig Error:', err.toString())
this.emit('end')
}
}))
.pipe(
twig({
base: 'src/twig',
data: {
baseUrl,
searchId,
getSrcset: getImageSrcset,
getMime: getImageMime,
...data
},
functions: [
{
name: 'getSrcset',
func: getImageSrcset
},
{
name: 'getMime',
func: getImageMime
}
]
})
)
.pipe(rename(path.basename(outputPath)))
.pipe(gulp.dest(path.dirname(outputPath)))
}
// Сервер с поддержкой HTTPS
const server = () => {
let serverConfig = {
server: 'dist/',
notify: settings.server.notify,
open: settings.server.open,
cors: true,
ui: false,
port: settings.server.port,
host: settings.server.host
}
if (settings.https.enabled) {
const keyPath = path.join(__dirname, settings.https.keyPath)
const certPath = path.join(__dirname, settings.https.certPath)
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
serverConfig.https = {
key: keyPath,
cert: certPath
}
console.log('🔒 Запуск сервера с HTTPS')
console.log(`📍 https://${serverConfig.host}:${serverConfig.port}`)
} else {
console.log('⚠️ HTTPS включен, но сертификаты не найдены')
console.log('Запустите: gulp ssl для создания сертификатов')
}
} else {
console.log('🌐 Запуск HTTP сервера')
console.log(`📍 http://${serverConfig.host}:${serverConfig.port}`)
}
sync.init(serverConfig)
// Умное наблюдение за страницами
if (settings.templateEngine === 'twig') {
// Отслеживание изменений в страницах Twig
const twigWatcher = gulp.watch('src/twig/pages/**/*.twig')
twigWatcher.on('change', (filePath) => {
console.log(`🔄 Изменен файл: ${path.relative(__dirname, filePath)}`)
return gulp.series(
() => processSingleTwigFile(filePath),
refresh
)()
})
// Наблюдение за общими файлами - полная пересборка
gulp.watch(['src/twig/**/*.twig', '!src/twig/pages/**/*.twig'], gulp.series(() => html(false), injectSprite, refresh))
} else {
// Отслеживание изменений в страницах Pug
const pugWatcher = gulp.watch('src/pug/pages/**/*.pug')
pugWatcher.on('change', (filePath) => {
console.log(`🔄 Изменен файл: ${path.relative(__dirname, filePath)}`)
return gulp.series(
() => processSinglePugFile(filePath),
refresh
)()
})
// Наблюдение за общими файлами - полная пересборка
gulp.watch(['src/pug/**/*.pug', '!src/pug/pages/**/*.pug'], gulp.series(() => html(false), injectSprite, refresh))
}
// Наблюдение за данными - полная пересборка
gulp.watch('src/data/**/*.json', gulp.series(() => html(false), injectSprite, refresh))
// Наблюдение за спрайтами - полная пересборка с инъекцией
gulp.watch('src/icons/**/*.svg', gulp.series(sprite, () => html(false), injectSprite, refresh))
// Остальные файлы
gulp.watch('src/scss/**/*.scss', gulp.series(css))
gulp.watch('src/js/**/*.js', gulp.series(js, refresh))
gulp.watch('src/img/**/*.{png,jpg,jpeg,svg}', gulp.series(processImages, refresh))
}
// Экспорт задач
export const build = gulp.series(
clean,
copy,
css,
js,
processImages,
sprite,
() => html(false),
injectSprite
)
export const dev = gulp.series(build, server)
export const data = createDataExample
export const ssl = createSSLCerts
export const configInit = createHTTPSConfig
export const configReload = reloadConfig
// HTTPS команды
export const devSSL = gulp.series(
(done) => {
settings.https.enabled = true
done()
},
ssl,
build,
server
)
// Команды с выбором шаблонизатора
export const buildPug = gulp.series(
(done) => { settings.templateEngine = 'pug'; done() },
build
)
export const buildTwig = gulp.series(
(done) => { settings.templateEngine = 'twig'; done() },
build
)
export const devPug = gulp.series(
(done) => { settings.templateEngine = 'pug'; done() },
build,
server
)
export const devTwig = gulp.series(
(done) => { settings.templateEngine = 'twig'; done() },
build,
server
)
export const devPugSSL = gulp.series(
(done) => {
settings.templateEngine = 'pug'
settings.https.enabled = true
done()
},
ssl,
build,
server
)
export const devTwigSSL = gulp.series(
(done) => {
settings.templateEngine = 'twig'
settings.https.enabled = true
done()
},
ssl,
build,
server
)

View File

@ -0,0 +1,257 @@
import autoprefixer from 'autoprefixer'
import concat from 'gulp-concat'
import csso from 'gulp-csso'
import { deleteAsync } from 'del'
import gulp from 'gulp'
import gulpWebp from 'gulp-webp'
import imagemin, { mozjpeg, optipng, svgo } from 'gulp-imagemin'
import imageminAvif from 'imagemin-avif'
import order from 'gulp-order'
import plumber from 'gulp-plumber'
import postcss from 'gulp-postcss'
import pug from 'gulp-pug'
import rename from 'gulp-rename'
import uglify from 'gulp-uglify-es'
import * as dartSass from 'sass'
import gulpSass from 'gulp-sass'
import sourcemap from 'gulp-sourcemaps'
import svgstore from 'gulp-svgstore'
import sync from 'browser-sync'
import { nanoid } from 'nanoid'
import postcssUrl from 'postcss-url'
import inject from 'gulp-inject'
import newer from 'gulp-newer' // Added gulp-newer
const baseUrl = '.'
const pathCss = '..'
const searchId = Date.now()
const getImageSrcset = (path, format, params) => {
return [1, 2]
.map((dpr) => `${baseUrl}${path}@${dpr}x.${format}${params} ${dpr}x`)
.join(', ')
}
const getImageMime = (format) => {
switch (format) {
case 'avif':
return 'image/avif'
case 'webp':
return 'image/webp'
case 'jpeg':
case 'jpg':
return 'image/jpeg'
case 'png':
return 'image/png'
default:
return null
}
}
const sass = gulpSass(dartSass)
const clean = async () => {
return await deleteAsync(['dist'])
}
const copy = () => {
return gulp
.src(
[
'src/fonts/**/*.{woff,woff2}',
'src/img/**/*.{webm,webp,avif,jpg,jpeg,png,svg}',
'src/favicon/**/*',
'src/favicon.ico',
'src/settings.js',
'src/urls.json',
'src/robots.txt'
],
{ base: 'src', encoding: false }
)
.pipe(gulp.dest('dist'))
}
const css = () => {
return gulp
.src('src/scss/index.scss')
.pipe(plumber())
.pipe(sourcemap.init())
.pipe(sass.sync().on('error', sass.logError))
.pipe(
postcss([
postcssUrl({
url: (asset) => {
if (asset.url.startsWith('/')) {
return `${pathCss}${asset.url}?v=${searchId}`
}
return asset.url
}
}),
autoprefixer({ remove: false })
])
)
.pipe(rename('style.css'))
.pipe(sourcemap.write('.'))
.pipe(gulp.dest('dist/css'))
.pipe(sync.stream())
.pipe(csso())
.pipe(rename('style.min.css'))
.pipe(gulp.dest('dist/css'))
}
const images = () => {
return gulp
.src('src/img/**/*.{png,jpg,jpeg,svg}', { base: 'src', encoding: false }) // Process all images in subfolders, keep base for dest path
.pipe(newer('dist')) // Process only newer files, compare with base 'dist' (e.g. dist/img/...)
.pipe(
imagemin(
[
optipng({ optimizationLevel: 3 }),
mozjpeg({ quality: 80, progressive: true }), // Quality for JPEG
svgo({
plugins: [{ name: 'removeUnknownsAndDefaults', active: false }]
})
],
{ silent: true }
)
)
.pipe(gulp.dest('dist')) // Output to dist, maintaining folder structure from base
}
const webp = () => {
return gulp
.src('src/img/**/*.{png,jpg,jpeg}', { base: 'src', encoding: false }) // Process all images in subfolders
.pipe(newer({ dest: 'dist', ext: '.webp' })) // Process only newer, check against .webp extension in dist
.pipe(gulpWebp({ quality: 80 })) // Quality for WebP
.pipe(gulp.dest('dist')) // Output to dist, maintaining folder structure
}
const avif = () => {
return gulp
.src('src/img/**/*.{png,jpg,jpeg}', { base: 'src', encoding: false }) // Process all images in subfolders
.pipe(newer({ dest: 'dist', ext: '.avif' })) // Process only newer, check against .avif extension in dist
.pipe(imagemin([imageminAvif({ quality: 50 })], { silent: true })) // Quality for AVIF
.pipe(rename((path) => (path.extname = '.avif')))
.pipe(gulp.dest('dist')) // Output to dist, maintaining folder structure
}
const processImages = gulp.parallel(images, webp, avif)
const sprite = () => {
return gulp
.src('src/icons/**/*.svg')
.pipe(svgstore({ inlineSvg: true }))
.pipe(rename(`sprite.svg`))
.pipe(gulp.dest('dist/img'))
}
const injectSprite = () => {
const spritePath = 'dist/img/sprite.svg'
return gulp
.src('dist/*.html')
.pipe(
inject(gulp.src(spritePath), {
transform: (_, file) => {
return file.contents.toString()
}
})
)
.pipe(gulp.dest('dist'))
}
const jsCommon = () => {
return gulp
.src('src/js/common/*.js')
.pipe(
plumber({
errorHandler(err) {
console.error(err.toString())
this.emit('end')
}
})
)
.pipe(order(['_utils.js', '*.js']))
.pipe(concat(`script.js`))
.pipe(gulp.dest('dist/js'))
.pipe(uglify.default())
.pipe(rename(`script.min.js`))
.pipe(gulp.dest('dist/js'))
}
const jsVendor = () => {
return gulp
.src('src/js/vendor/*.js')
.pipe(
plumber({
errorHandler(err) {
console.error(err.toString())
this.emit('end')
}
})
)
.pipe(concat(`vendor.js`))
.pipe(gulp.dest('dist/js'))
.pipe(uglify.default())
.pipe(rename(`script.min.js`))
.pipe(gulp.dest('dist/js'))
}
const html = () => {
return gulp
.src('src/pug/pages/**/*.pug')
.pipe(plumber())
.pipe(
pug({
pretty: true,
basedir: 'src/pug',
locals: {
baseUrl,
searchId,
getSrcset: getImageSrcset,
getMime: getImageMime
}
})
)
.pipe(gulp.dest('dist'))
}
const refresh = (done) => {
sync.reload()
done()
}
const server = () => {
sync.init({
server: 'dist/',
notify: false,
open: false,
cors: true,
ui: false
})
gulp.watch('src/pug/**/*.{pug,js}', gulp.series(html, injectSprite, refresh))
gulp.watch(
'src/icons/**/*.svg',
gulp.series(sprite, html, injectSprite, refresh)
)
gulp.watch('src/scss/**/*.scss', gulp.series(css))
gulp.watch('src/js/**/*.js', gulp.series(jsVendor, jsCommon, refresh))
// Watch for source image changes and run all image processing tasks.
// gulp-newer within each task will ensure only necessary files are processed.
gulp.watch('src/img/**/*.{png,jpg,jpeg,svg}', gulp.series(processImages, refresh))
}
export const build = gulp.series(
clean,
copy,
css,
jsVendor,
jsCommon,
processImages,
sprite,
html,
injectSprite
)
export const dev = gulp.series(build, server)

21
webpunk-templates/netlify.toml Executable file
View File

@ -0,0 +1,21 @@
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[[headers]]
for = "/*"
[headers.values]
cache-control = '''
max-age=0,
no-cache,
no-store,
must-revalidate'''
X-Robots-Tag = 'noindex'
[build]
command = "pnpm run build"
[build.environment]
NODE_VERSION = "20.18.2"

View File

@ -0,0 +1,79 @@
{
"name": "webpunk-templates",
"type":"module",
"scripts": {
"dev": "gulp dev",
"dev:pug": "gulp devPug",
"dev:twig": "gulp devTwig",
"dev:https": "gulp devSSL",
"dev:pug:https": "gulp devPugSSL",
"dev:twig:https": "gulp devTwigSSL",
"build": "gulp build",
"build:pug": "gulp buildPug",
"build:twig": "gulp buildTwig",
"ssl:create": "gulp ssl",
"ssl:check": "node ssl-manager.js check",
"ssl:info": "node ssl-manager.js info",
"ssl:delete": "node ssl-manager.js delete",
"data:create": "gulp data",
"config:create": "gulp configInit",
"config:reload": "gulp configReload",
"clean": "gulp clean",
"pugprettier:fix": "prettier --write '**/*.pug' --plugin='@prettier/plugin-pug'",
"stylelint": "stylelint 'src/scss/**/*.scss'",
"stylelint:fix": "stylelint 'src/scss/**/*.scss' --fix",
"eslint": "eslint .",
"gh-pages": "push-dir --dir=dist --branch=gh-pages",
"public": "pnpm run build && pnpm run gh-pages"
},
"author": "dreadwood",
"devDependencies": {
"@eslint/js": "9.16.0",
"autoprefixer": "10.4.19",
"browser-sync": "3.0.2",
"del": "7.1.0",
"dotenv": "^16.5.0",
"eslint": "9.16.0",
"globals": "15.13.0",
"gulp": "5.0.0",
"gulp-concat": "2.6.1",
"gulp-csso": "4.0.1",
"gulp-imagemin": "9.1.0",
"gulp-inject": "5.0.5",
"gulp-newer": "^1.4.0",
"gulp-order": "1.2.0",
"gulp-plumber": "1.2.1",
"gulp-postcss": "10.0.0",
"gulp-pug": "5.0.0",
"gulp-rename": "2.0.0",
"gulp-sass": "5.1.0",
"gulp-sourcemaps": "3.0.0",
"gulp-svgstore": "9.0.0",
"gulp-twig": "^1.2.0",
"gulp-uglify-es": "3.0.0",
"gulp-webp": "5.0.0",
"imagemin-avif": "0.1.6",
"nanoid": "5.0.8",
"postcss": "8.4.38",
"postcss-scss": "4.0.9",
"postcss-url": "10.1.3",
"pug": "3.0.2",
"push-dir": "0.4.1",
"sass": "1.72.0",
"stylelint": "16.3.1",
"stylelint-config-standard-scss": "13.0.0",
"stylelint-order": "6.0.4",
"stylelint-prettier": "5.0.2",
"uglify-js": "3.17.4"
},
"pnpm": {
"onlyBuiltDependencies": [
"cwebp-bin",
"es5-ext",
"gifsicle",
"mozjpeg",
"optipng-bin",
"sharp"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
# Шаблон Webpunk для верстки
## Разработка
* Установить [pnpm](https://pnpm.io/)
* Установка - `pnpm i`
* Сборка проекта - `pnpm run build`
* Запуск локального сервера для разработки - `pnpm run dev`
* Запуск тестирования на соответствия код-гайдам - `pnpm run test`
* Обновить версию Github Pages (перед выполнением нужно собрать проект) - `gh-pages`
**Каталоги:**
* Все разработка ведёться в директории `src/`
* Итоговый код попадает в директорию `dist/`

160
webpunk-templates/readme.md Normal file
View File

@ -0,0 +1,160 @@
# Webpunk Templates — Подробная документация по сборщику
## Быстрый старт
1. **Установите pnpm:**
[Инструкция по установке](https://pnpm.io/)
2. **Установите зависимости:**
```bash
pnpm install
```
3. **Запустите сборку или сервер разработки:**
- Сборка:
```bash
pnpm run build
```
- Сервер разработки:
```bash
pnpm run dev
```
---
## Структура проекта
- `src/` — исходные файлы (шаблоны, стили, скрипты, изображения, данные)
- `dist/` — результат сборки (готовый к публикации код)
- `ssl/` — dev SSL-сертификаты (создаются автоматически)
- `.env.local` — локальная конфигурация сборщика
---
## Основные возможности сборщика
### 1. **Гибкая работа с шаблонами**
- Поддержка **Pug** и **Twig** (выбор через `.env.local` или команды)
- Инкрементальная сборка страниц: при изменении страницы пересобирается только она
- Автоматическая подгрузка данных из `src/data/*.json` для шаблонов
- В шаблонах доступны функции для генерации srcset и mime-типов изображений
### 2. **Стили и скрипты**
- SCSS компиляция с sourcemaps, autoprefixer, postcss-url (автоматическое добавление версий к ассетам)
- Минификация CSS (`style.min.css`)
- Сборка и минификация JS (разделение на common/vendor)
- Поддержка sourcemaps для JS
### 3. **Изображения**
- Оптимизация PNG, JPEG, SVG
- Генерация webp и avif-версий изображений
- Инкрементальная обработка (обрабатываются только новые/изменённые файлы)
- Генерация SVG-спрайта и автоматическая инъекция в HTML
### 4. **Автоматизация и наблюдение**
- Watch-режим для всех исходников (шаблоны, данные, стили, скрипты, изображения, иконки)
- При изменении страницы пересобирается только она, при изменении общих файлов — пересобираются все страницы
- Автоматическая перезагрузка браузера через BrowserSync
### 5. **HTTPS для разработки**
- Автоматическое создание самоподписанных SSL-сертификатов для локальной разработки
- Включение HTTPS через `.env.local` или специальные команды
### 6. **Конфигурация**
- Все параметры (шаблонизатор, порт, хост, HTTPS, автооткрытие браузера и др.) настраиваются в `.env.local`
- Быстрое создание и обновление конфигурации через команды
---
## Основные команды
| Команда | Описание |
|------------------------|--------------------------------------------------------------------------|
| `pnpm run dev` | Запуск сервера разработки (шаблонизатор по умолчанию из `.env.local`) |
| `pnpm run dev:pug` | Запуск dev-сервера с Pug |
| `pnpm run dev:twig` | Запуск dev-сервера с Twig |
| `pnpm run dev:https` | Dev-сервер с HTTPS |
| `pnpm run dev:pug:https` | Dev-сервер с Pug и HTTPS |
| `pnpm run dev:twig:https` | Dev-сервер с Twig и HTTPS |
| `pnpm run build` | Сборка проекта (шаблонизатор по умолчанию) |
| `pnpm run build:pug` | Сборка с Pug |
| `pnpm run build:twig` | Сборка с Twig |
| `pnpm run ssl:create` | Создать dev SSL-сертификаты |
| `pnpm run ssl:check` | Проверить наличие SSL-сертификатов |
| `pnpm run ssl:info` | Информация о сертификатах |
| `pnpm run ssl:delete` | Удалить dev SSL-сертификаты |
| `pnpm run data:create` | Создать пример файла данных |
| `pnpm run config:create` | Создать `.env.local` с настройками |
| `pnpm run config:reload` | Перезагрузить конфиг без перезапуска сервера |
| `pnpm run clean` | Очистить папку `dist` |
| `pnpm run pugprettier:fix` | Применить prettier к pug-файлам |
| `pnpm run stylelint` | Проверить SCSS на ошибки |
| `pnpm run stylelint:fix` | Исправить ошибки SCSS автоматически |
| `pnpm run eslint` | Проверить JS на ошибки |
| `pnpm run gh-pages` | Залить содержимое `dist` на GitHub Pages |
| `pnpm run public` | Сборка и публикация на GitHub Pages |
---
## Как работает сборщик
1. **Конфигурация**
При запуске читается `.env.local`. Если файла нет — создаётся автоматически.
Можно выбрать шаблонизатор, порт, хост, включить HTTPS и др.
2. **Сборка**
- Очищается папка `dist`
- Копируются ассеты (шрифты, изображения, фавиконки, robots.txt и др.)
- Компилируются стили и скрипты (с минификацией и sourcemaps)
- Оптимизируются изображения, генерируются webp/avif
- Генерируется SVG-спрайт
- Собираются HTML-файлы из шаблонов (Pug или Twig), подставляются данные из JSON
- В HTML автоматически инъектируется SVG-спрайт
3. **Dev-сервер**
- Запускается BrowserSync на выбранном порту/хосте
- При изменении страницы пересобирается только она (инкрементальная сборка)
- При изменении общих файлов (layout, mixins, data) пересобираются все страницы
- При изменении ассетов, стилей, скриптов — пересобираются только они
- Браузер автоматически перезагружается
4. **HTTPS**
- При первом запуске с HTTPS создаются dev-сертификаты (openssl обязателен)
- Сервер работает по https://
---
## Примеры использования
- **Сменить шаблонизатор:**
В `.env.local` изменить `TEMPLATE_ENGINE=pug` или `TEMPLATE_ENGINE=twig`, либо использовать команды `dev:pug`, `dev:twig`, `build:pug`, `build:twig`.
- **Включить HTTPS:**
В `.env.local` поставить `HTTPS=true` или использовать команду `dev:https`.
- **Добавить новые данные:**
Добавьте JSON-файл в `src/data/`, он автоматически попадёт в шаблоны.
---
## Советы
- Для корректной работы HTTPS нужен установленный openssl.
- Для публикации на GitHub Pages используйте `pnpm run public`.
- Для быстрой генерации структуры данных используйте `pnpm run data:create`.
- Для обновления конфигурации без перезапуска сервера — `pnpm run config:reload`.
---
## Каталоги
- Исходники: `src/`
- Сборка: `dist/`
- Конфиг: `.env.local`
- Сертификаты: `ssl/`

View File

@ -0,0 +1,66 @@
{
"version":"1.1.1",
"title":"Fonbet",
"landing": "ru",
"theme_color": "#030303",
"app_title":"Фонбет",
"description":"Fonbet",
"gtm_name":"openarena",
"landing_app": "openarena",
"event": "openarena",
"month": "ноября",
"prize_title": "Чтобы участвовать в розыгрыше нейромерча,\nнужно быть клиентом Фонбет и сделать ставку\nот 500 рублей на любое спортивное событие",
"close_land": "false",
"show_winners": "false",
"theme": "start",
"date_end": "20 ноября",
"form_theme": "dark",
"form_promo_hidden":"true",
"form_promo_id":"",
"form_promo_id_mob":"",
"gtag":"GTM-K7XNHT2",
"re_captcha":"6LehDGAUAAAAAJoqkx-oc6W-KeapSBCr2veF3Mwd",
"domain":"fon.bet",
"domain1":"fon.bet",
"cdn":"origin.bk6bba-resources.com/webStaticRed/promo",
"cdn_js":"origin.bk6bba-resources.com/webStaticRed/",
"rules_alias":"",
"rules_alias_mob":"",
"_freebet_link_modal":"https://fon.bet/promo/freebet_app_base/",
"form_link":"https://fonbet.onelink.me/Ci45/n3kg5eiy",
"app_link_mob":"https://fonbet.onelink.me/Ci45/n3kg5eiy",
"reg_link":"https://clicks.af-ru2e2e.com/click?offer_id=125&partner_id=4596&landing_id=4898&utm_medium=affiliate&sub_1=%7Bflashscore%7D&sub_2=%7Breg_ban%7D&sub_3=%7Bfb15k%7D",
"reg_link2":"https://fon.bet/account/registration/",
"join_link":"https://fon.bet/account/payments/deposit",
"auth_link":"https://fon.bet/account/",
"btn_link": "https://www.fon.bet/authProcess/login/",
"tg_link":"https://t.me/fonbetesports",
"_redirect_link": "",
"home":{
"title":"Fonbet"
},
"logo_header":{
"utm_source":"landing_openarena",
"utm_content":"header_logo",
"utm_term":"landing_openarena",
"partner":"landing_openarena"
},
"logo_footer":{
"src":"fon.bet.svg",
"src_dark":"fon.bet-white.svg",
"utm_source":"landing_openarena",
"utm_content":"footer_logo",
"utm_term":"landing_openarena",
"partner":"landing_openarena"
},
"match":{
"free_bet_value": "15000",
"free_bet": "15 000 ₽",
"free_bet_mob": "15 000 ₽",
"fonbet_link": "https://fon.bet/account/payments/deposit",
"fonbet_link_mob": "https://fon.bet/account/payments/deposit"
}
}

View File

@ -0,0 +1,21 @@
{
"site": {
"title": "Мой сайт",
"description": "Описание сайта",
"author": "Автор"
},
"navigation": [
{
"title": "Главная",
"url": "/"
},
{
"title": "О нас",
"url": "/about"
},
{
"title": "Контакты",
"url": "/contacts"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,5 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 16C0 7.16344 7.16344 0 16 0H104C112.837 0 120 7.16344 120 16V104C120 112.837 112.837 120 104 120H16C7.16344 120 0 112.837 0 104V16Z" fill="white"/>
<path d="M13.2681 0.079235C7.11546 1.04179 2.34834 5.19723 0.446184 11.3013C0.0234834 12.6629 0 14.3768 0 60.0161C0 105.656 0.0234834 107.369 0.446184 108.731C2.13699 114.131 5.87084 117.864 11.272 119.554C12.6341 119.977 14.3483 120 60 120C105.652 120 107.366 119.977 108.728 119.554C114.129 117.864 117.863 114.131 119.554 108.731C119.977 107.369 120 105.656 120 60.0161C120 14.3768 119.977 12.6629 119.554 11.3013C117.886 5.97197 114.387 2.40346 108.963 0.525299C107.554 0.032281 107.507 0.0322809 60.7045 0.00880389C34.9432 -0.0146732 13.5969 0.00880388 13.2681 0.079235ZM98.137 26.5848C98.3483 26.6553 98.5127 26.9839 98.5127 27.3126C98.5127 28.0404 95.8356 39.0981 95.3425 40.4598C95.1311 40.9997 94.7789 41.5162 94.5675 41.6336C94.3562 41.7275 87.7808 41.8684 79.9609 41.9388L65.7534 42.0562L64.2035 42.8075C62.5127 43.6526 61.7143 44.5682 61.0568 46.3994C60.7515 47.2681 59.2955 52.9965 59.2955 53.3487C59.2955 53.3956 65.4247 53.4426 72.9393 53.4426C81.8865 53.4426 86.7241 53.5365 87.0294 53.6773C87.2642 53.8182 87.4755 54.0999 87.4755 54.3347C87.4755 54.9451 84.9393 65.3924 84.5401 66.5193C83.8591 68.327 84.681 68.2331 69.3229 68.2331H55.6321L52.8611 79.549C49.409 93.7996 49.3386 94.0109 48.5871 94.2926C48.2583 94.4335 44.407 94.5274 39.6634 94.5274C31.6556 94.5274 31.3268 94.5039 30.8806 94.0579C30.6223 93.7996 30.411 93.4005 30.411 93.1657C30.411 92.4849 42.364 43.7935 42.8806 42.291C44.2427 38.5346 46.7789 34.614 49.3386 32.3367C52.6027 29.4255 57.0881 27.383 61.7847 26.7022C63.3581 26.4674 97.5734 26.3735 98.137 26.5848Z" fill="#e80024"/>
<path d="M13.2681 0.079235C7.11546 1.04179 2.34834 5.19723 0.446184 11.3013C0.0234834 12.6629 0 14.3768 0 60.0161C0 105.656 0.0234834 107.369 0.446184 108.731C2.13699 114.131 5.87084 117.864 11.272 119.554C12.6341 119.977 14.3483 120 60 120C105.652 120 107.366 119.977 108.728 119.554C114.129 117.864 117.863 114.131 119.554 108.731C119.977 107.369 120 105.656 120 60.0161C120 14.3768 119.977 12.6629 119.554 11.3013C117.886 5.97197 114.387 2.40346 108.963 0.525299C107.554 0.032281 107.507 0.0322809 60.7045 0.00880389C34.9432 -0.0146732 13.5969 0.00880388 13.2681 0.079235ZM98.137 26.5848C98.3483 26.6553 98.5127 26.9839 98.5127 27.3126C98.5127 28.0404 95.8356 39.0981 95.3425 40.4598C95.1311 40.9997 94.7789 41.5162 94.5675 41.6336C94.3562 41.7275 87.7808 41.8684 79.9609 41.9388L65.7534 42.0562L64.2035 42.8075C62.5127 43.6526 61.7143 44.5682 61.0568 46.3994C60.7515 47.2681 59.2955 52.9965 59.2955 53.3487C59.2955 53.3956 65.4247 53.4426 72.9393 53.4426C81.8865 53.4426 86.7241 53.5365 87.0294 53.6773C87.2642 53.8182 87.4755 54.0999 87.4755 54.3347C87.4755 54.9451 84.9393 65.3924 84.5401 66.5193C83.8591 68.327 84.681 68.2331 69.3229 68.2331H55.6321L52.8611 79.549C49.409 93.7996 49.3386 94.0109 48.5871 94.2926C48.2583 94.4335 44.407 94.5274 39.6634 94.5274C31.6556 94.5274 31.3268 94.5039 30.8806 94.0579C30.6223 93.7996 30.411 93.4005 30.411 93.1657C30.411 92.4849 42.364 43.7935 42.8806 42.291C44.2427 38.5346 46.7789 34.614 49.3386 32.3367C52.6027 29.4255 57.0881 27.383 61.7847 26.7022C63.3581 26.4674 97.5734 26.3735 98.137 26.5848Z" fill="black" fill-opacity="0.05"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-4.5-.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 288 B

View File

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M4.5 7.5a.5.5 0 0 0 0 1h5.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -0,0 +1,22 @@
/**
* auth.js
*/
;(() => {
window.jsAuth = {
init() {
document.addEventListener('registrationCompleted', (evt) => {
const pin = evt.detail.clientId
if (!pin) return
console.log('userInfoUpdated')
})
document.addEventListener('userInfoUpdated', (evt) => {
if (evt.detail.clientId !== evt.detail.prevClientId) {
console.log('userInfoUpdated')
}
})
}
}
})()

View File

@ -0,0 +1,34 @@
/**
* fetch.js
*/
;(() => {
window.jsFetch = {
/**
* wrapper for fetch
* @param {string} url
* @param {string} method
* @param {JSON} data
* @returns
*/
async sendData(url, data, method = 'GET') {
const options = {
method,
headers: { 'Content-Type': 'application/json' },
body: data ? JSON.stringify(data) : undefined
}
try {
const res = await fetch(url, options)
if (!res.ok) {
console.error('Response error:', res)
return
}
return await res.json()
} catch (err) {
console.error('Request catch error:', err)
throw err
}
}
}
})()

View File

@ -0,0 +1,30 @@
/**
* lib.js
*/
;(() => {
window.jsLib = {
/** @type {MediaQueryList} */
mqTablet: window.matchMedia(window.jsUtils.BREAKPOINT_TABLET),
/** @type {MediaQueryList} */
mqDesktop: window.matchMedia(window.jsUtils.BREAKPOINT_DESKTOP),
getGapSwiper(gap, tabletGap, desktopGap) {
let rate
switch (true) {
case this.mqDesktop.matches:
rate = desktopGap / window.jsUtils.DESKTOP_WIDTH
break
case this.mqTablet.matches:
rate = tabletGap / window.jsUtils.TABLET_WIDTH
break
default:
rate = gap / window.jsUtils.MOBILE_WIDTH
break
}
return document.documentElement.scrollWidth * rate
}
}
})()

View File

@ -0,0 +1,96 @@
/**
* pin.js
*/
;(() => {
window.jsPin = {
init() {
let regFlag = false
let refreshIntervalId = setInterval(() => {
if (typeof window.registrationApi !== 'undefined') regFlag = true
if (regFlag) {
clearInterval(refreshIntervalId)
document.dispatchEvent(new Event('registrationInit'))
// eslint-disable-next-line no-undef
registrationApi.onRegistrationStateChanged = (state) => {
console.log('registrationApi state', state)
}
// eslint-disable-next-line no-undef
registrationApi.onRegistrationCompleted = (clientId) => {
document.dispatchEvent(
new CustomEvent('registrationCompleted', {
detail: { clientId: clientId }
})
)
console.log('Registration completed', clientId)
}
}
}, 500)
const userInfo = (() => {
let clientId = null
let prevClientId = null
let fsid = null
const getCookie = (name) => {
const matches = document.cookie.match(
new RegExp(
`(?:^|; )${name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1')}=([^;]*)`
)
)
return matches ? decodeURIComponent(matches[1]) : undefined
}
const getParameterByName = (name, url = window.location.href) => {
name = name.replace(/[[\]]/g, '\\$&')
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`)
const results = regex.exec(url)
if (!results) return null
if (!results[2]) return ''
return decodeURIComponent(results[2].replace(/\+/g, ' '))
}
const setClientID = () => {
prevClientId = clientId
clientId =
getParameterByName('clientId') ||
getCookie('headerApi.cid') ||
getParameterByName('fsid')
}
const setFSID = () => {
fsid = getCookie('headerApi.fsid') || getCookie('headerApi.FSID')
}
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
setClientID()
const event = new CustomEvent('userInfoUpdated', {
detail: {
clientId,
prevClientId
}
})
document.dispatchEvent(event)
}
}
setClientID()
setFSID()
document.addEventListener('visibilitychange', handleVisibilityChange)
return {
getClientID: () => clientId,
getPrevClientID: () => prevClientId,
setClientID,
getFSID: () => fsid,
setFSID,
handleVisibilityChange
}
})()
window.userInfo = userInfo
}
}
})()

View File

@ -0,0 +1,90 @@
'use strict'
/**
* utils.js
*/
;(() => {
window.jsUtils = {
BREAKPOINT_DESKTOP: '(min-width: 1024px)',
MOBILE_WIDTH: 360,
DESKTOP_WIDTH: 1920,
/**
* show html element
* @param {HTMLElement} el
*/
showEl(el) {
if (!(el instanceof Node)) return
el.removeAttribute('hidden')
},
/**
* hide html element
* @param {HTMLElement} el
*/
hideEl(el) {
if (!(el instanceof Node)) return
el.setAttribute('hidden', 'hidden')
},
/**
* Example: 'балл', 'балла', 'баллов'
* @param {number} number
* @param {string} one
* @param {string} few
* @param {string} many
* @returns
*/
pluralize(number, one, few, many) {
const lastDigit = number % 10
const lastTwoDigits = number % 100
switch (true) {
case lastDigit === 1:
case lastTwoDigits !== 11:
return one
case lastDigit >= 2:
case lastDigit <= 4:
case !(lastTwoDigits >= 12 && lastTwoDigits <= 14):
return few
default:
return many
}
},
/**
* get random integer
* @param {number} min
* @param {number} max
* @returns {number}
*/
randomInteger(min, max) {
const rand = min + Math.random() * (max + 1 - min)
return Math.floor(rand)
},
/**
* @param {(...args: any[]) => T} func
* @param {number} delay
* @returns {(...args: any[]) => void}
*/
throttle(func, delay) {
let lastCall = 0
return function (...args) {
const now = new Date().getTime()
if (now - lastCall >= delay) {
lastCall = now
func.apply(this, args)
}
}
},
detectSafari() {
return /^((?!chrome|android|crios|fxios).)*safari/i.test(
navigator.userAgent
)
}
}
})()

View File

@ -0,0 +1,8 @@
/**
* index.js
*/
;(() => {
// window.jsPin.init()
// window.jsAuth.init()
})()

View File

View File

@ -0,0 +1,2 @@
mixin b-footer({ className })
footer.b-footer(class=className)

View File

@ -0,0 +1,2 @@
mixin b-header({ className })
header.b-header(class=className)

View File

View File

@ -0,0 +1,5 @@
{
"site": {
"title": "Мой сайт"
}
}

View File

@ -0,0 +1,7 @@
mixin console({ data, id })
script
if data && id
| const pugConsole#{id} = !{JSON.stringify(data)};
| console.log(pugConsole#{id});
else
| console.log('Pug console params are not complete');

View File

@ -0,0 +1,5 @@
mixin favicon({ })
link(rel="icon" href=`${baseUrl}/favicon.ico`)
link(rel="icon" href=`${baseUrl}/favicon/favicon.svg?v=${searchId}` type="image/svg+xml")
link(rel="icon" href="https://origin.bk6bba-resources.com/webStaticRed/promo/lands/images/favicons-en/android-chrome-192x192.png" type="image/png")
link(rel="apple-touch-icon" href="https://origin.bk6bba-resources.com/webStaticRed/promo/lands/images/favicons-en/apple-touch-icon.png")

View File

@ -0,0 +1,23 @@
include /elements/favicon
//- include /script/s-gtm
//- include /script/s-yametrika
include /elements/open-graph
mixin head({ title, gtmId, yaMetrikaId })
head
meta(charset="utf-8")
meta(name="viewport" content="width=device-width, initial-scale=1")
//- meta(name="gtm_name" content="alias path")
title=title
+favicon({})
//- +open-graph({})
link(rel="stylesheet" href=`${baseUrl}/css/style.css?v=${searchId}`)
//- +s-gtm({ id: gtmId })
//- +s-yametrika({ id: yaMetrikaId })
//- UTM
//- script(src="https://origin.bk6bba-resources.com/webStaticRed/promo/lands/scripts/utm.js")

View File

@ -0,0 +1,54 @@
//- content raster image
mixin img({ src, w, h, alt, isLazy = true, className, isVersion = true })
- const sParams = isVersion ? `?v=${searchId}` : ""
- const url = src ? `${baseUrl}${src}${sParams}` : undefined
img(
class=className
src=url
width=w
height=h
loading=isLazy && "lazy"
alt=(alt || "")
)
//- simple raster image
mixin picture({ src, w, h, alt, isLazy = true, className, isVersion = true })
- const path = src.split(".")[0]
- const sParams = isVersion ? `?v=${searchId}` : ""
picture
source(type="image/avif" srcset=`${baseUrl}${path}.avif${sParams}`)
source(type="image/webp" srcset=`${baseUrl}${path}.webp${sParams}`)
img(
class=className
src=`${baseUrl}${src}?v=${sParams}`
width=w
height=h
loading=isLazy && "lazy"
alt=(alt || "")
)
//- retina raster image
mixin picture2x({ src, w, h, alt, isLazy = true, className, isVersion = true })
- const path = src.split("@")[0]
- const format = src.split(".")[1]
- const sParams = isVersion ? `?v=${searchId}` : ""
picture
source(type=getMime("avif") srcset=getSrcset(path, "avif", sParams))
source(type=getMime("webp") srcset=getSrcset(path, "webp", sParams))
img(
class=className
src=`${baseUrl}${src}${sParams}`
srcset=`${baseUrl}${path}@2x.${format}${sParams} 2x`
width=w
height=h
loading=isLazy && "lazy"
alt=(alt || "")
)
//- simple vector image
mixin svg({ name, w, h, className })
if name.includes('/img')
img(class=className || null src=`${baseUrl}${name}?v=${searchId}` width=w height=h alt="")
else
svg(class=className || null width=w height=h)
use(xlink:href=`#${name}`)

View File

@ -0,0 +1,30 @@
include /elements/head
include /elements/images
include /elements/sprite
//- include /script/s-gtm-noscript
//- include /script/s-yametrika-noscript
//- include /script/s-pin
include /components/block/b-header
include /components/block/b-footer
- let title = ""
- let gtmId = ""
- let yaMetrikaId = 0
block data
doctype html
html(lang="ru")
+head({ title, gtmId, yaMetrikaId })
body
//- +s-gtm-noscript({ id: gtmId })
//- +s-yametrika-noscript({ id: yaMetrikaId })
//- +s-pin({})
block libjs
+sprite({})
+b-header({})
main
block content
+b-footer({})
block modal
include /elements/script

View File

@ -0,0 +1,7 @@
mixin open-graph({})
meta(property='og:title' content='{описание сайта}')
meta(property='og:description' content='{описание страницы}')
meta(property='og:image' content='{изображение для превью, не меньше 200х200}')
meta(property='og:type' content='website')
meta(property='og:url' content='{адрес сайта}')
meta(property='og:locale' content='ru_RU')

View File

@ -0,0 +1,2 @@
//- script(src=`${baseUrl}/js/vendor.js?v=${searchId}`)
script(src=`${baseUrl}/js/script.js?v=${searchId}`)

View File

@ -0,0 +1,4 @@
mixin sprite({ className })
.visually-hidden(class=className)
// inject:svg
// endinject

View File

@ -0,0 +1,15 @@
extends /elements/layout-page
block data
- title = "About Template"
block content
.container
.row
.col-md-12
h1 About Template
p This is a template page.
p You can use this as a starting point for your own pages.
p Feel free to modify the content and structure as needed
block modal

View File

@ -0,0 +1,21 @@
extends /elements/layout-page
//- include /script/s-match
block data
- title = "Template"
block libjs
//- +s-match({})
//- script(src="https://origin.bk6bba-resources.com/webStaticRed/promo/lands/scripts/marked.min.js")
block content
.container
.row
.col-md-12
h1 Template
p This is a template page.
p You can use this as a starting point for your own pages.
p Feel free to modify the content and structure as needed
block modal

View File

@ -0,0 +1,4 @@
mixin s-gtm-noscript({ id })
// Google Tag Manager (noscript)
noscript
iframe(src=`https://www.googletagmanager.com/ns.html?id=${id}` height="0" width="0" style="display:none;visibility:hidden")

View File

@ -0,0 +1,11 @@
mixin s-gtm({ id })
// Google Tag Manager
script.
(function(w,d,s,l,i){
w[l]=w[l]||[];
w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});
var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';
j.async=true;
j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;
f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','#{id}');

View File

@ -0,0 +1,51 @@
mixin s-match({})
//- match and odds
script.
function initMatch() {
if (!data) return
for (var key in data) {
render(key, data[key])
}
for (var key in conf) {
render(key, conf[key])
}
utm();
}
function render(key, value) {
document.querySelectorAll('[data-content="' + key + '"]').forEach((el) => {
if (value) {
switch (el.tagName) {
case 'LABEL':
case 'DIV':
el.style.display = 'block'
break
case 'SPAN':
case 'P':
el.innerHTML = value
break
case 'TEXTAREA':
case 'INPUT':
el.value = value
break
case 'A':
el.href = value;
break
case 'IFRAME':
case 'VIDEO':
case 'IMG':
el.src = value
break
}
} else {
el.style.display = 'none'
}
});
}
script.
window.lang = 'ru'
window.alias = 'dota2'
window.callback = 'initMatch'
script(src="https://text.ajaxfeed.com/texts/line.js")

View File

@ -0,0 +1,10 @@
mixin s-pin({})
script.
window.initPin = () => {
[urlsConfig.registrationWidgetLoader].forEach((_item) => {
const script = document.createElement('script')
script.src = urlsConfig.cdnUrl + '/' + _item
document.head.appendChild(script);
})
}
script(src="https://fon.bet/settings.js" onload="initPin()")

View File

@ -0,0 +1,4 @@
mixin s-yametrika-noscript({ id })
// Yandex Metrika (noscript)
noscript
div #[img(src=`https://mc.yandex.ru/watch/${id}` style="position:absolute; left:-9999px;" alt="")]

View File

@ -0,0 +1,19 @@
mixin s-yametrika({ id })
// Yandex.Metrika counter
script(type="text/javascript").
(function(m,e,t,r,i,k,a){
m[i]=m[i] || function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {
if (document.scripts[j].src === r) { return; }
}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(#{id}, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true,
trackHash: true
});

View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@ -0,0 +1,7 @@
.b-footer {
width: 100%;
margin-right: auto;
margin-left: auto;
padding-right: $padding;
padding-left: $padding;
}

View File

@ -0,0 +1,7 @@
.b-header {
width: 100%;
margin-right: auto;
margin-left: auto;
padding-right: $padding;
padding-left: $padding;
}

View File

@ -0,0 +1,2 @@
@import "b-footer";
@import "b-header";

View File

@ -0,0 +1 @@
@import "block";

View File

@ -0,0 +1,59 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
height: 100%;
scroll-behavior: smooth;
font-size: max(1px, calc(100vw / 360));
@include desktop {
font-size: calc(100vw / 1920);
}
}
body {
position: relative;
display: flex;
flex-direction: column;
min-width: $container-min;
min-height: 100%;
margin: 0;
padding: 0;
font-family: $font-default;
font-size: 16rem;
line-height: 22rem;
font-weight: 400;
font-style: normal;
text-align: left;
color: $color-black;
word-break: break-word;
background-color: $color-white;
}
main {
flex-grow: 1;
flex-shrink: 0;
}
picture {
flex-shrink: 0;
}
a {
color: inherit;
text-decoration: none;
}
img,
svg {
display: block;
height: auto;
max-width: 100%;
}
button {
font: inherit;
color: inherit;
&:not([disabled]) {
cursor: pointer;
}
}
[hidden] {
/* stylelint-disable-next-line declaration-no-important */
display: none !important;
}

View File

@ -0,0 +1,8 @@
$color-gray-965: #f6f6f6; // hsl(0, 0%, 96.5%)
$color-gray-212: #363636; // hsl(0, 0%, 21.2%)
$color-white: #ffffff;
$color-black: #000000;
$color-blue: #2750d3;
$color-yellow: #ffd00e;
$color-red: #fe0000;

View File

@ -0,0 +1,39 @@
@font-face {
font-family: "Cera Pro";
font-weight: 400;
font-style: normal;
font-display: swap;
src: url("../fonts/cera-pro-400.woff2") format("woff2");
}
@font-face {
font-family: "Cera Pro";
font-weight: 500;
font-style: normal;
font-display: swap;
src: url("../fonts/cera-pro-500.woff2") format("woff2");
}
@font-face {
font-family: "Cera Pro";
font-weight: 700;
font-style: normal;
font-display: swap;
src: url("../fonts/cera-pro-700.woff2") format("woff2");
}
@font-face {
font-family: "Cera Pro";
font-weight: 900;
font-style: normal;
font-display: swap;
src: url("../fonts/cera-pro-900.woff2") format("woff2");
}
@font-face {
font-family: "DIN Condensed";
font-weight: 700;
font-style: normal;
font-display: swap;
src: url("../fonts/din-condensed-700.woff2") format("woff2");
}

View File

@ -0,0 +1,17 @@
@keyframes show-opacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes hide-opacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

View File

@ -0,0 +1,48 @@
@mixin desktop {
@media (min-width: $desktop) {
@content;
}
}
@mixin ultra-wide {
@media (aspect-ratio >= 2 / 1) {
@content;
}
}
@mixin list-reset {
margin: 0;
padding: 0;
list-style: none;
}
@mixin hide-scroll {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
@mixin transition($args...) {
transition-timing-function: ease-out;
transition-duration: 0.2s;
transition-property: $args;
}
@mixin word-break {
word-break: break-word;
overflow-wrap: break-word;
}
@mixin text-truncate {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
@mixin text-truncate-multiline($row-count: 3) {
-webkit-box-orient: vertical;
-webkit-line-clamp: $row-count;
display: -webkit-box;
overflow: hidden;
}

View File

@ -0,0 +1,18 @@
// fonts
$font-default: "Cera Pro", sans-serif;
$font-title: "DIN Condensed", sans-serif;
// retina
$retina-dpi: 144dpi;
$retina-dppx: 1.5dppx;
// container
$container-max: 1920px;
$container-min: 375px;
// breakpoints
$desktop: 1024px;
// padding
$padding: 10px;
$padding-desktop: 20px;

View File

@ -0,0 +1,5 @@
@import "variables";
@import "fonts";
@import "colors";
@import "mixins";
@import "basic";

View File

@ -0,0 +1,14 @@
.overlay {
@include hide-scroll;
position: fixed;
inset: 0;
z-index: 9;
display: none;
flex-direction: column;
padding: 10px;
overflow: scroll;
background-color: transparentize($color-black, 0.8);
&.show {
display: flex;
}
}

View File

@ -0,0 +1,3 @@
.scroll-lock {
overflow: hidden;
}

View File

@ -0,0 +1,15 @@
.visually-hidden:not(:focus, :active),
input[type="checkbox"].visually-hidden,
input[type="radio"].visually-hidden,
input[type="file"].visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
white-space: nowrap;
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(100%);
}

View File

@ -0,0 +1,3 @@
@import "overlay";
@import "scroll-lock";
@import "visually-hidden";

View File

@ -0,0 +1,11 @@
// In the symphony of life,
// order orchestrates the melody.
@use "sass:color";
@use "sass:math";
//
@import "vendor";
@import "global";
@import "helpers";
@import "components";

View File

@ -0,0 +1 @@
// @import "";

View File

@ -0,0 +1,11 @@
<footer class="l-footer b-footer">
<div class="container">
<div class="row">
<div class="column small-12 text-center">
<p class="copy">
Lorem ipsum dolor sit amet, <br>consectetur adipisicing.
</p>
</div>
</div>
</div>
</footer>

View File

@ -0,0 +1,24 @@
<header class="l-header b-header m-abs">
<div class="container b-header__wrp">
<div class="row align-middle">
<div class="column b-header__logo shrink">
<a href="./" class="b-logo">
<img class="b-logo--header" src="./img/logo.svg"/>
</a>
</div>
<div class="column b-header__menu shrink hide-for-small-only">
<nav class="b-header__menu-nav">
<ul class="inline-list">
</ul>
</nav>
</div>
<div class="column shrink b-header__burger">
<a class="menu-icon burger hide-for-large" data-menu-trigger="">
<span class="b-burger"><i></i></span>
</a>
</div>
</div>
</div>
</header>

View File

@ -0,0 +1,117 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0">
<title>{{ title }}</title>
<meta name="description" content="{{ description }}">
<meta name="gtm_name" content="{{ gtm_name }}">
<meta name="cdn" content="./">
<meta name="img_src" content="./img/bg/">
<link rel="shortcut icon" type="image/x-icon" href="https://{{ cdn }}/lands/images/favicons-en/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="https://{{ cdn }}/lands/images/favicons-en/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://{{ cdn }}/lands/images/favicons-en/favicon-32x32.png">
<meta name="theme-color" content="{{ theme_color }}">
<meta name="mobile-web-app-capable" content="yes">
<link rel="icon" type="image/png" sizes="192x192" href="https://{{ cdn }}/lands/images/favicons-en/android-chrome-192x192.png">
<link rel="manifest" href="https://{{ cdn }}/lands/images/favicons-en/manifest.json">
<meta name="apple-mobile-web-app-title" content="{{ app_title }}">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-icon" href="https://{{ cdn }}/lands/images/favicons-en/apple-touch-icon.png">
<meta name="application-name" content="{{ app_title }}">
<meta name="msapplication-navbutton-color" content="#db2532">
<meta name="msapplication-TileColor" content="#2b5797">
<meta name="msapplication-TileImage" content="https://{{ cdn }}/lands/images/favicons-en/mstile-150x150.png">
<meta name="msapplication-config" content="https://{{ cdn }}/lands/images/favicons-en/browserconfig.xml">
<link rel="mask-icon" type="image/png" href="https://{{ cdn }}/lands/images/favicons-en/safari-pinned-tab.svg" color="#db2532">
<link rel="stylesheet" type="text/css" href="https://{{ cdn }}/lands/styles/fonts.css">
<link rel="stylesheet" type="text/css" href="https://{{ cdn }}/lucid/styles/lucid.css">
<link rel="stylesheet" type="text/css" href="https://{{ cdn }}/lands/styles/markdown.css">
{# <link rel="stylesheet" type="text/css" href="https://{{ cdn }}/lucid/styles/ie10flex.css">#}
{% if {{ re_captcha }} %}
<!--<script src="https://www.google.com/recaptcha/api.js?render={{ re_captcha }}"></script>-->
{% endif %}
<script src="https://{{ cdn }}/lands/scripts/marked.min.js"></script>
<script src="https://{{ cdn }}/lands/scripts/jquery.min.js"></script>
{#<script src="https://{{ cdn }}/lands/scripts/jquery.cookie.js"></script>#}
{# <script src="https://{{ cdn }}/lands/scripts/jquery.form.min.js"></script>#}
<script src="https://{{ cdn }}/lands/scripts/jquery.mask.min.js"></script>
{# <script src="https://{{ cdn }}/lands/scripts/owl.carousel.min.js"></script>#}
<script src="https://{{ cdn }}/lands/scripts/utm.js"></script>
{% if show_qr == 'true' %}
<script src="https://{{ cdn }}/lands/scripts/qrcode.min.js"></script>
{% endif %}
{# <script src="https://text.ajaxfeed.com/texts/line.js"></script>#}
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-M35HZTK6');</script>
<!-- End Google Tag Manager -->
<!-- Yandex.Metrika counter -->
<script type="text/javascript" > (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; m[i].l=1*new Date(); for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }} k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)}) (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym"); ym(40810904, "init", { clickmap:true, trackLinks:true, accurateTrackBounce:true, webvisor:true, trackHash:true }); </script>
<!-- /Yandex.Metrika counter -->
<!-- -->
<link rel="stylesheet" href="./css/foundation-lib.css?v={{ version }}">
<link rel="stylesheet" href="./css/style.css?v={{ version }}">
<script type="text/javascript" src="./js/app.js?v={{ version }}"></script>
<script type="text/javascript" src="./js/auth.js?v={{ version }}"></script>
<script type="text/javascript" src="./js/script.js?v={{ version }}"></script>
{% block extraJS %}
{% endblock %}
{% block preload %}
{% endblock %}
</head>
{% block bodyBegin %}
<body class="{{ body_class }} {% if theme%}theme-{{ theme }} {% endif %}{% if landing%}landing-{{ landing }}{% endif %}{% if class %} {{ class }}{% endif %}">
{% endblock %}
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-M35HZTK6"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
{% block modal %}
{% include 'layouts/_modal.twig' %}
{% endblock %}
<div class="l-wrapper">
{% block header %}
{% include "layouts/_header.twig" %}
{% endblock %}
<main class="l-main" id="main">
{% block content %}{% endblock %}
</main>
{% block footer %}
{% include "layouts/_footer.twig" %}
{% endblock %}
</div>
{% block extraJsLib %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,75 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0">
<meta property="og:type" content="website">
<meta property="og:site_name" content="">
<meta property="og:title" content="{{ title }}">
<meta property="og:description" content="{{ description }}">
<meta property="og:url" content="">
<meta property="og:locale" content="ru_RU">
<meta property="og:image" content="img/logo.png"/>
<meta property="og:image:type" content="image/png"/>
<meta property="og:image:width" content="300">
<meta property="og:image:height" content="200">
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:url" content=""/>
<meta name="twitter:title" content="{{ title }}"/>
<meta name="twitter:description" content="{{ description }}"/>
<meta name="twitter:creator" content="@rudenich_"/>
<meta name="twitter:image:src" content="img/logo.png"/>
<meta name="twitter:image:width" content="300"/>
<meta name="twitter:image:height" content="200"/>
<meta name="twitter:site" content=""/>
<script>!function(c){"use strict";function e(e,t,n,o){var r,i=c.document,d=i.createElement("link");if(t)r=t;else{var a=(i.body||i.getElementsByTagName("head")[0]).childNodes;r=a[a.length-1]}var f=i.styleSheets;if(o)for(var l in o)o.hasOwnProperty(l)&&d.setAttribute(l,o[l]);d.rel="stylesheet",d.href=e,d.media="only x",function e(t){if(i.body)return t();setTimeout(function(){e(t)})}(function(){r.parentNode.insertBefore(d,t?r:r.nextSibling)});var s=function(e){for(var t=d.href,n=f.length;n--;)if(f[n].href===t)return e();setTimeout(function(){s(e)})};function u(){d.addEventListener&&d.removeEventListener("load",u),d.media=n||"all"}return d.addEventListener&&d.addEventListener("load",u),(d.onloadcssdefined=s)(u),d}"undefined"!=typeof exports?exports.loadCSS=e:c.loadCSS=e}("undefined"!=typeof global?global:this);</script>
<script>
//loadCSS('./css/foundation-lib.css')
loadCSS('./css/vendor.css')
//loadCSS('./css/style.css')
</script>
<link rel="stylesheet" href="css/foundation-lib.css">
{#<link rel="stylesheet" href="css/vendor.css">#}
<link rel="stylesheet" href="css/style.css">
</head>
{% block bodyBegin %}
<body class="{{ body_class }} ">
{% endblock %}
<div class="l-wrapper">
{% block header %}
{% include "layouts/_header.twig" %}
{% endblock %}
<main class="l-main" id="main">
{% block content %}{% endblock %}
</main>
{% block footer %}
{% include "layouts/_footer.twig" %}
{% endblock %}
</div>
{% block modal %}
{% include 'layouts/_modal.twig' %}
{% endblock %}
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
{#<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDGS_V5ZeY_F7e3PhrLcs8U2vGYV_e637U"></script>#}
{% block extraJsLib %}{% endblock %}
<script type="text/javascript" src="js/libs.js"></script>
<script type="text/javascript" src="js/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,112 @@
<!-- Modals -->
<!-- authStart-->
<div id="authStart" class="b-modal b-modal--form b-modal--dark b-modal--wide" style="display: none">
<div class="b-modal__bg"></div>
<div class="b-modal__body">
<a data-gtm="app_popup_close" class="b-modal__close btn-close" href="#">
<svg xmlns="http://www.w3.org/2000/svg" width="32.828" height="32.828" viewBox="0 0 32.828 32.828">
<g id="close" transform="translate(-7.586 -7.586)">
<path id="Контур_8334" data-name="Контур 8334" d="M9,9,39,39" transform="translate(0 0)" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" />
<path id="Контур_8335" data-name="Контур 8335" d="M39,9,9,39" transform="translate(0.001 0)" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" />
</g>
</svg>
</a>
<div class="size-em bg-el bg-el--left show-for-tablet">
<picture class="img-render">
<source srcset="./img/bg/reg_coin.webp" type="image/webp">
<img loading="lazy" class="img-adaptive --em" width="195" src="./img/bg/reg_coin.png" alt="pic">
</picture>
</div>
<div class="size-em bg-el bg-el--right show-for-tablet">
<picture class="img-render">
<source srcset="./img/bg/reg_ovi.webp" type="image/webp">
<img loading="lazy" class="img-adaptive --em" width="192" src="./img/bg/reg_ovi.png" alt="pic">
</picture>
</div>
<div class="b-modal__form">
<div class="b-modal__content">
<div class="s-hero__section-head text-center">
<div class="section-title mb-0">
Для участия в акции <br>войди в свой аккаунт
</div>
<div class="b-modal__btn">
<a class="btn m-large m-white" target="_blank" href="https://fon.bet/bonuses?promoId=INFO_RU_CRM_ACT_OVI_REC">Войди с FONBET</a>
</div>
</div>
<div class="s-hero__section-form pt-4">
<div class="b-reg text-center">
<div class="b-reg__txt">
Зарегистрироваться <br class="hide-for-tablet">и получить фрибет <br>
<span class="bold _large">до 15 000 р</span><br>
без депозита.
</div>
<div class="b-reg__btn">
<a class="btn m-large btn-gradient m-red" target="_blank" href="https://fon.bet/?deepLink=start_auth_process&promoAlias=thegr8chase_rekord">ЗАРЕГИСТРИРОВАТЬСЯ</a>
</div>
</div>
</div>
<div class="b-modal__pic fullwidth text-center hide-for-tablet">
<div class="size-em bg-el bg-el--coin">
<picture class="img-render">
<source srcset="./img/bg/reg_coin.webp" type="image/webp">
<img loading="lazy" class="img-adaptive --em" width="195" src="./img/bg/reg_coin.png" alt="pic">
</picture>
</div>
<picture class="img-render img-ovi">
<source srcset="./img/bg/reg_ovi_m.webp" type="image/webp">
<img loading="lazy" class="" width="256" src="./img/bg/reg_ovi_m.png" alt="pic">
</picture>
</div>
</div>
</div>
</div>
</div>
<div id="formSuccess" class="b-modal b-modal--form b-modal--popup b-modal--wide" style="display: none">
<div style="pointer-events: none" class="b-modal__bg"></div>
<div class="b-modal__body">
<a data-gtm="app_popup_close" class="b-modal__close btn-close" href="http://fon.bet/promo/thegr8chase/">
<svg xmlns="http://www.w3.org/2000/svg" width="32.828" height="32.828" viewBox="0 0 32.828 32.828">
<g id="close" transform="translate(-7.586 -7.586)">
<path id="Контур_8334" data-name="Контур 8334" d="M9,9,39,39" transform="translate(0 0)" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" />
<path id="Контур_8335" data-name="Контур 8335" d="M39,9,9,39" transform="translate(0.001 0)" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" />
</g>
</svg>
</a>
<div class="b-modal__form">
<div class="b-modal__content">
<div class="s-hero__section-head text-center">
<div class="s-openarena__title font2 _small text--primary mb-0">
ПОЧТИ <br class="hide-for-tablet">ГОТОВО!
</div>
</div>
<div class="s-hero__section-form pt-2 pb-2 text-center">
<div class="s-openarena__txt">
Сделай ставку на любой вид спорта <br class="show-for-tablet">
в&nbsp;FONBET от&nbsp;500&nbsp;рублей, чтобы принять <br class="show-for-tablet">
участие в&nbsp;розыгрыше слотов на&nbsp;турнир. <br class="show-for-tablet">
Мы подведем результаты розыгрыша 15&nbsp;июля!
</div>
</div>
<div class="s-openarena__btn pt-3 text-center mt-auto mb-auto">
<a class="btn m-large" href="https://fon.bet">ЗАЛЕТАЙ В ИГРУ</a>
</div>
</div>
</div>
</div>
</div>
<!--// Modals -->

View File

@ -0,0 +1,25 @@
{% extends "layouts/_main-lp.twig" %}
{% block bodyBegin %}
{% set body_class = 'has-transparent-header height-100% cera-pro no-recaptcha static-form-mob form-is-hide' %}
{{ parent() }}
{% endblock %}
{% block preload %}
{# <link rel="preload" as="image" href="./img/bg/bg_popup_bar.png" media="(min-width:1024px)">#}
{# <link rel="preload" as="image" href="./img/bg/bg_popup_bar_mob.jpg" media="(max-width:1023px)">#}
{% endblock %}
{% block footer %}{% endblock %}
{% block header %}{% endblock %}
{% block modal %}{% endblock %}
{% block extraJS %}
{% endblock %}
{% block content %}
<!------->
{% include 'partial/_hero.twig' %}
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends "layouts/_main-lp.twig" %}
{% block bodyBegin %}
{% set body_class = 'has-transparent-header height-100% cera-pro no-recaptcha static-form-mob form-is-hide' %}
{{ parent() }}
{% endblock %}
{% block preload %}
{# <link rel="preload" as="image" href="./img/bg/bg_popup_bar.png" media="(min-width:1024px)">#}
{# <link rel="preload" as="image" href="./img/bg/bg_popup_bar_mob.jpg" media="(max-width:1023px)">#}
{% endblock %}
{% block footer %}{% endblock %}
{% block header %}{% endblock %}
{# {% block modal %}{% endblock %} #}
{% block extraJS %}
{% endblock %}
{% block content %}
<!------->
{% include 'partial/_hero.twig' %}
{% endblock %}

View File

@ -0,0 +1,17 @@
{% set image_url = './img/owl.jpg' %}
{% set image_alt = 'A descriptive alt text for the image' %}
{% set title = 'Welcome to Our Website' %}
{% set subtitle = 'Discover amazing content and features' %}
{% set cta_text = 'Get Started' %}
{% set cta_link = 'https://example.com/get-started' %}
<section class="hero">
<div class="hero__content">
<h1 class="hero__title">{{ title }}</h1>
<p class="hero__subtitle">{{ subtitle }}</p>
<a href="{{ cta_link }}" class="hero__cta">{{ cta_text }}</a>
</div>
<div class="hero__image">
<img width="500" src="{{ image_url }}" alt="{{ image_alt }}">
</div>
</section>

View File

@ -0,0 +1,46 @@
{
"common": [
"//clientsapi01w.bk6bba-resources.com",
"//clientsapi02w.bk6bba-resources.com",
"//clientsapi03w.bk6bba-resources.com",
"//clientsapi04w.bk6bba-resources.com",
"//clientsapi05w.bk6bba-resources.com",
"//clientsapi06w.bk6bba-resources.com",
"//clientsapi31w.bk6bba-resources.com",
"//clientsapi51w.bk6bba-resources.com",
"//clientsapi52w.bk6bba-resources.com"
],
"line": [
"//line01w.bk6bba-resources.com",
"//line02w.bk6bba-resources.com",
"//line03w.bk6bba-resources.com",
"//line04w.bk6bba-resources.com",
"//line05w.bk6bba-resources.com",
"//line06w.bk6bba-resources.com",
"//line07w.bk6bba-resources.com",
"//line08w.bk6bba-resources.com",
"//line31w.bk6bba-resources.com",
"//line32w.bk6bba-resources.com",
"//line51w.bk6bba-resources.com",
"//line52w.bk6bba-resources.com",
"//line53w.bk6bba-resources.com",
"//line54w.bk6bba-resources.com",
"//line55w.bk6bba-resources.com"
],
"lineByScopeMarket": {},
"static": "//origin.bk6bba-resources.com",
"commonApi": "//fastviewdata.bk6bba-resources.com",
"staticSiteDir": "/webStaticRed/website",
"mapOfAdditionalOriginURIs": {},
"updateUtcDate": "Fri, 06 Dec 2024 13:39:24 GMT",
"site": {
"version": "1.43.56",
"forceUpdateVersion": 0,
"environment": "production",
"ref": "bWFzdGVy",
"configHash": "a4f175658adc16c9d8586b78dba20207"
},
"enableClickStream": true,
"defaultFracSize": 0,
"betRoundAccuracy": 1
}

View File

@ -0,0 +1,240 @@
#!/usr/bin/env node
import fs from 'fs'
import path from 'path'
import { execSync } from 'child_process'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const sslDir = path.join(__dirname, 'ssl')
const keyPath = path.join(sslDir, 'server.key')
const certPath = path.join(sslDir, 'server.crt')
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m'
}
const log = (message, color = 'reset') => {
console.log(`${colors[color]}${message}${colors.reset}`)
}
const checkOpenSSL = () => {
try {
execSync('openssl version', { stdio: 'pipe' })
return true
} catch (error) {
return false
}
}
const installInstructions = () => {
log('\n📋 Инструкции по установке OpenSSL:', 'cyan')
log('Windows: https://slproweb.com/products/Win32OpenSSL.html', 'blue')
log('macOS: brew install openssl', 'blue')
log('Ubuntu/Debian: sudo apt-get install openssl', 'blue')
log('CentOS/RHEL: sudo yum install openssl', 'blue')
}
const createCertificates = (domain = 'localhost', days = 365) => {
if (!fs.existsSync(sslDir)) {
fs.mkdirSync(sslDir, { recursive: true })
log(`📁 Создана папка: ${sslDir}`, 'green')
}
try {
log('🔐 Создание SSL сертификатов...', 'yellow')
// Генерируем приватный ключ
log('📝 Генерация приватного ключа...', 'blue')
execSync(`openssl genrsa -out "${keyPath}" 2048`, { stdio: 'pipe' })
// Конфигурация для сертификата с SAN
const configContent = `
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
C = RU
ST = Development
L = Local
O = Development Server
OU = IT Department
CN = ${domain}
[v3_req]
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = ${domain}
DNS.2 = *.${domain}
DNS.3 = localhost
DNS.4 = *.localhost
IP.1 = 127.0.0.1
IP.2 = ::1
`
const configPath = path.join(sslDir, 'openssl.conf')
fs.writeFileSync(configPath, configContent.trim())
// Генерируем сертификат
log('📜 Генерация сертификата...', 'blue')
const certCommand = `openssl req -new -x509 -key "${keyPath}" -out "${certPath}" -days ${days} -config "${configPath}" -extensions v3_req`
execSync(certCommand, { stdio: 'pipe' })
// Удаляем временный конфиг
fs.unlinkSync(configPath)
log('\n✅ SSL сертификаты успешно созданы!', 'green')
log(`🔑 Ключ: ${keyPath}`, 'cyan')
log(`📄 Сертификат: ${certPath}`, 'cyan')
log(`🌐 Домен: ${domain}`, 'cyan')
log(`⏰ Действителен: ${days} дней`, 'cyan')
// Показываем информацию о сертификате
showCertInfo()
log('\n⚠ Важно:', 'yellow')
log('• Это сертификаты только для разработки', 'yellow')
log('• Добавьте сертификат в доверенные в браузере для устранения предупреждений', 'yellow')
} catch (error) {
log('\n❌ Ошибка создания SSL сертификатов:', 'red')
log(error.message, 'red')
installInstructions()
}
}
const showCertInfo = () => {
if (!fs.existsSync(certPath)) {
log('❌ Сертификат не найден', 'red')
return
}
try {
log('\n📋 Информация о сертификате:', 'cyan')
const certInfo = execSync(`openssl x509 -in "${certPath}" -text -noout`, { encoding: 'utf8' })
// Извлекаем нужную информацию
const subjectMatch = certInfo.match(/Subject: (.+)/);
const validFromMatch = certInfo.match(/Not Before: (.+)/);
const validToMatch = certInfo.match(/Not After : (.+)/);
const sanMatch = certInfo.match(/DNS:([^,\n]+)/g);
if (subjectMatch) log(`📝 Subject: ${subjectMatch[1].trim()}`, 'blue')
if (validFromMatch) log(`📅 Действителен с: ${validFromMatch[1].trim()}`, 'blue')
if (validToMatch) log(`📅 Действителен до: ${validToMatch[1].trim()}`, 'blue')
if (sanMatch) {
log(`🌐 Домены: ${sanMatch.map(d => d.replace('DNS:', '')).join(', ')}`, 'blue')
}
} catch (error) {
log('❌ Ошибка чтения информации о сертификате', 'red')
}
}
const deleteCertificates = () => {
let deleted = false
if (fs.existsSync(keyPath)) {
fs.unlinkSync(keyPath)
log(`🗑️ Удален ключ: ${keyPath}`, 'yellow')
deleted = true
}
if (fs.existsSync(certPath)) {
fs.unlinkSync(certPath)
log(`🗑️ Удален сертификат: ${certPath}`, 'yellow')
deleted = true
}
if (deleted) {
log('✅ SSL сертификаты удалены', 'green')
} else {
log(' SSL сертификаты не найдены', 'blue')
}
}
const checkCertificates = () => {
const keyExists = fs.existsSync(keyPath)
const certExists = fs.existsSync(certPath)
log('\n🔍 Проверка SSL сертификатов:', 'cyan')
log(`🔑 Ключ: ${keyExists ? '✅ Существует' : '❌ Не найден'}`, keyExists ? 'green' : 'red')
log(`📄 Сертификат: ${certExists ? '✅ Существует' : '❌ Не найден'}`, certExists ? 'green' : 'red')
if (keyExists && certExists) {
showCertInfo()
}
return keyExists && certExists
}
const showHelp = () => {
log('\n🔧 SSL Manager - Утилита для управления SSL сертификатами\n', 'cyan')
log('Использование:', 'yellow')
log(' node ssl-manager.js <команда> [опции]\n', 'blue')
log('Команды:', 'yellow')
log(' create [domain] [days] Создать новые сертификаты (по умолчанию: localhost, 365 дней)', 'blue')
log(' check Проверить существующие сертификаты', 'blue')
log(' info Показать информацию о сертификате', 'blue')
log(' delete Удалить существующие сертификаты', 'blue')
log(' help Показать эту справку\n', 'blue')
log('Примеры:', 'yellow')
log(' node ssl-manager.js create', 'green')
log(' node ssl-manager.js create mysite.local 30', 'green')
log(' node ssl-manager.js check', 'green')
}
// Основная логика
const command = process.argv[2]
const arg1 = process.argv[3]
const arg2 = process.argv[4]
// Проверяем наличие OpenSSL
if (!checkOpenSSL() && command !== 'help' && command !== 'check') {
log('❌ OpenSSL не найден в системе', 'red')
installInstructions()
process.exit(1)
}
switch (command) {
case 'create':
const domain = arg1 || 'localhost'
const days = parseInt(arg2) || 365
createCertificates(domain, days)
break
case 'check':
checkCertificates()
break
case 'info':
showCertInfo()
break
case 'delete':
deleteCertificates()
break
case 'help':
case undefined:
showHelp()
break
default:
log(`❌ Неизвестная команда: ${command}`, 'red')
showHelp()
process.exit(1)
}