Site cover image

📖Ast-Rock Blog

Notion + astro なブログ

📘astro-notion-blog 記事一覧をデータベースビュー風のレイアウトにする

記事一覧をテーブルビュー風に

Image in a image block

記事一覧をデータベーステーブルビュー風のシンプルな表示にします。

ヘッダーのアイコンには astro-icon を使用しています。

💡

細かいところまでチェックできていないので表示崩れが発生する場合があります。

変更するファイル

編集する箇所が多いです。コードをまるごとコピーして、ファイルの中身を全て入れ替えてください。

  1. pages/index.astro
  2. page/posts/page/[page].astro
  3. page/posts/tag/[tag].astro
  4. page/posts/tag/[tag]/page/[page].astro
  5. components/PostTitle.astro
  6. layouts/Layout.astro
  7. 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;
}

アイコンを変えたい場合