mirror of
https://github.com/10h30/blog-balodeplao.git
synced 2026-05-12 23:21:16 +09:00
feat: implement taxonomy data for categories, destinations, and tags with updated routing (#2)
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
export const DEFAULT_PAGE_SIZE = 6;
|
||||
|
||||
export interface PageMeta {
|
||||
data: unknown[];
|
||||
total: number;
|
||||
currentPage: number;
|
||||
lastPage: number;
|
||||
url: { prev?: string; next?: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns paginated static paths using WordPress-style /page/{n} URLs.
|
||||
*
|
||||
* @param items Full sorted list of items to paginate.
|
||||
* @param basePath Base URL without trailing slash, e.g. "/blog" or "/category/travel".
|
||||
* @param pageSize Number of items per page (default: DEFAULT_PAGE_SIZE).
|
||||
* @returns Array of { pageParam, page } objects where pageParam is the route segment
|
||||
* (undefined for page 1, "page/N" for N≥2) and page is a PageMeta object.
|
||||
* Callers should map these into Astro's { params, props } format.
|
||||
*/
|
||||
export function buildPaginatedPaths<T>(
|
||||
items: T[],
|
||||
basePath: string,
|
||||
pageSize = DEFAULT_PAGE_SIZE,
|
||||
) {
|
||||
const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
|
||||
|
||||
return Array.from({ length: totalPages }, (_, i) => {
|
||||
const pageNum = i + 1;
|
||||
const data = items.slice(i * pageSize, (i + 1) * pageSize);
|
||||
|
||||
const prev =
|
||||
pageNum === 1
|
||||
? undefined
|
||||
: pageNum === 2
|
||||
? basePath
|
||||
: `${basePath}/page/${pageNum - 1}`;
|
||||
const next =
|
||||
pageNum === totalPages ? undefined : `${basePath}/page/${pageNum + 1}`;
|
||||
|
||||
return {
|
||||
// page param: undefined for page 1 (matches the base URL), "page/N" for N≥2
|
||||
pageParam: pageNum === 1 ? undefined : `page/${pageNum}`,
|
||||
page: {
|
||||
data,
|
||||
total: items.length,
|
||||
currentPage: pageNum,
|
||||
lastPage: totalPages,
|
||||
url: { prev, next },
|
||||
} as PageMeta & { data: T[] },
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import categoryData from "@/content/taxonomies/category.json";
|
||||
import destinationData from "@/content/taxonomies/destination.json";
|
||||
import tagData from "@/content/taxonomies/tag.json";
|
||||
|
||||
export type TaxonomyItem = {
|
||||
slug: string;
|
||||
name: string;
|
||||
parent?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type TaxonomyMap = Map<string, TaxonomyItem>;
|
||||
|
||||
export type TaxonomyType = "category" | "destination" | "tag";
|
||||
|
||||
function isTaxonomyItem(value: unknown): value is TaxonomyItem {
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
const v = value as Record<string, unknown>;
|
||||
return (
|
||||
typeof v.slug === "string" &&
|
||||
typeof v.name === "string" &&
|
||||
(v.parent === undefined || typeof v.parent === "string") &&
|
||||
(v.description === undefined || typeof v.description === "string")
|
||||
);
|
||||
}
|
||||
|
||||
function buildMap(data: TaxonomyItem[]): TaxonomyMap {
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error(`[taxonomy] expected an array but received ${typeof data}`);
|
||||
}
|
||||
return new Map(
|
||||
data.map((item, i) => {
|
||||
if (!isTaxonomyItem(item)) {
|
||||
throw new Error(
|
||||
`[taxonomy] invalid item at index ${i}: ${JSON.stringify(item)}`,
|
||||
);
|
||||
}
|
||||
return [item.slug, item];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const taxonomies: Record<TaxonomyType, TaxonomyMap> = {
|
||||
category: buildMap(categoryData as TaxonomyItem[]),
|
||||
destination: buildMap(destinationData as TaxonomyItem[]),
|
||||
tag: buildMap(tagData as TaxonomyItem[]),
|
||||
};
|
||||
|
||||
export function getTaxonomyMap(type: TaxonomyType): TaxonomyMap {
|
||||
if (!(type in taxonomies)) {
|
||||
throw new Error(
|
||||
`[taxonomy] unknown taxonomy type "${type}". Expected one of: ${Object.keys(taxonomies).join(", ")}`,
|
||||
);
|
||||
}
|
||||
return taxonomies[type];
|
||||
}
|
||||
|
||||
function buildHierarchicalPath(
|
||||
slug: string,
|
||||
map: TaxonomyMap,
|
||||
visited = new Set<string>(),
|
||||
): string {
|
||||
if (visited.has(slug)) return slug; // cycle protection
|
||||
visited.add(slug);
|
||||
const item = map.get(slug);
|
||||
if (!item?.parent) return slug;
|
||||
return `${buildHierarchicalPath(item.parent, map, visited)}/${slug}`;
|
||||
}
|
||||
|
||||
export function getTaxonomyPath(type: TaxonomyType, slug: string): string {
|
||||
return buildHierarchicalPath(slug, getTaxonomyMap(type));
|
||||
}
|
||||
|
||||
export function getTaxonomyItem(
|
||||
type: TaxonomyType,
|
||||
slug: string,
|
||||
): TaxonomyItem | undefined {
|
||||
return getTaxonomyMap(type).get(slug);
|
||||
}
|
||||
Reference in New Issue
Block a user