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,221 +0,0 @@
1
- ---
2
- import { render, type CollectionEntry } from 'astro:content';
3
- import { getFilteredCollection } from '../../utils/content-loader';
4
- import ArticleMeta from '../../components/ArticleMeta.astro';
5
- import Breadcrumbs from '../../components/Breadcrumbs.astro';
6
- import ContentFooter from '../../components/ContentFooter.astro';
7
- import HeroImage from '../../components/HeroImage.astro';
8
- import PostPreview from '../../components/PostPreview.astro';
9
- import ProjectPreview from '../../components/ProjectPreview.astro';
10
- import SummaryFAQCard from '../../components/SummaryFAQCard.astro';
11
- import TableOfContents from '../../components/TableOfContents.astro';
12
- import TagsAndShare from '../../components/TagsAndShare.astro';
13
- import { maugliConfig } from '../../config/maugli.config';
14
- import { LANGUAGES } from '../../i18n/languages';
15
- import BaseLayout from '../../layouts/BaseLayout.astro';
16
- import { sortItemsByDateDesc } from '../../utils/data-utils';
17
- import { calculateReadingTime } from '../../utils/reading-time';
18
-
19
- const dicts: Record<string, any> = Object.fromEntries(LANGUAGES.map((l) => [l.code, l.dict]));
20
- const lang: string = typeof maugliConfig.defaultLang === 'string' && dicts[maugliConfig.defaultLang] ? maugliConfig.defaultLang : 'en';
21
- const dict = dicts[lang] || dicts['en'];
22
- const articlesByTag = dict.pages?.productsId?.articlesByTag || dicts['en'].pages.productsId.articlesByTag;
23
- const casesByTag = dict.pages?.productsId?.casesByTag || dicts['en'].pages.productsId.casesByTag;
24
-
25
- export async function getStaticPaths() {
26
- const products = (await getFilteredCollection('products')).sort(sortItemsByDateDesc);
27
- return products.map((product) => ({
28
- params: { id: product.id },
29
- props: { product }
30
- }));
31
- }
32
-
33
- type Props = {
34
- product: CollectionEntry<'products'>;
35
- };
36
-
37
- const { href } = Astro.url;
38
- const { product } = Astro.props;
39
- const { title, description, seo, image, tags = [], publishDate } = product.data;
40
- const { Content, headings } = await render(product);
41
-
42
- // Вычисляем время чтения на основе контента
43
- const readingTime = calculateReadingTime(product.body || '');
44
-
45
- // Преобразуем встроенные заголовки Astro в нужный формат
46
- const tocItems = headings
47
- .filter((heading) => heading.depth === 2 || heading.depth === 3)
48
- .map((heading) => ({
49
- title: heading.text,
50
- id: heading.slug
51
- }));
52
-
53
- // Получаем связанные статьи (blog) и кейсы (projects), где productID совпадает с productID продукта
54
- const relatedPosts = (await getFilteredCollection('blog')).filter((post) => post.data.productID === product.data.productID);
55
- const relatedProjects = (await getFilteredCollection('projects')).filter((project) => project.data.productID === product.data.productID);
56
-
57
- // Сортировка: сначала featured, потом по дате
58
- const sortByFeaturedAndDate = (a, b) => {
59
- if ((b.data.isFeatured ? 1 : 0) !== (a.data.isFeatured ? 1 : 0)) return (b.data.isFeatured ? 1 : 0) - (a.data.isFeatured ? 1 : 0);
60
- return (b.data.publishDate?.getTime?.() || 0) - (a.data.publishDate?.getTime?.() || 0);
61
- };
62
-
63
- const sortedPosts = relatedPosts.sort(sortByFeaturedAndDate);
64
- const sortedProjects = relatedProjects.sort(sortByFeaturedAndDate);
65
- const moreLabel = dict.buttons?.more || dicts['en'].buttons.more || 'More';
66
- ---
67
-
68
- <BaseLayout title={seo?.title ?? title} description={seo?.description ?? description} image={seo?.image} pageType="article" showHeader={false} fullWidth={true}>
69
- <div class="w-full max-w-none lg:max-w-[1280px] mx-auto lg:px-8 mt-2">
70
- <div class="px-4 md:px-0">
71
- <Breadcrumbs />
72
- </div>
73
- <article class="mb-16 sm:mb-24 max-w-3xl mx-auto lg:max-w-none">
74
- {
75
- (seo?.image?.src || image?.src) && (
76
- <HeroImage
77
- src={seo?.image?.src || image?.src}
78
- alt={seo?.image?.alt || image?.alt || title || 'Продукт без названия'}
79
- width={seo?.image?.width || image?.width || 1200}
80
- height={seo?.image?.height || image?.height || 630}
81
- caption={seo?.image?.caption || image?.caption}
82
- />
83
- )
84
- }
85
- <div class="px-4 mt-2 md:px-0">
86
- <ArticleMeta publishDate={publishDate} readingTime={readingTime} />
87
- </div>
88
- <header class="mb-8 px-4 md:px-0">
89
- <h1 class="text-3xl leading-tight font-serif font-[800] sm:text-5xl sm:leading-tight" style="color: var(--text-heading)">{title}</h1>
90
- {description && <p class="mt-4 text-lg text-muted leading-relaxed">{description}</p>}
91
- </header>
92
- </article>
93
- <div class="md:grid md:grid-cols-16 md:gap-9">
94
- <aside class="hidden sm:block md:col-span-5 lg:col-span-5 min-h-screen">
95
- <div class="sticky top-8">
96
- {tocItems.length > 0 && <TableOfContents headings={tocItems} />}
97
- <TagsAndShare tags={tags} shareUrl={href} title={title} />
98
- </div>
99
- </aside>
100
- <div class="md:col-span-11 px-4 md:px-0 h-auto lg:col-span-11">
101
- {/* Summary/FAQ для продукта */}
102
- {
103
- product.data.generativeEngineOptimization?.generated &&
104
- (product.data.generativeEngineOptimization.generated.summary ||
105
- product.data.generativeEngineOptimization.generated.highlights?.length > 0 ||
106
- product.data.generativeEngineOptimization.generated.faq?.length > 0) && (
107
- <div class="not-prose mb-8">
108
- <SummaryFAQCard
109
- summary={product.data.generativeEngineOptimization.generated.summary}
110
- highlights={product.data.generativeEngineOptimization.generated.highlights}
111
- faq={product.data.generativeEngineOptimization.generated.faq}
112
- />
113
- </div>
114
- )
115
- }
116
- <div class="prose sm:prose-lg lg:prose-xl max-w-none">
117
- <Content />
118
- </div>
119
- <ContentFooter tags={tags} shareUrl={href} title={title} basePath="/products" />
120
- {/* Связанные статьи */}
121
- {
122
- sortedPosts.length > 0 && (
123
- <div class="my-16">
124
- <h2 class="mb-6 text-xl font-serif text-heading">{articlesByTag}</h2>
125
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
126
- {sortedPosts.slice(0, 2).map((post) => (
127
- <PostPreview post={post} />
128
- ))}
129
- </div>
130
- {sortedPosts.length > 2 && (
131
- <details class="mt-4 group">
132
- <summary class="cursor-pointer text-brand font-semibold flex items-center gap-2">
133
- {moreLabel} ({sortedPosts.length - 2})
134
- <svg
135
- class="transition-transform duration-300 group-open:rotate-90"
136
- width="16"
137
- height="16"
138
- viewBox="0 0 16 16"
139
- fill="none"
140
- xmlns="http://www.w3.org/2000/svg"
141
- >
142
- <g clip-path="url(#clip0_3589_7018)">
143
- <path
144
- d="M6.16116 0.382763L12.6315 7.09276C13.1219 7.60137 13.122 8.42598 12.6315 8.93458L12.6136 8.95319C12.1231 9.4618 11.328 9.4618 10.8375 8.95319L4.36717 2.24319C3.87673 1.73459 3.87673 0.909975 4.36717 0.40137L4.38511 0.382763C4.87555 -0.125843 5.67071 -0.125842 6.16116 0.382763Z"
145
- fill="var(--brand-color)"
146
- />
147
- <path
148
- d="M12.6136 7.04876L12.6315 7.06737C13.122 7.57597 13.122 8.40059 12.6315 8.90919L6.16116 15.6192C5.67071 16.1278 4.87555 16.1278 4.38511 15.6192L4.36717 15.6006C3.87673 15.092 3.87673 14.2674 4.36717 13.7588L10.8375 7.04876C11.328 6.54016 12.1231 6.54016 12.6136 7.04876Z"
149
- fill="var(--brand-color)"
150
- />
151
- </g>
152
- <defs>
153
- <clipPath id="clip0_3589_7018">
154
- <rect width="9" height="16" fill="white" transform="matrix(-1 0 0 1 13 0)" />
155
- </clipPath>
156
- </defs>
157
- </svg>
158
- </summary>
159
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
160
- {sortedPosts.slice(2).map((post) => (
161
- <PostPreview post={post} />
162
- ))}
163
- </div>
164
- </details>
165
- )}
166
- </div>
167
- )
168
- }
169
- {/* Связанные кейсы */}
170
- {
171
- sortedProjects.length > 0 && (
172
- <div class="my-16">
173
- <h2 class="mb-6 text-xl font-serif text-heading">{casesByTag}</h2>
174
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
175
- {sortedProjects.slice(0, 2).map((project) => (
176
- <ProjectPreview project={project} />
177
- ))}
178
- </div>
179
- {sortedProjects.length > 2 && (
180
- <details class="mt-4 group">
181
- <summary class="cursor-pointer text-brand font-semibold flex items-center gap-2">
182
- {moreLabel} ({sortedProjects.length - 2})
183
- <svg
184
- class="transition-transform duration-300 group-open:rotate-90"
185
- width="16"
186
- height="16"
187
- viewBox="0 0 16 16"
188
- fill="none"
189
- xmlns="http://www.w3.org/2000/svg"
190
- >
191
- <g clip-path="url(#clip0_3589_7018)">
192
- <path
193
- d="M6.16116 0.382763L12.6315 7.09276C13.1219 7.60137 13.122 8.42598 12.6315 8.93458L12.6136 8.95319C12.1231 9.4618 11.328 9.4618 10.8375 8.95319L4.36717 2.24319C3.87673 1.73459 3.87673 0.909975 4.36717 0.40137L4.38511 0.382763C4.87555 -0.125843 5.67071 -0.125842 6.16116 0.382763Z"
194
- fill="var(--brand-color)"
195
- />
196
- <path
197
- d="M12.6136 7.04876L12.6315 7.06737C13.122 7.57597 13.122 8.40059 12.6315 8.90919L6.16116 15.6192C5.67071 16.1278 4.87555 16.1278 4.38511 15.6192L4.36717 15.6006C3.87673 15.092 3.87673 14.2674 4.36717 13.7588L10.8375 7.04876C11.328 6.54016 12.1231 6.54016 12.6136 7.04876Z"
198
- fill="var(--brand-color)"
199
- />
200
- </g>
201
- <defs>
202
- <clipPath id="clip0_3589_7018">
203
- <rect width="9" height="16" fill="white" transform="matrix(-1 0 0 1 13 0)" />
204
- </clipPath>
205
- </defs>
206
- </svg>
207
- </summary>
208
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
209
- {sortedProjects.slice(2).map((project) => (
210
- <ProjectPreview project={project} />
211
- ))}
212
- </div>
213
- </details>
214
- )}
215
- </div>
216
- )
217
- }
218
- </div>
219
- </div>
220
- </div>
221
- </BaseLayout>
@@ -1,74 +0,0 @@
1
- ---
2
- import type { GetStaticPathsOptions, Page } from 'astro';
3
- import { getEntry, type CollectionEntry } from 'astro:content';
4
- import { getFilteredCollection } from '../../utils/content-loader';
5
- import Breadcrumbs from '../../components/Breadcrumbs.astro';
6
- import InfoCard from '../../components/InfoCard.astro';
7
- import Pagination from '../../components/Pagination.astro';
8
- import ProjectPreview from '../../components/ProjectPreview.astro';
9
- import TagsSection from '../../components/TagsSection.astro';
10
- import { maugliConfig } from '../../config/maugli.config';
11
- import siteConfig from '../../data/site-config';
12
- import { LANGUAGES } from '../../i18n/languages';
13
- import BaseLayout from '../../layouts/BaseLayout.astro';
14
- import { slugify } from '../../utils/common-utils';
15
- import { sortItemsByDateDesc } from '../../utils/data-utils';
16
-
17
- export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
18
- const projects = (await getFilteredCollection('projects')).sort(sortItemsByDateDesc);
19
- return paginate(projects, { pageSize: siteConfig.projectsPerPage || 6 });
20
- }
21
-
22
- type Props = { page: Page<CollectionEntry<'projects'>> };
23
-
24
- const { page } = Astro.props;
25
- let portfolio = page.data;
26
- let allProjects = await getFilteredCollection('projects');
27
- // Получаем все уникальные теги проектов
28
- const allTags = [...new Set(allProjects.flatMap((project) => project.data.tags || []))];
29
- const tagsWithCount = allTags.map((tag) => ({
30
- id: slugify(tag),
31
- name: tag,
32
- count: allProjects.filter((project) => (project.data.tags || []).includes(tag)).length
33
- }));
34
- const projectsSection = await getEntry('pages', 'projects');
35
-
36
- const dicts: Record<string, any> = Object.fromEntries(LANGUAGES.map((l) => [l.code, l.dict]));
37
- const lang: string = typeof maugliConfig.defaultLang === 'string' && dicts[maugliConfig.defaultLang] ? maugliConfig.defaultLang : 'en';
38
- const dict = dicts[lang] || dicts['en'];
39
-
40
- // Получаем мультиязычный title для проектов (кейсов)
41
- const projectsTitle = maugliConfig.pageTitles?.projects?.trim() || dict.pages?.projects?.title || dicts['en'].pages.projects.title;
42
- // Описание для проектов (кейсов) — если появится в словаре или конфиге
43
- const projectsDescription = maugliConfig.pageTitles?.projectsDescription?.trim?.() || '';
44
- ---
45
-
46
- <BaseLayout
47
- title={projectsTitle}
48
- description={projectsDescription}
49
- image={{ src: '/dante-preview.jpg', alt: 'The preview of the site' }}
50
- showHeader={false}
51
- fullWidth={true}
52
- >
53
- <div class="max-w-[1280px] mx-auto">
54
- <Breadcrumbs />
55
-
56
- <h1 class="mb-12 text-3xl leading-tight font-serif font-[800] sm:mb-16 sm:text-5xl" style="color: var(--text-heading)">{projectsTitle}</h1>
57
-
58
- <!-- Блок тегов -->
59
- {tagsWithCount.length > 0 && <TagsSection tags={tagsWithCount} basePath="/projects" totalCount={allProjects.length} class="mb-12" />}
60
-
61
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8 mb-16">
62
- {portfolio.map((project) => <ProjectPreview project={project} />)}
63
- </div>
64
-
65
- <!-- Пагинация (показывается только если больше одной страницы) -->
66
- {page.lastPage > 1 && <Pagination page={page} class="my-16 sm:my-24" />}
67
-
68
- {
69
- projectsSection && (
70
- <InfoCard title={projectsSection.data.title} description={projectsSection.body} jsonld={projectsSection.data.jsonld} class="mt-16" />
71
- )
72
- }
73
- </div>
74
- </BaseLayout>
@@ -1,165 +0,0 @@
1
- ---
2
- import { getCollection, render, type CollectionEntry } from 'astro:content';
3
- import ArticleMeta from '../../components/ArticleMeta.astro';
4
- import Breadcrumbs from '../../components/Breadcrumbs.astro';
5
- import ContentFooter from '../../components/ContentFooter.astro';
6
- import HeroImage from '../../components/HeroImage.astro';
7
- import ProductBannerCard from '../../components/ProductBannerCard.astro';
8
- import ProjectPreview from '../../components/ProjectPreview.astro';
9
- import SummaryFAQCard from '../../components/SummaryFAQCard.astro';
10
- import TableOfContents from '../../components/TableOfContents.astro';
11
- import TagsAndShare from '../../components/TagsAndShare.astro';
12
- import { maugliConfig } from '../../config/maugli.config';
13
- import { LANGUAGES } from '../../i18n/languages';
14
- import BaseLayout from '../../layouts/BaseLayout.astro';
15
- import { sortItemsByDateDesc } from '../../utils/data-utils';
16
- import { calculateReadingTime } from '../../utils/reading-time';
17
-
18
- export async function getStaticPaths() {
19
- const projects = (await getCollection('projects')).sort(sortItemsByDateDesc);
20
- const projectCount = projects.length;
21
- return projects.map((project, index) => ({
22
- params: { id: project.id },
23
- props: {
24
- project,
25
- prevProject: index + 1 !== projectCount ? projects[index + 1] : null,
26
- nextProject: index !== 0 ? projects[index - 1] : null
27
- }
28
- }));
29
- }
30
-
31
- type Props = {
32
- project: CollectionEntry<'projects'>;
33
- prevProject: CollectionEntry<'projects'>;
34
- nextProject: CollectionEntry<'projects'>;
35
- };
36
-
37
- const { href } = Astro.url;
38
- const { project, prevProject, nextProject } = Astro.props;
39
- const { title, description, seo, image, tags = [], publishDate } = project.data;
40
- const { Content, headings } = await render(project);
41
-
42
- // Вычисляем время чтения на основе контента
43
- const readingTime = calculateReadingTime(project.body || '');
44
-
45
- // Преобразуем встроенные заголовки Astro в нужный формат
46
- const tocItems = headings
47
- .filter((heading) => heading.depth === 2 || heading.depth === 3) // Только H2 и H3
48
- .map((heading) => ({
49
- title: heading.text,
50
- id: heading.slug
51
- }));
52
-
53
- const productIds = project.data.products || [];
54
- const allProducts = await getCollection('products');
55
- const products = productIds.map((id) => allProducts.find((p) => p.id === id)).filter(Boolean);
56
-
57
- const lang: string = typeof maugliConfig.defaultLang === 'string' ? maugliConfig.defaultLang : 'en';
58
- const languageObj = LANGUAGES.find((l) => l.code === lang) || LANGUAGES.find((l) => l.code === 'en');
59
- const fallbackDict = LANGUAGES.find((l) => l.code === 'en')?.dict || {};
60
- const dict = languageObj?.dict && Object.keys(languageObj.dict).length > 0 ? languageObj.dict : fallbackDict;
61
- const pages = (dict as any).pages || (fallbackDict as any).pages || {};
62
- const moreByTag = pages.projects?.moreByTag || 'More cases';
63
- ---
64
-
65
- <BaseLayout title={seo?.title ?? title} description={seo?.description ?? description} image={seo?.image} pageType="article" showHeader={false} fullWidth={true}>
66
- <!-- Расширяем контейнер для двухколоночной структуры -->
67
- <div class="w-full max-w-none lg:max-w-[1280px] mx-auto lg:px-8 mt-2">
68
- <!-- Хлебные крошки -->
69
- <div class="px-4 md:px-0">
70
- <Breadcrumbs />
71
- </div>
72
-
73
- <!-- Метаинформация над картинкой -->
74
-
75
- <article class="mb-16 sm:mb-24 max-w-3xl mx-auto lg:max-w-none">
76
- <HeroImage
77
- src={seo?.image?.src || image?.src || '/default.webp'}
78
- alt={seo?.image?.alt || image?.alt || title || 'No name project'}
79
- width={seo?.image?.width || image?.width || 1200}
80
- height={seo?.image?.height || image?.height || 630}
81
- caption={seo?.image?.caption || image?.caption}
82
- />
83
- <div class="px-4 mt-2 md:px-0">
84
- <ArticleMeta publishDate={publishDate} readingTime={readingTime} />
85
- </div>
86
-
87
- <header class="mb-8 px-4 mt-12 md:px-0">
88
- <h1 class="text-3xl leading-tight font-serif font-[800] sm:text-5xl sm:leading-tight" style="color: var(--text-heading)">{title}</h1>
89
- {description && <p class="mt-4 text-lg text-muted leading-relaxed">{description}</p>}
90
- </header>
91
- </article>
92
-
93
- <!-- 16-колоночная сетка -->
94
- <div class="md:grid md:grid-cols-16 md:gap-9">
95
- <!-- Боковая панель с Table of Contents -->
96
- <aside class="hidden sm:block md:col-span-5 lg:col-span-5 min-h-screen">
97
- <div class="sticky top-8">
98
- {tocItems.length > 0 && <TableOfContents headings={tocItems} />}
99
- <TagsAndShare tags={tags} shareUrl={href} title={title} />
100
- {/* Карточка продукта: показываем если есть productLink в проекте */}
101
- {
102
- project.data.productLink && (
103
- <div class="mt-1">
104
- <ProductBannerCard
105
- product={{
106
- data: {
107
- productLink: project.data.productLink,
108
- image: {
109
- src: project.data.productID ? `/public/${project.data.productID}.webp` : maugliConfig.seo.defaultImage,
110
- alt: project.data.title || 'Продукт'
111
- },
112
- title: project.data.title || 'Продукт'
113
- }
114
- }}
115
- />
116
- </div>
117
- )
118
- }
119
- </div>
120
- </aside>
121
-
122
- <!-- Основной контент -->
123
- <div class="md:col-span-11 px-4 md:px-0 h-auto lg:col-span-11">
124
- <!-- Summary/FAQ в начале контента -->
125
- {
126
- project.data.generativeEngineOptimization?.generated &&
127
- (project.data.generativeEngineOptimization.generated.summary ||
128
- project.data.generativeEngineOptimization.generated.highlights?.length > 0 ||
129
- project.data.generativeEngineOptimization.generated.faq?.length > 0) && (
130
- <div class="not-prose mb-8">
131
- <SummaryFAQCard
132
- summary={project.data.generativeEngineOptimization.generated.summary}
133
- highlights={project.data.generativeEngineOptimization.generated.highlights}
134
- faq={project.data.generativeEngineOptimization.generated.faq}
135
- />
136
- </div>
137
- )
138
- }
139
-
140
- <div class="prose sm:prose-lg lg:prose-xl max-w-none">
141
- <Content />
142
- </div>
143
-
144
- <!-- Блок тегов и кнопки поделиться под контентом -->
145
- <ContentFooter tags={tags} shareUrl={href} title={title} basePath="/projects" class="hidden sm:block" />
146
-
147
- {/* Мобильный блок: поделиться и "Больше о продукте" в один ряд */}
148
- <ContentFooter tags={tags} shareUrl={href} title={title} basePath="/projects" class="sm:hidden" productLink={project.data.productLink} />
149
-
150
- <!-- Блок "Еще по теме" в контенте -->
151
- {
152
- (prevProject || nextProject) && (
153
- <div class="my-16 sm:my-24">
154
- <h2 class="mb-12 text-xl font-serif sm:mb-16 sm:text-2xl text-heading">{moreByTag}</h2>
155
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
156
- {nextProject && <ProjectPreview project={nextProject} headingLevel="h3" />}
157
- {prevProject && <ProjectPreview project={prevProject} headingLevel="h3" />}
158
- </div>
159
- </div>
160
- )
161
- }
162
- </div>
163
- </div>
164
- </div>
165
- </BaseLayout>
@@ -1,58 +0,0 @@
1
- ---
2
- import type { GetStaticPathsOptions, Page } from 'astro';
3
- import { getCollection, type CollectionEntry } from 'astro:content';
4
- import Breadcrumbs from '../../../../components/Breadcrumbs.astro';
5
- import Pagination from '../../../../components/Pagination.astro';
6
- import ProjectPreview from '../../../../components/ProjectPreview.astro';
7
- import TagsSection from '../../../../components/TagsSection.astro';
8
- import siteConfig from '../../../../data/site-config';
9
- import BaseLayout from '../../../../layouts/BaseLayout.astro';
10
- import { slugify } from '../../../../utils/common-utils';
11
- import { sortItemsByDateDesc } from '../../../../utils/data-utils';
12
-
13
- export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
14
- const allProjects = (await getCollection('projects')).sort(sortItemsByDateDesc);
15
- const allTags = [...new Set(allProjects.flatMap((project) => project.data.tags || []))];
16
-
17
- return allTags.flatMap((tag) => {
18
- const filteredProjects = allProjects.filter((project) => (project.data.tags || []).includes(tag));
19
- return paginate(filteredProjects, {
20
- params: { id: slugify(tag) },
21
- pageSize: siteConfig.projectsPerPage || 6,
22
- props: { tag }
23
- });
24
- });
25
- }
26
-
27
- type Props = { page: Page<CollectionEntry<'projects'>>; tag: string };
28
-
29
- const { page, tag } = Astro.props;
30
- const projects = page.data;
31
-
32
- // Получаем все теги для отображения в TagsSection
33
- const allProjects = await getCollection('projects');
34
- const allTags = [...new Set(allProjects.flatMap((project) => project.data.tags || []))];
35
- const tagsWithCount = allTags.map((tagName) => ({
36
- id: slugify(tagName),
37
- name: tagName,
38
- count: allProjects.filter((project) => (project.data.tags || []).includes(tagName)).length
39
- }));
40
- ---
41
-
42
- <BaseLayout title={`Сервисы по тегу "${tag}"`} description={`Explore projects tagged with "${tag}"`} showHeader={false} fullWidth={true}>
43
- <div class="max-w-[1280px] mx-auto">
44
- <Breadcrumbs />
45
-
46
- <h1 class="mb-12 text-3xl leading-tight font-serif font-[800] sm:mb-16 sm:text-5xl" style="color: var(--text-heading)">Сервисы</h1>
47
-
48
- <!-- Блок тегов -->
49
- <TagsSection tags={tagsWithCount} activeTagId={slugify(tag)} basePath="/projects" totalCount={allProjects.length} class="mb-12" />
50
-
51
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8 mb-16">
52
- {projects.map((project) => <ProjectPreview project={project} />)}
53
- </div>
54
-
55
- <!-- Пагинация (показывается только если больше одной страницы) -->
56
- {page.lastPage > 1 && <Pagination page={page} class="my-16 sm:my-24" />}
57
- </div>
58
- </BaseLayout>
@@ -1,5 +0,0 @@
1
- import { generateBlogRss } from '../utils/rss';
2
-
3
- export async function GET(context) {
4
- return generateBlogRss(context);
5
- }
@@ -1,110 +0,0 @@
1
- ---
2
- import type { GetStaticPathsOptions, Page } from 'astro';
3
- import { getEntry, type CollectionEntry } from 'astro:content';
4
- import { getFilteredCollection } from '../../../utils/content-loader';
5
- import Breadcrumbs from '../../../components/Breadcrumbs.astro';
6
- import Pagination from '../../../components/Pagination.astro';
7
- import PostPreview from '../../../components/PostPreview.astro';
8
- import Subscribe from '../../../components/Subscribe.astro';
9
- import TagsSection from '../../../components/TagsSection.astro';
10
- import { maugliConfig } from '../../../config/maugli.config';
11
- import siteConfig from '../../../data/site-config';
12
- import { LANGUAGES } from '../../../i18n/languages';
13
- import BaseLayout from '../../../layouts/BaseLayout.astro';
14
- import { getAllTags, getPostsByTag, sortItemsWithFeaturedFirst } from '../../../utils/data-utils';
15
-
16
- export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
17
- const posts = (await getFilteredCollection('blog')).sort(sortItemsWithFeaturedFirst);
18
- const tags = getAllTags(posts);
19
-
20
- return tags.flatMap((tag) => {
21
- const filteredPosts = getPostsByTag(posts, tag.id);
22
- return paginate(filteredPosts, {
23
- params: { id: tag.id },
24
- pageSize: siteConfig.postsPerPage || 4
25
- });
26
- });
27
- }
28
-
29
- type Props = { page: Page<CollectionEntry<'blog'>> };
30
-
31
- const { page } = Astro.props;
32
- const blog = page.data;
33
- const params = Astro.params;
34
- const allPosts = await getFilteredCollection('blog');
35
- const allTags = getAllTags(allPosts);
36
- const allRubrics = await getFilteredCollection('tags');
37
- const currentTag = allTags.find((tag) => tag.id === params.id);
38
-
39
- // Получаем все теги с количеством постов для TagsSection
40
- let tagsWithCount = allTags.map((tag) => {
41
- // Проверяем, есть ли карточка рубрики для этого тега
42
- const hasRubricCard = allRubrics.some((rubric) => rubric.data.slug === tag.id);
43
- return {
44
- id: tag.id,
45
- name: tag.name,
46
- count: getPostsByTag(allPosts, tag.id).length,
47
- isRubric: hasRubricCard
48
- };
49
- });
50
-
51
- tagsWithCount = [...tagsWithCount.filter((tag) => tag.isRubric), ...tagsWithCount.filter((tag) => !tag.isRubric)];
52
-
53
- // Получаем рубрику из коллекции tags (если есть)
54
- const rubric = allRubrics.find((tag) => tag.data.slug === params.id && tag.data.isRubric);
55
- let rubricCard = null;
56
- if (rubric) {
57
- const postCount = getPostsByTag(allPosts, rubric.data.slug).length;
58
- const lastPost = allPosts
59
- .filter((post) => (post.data.tags || []).includes(rubric.data.slug))
60
- .sort((a, b) => new Date(b.data.updatedDate || b.data.publishDate).getTime() - new Date(a.data.updatedDate || a.data.publishDate).getTime())[0];
61
- const updatedAt = lastPost ? lastPost.data.updatedDate || lastPost.data.publishDate : undefined;
62
- rubricCard = {
63
- slug: rubric.data.slug,
64
- title: rubric.data.title,
65
- description: rubric.data.description,
66
- image: rubric.data.image,
67
- postCount,
68
- updatedAt,
69
- quote: rubric.data.quote,
70
- isFeatured: rubric.data.isFeatured
71
- };
72
- }
73
- const rubricsSection = await getEntry('pages', 'rubrics');
74
-
75
- const dicts: Record<string, any> = Object.fromEntries(LANGUAGES.map((l) => [l.code, l.dict]));
76
- const lang: string = typeof maugliConfig.defaultLang === 'string' && dicts[maugliConfig.defaultLang] ? maugliConfig.defaultLang : 'en';
77
- const dict = dicts[lang] || dicts['en'];
78
-
79
- function interpolate(str, vars) {
80
- return str.replace(/\{(\w+)\}/g, (_, k) => vars[k] ?? '');
81
- }
82
-
83
- const tagName = rubricCard ? rubricCard.title : currentTag?.name;
84
- const titleTemplate = dict.pages?.tags?.titleWithTag || dicts['en'].pages.tags.titleWithTag || 'Posts tagged {tag}';
85
- const descTemplate =
86
- dict.pages?.tags?.descriptionWithTag || dicts['en'].pages.tags.descriptionWithTag || 'Explore a curated collection of blog posts under {tag}';
87
- const pageTitle = interpolate(titleTemplate, { tag: tagName });
88
- const pageDesc = interpolate(descTemplate, { tag: tagName });
89
- ---
90
-
91
- <BaseLayout title={pageTitle} description={pageDesc} image={{ src: '/dante-preview.jpg', alt: 'The preview of the site' }} showHeader={false} fullWidth={true}>
92
- <div class="max-w-[1280px] mx-auto">
93
- <Breadcrumbs />
94
-
95
- <div class="mb-12 sm:mb-16">
96
- <h1 class="mb-4 text-3xl leading-tight font-serif font-[800] sm:text-5xl" style="color: var(--text-heading)">
97
- {currentTag?.name}
98
- </h1>
99
- </div>
100
- <TagsSection tags={tagsWithCount} totalCount={allPosts.length} activeTagId={params.id} />
101
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8 mb-16">
102
- {blog.map((post) => <PostPreview post={post} />)}
103
- </div>
104
-
105
- <!-- Пагинация (показывается только если больше одной страницы) -->
106
- {page.lastPage > 1 && <Pagination page={page} class="my-16 sm:my-24" />}
107
- {/* InfoCard о рубриках перенесён на страницу всех тегов */}
108
- </div>
109
- <Subscribe class="my-16 sm:my-24" />
110
- </BaseLayout>