add endpoint authors

This commit is contained in:
Profile Profile
2026-03-02 23:10:31 +03:00
parent 7739647549
commit e5af3fcfd6
7 changed files with 525 additions and 103 deletions

View File

@@ -23,19 +23,24 @@ const {
perLoad = 11 perLoad = 11
} = Astro.props; } = Astro.props;
// Используем perLoad везде
const loadMoreConfig = { const loadMoreConfig = {
type, type,
slug, slug,
perLoad // Теперь используем perLoad вместо first perLoad
}; };
function getCoauthorsNames(coauthors: any[]): string { function getCoauthorsNames(coauthors: any[]): string {
if (!coauthors || coauthors.length === 0) return ''; if (!coauthors || coauthors.length === 0) return '';
return coauthors return coauthors
.map((coauthor: any) => coauthor?.node?.name || coauthor?.name) .map((coauthor: any) => {
const name = coauthor?.node?.name || coauthor?.name;
const nickname = coauthor?.node?.nickname || coauthor?.nickname;
return name; // Возвращаем только имя, ссылки будут в шаблоне
})
.filter(Boolean) .filter(Boolean)
.join(' '); .join(', ');
} }
function shouldBeLarge(index: number): boolean { function shouldBeLarge(index: number): boolean {
@@ -55,7 +60,6 @@ function shouldBeLarge(index: number): boolean {
{items.map((item, index) => { {items.map((item, index) => {
const postUrl = item.uri || `/blog/${item.databaseId}`; const postUrl = item.uri || `/blog/${item.databaseId}`;
const postDate = new Date(item.date); const postDate = new Date(item.date);
const coauthorsNames = getCoauthorsNames(item.coauthors || []);
const isLarge = shouldBeLarge(index); const isLarge = shouldBeLarge(index);
return ( return (
@@ -66,54 +70,83 @@ function shouldBeLarge(index: number): boolean {
itemscope itemscope
itemtype="https://schema.org/BlogPosting" itemtype="https://schema.org/BlogPosting"
> >
<a href={postUrl} class="post-card-link"> <div class="post-image-container">
<div class="post-image-container"> {item.featuredImage?.node?.sourceUrl ? (
{item.featuredImage?.node?.sourceUrl ? ( <img
<img src={item.featuredImage.node.sourceUrl}
src={item.featuredImage.node.sourceUrl} alt={item.featuredImage.node.altText || item.title}
alt={item.featuredImage.node.altText || item.title} width="400"
width="400" height="400"
height="400" loading="lazy"
loading="lazy" class="post-image"
class="post-image" itemprop="image"
itemprop="image"
/>
) : (
<div class="post-image-placeholder"></div>
)}
<CategoryBadge
name={item.categories?.nodes?.[0]?.name}
color={item.categories?.nodes?.[0]?.color}
/> />
) : (
<div class="post-image-placeholder"></div>
)}
{item.categories?.nodes?.[0] && (
<a
href={`/${item.categories.nodes[0].slug}`}
class="category-badge-link"
onClick={(e) => e.stopPropagation()}
>
<CategoryBadge
name={item.categories.nodes[0].name}
color={item.categories.nodes[0].color}
/>
</a>
)}
<div class="post-content-overlay">
<div class="post-meta-overlay">
<time
datetime={item.date}
class="post-date-overlay"
itemprop="datePublished"
>
{postDate.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'short',
year: 'numeric'
}).replace(' г.', '')}
</time>
</div>
<div class="post-content-overlay"> <a href={postUrl} class="post-title-link" itemprop="url">
<div class="post-meta-overlay">
<time
datetime={item.date}
class="post-date-overlay"
itemprop="datePublished"
>
{postDate.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'short',
year: 'numeric'
}).replace(' г.', '')}
</time>
</div>
<h3 class="post-title-overlay" itemprop="headline"> <h3 class="post-title-overlay" itemprop="headline">
{item.title} {item.title}
</h3> </h3>
</a>
{coauthorsNames && ( {item.coauthors && item.coauthors.length > 0 && (
<div class="author-name" itemprop="author"> <div class="coauthors-wrapper" itemprop="author">
{coauthorsNames} {item.coauthors.map((coauthor: any, i: number) => {
</div> const name = coauthor?.node?.name || coauthor?.name;
)} const nickname = coauthor?.node?.nickname || coauthor?.nickname;
</div> console.log(nickname);
return (
<span key={nickname || name}>
{i > 0 && ', '}
{nickname ? (
<a
href={`/author/${nickname}`}
class="author-link"
onClick={(e) => e.stopPropagation()}
>
{name}
</a>
) : (
<span class="author-name">{name}</span>
)}
</span>
);
})}
</div>
)}
</div> </div>
</a> </div>
<div class="sr-only"> <div class="sr-only">
<h3 itemprop="headline"> <h3 itemprop="headline">

View File

@@ -72,7 +72,7 @@ const { post, pageInfo } = Astro.props;
{/* Блок с тегами */} {/* Блок с тегами */}
{post.tags?.nodes && post.tags.nodes.length > 0 && ( {post.tags?.nodes && post.tags.nodes.length > 0 && (
<div class="tags-block"> <div class="tags-block">
<span class="tags-label">Теги:</span> <span class="tags-label">Метки:</span>
<div class="tags-list"> <div class="tags-list">
{post.tags.nodes.map((tag) => ( {post.tags.nodes.map((tag) => (
<a <a
@@ -143,7 +143,7 @@ const { post, pageInfo } = Astro.props;
.tags-label { .tags-label {
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 600; font-weight: 500;
color: #333; color: #333;
} }
@@ -158,18 +158,17 @@ const { post, pageInfo } = Astro.props;
padding: 0.25rem 0.75rem; padding: 0.25rem 0.75rem;
font-size: 0.8125rem; font-size: 0.8125rem;
line-height: 1.5; line-height: 1.5;
color: #666; color: black;
background-color: #f5f5f5; font-weight: 700;
border: 1px solid #e0e0e0; background-color: #ececec;
border-radius: 4px; border-radius: 4px;
text-decoration: none; text-decoration: none;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.tag-link:hover { .tag-link:hover {
color: #fff; background-color: #d3d3d3;
background-color: #0d6efd; border-color: #d3d3d3;
border-color: #0d6efd;
} }
.featured-image { .featured-image {

370
src/lib/api/authors.ts Normal file
View File

@@ -0,0 +1,370 @@
import { fetchGraphQL } from './graphql-client.js';
//кэширование
import { cache } from '@lib/cache/manager.js';
import { CACHE_TTL } from '@lib/cache/cache-ttl';
export async function getPostsByCoauthorName(coauthorName, first = 14, after = null) {
// Создаем уникальный ключ для кэша
const cacheKey = `coauthor-posts-known:${coauthorName}:${first}:${after || 'first-page'}`;
return await cache.wrap(
cacheKey,
async () => {
const query = `
query GetPostsByCoauthorName($first: Int!, $after: String, $coauthorName: String!) {
# Информация об авторе
users(where: {search: $coauthorName}) {
nodes {
databaseId
name
nicename
email
avatar {
url
}
description
posts {
pageInfo {
total
}
}
}
}
# Посты автора
contentNodes(
first: $first
after: $after
where: {
contentTypes: [PROFILE_ARTICLE, ANEW]
coauthorName: $coauthorName
orderby: { field: DATE, order: DESC }
}
) {
pageInfo {
hasNextPage
endCursor
}
nodes {
__typename
id
databaseId
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 {
name
avatar {
url
}
}
}
}
# Coauthors для ProfileArticle
... on ProfileArticle {
coauthors {
id
name
firstName
lastName
url
nicename
}
}
# Coauthors для ANew
... on ANew {
coauthors {
id
name
firstName
lastName
url
nicename
}
}
}
}
}
`;
const data = await fetchGraphQL(query, {
first,
after,
coauthorName
});
// Находим автора (первый результат поиска)
const author = data.users?.nodes?.[0];
if (!author) {
return {
author: null,
posts: [],
pageInfo: { hasNextPage: false, endCursor: null },
totalPosts: 0
};
}
// Обрабатываем посты
const posts = data.contentNodes?.nodes?.map(node => {
// Приводим coauthors к единому формату
if (node.coauthors) {
node.coauthors = node.coauthors.map(coauthor => ({
id: coauthor.id,
nickname: coauthor.nickname,
name: coauthor.name || '',
firstName: coauthor.firstName || '',
lastName: coauthor.lastName || '',
url: coauthor.url || ''
}));
}
return node;
}) || [];
return {
author: {
id: author.databaseId,
name: author.name,
login: author.nicename,
email: author.email,
avatar: author.avatar?.url,
description: author.description,
totalPosts: author.posts?.pageInfo?.total || posts.length
},
posts,
pageInfo: data.contentNodes?.pageInfo || {
hasNextPage: false,
endCursor: null
},
authorName: coauthorName
};
},
{ ttl: CACHE_TTL.POSTS }
);
}
// все посты автора
export async function getPostsByCoauthorLogin(login, first = 14, after = null) {
// Создаем уникальный ключ для кэша
const cacheKey = `coauthor-posts-by-login:${login}:${first}:${after || 'first-page'}`;
return await cache.wrap(
cacheKey,
async () => {
const query = `
query GetPostsByCoauthorLogin($first: Int!, $after: String, $login: String!) {
contentNodes(
first: $first
after: $after
where: {
contentTypes: [PROFILE_ARTICLE, ANEW]
coauthorLogin: $login
orderby: { field: DATE, order: DESC }
}
) {
pageInfo {
hasNextPage
endCursor
}
nodes {
__typename
id
databaseId
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 {
name
avatar {
url
}
}
}
}
# Coauthors для ProfileArticle
... on ProfileArticle {
coauthors {
id
name
firstName
lastName
url
nicename # Добавляем nicename (login)
}
}
# Coauthors для ANew
... on ANew {
coauthors {
id
name
firstName
lastName
url
nicename # Добавляем nicename (login)
}
}
}
}
}
`;
const data = await fetchGraphQL(query, {
first,
after,
login
});
// Обрабатываем посты
const posts = data.contentNodes?.nodes?.map(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 || '',
nicename: author.nicename || '' // Добавляем login
}));
}
return node;
}) || [];
return {
posts,
pageInfo: data.contentNodes?.pageInfo || {
hasNextPage: false,
endCursor: null
},
authorLogin: login
};
},
{ ttl: CACHE_TTL.POSTS }
);
}
export async function getAuthorData(slug) {
const cacheKey = `author-data:slug:${slug}`;
return await cache.wrap(
cacheKey,
async () => {
const baseUrl = import.meta.env.WP_REST_BASE_URL.replace(/\/$/, '');
try {
const response = await fetch(`${baseUrl}/author/${slug}`);
if (!response.ok) {
if (response.status === 404) {
return null;
}
throw new Error(`HTTP error: ${response.status}`);
}
const author = await response.json();
// Форматируем ответ
return {
id: author.slug || author.id,
name: author.name,
firstName: author.firstName,
lastName: author.lastName,
avatar: author.photo || author.avatar,
avatar_sizes: author.photo_sizes || {},
bio: author.description,
social: author.social || {},
url: author.url,
type: author.type,
};
} catch (error) {
console.error(`❌ Failed to fetch author ${slug}:`, error);
return null;
}
},
{ ttl: CACHE_TTL.AUTHOR }
);
}

View File

@@ -398,6 +398,8 @@ export async function getPostsByTag(slug, first = 14, after = null) {
//последние новости (кэшированная версия) //последние новости (кэшированная версия)
export async function getLatestAnews(count = 12): Promise<AnewsPost[]> { export async function getLatestAnews(count = 12): Promise<AnewsPost[]> {
const cacheKey = `latest-anews:${count}`; const cacheKey = `latest-anews:${count}`;

View File

@@ -3,7 +3,8 @@
*/ */
export const CACHE_TTL = { export const CACHE_TTL = {
TAXONOMY: parseInt(import.meta.env.CACHE_TAXONOMY_TTL || '3600'), TAXONOMY: parseInt(import.meta.env.CACHE_TAXONOMY_TTL || '3600'),
POSTS: parseInt(import.meta.env.CACHE_POST_TTL || '1800') POSTS: parseInt(import.meta.env.CACHE_POST_TTL || '1800'),
AUTHOR: parseInt(import.meta.env.CACHE_AUTHOR_TTL || '8')
} as const; } as const;
// Для отключения кэша // Для отключения кэша

View File

@@ -1,56 +1,77 @@
--- ---
// pages/author/[slug]/index.astro import MainLayout from '@layouts/MainLayout.astro';
import { wpClient } from '@lib/wp-client'; import { getAuthorData, getPostsByCoauthorLogin } from '@lib/api/authors';
import ContentGrid from '@components/ContentGrid.astro';
export const prerender = false; export const prerender = false;
const { slug } = Astro.params; const { slug } = Astro.params;
// Функция для получения данных автора const author = await getAuthorData(slug);
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); const data = await getPostsByCoauthorLogin(slug);
const posts = data.posts;
// Если автор не найден - 404 // Если автор не найден - 404
//if (!authorData) { //if (!authorData) {
// return Astro.redirect('/404'); // return Astro.redirect('/404');
//} //}
const { author, posts } = authorData;
--- ---
<h1>Author: {author.name || slug}</h1> <MainLayout
title={`Публикации автора: ${slug}`}
description={`Информационное агентство Деловой журнал Профиль - записи по тегу ${slug}`}
>
{author && (
<div class="author-card">
{author.avatar && (
<img
src={author.avatar}
alt={author.name}
width="192"
height="192"
class="avatar"
/>
)}
<h1>{author.name}</h1>
{(author.firstName || author.lastName) && (
<p class="full-name">
{author.firstName} {author.lastName}
</p>
)}
{author.bio && (
<div class="bio">{author.bio}</div>
)}
{author.social && Object.values(author.social).some(Boolean) && (
<div class="social-links">
{author.social.twitter && (
<a href={author.social.twitter}>Twitter</a>
)}
{author.social.facebook && (
<a href={author.social.facebook}>Facebook</a>
)}
{author.social.instagram && (
<a href={author.social.instagram}>Instagram</a>
)}
</div>
)}
</div>
)}
<ContentGrid
items={posts}
pageInfo={data.pageInfo}
slug={slug}
showCount={false}
type='author'
perLoad={11}
/>
</MainLayout>
{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

@@ -1,5 +1,7 @@
--- ---
import ContentGrid from '@components/ContentGrid.astro'; import ContentGrid from '@components/ContentGrid.astro';
import { getLatestPosts, getPostsByCategory, getAuthorPosts, getPostsByTag } from '../lib/api/posts';
import { getPostsByCoauthorLogin } from '../lib/api/authors';
export const prerender = false; export const prerender = false;
@@ -22,12 +24,6 @@ const {
startIndex = 0 startIndex = 0
} = body; } = body;
console.log('📥 Load more request:', { perLoad, after, type, slug, startIndex });
// Импортируем функции для получения данных
const { getLatestPosts, getPostsByCategory, getAuthorPosts, getPostsByTag } =
await import('../lib/api/posts');
let result; let result;
@@ -39,7 +35,7 @@ switch (type) {
break; break;
case 'author': case 'author':
if (!slug) throw new Error('Slug required for author'); if (!slug) throw new Error('Slug required for author');
result = await getAuthorPosts(slug, perLoad, after); // Используем perLoad result = await getPostsByCoauthorLogin(slug, perLoad, after); // Используем perLoad
break; break;
case 'tag': case 'tag':
if (!slug) throw new Error('Slug required for tag'); if (!slug) throw new Error('Slug required for tag');