Skip to content

Commit

Permalink
Merge pull request #53 from OUCC/miyaji/blog-statistics
Browse files Browse the repository at this point in the history
タグ一覧と関連記事を追加
  • Loading branch information
miyaji255 authored Dec 6, 2023
2 parents 6f69509 + 9873423 commit 698eb72
Show file tree
Hide file tree
Showing 15 changed files with 292 additions and 35 deletions.
9 changes: 3 additions & 6 deletions src/components/blog/BlogContent.astro
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -30,7 +27,7 @@ const { Content } = await blog.render()
><AuthorIcon {...author.data} size={28} />{author.data.name}</a
>
</div>
<TagList tags={tags} />
<TagSmallList tags={tags} />
<div class="text-sm text-gray-700 flex gap-3 p-2">
{
blogMeta.updateDate && (
Expand Down
15 changes: 9 additions & 6 deletions src/components/blog/BlogList.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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 })),
Expand All @@ -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(),
)
}
---

<ul class="flex flex-col gap-y-3">
Expand Down
17 changes: 7 additions & 10 deletions src/components/blog/BlogListItem.astro
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
---
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
const author = await getEntry(blog.author)
const tags = await getEntries(blog.tags)
---

<li
class="block bg-white rounded-xl p-5 border max-sm:rounded-none max-sm:border-0 space-y-1"
>
<li class="block bg-white rounded-xl p-5 border space-y-1">
<a href={`/blog/articles/${slug}`}>
<h2 class="px-2 text-xl font-bold line-clamp-2 hover:underline">
{blog.title}
</h2>
</a>
<TagList tags={tags} />
<TagSmallList tags={tags} />
<div class="px-2 flex gap-3 text-gray-600">
<div class="hover:underline">
<a href={`/blog/authors/${author.id}`} class="flex gap-2 items-center"
Expand Down
16 changes: 16 additions & 0 deletions src/components/blog/RelatedBlogSection.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
import type { CollectionEntry } from 'astro:content'
import Section from '@/components/common/Section.astro'
import BlogList from '@/components/blog/BlogList.astro'
interface Props {
blogs: CollectionEntry<'blogs'>[]
}
const { blogs } = Astro.props
---

<Section background="secondary">
<Fragment slot="title">関連記事</Fragment>
<BlogList blogs={blogs} sorted />
</Section>
21 changes: 9 additions & 12 deletions src/components/blog/tag/TagList.astro
Original file line number Diff line number Diff line change
@@ -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
---

<ul class="flex flex-wrap gap-2">
<ul class="flex sm:flex-row flex-col flex-wrap gap-3">
{
tags.map((tag) => (
<li>
<a
href={`/blog/tags/${tag.id}`}
class="flex items-center gap-1 bg-gray-100 hover:bg-gray-200 text-sm text-justify p-1 rounded-full"
>
<TagIcon tagImage={tag.data.image} size={18} />
<p>{tag.data.name}</p>
</a>
</li>
<TagListItemCard
{...tag.data}
id={tag.id}
articleCount={tag.articleCount}
/>
))
}
</ul>
24 changes: 24 additions & 0 deletions src/components/blog/tag/TagListItemCard.astro
Original file line number Diff line number Diff line change
@@ -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
---

<li
class="block sm:basis-[calc((100%-0.75rem)/2)] bg-white rounded-xl border space-y-1 group"
>
<a class="w-full h-full p-4 flex items-center" href={`/blog/tags/${id}`}>
<TagIcon tagImage={tag.image} size={40} />
<div class="px-2">
<h2 class="text-xl font-bold line-clamp-1 group-hover:underline">
{tag.name}
</h2>
<p class="text-sm text-gray-600">記事数 : {articleCount}</p>
</div>
</a>
</li>
17 changes: 17 additions & 0 deletions src/components/blog/tag/TagListSection.astro
Original file line number Diff line number Diff line change
@@ -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
---

<Section background="secondary">
<Fragment slot="title">{title}</Fragment>
<TagList tags={tags} />
</Section>
25 changes: 25 additions & 0 deletions src/components/blog/tag/TagSmallList.astro
Original file line number Diff line number Diff line change
@@ -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
---

<ul class="flex flex-wrap gap-2">
{
tags.map((tag) => (
<li>
<a
href={`/blog/tags/${tag.id}`}
class="flex items-center gap-1 bg-gray-100 hover:bg-gray-200 text-sm text-justify p-1 rounded-full"
>
<TagIcon tagImage={tag.data.image} size={18} />
<p>{tag.data.name}</p>
</a>
</li>
))
}
</ul>
34 changes: 34 additions & 0 deletions src/components/layout/nav/BlogNav.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
const current = Astro.url.pathname
const navList: { target: string; title: string }[] = [
{
target: '/blog',
title: '記事一覧',
},
{
target: '/blog/tags',
title: 'タグ一覧',
},
]
---

<div class="flex text-lg px-9 pt-1 gap-3 flex-nowrap">
{
navList.map(({ target, title }) => (
<a
href={target}
class:list={[
'text-gray-700 hover:text-black whitespace-nowrap',
{
'after:block after:bg-primary after:h-1 after:rounded-t-lg':
current === target,
'pb-1': current !== target,
},
]}
>
{title}
</a>
))
}
</div>
110 changes: 110 additions & 0 deletions src/content/_blog-statistics.ts
Original file line number Diff line number Diff line change
@@ -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<TagStatistics>
export async function getTagStatistics(id: TagId): Promise<TagStatistics[TagId]>
export async function getTagStatistics(id?: TagId) {
await analyze()
if (tagStatistics === null || blogStatistics === null) return unreachable()

return id === undefined ? tagStatistics : tagStatistics[id]
}
2 changes: 2 additions & 0 deletions src/layouts/BlogLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -38,6 +39,7 @@ const ogImageUrl = new URL(image, Astro.site).toString()
headerTitle="BLOG"
rootPath="/blog"
/>
<BlogNav />
<div class="flex-grow bg-dot bg-dot-secondary">
<slot />
</div>
Expand Down
Loading

0 comments on commit 698eb72

Please sign in to comment.