core-maugli 1.2.21 → 1.2.23
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
CHANGED
@@ -99,8 +99,8 @@ let defaultAuthorName = defaultAuthor.data.name;
|
|
99
99
|
--bg-main: #ffffff;
|
100
100
|
--bg-muted: rgba(237, 241, 247, 0.621);
|
101
101
|
--border-main: rgba(17, 28, 44, 0.13);
|
102
|
-
--font-sans: 'Inter
|
103
|
-
--font-serif: 'Geologica
|
102
|
+
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
103
|
+
--font-serif: 'Geologica', Georgia, 'Times New Roman', serif;
|
104
104
|
}
|
105
105
|
html.dark {
|
106
106
|
--brand-color-rgb: 13, 211, 18;
|
@@ -124,12 +124,34 @@ let defaultAuthorName = defaultAuthor.data.name;
|
|
124
124
|
}
|
125
125
|
</style>
|
126
126
|
|
127
|
-
<!--
|
128
|
-
<link rel="preload" href="/
|
129
|
-
<link rel="preload" href="/
|
127
|
+
<!-- Critical Resource Preloading for Performance -->
|
128
|
+
<link rel="preload" href="/favicon.svg" as="image" type="image/svg+xml" />
|
129
|
+
<link rel="preload" href="/footerlabel.svg" as="image" type="image/svg+xml" />
|
130
|
+
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
|
131
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
|
132
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
133
|
+
|
134
|
+
<!-- Image Optimization Hints -->
|
135
|
+
<meta name="format-detection" content="telephone=no" />
|
136
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
137
|
+
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
138
|
+
|
139
|
+
<!-- Font Loading Strategy - Non-blocking -->
|
140
|
+
<link
|
141
|
+
rel="preload"
|
142
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
|
143
|
+
as="style"
|
144
|
+
onload="this.onload=null;this.rel='stylesheet'"
|
145
|
+
/>
|
146
|
+
<link
|
147
|
+
rel="preload"
|
148
|
+
href="https://fonts.googleapis.com/css2?family=Geologica:wght@400;500;600&display=swap"
|
149
|
+
as="style"
|
150
|
+
onload="this.onload=null;this.rel='stylesheet'"
|
151
|
+
/>
|
130
152
|
<noscript>
|
131
|
-
<link rel="stylesheet" href="/
|
132
|
-
<link rel="stylesheet" href="/
|
153
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" />
|
154
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geologica:wght@400;500;600&display=swap" />
|
133
155
|
</noscript>
|
134
156
|
|
135
157
|
<!-- SEO -->
|
@@ -1,9 +1,7 @@
|
|
1
1
|
---
|
2
|
-
import fs from 'fs';
|
3
|
-
import path from 'path';
|
4
|
-
import { fileURLToPath } from 'url';
|
5
2
|
import { maugliConfig } from '../config/maugli.config';
|
6
3
|
import FormattedDate from './FormattedDate.astro';
|
4
|
+
import ResponsiveImage from './ResponsiveImage.astro';
|
7
5
|
|
8
6
|
type Props = {
|
9
7
|
href: string;
|
@@ -16,7 +14,7 @@ type Props = {
|
|
16
14
|
headingLevel?: 'h2' | 'h3';
|
17
15
|
isFeatured?: boolean;
|
18
16
|
class?: string;
|
19
|
-
type?: string;
|
17
|
+
type?: string;
|
20
18
|
};
|
21
19
|
|
22
20
|
const { href, title, image, seo, publishDate, excerpt, description, headingLevel = 'h2', isFeatured = false, class: className, type } = Astro.props;
|
@@ -35,62 +33,8 @@ const baseImage =
|
|
35
33
|
? maugliConfig.defaultProductImage
|
36
34
|
: maugliConfig.seo.defaultImage);
|
37
35
|
|
38
|
-
// Генерируем адаптивные версии изображений
|
39
|
-
function getResponsiveImages(imagePath: string) {
|
40
|
-
const basePath = imagePath.replace(/\.(webp|jpg|jpeg|png)$/i, '');
|
41
|
-
const extension = imagePath.match(/\.(webp|jpg|jpeg|png)$/i)?.[0] || '.webp';
|
42
|
-
|
43
|
-
// Проверяем существование адаптивных версий
|
44
|
-
const __filename = fileURLToPath(import.meta.url);
|
45
|
-
const projectRoot = path.resolve(path.dirname(__filename), '../..');
|
46
|
-
|
47
|
-
const variants = [
|
48
|
-
{ suffix: '-400', width: '400w' },
|
49
|
-
{ suffix: '-800', width: '800w' },
|
50
|
-
{ suffix: '-1200', width: '1200w' }
|
51
|
-
];
|
52
|
-
|
53
|
-
const srcsetItems = [];
|
54
|
-
|
55
|
-
// Добавляем адаптивные версии, если они существуют
|
56
|
-
for (const variant of variants) {
|
57
|
-
const variantPath = `${basePath}${variant.suffix}${extension}`;
|
58
|
-
const filePath = path.join(projectRoot, 'public', variantPath.replace(/^\//, ''));
|
59
|
-
|
60
|
-
if (fs.existsSync(filePath)) {
|
61
|
-
srcsetItems.push(`${variantPath} ${variant.width}`);
|
62
|
-
}
|
63
|
-
}
|
64
|
-
|
65
|
-
// Всегда добавляем оригинальное изображение
|
66
|
-
srcsetItems.push(`${imagePath} 1200w`);
|
67
|
-
|
68
|
-
return {
|
69
|
-
src: imagePath,
|
70
|
-
srcset: srcsetItems.join(', '),
|
71
|
-
sizes: '(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 400px'
|
72
|
-
};
|
73
|
-
}
|
74
|
-
|
75
|
-
const cardImage = getResponsiveImages(baseImage);
|
76
36
|
const cardImageAlt = seo?.image?.alt || image?.alt || title || 'Изображение';
|
77
37
|
|
78
|
-
// Используем уменьшенное превью, если оно существует
|
79
|
-
let previewImage;
|
80
|
-
if (cardImage.src) {
|
81
|
-
previewImage = cardImage.src.replace(/\/([^\/]+)$/, '/previews/$1');
|
82
|
-
|
83
|
-
const __filename = fileURLToPath(import.meta.url);
|
84
|
-
const projectRoot = path.resolve(path.dirname(__filename), '../..');
|
85
|
-
const previewFilePath = path.join(projectRoot, 'public', previewImage.replace(/^\//, ''));
|
86
|
-
|
87
|
-
if (!fs.existsSync(previewFilePath)) {
|
88
|
-
previewImage = undefined;
|
89
|
-
}
|
90
|
-
}
|
91
|
-
|
92
|
-
const finalImage = previewImage || cardImage.src;
|
93
|
-
|
94
38
|
// Определяем контент для отображения
|
95
39
|
const content = excerpt || description;
|
96
40
|
---
|
@@ -105,15 +49,16 @@ const content = excerpt || description;
|
|
105
49
|
<a href={href} class="block w-full h-full">
|
106
50
|
<!-- Изображение -->
|
107
51
|
<div class="w-full aspect-[1200/630] bg-[var(--bg-muted)] overflow-hidden relative">
|
108
|
-
<
|
109
|
-
src={
|
110
|
-
srcset={cardImage.srcset}
|
111
|
-
sizes={cardImage.sizes}
|
52
|
+
<ResponsiveImage
|
53
|
+
src={baseImage}
|
112
54
|
alt={cardImageAlt}
|
55
|
+
width={1200}
|
56
|
+
height={630}
|
113
57
|
loading="lazy"
|
114
|
-
width="1200"
|
115
|
-
height="630"
|
116
58
|
class="w-full h-full object-cover rounded-custom transition-transform duration-300 group-hover:scale-105"
|
59
|
+
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 400px"
|
60
|
+
quality={75}
|
61
|
+
format="webp"
|
117
62
|
/>
|
118
63
|
|
119
64
|
<!-- Звездочка featured в правом верхнем углу -->
|
@@ -2,6 +2,7 @@
|
|
2
2
|
import { maugliConfig } from '../config/maugli.config';
|
3
3
|
import { LANGUAGES } from '../i18n/languages';
|
4
4
|
import AuthorLinksGroup from './AuthorLinksGroup.astro';
|
5
|
+
import OptimizedImage from './OptimizedImage.astro';
|
5
6
|
// Универсальный импорт словарей по доступным языкам
|
6
7
|
const dicts: Record<string, any> = {};
|
7
8
|
for (const lang of LANGUAGES) {
|
@@ -9,7 +10,7 @@ for (const lang of LANGUAGES) {
|
|
9
10
|
dicts[lang.code] = await import(`../i18n/${lang.code}.json`).then((m) => m.default);
|
10
11
|
} catch {}
|
11
12
|
}
|
12
|
-
const lang = maugliConfig.
|
13
|
+
const lang = maugliConfig.defaultLang || 'en';
|
13
14
|
const dict = dicts[lang] || dicts['en'] || {};
|
14
15
|
|
15
16
|
const navLinks = (maugliConfig.navLinks || []).map((link) => ({
|
@@ -0,0 +1,67 @@
|
|
1
|
+
---
|
2
|
+
export interface Props {
|
3
|
+
src: string;
|
4
|
+
alt: string;
|
5
|
+
width?: number;
|
6
|
+
height?: number;
|
7
|
+
class?: string;
|
8
|
+
loading?: 'lazy' | 'eager';
|
9
|
+
decoding?: 'async' | 'sync' | 'auto';
|
10
|
+
sizes?: string;
|
11
|
+
priority?: boolean;
|
12
|
+
quality?: number;
|
13
|
+
}
|
14
|
+
|
15
|
+
const {
|
16
|
+
src,
|
17
|
+
alt,
|
18
|
+
width,
|
19
|
+
height,
|
20
|
+
class: className = '',
|
21
|
+
loading = 'lazy',
|
22
|
+
decoding = 'async',
|
23
|
+
sizes,
|
24
|
+
priority = false,
|
25
|
+
quality = 75,
|
26
|
+
...rest
|
27
|
+
} = Astro.props;
|
28
|
+
|
29
|
+
// Generate responsive image variations with quality optimization
|
30
|
+
const generateSrcSet = (baseSrc: string) => {
|
31
|
+
const baseUrl = baseSrc.replace(/\.[^.]+$/, '');
|
32
|
+
const ext = 'webp'; // Always use WebP for better compression
|
33
|
+
|
34
|
+
const variations = [
|
35
|
+
{ width: 400, suffix: '-400' },
|
36
|
+
{ width: 800, suffix: '-800' },
|
37
|
+
{ width: 1200, suffix: '-1200' }
|
38
|
+
];
|
39
|
+
|
40
|
+
return variations
|
41
|
+
.map(({ width, suffix }) => `${baseUrl}${suffix}.${ext} ${width}w`)
|
42
|
+
.join(', ');
|
43
|
+
};
|
44
|
+
|
45
|
+
const srcSet = generateSrcSet(src);
|
46
|
+
const defaultSizes = sizes || '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw';
|
47
|
+
|
48
|
+
// For priority images, we'll preload them
|
49
|
+
const shouldPreload = priority && loading === 'eager';
|
50
|
+
---
|
51
|
+
|
52
|
+
<img
|
53
|
+
src={src}
|
54
|
+
srcset={srcSet}
|
55
|
+
sizes={defaultSizes}
|
56
|
+
alt={alt}
|
57
|
+
width={width}
|
58
|
+
height={height}
|
59
|
+
class={className}
|
60
|
+
loading={priority ? 'eager' : loading}
|
61
|
+
decoding={decoding}
|
62
|
+
{...rest}
|
63
|
+
/>
|
64
|
+
|
65
|
+
{priority && (
|
66
|
+
<link rel="preload" as="image" href={src} type="image/webp" />
|
67
|
+
)}
|
@@ -0,0 +1,84 @@
|
|
1
|
+
---
|
2
|
+
export interface Props {
|
3
|
+
src: string;
|
4
|
+
alt: string;
|
5
|
+
width?: number;
|
6
|
+
height?: number;
|
7
|
+
class?: string;
|
8
|
+
loading?: 'lazy' | 'eager';
|
9
|
+
decoding?: 'async' | 'sync' | 'auto';
|
10
|
+
sizes?: string;
|
11
|
+
quality?: number;
|
12
|
+
format?: 'webp' | 'avif' | 'jpeg' | 'png';
|
13
|
+
}
|
14
|
+
|
15
|
+
const {
|
16
|
+
src,
|
17
|
+
alt,
|
18
|
+
width = 1200,
|
19
|
+
height = 630,
|
20
|
+
class: className = '',
|
21
|
+
loading = 'lazy',
|
22
|
+
decoding = 'async',
|
23
|
+
sizes = '(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 400px',
|
24
|
+
quality = 75,
|
25
|
+
format = 'webp',
|
26
|
+
...rest
|
27
|
+
} = Astro.props;
|
28
|
+
|
29
|
+
// Генерируем оптимизированные URL для разных размеров
|
30
|
+
const generateImageURL = (baseSrc: string, width: number, quality: number, format: string) => {
|
31
|
+
// Если это внешний URL, возвращаем как есть
|
32
|
+
if (baseSrc.startsWith('http')) {
|
33
|
+
return baseSrc;
|
34
|
+
}
|
35
|
+
|
36
|
+
// Для карточек проверяем наличие preview версии
|
37
|
+
const previewSrc = baseSrc.replace(/\/([^\/]+)$/, '/previews/$1');
|
38
|
+
|
39
|
+
// Возвращаем базовое изображение (preview будет проверен в build time)
|
40
|
+
return baseSrc;
|
41
|
+
};
|
42
|
+
|
43
|
+
const optimizedSrc = generateImageURL(src, width, quality, format);
|
44
|
+
|
45
|
+
// Генерируем srcset для адаптивности (используем готовые варианты)
|
46
|
+
const generateSrcSet = (baseSrc: string) => {
|
47
|
+
if (baseSrc.startsWith('http')) {
|
48
|
+
return baseSrc;
|
49
|
+
}
|
50
|
+
|
51
|
+
const baseUrl = baseSrc.replace(/\.[^.]+$/, '');
|
52
|
+
const ext = '.webp';
|
53
|
+
|
54
|
+
// Используем существующие responsive варианты
|
55
|
+
const variants = [
|
56
|
+
{ width: 400, suffix: '-400' },
|
57
|
+
{ width: 800, suffix: '-800' },
|
58
|
+
{ width: 1200, suffix: '-1200' }
|
59
|
+
];
|
60
|
+
|
61
|
+
const srcsetItems = variants
|
62
|
+
.map(({ width, suffix }) => `${baseUrl}${suffix}${ext} ${width}w`);
|
63
|
+
|
64
|
+
// Добавляем оригинал как fallback
|
65
|
+
srcsetItems.push(`${baseSrc} 1200w`);
|
66
|
+
|
67
|
+
return srcsetItems.join(', ');
|
68
|
+
};
|
69
|
+
|
70
|
+
const srcSet = generateSrcSet(src);
|
71
|
+
---
|
72
|
+
|
73
|
+
<img
|
74
|
+
src={optimizedSrc}
|
75
|
+
srcset={srcSet}
|
76
|
+
alt={alt}
|
77
|
+
width={width}
|
78
|
+
height={height}
|
79
|
+
class={className}
|
80
|
+
loading={loading}
|
81
|
+
decoding={decoding}
|
82
|
+
sizes={sizes}
|
83
|
+
{...rest}
|
84
|
+
/>
|