add tags logic

This commit is contained in:
Profile Profile
2026-03-02 00:39:07 +03:00
parent 0940493c21
commit 7739647549
11 changed files with 585 additions and 138 deletions

View File

@@ -15,8 +15,8 @@
<div class="top-bar">
<img alt="Профиль" width="249" height="55" src="https://cdn.profile.ru/wp-content/themes/profile/assets/img/profile-logo-delovoy.svg">
{category && (
<div class="parttile-block">
{category.name}
<div class="header__subtitle">
<a href={`/${category.slug}`}>{category.name}</a>
</div>
)}
<Stores />
@@ -32,6 +32,25 @@
.top-bar{
display: flex;
justify-content: space-between;
align-items: center;
}
.header__subtitle{
font-size: 22px;
font-weight: bold;
margin-left: 42px;
position: relative;
}
.header__subtitle::before{
content: '';
position: absolute;
top: 50%;
left: -15px;
width: 3px;
height: 80%;
border-left: 3px solid;
transform: translate(0, -40%);
}
</style>

View File

@@ -31,7 +31,6 @@ const { post, pageInfo } = Astro.props;
</div>
</div>
<h1>{post.title}</h1>
{post.secondaryTitle && <h2 class="secondary-title">{post.secondaryTitle}</h2>}
@@ -58,21 +57,37 @@ const { post, pageInfo } = Astro.props;
</figure>
)}
{post.content && <div set:html={post.content} />}
<Subscribe />
</article>
<ShareButtons url={post.uri} title={post.title} />
</div>
) : (
<div>Новость не найдена</div>
)}
<style>
{/* Блок с тегами */}
{post.tags?.nodes && post.tags.nodes.length > 0 && (
<div class="tags-block">
<span class="tags-label">Теги:</span>
<div class="tags-list">
{post.tags.nodes.map((tag) => (
<a
href={`/tag/${encodeURIComponent(tag.name.replace(/\s+/g, '+'))}/`}
class="tag-link"
key={tag.id}
>
{tag.name}
</a>
))}
</div>
</div>
)}
<style>
.article-wrapper {
position: relative;
max-width: 75%;
@@ -95,11 +110,10 @@ const { post, pageInfo } = Astro.props;
color: #0d6efd;
}
.article_info {
display: flex;
align-items: center;
gap: .475rem; /* одинаковое расстояние с обеих сторон от черты */
gap: .475rem;
margin-top: 1.9375rem;
margin-bottom: .9375rem;
font-size: 0.875rem;
@@ -118,8 +132,45 @@ const { post, pageInfo } = Astro.props;
text-decoration: underline;
}
/* Стили для блока тегов */
.tags-block {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 1.5rem 0;
flex-wrap: wrap;
}
.tags-label {
font-size: 0.875rem;
font-weight: 600;
color: #333;
}
.tags-list {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.tag-link {
display: inline-block;
padding: 0.25rem 0.75rem;
font-size: 0.8125rem;
line-height: 1.5;
color: #666;
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
border-radius: 4px;
text-decoration: none;
transition: all 0.2s ease;
}
.tag-link:hover {
color: #fff;
background-color: #0d6efd;
border-color: #0d6efd;
}
.featured-image {
margin: 1.5rem 0;

View File

@@ -114,13 +114,15 @@ export async function getPostsByCategory(slug, first = 14, after = null) {
cacheKey,
async () => {
const query = `
query GetPostsByCategory($first: Int!, $after: String, $slug: String!) {
profileArticles(
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 }
categoryName: $slug
}
) {
pageInfo {
@@ -130,46 +132,77 @@ export async function getPostsByCategory(slug, first = 14, after = null) {
edges {
cursor
node {
__typename
id
databaseId
title
uri
date
... on NodeWithTitle {
title
}
... on NodeWithFeaturedImage {
featuredImage {
node {
sourceUrl(size: LARGE)
altText
}
}
}
# Для ваших кастомных типов
... on ProfileArticle {
categories {
nodes {
name
slug
color
}
}
}
... on ANew {
categories {
nodes {
name
slug
color
}
}
}
... on NodeWithAuthor {
author {
node {
id
name
firstName
lastName
avatar {
url
}
uri
}
}
# Соавторы как массив
}
# Coauthors для ProfileArticle (без description)
... on ProfileArticle {
coauthors {
id
name
firstName
lastName
url
description
}
categories {
nodes {
}
# Coauthors для ANew (без description)
... on ANew {
coauthors {
id
name
color
slug
uri
databaseId
firstName
lastName
url
}
}
}
}
@@ -180,11 +213,31 @@ export async function getPostsByCategory(slug, first = 14, after = null) {
const data = await fetchGraphQL(query, { first, after, slug });
const posts = data.profileArticles?.edges?.map(edge => edge.node) || [];
// Обрабатываем посты
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.profileArticles?.pageInfo || { hasNextPage: false, endCursor: null }
pageInfo: data.category?.contentNodes?.pageInfo || {
hasNextPage: false,
endCursor: null
},
categoryName: data.category?.name || ''
};
},
{ ttl: CACHE_TTL.POSTS }
@@ -192,6 +245,156 @@ export async function getPostsByCategory(slug, first = 14, after = null) {
}
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
}
}
}
}
}
}
}
`;
console.log('Fetching with:', { first, after, slug }); // Добавим лог параметров
const data = await fetchGraphQL(query, { first, after, slug });
// Проверяем структуру 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: [] }
};
}) || [];
return {
posts,
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 || ''
};
},
{ ttl: CACHE_TTL.POSTS }
);
}

