Skip to content

SEO: server-render list pages, GET-based search, URL pagination (size=10), and SSR locale fallback#9

Open
Copilot wants to merge 7 commits into
mainfrom
copilot/seo-goodactionhub
Open

SEO: server-render list pages, GET-based search, URL pagination (size=10), and SSR locale fallback#9
Copilot wants to merge 7 commits into
mainfrom
copilot/seo-goodactionhub

Conversation

Copilot AI commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

PR-9 PR-9 PR-9 Powered by Pull Request Badge

This PR addresses SEO for the three list surfaces by moving rendering/filtering state into URL-driven server pages. It also standardizes pagination to URL parameters with a 10-item page size and visible page links at the end of each list.

  • Scope (issue requirements)

    • Convert list pages to server-rendered components.
    • Switch list search to same-page GET form submissions.
    • Add page query-param pagination in units of 10 with centered page links.
  • Server-rendered list pages

    • Reworked:
      • app/activities/page.tsx
      • app/restaurants/page.tsx
      • app/tutoring/page.tsx
    • Pages now read searchParams on the server, compute filtered datasets, and slice results by page.
    • app/activities/page.tsx now also parses Accept-Language on the server to choose zh-CN/en fallback copy for key list-page UI text.
  • URL-driven search + filtering

    • Replaced client-local search state with GET forms:
      • /activities?query=...
      • /restaurants?query=...&filter=...
      • /tutoring?query=...&tag=...
    • Filter/tag interactions now generate navigable links so state is crawlable/shareable.
  • Pagination behavior

    • Introduced shared pagination helpers in lib/pagination.ts (parsePage, getVisiblePages).
    • Added reusable components/Pager.tsx and applied it to all three list pages.
    • All three list pages now use:
      • PAGE_SIZE = 10
      • page query param
      • centered numbered links + prev/next controls
  • Data access alignment

    • Added fetchActivitiesCatalog() in lib/activities.ts and reused it in activities list/detail and /api/data path to keep fetch behavior consistent.
<form action="/tutoring" method="get">
  <input name="query" defaultValue={query} />
  {selectedTag && <input type="hidden" name="tag" value={selectedTag} />}
  <button type="submit">搜索</button>
</form>

// URL shape: /tutoring?query=数学&tag=小学&page=2

Human changes

  1. [refactor] simplify Source Code with MobX-i18n & MobX-RESTful-Shadcn
  2. [add] Traditional Chinese translation data
  3. [optimize] update Upstream packages

Copilot AI linked an issue Jun 6, 2026 that may be closed by this pull request
@TechQuery TechQuery added the feature New feature or request label Jun 6, 2026
Copilot AI changed the title [WIP] Implement SEO improvements for GoodActionHub SEO: server-render list pages, GET-based search, and URL pagination (size=10) Jun 6, 2026
Copilot AI requested a review from TechQuery June 6, 2026 23:58

@TechQuery TechQuery left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

先合并主分支最新代码,再通篇修改以下建议。

