@fullstackdatasolutions/articles 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +18 -3
- package/dist/index.cjs +11 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +11 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ArticlesPage.tsx +10 -2
- package/src/__tests__/ArticlesPage.test.tsx +17 -0
- package/src/__tests__/useArticles.test.ts +28 -0
- package/src/useArticles.ts +10 -5
package/package.json
CHANGED
package/src/ArticlesPage.tsx
CHANGED
|
@@ -152,8 +152,16 @@ function renderSection(
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
export function ArticlesPage({
|
|
156
|
-
|
|
155
|
+
export function ArticlesPage({
|
|
156
|
+
config,
|
|
157
|
+
initialArticles,
|
|
158
|
+
initialCategories,
|
|
159
|
+
}: Readonly<{
|
|
160
|
+
config: ArticlesConfig
|
|
161
|
+
initialArticles?: Article[]
|
|
162
|
+
initialCategories?: CategoryInfo[]
|
|
163
|
+
}>) {
|
|
164
|
+
const state = useArticles(initialArticles, initialCategories)
|
|
157
165
|
const layout = config.layout ?? DEFAULT_LAYOUT
|
|
158
166
|
const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE
|
|
159
167
|
const categoriesPageSize = config.categoriesPageSize ?? DEFAULT_CATEGORIES_PAGE_SIZE
|
|
@@ -273,6 +273,23 @@ describe('ArticlesPage', () => {
|
|
|
273
273
|
await waitFor(() => expect(screen.getByText('Browse by Category')).toBeInTheDocument())
|
|
274
274
|
})
|
|
275
275
|
|
|
276
|
+
it('renders real article links synchronously when given server-fetched initialArticles, with no client fetch', () => {
|
|
277
|
+
// Simulates the SSR path: a server component fetches articles via getAllArticles()
|
|
278
|
+
// and passes them down, so Googlebot's first request already contains real hrefs.
|
|
279
|
+
;(globalThis.fetch as jest.Mock).mockReturnValue(new Promise(() => {}))
|
|
280
|
+
render(
|
|
281
|
+
<ArticlesPage config={{ ...baseConfig, layout: ['latest'] }} initialArticles={mockArticles} />
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
expect(screen.getByText('Latest Articles')).toBeInTheDocument()
|
|
285
|
+
expect(screen.queryByText('Loading articles...')).not.toBeInTheDocument()
|
|
286
|
+
expect(screen.getByRole('link', { name: 'Second Article' })).toHaveAttribute(
|
|
287
|
+
'href',
|
|
288
|
+
'/articles/article-2'
|
|
289
|
+
)
|
|
290
|
+
expect(globalThis.fetch).not.toHaveBeenCalled()
|
|
291
|
+
})
|
|
292
|
+
|
|
276
293
|
it('uses pageSize from config', async () => {
|
|
277
294
|
const manyArticles = Array.from({ length: 10 }, (_, i) => ({
|
|
278
295
|
...mockArticles[0],
|
|
@@ -190,6 +190,34 @@ describe('useArticles', () => {
|
|
|
190
190
|
})
|
|
191
191
|
})
|
|
192
192
|
|
|
193
|
+
describe('with server-seeded initial data', () => {
|
|
194
|
+
it('starts populated and not loading, and skips the mount fetch', () => {
|
|
195
|
+
;(globalThis.fetch as jest.Mock).mockReturnValue(new Promise(() => {}))
|
|
196
|
+
const mockCategories = [
|
|
197
|
+
{ name: 'Campaigns', slug: 'campaigns', count: 2, featuredImage: '/x.jpg' },
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
const { result } = renderHook(() => useArticles(mockArticles, mockCategories))
|
|
201
|
+
|
|
202
|
+
expect(result.current.loading).toBe(false)
|
|
203
|
+
expect(result.current.articles).toHaveLength(3)
|
|
204
|
+
expect(result.current.categories).toEqual(mockCategories)
|
|
205
|
+
expect(globalThis.fetch).not.toHaveBeenCalled()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('still fetches when a search is performed after seeding', async () => {
|
|
209
|
+
;(globalThis.fetch as jest.Mock).mockResolvedValue({
|
|
210
|
+
ok: true,
|
|
211
|
+
json: async () => ({ articles: [mockArticles[0]] }),
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const { result } = renderHook(() => useArticles(mockArticles, []))
|
|
215
|
+
act(() => result.current.handleSearch(''))
|
|
216
|
+
|
|
217
|
+
await waitFor(() => expect(globalThis.fetch).toHaveBeenCalledWith('/api/articles'))
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
193
221
|
it('refetches all articles when handleSearch is called with empty string', async () => {
|
|
194
222
|
;(globalThis.fetch as jest.Mock).mockResolvedValue({
|
|
195
223
|
ok: true,
|
package/src/useArticles.ts
CHANGED
|
@@ -12,12 +12,15 @@ export interface UseArticlesReturn {
|
|
|
12
12
|
handleSearch: (query: string) => void
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function useArticles(
|
|
15
|
+
export function useArticles(
|
|
16
|
+
initialArticles?: Article[],
|
|
17
|
+
initialCategories?: CategoryInfo[]
|
|
18
|
+
): UseArticlesReturn {
|
|
16
19
|
const [searchQuery, setSearchQuery] = useState('')
|
|
17
20
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
18
|
-
const [articles, setArticles] = useState<Article[]>([])
|
|
19
|
-
const [categories, setCategories] = useState<CategoryInfo[]>([])
|
|
20
|
-
const [loading, setLoading] = useState(
|
|
21
|
+
const [articles, setArticles] = useState<Article[]>(initialArticles ?? [])
|
|
22
|
+
const [categories, setCategories] = useState<CategoryInfo[]>(initialCategories ?? [])
|
|
23
|
+
const [loading, setLoading] = useState(initialArticles === undefined)
|
|
21
24
|
const [error, setError] = useState<string | null>(null)
|
|
22
25
|
|
|
23
26
|
const fetchArticles = useCallback(async (query: string) => {
|
|
@@ -66,7 +69,9 @@ export function useArticles(): UseArticlesReturn {
|
|
|
66
69
|
}, [])
|
|
67
70
|
|
|
68
71
|
useEffect(() => {
|
|
69
|
-
fetchArticles('')
|
|
72
|
+
if (initialArticles === undefined) fetchArticles('')
|
|
73
|
+
// Seeded data came from the server render; skip the redundant client fetch on mount.
|
|
74
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
70
75
|
}, [fetchArticles])
|
|
71
76
|
|
|
72
77
|
const handleSearch = useCallback(
|