diff --git a/apps/hudozka/src/app/sitemap.ts b/apps/hudozka/src/app/sitemap.ts index 80bcef37..7275f922 100644 --- a/apps/hudozka/src/app/sitemap.ts +++ b/apps/hudozka/src/app/sitemap.ts @@ -1,9 +1,9 @@ import type { MetadataRoute } from "next" import { siteUrl } from "@/const" -import { getUrls } from "@/remote/api" +import { getAllTagSlugs, getUrls } from "@/remote/api" export default async function sitemap(): Promise { - const urls = await getUrls() + const [urls, tagSlugs] = await Promise.all([getUrls(), getAllTagSlugs()]) const staticPages: MetadataRoute.Sitemap = [ { @@ -14,6 +14,10 @@ export default async function sitemap(): Promise { url: `${siteUrl}/collective`, changeFrequency: "daily", }, + { + url: `${siteUrl}/tags`, + changeFrequency: "weekly", + }, ] const dynamicPages: MetadataRoute.Sitemap = urls.map(path => ({ @@ -21,5 +25,10 @@ export default async function sitemap(): Promise { changeFrequency: "daily", })) - return [...staticPages, ...dynamicPages] + const tagPages: MetadataRoute.Sitemap = tagSlugs.map(slug => ({ + url: `${siteUrl}/tags/${slug}`, + changeFrequency: "weekly", + })) + + return [...staticPages, ...dynamicPages, ...tagPages] } diff --git a/apps/hudozka/src/app/tags/[slug]/page.tsx b/apps/hudozka/src/app/tags/[slug]/page.tsx new file mode 100644 index 00000000..0b6eaf6b --- /dev/null +++ b/apps/hudozka/src/app/tags/[slug]/page.tsx @@ -0,0 +1,96 @@ +import { Title } from "@hudozka/ui" +import type { Metadata } from "next" +import { notFound } from "next/navigation" +import { App } from "@/components/App" +import { PageGrid } from "@/components/PageGrid" +import { Pagination } from "@/components/Pagination" +import { menu } from "@/const" +import { buildMetadata, MetaBuilder } from "@/lib/meta" +import { getAllTagSlugs, getPagesByTag } from "@/remote/api" + +export const revalidate = 30 +export const dynamicParams = true + +const PER_PAGE = 24 + +export async function generateStaticParams() { + const slugs = await getAllTagSlugs() + return slugs.map(slug => ({ slug })) +} + +type Props = { + params: Promise<{ slug: string }> + searchParams: Promise<{ page?: string }> +} + +function parsePage(value: string | undefined): number { + const n = Number.parseInt(value ?? "1", 10) + return Number.isFinite(n) && n >= 1 ? n : 1 +} + +export async function generateMetadata({ params, searchParams }: Props): Promise { + const [{ slug }, sp] = await Promise.all([params, searchParams]) + const page = parsePage(sp.page) + + const listing = await getPagesByTag(slug, page, PER_PAGE) + if (!listing) { + return {} + } + + const titleBase = `${listing.tag.name} — метка` + const title = page > 1 ? `${titleBase}, страница ${page}` : titleBase + + const meta = new MetaBuilder().setTitle(title).setDescription(`Записи с меткой «${listing.tag.name}».`).build() + + const metadata = buildMetadata(meta) + if (page > 1) { + return { ...metadata, robots: { index: false, follow: true } } + } + return metadata +} + +export default async function TagPage({ params, searchParams }: Props) { + const [{ slug }, sp] = await Promise.all([params, searchParams]) + const page = parsePage(sp.page) + + const listing = await getPagesByTag(slug, page, PER_PAGE) + if (!listing) { + notFound() + } + + const totalPages = Math.max(1, Math.ceil(listing.total / listing.perPage)) + if (listing.total > 0 && page > totalPages) { + notFound() + } + + const breadcrumbs = [ + { name: menu[0].name, href: "/" }, + { name: "Метки", href: "/tags" }, + { name: listing.tag.name, href: listing.tag.href }, + ] + + return ( + + {listing.tag.name} + {listing.items.length === 0 ? ( +

Пока нет записей.

+ ) : ( + <> + + + + )} +
+ ) +} diff --git a/apps/hudozka/src/app/tags/page.tsx b/apps/hudozka/src/app/tags/page.tsx new file mode 100644 index 00000000..e466883e --- /dev/null +++ b/apps/hudozka/src/app/tags/page.tsx @@ -0,0 +1,36 @@ +import { Title } from "@hudozka/ui" +import type { Metadata } from "next" +import { App } from "@/components/App" +import { TagsIndex } from "@/components/TagsIndex" +import { menu } from "@/const" +import { buildMetadata, MetaBuilder } from "@/lib/meta" +import { getAllTagsWithCounts } from "@/remote/api" + +export const revalidate = 30 + +export async function generateMetadata(): Promise { + const meta = new MetaBuilder().setTitle("Метки").setDescription("Список всех меток сайта.").build() + return buildMetadata(meta) +} + +export default async function TagsIndexPage() { + const tags = await getAllTagsWithCounts() + + const breadcrumbs = [ + { name: menu[0].name, href: "/" }, + { name: "Метки", href: "/tags" }, + ] + + return ( + + Метки + + + ) +} diff --git a/apps/hudozka/src/components/PageGrid/index.tsx b/apps/hudozka/src/components/PageGrid/index.tsx index df3821c7..734bbefd 100644 --- a/apps/hudozka/src/components/PageGrid/index.tsx +++ b/apps/hudozka/src/components/PageGrid/index.tsx @@ -1,3 +1,5 @@ +"use client" + import { useMobile } from "@hudozka/hooks" import { Box } from "@hudozka/ui" import Image from "next/image" diff --git a/apps/hudozka/src/components/Pagination/Pagination.test.tsx b/apps/hudozka/src/components/Pagination/Pagination.test.tsx new file mode 100644 index 00000000..e3796741 --- /dev/null +++ b/apps/hudozka/src/components/Pagination/Pagination.test.tsx @@ -0,0 +1,52 @@ +import { render, within } from "@testing-library/react" +import { describe, expect, it } from "vitest" +import { Pagination } from "." + +function getNav(container: HTMLElement) { + const nav = container.querySelector("nav") + if (!nav) throw new Error("expected