2025-12-11 01:12:45 +03:00
|
|
|
|
import { fetchGraphQL } from './graphql-client.js';
|
|
|
|
|
|
import type { ProfileArticle } from '../types/graphql.js';
|
|
|
|
|
|
|
2025-12-14 23:03:30 +03:00
|
|
|
|
//кэширование
|
|
|
|
|
|
import { cache } from '@lib/cache/manager.js';
|
|
|
|
|
|
import { CACHE_TTL } from '@lib/cache/cache-ttl';
|
|
|
|
|
|
|
2025-12-13 23:29:25 +03:00
|
|
|
|
export interface AnewsPost {
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
uri: string;
|
|
|
|
|
|
date: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-28 12:02:55 +03:00
|
|
|
|
export async function getLatestPosts(first = 14, after = null) {
|
2025-12-14 23:03:30 +03:00
|
|
|
|
// Создаем уникальный ключ для кэша
|
|
|
|
|
|
const cacheKey = `latest-posts:${first}:${after || 'first-page'}`;
|
|
|
|
|
|
|
|
|
|
|
|
return await cache.wrap(
|
|
|
|
|
|
cacheKey,
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const query = `
|
|
|
|
|
|
query GetLatestProfileArticles($first: Int!, $after: String) {
|
2026-01-28 12:02:55 +03:00
|
|
|
|
profileArticles(
|
|
|
|
|
|
first: $first
|
|
|
|
|
|
after: $after
|
|
|
|
|
|
where: { orderby: { field: DATE, order: DESC } }
|
|
|
|
|
|
) {
|
|
|
|
|
|
pageInfo {
|
|
|
|
|
|
hasNextPage
|
|
|
|
|
|
endCursor
|
|
|
|
|
|
}
|
|
|
|
|
|
edges {
|
|
|
|
|
|
cursor
|
|
|
|
|
|
node {
|
|
|
|
|
|
id
|
|
|
|
|
|
databaseId
|
|
|
|
|
|
title
|
|
|
|
|
|
uri
|
|
|
|
|
|
date
|
|
|
|
|
|
featuredImage {
|
|
|
|
|
|
node {
|
|
|
|
|
|
sourceUrl(size: LARGE)
|
|
|
|
|
|
altText
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
author {
|
|
|
|
|
|
node {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
firstName
|
|
|
|
|
|
lastName
|
|
|
|
|
|
avatar {
|
|
|
|
|
|
url
|
2025-12-11 01:12:45 +03:00
|
|
|
|
}
|
2026-01-28 12:02:55 +03:00
|
|
|
|
uri
|
2025-12-11 01:12:45 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-28 12:02:55 +03:00
|
|
|
|
# Соавторы как массив
|
|
|
|
|
|
coauthors {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
firstName
|
|
|
|
|
|
lastName
|
|
|
|
|
|
url
|
|
|
|
|
|
description
|
|
|
|
|
|
}
|
|
|
|
|
|
categories {
|
|
|
|
|
|
nodes {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
color
|
|
|
|
|
|
slug
|
|
|
|
|
|
uri
|
|
|
|
|
|
databaseId
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
tags {
|
|
|
|
|
|
nodes {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
slug
|
|
|
|
|
|
uri
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-14 23:03:30 +03:00
|
|
|
|
`;
|
2025-12-11 01:12:45 +03:00
|
|
|
|
|
2025-12-14 23:03:30 +03:00
|
|
|
|
const data = await fetchGraphQL(query, { first, after });
|
|
|
|
|
|
|
|
|
|
|
|
// Преобразуем edges в nodes
|
|
|
|
|
|
const posts = data.profileArticles?.edges?.map(edge => edge.node) || [];
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
posts,
|
|
|
|
|
|
pageInfo: data.profileArticles?.pageInfo || { hasNextPage: false, endCursor: null }
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
{ ttl: CACHE_TTL.POSTS } // из конфигурации
|
|
|
|
|
|
);
|
2025-12-11 01:12:45 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-26 02:02:52 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function getPostsByCategory(slug, first = 14, after = null) {
|
|
|
|
|
|
// Создаем уникальный ключ для кэша
|
|
|
|
|
|
const cacheKey = `category-posts:${slug}:${first}:${after || 'first-page'}`;
|
|
|
|
|
|
|
|
|
|
|
|
return await cache.wrap(
|
|
|
|
|
|
cacheKey,
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const query = `
|
2026-03-02 00:39:07 +03:00
|
|
|
|
query GetPostsByCategory($first: Int!, $after: String, $slug: ID!) {
|
|
|
|
|
|
category(id: $slug, idType: SLUG) {
|
|
|
|
|
|
name
|
|
|
|
|
|
contentNodes(
|
|
|
|
|
|
first: $first
|
|
|
|
|
|
after: $after
|
|
|
|
|
|
where: {
|
|
|
|
|
|
contentTypes: [PROFILE_ARTICLE, ANEW]
|
|
|
|
|
|
orderby: { field: DATE, order: DESC }
|
|
|
|
|
|
}
|
|
|
|
|
|
) {
|
|
|
|
|
|
pageInfo {
|
|
|
|
|
|
hasNextPage
|
|
|
|
|
|
endCursor
|
|
|
|
|
|
}
|
|
|
|
|
|
edges {
|
|
|
|
|
|
cursor
|
|
|
|
|
|
node {
|
|
|
|
|
|
__typename
|
|
|
|
|
|
id
|
|
|
|
|
|
databaseId
|
|
|
|
|
|
uri
|
|
|
|
|
|
date
|
|
|
|
|
|
|
|
|
|
|
|
... on NodeWithTitle {
|
|
|
|
|
|
title
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
... on NodeWithFeaturedImage {
|
|
|
|
|
|
featuredImage {
|
|
|
|
|
|
node {
|
|
|
|
|
|
sourceUrl(size: LARGE)
|
|
|
|
|
|
altText
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Для ваших кастомных типов
|
|
|
|
|
|
... on ProfileArticle {
|
|
|
|
|
|
categories {
|
|
|
|
|
|
nodes {
|
|
|
|
|
|
name
|
|
|
|
|
|
slug
|
|
|
|
|
|
color
|
|
|
|
|
|
}
|
2026-02-26 02:02:52 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-02 00:39:07 +03:00
|
|
|
|
|
|
|
|
|
|
... on ANew {
|
|
|
|
|
|
categories {
|
|
|
|
|
|
nodes {
|
|
|
|
|
|
name
|
|
|
|
|
|
slug
|
|
|
|
|
|
color
|
2026-02-26 02:02:52 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-02 00:39:07 +03:00
|
|
|
|
|
|
|
|
|
|
... on NodeWithAuthor {
|
|
|
|
|
|
author {
|
|
|
|
|
|
node {
|
|
|
|
|
|
name
|
|
|
|
|
|
avatar {
|
|
|
|
|
|
url
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Coauthors для ProfileArticle (без description)
|
|
|
|
|
|
... on ProfileArticle {
|
|
|
|
|
|
coauthors {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
firstName
|
|
|
|
|
|
lastName
|
|
|
|
|
|
url
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Coauthors для ANew (без description)
|
|
|
|
|
|
... on ANew {
|
|
|
|
|
|
coauthors {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
firstName
|
|
|
|
|
|
lastName
|
|
|
|
|
|
url
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-26 02:02:52 +03:00
|
|
|
|
}
|
2026-03-02 00:39:07 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const data = await fetchGraphQL(query, { first, after, slug });
|
|
|
|
|
|
|
|
|
|
|
|
// Обрабатываем посты
|
|
|
|
|
|
const posts = data.category?.contentNodes?.edges?.map(edge => {
|
|
|
|
|
|
const node = edge.node;
|
|
|
|
|
|
|
|
|
|
|
|
// Приводим coauthors к единому формату (если нужно)
|
|
|
|
|
|
if (node.coauthors) {
|
|
|
|
|
|
node.coauthors = node.coauthors.map(author => ({
|
|
|
|
|
|
id: author.id,
|
|
|
|
|
|
name: author.name || '',
|
|
|
|
|
|
firstName: author.firstName || '',
|
|
|
|
|
|
lastName: author.lastName || '',
|
|
|
|
|
|
url: author.url || ''
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return node;
|
|
|
|
|
|
}) || [];
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
posts,
|
|
|
|
|
|
pageInfo: data.category?.contentNodes?.pageInfo || {
|
|
|
|
|
|
hasNextPage: false,
|
|
|
|
|
|
endCursor: null
|
|
|
|
|
|
},
|
|
|
|
|
|
categoryName: data.category?.name || ''
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
{ ttl: CACHE_TTL.POSTS }
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function getPostsByTag(slug, first = 14, after = null) {
|
|
|
|
|
|
// Создаем уникальный ключ для кэша
|
|
|
|
|
|
const cacheKey = `tag-posts:${slug}:${first}:${after || 'first-page'}`;
|
|
|
|
|
|
|
|
|
|
|
|
return await cache.wrap(
|
|
|
|
|
|
cacheKey,
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const query = `
|
|
|
|
|
|
query GetPostsByTag($first: Int!, $after: String, $slug: ID!) {
|
|
|
|
|
|
tag(id: $slug, idType: NAME) {
|
|
|
|
|
|
id
|
|
|
|
|
|
databaseId
|
|
|
|
|
|
name
|
|
|
|
|
|
slug
|
|
|
|
|
|
count
|
|
|
|
|
|
description
|
|
|
|
|
|
|
|
|
|
|
|
contentNodes(
|
|
|
|
|
|
first: $first
|
|
|
|
|
|
after: $after
|
|
|
|
|
|
where: {
|
|
|
|
|
|
contentTypes: [PROFILE_ARTICLE, ANEW]
|
|
|
|
|
|
orderby: { field: DATE, order: DESC }
|
|
|
|
|
|
}
|
|
|
|
|
|
) {
|
|
|
|
|
|
pageInfo {
|
|
|
|
|
|
hasNextPage
|
|
|
|
|
|
endCursor
|
|
|
|
|
|
}
|
|
|
|
|
|
edges {
|
|
|
|
|
|
cursor
|
|
|
|
|
|
node {
|
|
|
|
|
|
__typename
|
|
|
|
|
|
id
|
|
|
|
|
|
databaseId
|
|
|
|
|
|
uri
|
|
|
|
|
|
date
|
|
|
|
|
|
|
|
|
|
|
|
... on NodeWithTitle {
|
|
|
|
|
|
title
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
... on NodeWithFeaturedImage {
|
|
|
|
|
|
featuredImage {
|
|
|
|
|
|
node {
|
|
|
|
|
|
sourceUrl(size: LARGE)
|
|
|
|
|
|
altText
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
... on NodeWithAuthor {
|
|
|
|
|
|
author {
|
|
|
|
|
|
node {
|
|
|
|
|
|
name
|
|
|
|
|
|
avatar {
|
|
|
|
|
|
url
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
... on ProfileArticle {
|
|
|
|
|
|
categories {
|
|
|
|
|
|
nodes {
|
|
|
|
|
|
name
|
|
|
|
|
|
slug
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
... on ANew {
|
|
|
|
|
|
categories {
|
|
|
|
|
|
nodes {
|
|
|
|
|
|
name
|
|
|
|
|
|
slug
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
... on ProfileArticle {
|
|
|
|
|
|
coauthors {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
... on ANew {
|
|
|
|
|
|
coauthors {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
}
|
2026-02-26 02:02:52 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
2026-03-02 00:39:07 +03:00
|
|
|
|
console.log('Fetching with:', { first, after, slug }); // Добавим лог параметров
|
|
|
|
|
|
|
2026-02-26 02:02:52 +03:00
|
|
|
|
const data = await fetchGraphQL(query, { first, after, slug });
|
|
|
|
|
|
|
2026-03-02 00:39:07 +03:00
|
|
|
|
|
|
|
|
|
|
// Проверяем структуру data
|
|
|
|
|
|
if (!data?.tag) {
|
|
|
|
|
|
console.log('Tag not found');
|
|
|
|
|
|
return {
|
|
|
|
|
|
posts: [],
|
|
|
|
|
|
pageInfo: { hasNextPage: false, endCursor: null },
|
|
|
|
|
|
tagName: slug,
|
|
|
|
|
|
tagSlug: slug,
|
|
|
|
|
|
tagCount: 0,
|
|
|
|
|
|
tagDescription: ''
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Обрабатываем посты
|
|
|
|
|
|
const posts = data.tag?.contentNodes?.edges?.map(edge => {
|
|
|
|
|
|
const node = edge.node;
|
|
|
|
|
|
|
|
|
|
|
|
// Приводим данные к единому формату
|
|
|
|
|
|
return {
|
|
|
|
|
|
...node,
|
|
|
|
|
|
// Убеждаемся, что coauthors всегда массив
|
|
|
|
|
|
coauthors: node.coauthors || [],
|
|
|
|
|
|
// Убеждаемся, что categories всегда есть
|
|
|
|
|
|
categories: node.categories || { nodes: [] }
|
|
|
|
|
|
};
|
|
|
|
|
|
}) || [];
|
2026-02-26 02:02:52 +03:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
posts,
|
2026-03-02 00:39:07 +03:00
|
|
|
|
pageInfo: data.tag?.contentNodes?.pageInfo || {
|
|
|
|
|
|
hasNextPage: false,
|
|
|
|
|
|
endCursor: null
|
|
|
|
|
|
},
|
|
|
|
|
|
tagName: data.tag?.name || slug,
|
|
|
|
|
|
tagSlug: data.tag?.slug || slug,
|
|
|
|
|
|
tagCount: data.tag?.count || 0,
|
|
|
|
|
|
tagDescription: data.tag?.description || ''
|
2026-02-26 02:02:52 +03:00
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
{ ttl: CACHE_TTL.POSTS }
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-02 00:39:07 +03:00
|
|
|
|
|
2026-03-02 23:10:31 +03:00
|
|
|
|
|
|
|
|
|
|
|
2025-12-14 23:03:30 +03:00
|
|
|
|
//последние новости (кэшированная версия)
|
2025-12-11 01:12:45 +03:00
|
|
|
|
export async function getLatestAnews(count = 12): Promise<AnewsPost[]> {
|
2025-12-14 23:03:30 +03:00
|
|
|
|
const cacheKey = `latest-anews:${count}`;
|
|
|
|
|
|
|
|
|
|
|
|
return await cache.wrap(
|
|
|
|
|
|
cacheKey,
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const query = `
|
|
|
|
|
|
query GetAnews($count: Int!) {
|
|
|
|
|
|
aNews(first: $count, where: {orderby: {field: DATE, order: DESC}}) {
|
|
|
|
|
|
nodes {
|
|
|
|
|
|
title
|
|
|
|
|
|
uri
|
|
|
|
|
|
date
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-11 01:12:45 +03:00
|
|
|
|
}
|
2025-12-14 23:03:30 +03:00
|
|
|
|
`;
|
2025-12-11 01:12:45 +03:00
|
|
|
|
|
2025-12-14 23:03:30 +03:00
|
|
|
|
const data = await fetchGraphQL(query, { count });
|
|
|
|
|
|
return data.aNews?.nodes || [];
|
|
|
|
|
|
},
|
|
|
|
|
|
{ ttl: CACHE_TTL.POSTS }
|
|
|
|
|
|
);
|
2025-12-13 23:29:25 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Получить ProfileArticle по databaseId
|
|
|
|
|
|
export async function getProfileArticleById(databaseId) {
|
|
|
|
|
|
const query = `
|
|
|
|
|
|
query GetProfileArticleById($id: ID!) {
|
|
|
|
|
|
profileArticle(id: $id, idType: DATABASE_ID) {
|
|
|
|
|
|
id
|
|
|
|
|
|
databaseId
|
|
|
|
|
|
title
|
2026-02-18 01:02:01 +03:00
|
|
|
|
secondaryTitle
|
2025-12-13 23:29:25 +03:00
|
|
|
|
content
|
|
|
|
|
|
excerpt
|
|
|
|
|
|
uri
|
|
|
|
|
|
slug
|
|
|
|
|
|
date
|
|
|
|
|
|
modified
|
|
|
|
|
|
status
|
|
|
|
|
|
featuredImage {
|
|
|
|
|
|
node {
|
|
|
|
|
|
id
|
|
|
|
|
|
sourceUrl
|
|
|
|
|
|
altText
|
|
|
|
|
|
caption
|
2026-02-18 01:02:01 +03:00
|
|
|
|
description
|
2025-12-13 23:29:25 +03:00
|
|
|
|
mediaDetails {
|
|
|
|
|
|
width
|
|
|
|
|
|
height
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
author {
|
|
|
|
|
|
node {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
firstName
|
|
|
|
|
|
lastName
|
|
|
|
|
|
avatar {
|
|
|
|
|
|
url
|
|
|
|
|
|
}
|
|
|
|
|
|
description
|
|
|
|
|
|
uri
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-18 01:02:01 +03:00
|
|
|
|
# Соавторы как массив
|
|
|
|
|
|
coauthors {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
firstName
|
|
|
|
|
|
lastName
|
|
|
|
|
|
url
|
|
|
|
|
|
description
|
|
|
|
|
|
}
|
2025-12-13 23:29:25 +03:00
|
|
|
|
categories {
|
|
|
|
|
|
nodes {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
slug
|
|
|
|
|
|
uri
|
|
|
|
|
|
description
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
tags {
|
|
|
|
|
|
nodes {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
slug
|
|
|
|
|
|
uri
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const data = await fetchGraphQL(query, { id: databaseId });
|
|
|
|
|
|
return data?.profileArticle || null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-22 00:00:07 +03:00
|
|
|
|
/**
|
|
|
|
|
|
* Получить Anews пост по databaseId
|
|
|
|
|
|
*/
|
2026-02-18 23:33:35 +03:00
|
|
|
|
export async function getAnewById(databaseId: number): Promise<SingleAnewsPost | null> {
|
2026-01-22 00:00:07 +03:00
|
|
|
|
const cacheKey = `anews:${databaseId}`;
|
|
|
|
|
|
|
|
|
|
|
|
return await cache.wrap(
|
|
|
|
|
|
cacheKey,
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const query = `
|
2026-02-18 23:33:35 +03:00
|
|
|
|
query GetAnewById($id: ID!) {
|
|
|
|
|
|
aNew(id: $id, idType: DATABASE_ID) {
|
2026-01-22 00:00:07 +03:00
|
|
|
|
id
|
|
|
|
|
|
databaseId
|
|
|
|
|
|
title
|
|
|
|
|
|
content
|
|
|
|
|
|
excerpt
|
|
|
|
|
|
uri
|
|
|
|
|
|
slug
|
|
|
|
|
|
date
|
|
|
|
|
|
modified
|
|
|
|
|
|
status
|
|
|
|
|
|
featuredImage {
|
|
|
|
|
|
node {
|
|
|
|
|
|
id
|
|
|
|
|
|
sourceUrl
|
|
|
|
|
|
altText
|
|
|
|
|
|
caption
|
|
|
|
|
|
mediaDetails {
|
|
|
|
|
|
width
|
|
|
|
|
|
height
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
author {
|
|
|
|
|
|
node {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
firstName
|
|
|
|
|
|
lastName
|
|
|
|
|
|
avatar {
|
|
|
|
|
|
url
|
|
|
|
|
|
}
|
|
|
|
|
|
description
|
|
|
|
|
|
uri
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
categories {
|
|
|
|
|
|
nodes {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
slug
|
|
|
|
|
|
uri
|
|
|
|
|
|
description
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
tags {
|
|
|
|
|
|
nodes {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
slug
|
|
|
|
|
|
uri
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await fetchGraphQL(query, { id: databaseId });
|
2026-02-18 23:33:35 +03:00
|
|
|
|
return data?.aNew || null;
|
2026-01-22 00:00:07 +03:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`Error fetching Anews post with ID ${databaseId}:`, error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ ttl: CACHE_TTL.POST_DETAIL } // Можно использовать отдельный TTL для деталей постов
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-13 23:29:25 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Получить тег по slug
|
|
|
|
|
|
*/
|
|
|
|
|
|
export async function getTagBySlug(slug) {
|
|
|
|
|
|
const query = `
|
|
|
|
|
|
query GetTagBySlug($slug: ID!) {
|
|
|
|
|
|
tag(id: $slug, idType: SLUG) {
|
|
|
|
|
|
id
|
|
|
|
|
|
databaseId
|
|
|
|
|
|
name
|
|
|
|
|
|
slug
|
|
|
|
|
|
description
|
|
|
|
|
|
count
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await fetchGraphQL(query, { slug });
|
|
|
|
|
|
return data?.tag || null;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error fetching tag:', error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-14 23:03:30 +03:00
|
|
|
|
* Получить тег с постами по slug с пагинацией
|
2025-12-13 23:29:25 +03:00
|
|
|
|
*/
|
2025-12-14 23:03:30 +03:00
|
|
|
|
export async function getTagWithPostsPaginated(
|
|
|
|
|
|
tagSlug: string,
|
|
|
|
|
|
perPage: number = 12,
|
|
|
|
|
|
page: number = 1
|
|
|
|
|
|
) {
|
2025-12-13 23:29:25 +03:00
|
|
|
|
const offset = (page - 1) * perPage;
|
|
|
|
|
|
|
|
|
|
|
|
const query = `
|
2025-12-14 23:03:30 +03:00
|
|
|
|
query GetTagWithPostsPaginated($slug: ID!, $first: Int!, $offset: Int!) {
|
2025-12-13 23:29:25 +03:00
|
|
|
|
tag(id: $slug, idType: SLUG) {
|
|
|
|
|
|
id
|
|
|
|
|
|
databaseId
|
|
|
|
|
|
name
|
|
|
|
|
|
slug
|
|
|
|
|
|
description
|
|
|
|
|
|
count
|
|
|
|
|
|
seo {
|
|
|
|
|
|
title
|
|
|
|
|
|
metaDesc
|
|
|
|
|
|
canonical
|
2025-12-14 23:03:30 +03:00
|
|
|
|
opengraphTitle
|
|
|
|
|
|
opengraphDescription
|
|
|
|
|
|
opengraphImage {
|
|
|
|
|
|
sourceUrl
|
|
|
|
|
|
}
|
2025-12-13 23:29:25 +03:00
|
|
|
|
}
|
|
|
|
|
|
posts(
|
|
|
|
|
|
first: $first
|
|
|
|
|
|
offset: $offset
|
|
|
|
|
|
where: {
|
2025-12-14 23:03:30 +03:00
|
|
|
|
orderby: {
|
|
|
|
|
|
field: DATE,
|
|
|
|
|
|
order: DESC
|
|
|
|
|
|
}
|
2025-12-13 23:29:25 +03:00
|
|
|
|
}
|
|
|
|
|
|
) {
|
|
|
|
|
|
nodes {
|
|
|
|
|
|
id
|
|
|
|
|
|
databaseId
|
|
|
|
|
|
title
|
|
|
|
|
|
excerpt
|
|
|
|
|
|
uri
|
|
|
|
|
|
slug
|
|
|
|
|
|
date
|
|
|
|
|
|
modified
|
|
|
|
|
|
featuredImage {
|
|
|
|
|
|
node {
|
|
|
|
|
|
sourceUrl(size: MEDIUM_LARGE)
|
|
|
|
|
|
altText
|
|
|
|
|
|
caption
|
|
|
|
|
|
mediaDetails {
|
|
|
|
|
|
width
|
|
|
|
|
|
height
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
author {
|
|
|
|
|
|
node {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
firstName
|
|
|
|
|
|
lastName
|
|
|
|
|
|
avatar {
|
|
|
|
|
|
url
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
categories {
|
|
|
|
|
|
nodes {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
slug
|
|
|
|
|
|
uri
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
tags {
|
|
|
|
|
|
nodes {
|
|
|
|
|
|
id
|
|
|
|
|
|
name
|
|
|
|
|
|
slug
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
seo {
|
|
|
|
|
|
title
|
|
|
|
|
|
metaDesc
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
pageInfo {
|
|
|
|
|
|
offsetPagination {
|
|
|
|
|
|
total
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await fetchGraphQL(query, {
|
|
|
|
|
|
slug: tagSlug,
|
|
|
|
|
|
first: perPage,
|
|
|
|
|
|
offset
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const tag = data?.tag;
|
|
|
|
|
|
|
|
|
|
|
|
if (!tag) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
tag: null,
|
|
|
|
|
|
posts: [],
|
|
|
|
|
|
total: 0,
|
|
|
|
|
|
totalPages: 0,
|
|
|
|
|
|
currentPage: page,
|
|
|
|
|
|
hasNext: false,
|
|
|
|
|
|
hasPrevious: false
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const posts = tag.posts?.nodes || [];
|
|
|
|
|
|
const total = tag.posts?.pageInfo?.offsetPagination?.total || 0;
|
|
|
|
|
|
const totalPages = Math.ceil(total / perPage);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
tag,
|
|
|
|
|
|
posts,
|
|
|
|
|
|
total,
|
|
|
|
|
|
totalPages,
|
|
|
|
|
|
currentPage: page,
|
|
|
|
|
|
hasNext: page < totalPages,
|
|
|
|
|
|
hasPrevious: page > 1,
|
|
|
|
|
|
perPage
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-12-14 23:03:30 +03:00
|
|
|
|
console.error('Error fetching tag with posts:', error);
|
2025-12-13 23:29:25 +03:00
|
|
|
|
return {
|
|
|
|
|
|
tag: null,
|
|
|
|
|
|
posts: [],
|
|
|
|
|
|
total: 0,
|
|
|
|
|
|
totalPages: 0,
|
|
|
|
|
|
currentPage: page,
|
|
|
|
|
|
hasNext: false,
|
|
|
|
|
|
hasPrevious: false
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-14 23:03:30 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-13 23:29:25 +03:00
|
|
|
|
/**
|
|
|
|
|
|
* Получить общее количество постов для всех тегов
|
|
|
|
|
|
*/
|
|
|
|
|
|
export async function getAllTagsWithCount(first = 100) {
|
|
|
|
|
|
const query = `
|
|
|
|
|
|
query GetAllTagsWithCount($first: Int!) {
|
|
|
|
|
|
tags(first: $first, where: { hideEmpty: true }) {
|
|
|
|
|
|
nodes {
|
|
|
|
|
|
id
|
|
|
|
|
|
databaseId
|
|
|
|
|
|
name
|
|
|
|
|
|
slug
|
|
|
|
|
|
count
|
|
|
|
|
|
posts(first: 1) {
|
|
|
|
|
|
pageInfo {
|
|
|
|
|
|
offsetPagination {
|
|
|
|
|
|
total
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const data = await fetchGraphQL(query, { first });
|
|
|
|
|
|
return data?.tags?.nodes || [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|