Comment thread lib/activities.ts Outdated
Comment thread app/activities/page.tsx
const { t } = useTranslation();
type PageSearchParams = Promise<{
page?: string;
query?: string;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
query?: string;
keywords?: string;

Comment thread app/activities/page.tsx Outdated
Comment thread app/activities/page.tsx Outdated
Comment thread app/activities/page.tsx Outdated
Comment thread app/restaurants/page.tsx Outdated
Comment on lines +51 to +53
if (page > 1) params.set('page', String(page));
const queryString = params.toString();
return queryString ? `/restaurants?${queryString}` : '/restaurants';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (page > 1) params.set('page', String(page));
const queryString = params.toString();
return queryString ? `/restaurants?${queryString}` : '/restaurants';
if (page > 1) params.set('page', String(page));
return (params + '') ? `/restaurants?${params}` : '/restaurants';

Comment thread app/restaurants/page.tsx Outdated
Comment thread app/restaurants/page.tsx Outdated
Comment thread app/restaurants/page.tsx Outdated
Comment on lines +190 to +192
pagedRestaurants.map((restaurant) => {
const types = getAccessibilityTypes(restaurant);
const isNavigating = navigationLoading === restaurant.name;
const markerUrl = `https://uri.amap.com/marker?address=${encodeURIComponent(restaurant.address)}&name=${encodeURIComponent(restaurant.name)}`;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pagedRestaurants.map((restaurant) => {
const types = getAccessibilityTypes(restaurant);
const isNavigating = navigationLoading === restaurant.name;
const markerUrl = `https://uri.amap.com/marker?address=${encodeURIComponent(restaurant.address)}&name=${encodeURIComponent(restaurant.name)}`;
pagedRestaurants.map((restaurant) => {
const types = getAccessibilityTypes(restaurant);
const { address, name } = restaurant;
const markerUrl = `https://uri.amap.com/marker?${new URLSearchParams(restaurant{ address, name })}`;

Comment thread app/tutoring/page.tsx Outdated

const courses = await fetchTutoringCatalog();

const allTags = Array.from(new Set(courses.flatMap((course) => course.tags)));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const allTags = Array.from(new Set(courses.flatMap((course) => course.tags)));
const allTags = [...new Set(courses.flatMap(({ tags }) => tags))];

Copilot AI changed the title SEO: server-render list pages, GET-based search, and URL pagination (size=10) SEO: server-render list pages, GET-based search, URL pagination (size=10), and SSR locale fallback Jun 7, 2026
Copilot AI requested a review from TechQuery June 7, 2026 21:25
[add] Traditional Chinese translation data
[optimize] update Upstream packages
@TechQuery TechQuery marked this pull request as ready for review June 8, 2026 00:54
Copilot AI review requested due to automatic review settings June 8, 2026 00:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to improve SEO and shareability of the three “list” surfaces by moving search/filter/pagination state into URL-driven server-rendered pages, and by adding SSR language selection/fallback for list-page UI text.

Changes:

  • Reworked /activities, /restaurants, and /tutoring list pages to be server components with GET-based search and URL pagination (page size = 10).
  • Introduced SSR language loading via mobx-i18n + new translation maps for zh-CN, zh-TW, and en-US.
  • Aligned activities data fetching by adding fetchActivitiesCatalog() and reusing it in the API route and activities pages.

Reviewed changes

Copilot reviewed 20 out of 22 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
public/locales/zh-TW/translation.json Adds Traditional Chinese i18next translation namespace data.
public/locales/zh-TW/common.json Adds Traditional Chinese i18next common namespace data.
package.json Updates scripts/deps; introduces shadcn-helper install hook and dependency upgrades.
lib/pagination.ts Adds shared page parsing + visible page calculation helpers.
lib/activities.ts Adds fetchActivitiesCatalog() to centralize activities fetch behavior.
i18n/zh-TW.ts Adds SSR translation map for Traditional Chinese.
i18n/zh-CN.ts Adds SSR translation map for Simplified Chinese.
i18n/en-US.ts Adds SSR translation map for English (US).
i18n/index.ts Adds SSR i18n store + cookie/header/query language loading.
i18n/config.ts Extends client i18next config to support zh-TW and formatting tweaks.
eslint.config.ts Adds React version setting and keeps existing TS-eslint rule override.
components/index.ini Declares a shadcn registry component to be installed (pager).
components.json Adds a custom shadcn registry URL.
app/tutoring/page.tsx Converts tutoring list to SSR with GET search + URL tag selection + pagination.
app/tutoring/[slug]/page.tsx Adds SSR i18n text usage in tutoring detail page.
app/restaurants/page.tsx Converts restaurants list to SSR with GET search + URL filter + pagination.
app/restaurants/[id]/page.tsx Adds SSR i18n text usage in restaurant detail page.
app/api/data/route.ts Uses fetchActivitiesCatalog() for consistent upstream fetch behavior.
app/activities/page.tsx Converts activities list to SSR with GET search + pagination and SSR locale loading.
app/activities/[id]/page.tsx Uses fetchActivitiesCatalog() + SSR i18n text usage in activity detail page.
.gitignore Ignores components/ui/ (shadcn UI components).
Comments suppressed due to low confidence (1)

app/api/data/route.ts:14

  • fetchActivitiesCatalog() now throws on upstream fetch errors, but the API route catches everything and returns HTTP 500. Previously, upstream fetch failures returned 502 (bad gateway), which more accurately signals an upstream dependency issue. Consider preserving 502 for fetch failures so monitoring/clients can distinguish upstream vs server errors.
export async function GET() {
  try {
    const externalData = await fetchActivitiesCatalog();
    const data = externalData.map(transformItem);
    return NextResponse.json(data);
  } catch (err) {
    console.error('Failed to fetch data from external API:', err);
    return NextResponse.json({ error: 'Failed to load data' }, { status: 500 });
  }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/pagination.ts
Comment on lines +1 to +4
export function parsePage(rawPage?: string): number {
const parsed = Number.parseInt(rawPage ?? '1', 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
}
Comment thread app/tutoring/page.tsx
Comment on lines +11 to +13
import { createI18nStore, loadSSRLanguage } from '@/i18n';
import { Pager } from '@/components/ui/mobx-restful-shadcn/pager';
import { parsePage } from '@/lib/pagination';
Comment thread app/restaurants/page.tsx
Comment on lines +5 to +9
import { createI18nStore, loadSSRLanguage } from '@/i18n';
import FoodAIDialog from '@/components/FoodAIDialog';
import SafeTranslation from '@/components/SafeTranslation';
import { Pager } from '@/components/ui/mobx-restful-shadcn/pager';
import { fetchBitesCatalog, BitesRestaurant } from '@/lib/bitesCatalog';
import { parsePage } from '@/lib/pagination';
Comment thread app/activities/page.tsx
Comment on lines +6 to +10
import { createI18nStore, loadSSRLanguage } from '@/i18n';
import { EventCard } from '@/components/EventCard';
import { FilterBar } from '@/components/FilterBar';
import { GitCodeIcon } from '@/components/icons/GitCodeIcon';
import { GitHubIcon } from '@/components/icons/GitHubIcon';

import { DeadlineItem, EventData } from '@/lib/data';
import { useEventStore } from '@/lib/store';
import Fuse from 'fuse.js';

import { DateTime } from 'luxon';
import Link from 'next/link';
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Pager } from '@/components/ui/mobx-restful-shadcn/pager';
Comment thread app/activities/page.tsx
Comment on lines 161 to +165
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-6 shadow-lg border border-white/20 mb-8">
<FilterBar />
<form action="/activities" method="get" className="flex gap-3">
<input
type="text"
name="query"
Comment thread package.json
Comment on lines 13 to 15
"start": "next start",
"test": "next typegen && lint-staged && tsc --noEmit",
"test": "next typegen && lint-staged && git add . && tsc --noEmit",
"knip": "knip",
Comment thread components.json
Comment on lines +6 to +8
"registries": {
"@mobx-restful-shadcn": "https://mobx-restful-shadcn.idea2.app/r/{name}.json"
},
Comment thread i18n/index.ts
Comment on lines +11 to +15
const i18nData = {
'zh-CN': zhCN,
'zh-TW': () => import('./zh-TW'),
'en-US': () => import('./en-US'),
};
Comment thread i18n/index.ts
Comment on lines +42 to +46
export const LanguageName: Record<LanguageCode, string> = {
'zh-CN': '简体中文',
'zh-TW': '繁體中文',
'en-US': 'English',
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

对 GoodActionHub 进行 SEO

3 participants