core-maugli 1.2.27 → 1.2.29
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 +13 -9
- 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
@@ -9,6 +9,7 @@ import { LANGUAGES } from '../i18n/languages';
|
|
9
9
|
const lang = typeof maugliConfig.defaultLang === 'string' ? maugliConfig.defaultLang : 'en';
|
10
10
|
const languageObj = LANGUAGES.find((l) => l.code === lang) || LANGUAGES.find((l) => l.code === 'en');
|
11
11
|
const dict = languageObj?.dict && Object.keys(languageObj.dict).length > 0 ? languageObj.dict : LANGUAGES.find((l) => l.code === 'en')?.dict || {};
|
12
|
+
const langLabel = languageObj?.label || lang;
|
12
13
|
|
13
14
|
const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
14
15
|
const d = dict as any;
|
@@ -17,6 +18,9 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
|
17
18
|
label: d?.nav?.[link.key] || link.label
|
18
19
|
};
|
19
20
|
});
|
21
|
+
|
22
|
+
const openMenuLabel = dict.nav?.openMenu || 'Open menu';
|
23
|
+
const closeMenuLabel = dict.nav?.closeMenu || 'Close menu';
|
20
24
|
---
|
21
25
|
|
22
26
|
<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">
|
@@ -24,15 +28,7 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
|
24
28
|
<a href={maugliConfig.brand.logoHref && maugliConfig.brand.logoHref.trim() ? maugliConfig.brand.logoHref : '/'} class="ml-6 flex-shrink-0 card-blur">
|
25
29
|
<picture>
|
26
30
|
<source srcset={maugliConfig.brand.logoDark} media="(prefers-color-scheme: dark)" />
|
27
|
-
<img
|
28
|
-
src={maugliConfig.brand.logoLight}
|
29
|
-
alt="Логотип"
|
30
|
-
width="48"
|
31
|
-
height="48"
|
32
|
-
class="w-12 h-12"
|
33
|
-
loading="eager"
|
34
|
-
decoding="async"
|
35
|
-
/>
|
31
|
+
<img src={maugliConfig.brand.logoLight} alt="Логотип" width="48" height="48" class="w-12 h-12" loading="eager" decoding="async" />
|
36
32
|
</picture>
|
37
33
|
</a>
|
38
34
|
<ul
|
@@ -57,6 +53,7 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
|
57
53
|
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
54
|
aria-expanded="false"
|
59
55
|
aria-controls="menu-items"
|
56
|
+
aria-label={openMenuLabel}
|
60
57
|
>
|
61
58
|
<span class="menu-toggle-icon w-6 h-px relative bg-current"></span>
|
62
59
|
</button>
|
@@ -121,6 +118,9 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
|
121
118
|
</style>
|
122
119
|
|
123
120
|
<script>
|
121
|
+
const openLabel = '{openMenuLabel}';
|
122
|
+
const closeLabel = '{closeMenuLabel}';
|
123
|
+
|
124
124
|
function menuToggle() {
|
125
125
|
const menu = document.querySelector('.menu');
|
126
126
|
const menuToggleBtn = document.querySelector('.menu-toggle');
|
@@ -129,6 +129,7 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
|
129
129
|
const isMenuExpanded = menuToggleBtn.getAttribute('aria-expanded') === 'true';
|
130
130
|
menuToggleBtn.classList.toggle('is-active');
|
131
131
|
menuToggleBtn.setAttribute('aria-expanded', isMenuExpanded ? 'false' : 'true');
|
132
|
+
menuToggleBtn.setAttribute('aria-label', isMenuExpanded ? openLabel : closeLabel);
|
132
133
|
menu.classList.toggle('is-visible');
|
133
134
|
});
|
134
135
|
// Закрытие при клике вне меню
|
@@ -136,6 +137,7 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
|
136
137
|
if (!menuToggleBtn.contains(e.target as Node) && !menu.contains(e.target as Node)) {
|
137
138
|
menuToggleBtn.classList.remove('is-active');
|
138
139
|
menuToggleBtn.setAttribute('aria-expanded', 'false');
|
140
|
+
menuToggleBtn.setAttribute('aria-label', openLabel);
|
139
141
|
menu.classList.remove('is-visible');
|
140
142
|
}
|
141
143
|
});
|
@@ -143,6 +145,7 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
|
143
145
|
window.addEventListener('scroll', () => {
|
144
146
|
menuToggleBtn.classList.remove('is-active');
|
145
147
|
menuToggleBtn.setAttribute('aria-expanded', 'false');
|
148
|
+
menuToggleBtn.setAttribute('aria-label', openLabel);
|
146
149
|
menu.classList.remove('is-visible');
|
147
150
|
});
|
148
151
|
// Закрытие при выборе пункта меню
|
@@ -150,6 +153,7 @@ const navLinks = (maugliConfig.navLinks || []).map((link) => {
|
|
150
153
|
item.addEventListener('click', () => {
|
151
154
|
menuToggleBtn.classList.remove('is-active');
|
152
155
|
menuToggleBtn.setAttribute('aria-expanded', 'false');
|
156
|
+
menuToggleBtn.setAttribute('aria-label', openLabel);
|
153
157
|
menu.classList.remove('is-visible');
|
154
158
|
});
|
155
159
|
});
|
@@ -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-интеграций.**
|