From 9873423b4279d57eea546e9b2f638aa56ad3108a Mon Sep 17 00:00:00 2001 From: miyaji255 <84168445+miyaji255@users.noreply.github.com> Date: Tue, 5 Dec 2023 23:17:19 +0900 Subject: [PATCH] =?UTF-8?q?=E3=82=BF=E3=82=B0=E4=B8=80=E8=A6=A7=E3=81=A8?= =?UTF-8?q?=E9=96=A2=E9=80=A3=E8=A8=98=E4=BA=8B=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/blog/BlogContent.astro | 9 +- src/components/blog/BlogList.astro | 15 ++- src/components/blog/BlogListItem.astro | 17 ++- src/components/blog/RelatedBlogSection.astro | 16 +++ src/components/blog/tag/TagList.astro | 21 ++-- src/components/blog/tag/TagListItemCard.astro | 24 ++++ src/components/blog/tag/TagListSection.astro | 17 +++ src/components/blog/tag/TagSmallList.astro | 25 ++++ src/components/layout/nav/BlogNav.astro | 34 ++++++ src/content/_blog-statistics.ts | 110 ++++++++++++++++++ src/layouts/BlogLayout.astro | 2 + src/pages/blog/articles/[slug]/index.astro | 4 + src/pages/blog/index.astro | 2 +- src/pages/blog/tags/index.astro | 25 ++++ src/utils/format-date.ts | 6 + 15 files changed, 292 insertions(+), 35 deletions(-) create mode 100644 src/components/blog/RelatedBlogSection.astro create mode 100644 src/components/blog/tag/TagListItemCard.astro create mode 100644 src/components/blog/tag/TagListSection.astro create mode 100644 src/components/blog/tag/TagSmallList.astro create mode 100644 src/components/layout/nav/BlogNav.astro create mode 100644 src/content/_blog-statistics.ts create mode 100644 src/pages/blog/tags/index.astro create mode 100644 src/utils/format-date.ts diff --git a/src/components/blog/BlogContent.astro b/src/components/blog/BlogContent.astro index a74bb37..4fbbb81 100644 --- a/src/components/blog/BlogContent.astro +++ b/src/components/blog/BlogContent.astro @@ -1,15 +1,12 @@ --- import { getEntries, type CollectionEntry, getEntry } from 'astro:content' -import TagList from './tag/TagList.astro' +import TagSmallList from './tag/TagSmallList.astro' import AuthorIcon from './icon/AuthorIcon.astro' +import { formatDate } from '@/utils/format-date' interface Props { blog: CollectionEntry<'blogs'> } -function formatDate(date: Date): string { - return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}` -} - const { blog } = Astro.props const { data: blogMeta } = (await getEntry({ collection: 'blog-metas', @@ -30,7 +27,7 @@ const { Content } = await blog.render() >{author.data.name} - +
{ blogMeta.updateDate && ( diff --git a/src/components/blog/BlogList.astro b/src/components/blog/BlogList.astro index db6f981..28ace53 100644 --- a/src/components/blog/BlogList.astro +++ b/src/components/blog/BlogList.astro @@ -4,9 +4,10 @@ import BlogListItem from './BlogListItem.astro' interface Props { blogs: CollectionEntry<'blogs'>[] + sorted?: boolean } -const { blogs } = Astro.props +const { blogs, sorted } = Astro.props const blogMetas = await Promise.all( blogs.map((b) => getEntry({ collection: 'blog-metas', id: b.slug })), @@ -17,11 +18,13 @@ const sortedBlogs = blogs.map((b, i) => ({ ...(blogMetas[i]?.data ?? { postDate: new Date() }), slug: b.slug, })) -sortedBlogs.sort( - (b1, b2) => - (b2.updateDate ?? b2.postDate).getTime() - - (b1.updateDate ?? b1.postDate).getTime(), -) +if (!sorted) { + sortedBlogs.sort( + (b1, b2) => + (b2.updateDate ?? b2.postDate).getTime() - + (b1.updateDate ?? b1.postDate).getTime(), + ) +} ---
    diff --git a/src/components/blog/BlogListItem.astro b/src/components/blog/BlogListItem.astro index c24ae97..be01d86 100644 --- a/src/components/blog/BlogListItem.astro +++ b/src/components/blog/BlogListItem.astro @@ -1,13 +1,12 @@ --- import { getEntries, getEntry, type CollectionEntry } from 'astro:content' -import TagList from './tag/TagList.astro' +import { formatDate } from '@/utils/format-date' import AuthorIcon from './icon/AuthorIcon.astro' +import TagSmallList from './tag/TagSmallList.astro' type Props = CollectionEntry<'blogs'>['data'] & - CollectionEntry<'blog-metas'>['data'] & { slug: string } - -function formatDate(date: Date): string { - return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}` -} + CollectionEntry<'blog-metas'>['data'] & { + slug: CollectionEntry<'blogs'>['slug'] + } const { slug, ...blog } = Astro.props @@ -15,15 +14,13 @@ const author = await getEntry(blog.author) const tags = await getEntries(blog.tags) --- -
  • +
  • {blog.title}

    - +
    [] +} + +const { blogs } = Astro.props +--- + +
    + 関連記事 + +
    diff --git a/src/components/blog/tag/TagList.astro b/src/components/blog/tag/TagList.astro index 7155fef..4ad5e46 100644 --- a/src/components/blog/tag/TagList.astro +++ b/src/components/blog/tag/TagList.astro @@ -1,25 +1,22 @@ --- import type { CollectionEntry } from 'astro:content' -import TagIcon from '../icon/TagIcon.astro' +import TagListItemCard from './TagListItemCard.astro' + interface Props { - tags: CollectionEntry<'tags'>[] + tags: (CollectionEntry<'tags'> & { articleCount: number })[] } const { tags } = Astro.props --- -
      + diff --git a/src/components/blog/tag/TagListItemCard.astro b/src/components/blog/tag/TagListItemCard.astro new file mode 100644 index 0000000..daa0daa --- /dev/null +++ b/src/components/blog/tag/TagListItemCard.astro @@ -0,0 +1,24 @@ +--- +import type { CollectionEntry } from 'astro:content' +import TagIcon from '../icon/TagIcon.astro' +type Props = CollectionEntry<'tags'>['data'] & { + id: CollectionEntry<'tags'>['id'] + articleCount: number +} + +const { id, articleCount, ...tag } = Astro.props +--- + +
    • + + +
      +

      + {tag.name} +

      +

      記事数 : {articleCount}

      +
      +
      +
    • diff --git a/src/components/blog/tag/TagListSection.astro b/src/components/blog/tag/TagListSection.astro new file mode 100644 index 0000000..04efdd3 --- /dev/null +++ b/src/components/blog/tag/TagListSection.astro @@ -0,0 +1,17 @@ +--- +import type { CollectionEntry } from 'astro:content' +import Section from '@/components/common/Section.astro' +import TagList from './TagList.astro' + +interface Props { + title: string + tags: (CollectionEntry<'tags'> & { articleCount: number })[] +} + +const { title, tags } = Astro.props +--- + +
      + {title} + +
      diff --git a/src/components/blog/tag/TagSmallList.astro b/src/components/blog/tag/TagSmallList.astro new file mode 100644 index 0000000..7155fef --- /dev/null +++ b/src/components/blog/tag/TagSmallList.astro @@ -0,0 +1,25 @@ +--- +import type { CollectionEntry } from 'astro:content' +import TagIcon from '../icon/TagIcon.astro' +interface Props { + tags: CollectionEntry<'tags'>[] +} + +const { tags } = Astro.props +--- + + diff --git a/src/components/layout/nav/BlogNav.astro b/src/components/layout/nav/BlogNav.astro new file mode 100644 index 0000000..587cc15 --- /dev/null +++ b/src/components/layout/nav/BlogNav.astro @@ -0,0 +1,34 @@ +--- +const current = Astro.url.pathname + +const navList: { target: string; title: string }[] = [ + { + target: '/blog', + title: '記事一覧', + }, + { + target: '/blog/tags', + title: 'タグ一覧', + }, +] +--- + +
      + { + navList.map(({ target, title }) => ( + + {title} + + )) + } +
      diff --git a/src/content/_blog-statistics.ts b/src/content/_blog-statistics.ts new file mode 100644 index 0000000..6a77865 --- /dev/null +++ b/src/content/_blog-statistics.ts @@ -0,0 +1,110 @@ +import { unreachable } from '@/utils/unreachable' +import { type CollectionEntry, getCollection, getEntry } from 'astro:content' + +type TagId = CollectionEntry<'tags'>['id'] +type BlogSlug = CollectionEntry<'blogs'>['slug'] +export type TagStatistics = Readonly< + Record< + TagId, + readonly Readonly<{ slug: BlogSlug; time: number; timeScore: number }>[] + > +> +export type BlogStatistics = Readonly< + Record< + BlogSlug, + readonly Readonly<{ collection: 'blogs'; slug: BlogSlug; score: number }>[] + > +> + +let tagStatistics: TagStatistics | null = null +let blogStatistics: BlogStatistics | null = null + +function calcTimeScore(milliseconds: number): number { + const diff = Date.now() - milliseconds + + if (diff <= 365 * 24 * 3600_000) return 3 + else if (diff <= 2 * 365 * 24 * 3600_000) return 2 + else return 1 +} + +async function analyze() { + if (blogStatistics === null || tagStatistics === null) { + const blogs = await getCollection('blogs') + + const blogMetas = await Promise.all( + blogs.map((b) => getEntry({ collection: 'blog-metas', id: b.slug })), + ) + + const blogWithMeta = blogs + .map((b, i) => ({ + slug: b.slug, + tags: b.data.tags, + time: + blogMetas[i]?.data.updateDate?.getTime() ?? + blogMetas[i]?.data.postDate.getTime() ?? + Date.now(), + timeScore: calcTimeScore( + blogMetas[i]?.data.updateDate?.getTime() ?? + blogMetas[i]?.data.postDate.getTime() ?? + Date.now(), + ), + })) + .sort((blog1, blog2) => blog2.time - blog1.time) + const tags = await getCollection('tags') + tagStatistics = Object.fromEntries( + tags.map( + (tag) => + [ + tag.id, + blogWithMeta + .filter((b) => b.tags.some((t) => t.id === tag.id)) + .map(({ tags: _, ...b }) => b) as TagStatistics[TagId], + ] as const, + ), + ) as TagStatistics + + blogStatistics = Object.fromEntries( + blogs.map((blog) => { + const result: { [K in BlogSlug]?: { score: number; time: number } } = {} + blog.data.tags + .flatMap((t) => tagStatistics![t.id]) + .forEach(({ slug, timeScore, time }) => { + if (slug !== blog.slug) { + result[slug] ??= { score: 0, time } + result[slug]!.score += timeScore + } + }) + const resultArr = Object.entries(result) + .sort( + ( + [_, { score: score1, time: time1 }], + [__, { score: score2, time: time2 }], + ) => (score1 !== score2 ? score2 - score1 : time2 - time1), + ) + .map(([slug, { score }]) => ({ + slug, + score, + collection: 'blogs', + })) as BlogStatistics[BlogSlug] + + return [blog.slug, resultArr] as const + }), + ) as BlogStatistics + } +} + +export async function getBlogStatistics(slug: BlogSlug) { + await analyze() + if (tagStatistics === null || blogStatistics === null) return unreachable() + + return blogStatistics[slug] +} + +export async function getTagStatistics(): Promise +export async function getTagStatistics(id: TagId): Promise +export async function getTagStatistics(id?: TagId) { + await analyze() + if (tagStatistics === null || blogStatistics === null) return unreachable() + + return id === undefined ? tagStatistics : tagStatistics[id] +} diff --git a/src/layouts/BlogLayout.astro b/src/layouts/BlogLayout.astro index 0f3c540..42ae74e 100644 --- a/src/layouts/BlogLayout.astro +++ b/src/layouts/BlogLayout.astro @@ -2,6 +2,7 @@ import BaseLayout from './BaseLayout.astro' import Header from '@/components/layout/nav/Header.astro' import Footer from '@/components/layout/Footer.astro' +import BlogNav from '@/components/layout/nav/BlogNav.astro' export interface Props { title: string @@ -38,6 +39,7 @@ const ogImageUrl = new URL(image, Astro.site).toString() headerTitle="BLOG" rootPath="/blog" /> +
      diff --git a/src/pages/blog/articles/[slug]/index.astro b/src/pages/blog/articles/[slug]/index.astro index d450fe6..a08342f 100644 --- a/src/pages/blog/articles/[slug]/index.astro +++ b/src/pages/blog/articles/[slug]/index.astro @@ -1,6 +1,8 @@ --- import BlogContent from '@/components/blog/BlogContent.astro' +import RelatedBlogSection from '@/components/blog/RelatedBlogSection.astro' import BlogLayout from '@/layouts/BlogLayout.astro' +import { getBlogStatistics } from '@/content/_blog-statistics' import { getCollection, getEntries } from 'astro:content' export async function getStaticPaths() { @@ -13,6 +15,7 @@ export async function getStaticPaths() { const { blog } = Astro.props const tags = await getEntries(blog.data.tags) +const statistics = await getEntries([...(await getBlogStatistics(blog.slug))]) ---
      +
      diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index deded92..1d370d5 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -6,7 +6,7 @@ import BlogListSection from '@/components/blog/BlogListSection.astro' const blogEntries = await getCollection('blogs') --- - +
      diff --git a/src/pages/blog/tags/index.astro b/src/pages/blog/tags/index.astro new file mode 100644 index 0000000..c82ebd2 --- /dev/null +++ b/src/pages/blog/tags/index.astro @@ -0,0 +1,25 @@ +--- +import BlogLayout from '@/layouts/BlogLayout.astro' +import { getCollection } from 'astro:content' +import { getTagStatistics } from '@/content/_blog-statistics' +import TagListSection from '@/components/blog/tag/TagListSection.astro' + +const tagEntries = await getCollection('tags') +const statistics = await getTagStatistics() +const tags = tagEntries.map((tag) => ({ + ...tag, + articleCount: statistics[tag.id].length, + lastPostedTime: statistics[tag.id][0]?.time ?? 0, +})) +tags.sort((tag1, tag2) => + tag2.articleCount !== tag1.articleCount + ? tag2.articleCount - tag1.articleCount + : tag2.lastPostedTime - tag1.lastPostedTime, +) +--- + + +
      + +
      +
      diff --git a/src/utils/format-date.ts b/src/utils/format-date.ts new file mode 100644 index 0000000..091dc89 --- /dev/null +++ b/src/utils/format-date.ts @@ -0,0 +1,6 @@ +export function formatDate(date: Date): string { + date = new Date(date.getTime() + 9 * 3600_000) + return `${date.getUTCFullYear()}/${ + date.getUTCMonth() + 1 + }/${date.getUTCDate()}` +}