core-maugli 1.2.2 → 1.2.4

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.
Files changed (92) hide show
  1. package/README.md +29 -15
  2. package/package.json +6 -13
  3. package/public/img/examples/blog/previews/post-1-avtomatizaciya-marketinga-kak-ii-osvobozhdaet-predprinimatelei-ot-cifrovogo-rabstva.webp +0 -0
  4. package/public/img/examples/blog/previews/post-2-avtomatizaciya-kontenta-kak-neiroseti-ubivayut-perfekcionizm-v-biznese.webp +0 -0
  5. package/public/img/examples/blog/previews/post-3-laik-ne-valyuta-kak-avtomatizaciya-marketinga-spasaet-ot-lozhnyh-metrik.webp +0 -0
  6. package/public/img/examples/blog/previews/post-5-5-fatalnyh-oshibok-marketinga-kotorye-ubivayut-startapy-na-starte.webp +0 -0
  7. package/public/img/examples/blog/previews/post-6-5-strategii-kontent-marketinga-dlya-startapov-avtomatizaciya-i-revolyuciya.webp +0 -0
  8. package/public/img/examples/blog/previews/post-7-viralnyi-kontent-ne-udacha-a-strategiya-avtomatizaciya-marketinga.webp +0 -0
  9. package/public/img/examples/blog/previews/post-agent-experience-mcp-biznes-v-epohu-ii-agentov.webp +0 -0
  10. package/public/img/examples/blog/previews/post_11.webp +0 -0
  11. package/public/img/examples/blog/previews/post_12.webp +0 -0
  12. package/public/img/examples/blog/previews/post_1_jsonld_guide.webp +0 -0
  13. package/public/img/examples/blog/previews/test-post.webp +0 -0
  14. package/public/img/examples/blog/previews/tr-post-1.webp +0 -0
  15. package/public/img/examples/products/previews/product_1.webp +0 -0
  16. package/public/img/examples/products/previews/product_2.webp +0 -0
  17. package/public/img/examples/projects/previews/project_1.webp +0 -0
  18. package/public/img/examples/projects/previews/project_2.webp +0 -0
  19. package/scripts/generate-previews.js +175 -0
  20. package/scripts/update-components.js +166 -0
  21. package/scripts/upgrade-config.js +23 -3
  22. package/src/components/LanguageSwitcher.astro +2 -16
  23. package/astro.config.mjs +0 -92
  24. package/bin/init.js +0 -201
  25. package/public/img/default/autor_default.webp +0 -0
  26. package/public/img/default/blog_default.webp +0 -0
  27. package/public/img/default/default.webp +0 -0
  28. package/public/img/default/product_default.webp +0 -0
  29. package/public/img/default/project_default.webp +0 -0
  30. package/public/img/default/rubric_default.webp +0 -0
  31. package/public/img/default/test.webp +0 -0
  32. package/public/img/default/test2.webp +0 -0
  33. package/resize-all.cjs +0 -29
  34. package/src/i18n/de.json +0 -126
  35. package/src/i18n/en.json +0 -126
  36. package/src/i18n/es.json +0 -126
  37. package/src/i18n/fr.json +0 -126
  38. package/src/i18n/index.ts +0 -10
  39. package/src/i18n/ja.json +0 -126
  40. package/src/i18n/languages.ts +0 -23
  41. package/src/i18n/pt.json +0 -126
  42. package/src/i18n/ru.json +0 -123
  43. package/src/i18n/zh.json +0 -126
  44. package/src/icons/ArrowLeft.astro +0 -13
  45. package/src/icons/ArrowRight.astro +0 -13
  46. package/src/icons/flags/brazil.svg +0 -14
  47. package/src/icons/flags/china.svg +0 -15
  48. package/src/icons/flags/france.svg +0 -12
  49. package/src/icons/flags/germany.svg +0 -12
  50. package/src/icons/flags/japan.svg +0 -11
  51. package/src/icons/flags/russia.svg +0 -12
  52. package/src/icons/flags/spain.svg +0 -12
  53. package/src/icons/flags/united arab emirates.svg +0 -13
  54. package/src/icons/flags/united states.svg +0 -15
  55. package/src/icons/socials/BlueskyIcon.astro +0 -9
  56. package/src/icons/socials/EmailIcon.astro +0 -8
  57. package/src/icons/socials/LinkedinIcon.astro +0 -9
  58. package/src/icons/socials/MastodonIcon.astro +0 -9
  59. package/src/icons/socials/MediumIcon.astro +0 -9
  60. package/src/icons/socials/RedditIcon.astro +0 -11
  61. package/src/icons/socials/TelegramIcon.astro +0 -11
  62. package/src/icons/socials/TwitterIcon.astro +0 -9
  63. package/src/layouts/BaseLayout.astro +0 -59
  64. package/src/pages/404.astro +0 -24
  65. package/src/pages/[...id].astro +0 -50
  66. package/src/pages/about.astro +0 -0
  67. package/src/pages/authors/[...page].astro +0 -105
  68. package/src/pages/authors/[id].astro +0 -175
  69. package/src/pages/blog/[...page].astro +0 -59
  70. package/src/pages/blog/[id].astro +0 -175
  71. package/src/pages/index.astro +0 -90
  72. package/src/pages/products/[...page].astro +0 -50
  73. package/src/pages/products/[id].astro +0 -221
  74. package/src/pages/projects/[...page].astro +0 -74
  75. package/src/pages/projects/[id].astro +0 -165
  76. package/src/pages/projects/tags/[id]/[...page].astro +0 -58
  77. package/src/pages/rss.xml.js +0 -5
  78. package/src/pages/tags/[id]/[...page].astro +0 -110
  79. package/src/pages/tags/index.astro +0 -124
  80. package/src/scripts/infoCardFadeIn.js +0 -22
  81. package/src/styles/global.css +0 -273
  82. package/src/utils/common-utils.ts +0 -0
  83. package/src/utils/content-loader.ts +0 -14
  84. package/src/utils/data-utils.ts +0 -49
  85. package/src/utils/featuredManager.ts +0 -118
  86. package/src/utils/posts.ts +0 -43
  87. package/src/utils/reading-time.ts +0 -28
  88. package/src/utils/remark-slugify.js +0 -8
  89. package/src/utils/rss.ts +0 -23
  90. package/tsconfig.json +0 -8
  91. package/typograf-batch.js +0 -49
  92. package/vite.config.js +0 -11
