From e0a8e1b595dd5a636f49edce7c08b2fd12c1e452 Mon Sep 17 00:00:00 2001
From: nsfisis
Date: Sat, 21 Jun 2025 15:03:29 +0900
Subject: feat(blog/nuldoc): implement pagination
---
vhosts/blog/nginx.conf | 6 +-
vhosts/blog/nuldoc-src/commands/build.ts | 20 +-
vhosts/blog/nuldoc-src/components/Pagination.tsx | 45 ++
vhosts/blog/nuldoc-src/config.ts | 1 +
vhosts/blog/nuldoc-src/generators/post_list.ts | 48 +-
vhosts/blog/nuldoc-src/pages/PostListPage.tsx | 37 +-
vhosts/blog/nuldoc.toml | 1 +
vhosts/blog/public/404.html | 2 +-
vhosts/blog/public/about/index.html | 2 +-
vhosts/blog/public/index.html | 2 +-
vhosts/blog/public/posts/2/index.html | 222 +++++++
.../posts/2021-03-05/my-first-post/index.html | 2 +-
.../posts/2021-03-30/phperkaigi-2021/index.html | 2 +-
.../index.html | 2 +-
.../python-unbound-local-error/index.html | 2 +-
.../ruby-detect-running-implementation/index.html | 2 +-
.../ruby-then-keyword-and-case-in/index.html | 2 +-
.../rust-where-are-primitive-types-from/index.html | 2 +-
.../index.html | 2 +-
.../vim-swap-order-of-selected-lines/index.html | 2 +-
.../2022-04-09/phperkaigi-2022-tokens/index.html | 2 +-
.../index.html | 2 +-
.../posts/2022-05-01/phperkaigi-2022/index.html | 2 +-
.../php-conference-okinawa-code-golf/index.html | 2 +-
.../index.html | 2 +-
.../index.html | 2 +-
.../phperkaigi-2023-unused-token-quiz-1/index.html | 2 +-
.../setup-server-for-this-site/index.html | 2 +-
.../phperkaigi-2023-unused-token-quiz-2/index.html | 2 +-
.../phperkaigi-2023-unused-token-quiz-3/index.html | 2 +-
.../rewrite-this-blog-generator/index.html | 2 +-
.../index.html | 2 +-
.../2023-04-04/phperkaigi-2023-report/index.html | 2 +-
.../2023-06-25/phpconfuk-2023-report/index.html | 2 +-
.../compile-php-runtime-to-wasm/index.html | 2 +-
.../index.html | 2 +-
.../public/posts/2023-12-03/isucon-13/index.html | 2 +-
.../posts/2023-12-31/2023-reflections/index.html | 2 +-
.../index.html | 2 +-
.../index.html | 2 +-
.../2024-02-10/yapcjapan-2024-report/index.html | 2 +-
.../2024-02-22/phpkansai-2024-report/index.html | 2 +-
.../2024-03-17/phperkaigi-2024-report/index.html | 2 +-
.../posts/2024-03-20/my-bucket-list/index.html | 2 +-
.../phpcon-odawara-2024-report/index.html | 2 +-
.../pipefail-option-in-gitlab-ci-cd/index.html | 2 +-
.../index.html | 2 +-
.../2024-05-11/phpconkagawa-2024-report/index.html | 2 +-
.../2024-06-19/scalamatsuri-2024-report/index.html | 2 +-
.../reparojson-fix-only-json-formatter/index.html | 2 +-
.../index.html | 2 +-
.../posts/2024-09-28/mncore-challenge-1/index.html | 2 +-
.../posts/2024-12-04/cohackpp-report/index.html | 2 +-
.../posts/2024-12-33/2024-reflections/index.html | 2 +-
.../phperkaigi-2023-tokens-q1/index.html | 2 +-
.../index.html | 2 +-
.../phpcon-nagoya-2025-report/index.html | 2 +-
.../index.html | 2 +-
.../http-1-1-send-multiple-same-headers/index.html | 2 +-
.../trick-2025-most-ruby-on-ruby-award/index.html | 2 +-
.../index.html | 2 +-
.../make-tiny-self-hosted-c-compiler/index.html | 2 +-
.../public/posts/2025-06-14/baba-is-you/index.html | 2 +-
vhosts/blog/public/posts/3/index.html | 222 +++++++
vhosts/blog/public/posts/4/index.html | 222 +++++++
vhosts/blog/public/posts/5/index.html | 222 +++++++
vhosts/blog/public/posts/6/index.html | 100 ++++
vhosts/blog/public/posts/index.html | 662 +--------------------
.../2023-01-18/phpstudy-tokyo-148/index.html | 2 +-
.../2023-02-15/phpstudy-tokyo-149/index.html | 2 +-
.../2023-03-15/phpstudy-tokyo-150/index.html | 2 +-
.../slides/2023-03-24/phperkaigi-2023/index.html | 2 +-
.../2023-03-25/phperkaigi-2023-tokens/index.html | 2 +-
.../2023-04-12/phpstudy-tokyo-151/index.html | 2 +-
.../2023-06-21/phpstudy-tokyo-153/index.html | 2 +-
.../2023-06-23/phpconfuk-2023-eve/index.html | 2 +-
.../2023-07-26/phpstudy-tokyo-154/index.html | 2 +-
.../2023-08-24/phpstudy-tokyo-155/index.html | 2 +-
.../2023-10-25/phpstudy-tokyo-157/index.html | 2 +-
.../2024-01-24/phpstudy-tokyo-160/index.html | 2 +-
.../slides/2024-03-08/phperkaigi-2024/index.html | 2 +-
.../public/slides/2024-03-15/ya8-2024/index.html | 2 +-
.../2024-04-13/phpcon-odawara-2024/index.html | 2 +-
.../2024-04-25/phpstudy-tokyo-163/index.html | 2 +-
.../2024-07-18/phpstudy-tokyo-166/index.html | 2 +-
.../2024-10-30/phpstudy-tokyo-169/index.html | 2 +-
.../public/slides/2024-11-30/cohackpp/index.html | 2 +-
.../2025-02-22/phpcon-nagoya-2025/index.html | 2 +-
.../slides/2025-03-23/phperkaigi-2025/index.html | 2 +-
.../2025-04-12/phpcon-odawara-2025/index.html | 2 +-
vhosts/blog/public/slides/index.html | 2 +-
vhosts/blog/public/style.css | 51 ++
vhosts/blog/public/tags/c/index.html | 2 +-
vhosts/blog/public/tags/ci-cd/index.html | 2 +-
vhosts/blog/public/tags/cohackpp/index.html | 2 +-
vhosts/blog/public/tags/composer/index.html | 2 +-
vhosts/blog/public/tags/conference/index.html | 2 +-
vhosts/blog/public/tags/cpp/index.html | 2 +-
vhosts/blog/public/tags/cpp17/index.html | 2 +-
vhosts/blog/public/tags/game/index.html | 2 +-
vhosts/blog/public/tags/gitlab/index.html | 2 +-
vhosts/blog/public/tags/go/index.html | 2 +-
vhosts/blog/public/tags/http/index.html | 2 +-
vhosts/blog/public/tags/index.html | 2 +-
vhosts/blog/public/tags/isucon/index.html | 2 +-
vhosts/blog/public/tags/macos/index.html | 2 +-
.../blog/public/tags/mncore-challenge/index.html | 2 +-
vhosts/blog/public/tags/neovim/index.html | 2 +-
vhosts/blog/public/tags/note-to-self/index.html | 2 +-
vhosts/blog/public/tags/ouj/index.html | 2 +-
vhosts/blog/public/tags/perl/index.html | 2 +-
vhosts/blog/public/tags/php/index.html | 2 +-
vhosts/blog/public/tags/phpcon-nagoya/index.html | 2 +-
vhosts/blog/public/tags/phpcon-odawara/index.html | 2 +-
vhosts/blog/public/tags/phpconfuk/index.html | 2 +-
vhosts/blog/public/tags/phpconkagawa/index.html | 2 +-
vhosts/blog/public/tags/phpconokinawa/index.html | 2 +-
vhosts/blog/public/tags/phperkaigi/index.html | 2 +-
vhosts/blog/public/tags/phpkansai/index.html | 2 +-
vhosts/blog/public/tags/phpstudy-tokyo/index.html | 2 +-
vhosts/blog/public/tags/piet/index.html | 2 +-
vhosts/blog/public/tags/python/index.html | 2 +-
vhosts/blog/public/tags/python3/index.html | 2 +-
vhosts/blog/public/tags/ruby/index.html | 2 +-
vhosts/blog/public/tags/ruby3/index.html | 2 +-
vhosts/blog/public/tags/rubykaigi/index.html | 2 +-
vhosts/blog/public/tags/rust/index.html | 2 +-
vhosts/blog/public/tags/scala/index.html | 2 +-
vhosts/blog/public/tags/scalamatsuri/index.html | 2 +-
vhosts/blog/public/tags/trick/index.html | 2 +-
vhosts/blog/public/tags/vim/index.html | 2 +-
vhosts/blog/public/tags/wasm/index.html | 2 +-
vhosts/blog/public/tags/wireguard/index.html | 2 +-
vhosts/blog/public/tags/ya8/index.html | 2 +-
vhosts/blog/public/tags/yaml/index.html | 2 +-
vhosts/blog/public/tags/yapc/index.html | 2 +-
vhosts/blog/public/tags/zsh/index.html | 2 +-
vhosts/blog/static/style.css | 51 ++
138 files changed, 1376 insertions(+), 780 deletions(-)
create mode 100644 vhosts/blog/nuldoc-src/components/Pagination.tsx
create mode 100644 vhosts/blog/public/posts/2/index.html
create mode 100644 vhosts/blog/public/posts/3/index.html
create mode 100644 vhosts/blog/public/posts/4/index.html
create mode 100644 vhosts/blog/public/posts/5/index.html
create mode 100644 vhosts/blog/public/posts/6/index.html
(limited to 'vhosts')
diff --git a/vhosts/blog/nginx.conf b/vhosts/blog/nginx.conf
index 7cc8bda1..eb1d9136 100644
--- a/vhosts/blog/nginx.conf
+++ b/vhosts/blog/nginx.conf
@@ -18,7 +18,10 @@ server {
error_page 404 /404.html;
- # Old URLs.
+ # Redirect to canonical path.
+ rewrite ^/posts/1/?$ /posts/ permanent;
+
+ # Old URL patterns.
rewrite ^/posts/(my-first-post)/?$ /posts/2021-03-05/$1/ permanent;
rewrite ^/posts/(phperkaigi-2021)/?$ /posts/2021-03-30/$1/ permanent;
rewrite ^/posts/(cpp-you-can-use-keywords-in-attributes)/?$ /posts/2021-10-02/$1/ permanent;
@@ -33,5 +36,6 @@ server {
# I mistakenly wrote 2023 in the URL instead of 2024.
rewrite ^/posts/2023-01-10/(neovim-insert-namespace-declaration-to-empty-php-file)/?$ /posts/2024-01-10/$1/ permanent;
+ # Renamed posts.
rewrite ^/posts/2024-03-20/todos-in-my-life/?$ /posts/2024-03-20/my-bucket-list/ permanent;
}
diff --git a/vhosts/blog/nuldoc-src/commands/build.ts b/vhosts/blog/nuldoc-src/commands/build.ts
index 52aca1f7..3f765441 100644
--- a/vhosts/blog/nuldoc-src/commands/build.ts
+++ b/vhosts/blog/nuldoc-src/commands/build.ts
@@ -14,7 +14,7 @@ import {
getPostPublishedDate,
PostPage,
} from "../generators/post.ts";
-import { generatePostListPage } from "../generators/post_list.ts";
+import { generatePostListPages } from "../generators/post_list.ts";
import { generateSlidePage, SlidePage } from "../generators/slide.ts";
import { generateSlideListPage } from "../generators/slide_list.ts";
import { generateTagPage, TagPage } from "../generators/tag.ts";
@@ -70,10 +70,22 @@ async function parsePosts(
}
async function buildPostListPage(posts: PostPage[], config: Config) {
- const postListPage = await generatePostListPage(posts, config);
- await writePage(postListPage, config);
+ // Sort posts by published date (newest first)
+ const sortedPosts = [...posts].sort((a, b) => {
+ const ta = dateToString(getPostPublishedDate(a));
+ const tb = dateToString(getPostPublishedDate(b));
+ if (ta > tb) return -1;
+ if (ta < tb) return 1;
+ return 0;
+ });
+
+ const postListPages = await generatePostListPages(sortedPosts, config);
+ for (const page of postListPages) {
+ await writePage(page, config);
+ }
+
const postFeedPage = await generateFeedPageFromEntries(
- postListPage.href,
+ "/posts/",
"posts",
`投稿一覧|${config.blog.siteName}`,
posts,
diff --git a/vhosts/blog/nuldoc-src/components/Pagination.tsx b/vhosts/blog/nuldoc-src/components/Pagination.tsx
new file mode 100644
index 00000000..5527c924
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/components/Pagination.tsx
@@ -0,0 +1,45 @@
+type Props = {
+ currentPage: number;
+ totalPages: number;
+ basePath: string;
+};
+
+export default function Pagination(
+ { currentPage, totalPages, basePath }: Props,
+) {
+ if (totalPages <= 1) {
+ return ;
+ }
+
+ const prevPage = currentPage > 1 ? currentPage - 1 : null;
+ const nextPage = currentPage < totalPages ? currentPage + 1 : null;
+
+ const prevHref = prevPage === 1 ? basePath : `${basePath}${prevPage}/`;
+ const nextHref = `${basePath}${nextPage}/`;
+
+ return (
+
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/config.ts b/vhosts/blog/nuldoc-src/config.ts
index 5a07896f..adcb5632 100644
--- a/vhosts/blog/nuldoc-src/config.ts
+++ b/vhosts/blog/nuldoc-src/config.ts
@@ -18,6 +18,7 @@ const ConfigSchema = z.object({
fqdn: z.string(),
siteName: z.string(),
siteCopyrightYear: z.number(),
+ postsPerPage: z.number().default(10),
tagLabels: z.record(z.string(), z.string()),
}),
});
diff --git a/vhosts/blog/nuldoc-src/generators/post_list.ts b/vhosts/blog/nuldoc-src/generators/post_list.ts
index 67a4b996..b05f7ee6 100644
--- a/vhosts/blog/nuldoc-src/generators/post_list.ts
+++ b/vhosts/blog/nuldoc-src/generators/post_list.ts
@@ -6,18 +6,58 @@ import { PostPage } from "./post.ts";
export type PostListPage = Page;
-export async function generatePostListPage(
+export async function generatePostListPages(
posts: PostPage[],
config: Config,
+): Promise {
+ const postsPerPage = config.blog.postsPerPage;
+ const totalPages = Math.ceil(posts.length / postsPerPage);
+ const pages: PostListPage[] = [];
+
+ for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) {
+ const pagePosts = posts.slice(
+ pageIndex * postsPerPage,
+ (pageIndex + 1) * postsPerPage,
+ );
+
+ const page = await generatePostListPage(
+ pagePosts,
+ config,
+ pageIndex + 1,
+ totalPages,
+ );
+
+ pages.push(page);
+ }
+
+ return pages;
+}
+
+async function generatePostListPage(
+ posts: PostPage[],
+ config: Config,
+ currentPage: number,
+ totalPages: number,
): Promise {
const html = await renderToDOM(
- PostListPage(posts, config),
+ PostListPage(
+ posts,
+ config,
+ currentPage,
+ totalPages,
+ ),
);
+ const destFilePath = currentPage === 1
+ ? "/posts/index.html"
+ : `/posts/${currentPage}/index.html`;
+
+ const href = currentPage === 1 ? "/posts/" : `/posts/${currentPage}/`;
+
return {
root: html,
renderer: "html",
- destFilePath: "/posts/index.html",
- href: "/posts/",
+ destFilePath,
+ href,
};
}
diff --git a/vhosts/blog/nuldoc-src/pages/PostListPage.tsx b/vhosts/blog/nuldoc-src/pages/PostListPage.tsx
index c1c5214c..054955e6 100644
--- a/vhosts/blog/nuldoc-src/pages/PostListPage.tsx
+++ b/vhosts/blog/nuldoc-src/pages/PostListPage.tsx
@@ -1,22 +1,28 @@
import GlobalFooter from "../components/GlobalFooter.tsx";
import GlobalHeader from "../components/GlobalHeader.tsx";
import PageLayout from "../components/PageLayout.tsx";
+import Pagination from "../components/Pagination.tsx";
import PostPageEntry from "../components/PostPageEntry.tsx";
import { Config } from "../config.ts";
-import { dateToString } from "../revision.ts";
-import { getPostPublishedDate, PostPage } from "../generators/post.ts";
+import { PostPage } from "../generators/post.ts";
export default function PostListPage(
posts: PostPage[],
config: Config,
+ currentPage: number,
+ totalPages: number,
) {
const pageTitle = "投稿一覧";
+ const pageInfoSuffix = ` (${currentPage}ページ目)`;
+ const metaTitle = `${pageTitle}${pageInfoSuffix}|${config.blog.siteName}`;
+ const metaDescription = `投稿した記事の一覧${pageInfoSuffix}`;
+
return (
@@ -24,15 +30,22 @@ export default function PostListPage(
- {pageTitle}
+ {pageTitle}{pageInfoSuffix}
- {Array.from(posts).sort((a, b) => {
- const ta = dateToString(getPostPublishedDate(a));
- const tb = dateToString(getPostPublishedDate(b));
- if (ta > tb) return -1;
- if (ta < tb) return 1;
- return 0;
- }).map((post) => )}
+
+
+
+ {posts.map((post) => )}
+
+