core-maugli 1.2.27 → 1.2.28
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 +1 -1
- package/src/components/BaseHead.astro +72 -4
- package/src/components/Card.astro +3 -62
- package/src/components/Footer.astro +11 -2
- package/src/components/Nav.astro +11 -0
- package/src/components/OptimizedImage.astro +51 -0
- package/src/components/ProductBannerCard.astro +1 -1
- package/src/components/ResponsiveImage.astro +46 -0
- package/src/components/RubricCard.astro +1 -1
- package/src/content/products/maugli-freeblog.md +2 -2
package/package.json
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
---
|
2
|
-
import {
|
3
|
-
import siteConfig from '../data/site-config.ts';
|
2
|
+
import type { CollectionEntry } from 'astro:content';
|
4
3
|
import '../styles/global.css';
|
4
|
+
import { getFilteredCollection } from '../utils/content-loader';
|
5
|
+
import siteConfig from '../data/site-config';
|
6
|
+
import { getAbsoluteLocaleUrl, getRelativeLocaleUrl } from 'astro:i18n';
|
5
7
|
// Определи тип выше:
|
6
8
|
export type ImageMimeType = 'image/jpeg' | 'image/png' | 'image/webp';
|
7
9
|
|
@@ -86,6 +88,72 @@ let defaultAuthorName = defaultAuthor.data.name;
|
|
86
88
|
<meta name="robots" content="index, follow" />
|
87
89
|
<meta name="generator" content={Astro.generator} />
|
88
90
|
|
91
|
+
<!-- Critical CSS Inline for Performance -->
|
92
|
+
<style>
|
93
|
+
:root {
|
94
|
+
--brand-color-rgb: 12, 191, 17;
|
95
|
+
--brand-color: rgb(var(--brand-color-rgb));
|
96
|
+
--text-main: #111c2d;
|
97
|
+
--text-heading: #3b5174;
|
98
|
+
--text-muted: #6b7280;
|
99
|
+
--bg-main: #ffffff;
|
100
|
+
--bg-muted: rgba(237, 241, 247, 0.621);
|
101
|
+
--border-main: rgba(17, 28, 44, 0.13);
|
102
|
+
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
103
|
+
--font-serif: 'Geologica', Georgia, 'Times New Roman', serif;
|
104
|
+
}
|
105
|
+
html.dark {
|
106
|
+
--brand-color-rgb: 13, 211, 18;
|
107
|
+
--brand-color: rgb(var(--brand-color-rgb));
|
108
|
+
--text-main: #ffffff;
|
109
|
+
--text-heading: rgba(202, 252, 254, 0.7);
|
110
|
+
--text-muted: #9ca3af;
|
111
|
+
--bg-main: #0b131e;
|
112
|
+
--bg-muted: #131d2cde;
|
113
|
+
--border-main: rgba(51, 66, 66, 0.6);
|
114
|
+
}
|
115
|
+
body {
|
116
|
+
font-family: var(--font-sans);
|
117
|
+
color: var(--text-main);
|
118
|
+
background-color: var(--bg-main);
|
119
|
+
margin: 0;
|
120
|
+
line-height: 1.5;
|
121
|
+
}
|
122
|
+
* {
|
123
|
+
box-sizing: border-box;
|
124
|
+
}
|
125
|
+
</style>
|
126
|
+
|
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
|
+
/>
|
152
|
+
<noscript>
|
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" />
|
155
|
+
</noscript>
|
156
|
+
|
89
157
|
<!-- SEO -->
|
90
158
|
<meta name="description" content={seoDescription} />
|
91
159
|
<meta name="keywords" content={seoKeywords} />
|
@@ -108,8 +176,8 @@ let defaultAuthorName = defaultAuthor.data.name;
|
|
108
176
|
<meta property="og:description" content={seoDescription} />
|
109
177
|
{resolvedImage?.src && <meta property="og:image" content={resolvedImage.src} />}
|
110
178
|
{resolvedImage?.alt && <meta property="og:image:alt" content={resolvedImage.alt} />}
|
111
|
-
{typeof resolvedImage?.width !== 'undefined' && <meta property="og:image:width" content={resolvedImage.width} />}
|
112
|
-
{typeof resolvedImage?.height !== 'undefined' && <meta property="og:image:height" content={resolvedImage.height} />}
|
179
|
+
{typeof resolvedImage?.width !== 'undefined' && <meta property="og:image:width" content={resolvedImage.width.toString()} />}
|
180
|
+
{typeof resolvedImage?.height !== 'undefined' && <meta property="og:image:height" content={resolvedImage.height.toString()} />}
|
113
181
|
{resolvedImage?.type && <meta property="og:image:type" content={resolvedImage.type} />}
|
114
182
|
|
115
183
|
<!-- X/Twitter -->
|
@@ -1,7 +1,4 @@
|
|
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';
|
7
4
|
|
@@ -16,7 +13,7 @@ type Props = {
|
|
16
13
|
headingLevel?: 'h2' | 'h3';
|
17
14
|
isFeatured?: boolean;
|
18
15
|
class?: string;
|
19
|
-
type?: string;
|
16
|
+
type?: string;
|
20
17
|
};
|
21
18
|
|
22
19
|
const { href, title, image, seo, publishDate, excerpt, description, headingLevel = 'h2', isFeatured = false, class: className, type } = Astro.props;
|
@@ -35,62 +32,8 @@ const baseImage =
|
|
35
32
|
? maugliConfig.defaultProductImage
|
36
33
|
: maugliConfig.seo.defaultImage);
|
37
34
|
|
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
35
|
const cardImageAlt = seo?.image?.alt || image?.alt || title || 'Изображение';
|
77
36
|
|
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
37
|
// Определяем контент для отображения
|
95
38
|
const content = excerpt || description;
|
96
39
|
---
|
@@ -106,11 +49,9 @@ const content = excerpt || description;
|
|
106
49
|
<!-- Изображение -->
|
107
50
|
<div class="w-full aspect-[1200/630] bg-[var(--bg-muted)] overflow-hidden relative">
|
108
51
|
<img
|
109
|
-
src={
|
110
|
-
srcset={cardImage.srcset}
|
111
|
-
sizes={cardImage.sizes}
|
52
|
+
src={baseImage}
|
112
53
|
alt={cardImageAlt}
|
113
|
-
loading="
|
54
|
+
loading="eager"
|
114
55
|
width="1200"
|
115
56
|
height="630"
|
116
57
|
class="w-full h-full object-cover rounded-custom transition-transform duration-300 group-hover:scale-105"
|
@@ -9,7 +9,7 @@ for (const lang of LANGUAGES) {
|
|
9
9
|
dicts[lang.code] = await import(`../i18n/${lang.code}.json`).then((m) => m.default);
|
10
10
|
} catch {}
|
11
11
|
}
|
12
|
-
const lang = maugliConfig.
|
12
|
+
const lang = maugliConfig.defaultLang || 'en';
|
13
13
|
const dict = dicts[lang] || dicts['en'] || {};
|
14
14
|
|
15
15
|
const navLinks = (maugliConfig.navLinks || []).map((link) => ({
|
@@ -110,7 +110,16 @@ const copyrightYears = yearStart === yearEnd ? `${yearStart}` : `${yearStart}–
|
|
110
110
|
>
|
111
111
|
<div class="flex flex-row justify-between items-end w-full mt-2">
|
112
112
|
<a href="https://www.npmjs.com/package/core-maugli" target="_blank" rel="noopener" aria-label="Core Maugli на NPM">
|
113
|
-
<img src="/footerlabel.svg" alt="Maugli Label" style="height:32px;width:auto;" loading="lazy" decoding="async" />
|
113
|
+
<img src="/footerlabel.svg" alt="Maugli Label" style="height:32px;width:auto;" loading="lazy" decoding="async" width="184" height="68" />
|
114
|
+
</a>
|
115
|
+
<a
|
116
|
+
href="https://www.npmjs.com/package/core-maugli"
|
117
|
+
target="_blank"
|
118
|
+
rel="noopener noreferrer"
|
119
|
+
class="footer-link text-xs opacity-60 hover:opacity-100"
|
120
|
+
style="color:var(--text-muted);font-family:var(--font-sans);margin-left:8px;"
|
121
|
+
>
|
122
|
+
npm package
|
114
123
|
</a>
|
115
124
|
{
|
116
125
|
Object.values(maugliConfig.links || {}).some(Boolean) && (
|
package/src/components/Nav.astro
CHANGED
@@ -17,6 +17,9 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
|
17
17
|
label: d?.nav?.[link.key] || link.label
|
18
18
|
};
|
19
19
|
});
|
20
|
+
|
21
|
+
const openMenuLabel = (dict as any).openMenu || 'Open menu';
|
22
|
+
const closeMenuLabel = (dict as any).closeMenu || 'Close menu';
|
20
23
|
---
|
21
24
|
|
22
25
|
<nav class="fixed top-0 left-0 w-full z-50 bg-[var(--bg-main)] opacity-95 backdrop-blur-lg flex items-center justify-between pt-4 pb-4">
|
@@ -57,6 +60,7 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
|
57
60
|
class="menu-toggle cursor-pointer w-8 h-8 flex items-center justify-center relative z-30 md:hidden ml-auto text-[var(--text-heading)] order-3"
|
58
61
|
aria-expanded="false"
|
59
62
|
aria-controls="menu-items"
|
63
|
+
aria-label={openMenuLabel}
|
60
64
|
>
|
61
65
|
<span class="menu-toggle-icon w-6 h-px relative bg-current"></span>
|
62
66
|
</button>
|
@@ -121,6 +125,9 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
|
121
125
|
</style>
|
122
126
|
|
123
127
|
<script>
|
128
|
+
const openLabel = ${JSON.stringify(openMenuLabel)};
|
129
|
+
const closeLabel = ${JSON.stringify(closeMenuLabel)};
|
130
|
+
|
124
131
|
function menuToggle() {
|
125
132
|
const menu = document.querySelector('.menu');
|
126
133
|
const menuToggleBtn = document.querySelector('.menu-toggle');
|
@@ -129,6 +136,7 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
|
129
136
|
const isMenuExpanded = menuToggleBtn.getAttribute('aria-expanded') === 'true';
|
130
137
|
menuToggleBtn.classList.toggle('is-active');
|
131
138
|
menuToggleBtn.setAttribute('aria-expanded', isMenuExpanded ? 'false' : 'true');
|
139
|
+
menuToggleBtn.setAttribute('aria-label', isMenuExpanded ? openLabel : closeLabel);
|
132
140
|
menu.classList.toggle('is-visible');
|
133
141
|
});
|
134
142
|
// Закрытие при клике вне меню
|
@@ -136,6 +144,7 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
|
136
144
|
if (!menuToggleBtn.contains(e.target as Node) && !menu.contains(e.target as Node)) {
|
137
145
|
menuToggleBtn.classList.remove('is-active');
|
138
146
|
menuToggleBtn.setAttribute('aria-expanded', 'false');
|
147
|
+
menuToggleBtn.setAttribute('aria-label', openLabel);
|
139
148
|
menu.classList.remove('is-visible');
|
140
149
|
}
|
141
150
|
});
|
@@ -143,6 +152,7 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
|
143
152
|
window.addEventListener('scroll', () => {
|
144
153
|
menuToggleBtn.classList.remove('is-active');
|
145
154
|
menuToggleBtn.setAttribute('aria-expanded', 'false');
|
155
|
+
menuToggleBtn.setAttribute('aria-label', openLabel);
|
146
156
|
menu.classList.remove('is-visible');
|
147
157
|
});
|
148
158
|
// Закрытие при выборе пункта меню
|
@@ -150,6 +160,7 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
|
150
160
|
item.addEventListener('click', () => {
|
151
161
|
menuToggleBtn.classList.remove('is-active');
|
152
162
|
menuToggleBtn.setAttribute('aria-expanded', 'false');
|
163
|
+
menuToggleBtn.setAttribute('aria-label', openLabel);
|
153
164
|
menu.classList.remove('is-visible');
|
154
165
|
});
|
155
166
|
});
|
@@ -0,0 +1,51 @@
|
|
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 { src, alt, width, height, class: className = '', loading = 'lazy', decoding = 'async', sizes, priority = false, quality = 75, ...rest } = Astro.props;
|
16
|
+
|
17
|
+
// Generate responsive image variations with quality optimization
|
18
|
+
const generateSrcSet = (baseSrc: string) => {
|
19
|
+
const baseUrl = baseSrc.replace(/\.[^.]+$/, '');
|
20
|
+
const ext = 'webp'; // Always use WebP for better compression
|
21
|
+
|
22
|
+
const variations = [
|
23
|
+
{ width: 400, suffix: '-400' },
|
24
|
+
{ width: 800, suffix: '-800' },
|
25
|
+
{ width: 1200, suffix: '-1200' }
|
26
|
+
];
|
27
|
+
|
28
|
+
return variations.map(({ width, suffix }) => `${baseUrl}${suffix}.${ext} ${width}w`).join(', ');
|
29
|
+
};
|
30
|
+
|
31
|
+
const srcSet = generateSrcSet(src);
|
32
|
+
const defaultSizes = sizes || '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw';
|
33
|
+
|
34
|
+
// For priority images, we'll preload them
|
35
|
+
const shouldPreload = priority && loading === 'eager';
|
36
|
+
---
|
37
|
+
|
38
|
+
<img
|
39
|
+
src={src}
|
40
|
+
srcset={srcSet}
|
41
|
+
sizes={defaultSizes}
|
42
|
+
alt={alt}
|
43
|
+
width={width}
|
44
|
+
height={height}
|
45
|
+
class={className}
|
46
|
+
loading={priority ? 'eager' : loading}
|
47
|
+
decoding={decoding}
|
48
|
+
{...rest}
|
49
|
+
/>
|
50
|
+
|
51
|
+
{priority && <link rel="preload" as="image" href={src} type="image/webp" />}
|
@@ -28,7 +28,7 @@ const buttons = (dict as any).buttons || (fallbackDict as any).buttons || {};
|
|
28
28
|
src={bannerImage}
|
29
29
|
alt={seo?.image?.alt || image?.alt || title || 'Product'}
|
30
30
|
class="absolute inset-0 w-full h-full object-cover rounded-custom z-0"
|
31
|
-
loading="
|
31
|
+
loading="eager"
|
32
32
|
decoding="async"
|
33
33
|
/>
|
34
34
|
{
|
@@ -0,0 +1,46 @@
|
|
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
|
+
// Простая стратегия - используем базовое изображение и не генерируем srcset для несуществующих файлов
|
30
|
+
const optimizedSrc = src;
|
31
|
+
|
32
|
+
// Генерируем srcset только если есть базовое изображение
|
33
|
+
const generateSrcSet = (baseSrc: string) => {
|
34
|
+
if (baseSrc.startsWith('http')) {
|
35
|
+
return baseSrc;
|
36
|
+
}
|
37
|
+
|
38
|
+
// Возвращаем только базовое изображение без попыток создания srcset
|
39
|
+
// Responsive варианты будут обработаны build процессом
|
40
|
+
return baseSrc;
|
41
|
+
};
|
42
|
+
|
43
|
+
const srcSet = generateSrcSet(src);
|
44
|
+
---
|
45
|
+
|
46
|
+
<img src={optimizedSrc} alt={alt} width={width} height={height} class={className} loading={loading} decoding={decoding} sizes={sizes} {...rest} />
|
@@ -56,7 +56,7 @@ jsonld:
|
|
56
56
|
'@type': 'Offer'
|
57
57
|
'priceCurrency': 'USD'
|
58
58
|
'price': '0'
|
59
|
-
'url': 'https://
|
59
|
+
'url': 'https://freeblog.maugli.ru'
|
60
60
|
|
61
61
|
isExample: true
|
62
62
|
---
|
@@ -157,6 +157,6 @@ Maugli Free Blog спроектирован для лёгкой развертк
|
|
157
157
|
|
158
158
|
## **Где взять?**
|
159
159
|
|
160
|
-
- **
|
160
|
+
- **GitHub:** [ссылка].
|
161
161
|
|
162
162
|
**Maugli Free Blog — это платформа, созданная для автоматизации, SEO и AI-интеграций.**
|