Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Changed

- **SEO-Friendly Project and Event Filters**: Replace client-side-only filters with server-rendered, shareable query-string URLs for project category/language and event type; add explicit empty states, request-time event status calculation, and page-specific bilingual meta descriptions across list, policy, and blog pages

## [0.4.0] - 2026-07-17

### Added
Expand Down
2 changes: 2 additions & 0 deletions src/layouts/MarkdownLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ export interface Props {
frontmatter: {
title?: string;
titleEn?: string;
description?: string;
};
}

const { frontmatter } = Astro.props;

const metadata: MetaData = {
title: frontmatter?.title,
description: frontmatter?.description,
};
---

Expand Down
2 changes: 2 additions & 0 deletions src/pages/[...blog]/[...page].astro
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const currentPage = page.currentPage ?? 1;

const metadata = {
title: `Blog${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
description:
'讯飞开源博客 — Articles from the iFLYTEK open source community: technical deep dives, project updates, release notes and community news.',
robots: {
index: blogListRobots?.index && currentPage === 1,
follow: blogListRobots?.follow,
Expand Down
16 changes: 9 additions & 7 deletions src/pages/[...blog]/[category]/[...page].astro
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,6 @@ const { page, category } = Astro.props as Props;

const currentPage = page.currentPage ?? 1;

const metadata = {
title: `Category '${category.title}' ${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
robots: {
index: blogCategoryRobots?.index,
follow: blogCategoryRobots?.follow,
},
};
const categoryMeta: Record<string, { titleZh: string; titleEn: string; description: string; descriptionEn: string }> = {
tech: {
titleZh: '技术博客',
Expand All @@ -48,6 +41,15 @@ const cat = categoryMeta[category.slug] || {
description: `${category.title} 相关文章`,
descriptionEn: `${category.title} related posts`,
};

const metadata = {
title: `Category '${category.title}' ${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
description: `${cat.description} — ${cat.descriptionEn}`,
robots: {
index: blogCategoryRobots?.index,
follow: blogCategoryRobots?.follow,
},
};
---

<Layout metadata={metadata}>
Expand Down
1 change: 1 addition & 0 deletions src/pages/[...blog]/[tag]/[...page].astro
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const currentPage = page.currentPage ?? 1;

const metadata = {
title: `Posts by tag '${tag.title}'${currentPage > 1 ? ` — Page ${currentPage} ` : ''}`,
description: `标签 '${tag.title}' 下的文章 — Posts tagged '${tag.title}' from the iFLYTEK open source blog.`,
robots: {
index: blogTagRobots?.index,
follow: blogTagRobots?.follow,
Expand Down
2 changes: 2 additions & 0 deletions src/pages/adopters.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { getCollection } from 'astro:content';

const metadata = {
title: 'Adopters',
description:
'谁在使用讯飞开源项目 — Companies and organizations adopting iFLYTEK open source projects in production, and how to add your organization to the list.',
};

const adopters = await getCollection('adopter');
Expand Down
9 changes: 8 additions & 1 deletion src/pages/cla.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
import MarkdownLayout from '~/layouts/MarkdownLayout.astro';
---

<MarkdownLayout frontmatter={{ title: '贡献者许可协议', titleEn: 'Contributor License Agreement' }}>
<MarkdownLayout
frontmatter={{
title: '贡献者许可协议',
titleEn: 'Contributor License Agreement',
description:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add en description?

'科大讯飞开源社区贡献者许可协议(CLA)— The iFLYTEK open source Contributor License Agreement covering copyright and patent license grants for contributions.',
}}
>
<!-- Chinese Content -->
<div class="i18n-zh">
<p>
Expand Down
2 changes: 2 additions & 0 deletions src/pages/contribute.astro
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const prerender = false;

const metadata = {
title: 'Contribute',
description:
'参与贡献科大讯飞开源社区 — How to contribute to iFLYTEK open source: find good first issues, submit pull requests, sign the CLA and join the community.',
};

// Load projects count
Expand Down
96 changes: 38 additions & 58 deletions src/pages/events.astro
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
---
import Layout from '~/layouts/PageLayout.astro';
import { getCollection } from 'astro:content';
import { events } from '~/data/events';
import { events as eventsData } from '~/data/events';

export const prerender = false;

const metadata = {
title: 'Events',
description:
'科大讯飞开源社区活动 — iFLYTEK open source community events: meetups, hackathons, workshops and conferences, with the latest community news.',
};

// Auto-determine status based on event date vs current date
const now = new Date();
const todayStr = now.toISOString().split('T')[0];
events.forEach((event) => {
event.status = event.date === todayStr ? 'ongoing' : event.date > todayStr ? 'upcoming' : 'past';
});

// Sort by date descending
events.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
const events = eventsData
.map((event) => ({
...event,
status: (event.date === todayStr ? 'ongoing' : event.date > todayStr ? 'upcoming' : 'past') as
'ongoing' | 'upcoming' | 'past',
}))
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());

const typeLabels: Record<string, string> = {
conference: 'Conference',
Expand All @@ -36,6 +41,15 @@ const typeColors: Record<string, string> = {
// Get unique types for filter
const eventTypes = [...new Set(events.map((e) => e.type))];

// URL-driven filter state so filtered views are crawlable and shareable
const typeParam = Astro.url.searchParams.get('type') || 'all';
const activeType = (eventTypes as string[]).includes(typeParam) ? typeParam : 'all';
const filteredEvents = activeType === 'all' ? events : events.filter((e) => e.type === activeType);

const activeChipClass = 'bg-primary-600 text-white';
const inactiveChipClass =
'bg-gray-100 text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700';

// Blog posts for news section
const posts = await getCollection('post');
const newsPosts = posts
Expand Down Expand Up @@ -75,31 +89,37 @@ const newsPosts = posts
<div class="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
<!-- Filter Tabs -->
<div class="mb-10 flex flex-wrap items-center justify-center gap-3">
<button
class="event-filter-btn rounded-full bg-primary-600 px-5 py-2 text-sm font-medium text-white"
data-type="all"
<a
href="/events"
class:list={[
'rounded-full px-5 py-2 text-sm font-medium',
activeType === 'all' ? activeChipClass : inactiveChipClass,
]}
>
<span data-lang-zh={`全部 (${events.length})`} data-lang-en={`All (${events.length})`}
>全部 ({events.length})</span
>
</button>
</a>
{
eventTypes.map((type) => (
<button
class="event-filter-btn rounded-full bg-gray-100 px-5 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
data-type={type}
<a
href={`/events?type=${type}`}
class:list={[
'rounded-full px-5 py-2 text-sm font-medium',
activeType === type ? activeChipClass : inactiveChipClass,
]}
>
{typeLabels[type]} ({events.filter((e) => e.type === type).length})
</button>
</a>
))
}
</div>

<!-- Timeline -->
<div class="space-y-1">
{
events.map((event, index) => (
<div class="event-item relative flex gap-4 sm:gap-8" data-type={event.type}>
filteredEvents.map((event, index) => (
<div class="relative flex gap-4 sm:gap-8">
{/* Timeline line & dot */}
<div class="flex flex-col items-center">
<div class="relative z-10 flex h-10 w-10 flex-shrink-0 items-center justify-center">
Expand All @@ -114,7 +134,7 @@ const newsPosts = posts
]}
/>
</div>
{index < events.length - 1 && <div class="w-px flex-1 bg-gray-200 dark:bg-gray-800" />}
{index < filteredEvents.length - 1 && <div class="w-px flex-1 bg-gray-200 dark:bg-gray-800" />}
</div>

{/* Event Card */}
Expand Down Expand Up @@ -251,44 +271,4 @@ const newsPosts = posts
</section>
)
}

<!-- Filter Script -->
<script is:inline>
(function () {
function setupFilters() {
const buttons = document.querySelectorAll('.event-filter-btn');
const items = document.querySelectorAll('.event-item');

buttons.forEach((btn) => {
btn.addEventListener('click', () => {
const type = btn.dataset.type;

// Update button styles
buttons.forEach((b) => {
b.classList.remove('bg-primary-600', 'text-white');
b.classList.add('bg-gray-100', 'text-gray-700', 'dark:bg-gray-800', 'dark:text-gray-300');
});
btn.classList.add('bg-primary-600', 'text-white');
btn.classList.remove('bg-gray-100', 'text-gray-700', 'dark:bg-gray-800', 'dark:text-gray-300');

// Filter items
items.forEach((item) => {
const itemType = item.dataset.type;
if (type === 'all' || itemType === type) {
item.style.display = '';
} else {
item.style.display = 'none';
}
});
});
});
}

if (document.readyState !== 'loading') {
setupFilters();
} else {
document.addEventListener('DOMContentLoaded', setupFilters);
}
})();
</script>
</Layout>
2 changes: 2 additions & 0 deletions src/pages/landscape.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { getProjectsWithStats } from '~/utils/project-stats';

const metadata = {
title: 'Landscape',
description:
'讯飞开源全景图 — The iFLYTEK open source landscape: an at-a-glance map of all projects organized by category, from AI Agent platforms to developer tools.',
};

const projects = await getProjectsWithStats();
Expand Down
Loading
Loading