@@ -1,124 +0,0 @@
1
- ---
2
- import { getEntry } from 'astro:content';
3
- import { getFilteredCollection } from '../../utils/content-loader';
4
- import Breadcrumbs from '../../components/Breadcrumbs.astro';
5
- import InfoCard from '../../components/InfoCard.astro';
6
- import RubricCard from '../../components/RubricCard.astro';
7
- import Subscribe from '../../components/Subscribe.astro';
8
- import TagPills from '../../components/TagPills.astro';
9
- import { maugliConfig } from '../../config/maugli.config';
10
- import { LANGUAGES } from '../../i18n/languages';
11
- import BaseLayout from '../../layouts/BaseLayout.astro';
12
- import { slugify } from '../../utils/common-utils';
13
- import { getAllTags, getPostsByTag, sortItemsByDateDesc } from '../../utils/data-utils';
14
-
15
- let posts = (await getFilteredCollection('blog')).sort(sortItemsByDateDesc);
16
- const tags = getAllTags(posts).sort((tagA, tagB) => {
17
- const postCountTagA = getPostsByTag(posts, tagA.id).length;
18
- const postCountTagB = getPostsByTag(posts, tagB.id).length;
19
- return postCountTagB - postCountTagA;
20
- });
21
-
22
- // Получаем все mdx-файлы рубрик
23
- let allRubricFiles = await getFilteredCollection('tags');
24
- const rubricSlugs = allRubricFiles.map((r) => r.data.slug);
25
- const rubricMeta = Object.fromEntries(allRubricFiles.map((r) => [r.data.slug, r]));
26
-
27
- // Формируем список рубрик только по реально используемым тегам
28
- const rubricCards = tags
29
- .filter((tag) => rubricSlugs.includes(tag.id))
30
- .map((tag) => {
31
- const rubric = rubricMeta[tag.id];
32
- const postCount = getPostsByTag(posts, tag.id).length;
33
- const lastPost = posts
34
- .filter((post) => (post.data.tags || []).some((t) => slugify(t) === tag.id))
35
- .sort((a, b) => new Date(b.data.publishDate).getTime() - new Date(a.data.publishDate).getTime())[0];
36
- const updatedAt = lastPost ? lastPost.data.publishDate?.toString() : undefined;
37
- return {
38
- slug: rubric.data.slug,
39
- title: rubric.data.title,
40
- description: rubric.data.description,
41
- image: rubric.data.image,
42
- postCount,
43
- updatedAt,
44
- quote: rubric.data.quote,
45
- isFeatured: rubric.data.isFeatured
46
- };
47
- });
48
-
49
- // Обычные теги — только те, для которых нет mdx-файла
50
- const simpleTags = tags.filter((tag) => !rubricSlugs.includes(tag.id));
51
-
52
- // Сортируем rubricCards по дате последнего поста (lastPublishDate), самые свежие — выше
53
- const rubricCardsSorted = [...rubricCards].sort((a, b) => {
54
- const dateA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
55
- const dateB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
56
- return dateB - dateA;
57
- });
58
-
59
- const dicts: Record<string, any> = Object.fromEntries(LANGUAGES.map((l) => [l.code, l.dict]));
60
- const lang: string = typeof maugliConfig.defaultLang === 'string' && dicts[maugliConfig.defaultLang] ? maugliConfig.defaultLang : 'en';
61
- const dict = dicts[lang] || dicts['en'];
62
- const pageTitle = dict.pages?.tags?.title || dicts['en'].pages.tags.title;
63
- const pageDesc = dict.pages?.tags?.description || dicts['en'].pages.tags.description;
64
- const blogRubrics = dict.pages?.tags?.blogRubrics || dicts['en'].pages.tags.blogRubrics;
65
- const moreTags = dict.pages?.tags?.moreTags || dicts['en'].pages.tags.moreTags || 'More tags';
66
-
67
- const rubricsSection = await getEntry('pages', 'rubrics');
68
- ---
69
-
70
- <BaseLayout title={pageTitle} description={pageDesc} showHeader={false} fullWidth={true}>
71
- <div class="max-w-[1280px] mx-auto">
72
- <Breadcrumbs />
73
-
74
- <h1 class="mb-12 text-3xl font-serif font-[800] sm:mb-16 sm:text-5xl" style="color: var(--text-heading)">{blogRubrics}</h1>
75
-
76
- {/* Карточки рубрик */}
77
- {
78
- rubricCardsSorted.length > 0 && (
79
- <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-16">
80
- {rubricCardsSorted.map((rubric) => (
81
- <RubricCard rubric={rubric} />
82
- ))}
83
- </div>
84
- )
85
- }
86
-
87
- {/* Обычные теги */}
88
- {
89
- simpleTags.length > 0 && (
90
- <h2 class="mb-8 text-2xl sm:text-3xl font-serif font-[700]" style="color: var(--text-heading)">
91
- {moreTags}
92
- </h2>
93
- )
94
- }
95
- {
96
- simpleTags.length > 0 && (
97
- <TagPills
98
- tags={simpleTags.map((tag) => ({
99
- id: tag.id,
100
- name: tag.name,
101
- count: getPostsByTag(posts, tag.id).length,
102
- isRubric: rubricSlugs.includes(tag.id)
103
- }))}
104
- basePath="/tags"
105
- class="mb-10"
106
- />
107
- )
108
- }
109
- </div>
110
- {maugliConfig.subscribe?.enabled && <Subscribe class="my-16 sm:my-24" />}
111
-
112
- {/* InfoCard о рубриках внизу страницы */}
113
- {
114
- rubricsSection && (
115
- <div class="max-w-[1280px] mx-auto">
116
- <InfoCard title={rubricsSection.data.title} description={rubricsSection.body} image={rubricsSection.data.image} class="mt-16" />
117
- </div>
118
- )
119
- }
120
- </BaseLayout>
121
-
122
- <!-- Пример использования TagsSection:
123
- <TagsSection tags={tagsWithRubricFlag} totalCount={posts.length} />
124
- -->
@@ -1,22 +0,0 @@
1
- // Автоматически добавляет класс для анимации InfoCard при появлении в viewport
2
- export function animateInfoCardsOnView() {
3
- if (typeof window === 'undefined') return;
4
- const cards = document.querySelectorAll('.info-card-fade-in');
5
- if (!('IntersectionObserver' in window) || !cards.length) return;
6
-
7
- const observer = new window.IntersectionObserver((entries, obs) => {
8
- entries.forEach(entry => {
9
- if (entry.isIntersecting) {
10
- entry.target.classList.add('info-card-fade-in--active');
11
- obs.unobserve(entry.target);
12
- }
13
- });
14
- }, { threshold: 0.15 });
15
-
16
- cards.forEach(card => observer.observe(card));
17
- }
18
-
19
- // Для автоинициализации на всех страницах, если нужно:
20
- if (typeof window !== 'undefined') {
21
- window.addEventListener('DOMContentLoaded', animateInfoCardsOnView);
22
- }
@@ -1,273 +0,0 @@
1
- /* Import fonts. Replace or add lines to use other Fontsource packages. */
2
- @import '@fontsource-variable/inter/wght.css' layer(base);
3
- @import '@fontsource-variable/geologica/wght.css' layer(base);
4
-
5
- @import 'tailwindcss';
6
- @plugin '@tailwindcss/typography';
7
-
8
- @custom-variant dark (&:where(.dark, .dark *));
9
-
10
- @layer base {
11
- :root {
12
- /* Brand */
13
- --brand-color-rgb: 12, 191, 17;
14
- --brand-color: rgb(var(--brand-color-rgb));
15
- --brand-color-60: rgba(var(--brand-color-rgb), 0.6);
16
- --brand-color-40: rgba(var(--brand-color-rgb), 0.4);
17
- --brand-color-20: rgba(var(--brand-color-rgb), 0.1);
18
-
19
- /* Light Theme */
20
- --text-main: #111c2d;
21
- --text-heading: #3b5174;
22
- --text-muted: #6b7280;
23
- --bg-main: #ffffff;
24
- --bg-main-40: rgba(255, 255, 255, 0.4);
25
- --bg-muted: rgba(237, 241, 247, 0.621);
26
- --border-main: rgba(17, 28, 44, 0.13);
27
- --font-sans: 'Inter Variable', sans-serif; /* Update if you change the sans-serif font */
28
- --font-serif: 'Geologica Variable', sans-serif; /* Update if you change the serif font */
29
-
30
- /* Card Styles */
31
- --card-shadow:
32
- 0px 26.6953px 12.9008px -4px #8b8da80d, 0px 22.5312px 10.1812px -4px #8b8da80d, 0px 18.6953px 7.80859px -4px #8b8da60d,
33
- 0px 15.375px 5.75px -4px #8b8da80d, 0px 12.7578px 3.97266px -4px #8b8da60d, 0px 11.0312px 2.44375px -4px #8b8da60d,
34
- 0px 10.3828px 1.13047px -4px #8b8da60d;
35
- --card-blur: blur(10px);
36
- --card-bg: var(--bg-muted);
37
-
38
- /* Rounded Styles */
39
- --rounded-circle: 50%;
40
- --rounded-custom: 8px 24px 8px 24px;
41
- --rounded-normal: 4px;
42
- }
43
-
44
- html.dark {
45
- /* Brand */
46
- --brand-color-rgb: 13, 211, 18;
47
- --brand-color: rgb(var(--brand-color-rgb));
48
- --brand-color-60: rgba(var(--brand-color-rgb), 0.6);
49
- --brand-color-40: rgba(var(--brand-color-rgb), 0.4);
50
- --brand-color-20: rgba(var(--brand-color-rgb), 0.3);
51
-
52
- /* Dark Theme */
53
- --text-main: #ffffff;
54
- --text-heading: rgba(202, 252, 254, 0.7);
55
- --text-muted: #9ca3af;
56
- --bg-main: #0b131e;
57
- --bg-main-40: rgba(11, 19, 30, 0.4);
58
- --bg-muted: #131d2cde;
59
- --border-main: rgba(51, 66, 66, 0.6);
60
-
61
- /* Card Styles */
62
- --card-shadow:
63
- 0px 26.6953px 12.9008px -4px rgba(12, 12, 12, 0.0945547), 0px 22.5312px 10.1812px -4px rgba(12, 12, 12, 0.073125),
64
- 0px 18.6953px 7.80859px -4px rgba(12, 12, 12, 0.0614453), 0px 15.375px 5.75px -4px rgba(12, 12, 12, 0.05525),
65
- 0px 12.7578px 3.97266px -4px rgba(12, 12, 12, 0.0502734), 0px 11.0312px 2.44375px -4px rgba(12, 12, 12, 0.04225),
66
- 0px 10.3828px 1.13047px -4px rgba(12, 12, 12, 0.0269141);
67
- --card-blur: blur(8px);
68
- --card-bg: var(--bg-muted);
69
- }
70
- }
71
- /* Cyan */
72
- /*
73
- :root {
74
- --text-main: #162a2b;
75
- --bg-main: #d6e0e2;
76
- --bg-muted: #ccd8db;
77
- --border-main: #162a2b;
78
- }
79
- html.dark {
80
- --text-main: #d6e0e2;
81
- --bg-main: #162a2b;
82
- --bg-muted: #1c3537;
83
- --border-main: #d6e0e2;
84
- }
85
- */
86
-
87
- /* Green */
88
- /*
89
- :root {
90
- --text-main: #3a4238;
91
- --bg-main: #f3efe6;
92
- --bg-muted: #eee9dc;
93
- --border-main: #3a4238;
94
- }
95
- html.dark {
96
- --text-main: #f3efe6;
97
- --bg-main: #5e6c5b;
98
- --bg-muted: #596756;
99
- --border-main: #f3efe6;
100
- }
101
- */
102
-
103
- @theme inline {
104
- --font-sans: 'Inter Variable', sans-serif; /* Match --font-sans above if changed */
105
- --font-serif: 'Geologica Variable', sans-serif; /* Match --font-serif above if changed */
106
- --font-weight-bold: 700;
107
- --font-weight-extra-bold: 800;
108
- --text-color-main: var(--text-main);
109
- --text-color-heading: var(--text-heading);
110
- --background-color-main: var(--bg-main);
111
- --background-color-muted: var(--bg-muted);
112
- --border-color-main: var(--border-main);
113
- --color-brand: var(--brand-color);
114
- --color-brand-60: var(--brand-color-60);
115
- --color-brand-20: var(--brand-color-20);
116
- }
117
-
118
- @layer utilities {
119
- /* Кастомные скругления для плашек */
120
- .rounded-custom {
121
- border-radius: var(--rounded-custom);
122
- }
123
-
124
- /* Обычные скругления для тегов и форм */
125
- .rounded-normal {
126
- border-radius: var(--rounded-normal);
127
- }
128
-
129
- .rounded-circle {
130
- border-radius: var(--rounded-circle);
131
- }
132
-
133
- .prose {
134
- --tw-prose-body: var(--text-color-main);
135
- --tw-prose-headings: var(--text-color-heading);
136
- --tw-prose-lead: var(--text-color-main);
137
- --tw-prose-links: var(--text-color-main);
138
- --tw-prose-bold: var(--text-color-main);
139
- --tw-prose-counters: var(--text-color-main);
140
- --tw-prose-bullets: var(--text-color-main);
141
- --tw-prose-hr: var(--border-color-main);
142
- --tw-prose-quotes: var(--text-color-main);
143
- --tw-prose-quote-borders: var(--border-color-main);
144
- --tw-prose-captions: var(--text-color-main);
145
- --tw-prose-kbd: var(--text-color-main);
146
- --tw-prose-code: var(--text-color-main);
147
- --tw-prose-th-borders: var(--border-color-main);
148
- --tw-prose-td-borders: var(--border-color-main);
149
- }
150
-
151
- .prose a {
152
- /* background: var(--brand-color-20); */ /* Фон под ссылкой */
153
- /* border-radius: 8px 24px 8px 24px; */ /* кастомные скругления */
154
- /* padding: 4px 12px; немного отступов для видимости фона */
155
- text-decoration: none; /* убираем подчеркивание */
156
- color: var(--color-brand);
157
- /* transition: background-color 0.2s ease; */ /* плавный переход */
158
- }
159
-
160
- .prose a:hover {
161
- /* background: var(--brand-color-60); */ /* более яркий фон при наведении */
162
- text-decoration: underline; /* подчеркивание при наведении */
163
- }
164
-
165
- .prose :where(h1, h2, h3, h4, h5, h6) {
166
- @apply font-serif font-[800];
167
- line-height: 1.05 !important; /* 105% для заголовков */
168
- }
169
-
170
- .prose blockquote {
171
- @apply font-serif font-[300] px-6 py-3 my-8;
172
- background: var(--bg-muted); /* серый фон как у плашки */
173
- border: 0px solid var(--border-main); /* прямые границы */
174
- color: var(--text-main); /* тёмный текст */
175
- font-size: 1.08em;
176
- box-shadow: none;
177
- margin-left: 0; /* чтобы не было сильного сдвига влево */
178
- padding: 1em;
179
- padding-left: 3em;
180
-
181
- margin-right: 0;
182
- border-radius: 8px 24px 8px 24px; /* кастомные скругления */
183
- }
184
- .prose blockquote p {
185
- @apply m-0;
186
- }
187
- .prose blockquote p:first-child::before,
188
- .prose blockquote p:last-child::after,
189
- .prose blockquote::before,
190
- .prose blockquote::after {
191
- content: none !important;
192
- }
193
-
194
- .prose p,
195
- .prose li,
196
- .prose ul,
197
- .prose ol {
198
- line-height: 1.4 !important; /* 130% для body */
199
- }
200
-
201
- /* Sticky positioning support and fixes */
202
- @supports (position: sticky) {
203
- .sticky-container {
204
- height: 100vh;
205
- overflow-y: auto;
206
- }
207
- }
208
-
209
- /* Ensure sticky positioning works in all browsers */
210
- .sticky-toc {
211
- position: -webkit-sticky !important;
212
- position: sticky !important;
213
- top: 2rem !important;
214
- align-self: flex-start !important;
215
- z-index: 10;
216
- }
217
-
218
- /* Grid layout for ToC */
219
- .toc-grid {
220
- display: grid;
221
- grid-template-columns: minmax(320px, 360px) 1fr;
222
- gap: 2.25rem;
223
- align-items: start;
224
- }
225
-
226
- @media (max-width: 1023px) {
227
- .toc-grid {
228
- display: block;
229
- }
230
- }
231
-
232
- .card-shadow {
233
- box-shadow: var(--card-shadow) !important;
234
- }
235
- .card-blur {
236
- backdrop-filter: var(--card-blur) !important;
237
- -webkit-backdrop-filter: var(--card-blur) !important;
238
- }
239
- .card-bg {
240
- background: var(--card-bg) !important;
241
- }
242
-
243
- /* Custom grid template for 16 columns */
244
- .grid-cols-16 {
245
- grid-template-columns: repeat(16, minmax(0, 1fr));
246
- }
247
- }
248
-
249
- @layer base {
250
- html {
251
- background-color: var(--bg-main); /* Фон для html элемента */
252
- /* Предотвращаем overscroll в Safari */
253
- overscroll-behavior: none;
254
- -webkit-overflow-scrolling: touch;
255
- }
256
-
257
- body {
258
- min-height: 100vh;
259
- background-color: var(--bg-main); /* Фон для body элемента */
260
- /* Предотвращаем overscroll в Safari */
261
- overscroll-behavior: none;
262
- }
263
-
264
- /* Предотвращаем переполнение контейнеров */
265
- * {
266
- box-sizing: border-box;
267
- }
268
-
269
- /* Custom grid template for 16 columns */
270
- .grid-cols-16 {
271
- grid-template-columns: repeat(16, minmax(0, 1fr));
272
- }
273
- }
Binary file
@@ -1,14 +0,0 @@
1
- import { getCollection, type CollectionEntry, type AnyEntryMap } from 'astro:content';
2
- import { maugliConfig } from '../config/maugli.config';
3
-
4
- /**
5
- * Load a collection and filter out demo/example content when showExamples is disabled.
6
- * @param name Name of the collection
7
- */
8
- export async function getFilteredCollection<C extends keyof AnyEntryMap>(name: C): Promise<CollectionEntry<C>[]> {
9
- const items = await getCollection(name);
10
- if (maugliConfig.showExamples) {
11
- return items as CollectionEntry<C>[];
12
- }
13
- return items.filter((item: any) => !item.data.isExample) as CollectionEntry<C>[];
14
- }
@@ -1,49 +0,0 @@
1
- import { type CollectionEntry } from 'astro:content';
2
- import { maugliConfig } from '../config/maugli.config';
3
- import { slugify } from './common-utils';
4
-
5
- export function sortItemsByDateDesc(itemA: CollectionEntry<'blog' | 'projects'>, itemB: CollectionEntry<'blog' | 'projects'>) {
6
- return new Date(itemB.data.publishDate).getTime() - new Date(itemA.data.publishDate).getTime();
7
- }
8
-
9
- export function sortItemsWithFeaturedFirst(itemA: CollectionEntry<'blog' | 'projects'>, itemB: CollectionEntry<'blog' | 'projects'>) {
10
- // Сначала сортируем по featured (featured посты идут первыми)
11
- if (itemA.data.isFeatured && !itemB.data.isFeatured) {
12
- return -1; // A идет первым
13
- }
14
- if (!itemA.data.isFeatured && itemB.data.isFeatured) {
15
- return 1; // B идет первым
16
- }
17
-
18
- // Если оба featured или оба не featured, сортируем по дате
19
- return new Date(itemB.data.publishDate).getTime() - new Date(itemA.data.publishDate).getTime();
20
- }
21
-
22
- export function getAllTags(posts: CollectionEntry<'blog'>[]) {
23
- const tags: string[] = [...new Set(posts.flatMap((post) => post.data.tags || []).filter(Boolean))];
24
- return tags
25
- .map((tag) => {
26
- return {
27
- name: tag,
28
- id: slugify(tag)
29
- };
30
- })
31
- .filter((obj, pos, arr) => {
32
- return arr.map((mapObj) => mapObj.id).indexOf(obj.id) === pos;
33
- });
34
- }
35
-
36
- export function getPostsByTag(posts: CollectionEntry<'blog'>[], tagId: string) {
37
- const filteredPosts: CollectionEntry<'blog'>[] = posts.filter((post) => (post.data.tags || []).map((tag) => slugify(tag)).includes(tagId));
38
- return filteredPosts;
39
- }
40
-
41
- // Получение автора поста с fallback на дефолтного автора из конфига
42
- export function getPostAuthor(post: CollectionEntry<'blog'>): string {
43
- return post.data.author || maugliConfig.defaultAuthorId || 'unknown-author';
44
- }
45
-
46
- // Получение постов по автору с учетом дефолтного автора
47
- export function getPostsByAuthor(posts: CollectionEntry<'blog'>[], authorId: string) {
48
- return posts.filter(post => getPostAuthor(post) === authorId);
49
- }
@@ -1,118 +0,0 @@
1
- import { getCollection } from 'astro:content';
2
- import fs from 'fs/promises';
3
- import path from 'path';
4
-
5
- /**
6
- * Универсальный менеджер featured-элементов для коллекций: blog, products, projects
7
- */
8
- export class FeaturedManager {
9
- private static readonly MAX_FEATURED = 3;
10
- private static readonly CONTENT_DIRS = {
11
- blog: './src/content/blog',
12
- products: './src/content/products',
13
- projects: './src/content/projects',
14
- };
15
-
16
- /**
17
- * Добавляет элемент в featured и управляет лимитом
18
- */
19
- static async addFeatured(collection: 'blog' | 'products' | 'projects', newId: string): Promise<void> {
20
- try {
21
- // Получаем все элементы коллекции
22
- const allItems = await getCollection(collection);
23
- // Находим текущие featured, сортируем по дате (старые первые)
24
- const currentFeatured = allItems
25
- .filter(item => item.data.isFeatured)
26
- .sort((a, b) => a.data.publishDate.getTime() - b.data.publishDate.getTime());
27
- // Если уже есть MAX_FEATURED, удаляем самый старый
28
- if (currentFeatured.length >= this.MAX_FEATURED) {
29
- const oldestFeatured = currentFeatured[0];
30
- await this.removeFeatured(collection, oldestFeatured.id);
31
- console.log(`🗑️ Убрали из featured самый старый: ${oldestFeatured.id}`);
32
- }
33
- // Добавляем новый элемент в featured
34
- await this.setFeaturedStatus(collection, newId, true);
35
- console.log(`⭐ Добавили в featured: ${newId}`);
36
- // Выводим текущий список featured
37
- await this.logCurrentFeatured(collection);
38
- } catch (error) {
39
- console.error('Ошибка при управлении featured:', error);
40
- }
41
- }
42
-
43
- /**
44
- * Убирает элемент из featured
45
- */
46
- static async removeFeatured(collection: 'blog' | 'products' | 'projects', id: string): Promise<void> {
47
- await this.setFeaturedStatus(collection, id, false);
48
- }
49
-
50
- /**
51
- * Изменяет статус isFeatured в frontmatter файла
52
- */
53
- private static async setFeaturedStatus(collection: 'blog' | 'products' | 'projects', id: string, isFeatured: boolean): Promise<void> {
54
- const dir = this.CONTENT_DIRS[collection];
55
- const filePath = path.join(dir, `${id}.md`);
56
- try {
57
- const content = await fs.readFile(filePath, 'utf-8');
58
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
59
- if (!frontmatterMatch) {
60
- throw new Error(`Не найден frontmatter в файле: ${filePath}`);
61
- }
62
- let [, frontmatter, body] = frontmatterMatch;
63
- if (frontmatter.includes('isFeatured:')) {
64
- frontmatter = frontmatter.replace(
65
- /isFeatured:\s*(true|false)/,
66
- `isFeatured: ${isFeatured}`
67
- );
68
- } else {
69
- if (frontmatter.includes('title:')) {
70
- frontmatter = frontmatter.replace(
71
- /(title:.*\n)/,
72
- `$1isFeatured: ${isFeatured}\n`
73
- );
74
- } else {
75
- frontmatter = `${frontmatter.trim()}\nisFeatured: ${isFeatured}`;
76
- }
77
- }
78
- const updatedContent = `---\n${frontmatter}\n---\n${body}`;
79
- await fs.writeFile(filePath, updatedContent, 'utf-8');
80
- } catch (error) {
81
- console.error(`Ошибка при обновлении файла ${filePath}:`, error);
82
- }
83
- }
84
-
85
- /**
86
- * Выводит текущий список featured элементов
87
- */
88
- static async logCurrentFeatured(collection: 'blog' | 'products' | 'projects'): Promise<void> {
89
- try {
90
- const allItems = await getCollection(collection);
91
- const featured = allItems
92
- .filter(item => item.data.isFeatured)
93
- .sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime());
94
- console.log(`\n📌 Текущие featured (${collection}):`);
95
- featured.forEach((item, index) => {
96
- console.log(`${index + 1}. ${item.id} (${item.data.publishDate.toLocaleDateString()})`);
97
- });
98
- console.log(`📊 Всего featured: ${featured.length}/${this.MAX_FEATURED}\n`);
99
- } catch (error) {
100
- console.error('Ошибка при получении featured:', error);
101
- }
102
- }
103
-
104
- /**
105
- * Получает все featured элементы для отображения
106
- */
107
- static async getFeaturedItems(collection: 'blog' | 'products' | 'projects') {
108
- try {
109
- const allItems = await getCollection(collection);
110
- return allItems
111
- .filter(item => item.data.isFeatured)
112
- .sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime());
113
- } catch (error) {
114
- console.error('Ошибка при получении featured:', error);
115
- return [];
116
- }
117
- }
118
- }
@@ -1,43 +0,0 @@
1
- import { getFilteredCollection } from './content-loader';
2
-
3
- /**
4
- * Получает featured посты для отображения на главной странице
5
- */
6
- export async function getFeaturedPosts() {
7
- try {
8
- const allPosts = await getFilteredCollection('blog');
9
- return allPosts
10
- .filter((post) => post.data.isFeatured)
11
- .sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime())
12
- .slice(0, 3); // Максимум 3 featured поста
13
- } catch (error) {
14
- console.error('Ошибка при получении featured постов:', error);
15
- return [];
16
- }
17
- }
18
-
19
- /**
20
- * Получает все посты (не featured) для отображения в блоге
21
- */
22
- export async function getAllPosts() {
23
- try {
24
- const allPosts = await getFilteredCollection('blog');
25
- return allPosts.sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime());
26
- } catch (error) {
27
- console.error('Ошибка при получении постов:', error);
28
- return [];
29
- }
30
- }
31
-
32
- /**
33
- * Получает только обычные посты (не featured)
34
- */
35
- export async function getRegularPosts() {
36
- try {
37
- const allPosts = await getFilteredCollection('blog');
38
- return allPosts.filter((post) => !post.data.isFeatured).sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime());
39
- } catch (error) {
40
- console.error('Ошибка при получении обычных постов:', error);
41
- return [];
42
- }
43
- }
@@ -1,28 +0,0 @@
1
- /**
2
- * Вычисляет время чтения статьи на основе количества слов
3
- * @param content - HTML или markdown контент
4
- * @returns время чтения в формате "X мин"
5
- */
6
- export function calculateReadingTime(content: string): string {
7
- // Убираем HTML теги и markdown разметку
8
- const plainText = content
9
- .replace(/<[^>]*>/g, '') // убираем HTML теги
10
- .replace(/#{1,6}\s+/g, '') // убираем markdown заголовки
11
- .replace(/\*{1,2}([^*]+)\*{1,2}/g, '$1') // убираем markdown жирный/курсив
12
- .replace(/`([^`]+)`/g, '$1') // убираем код в бэктиках
13
- .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // убираем markdown ссылки
14
- .replace(/!\[([^\]]*)\]\([^)]+\)/g, '') // убираем markdown изображения
15
- .trim();
16
-
17
- // Считаем слова (разделяем по пробелам и фильтруем пустые)
18
- const words = plainText.split(/\s+/).filter(word => word.length > 0);
19
- const wordCount = words.length;
20
-
21
- // Средняя скорость чтения на русском языке: 200-250 слов в минуту
22
- // Используем 220 слов в минуту как среднее значение
23
- const wordsPerMinute = 220;
24
- const minutes = Math.ceil(wordCount / wordsPerMinute);
25
-
26
- // Минимум 1 минута
27
- return `${Math.max(1, minutes)} `;
28
- }