core-maugli 1.0.2
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/LICENSE +727 -0
- package/README.md +108 -0
- package/bin/index.js +9 -0
- package/bin/init.js +38 -0
- package/package.json +73 -0
- package/public/blackbox.webp +0 -0
- package/public/favicon.svg +10 -0
- package/public/footerlabel.svg +18 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/img/page-images/blog_default.webp +0 -0
- package/public/logo-icon.svg +3 -0
- package/public/logoblog-icon.svg +10 -0
- package/public/manifest.webmanifest +21 -0
- package/public/maugli_for_animation.svg +6 -0
- package/public/mauglilabel.svg +17 -0
- package/public/tr-about-1200.webp +0 -0
- package/public/tr-about-400.webp +0 -0
- package/public/tr-about-800.webp +0 -0
- package/public/tr-about.webp +0 -0
- package/public/tr-post-1-1200.webp +0 -0
- package/public/tr-post-1-400.webp +0 -0
- package/public/tr-post-1-800.webp +0 -0
- package/public/tr-post0-1200.webp +0 -0
- package/public/tr-post0-400.webp +0 -0
- package/public/tr-post0-800.webp +0 -0
- package/public/tr-post0.webp +0 -0
- package/public/tr-prewiew.webp +0 -0
- package/resize-all.cjs +29 -0
- package/scripts/featured.js +208 -0
- package/scripts/update-with-backup.js +34 -0
- package/scripts/upgrade-config.js +90 -0
- package/scripts/verify-assets.js +28 -0
- package/src/assets/img/default/autor_default.webp +0 -0
- package/src/assets/img/default/blog_default.webp +0 -0
- package/src/assets/img/default/product_default.webp +0 -0
- package/src/assets/img/default/project_default.webp +0 -0
- package/src/assets/img/default/rubric_default.webp +0 -0
- package/src/assets/img/examples/authors/anna.webp +0 -0
- package/src/assets/img/examples/authors/carlos.webp +0 -0
- package/src/assets/img/examples/authors/daria.webp +0 -0
- package/src/assets/img/examples/authors/dmitry.webp +0 -0
- package/src/assets/img/examples/authors/igor.webp +0 -0
- package/src/assets/img/examples/authors/john.webp +0 -0
- package/src/assets/img/examples/blog/post-1-avtomatizaciya-marketinga-kak-ii-osvobozhdaet-predprinimatelei-ot-cifrovogo-rabstva.webp +0 -0
- package/src/assets/img/examples/blog/post_11.webp +0 -0
- package/src/assets/img/examples/blog/post_12.webp +0 -0
- package/src/assets/img/examples/blog/post_1_jsonld_guide.webp +0 -0
- package/src/assets/img/examples/blog/test-post.webp +0 -0
- package/src/assets/img/examples/blog/tr-post-1.webp +0 -0
- package/src/assets/img/examples/products/product_1.webp +0 -0
- package/src/assets/img/examples/products/product_2.webp +0 -0
- package/src/assets/img/examples/projects/project_1.webp +0 -0
- package/src/assets/img/examples/projects/project_2.webp +0 -0
- package/src/components/ArticleMeta.astro +103 -0
- package/src/components/AuthorCard.astro +95 -0
- package/src/components/AuthorLink.astro +25 -0
- package/src/components/AuthorLinksGroup.astro +135 -0
- package/src/components/Avatar.astro +32 -0
- package/src/components/BaseHead.astro +121 -0
- package/src/components/Breadcrumbs.astro +90 -0
- package/src/components/Button.astro +25 -0
- package/src/components/Card.astro +99 -0
- package/src/components/ContentFooter.astro +147 -0
- package/src/components/CopyLinkButton.astro +133 -0
- package/src/components/CountBadge.astro +17 -0
- package/src/components/Footer.astro +122 -0
- package/src/components/FormattedDate.astro +59 -0
- package/src/components/Header.astro +18 -0
- package/src/components/HeroImage.astro +38 -0
- package/src/components/IconButton.astro +23 -0
- package/src/components/Image.astro +13 -0
- package/src/components/InfoCard.astro +123 -0
- package/src/components/LanguageSwitcher.astro +122 -0
- package/src/components/MaugliFloatingLabel.astro +454 -0
- package/src/components/MobileTagsAndShare.astro +71 -0
- package/src/components/Nav.astro +151 -0
- package/src/components/NavLink.astro +31 -0
- package/src/components/Pagination.astro +70 -0
- package/src/components/PostPreview.astro +22 -0
- package/src/components/ProductBannerCard.astro +47 -0
- package/src/components/ProductPreview.astro +21 -0
- package/src/components/ProjectPreview.astro +21 -0
- package/src/components/RubricCard.astro +142 -0
- package/src/components/ShareIcon.astro +93 -0
- package/src/components/ShareLink.astro +58 -0
- package/src/components/Subscribe.astro +68 -0
- package/src/components/SummaryFAQCard.astro +106 -0
- package/src/components/TableOfContents.astro +143 -0
- package/src/components/TagPill.astro +41 -0
- package/src/components/TagPills.astro +42 -0
- package/src/components/TagsAndShare.astro +379 -0
- package/src/components/TagsSection.astro +203 -0
- package/src/components/ThemeToggle.astro +58 -0
- package/src/config/maugli.config.ts +213 -0
- package/src/content/authors/daria-zorina.md +42 -0
- package/src/content/authors/default-autor.md +47 -0
- package/src/content/authors/igor-sokolov.md +43 -0
- package/src/content/authors/john-walker.md +46 -0
- package/src/content/blog/jsonld-guide.md +260 -0
- package/src/content/blog/post-0.md +49 -0
- package/src/content/blog/post-1-avtomatizaciya-marketinga-kak-ii-osvobozhdaet-predprinimatelei-ot-cifrovogo-rabstva.md +72 -0
- package/src/content/blog/post-agent-experience-mcp-biznes-v-epohu-ii-agentov.md +116 -0
- package/src/content/blog/test-post-2025-07-11.md +73 -0
- package/src/content/config.ts +80 -0
- package/src/content/pages/about.md +40 -0
- package/src/content/pages/about.mdx +27 -0
- package/src/content/pages/authors.mdx +49 -0
- package/src/content/pages/blog.mdx +31 -0
- package/src/content/pages/contact.md +10 -0
- package/src/content/pages/products.mdx +30 -0
- package/src/content/pages/projects.mdx +28 -0
- package/src/content/pages/rubrics.mdx +35 -0
- package/src/content/pages/terms.md +12 -0
- package/src/content/products/example-product.md +28 -0
- package/src/content/products/maugli-editor.md +35 -0
- package/src/content/products/maugli-freeblog.md +162 -0
- package/src/content/projects/example-project.md +28 -0
- package/src/content/projects/project-1.md +70 -0
- package/src/content/projects/project-2.md +33 -0
- package/src/content/tags/ai-business.mdx +18 -0
- package/src/content/tags/automation.mdx +18 -0
- package/src/content/tags/content-strategy.mdx +18 -0
- package/src/content/tags/growth-marketing.mdx +18 -0
- package/src/content/tags/industry-reviews.mdx +18 -0
- package/src/content/tags/interesting.mdx +18 -0
- package/src/content/tags/seo-ai-seo.mdx +18 -0
- package/src/content.config.ts +260 -0
- package/src/data/site-config.ts +164 -0
- package/src/i18n/de.json +126 -0
- package/src/i18n/en.json +126 -0
- package/src/i18n/es.json +126 -0
- package/src/i18n/fr.json +126 -0
- package/src/i18n/index.ts +10 -0
- package/src/i18n/ja.json +126 -0
- package/src/i18n/languages.ts +23 -0
- package/src/i18n/pt.json +126 -0
- package/src/i18n/ru.json +123 -0
- package/src/i18n/zh.json +126 -0
- package/src/icons/ArrowLeft.astro +13 -0
- package/src/icons/ArrowRight.astro +13 -0
- package/src/icons/flags/brazil.svg +14 -0
- package/src/icons/flags/china.svg +15 -0
- package/src/icons/flags/france.svg +12 -0
- package/src/icons/flags/germany.svg +12 -0
- package/src/icons/flags/japan.svg +11 -0
- package/src/icons/flags/russia.svg +12 -0
- package/src/icons/flags/spain.svg +12 -0
- package/src/icons/flags/united arab emirates.svg +13 -0
- package/src/icons/flags/united states.svg +15 -0
- package/src/icons/socials/BlueskyIcon.astro +9 -0
- package/src/icons/socials/EmailIcon.astro +8 -0
- package/src/icons/socials/LinkedinIcon.astro +9 -0
- package/src/icons/socials/MastodonIcon.astro +9 -0
- package/src/icons/socials/MediumIcon.astro +9 -0
- package/src/icons/socials/RedditIcon.astro +11 -0
- package/src/icons/socials/TelegramIcon.astro +11 -0
- package/src/icons/socials/TwitterIcon.astro +9 -0
- package/src/img/default/autor_default.webp +0 -0
- package/src/img/default/default.webp +0 -0
- package/src/img/default/rubric_default.webp +0 -0
- package/src/img/default/test.webp +0 -0
- package/src/img/default/test2.webp +0 -0
- package/src/img/examples/authors/anna.webp +0 -0
- package/src/img/examples/authors/carlos.webp +0 -0
- package/src/img/examples/authors/daria.webp +0 -0
- package/src/img/examples/authors/dmitry.webp +0 -0
- package/src/img/examples/authors/igor.webp +0 -0
- package/src/img/examples/authors/john.webp +0 -0
- package/src/img/examples/blog/post-1-avtomatizaciya-marketinga-kak-ii-osvobozhdaet-predprinimatelei-ot-cifrovogo-rabstva.webp +0 -0
- package/src/img/examples/blog/post-2-avtomatizaciya-kontenta-kak-neiroseti-ubivayut-perfekcionizm-v-biznese.webp +0 -0
- package/src/img/examples/blog/post-3-laik-ne-valyuta-kak-avtomatizaciya-marketinga-spasaet-ot-lozhnyh-metrik.webp +0 -0
- package/src/img/examples/blog/post-5-5-fatalnyh-oshibok-marketinga-kotorye-ubivayut-startapy-na-starte.webp +0 -0
- package/src/img/examples/blog/post-6-5-strategii-kontent-marketinga-dlya-startapov-avtomatizaciya-i-revolyuciya.webp +0 -0
- package/src/img/examples/blog/post-7-viralnyi-kontent-ne-udacha-a-strategiya-avtomatizaciya-marketinga.webp +0 -0
- package/src/img/examples/blog/post-agent-experience-mcp-biznes-v-epohu-ii-agentov.webp +0 -0
- package/src/img/examples/blog/post_1_jsonld_guide.webp +0 -0
- package/src/img/examples/blog/tr-post-1.webp +0 -0
- package/src/layouts/BaseLayout.astro +59 -0
- package/src/pages/404.astro +24 -0
- package/src/pages/[...id].astro +50 -0
- package/src/pages/about.astro +0 -0
- package/src/pages/authors/[...page].astro +105 -0
- package/src/pages/authors/[id].astro +165 -0
- package/src/pages/blog/[...page].astro +59 -0
- package/src/pages/blog/[id].astro +175 -0
- package/src/pages/index.astro +90 -0
- package/src/pages/products/[...page].astro +50 -0
- package/src/pages/products/[id].astro +221 -0
- package/src/pages/projects/[...page].astro +74 -0
- package/src/pages/projects/[id].astro +165 -0
- package/src/pages/projects/tags/[id]/[...page].astro +58 -0
- package/src/pages/rss.xml.js +5 -0
- package/src/pages/tags/[id]/[...page].astro +110 -0
- package/src/pages/tags/index.astro +124 -0
- package/src/scripts/infoCardFadeIn.js +22 -0
- package/src/styles/global.css +272 -0
- package/src/utils/common-utils.ts +0 -0
- package/src/utils/content-loader.ts +14 -0
- package/src/utils/data-utils.ts +49 -0
- package/src/utils/featuredManager.ts +118 -0
- package/src/utils/posts.ts +43 -0
- package/src/utils/reading-time.ts +28 -0
- package/src/utils/remark-slugify.js +8 -0
- package/src/utils/rss.ts +23 -0
- package/typograf-batch.js +49 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
---
|
2
|
+
import { getFilteredCollection } from '../utils/content-loader';
|
3
|
+
import { maugliConfig } from '../config/maugli.config';
|
4
|
+
import { getPostsByAuthor } from '../utils/data-utils';
|
5
|
+
import AuthorLinksGroup from './AuthorLinksGroup.astro';
|
6
|
+
import Avatar from './Avatar.astro';
|
7
|
+
import CountBadge from './CountBadge.astro';
|
8
|
+
|
9
|
+
export interface Props {
|
10
|
+
author: {
|
11
|
+
name: string;
|
12
|
+
position: string;
|
13
|
+
description: string;
|
14
|
+
avatar?: string;
|
15
|
+
socials?: {
|
16
|
+
email?: string;
|
17
|
+
telegram?: string;
|
18
|
+
mastodon?: string;
|
19
|
+
medium?: string;
|
20
|
+
bluesky?: string;
|
21
|
+
reddit?: string;
|
22
|
+
linkedin?: string;
|
23
|
+
twitter?: string;
|
24
|
+
};
|
25
|
+
slug: string;
|
26
|
+
};
|
27
|
+
class?: string;
|
28
|
+
}
|
29
|
+
|
30
|
+
const { author, class: className } = Astro.props;
|
31
|
+
const { name, position, description, avatar, socials, slug } = author;
|
32
|
+
|
33
|
+
// Получаем количество статей автора через getPostsByAuthor
|
34
|
+
let posts: import('astro:content').CollectionEntry<'blog'>[] = [];
|
35
|
+
try {
|
36
|
+
posts = await getFilteredCollection('blog');
|
37
|
+
} catch (e) {
|
38
|
+
posts = [];
|
39
|
+
}
|
40
|
+
const postCount = getPostsByAuthor(posts, slug).length;
|
41
|
+
---
|
42
|
+
|
43
|
+
<article class="w-full border border-[var(--border-main)] rounded-custom card-bg hover:card-shadow hover:-translate-y-1 transition-all duration-300 p-6 group">
|
44
|
+
<div class="flex flex-col gap-2">
|
45
|
+
<!-- Аватар и имя/должность в одной строке -->
|
46
|
+
<div class="flex flex-row items-start gap-4">
|
47
|
+
<!-- Аватар автора слева -->
|
48
|
+
<Avatar
|
49
|
+
src={avatar || maugliConfig.defaultAuthorImage || 'src/img/default/autor_default.webp'}
|
50
|
+
alt={name}
|
51
|
+
size="clamp(70px, 15vw, 100px)"
|
52
|
+
class="no-border"
|
53
|
+
/>
|
54
|
+
|
55
|
+
<!-- Имя, должность и соцсети справа -->
|
56
|
+
<div class="flex flex-col mt-1gap-0.5 flex-1 min-w-0">
|
57
|
+
<a href={`/authors/${slug}/`} class="block" style="text-decoration: none;">
|
58
|
+
<h3
|
59
|
+
class="font-serif font-[800] text-[1.3rem] text-[var(--text-heading)] hover:text-[var(--brand-color)] transition-colors duration-300 break-words"
|
60
|
+
style="line-height: 1.05; word-wrap: break-word; overflow-wrap: break-word;"
|
61
|
+
>
|
62
|
+
{name}
|
63
|
+
{maugliConfig.showAuthorArticleCount && postCount > 0 && <CountBadge count={postCount} />}
|
64
|
+
</h3>
|
65
|
+
<p class="text-sm text-[var(--text-main)] mt-1 mb-2 opacity-37 break-words" style="word-wrap: break-word; overflow-wrap: break-word;">
|
66
|
+
{position}
|
67
|
+
</p>
|
68
|
+
</a>
|
69
|
+
<!-- Иконки соцсетей -->
|
70
|
+
|
71
|
+
<AuthorLinksGroup authorName={name} socials={socials} maxLinks={8} class="flex-row items-center gap-2 shrink-0" />
|
72
|
+
</div>
|
73
|
+
</div>
|
74
|
+
|
75
|
+
<!-- Описание автора -->
|
76
|
+
{
|
77
|
+
description && (
|
78
|
+
<div class="mt-0 ml-2">
|
79
|
+
<a href={`/authors/${slug}/`} class="block" style="text-decoration: none;">
|
80
|
+
<p class="text-[1rem] text-[var(--text-main)] leading-[1.35] opacity-65 line-clamp-3 hover:opacity-80 transition-opacity duration-300">
|
81
|
+
{description}
|
82
|
+
</p>
|
83
|
+
</a>
|
84
|
+
</div>
|
85
|
+
)
|
86
|
+
}
|
87
|
+
</div>
|
88
|
+
</article>
|
89
|
+
|
90
|
+
<style>
|
91
|
+
/* Убираем тень при наведении, чтобы она совпадала с рубриками */
|
92
|
+
article:hover {
|
93
|
+
box-shadow: none;
|
94
|
+
}
|
95
|
+
</style>
|
@@ -0,0 +1,25 @@
|
|
1
|
+
---
|
2
|
+
export interface Props {
|
3
|
+
href?: string;
|
4
|
+
platform: string;
|
5
|
+
icon: any;
|
6
|
+
ariaLabel: string;
|
7
|
+
}
|
8
|
+
|
9
|
+
const { href, platform, icon: IconComponent, ariaLabel } = Astro.props;
|
10
|
+
---
|
11
|
+
|
12
|
+
{
|
13
|
+
href && (
|
14
|
+
<a
|
15
|
+
href={href}
|
16
|
+
target={platform === 'email' ? undefined : '_blank'}
|
17
|
+
rel={platform === 'email' ? undefined : 'noopener noreferrer'}
|
18
|
+
aria-label={ariaLabel}
|
19
|
+
class="flex items-center justify-center w-7 h-7 border border-[var(--text-main)] rounded-circle shrink-0 opacity-30 hover:opacity-100 hover:border-[var(--text-main)] transition-all duration-300"
|
20
|
+
style="text-decoration: none; padding: 0;"
|
21
|
+
>
|
22
|
+
<IconComponent class="w-4 h-4 text-[var(--text-main)]" />
|
23
|
+
</a>
|
24
|
+
)
|
25
|
+
}
|
@@ -0,0 +1,135 @@
|
|
1
|
+
---
|
2
|
+
import { maugliConfig } from '../config/maugli.config';
|
3
|
+
import { LANGUAGES } from '../i18n/languages';
|
4
|
+
import BlueskyIcon from '../icons/socials/BlueskyIcon.astro';
|
5
|
+
import EmailIcon from '../icons/socials/EmailIcon.astro';
|
6
|
+
import LinkedinIcon from '../icons/socials/LinkedinIcon.astro';
|
7
|
+
import MastodonIcon from '../icons/socials/MastodonIcon.astro';
|
8
|
+
import MediumIcon from '../icons/socials/MediumIcon.astro';
|
9
|
+
import RedditIcon from '../icons/socials/RedditIcon.astro';
|
10
|
+
import TelegramIcon from '../icons/socials/TelegramIcon.astro';
|
11
|
+
import TwitterIcon from '../icons/socials/TwitterIcon.astro';
|
12
|
+
import AuthorLink from './AuthorLink.astro';
|
13
|
+
// Универсальный импорт словарей по доступным языкам
|
14
|
+
const dicts: Record<string, any> = {};
|
15
|
+
for (const lang of LANGUAGES) {
|
16
|
+
try {
|
17
|
+
dicts[lang.code] = await import(`../i18n/${lang.code}.json`).then((m) => m.default);
|
18
|
+
} catch {}
|
19
|
+
}
|
20
|
+
|
21
|
+
export interface Props {
|
22
|
+
socials?: {
|
23
|
+
email?: string;
|
24
|
+
telegram?: string;
|
25
|
+
mastodon?: string;
|
26
|
+
medium?: string;
|
27
|
+
bluesky?: string;
|
28
|
+
reddit?: string;
|
29
|
+
linkedin?: string;
|
30
|
+
twitter?: string;
|
31
|
+
};
|
32
|
+
authorName: string;
|
33
|
+
maxLinks?: number;
|
34
|
+
showContestButton?: boolean;
|
35
|
+
contestUrl?: string;
|
36
|
+
contestLabel?: string;
|
37
|
+
showRss?: boolean;
|
38
|
+
rssLang?: string;
|
39
|
+
}
|
40
|
+
|
41
|
+
const {
|
42
|
+
socials,
|
43
|
+
authorName,
|
44
|
+
maxLinks = 3,
|
45
|
+
showContestButton = false,
|
46
|
+
contestUrl = '#',
|
47
|
+
contestLabel = 'Конкурс',
|
48
|
+
showRss = false,
|
49
|
+
rssLang = 'en'
|
50
|
+
} = Astro.props;
|
51
|
+
|
52
|
+
// Все доступные платформы в нужном порядке
|
53
|
+
const allPlatforms = ['email', 'linkedin', 'twitter', 'telegram', 'reddit', 'medium', 'mastodon', 'bluesky'];
|
54
|
+
|
55
|
+
// Берем первые maxLinks платформ
|
56
|
+
const platformsToShow = allPlatforms.slice(0, maxLinks);
|
57
|
+
|
58
|
+
// Функция для получения компонента иконки
|
59
|
+
const getIconComponent = (platform: string) => {
|
60
|
+
switch (platform) {
|
61
|
+
case 'email':
|
62
|
+
return EmailIcon;
|
63
|
+
case 'telegram':
|
64
|
+
return TelegramIcon;
|
65
|
+
case 'mastodon':
|
66
|
+
return MastodonIcon;
|
67
|
+
case 'medium':
|
68
|
+
return MediumIcon;
|
69
|
+
case 'bluesky':
|
70
|
+
return BlueskyIcon;
|
71
|
+
case 'reddit':
|
72
|
+
return RedditIcon;
|
73
|
+
case 'linkedin':
|
74
|
+
return LinkedinIcon;
|
75
|
+
case 'twitter':
|
76
|
+
return TwitterIcon;
|
77
|
+
default:
|
78
|
+
return EmailIcon;
|
79
|
+
}
|
80
|
+
};
|
81
|
+
|
82
|
+
const lang = maugliConfig.defaultLang || 'en';
|
83
|
+
const dict = dicts[lang] || dicts['en'] || {};
|
84
|
+
---
|
85
|
+
|
86
|
+
{
|
87
|
+
(platformsToShow.length > 0 || showContestButton) && (
|
88
|
+
<div class="flex flex-row flex-wrap items-center justify-start p-0 gap-2 shrink-0">
|
89
|
+
{platformsToShow.map((platform) => {
|
90
|
+
const IconComponent = getIconComponent(platform);
|
91
|
+
const rawUrl = socials?.[platform as keyof typeof socials];
|
92
|
+
// Для email добавляем mailto: если его нет
|
93
|
+
const url = platform === 'email' && rawUrl && !rawUrl.startsWith('mailto:') ? `mailto:${rawUrl}` : rawUrl;
|
94
|
+
const platformLabel = dict.socials[platform] || platform;
|
95
|
+
const ariaLabel = `${authorName} ${dict.pages.authors.onPlatform} ${platformLabel}`;
|
96
|
+
return <AuthorLink href={url} platform={platform} icon={IconComponent} ariaLabel={ariaLabel} />;
|
97
|
+
})}
|
98
|
+
{showContestButton && (
|
99
|
+
<a
|
100
|
+
href={contestUrl}
|
101
|
+
class="flex items-center justify-center w-7 h-7 border border-[var(--text-main)] rounded-circle shrink-0 opacity-30 hover:opacity-100 hover:border-[var(--text-main)] transition-all duration-300 bg-transparent text-[var(--text-main)] text-xs font-bold"
|
102
|
+
style="text-decoration: none; padding: 0;"
|
103
|
+
target="_blank"
|
104
|
+
rel="noopener noreferrer"
|
105
|
+
>
|
106
|
+
{contestLabel}
|
107
|
+
</a>
|
108
|
+
)}
|
109
|
+
{showRss &&
|
110
|
+
(rssLang === 'ru' ? (
|
111
|
+
<a class="footer-link ml-2 flex items-center text-muted" href="/rss.xml" target="_blank" rel="noopener" style="color:var(--text-muted);">
|
112
|
+
RSS
|
113
|
+
</a>
|
114
|
+
) : (
|
115
|
+
<a class="footer-link ml-2 flex items-center rss-outline" href="/rss.xml" aria-label="RSS" target="_blank" rel="noopener">
|
116
|
+
<svg
|
117
|
+
width="20"
|
118
|
+
height="20"
|
119
|
+
viewBox="0 0 22 22"
|
120
|
+
fill="none"
|
121
|
+
stroke="var(--text-muted)"
|
122
|
+
stroke-width="1.7"
|
123
|
+
stroke-linecap="round"
|
124
|
+
stroke-linejoin="round"
|
125
|
+
style="display:inline;vertical-align:middle;"
|
126
|
+
>
|
127
|
+
<circle cx="6.18" cy="17.82" r="2.18" fill="none" stroke="var(--text-muted)" stroke-width="1.7" />
|
128
|
+
<path d="M4 4a16 16 0 0 1 16 16" />
|
129
|
+
<path d="M4 11a9 9 0 0 1 9 9" />
|
130
|
+
</svg>
|
131
|
+
</a>
|
132
|
+
))}
|
133
|
+
</div>
|
134
|
+
)
|
135
|
+
}
|
@@ -0,0 +1,32 @@
|
|
1
|
+
---
|
2
|
+
export interface Props {
|
3
|
+
src?: string;
|
4
|
+
alt: string;
|
5
|
+
size?: string | number;
|
6
|
+
class?: string;
|
7
|
+
}
|
8
|
+
|
9
|
+
const { src, alt, size = '100px', class: className } = Astro.props;
|
10
|
+
|
11
|
+
// Определяем размер - может быть строкой (например, "80px", "5rem") или числом (пиксели)
|
12
|
+
const avatarSize = typeof size === 'number' ? `${size}px` : size;
|
13
|
+
---
|
14
|
+
|
15
|
+
<div class:list={['avatar-container', className]} style={`width: ${avatarSize}; height: ${avatarSize};`}>
|
16
|
+
<img src={src || 'src/img/default/autor_default.webp'} alt={alt} loading="lazy" class="w-full h-full object-cover" />
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<style>
|
20
|
+
.avatar-container {
|
21
|
+
border-radius: var(--rounded-circle, 50%);
|
22
|
+
overflow: hidden;
|
23
|
+
border: 2px solid var(--border-main);
|
24
|
+
background: var(--bg-muted);
|
25
|
+
flex-shrink: 0;
|
26
|
+
}
|
27
|
+
|
28
|
+
.avatar-container.no-border {
|
29
|
+
border: none;
|
30
|
+
background: transparent;
|
31
|
+
}
|
32
|
+
</style>
|
@@ -0,0 +1,121 @@
|
|
1
|
+
---
|
2
|
+
import { getFilteredCollection } from '../utils/content-loader';
|
3
|
+
import siteConfig from '../data/site-config.ts';
|
4
|
+
import '../styles/global.css';
|
5
|
+
// Определи тип выше:
|
6
|
+
export type ImageMimeType = 'image/jpeg' | 'image/png' | 'image/webp';
|
7
|
+
|
8
|
+
export type Props = {
|
9
|
+
title?: string;
|
10
|
+
description?: string;
|
11
|
+
image?: {
|
12
|
+
width?: string;
|
13
|
+
height?: string;
|
14
|
+
type?: ImageMimeType;
|
15
|
+
src: string;
|
16
|
+
alt?: string;
|
17
|
+
};
|
18
|
+
pageType?: 'website' | 'article';
|
19
|
+
seo?: {
|
20
|
+
title?: string;
|
21
|
+
description?: string;
|
22
|
+
keywords?: string[];
|
23
|
+
image?: {
|
24
|
+
width?: string;
|
25
|
+
height?: string;
|
26
|
+
type?: ImageMimeType;
|
27
|
+
src: string;
|
28
|
+
alt?: string;
|
29
|
+
};
|
30
|
+
pageType?: 'website' | 'article';
|
31
|
+
};
|
32
|
+
};
|
33
|
+
|
34
|
+
const { seo = {}, title: baseTitle, description: baseDescription = '', image: baseImage = siteConfig.image, pageType: basePageType = 'website' } = Astro.props;
|
35
|
+
|
36
|
+
// ОДНА переменная для title, description, keywords, image, pageType
|
37
|
+
const seoTitle = seo.title || baseTitle || siteConfig.title;
|
38
|
+
const seoDescription = seo.description || baseDescription || siteConfig.description;
|
39
|
+
const seoKeywords = seo.keywords?.length ? seo.keywords.join(', ') : '';
|
40
|
+
const seoImage = seo.image || baseImage || siteConfig.image;
|
41
|
+
|
42
|
+
const resolvedImage =
|
43
|
+
seoImage && seoImage.src
|
44
|
+
? {
|
45
|
+
src: new URL(seoImage.src, Astro.site).toString(),
|
46
|
+
alt: seoImage.alt,
|
47
|
+
width: 'width' in seoImage ? seoImage.width : undefined,
|
48
|
+
height: 'height' in seoImage ? seoImage.height : undefined,
|
49
|
+
type: 'type' in seoImage ? seoImage.type : undefined
|
50
|
+
}
|
51
|
+
: undefined;
|
52
|
+
const pageType = seo.pageType || basePageType;
|
53
|
+
const canonicalURL = new URL(Astro.request.url, Astro.site);
|
54
|
+
|
55
|
+
/**
|
56
|
+
* Enforce some standard canonical URL formatting across the site.
|
57
|
+
*/
|
58
|
+
function formatCanonicalURL(url: string | URL) {
|
59
|
+
const path = url.toString();
|
60
|
+
const hasQueryParams = path.includes('?');
|
61
|
+
// If there are query params, make sure the URL has no trailing slash
|
62
|
+
if (hasQueryParams) {
|
63
|
+
return path.replace(/\/?$/, '');
|
64
|
+
}
|
65
|
+
// otherwise, canonical URL always has a trailing slash
|
66
|
+
return path.replace(/\/?$/, '/');
|
67
|
+
}
|
68
|
+
|
69
|
+
let authors = [];
|
70
|
+
try {
|
71
|
+
authors = await getFilteredCollection('authors');
|
72
|
+
} catch (e) {
|
73
|
+
console.warn('Не удалось загрузить коллекцию авторов:', e);
|
74
|
+
}
|
75
|
+
if (authors.length === 0) {
|
76
|
+
throw new Error('В коллекции авторов нет ни одного автора. Создайте хотя бы одного автора!');
|
77
|
+
}
|
78
|
+
let defaultAuthor = authors[0];
|
79
|
+
let defaultAuthorName = defaultAuthor.data.name;
|
80
|
+
---
|
81
|
+
|
82
|
+
<!-- High Priority Global Metadata -->
|
83
|
+
<meta charset="utf-8" />
|
84
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
85
|
+
<title>{seoTitle}</title>
|
86
|
+
<meta name="robots" content="index, follow" />
|
87
|
+
<meta name="generator" content={Astro.generator} />
|
88
|
+
|
89
|
+
<!-- SEO -->
|
90
|
+
<meta name="description" content={seoDescription} />
|
91
|
+
<meta name="keywords" content={seoKeywords} />
|
92
|
+
<meta name="author" content={defaultAuthorName} />
|
93
|
+
<link rel="canonical" href={formatCanonicalURL(canonicalURL)} />
|
94
|
+
|
95
|
+
<!-- Low Priority Global Metadata -->
|
96
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
97
|
+
<link rel="manifest" href="/manifest.webmanifest" />
|
98
|
+
<meta name="theme-color" content="var(--theme-color)" />
|
99
|
+
<link rel="apple-touch-icon" sizes="192x192" href="/icon-192.png" />
|
100
|
+
<link rel="apple-touch-icon" sizes="512x512" href="/icon-512.png" />
|
101
|
+
<link rel="sitemap" href="/sitemap-index.xml" />
|
102
|
+
<link rel="alternate" type="application/rss+xml" href="/rss.xml" title="RSS" />
|
103
|
+
|
104
|
+
<!-- Open Graph / Facebook -->
|
105
|
+
<meta property="og:type" content={pageType} />
|
106
|
+
<meta property="og:url" content={formatCanonicalURL(canonicalURL)} />
|
107
|
+
<meta property="og:title" content={seoTitle} />
|
108
|
+
<meta property="og:description" content={seoDescription} />
|
109
|
+
{resolvedImage?.src && <meta property="og:image" content={resolvedImage.src} />}
|
110
|
+
{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} />}
|
113
|
+
{resolvedImage?.type && <meta property="og:image:type" content={resolvedImage.type} />}
|
114
|
+
|
115
|
+
<!-- X/Twitter -->
|
116
|
+
<meta property="twitter:card" content="summary_large_image" />
|
117
|
+
<meta property="twitter:url" content={formatCanonicalURL(canonicalURL)} />
|
118
|
+
<meta property="twitter:title" content={seoTitle} />
|
119
|
+
<meta property="twitter:description" content={seoDescription} />
|
120
|
+
{resolvedImage?.src && <meta property="twitter:image" content={resolvedImage.src} />}
|
121
|
+
{resolvedImage?.alt && <meta name="twitter:image:alt" content={resolvedImage?.alt} />}
|
@@ -0,0 +1,90 @@
|
|
1
|
+
---
|
2
|
+
import { maugliConfig } from '../config/maugli.config';
|
3
|
+
import { LANGUAGES } from '../i18n/languages';
|
4
|
+
// Универсальный импорт словарей по доступным языкам
|
5
|
+
const dicts: Record<string, any> = {};
|
6
|
+
for (const lang of LANGUAGES) {
|
7
|
+
try {
|
8
|
+
dicts[lang.code] = await import(`../i18n/${lang.code}.json`).then((m) => m.default);
|
9
|
+
} catch {}
|
10
|
+
}
|
11
|
+
|
12
|
+
export interface Props {
|
13
|
+
class?: string;
|
14
|
+
}
|
15
|
+
|
16
|
+
const { class: className = '' } = Astro.props;
|
17
|
+
const lang: string = maugliConfig.defaultLang || 'en';
|
18
|
+
const dict = dicts[lang] || dicts['en'] || {};
|
19
|
+
const navLinks = maugliConfig.navLinks || [];
|
20
|
+
const currentUrl = Astro.url.pathname;
|
21
|
+
|
22
|
+
// Не показываем хлебные крошки на главной странице
|
23
|
+
if (currentUrl === '/') {
|
24
|
+
return null;
|
25
|
+
}
|
26
|
+
|
27
|
+
// Универсальная логика: строим крошки по navLinks и сегментам
|
28
|
+
const crumbs = [];
|
29
|
+
// Логотип
|
30
|
+
crumbs.push({
|
31
|
+
href: maugliConfig.brand.logoHref && maugliConfig.brand.logoHref.trim() ? maugliConfig.brand.logoHref : '/',
|
32
|
+
icon: maugliConfig.brand.logoBreadcrumbsLight
|
33
|
+
});
|
34
|
+
// Главная страница (navLinks[0])
|
35
|
+
const mainNav = navLinks[0];
|
36
|
+
if (mainNav) {
|
37
|
+
const mainLabel = mainNav.label || dict.nav?.[mainNav.key] || en.nav[mainNav.key] || 'Blog';
|
38
|
+
crumbs.push({ href: mainNav.href, label: mainLabel });
|
39
|
+
}
|
40
|
+
// Сегменты пути (кроме главной и последнего)
|
41
|
+
const pathParts = currentUrl.split('/').filter(Boolean);
|
42
|
+
let path = '';
|
43
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
44
|
+
path += '/' + pathParts[i];
|
45
|
+
const nav = navLinks.find((l) => l.href === path);
|
46
|
+
if (nav) {
|
47
|
+
const navLabel = nav.label || dict.nav?.[nav.key] || en.nav[nav.key] || nav.key;
|
48
|
+
crumbs.push({ href: nav.href, label: navLabel });
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
// Удаляем устаревшую логику с isCurrent
|
53
|
+
|
54
|
+
const isTagsRoot = currentUrl === '/tags';
|
55
|
+
const isTagPage = currentUrl.startsWith('/tags/') && pathParts.length === 1;
|
56
|
+
---
|
57
|
+
|
58
|
+
<div class:list={['flex items-center gap-3 mb-6', className]}>
|
59
|
+
{
|
60
|
+
crumbs.map((crumb, idx) =>
|
61
|
+
idx === 0 ? (
|
62
|
+
<a href={crumb.href} class="flex-shrink-0" target="_blank" rel="noopener noreferrer">
|
63
|
+
<picture>
|
64
|
+
<img src={crumb.icon} alt="Maugli" class="w-9 h-9" />
|
65
|
+
</picture>
|
66
|
+
</a>
|
67
|
+
) : (
|
68
|
+
<>
|
69
|
+
<svg class="w-4 h-4 flex-shrink-0" style="color: var(--text-muted)" fill="currentColor" viewBox="0 0 20 20">
|
70
|
+
<path
|
71
|
+
fill-rule="evenodd"
|
72
|
+
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
73
|
+
clip-rule="evenodd"
|
74
|
+
/>
|
75
|
+
</svg>
|
76
|
+
<a href={crumb.href} class="text-sm font-bold" style="color: var(--text-heading)" data-astro-reload>
|
77
|
+
{crumb.label}
|
78
|
+
</a>
|
79
|
+
</>
|
80
|
+
)
|
81
|
+
)
|
82
|
+
}
|
83
|
+
<!-- Добавляем стрелку в самом конце всегда -->
|
84
|
+
<svg class="w-4 h-4 flex-shrink-0" style="color: var(--text-muted)" fill="currentColor" viewBox="0 0 20 20">
|
85
|
+
<path
|
86
|
+
fill-rule="evenodd"
|
87
|
+
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
88
|
+
clip-rule="evenodd"></path>
|
89
|
+
</svg>
|
90
|
+
</div>
|
@@ -0,0 +1,25 @@
|
|
1
|
+
---
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
3
|
+
|
4
|
+
type AnchorProps = HTMLAttributes<'a'> & { type?: never };
|
5
|
+
type ButtonProps = HTMLAttributes<'button'> & { href?: never };
|
6
|
+
|
7
|
+
type Props = ButtonProps | AnchorProps;
|
8
|
+
|
9
|
+
const { href, class: className, ...rest } = Astro.props;
|
10
|
+
const baseClasses = 'inline-flex items-center justify-center px-9 py-4 font-serif leading-tight transition rounded-custom';
|
11
|
+
const defaultClasses = 'text-main bg-main border border-main hover:bg-muted';
|
12
|
+
const buttonClasses = className?.includes('!bg-') ? baseClasses : `${baseClasses} ${defaultClasses}`;
|
13
|
+
---
|
14
|
+
|
15
|
+
{
|
16
|
+
href ? (
|
17
|
+
<a href={href} class:list={[buttonClasses, className]} {...rest}>
|
18
|
+
<slot />
|
19
|
+
</a>
|
20
|
+
) : (
|
21
|
+
<button class:list={[buttonClasses, 'cursor-pointer', className]} {...rest}>
|
22
|
+
<slot />
|
23
|
+
</button>
|
24
|
+
)
|
25
|
+
}
|
@@ -0,0 +1,99 @@
|
|
1
|
+
---
|
2
|
+
import { maugliConfig } from '../config/maugli.config';
|
3
|
+
import FormattedDate from './FormattedDate.astro';
|
4
|
+
|
5
|
+
type Props = {
|
6
|
+
href: string;
|
7
|
+
title: string;
|
8
|
+
image?: any;
|
9
|
+
seo?: any;
|
10
|
+
publishDate?: Date;
|
11
|
+
excerpt?: string;
|
12
|
+
description?: string;
|
13
|
+
headingLevel?: 'h2' | 'h3';
|
14
|
+
isFeatured?: boolean;
|
15
|
+
class?: string;
|
16
|
+
type?: string; // добавлен тип
|
17
|
+
};
|
18
|
+
|
19
|
+
const { href, title, image, seo, publishDate, excerpt, description, headingLevel = 'h2', isFeatured = false, class: className, type } = Astro.props;
|
20
|
+
|
21
|
+
const TitleTag = headingLevel;
|
22
|
+
|
23
|
+
// Определяем изображение для карточки
|
24
|
+
const cardImage =
|
25
|
+
seo?.image?.src ||
|
26
|
+
(image && typeof image.src === 'string' && image.src.length > 0 ? image.src : undefined) ||
|
27
|
+
(type === 'blog'
|
28
|
+
? maugliConfig.defaultBlogImage
|
29
|
+
: type === 'project'
|
30
|
+
? maugliConfig.defaultProjectImage
|
31
|
+
: type === 'product'
|
32
|
+
? maugliConfig.defaultProductImage
|
33
|
+
: maugliConfig.seo.defaultImage);
|
34
|
+
const cardImageAlt = seo?.image?.alt || image?.alt || title || 'Изображение';
|
35
|
+
|
36
|
+
// Определяем контент для отображения
|
37
|
+
const content = excerpt || description;
|
38
|
+
---
|
39
|
+
|
40
|
+
<article
|
41
|
+
class:list={[
|
42
|
+
'w-full border border-[var(--border-main)] rounded-custom overflow-hidden transition-all duration-300 relative group hover:-translate-y-1',
|
43
|
+
isFeatured ? 'bg-[var(--bg-muted)]' : 'bg-[var(--bg-main)]',
|
44
|
+
className
|
45
|
+
]}
|
46
|
+
>
|
47
|
+
<a href={href} class="block w-full h-full">
|
48
|
+
<!-- Изображение -->
|
49
|
+
<div class="w-full aspect-[1200/630] bg-[var(--bg-muted)] overflow-hidden relative">
|
50
|
+
<img
|
51
|
+
src={cardImage}
|
52
|
+
alt={cardImageAlt}
|
53
|
+
loading="lazy"
|
54
|
+
class="w-full h-full object-cover rounded-custom transition-transform duration-300 group-hover:scale-105"
|
55
|
+
/>
|
56
|
+
|
57
|
+
<!-- Звездочка featured в правом верхнем углу -->
|
58
|
+
{
|
59
|
+
isFeatured && (
|
60
|
+
<div class="absolute top-0 right-0 mr-4">
|
61
|
+
<svg width="36" height="48" viewBox="0 0 36 48" fill="none" class="w-6 h-8">
|
62
|
+
<path
|
63
|
+
d="M36 40C36 44.4183 32.4183 48 28 48H8C3.58172 48 1.28855e-07 44.4183 0 40V0H36V40ZM18.8574 18.4238C18.4688 17.7781 17.5323 17.7782 17.1436 18.4238L13.6104 24.2939C13.4708 24.5258 13.2431 24.6918 12.9795 24.7529L6.30469 26.2988C5.57047 26.469 5.28131 27.3595 5.77539 27.9287L10.2666 33.1025C10.4439 33.307 10.5312 33.5751 10.5078 33.8447L9.91504 40.6709C9.84998 41.4217 10.6078 41.9716 11.3018 41.6777L17.6104 39.0049C17.8595 38.8995 18.1415 38.8994 18.3906 39.0049L24.6992 41.6777C25.3931 41.9714 26.151 41.4216 26.0859 40.6709L25.4932 33.8447C25.4698 33.5751 25.557 33.307 25.7344 33.1025L30.2266 27.9287C30.72 27.3596 30.4301 26.4691 29.6963 26.2988L23.0215 24.7529C22.7578 24.6918 22.5302 24.5258 22.3906 24.2939L18.8574 18.4238Z"
|
64
|
+
fill="var(--brand-color)"
|
65
|
+
/>
|
66
|
+
</svg>
|
67
|
+
</div>
|
68
|
+
)
|
69
|
+
}
|
70
|
+
{/* Дата только для статей */}
|
71
|
+
{
|
72
|
+
type === 'blog' && publishDate && (
|
73
|
+
<div class="absolute top-4 left-5 md:left-6 lg:left-7">
|
74
|
+
<div class="px-2 py-1 bg-[var(--bg-main-40)] backdrop-blur-sm rounded-normal text-[11px] text-[var(--text-main)] leading-[1.4]">
|
75
|
+
<FormattedDate date={publishDate} />
|
76
|
+
</div>
|
77
|
+
</div>
|
78
|
+
)
|
79
|
+
}
|
80
|
+
</div>
|
81
|
+
|
82
|
+
<!-- Контент -->
|
83
|
+
<div class="p-5 md:p-6 lg:p-7 flex flex-col gap-3 md:gap-4">
|
84
|
+
<TitleTag
|
85
|
+
class="text-xl md:text-xl lg:text-[22px] leading-[1.15] font-serif font-extrabold text-[var(--text-heading)] m-0 group-hover:text-[var(--brand-color)] transition-colors duration-300"
|
86
|
+
>
|
87
|
+
{title}
|
88
|
+
</TitleTag>
|
89
|
+
|
90
|
+
{content && <div class="text-sm md:text-[15px] opacity-60 leading-[1.5] text-[var(--text-main)] flex-grow">{content}</div>}
|
91
|
+
</div>
|
92
|
+
</a>
|
93
|
+
</article>
|
94
|
+
|
95
|
+
<style>
|
96
|
+
.group:hover {
|
97
|
+
box-shadow: var(--card-shadow);
|
98
|
+
}
|
99
|
+
</style>
|