📖
記事一覧をテーブルビュー風に
記事一覧をデータベーステーブルビュー風のシンプルな表示にします。
ヘッダーのアイコンには astro-icon を使用しています。
💡
細かいところまでチェックできていないので表示崩れが発生する場合があります。
変更するファイル
編集する箇所が多いです。コードをまるごとコピーして、ファイルの中身を全て入れ替えてください。
- pages/index.astro
- page/posts/page/[page].astro
- page/posts/tag/[tag].astro
- page/posts/tag/[tag]/page/[page].astro
- components/PostTitle.astro
- layouts/Layout.astro
- css/styles/blog.module.css
index.astro
pages/index.astro
---
import { NUMBER_OF_POSTS_PER_PAGE } from '../server-constants.ts'
import {
getPosts,
getRankedPosts,
getAllTags,
getNumberOfPages,
} from '../lib/notion/client.ts'
import Layout from '../layouts/Layout.astro'
import NoContents from '../components/NoContents.astro'
import PostDate from '../components/PostDate.astro'
import PostTags from '../components/PostTags.astro'
import PostTitle from '../components/PostTitle.astro'
import Pagination from '../components/Pagination.astro'
import BlogPostsLink from '../components/BlogPostsLink.astro'
import BlogTagsLink from '../components/BlogTagsLink.astro'
import styles from '../styles/blog.module.css'
import { Icon } from 'astro-icon'
const [posts, rankedPosts, tags, numberOfPages] = await Promise.all([
getPosts(NUMBER_OF_POSTS_PER_PAGE),
getRankedPosts(),
getAllTags(),
getNumberOfPages(),
])
---
<Layout>
<div slot="main" class={styles.main}>
<div class="db-view">
<div class="view-list view-head">
<div class="view-title">
<Icon name="ri:character-recognition-fill" />Title
</div>
<div class="view-tags">
<Icon name="ph:tag" />Category
</div>
<div class="view-date">
<Icon name="uiw:date" />Date
</div>
</div>
{
posts.length === 0 ? (
<NoContents contents={posts} />
) : (
posts.map((post) => (
<div className={styles.post} key={post.Slug}>
<div class="view-list">
<div class="view-title">
<a href={"/posts/" + post.Slug}>
<PostTitle post={post} />
</a>
</div>
<div class="view-tags">
<PostTags post={post} />
</div>
<div class="view-date">
<PostDate post={post} />
</div>
</div>
</div>
))
)
}
</div>
<footer>
<Pagination currentPage={1} numberOfPages={numberOfPages} />
</footer>
</div>
<div slot="aside" class={styles.aside}>
<BlogPostsLink heading="Recommended" posts={rankedPosts} />
<BlogTagsLink heading="Categories" tags={tags} />
</div>
</Layout>
<style>
.db-view {
white-space: nowrap;
overflow: scroll hidden;
&::-webkit-scrollbar {
background: transparent;
}
> div:last-child {
border-bottom: 1px solid #eee;
}
}
.view-list {
display: flex;
height: 3rem;
.view-title,
.view-tags,
.view-date {
border-top: 1px solid #eee;
display: inline-flex;
align-items: center;
padding: 0 1rem;
}
.view-tags {
min-width: 150px;
border-right: 1px solid #eee;
}
.view-title {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
border-right: 1px solid #eee;
position: relative;
}
.view-date {
min-width: 120px;
}
&:not(.view-head) {
.view-title:active,
.view-tags:active,
.view-date:active {
background: #f0f6fd;
border: 2px solid #8cbaeb;
}
.view-title:hover:after {
position: absolute;
display: block;
right: 1.5rem;
content: "読む";
box-shadow: 0 0 4px #ccc;
width: 3rem;
height: 1.8rem;
line-height: 1.8rem;
cursor: pointer;
font-size: smaller;
text-align:center;
border-radius: 4px;
background: rgba(255,255,255,.9);
}
}
}
.view-list .view-title a {
display: block;
position: absolute;
top: 0;
left: 42px;
width: 100%;
height: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
svg {
min-width: 1.1rem;
height: 1.1rem;
margin-right: 0.5rem;
opacity: 0.6;
}
@media (width <= 959px) {
.view-list {
.view-title {
min-width: 280px;
padding: 4px 0;
white-space: wrap;
line-height: 1.5rem;
}
&:not(.view-head) {
height: 4rem;
}
}
}
</style>
page.astro
page/posts/page/[page].astro
---
import {
getPostsByPage,
getRankedPosts,
getAllTags,
getNumberOfPages,
getPostsByPage,
} from '../../../lib/notion/client.ts'
import Layout from '../../../layouts/Layout.astro'
import NoContents from '../../../components/NoContents.astro'
import PostDate from '../../../components/PostDate.astro'
import PostTags from '../../../components/PostTags.astro'
import PostTitle from '../../../components/PostTitle.astro'
import Pagination from '../../../components/Pagination.astro'
import BlogPostsLink from '../../../components/BlogPostsLink.astro'
import BlogTagsLink from '../../../components/BlogTagsLink.astro'
import styles from '../../../styles/blog.module.css'
import { Icon } from 'astro-icon'
export async function getStaticPaths() {
const numberOfPages = await getNumberOfPages()
let params = []
for (let i = 2; i <= numberOfPages; i++) {
params.push({ params: { page: i.toString() } })
}
return params
}
const { page } = Astro.params
const [posts, rankedPosts, tags, numberOfPages] = await Promise.all([
getPostsByPage(parseInt(page, 10)),
getRankedPosts(),
getAllTags(),
getNumberOfPages(),
])
---
<Layout title={`Posts ${page}/${numberOfPages}`} path={`/posts/page/${page}`}>
<div slot="main" class={styles.main}>
<header>
<div class="page-container">{page}/{numberOfPages}</div>
</header>
<div slot="main" class={styles.main}>
<div class="db-view">
<div class="view-list view-head">
<div class="view-title">
<Icon name="ri:character-recognition-fill" />Title
</div>
<div class="view-tags">
<Icon name="ph:tag" />Category
</div>
<div class="view-date">
<Icon name="uiw:date" />Date
</div>
</div>
{
posts.length === 0 ? (
<NoContents contents={posts} />
) : (
posts.map((post) => (
<div className={styles.post} key={post.Slug}>
<div class="view-list">
<div class="view-title">
<a href={"/posts/" + post.Slug}>
<PostTitle post={post} />
</a>
</div>
<div class="view-tags">
<PostTags post={post} />
</div>
<div class="view-date">
<PostDate post={post} />
</div>
</div>
</div>
))
)
}
</div>
</div>
<footer>
<Pagination
currentPage={parseInt(page, 10)}
numberOfPages={numberOfPages}
/>
</footer>
</div>
<div slot="aside" class={styles.aside}>
<BlogPostsLink heading="Recommended" posts={rankedPosts} />
<BlogTagsLink heading="Categories" tags={tags} />
</div>
</Layout>
<style>
.page-container {
margin: 0;
line-height: 1.3;
font-size: 1.1rem;
font-weight: normal;
}
.db-view {
white-space: nowrap;
overflow: scroll hidden;
&::-webkit-scrollbar {
background: transparent;
}
> div:last-child {
border-bottom: 1px solid #eee;
}
}
.view-list {
display: flex;
height: 3rem;
.view-title,
.view-tags,
.view-date {
border-top: 1px solid #eee;
display: inline-flex;
align-items: center;
padding: 0 1rem;
}
.view-tags {
min-width: 150px;
border-right: 1px solid #eee;
}
.view-title {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
border-right: 1px solid #eee;
position: relative;
}
.view-date {
min-width: 120px;
}
&:not(.view-head) {
.view-title:active,
.view-tags:active,
.view-date:active {
background: #f0f6fd;
border: 2px solid #8cbaeb;
}
.view-title:hover:after {
position: absolute;
display: block;
right: 1.5rem;
content: "読む";
box-shadow: 0 0 4px #ccc;
width: 3rem;
height: 1.8rem;
line-height: 1.8rem;
cursor: pointer;
font-size: smaller;
text-align:center;
border-radius: 4px;
background: rgba(255,255,255,.9);
}
}
}
.view-list .view-title a {
display: block;
position: absolute;
top: 0;
left: 42px;
width: 100%;
height: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
svg {
min-width: 1.1rem;
height: 1.1rem;
margin-right: 0.5rem;
opacity: 0.6;
}
@media (width <= 959px) {
.view-list {
.view-title {
min-width: 280px;
padding: 4px 0;
white-space: wrap;
line-height: 1.5rem;
}
&:not(.view-head) {
height: 4rem;
}
}
}
</style>
tag.astro
page/posts/tag/[tag].astro
---
import type { SelectProperty } from '../../../lib/interfaces.ts'
import { NUMBER_OF_POSTS_PER_PAGE } from '../../../server-constants.ts'
import {
getPostsByTag,
getRankedPosts,
getAllTags,
getNumberOfPagesByTag,
} from '../../../lib/notion/client.ts'
import Layout from '../../../layouts/Layout.astro'
import NoContents from '../../../components/NoContents.astro'
import PostDate from '../../../components/PostDate.astro'
import PostTags from '../../../components/PostTags.astro'
import PostTitle from '../../../components/PostTitle.astro'
import Pagination from '../../../components/Pagination.astro'
import BlogPostsLink from '../../../components/BlogPostsLink.astro'
import BlogTagsLink from '../../../components/BlogTagsLink.astro'
import styles from '../../../styles/blog.module.css'
import '../../../styles/notion-color.css'
import { Icon } from 'astro-icon'
export async function getStaticPaths() {
const allTags = await getAllTags()
return allTags.map((tag: SelectProperty) => ({ params: { tag: tag.name } }))
}
const { tag } = Astro.params
const [posts, rankedPosts, tags, numberOfPages] = await Promise.all([
getPostsByTag(tag, NUMBER_OF_POSTS_PER_PAGE),
getRankedPosts(),
getAllTags(),
getNumberOfPagesByTag(tag),
])
const currentTag = posts[0].Tags.find((t) => t.name === tag)
---
<Layout title={`Posts in ${tag}`} path={`/posts/tag/${tag}`}>
<div slot="main" class={styles.main}>
<header>
<div class="tag-container">
<span class={`tag ${currentTag.color}`}>{tag}</span>
</div>
</header>
<div class="db-view">
<div class="view-list view-head">
<div class="view-title">
<Icon name="ri:character-recognition-fill" />Title
</div>
<div class="view-tags">
<Icon name="ph:tag" />Category
</div>
<div class="view-date">
<Icon name="uiw:date" />Date
</div>
</div>
{
posts.length === 0 ? (
<NoContents contents={posts} />
) : (
posts.map((post) => (
<div className={styles.post} key={post.Slug}>
<div class="view-list">
<div class="view-title">
<a href={"/posts/" + post.Slug}>
<PostTitle post={post} />
</a>
</div>
<div class="view-tags">
<PostTags post={post} />
</div>
<div class="view-date">
<PostDate post={post} />
</div>
</div>
</div>
))
)
}
</div>
<footer>
<Pagination tag={tag} currentPage={1} numberOfPages={numberOfPages} />
</footer>
</div>
<div slot="aside" class={styles.aside}>
<BlogPostsLink heading="Recommended" posts={rankedPosts} />
<BlogTagsLink heading="Categories" tags={tags} />
</div>
</Layout>
<style>
.tag-container {
margin: 0;
line-height: 1.3;
font-size: 1.2rem;
font-weight: normal;
span.tag {
border-radius: 4px;
padding: 3px 9px;
background: var(--tag-bg-light-gray);
}
}
.db-view {
white-space: nowrap;
overflow: scroll hidden;
&::-webkit-scrollbar {
background: transparent;
}
> div:last-child {
border-bottom: 1px solid #eee;
}
}
.view-list {
display: flex;
height: 3rem;
.view-title,
.view-tags,
.view-date {
border-top: 1px solid #eee;
display: inline-flex;
align-items: center;
padding: 0 1rem;
}
.view-tags {
min-width: 150px;
border-right: 1px solid #eee;
}
.view-title {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
border-right: 1px solid #eee;
position: relative;
}
.view-date {
min-width: 120px;
}
&:not(.view-head) {
.view-title:active,
.view-tags:active,
.view-date:active {
background: #f0f6fd;
border: 2px solid #8cbaeb;
}
.view-title:hover:after {
position: absolute;
display: block;
right: 1.5rem;
content: "読む";
box-shadow: 0 0 4px #ccc;
width: 3rem;
height: 1.8rem;
line-height: 1.8rem;
cursor: pointer;
font-size: smaller;
text-align:center;
border-radius: 4px;
background: rgba(255,255,255,.9);
}
}
}
.view-list .view-title a {
display: block;
position: absolute;
top: 0;
left: 42px;
width: 100%;
height: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
svg {
min-width: 1.1rem;
height: 1.1rem;
margin-right: 0.5rem;
opacity: 0.6;
}
@media (width <= 959px) {
.view-list {
.view-title {
min-width: 280px;
padding: 4px 0;
white-space: wrap;
line-height: 1.5rem;
}
&:not(.view-head) {
height: 4rem;
}
}
}
</style>
tag/page.astro
page/posts/tag/[tag]/page/[page].astro
---
import type { SelectProperty } from '../../../../../lib/interfaces.ts'
import {
getPostsByTagAndPage,
getRankedPosts,
getAllTags,
getNumberOfPagesByTag,
} from '../../../../../lib/notion/client.ts'
import Layout from '../../../../../layouts/Layout.astro'
import NoContents from '../../../../../components/NoContents.astro'
import PostDate from '../../../../../components/PostDate.astro'
import PostTags from '../../../../../components/PostTags.astro'
import PostTitle from '../../../../../components/PostTitle.astro'
import Pagination from '../../../../../components/Pagination.astro'
import BlogPostsLink from '../../../../../components/BlogPostsLink.astro'
import BlogTagsLink from '../../../../../components/BlogTagsLink.astro'
import styles from '../../../../../styles/blog.module.css'
import '../../../../../styles/notion-color.css'
import { Icon } from 'astro-icon'
export async function getStaticPaths() {
const allTags = await getAllTags()
let params = []
await Promise.all(
allTags.map((tag: SelectProperty) => {
return getNumberOfPagesByTag(tag.name).then((numberOfPages: number) => {
for (let i = 2; i <= numberOfPages; i++) {
params.push({ params: { tag: tag.name, page: i.toString() } })
}
})
})
)
return params
}
const { tag, page } = Astro.params
const [posts, rankedPosts, tags, numberOfPages] = await Promise.all([
getPostsByTagAndPage(tag, parseInt(page, 10)),
getRankedPosts(),
getAllTags(),
getNumberOfPagesByTag(tag),
])
const currentTag = posts[0].Tags.find((t) => t.name === tag)
---
<Layout
title={`Posts in ${tag} ${page}/${numberOfPages}`}
path={`/posts/tag/${tag}/page/${page}`}
>
<div slot="main" class={styles.main}>
<header>
<div class="tag-container">
<span class={`tag ${currentTag.color}`}>{tag}</span>
{page}/{numberOfPages}
</div>
</header>
<div slot="main" class={styles.main}>
<div class="db-view">
<div class="view-list view-head">
<div class="view-title">
<Icon name="ri:character-recognition-fill" />Title
</div>
<div class="view-tags">
<Icon name="ph:tag" />Category
</div>
<div class="view-date">
<Icon name="uiw:date" />Date
</div>
</div>
{
posts.length === 0 ? (
<NoContents contents={posts} />
) : (
posts.map((post) => (
<div className={styles.post} key={post.Slug}>
<div class="view-list">
<div class="view-title">
<a href={"/posts/" + post.Slug}>
<PostTitle post={post} />
</a>
</div>
<div class="view-tags">
<PostTags post={post} />
</div>
<div class="view-date">
<PostDate post={post} />
</div>
</div>
</div>
))
)
}
</div>
</div>
<footer>
<Pagination tag={tag} currentPage={1} numberOfPages={numberOfPages} />
</footer>
</div>
<div slot="aside" class={styles.aside}>
<BlogPostsLink heading="Recommended" posts={rankedPosts} />
<BlogTagsLink heading="Categories" tags={tags} />
</div>
</Layout>
<style>
.tag-container {
margin: 0;
line-height: 1.3;
font-size: 1.2rem;
font-weight: normal;
}
.tag-container span.tag {
border-radius: 4px;
padding: 3px 9px;
background: var(--tag-bg-light-gray);
margin-right: .5rem;
}
.db-view {
white-space: nowrap;
overflow: scroll hidden;
&::-webkit-scrollbar {
background: transparent;
}
> div:last-child {
border-bottom: 1px solid #eee;
}
}
.view-list {
display: flex;
height: 3rem;
.view-title,
.view-tags,
.view-date {
border-top: 1px solid #eee;
display: inline-flex;
align-items: center;
padding: 0 1rem;
}
.view-tags {
min-width: 150px;
border-right: 1px solid #eee;
}
.view-title {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
border-right: 1px solid #eee;
position: relative;
}
.view-date {
min-width: 120px;
}
&:not(.view-head) {
.view-title:active,
.view-tags:active,
.view-date:active {
background: #f0f6fd;
border: 2px solid #8cbaeb;
}
.view-title:hover:after {
position: absolute;
display: block;
right: 1.5rem;
content: "読む";
box-shadow: 0 0 4px #ccc;
width: 3rem;
height: 1.8rem;
line-height: 1.8rem;
cursor: pointer;
font-size: smaller;
text-align:center;
border-radius: 4px;
background: rgba(255,255,255,.9);
}
}
}
.view-list .view-title a {
display: block;
position: absolute;
top: 0;
left: 42px;
width: 100%;
height: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
svg {
min-width: 1.1rem;
height: 1.1rem;
margin-right: 0.5rem;
opacity: 0.6;
}
@media (width <= 959px) {
.view-list {
.view-title {
min-width: 280px;
padding: 4px 0;
white-space: wrap;
line-height: 1.5rem;
}
&:not(.view-head) {
height: 4rem;
}
}
}
</style>
PostTitle.astro
components/PostTitle.astro
---
import { Post } from '../lib/interfaces.ts'
import { getPostLink } from '../lib/blog-helpers.ts'
export interface Props {
post: Post
enableLink: boolean
}
const { post, enableLink = true } = Astro.props
let title = post.Title
---
<h2 class="post-title">
{
enableLink ? (
<a href={getPostLink(post.Slug)}>
{post.Icon && post.Icon.Type === 'emoji' ? (
<>
<span>{post.Icon.Emoji}</span>
{title}
</>
) : post.Icon && post.Icon.Type === 'external' ? (
<>
<img src={post.Icon.Url} alt="Post title icon" />
{title}
</>
) : (
<>
<span>📄</span>{title}
</>
)}
</a>
) : (
<>
{post.Icon && post.Icon.Type === 'emoji' ? (
<>
<span>{post.Icon.Emoji}</span>
{title}
</>
) : post.Icon && post.Icon.Type === 'external' ? (
<>
<img src={post.Icon.Url} alt="Post title icon" />
{title}
</>
) : (
title
)}
</>
)
}
</h2>
<style>
.post-title {
padding: 0.2rem 0;
font-size: 1.8rem;
font-weight: 700;
color: var(--fg);
display: inline-flex;
}
.post-title span {
padding-right: .25rem;
}
.post-title a {
font-size: 1rem;
color: inherit;
font-weight: normal;
}
.post-title span,
.post-title img {
display: inline-block;
margin-right: 0.2em;
}
.post-title span {
font-size: 1.2em;
}
.post-title img {
width: 1.3em;
height: 1.3em;
vertical-align: sub;
}
@media (max-width: 959px) {
.post-title {
font-size: 1.6rem;
}
.post-title a {
font-size: 1rem;
}
}
</style>
Layout.astro
layouts/Layout.astro
6ヶ所を置換
@media (max-width: 640px)
=>
@media (max-width: 959px)
blog.module.css
css/styles/blog.module.css
.main header {
padding: 0 0 20px;
}
.main footer {
/* border-top: 1px dashed #888; */
margin: 0 auto;
padding: 40px 0 0;
}
@media (max-width: 640px) {
.main footer {
margin: 0 auto 40px;
}
}
.post {
/* margin: 0 auto 40px; */
}
.post footer {
margin-top: 0.5rem;
padding: 0;
border: 0;
}