View File

@@ -13,7 +13,7 @@ import { detectPageType } from '@lib/detect-page-type';
export const prerender = false;
const pathname = Astro.url.pathname; // "/news/society/chto-sluchilos-nochju-27-oktyabrya-2025-goda-1772178/"
const pathname = Astro.url.pathname;
const pageInfo = detectPageType(pathname); //определяем тип страницы
@@ -37,7 +37,7 @@ if (pageInfo.type === 'single') { //одиночная статья
try {
article = await getProfileArticleById(pageInfo.postId); //получвем данные поста
title=article.titleж
title=article.title
} catch (error) {
console.error('Error fetching node:', error);
}
@@ -64,6 +64,12 @@ if (pageInfo.type === 'single') { //одиночная статья
category={category}
>
{/* Page (страница) */}
{pageInfo.type === 'unknown' && (
<div>✅ Это страница: {pageInfo.pageSlug}</div>
)}
{/* Single post */}
{pageInfo.type === 'single' && article && (
<NewsSingle post={article} pageInfo={pageInfo} />

View File

@@ -0,0 +1,74 @@
---
// pages/author/[slug]/[page].astro
import { wpClient } from '@lib/wp-client';
import MainLayout from '@layouts/MainLayout.astro';
export const prerender = false;
const { slug, page } = Astro.params;
// Преобразуем номер страницы в cursor
// Для страницы 1: cursor = 0
// Для страницы 2: нужно знать ID последнего поста с первой страницы
// Это сложнее для cursor-based пагинации, поэтому либо:
// Вариант 1: Используем номер страницы как offset (нужно изменить эндпоинт)
// Вариант 2: Делаем отдельный запрос для получения cursor по номеру страницы
const currentPage = parseInt(page) || 1;
let cursor = 0;
// Если страница > 1, нам нужно получить cursor для этой страницы
// Для простоты пока используем номер страницы как offset в URL
// или модифицируем эндпоинт для поддержки page
// Временное решение: используем cursor = (page - 1) * limit
// Но это не точно, лучше модифицировать эндпоинт
const limit = 10;
const estimatedCursor = (currentPage - 1) * limit;
const data = await wpClient.get(`my/v1/author-posts/${slug}`, {
cursor: currentPage === 1 ? 0 : estimatedCursor,
limit
});
//if (!data) {
// return Astro.redirect('/404');
//}
const { author, posts, pagination } = data;
---
<MainLayout>
<div class="author-header">
<h1>{author.name}</h1>
<p>Статьи автора</p>
</div>
<div class="posts-list">
{posts.map(post => (
<article key={post.id} class="post-item">
<h2>
<a href={`/posts/${post.slug}`}>{post.title}</a>
</h2>
<time datetime={post.date}>
{new Date(post.date).toLocaleDateString('ru-RU')}
</time>
<div class="post-meta">
<span class="cursor-info">ID: {post.id}</span>
</div>
</article>
))}
</div>
{pagination.has_more && (
<div class="load-more">
<button
hx-get={`/author/${slug}/page/${currentPage + 1}`}
hx-target=".posts-list"
hx-swap="beforeend"
>
Загрузить еще
</button>
</div>
)}
</MainLayout>

View File

@@ -0,0 +1,56 @@
---
// pages/author/[slug]/index.astro
import { wpClient } from '@lib/wp-client';
export const prerender = false;
const { slug } = Astro.params;
// Функция для получения данных автора
async function getAuthorData(authorSlug) {
try {
// Используем ваш кастомный эндпоинт или стандартный WP endpoint
const data = await wpClient.get(`my/v1/author-posts/${authorSlug}/1`);
if (!data) {
// Если кастомный эндпоинт не работает, пробуем стандартный
const users = await wpClient.get('wp/v2/users', { slug: authorSlug });
if (users && users.length > 0) {
return { author: users[0], posts: [] };
}
return null;
}
return data;
} catch (error) {
console.error('Error fetching author:', error);
return null;
}
}
const authorData = await getAuthorData(slug);
// Если автор не найден - 404
//if (!authorData) {
// return Astro.redirect('/404');
//}
const { author, posts } = authorData;
---
<h1>Author: {author.name || slug}</h1>
{posts && posts.length > 0 ? (
<div>
<h2>Статьи автора:</h2>
<ul>
{posts.map(post => (
<li key={post.id}>
<a href={post.link}>{post.title}</a>
</li>
))}
</ul>
</div>
) : (
<p>У автора пока нет статей</p>
)}

View File

@@ -26,7 +26,7 @@ console.log('📥 Load more request:', { perLoad, after, type, slug, startIndex
// Импортируем функции для получения данных
const { getLatestPosts, getPostsByCategory, getAuthorPosts, getTagPosts } =
const { getLatestPosts, getPostsByCategory, getAuthorPosts, getPostsByTag } =
await import('../lib/api/posts');
let result;
@@ -43,7 +43,7 @@ switch (type) {
break;
case 'tag':
if (!slug) throw new Error('Slug required for tag');
result = await getTagPosts(slug, perLoad, after); // Используем perLoad
result = await getPostsByTag(slug, perLoad, after); // Используем perLoad
break;
case 'latest':
default:

View File

View File

@@ -0,0 +1,68 @@
---
import { slugParse } from '@utils/slugParser';
//api
import { getTag, getTagWithPostsById } from '@api/tags.js';
import { getArchivePostsById } from '@/lib/api/archiveById';
import MainLayout from '@layouts/MainLayout.astro';
import TagArchive from '@/templates/TagArchive.astro';
//ISR
export const prerender = false;
const { slug } = Astro.params;
// Используем функцию (правильное название)
const { slug: tagSlug, page: currentPage } = slugParse(slug);
// Если slug пустой - 404
if (!tagSlug) {
return Astro.redirect('/404');
}
// Получаем данные тега
const tag = await getTag(tagSlug);
if (!tag) {
return Astro.redirect('/404');
}
// ----------------------------
// fetch archive
// ----------------------------
const perPage = 35;
const { posts, pageInfo } = await getArchivePostsById({
type: 'tag',
id: tag.databaseId,
page: currentPage,
perPage,
});
// 404 если страница невалидна
if (!posts.length && currentPage > 1) {
return Astro.redirect('/404');
}
---
<MainLayout
title='Тег'
description="Информационное агентство Деловой журнал Профиль"
>
<TagArchive
tag={tag}
posts={posts}
page={1}
hasNextPage={pageInfo.hasNextPage}
baseUrl={`/tag/${slug}`}
/>
</MainLayout>

View File

@@ -1,68 +1,38 @@
---
import { slugParse } from '@utils/slugParser';
//api
import { getTag, getTagWithPostsById } from '@api/tags.js';
import { getArchivePostsById } from '@/lib/api/archiveById';
import MainLayout from '@layouts/MainLayout.astro';
import TagArchive from '@/templates/TagArchive.astro';
import ContentGrid from '@components/ContentGrid.astro';
import { getPostsByTag } from '@lib/api/posts';
//ISR
export const prerender = false;
const { slug } = Astro.params;
// Используем функцию (правильное название)
const { slug: tagSlug, page: currentPage } = slugParse(slug);
// Декодируем URL и преобразуем в формат для WP
const decodedSlug = slug ? decodeURIComponent(slug.replace(/\+/g, ' ')) : '';
// Если slug пустой - 404
if (!tagSlug) {
return Astro.redirect('/404');
}
// Получаем данные тега
const tag = await getTag(tagSlug);
if (!tag) {
return Astro.redirect('/404');
}
// ----------------------------
// fetch archive
// ----------------------------
const perPage = 35;
const { posts, pageInfo } = await getArchivePostsById({
type: 'tag',
id: tag.databaseId,
page: currentPage,
perPage,
});
// 404 если страница невалидна
if (!posts.length && currentPage > 1) {
return Astro.redirect('/404');
}
// Получаем данные по тегу
const result = await getPostsByTag(decodedSlug);
const posts = result?.posts || [];
const pageInfo = result?.pageInfo || {};
const tagName = result?.tagName || slug;
const hasNextPage = pageInfo?.hasNextPage || false;
const endCursor = pageInfo?.endCursor || null;
---
<MainLayout
title='Тег'
description="Информационное агентство Деловой журнал Профиль"
title={`Записи по тегу: ${tagName}`}
description={`Информационное агентство Деловой журнал Профиль - записи по тегу ${tagName}`}
>
<TagArchive
tag={tag}
posts={posts}
page={1}
hasNextPage={pageInfo.hasNextPage}
baseUrl={`/tag/${slug}`}
<h1>#{decodedSlug}</h1>
<ContentGrid
items={posts}
pageInfo={pageInfo}
slug={slug}
showCount={false}
type='tag'
perLoad={11}
/>
</MainLayout>

View File

@@ -10,7 +10,7 @@ html{
}
body {
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
font-size: 18px;
}