core-maugli 1.2.58 → 1.2.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/scripts/generate-previews-build.js +104 -0
- package/scripts/generate-previews.js +8 -9
- package/scripts/resize-for-build.cjs +42 -28
- package/src/components/ArticleMeta.astro +27 -2
- package/src/components/Avatar.astro +26 -1
- package/src/components/BaseHead.astro +12 -12
- package/src/components/RubricCard.astro +2 -2
- package/src/components/TableOfContents.astro +3 -3
- package/src/i18n/de.json +2 -1
- package/src/i18n/en.json +2 -1
- package/src/i18n/es.json +2 -1
- package/src/i18n/fr.json +2 -1
- package/src/i18n/ja.json +2 -1
- package/src/i18n/pt.json +2 -1
- package/src/i18n/ru.json +2 -1
- package/src/i18n/zh.json +2 -1
- package/src/utils/image-utils.ts +76 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "core-maugli",
|
|
3
3
|
"description": "Astro & Tailwind CSS blog theme for Maugli.",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "1.2.
|
|
5
|
+
"version": "1.2.59",
|
|
6
6
|
"license": "GPL-3.0-or-later OR Commercial",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
21
|
"typograf": "node typograf-batch.js",
|
|
22
|
-
"dev": "
|
|
23
|
-
"prestart": "
|
|
22
|
+
"dev": "astro dev",
|
|
23
|
+
"prestart": "astro dev",
|
|
24
24
|
"start": "astro dev",
|
|
25
|
-
"build": "node scripts/check-version.js && node scripts/flatten-images.cjs && node scripts/optimize-images.cjs && node typograf-batch.js && node scripts/verify-assets.js &&
|
|
26
|
-
"build:fast": "node typograf-batch.js && node scripts/verify-assets.js &&
|
|
27
|
-
"build:no-check": "node scripts/flatten-images.cjs && node scripts/optimize-images.cjs && node typograf-batch.js && node scripts/verify-assets.js &&
|
|
25
|
+
"build": "node scripts/check-version.js && node scripts/flatten-images.cjs && node scripts/optimize-images.cjs && node typograf-batch.js && node scripts/verify-assets.js && astro build",
|
|
26
|
+
"build:fast": "node typograf-batch.js && node scripts/verify-assets.js && astro build",
|
|
27
|
+
"build:no-check": "node scripts/flatten-images.cjs && node scripts/optimize-images.cjs && node typograf-batch.js && node scripts/verify-assets.js && astro build",
|
|
28
28
|
"optimize": "node scripts/optimize-images.cjs",
|
|
29
29
|
"optimize:squoosh": "node scripts/squoosh-optimize.js",
|
|
30
30
|
"clean:resized": "node scripts/clean-resized-images.js",
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
// Универсальное определение корня проекта
|
|
10
|
+
const rootDir = __dirname.includes('node_modules')
|
|
11
|
+
? path.join(__dirname, '../../..')
|
|
12
|
+
: path.join(__dirname, '..');
|
|
13
|
+
|
|
14
|
+
// Размеры для разных типов контента
|
|
15
|
+
const blogPreviewWidth = 400;
|
|
16
|
+
const blogPreviewHeight = 210;
|
|
17
|
+
const rubricPreviewWidth = 210;
|
|
18
|
+
const rubricPreviewHeight = 214;
|
|
19
|
+
|
|
20
|
+
// Генерируем в dist вместо public
|
|
21
|
+
const outputDir = path.join(rootDir, 'dist');
|
|
22
|
+
|
|
23
|
+
// Функция для создания превью в dist
|
|
24
|
+
async function createPreviewForBuild(sourcePath, outputPath, width, height) {
|
|
25
|
+
const previewDir = path.dirname(outputPath);
|
|
26
|
+
if (!fs.existsSync(previewDir)) {
|
|
27
|
+
fs.mkdirSync(previewDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (fs.existsSync(outputPath)) {
|
|
31
|
+
return; // Превью уже существует
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await sharp(sourcePath)
|
|
36
|
+
.resize(width, height, { fit: 'cover' })
|
|
37
|
+
.toFile(outputPath);
|
|
38
|
+
console.log(`✅ Превью создано: ${path.relative(rootDir, outputPath)}`);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error(`❌ Ошибка при создании превью ${outputPath}:`, error.message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Функция для обработки папки
|
|
45
|
+
async function processDirectory(sourceDir, outputSubDir) {
|
|
46
|
+
if (!fs.existsSync(sourceDir)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const items = fs.readdirSync(sourceDir);
|
|
51
|
+
|
|
52
|
+
for (const item of items) {
|
|
53
|
+
const sourcePath = path.join(sourceDir, item);
|
|
54
|
+
const stat = fs.statSync(sourcePath);
|
|
55
|
+
|
|
56
|
+
if (stat.isDirectory()) {
|
|
57
|
+
if (item === 'previews') continue; // Пропускаем папки превью
|
|
58
|
+
await processDirectory(sourcePath, path.join(outputSubDir, item));
|
|
59
|
+
} else if (item.match(/\.(webp|jpg|jpeg|png)$/i)) {
|
|
60
|
+
const ext = path.extname(item);
|
|
61
|
+
const name = path.basename(item, ext);
|
|
62
|
+
|
|
63
|
+
// Пропускаем файлы, которые уже содержат размер
|
|
64
|
+
const hasResizeSuffix = [400, 800, 1200].some(size => name.includes(`-${size}`));
|
|
65
|
+
if (hasResizeSuffix) continue;
|
|
66
|
+
|
|
67
|
+
// Определяем размер превью
|
|
68
|
+
let previewWidth, previewHeight;
|
|
69
|
+
if (sourcePath.includes('/img/default/') && (name.includes('rubric') || name.includes('tag'))) {
|
|
70
|
+
previewWidth = rubricPreviewWidth;
|
|
71
|
+
previewHeight = rubricPreviewHeight;
|
|
72
|
+
} else {
|
|
73
|
+
previewWidth = blogPreviewWidth;
|
|
74
|
+
previewHeight = blogPreviewHeight;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Создаем превью
|
|
78
|
+
const outputDir = path.join(rootDir, 'dist', outputSubDir, 'previews');
|
|
79
|
+
const outputPath = path.join(outputDir, `${name}${ext}`);
|
|
80
|
+
|
|
81
|
+
console.log(`🎭 Создаем превью для сборки: ${name}`);
|
|
82
|
+
await createPreviewForBuild(sourcePath, outputPath, previewWidth, previewHeight);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Основная функция
|
|
88
|
+
async function generatePreviewsForBuild() {
|
|
89
|
+
console.log('🚀 Начинаем генерацию превью для сборки...');
|
|
90
|
+
|
|
91
|
+
// Обрабатываем системные папки
|
|
92
|
+
await processDirectory(path.join(rootDir, 'public/img/default'), 'img/default');
|
|
93
|
+
await processDirectory(path.join(rootDir, 'public/img/examples'), 'img/examples');
|
|
94
|
+
|
|
95
|
+
// Обрабатываем пользовательские изображения, если они есть
|
|
96
|
+
const pageImagesDir = path.join(rootDir, 'public/img/page-images');
|
|
97
|
+
if (fs.existsSync(pageImagesDir)) {
|
|
98
|
+
await processDirectory(pageImagesDir, 'img/page-images');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log('✅ Генерация превью для сборки завершена!');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
generatePreviewsForBuild().catch(console.error);
|
|
@@ -16,6 +16,8 @@ const blogPreviewWidth = 400;
|
|
|
16
16
|
const blogPreviewHeight = 210;
|
|
17
17
|
const rubricPreviewWidth = 210; // Увеличенный размер для качества на retina дисплеях (105px * 2)
|
|
18
18
|
const rubricPreviewHeight = 214; // Увеличенный размер для качества на retina дисплеях (107px * 2)
|
|
19
|
+
const authorPreviewWidth = 192; // Квадратный размер для аватарок
|
|
20
|
+
const authorPreviewHeight = 192;
|
|
19
21
|
|
|
20
22
|
// Функция для извлечения путей изображений из markdown файлов
|
|
21
23
|
function extractImagePaths() {
|
|
@@ -62,7 +64,7 @@ function extractImagePaths() {
|
|
|
62
64
|
} else {
|
|
63
65
|
imagePath = match.match(/src\s*=\s*['"]*([^'">\s]+)['"]*/)?.[1];
|
|
64
66
|
}
|
|
65
|
-
if (imagePath && !imagePath.startsWith('http') && imagePath.includes('/')
|
|
67
|
+
if (imagePath && !imagePath.startsWith('http') && imagePath.includes('/')) {
|
|
66
68
|
imagePaths.add(imagePath);
|
|
67
69
|
}
|
|
68
70
|
});
|
|
@@ -73,21 +75,16 @@ function extractImagePaths() {
|
|
|
73
75
|
|
|
74
76
|
scanDirectory(contentDir);
|
|
75
77
|
|
|
76
|
-
// Также добавляем изображения из public/img/examples/
|
|
78
|
+
// Также добавляем изображения из public/img/examples/
|
|
77
79
|
const examplesDir = path.join(rootDir, 'public/img/examples');
|
|
78
80
|
if (fs.existsSync(examplesDir)) {
|
|
79
81
|
function addExampleImages(dir, relativePath = '') {
|
|
80
|
-
// Пропускаем папку authors - аватары не нужно обрабатывать
|
|
81
|
-
if (relativePath.includes('authors/')) return;
|
|
82
|
-
|
|
83
82
|
const items = fs.readdirSync(dir);
|
|
84
83
|
for (const item of items) {
|
|
85
84
|
const itemPath = path.join(dir, item);
|
|
86
85
|
const stat = fs.statSync(itemPath);
|
|
87
86
|
|
|
88
87
|
if (stat.isDirectory()) {
|
|
89
|
-
// Пропускаем папку authors
|
|
90
|
-
if (item === 'authors') continue;
|
|
91
88
|
addExampleImages(itemPath, `${relativePath}${item}/`);
|
|
92
89
|
} else if (item.match(/\.(webp|jpg|jpeg|png)$/i) && !dir.includes('previews')) {
|
|
93
90
|
imagePaths.add(`/img/examples/${relativePath}${item}`);
|
|
@@ -103,8 +100,6 @@ function extractImagePaths() {
|
|
|
103
100
|
const items = fs.readdirSync(defaultDir);
|
|
104
101
|
for (const item of items) {
|
|
105
102
|
if (item.match(/\.(webp|jpg|jpeg|png)$/i)) {
|
|
106
|
-
// Исключаем изображения авторов и рубрик
|
|
107
|
-
if (item.includes('autor') || item.includes('author') || item.includes('rubric')) continue;
|
|
108
103
|
imagePaths.add(`/img/default/${item}`);
|
|
109
104
|
}
|
|
110
105
|
}
|
|
@@ -160,6 +155,10 @@ async function createPreview(imagePath) {
|
|
|
160
155
|
previewWidth = rubricPreviewWidth;
|
|
161
156
|
previewHeight = rubricPreviewHeight;
|
|
162
157
|
console.log(`Creating rubric preview (${previewWidth}x${previewHeight}): ${name}`);
|
|
158
|
+
} else if (imagePath.includes('author') || name.includes('autor')) {
|
|
159
|
+
previewWidth = authorPreviewWidth;
|
|
160
|
+
previewHeight = authorPreviewHeight;
|
|
161
|
+
console.log(`Creating author preview (${previewWidth}x${previewHeight}): ${name}`);
|
|
163
162
|
} else {
|
|
164
163
|
previewWidth = blogPreviewWidth;
|
|
165
164
|
previewHeight = blogPreviewHeight;
|
|
@@ -1,26 +1,37 @@
|
|
|
1
|
-
// resize-for-build.cjs -
|
|
1
|
+
// resize-for-build.cjs - Generate resized images for build in dist/
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const sharp = require('sharp');
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// Sizes to generate
|
|
7
7
|
const sizes = [400, 800, 1200];
|
|
8
8
|
|
|
9
9
|
const inputDir = './public';
|
|
10
|
-
const outputDir = './dist';
|
|
10
|
+
const outputDir = './dist';
|
|
11
11
|
const processedFiles = new Set();
|
|
12
12
|
|
|
13
|
-
//
|
|
13
|
+
// Function to create directory if it doesn't exist
|
|
14
14
|
function ensureDir(dir) {
|
|
15
15
|
if (!fs.existsSync(dir)) {
|
|
16
16
|
fs.mkdirSync(dir, { recursive: true });
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
//
|
|
20
|
+
// Copy file if it doesn't exist
|
|
21
|
+
function copyIfNotExists(src, dest) {
|
|
22
|
+
if (!fs.existsSync(dest)) {
|
|
23
|
+
ensureDir(path.dirname(dest));
|
|
24
|
+
fs.copyFileSync(src, dest);
|
|
25
|
+
console.log(`📋 Copied: ${path.relative('./dist', dest)}`);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Recursive function to process directories
|
|
21
32
|
function processDirectory(dir, relativePath = '') {
|
|
22
33
|
if (!fs.existsSync(dir)) {
|
|
23
|
-
console.log(
|
|
34
|
+
console.log(`Directory ${dir} does not exist`);
|
|
24
35
|
return;
|
|
25
36
|
}
|
|
26
37
|
|
|
@@ -32,67 +43,70 @@ function processDirectory(dir, relativePath = '') {
|
|
|
32
43
|
const currentRelativePath = path.join(relativePath, item);
|
|
33
44
|
|
|
34
45
|
if (stat.isDirectory()) {
|
|
35
|
-
//
|
|
46
|
+
// Recursively process subdirectories
|
|
36
47
|
processDirectory(itemPath, currentRelativePath);
|
|
37
48
|
} else if (stat.isFile()) {
|
|
38
49
|
const ext = path.extname(item).toLowerCase();
|
|
39
50
|
const baseName = path.basename(item, ext);
|
|
40
51
|
|
|
41
|
-
//
|
|
52
|
+
// Check if it's an image and doesn't contain size in name
|
|
42
53
|
if (['.jpg', '.jpeg', '.png', '.webp'].includes(ext)) {
|
|
43
|
-
//
|
|
54
|
+
// Exclude PWA icons and system files
|
|
44
55
|
const excludePatterns = [
|
|
45
|
-
'icon-192', 'icon-512', // PWA
|
|
46
|
-
'favicon', //
|
|
47
|
-
'logo', //
|
|
48
|
-
'manifest' //
|
|
56
|
+
'icon-192', 'icon-512', // PWA icons
|
|
57
|
+
'favicon', // Favicons
|
|
58
|
+
'logo', // Logos
|
|
59
|
+
'manifest' // Manifest files
|
|
49
60
|
];
|
|
50
61
|
|
|
51
62
|
const shouldExclude = excludePatterns.some(pattern => baseName.includes(pattern));
|
|
52
63
|
|
|
53
|
-
//
|
|
64
|
+
// Skip files that already contain size
|
|
54
65
|
const hasResizeSuffix = sizes.some(size => baseName.includes(`-${size}`));
|
|
55
66
|
|
|
56
67
|
if (!hasResizeSuffix && !shouldExclude && !processedFiles.has(itemPath)) {
|
|
57
68
|
processedFiles.add(itemPath);
|
|
58
69
|
|
|
59
|
-
console.log(`🔄
|
|
70
|
+
console.log(`🔄 Processing for build: ${currentRelativePath}`);
|
|
60
71
|
|
|
61
|
-
//
|
|
72
|
+
// First copy original
|
|
62
73
|
const outputDirPath = path.join(outputDir, relativePath);
|
|
63
|
-
ensureDir(outputDirPath);
|
|
64
|
-
|
|
65
74
|
const originalOutputPath = path.join(outputDirPath, item);
|
|
66
|
-
|
|
67
|
-
fs.copyFileSync(itemPath, originalOutputPath);
|
|
68
|
-
console.log(`📋 Скопирован оригинал: ${path.relative('./dist', originalOutputPath)}`);
|
|
69
|
-
}
|
|
75
|
+
copyIfNotExists(itemPath, originalOutputPath);
|
|
70
76
|
|
|
71
|
-
//
|
|
77
|
+
// Generate resized versions
|
|
72
78
|
sizes.forEach(width => {
|
|
73
79
|
const outputPath = path.join(outputDirPath, `${baseName}-${width}${ext}`);
|
|
74
80
|
|
|
75
81
|
if (!fs.existsSync(outputPath)) {
|
|
82
|
+
ensureDir(path.dirname(outputPath));
|
|
76
83
|
sharp(itemPath)
|
|
77
84
|
.resize(width)
|
|
78
85
|
.toFile(outputPath, (err) => {
|
|
79
86
|
if (err) {
|
|
80
|
-
console.error(`❌
|
|
87
|
+
console.error(`❌ Error creating ${outputPath}:`, err.message);
|
|
81
88
|
} else {
|
|
82
|
-
console.log(`✅
|
|
89
|
+
console.log(`✅ Created: ${path.relative('./dist', outputPath)}`);
|
|
83
90
|
}
|
|
84
91
|
});
|
|
92
|
+
} else {
|
|
93
|
+
console.log(`⏭️ Skipped (exists): ${path.relative('./dist', outputPath)}`);
|
|
85
94
|
}
|
|
86
95
|
});
|
|
87
96
|
}
|
|
97
|
+
} else {
|
|
98
|
+
// Copy non-image files as is
|
|
99
|
+
const outputDirPath = path.join(outputDir, relativePath);
|
|
100
|
+
const outputPath = path.join(outputDirPath, item);
|
|
101
|
+
copyIfNotExists(itemPath, outputPath);
|
|
88
102
|
}
|
|
89
103
|
}
|
|
90
104
|
});
|
|
91
105
|
}
|
|
92
106
|
|
|
93
|
-
//
|
|
107
|
+
// Ensure dist exists
|
|
94
108
|
ensureDir(outputDir);
|
|
95
109
|
|
|
96
|
-
console.log('🚀
|
|
110
|
+
console.log('🚀 Starting image processing for build...');
|
|
97
111
|
processDirectory(inputDir);
|
|
98
|
-
console.log('✅
|
|
112
|
+
console.log('✅ Image processing completed!');
|
|
@@ -58,6 +58,31 @@ if (!authorData) {
|
|
|
58
58
|
}
|
|
59
59
|
let authorName = authorData.data.name;
|
|
60
60
|
let authorImg = authorData.data.avatar || '/img/default/autor_default.webp';
|
|
61
|
+
|
|
62
|
+
// Функция для получения пути к превью изображения автора
|
|
63
|
+
function getAuthorPreviewSrc(imageSrc: string): string {
|
|
64
|
+
if (!imageSrc) return '/img/default/previews/autor_default.webp';
|
|
65
|
+
|
|
66
|
+
// Если это дефолтное изображение автора
|
|
67
|
+
if (imageSrc.includes('/img/default/autor_default.webp')) {
|
|
68
|
+
return '/img/default/previews/autor_default.webp';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Для изображений авторов из examples
|
|
72
|
+
if (imageSrc.includes('/img/examples/authors/')) {
|
|
73
|
+
const fileName = imageSrc.split('/').pop();
|
|
74
|
+
if (fileName) {
|
|
75
|
+
const baseName = fileName.replace(/\.(webp|jpg|jpeg|png)$/i, '');
|
|
76
|
+
const extension = fileName.match(/\.(webp|jpg|jpeg|png)$/i)?.[0] || '.webp';
|
|
77
|
+
return imageSrc.replace(fileName, `previews/${baseName}${extension}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Fallback к оригинальному изображению если превью нет
|
|
82
|
+
return imageSrc;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const authorImgPreview = getAuthorPreviewSrc(authorImg);
|
|
61
86
|
---
|
|
62
87
|
|
|
63
88
|
<!-- Метаинформация сверху -->
|
|
@@ -72,12 +97,12 @@ let authorImg = authorData.data.avatar || '/img/default/autor_default.webp';
|
|
|
72
97
|
data-astro-reload
|
|
73
98
|
style="text-decoration: none; color: inherit; z-index: 10; position: relative;"
|
|
74
99
|
>
|
|
75
|
-
<img src={
|
|
100
|
+
<img src={authorImgPreview} alt={dict.ui?.authorAvatar || 'Author avatar'} class="w-8 h-8 rounded-full" width="32" height="32" decoding="async" />
|
|
76
101
|
<span class="font-medium hover:text-[var(--brand-color)] transition-colors duration-200">{authorName}</span>
|
|
77
102
|
</a>
|
|
78
103
|
) : (
|
|
79
104
|
<>
|
|
80
|
-
<img src={
|
|
105
|
+
<img src={authorImgPreview} alt={dict.ui?.authorAvatar || 'Author avatar'} class="w-8 h-8 rounded-full" width="32" height="32" decoding="async" />
|
|
81
106
|
<span class="font-medium">{authorName}</span>
|
|
82
107
|
</>
|
|
83
108
|
)
|
|
@@ -10,10 +10,35 @@ const { src, alt, size = '100px', class: className } = Astro.props;
|
|
|
10
10
|
|
|
11
11
|
// Определяем размер - может быть строкой (например, "80px", "5rem") или числом (пиксели)
|
|
12
12
|
const avatarSize = typeof size === 'number' ? `${size}px` : size;
|
|
13
|
+
|
|
14
|
+
// Функция для получения пути к превью изображения автора
|
|
15
|
+
function getAuthorPreviewSrc(imageSrc?: string): string {
|
|
16
|
+
if (!imageSrc) return '/img/default/previews/autor_default.webp';
|
|
17
|
+
|
|
18
|
+
// Если это дефолтное изображение автора
|
|
19
|
+
if (imageSrc.includes('/img/default/autor_default.webp')) {
|
|
20
|
+
return '/img/default/previews/autor_default.webp';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Для изображений авторов из examples
|
|
24
|
+
if (imageSrc.includes('/img/examples/authors/')) {
|
|
25
|
+
const fileName = imageSrc.split('/').pop();
|
|
26
|
+
if (fileName) {
|
|
27
|
+
const baseName = fileName.replace(/\.(webp|jpg|jpeg|png)$/i, '');
|
|
28
|
+
const extension = fileName.match(/\.(webp|jpg|jpeg|png)$/i)?.[0] || '.webp';
|
|
29
|
+
return imageSrc.replace(fileName, `previews/${baseName}${extension}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fallback к оригинальному изображению если превью нет
|
|
34
|
+
return imageSrc;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const previewSrc = getAuthorPreviewSrc(src);
|
|
13
38
|
---
|
|
14
39
|
|
|
15
40
|
<div class:list={['avatar-container', className]} style={`width: ${avatarSize}; height: ${avatarSize};`}>
|
|
16
|
-
<img src={
|
|
41
|
+
<img src={previewSrc} alt={alt} decoding="async" class="w-full h-full object-cover" />
|
|
17
42
|
</div>
|
|
18
43
|
|
|
19
44
|
<style>
|
|
@@ -98,8 +98,8 @@ let defaultAuthorName = defaultAuthor.data.name;
|
|
|
98
98
|
--bg-main: #ffffff;
|
|
99
99
|
--bg-muted: rgba(237, 241, 247, 0.621);
|
|
100
100
|
--border-main: rgba(17, 28, 44, 0.13);
|
|
101
|
-
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
102
|
-
--font-serif: 'Geologica', Georgia, 'Times New Roman', serif;
|
|
101
|
+
--font-sans: 'Inter Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
102
|
+
--font-serif: 'Geologica Variable', Georgia, 'Times New Roman', serif;
|
|
103
103
|
}
|
|
104
104
|
html.dark {
|
|
105
105
|
--brand-color-rgb: 13, 211, 18;
|
|
@@ -171,15 +171,15 @@ let defaultAuthorName = defaultAuthor.data.name;
|
|
|
171
171
|
|
|
172
172
|
<!-- PWA Registration -->
|
|
173
173
|
<script>
|
|
174
|
-
// PWA Service Worker Registration
|
|
175
|
-
if ('serviceWorker' in navigator) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
174
|
+
// PWA Service Worker Registration
|
|
175
|
+
if ('serviceWorker' in navigator) {
|
|
176
|
+
window.addEventListener('load', async () => {
|
|
177
|
+
try {
|
|
178
|
+
const registration = await navigator.serviceWorker.register('/sw.js');
|
|
179
|
+
console.log('SW registered: ', registration);
|
|
180
|
+
} catch (registrationError) {
|
|
181
|
+
console.log('SW registration failed: ', registrationError);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
182
184
|
}
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
185
|
</script>
|
|
@@ -250,8 +250,8 @@ if (import.meta.env.DEV) {
|
|
|
250
250
|
</span>
|
|
251
251
|
)
|
|
252
252
|
}
|
|
253
|
-
<div class="flex
|
|
254
|
-
<span class={`flex items-center gap-1 text-[12px]
|
|
253
|
+
<div class="flex items-center gap-1 absolute bottom-0 right-0">
|
|
254
|
+
<span class={`flex items-center gap-1 text-[12px] whitespace-nowrap ${isBrandDate ? 'text-[var(--brand-color)]' : 'text-[var(--text-muted)]'}`}>
|
|
255
255
|
<svg
|
|
256
256
|
width="16"
|
|
257
257
|
height="16"
|
|
@@ -30,16 +30,16 @@ const { headings, title = dict.tableOfContents?.title || fallbackDict.tableOfCon
|
|
|
30
30
|
id="table-of-contents"
|
|
31
31
|
aria-label={dict.tableOfContents.ariaLabel || fallbackDict.tableOfContents.ariaLabel || 'Contents'}
|
|
32
32
|
>
|
|
33
|
-
<h3 class="font-serif font-bold text-[
|
|
33
|
+
<h3 class="font-serif font-bold text-[20px] leading-[110%] text-[var(--brand-color)] flex-none">
|
|
34
34
|
{title}
|
|
35
35
|
</h3>
|
|
36
36
|
|
|
37
|
-
<div class="flex flex-col gap-
|
|
37
|
+
<div class="flex flex-col gap-3 flex-none">
|
|
38
38
|
{
|
|
39
39
|
headings.map((heading) => (
|
|
40
40
|
<a
|
|
41
41
|
href={`#${heading.id}`}
|
|
42
|
-
class="toc-link font-sans font-normal text-[
|
|
42
|
+
class="toc-link font-sans font-normal text-[14px] leading-[18px] text-[var(--text-heading)] hover:text-[var(--brand-color)] transition-all duration-300 ease-in-out hover:translate-x-1 transform"
|
|
43
43
|
data-target={heading.id}
|
|
44
44
|
>
|
|
45
45
|
{heading.title}
|
package/src/i18n/de.json
CHANGED
package/src/i18n/en.json
CHANGED
package/src/i18n/es.json
CHANGED
package/src/i18n/fr.json
CHANGED
package/src/i18n/ja.json
CHANGED
package/src/i18n/pt.json
CHANGED
package/src/i18n/ru.json
CHANGED
package/src/i18n/zh.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/utils/image-utils.ts - Утилиты для работы с изображениями
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Определяет правильный путь к изображению в зависимости от типа контента
|
|
5
|
+
* Все пользовательские изображения должны находиться в /img/page-images/
|
|
6
|
+
* Системные файлы остаются в /img/default/ и /img/examples/
|
|
7
|
+
*/
|
|
8
|
+
export function getImagePath(imageName: string, contentType?: 'blog' | 'author' | 'product' | 'project' | 'tag'): string {
|
|
9
|
+
// Если путь уже абсолютный (начинается с /), возвращаем как есть
|
|
10
|
+
if (imageName.startsWith('/')) {
|
|
11
|
+
return imageName;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Если это системные папки, не меняем путь
|
|
15
|
+
if (imageName.startsWith('default/') || imageName.startsWith('examples/')) {
|
|
16
|
+
return `/img/${imageName}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Все остальные изображения ищем в page-images
|
|
20
|
+
return `/img/page-images/${imageName}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Получает путь к изображению для конкретного типа контента
|
|
25
|
+
* Добавляет префикс типа если его нет
|
|
26
|
+
*/
|
|
27
|
+
export function getContentImagePath(slug: string, contentType: 'blog' | 'author' | 'product' | 'project' | 'tag', extension: string = '.webp'): string {
|
|
28
|
+
const fileName = `${contentType}_${slug}${extension}`;
|
|
29
|
+
return getImagePath(fileName);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Получает путь к превью изображению
|
|
34
|
+
*/
|
|
35
|
+
export function getPreviewImagePath(slug: string, contentType: 'blog' | 'author' | 'product' | 'project' | 'tag', extension: string = '.webp'): string {
|
|
36
|
+
const fileName = `previews_${contentType}_${slug}${extension}`;
|
|
37
|
+
return getImagePath(fileName);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Получает fallback путь для изображения в зависимости от типа контента
|
|
42
|
+
*/
|
|
43
|
+
export function getDefaultImagePath(contentType: 'blog' | 'author' | 'product' | 'project' | 'tag'): string {
|
|
44
|
+
const defaultImages = {
|
|
45
|
+
blog: '/img/default/blog_default.webp',
|
|
46
|
+
author: '/img/default/autor_default.webp',
|
|
47
|
+
product: '/img/default/product_default.webp',
|
|
48
|
+
project: '/img/default/project_default.webp',
|
|
49
|
+
tag: '/img/default/rubric_default.webp'
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return defaultImages[contentType];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Конвертирует старые пути к новой структуре
|
|
57
|
+
* Для обратной совместимости
|
|
58
|
+
*/
|
|
59
|
+
export function convertLegacyImagePath(imagePath: string): string {
|
|
60
|
+
// Конвертируем старые пути в новые
|
|
61
|
+
const conversions = [
|
|
62
|
+
{ from: '/img/blog/', to: '/img/page-images/' },
|
|
63
|
+
{ from: '/img/authors/', to: '/img/page-images/' },
|
|
64
|
+
{ from: '/img/products/', to: '/img/page-images/' },
|
|
65
|
+
{ from: '/img/projects/', to: '/img/page-images/' },
|
|
66
|
+
{ from: '/img/uploads/', to: '/img/page-images/' }
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
for (const conversion of conversions) {
|
|
70
|
+
if (imagePath.startsWith(conversion.from)) {
|
|
71
|
+
return imagePath.replace(conversion.from, conversion.to);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return imagePath;
|
|
76
|
+
}
|