Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions src/components/BlogCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Badge } from '@/components/ui/badge';
import member from '@/data/member';
import type { CollectionEntry } from 'astro:content';
import MemberIcon from '@/components/MemberIcon';
import { cn } from '@/lib/utils';

type Props = {
blog: CollectionEntry<'blog'>;
Expand All @@ -13,10 +14,20 @@ const BlogCard = ({ blog, color }: Props) => {
const author = member.find(m => m.name === blog.data.author);

return (
<Card className="bg-transparent transition hover:opacity-70">
<a href={`/blog/${blog.slug}`} className="block p-6">
<Card className="relative bg-transparent transition hover:opacity-70">
<a
href={`/blog/${blog.slug}`}
className="absolute block size-full"
aria-label={`${blog.data.title} を読む`}
>
&nbsp;
</a>
<div className="relative z-10 p-6">
<CardTitle
className={`text-xl md:text-2xl ${color === 'white' && 'text-white'}`}
className={cn(
'text-xl md:text-2xl',
color === 'white' && 'text-white'
)}
>
{blog.data.title}
</CardTitle>
Expand All @@ -29,27 +40,34 @@ const BlogCard = ({ blog, color }: Props) => {
<div className="flex flex-wrap gap-1">
{blog.data.tags.map(tag => {
return (
<Badge
variant="outline"
className={`${color === 'white' && 'text-white'}`}
key={tag}
>
#{tag}
</Badge>
<a href={`/blog/tag/${encodeURIComponent(tag)}`} key={tag}>
<Badge
variant="outline"
className={cn(
'transition hover:bg-gray-100',
color === 'white' && 'text-white'
)}
>
#{tag}
</Badge>
</a>
);
})}
</div>
{author && (
<CardDescription className="flex items-center">
Author:&nbsp;
<span className="inline-flex items-center gap-2">
<a
href={`/blog/author/${author.name}`}
className="inline-flex items-center gap-2 transition hover:underline"
>
{author.name}
<MemberIcon memberName={author.name} />
</span>
</a>
</CardDescription>
)}
</div>
</a>
</div>
</Card>
);
};
Expand Down
30 changes: 30 additions & 0 deletions src/components/BlogFilter.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
import getBlog from '@/lib/getBlog';
import { Badge } from '@/components/ui/badge';
const blogs = await getBlog();
---

<div class="flex items-baseline gap-2">
<span class="font-medium">Author:</span>
<div class="flex flex-wrap gap-2">
{
Array.from(new Set(blogs.map(blog => blog.data.author))).map(author => (
<a href={`/blog/author/${author}`}>
<Badge>{author}</Badge>
</a>
))
}
</div>
</div>
<div class="my-4 flex items-baseline gap-2">
<span class="self-center font-medium">Tag:</span>
<div class="flex flex-wrap gap-2">
{
Array.from(new Set(blogs.map(blog => blog.data.tags).flat())).map(tag => (
<a href={`/blog/tag/${encodeURIComponent(tag)}`}>
<Badge>#{tag}</Badge>
</a>
))
}
</div>
</div>
14 changes: 14 additions & 0 deletions src/lib/getTags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getCollection } from 'astro:content';

const getTags = async () => {
const blogs = await getCollection('blog');
// NOTE: 特殊文字を含むタグがある場合に備えてエンコード
const allTags = blogs.flatMap(blog =>
blog.data.tags.map(tag => encodeURIComponent(tag))
);
const uniqueTags = [...new Set(allTags)].sort();

return uniqueTags;
};

export default getTags;
2 changes: 2 additions & 0 deletions src/pages/blog.astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import BlogCard from '@/components/BlogCard';
import Breadcrumb from '@/components/Breadcrumb.astro';
import getBlog from '@/lib/getBlog';
import siteInfo from '@/data/siteInfo';
import BlogFilter from '@/components/BlogFilter.astro';
const title = 'Blog';
const blogs = await getBlog();
const description = `${siteInfo.appName} Memberが執筆したBlogの一覧ページです。直近では「${blogs[0].data.title}」、「${blogs[1].data.title}」、「${blogs[2].data.title}」を投稿しています。`;
Expand All @@ -15,6 +16,7 @@ const description = `${siteInfo.appName} Memberが執筆したBlogの一覧ペ
<Layout title={title} description={description}>
<PageTitle title={title} />
<Section>
<BlogFilter />
<div class="flex flex-col gap-3">
{blogs.map(blog => <BlogCard blog={blog} client:load />)}
</div>
Expand Down
52 changes: 52 additions & 0 deletions src/pages/blog/author/[name].astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
import '@/styles/globals.css';
import Layout from '@/layouts/Layout.astro';
import PageTitle from '@/components/PageTitle.astro';
import Section from '@/components/Section.astro';
import BlogCard from '@/components/BlogCard';
import Breadcrumb from '@/components/Breadcrumb.astro';
import BlogFilter from '@/components/BlogFilter.astro';
import { getCollection } from 'astro:content';
import member from '@/data/member';

export async function getStaticPaths() {
const authors = member.map(m => m.name);

return authors.map(author => ({
params: { name: author },
}));
}

const { name } = Astro.params;
const blogs = await getCollection('blog');
const filteredBlogs = blogs
.filter(blog => blog.data.author === name)
.sort((a, b) => {
const dateA = new Date(a.data.pubDate).getTime();
const dateB = new Date(b.data.pubDate).getTime();
return dateB - dateA;
});

const title = `Blog - ${name}`;
const description = `${name}が執筆したBlogの一覧ページです。`;
---

<Layout title={title} description={description}>
<PageTitle title={title} />
<Section>
<BlogFilter />
{
filteredBlogs.length > 0 ? (
<div class="flex flex-col gap-3">
{filteredBlogs.map(blog => (
<BlogCard blog={blog} client:load />
))}
</div>
) : (
<p class="text-center text-gray-500">記事がありません。</p>
)
}
</Section>

<Breadcrumb items={[{ name: 'Blog', href: '/blog' }, { name: name }]} />
</Layout>
57 changes: 57 additions & 0 deletions src/pages/blog/tag/[tag].astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
import '@/styles/globals.css';
import Layout from '@/layouts/Layout.astro';
import PageTitle from '@/components/PageTitle.astro';
import Section from '@/components/Section.astro';
import BlogCard from '@/components/BlogCard';
import Breadcrumb from '@/components/Breadcrumb.astro';
import BlogFilter from '@/components/BlogFilter.astro';
import { getCollection } from 'astro:content';
import getTags from '@/lib/getTags';

export async function getStaticPaths() {
const tags = await getTags();

return tags.map(tag => ({
// NOTE: tagに特殊文字が含まれる場合に備えてエンコード
params: { tag },
}));
}

const { tag } = Astro.params;
const blogs = await getCollection('blog');
const filteredBlogs = blogs
.filter(blog =>
blog.data.tags.map(tag => encodeURIComponent(tag)).includes(tag)
)
.sort((a, b) => {
const dateA = new Date(a.data.pubDate).getTime();
const dateB = new Date(b.data.pubDate).getTime();
return dateB - dateA;
});

// NOTE: tagをデコードして表示用に戻す
const decodedTag = decodeURIComponent(tag);
const title = `Blog - #${decodedTag}`;
const description = `#${decodedTag}タグが付けられたBlogの一覧ページです。`;
---

<Layout title={title} description={description}>
<PageTitle title={title} />
<Section>
<BlogFilter />
{
filteredBlogs.length > 0 ? (
<div class="flex flex-col gap-3">
{filteredBlogs.map(blog => (
<BlogCard blog={blog} client:load />
))}
</div>
) : (
<p class="text-center text-gray-500">記事がありません。</p>
)
}
</Section>

<Breadcrumb items={[{ name: 'Blog', href: '/blog' }, { name: `#${tag}` }]} />
</Layout>