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 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.27",
5
+ "version": "1.2.28",
6
6
  "license": "GPL-3.0-or-later OR Commercial",
7
7
  "repository": {
8
8
  "type": "git",
@@ -1,7 +1,9 @@
1
1
  ---
2
- import { getFilteredCollection } from '../utils/content-loader';
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={finalImage}
110
- srcset={cardImage.srcset}
111
- sizes={cardImage.sizes}
52
+ src={baseImage}
112
53
  alt={cardImageAlt}
113
- loading="lazy"
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.lang || maugliConfig.defaultLang || 'en';
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) && (
@@ -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="lazy"
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} />
@@ -236,7 +236,7 @@ if (import.meta.env.DEV) {
236
236
  sizes={rubricImage.sizes}
237
237
  alt={title}
238
238
  class="w-[105px] h-[107px] object-cover rounded-custom"
239
- loading="lazy"
239
+ loading="eager"
240
240
  width="105"
241
241
  height="107"
242
242
  />
@@ -56,7 +56,7 @@ jsonld:
56
56
  '@type': 'Offer'
57
57
  'priceCurrency': 'USD'
58
58
  'price': '0'
59
- 'url': 'https://www.npmjs.com/package/core-maugli'
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
- - **NPM:** [core-maugli](https://www.npmjs.com/package/core-maugli)
160
+ - **GitHub:** [ссылка].
161
161
 
162
162
  **Maugli Free Blog — это платформа, созданная для автоматизации, SEO и AI-интеграций.**