Skip to content

Commit 04fda6c

Browse files
committed
return placeholder routes for dynamic pages
1 parent c6e1034 commit 04fda6c

9 files changed

Lines changed: 61 additions & 101 deletions

File tree

.github/workflows/build-container.yml

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,10 @@ jobs:
7575
working-directory: ./TechStacks.Client
7676
run: npm install
7777

78-
# Generate static JSON data files for build-time SSG
79-
# This fetches posts, technologies, and stacks from the API and saves them
80-
# to src/data/*.json so the Next.js build doesn't need to call APIs
81-
# - name: Generate static data
82-
# if: steps.check_client.outputs.client_exists == 'true'
83-
# working-directory: ./TechStacks.Client
84-
# env:
85-
# API_URL: https://react.techstacks.io
86-
# run: npm run generate-data
78+
- name: Build client
79+
if: steps.check_client.outputs.client_exists == 'true'
80+
working-directory: ./TechStacks.Client
81+
run: npm run build
8782

8883
- name: Install x tool
8984
run: dotnet tool install -g x

TechStacks.Client/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
"dev": "node server.js",
99
"dtos": "npx get-dtos ts",
1010
"generate-data": "node scripts/generate-static-data.mjs",
11-
"prebuild:local": "npm run generate-data",
12-
"build": "next build && cp -r dist/* ../TechStacks/wwwroot/ && cp dist/_next/static/chunks/*.css ../TechStacks/wwwroot/css/app.css",
11+
"build": "next build && rm -rf ../TechStacks/wwwroot/ && cp -rf dist ../TechStacks/wwwroot && cp dist/_next/static/chunks/*.css ../TechStacks/wwwroot/css/app.css",
1312
"build:prod": "next build",
1413
"start": "next start",
1514
"lint": "next lint",

TechStacks.Client/src/app/posts/[id]/[slug]/PostDetailClient.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useEffect, useState } from 'react';
44
import Link from 'next/link';
5+
import { usePathname } from 'next/navigation';
56
import { formatDistanceToNow } from 'date-fns';
67
import { PrimaryButton } from '@servicestack/react';
78
import { useAuthorization } from '@/lib/hooks/useAuthorization';
@@ -11,7 +12,7 @@ import * as gateway from '@/lib/api/gateway';
1112
import { TechnologyTags } from '@/components/TechnologyTags';
1213
import { Avatar } from '@/components/ui/Avatar';
1314

14-
export default function PostDetailClient({ id, slug }: { id: string; slug: string }) {
15+
export default function PostDetailClient() {
1516
const { canEditPost, canDeleteComment } = useAuthorization();
1617
const { sessionInfo, isAuthenticated } = useAppStore();
1718
const [post, setPost] = useState<any>(null);
@@ -26,10 +27,22 @@ export default function PostDetailClient({ id, slug }: { id: string; slug: strin
2627
const [downVotedCommentIds, setDownVotedCommentIds] = useState<number[]>([]);
2728
const [localCommentPoints, setLocalCommentPoints] = useState<Record<number, number>>({});
2829

30+
const pathname = usePathname();
31+
const segments = pathname.split('/').filter(Boolean);
32+
const idSegment = segments[1]; // /posts/{id}/{slug}
33+
const slug = segments[2] ?? '';
34+
35+
const postId = idSegment ? parseInt(idSegment, 10) : NaN;
36+
2937
useEffect(() => {
3038
const loadPost = async () => {
39+
if (!postId || Number.isNaN(postId)) {
40+
setNotFound(true);
41+
setLoading(false);
42+
return;
43+
}
44+
3145
try {
32-
const postId = parseInt(id);
3346
const response = await gateway.getPost(postId);
3447
setPost(response.post);
3548
setComments(response.comments || []);
@@ -53,7 +66,7 @@ export default function PostDetailClient({ id, slug }: { id: string; slug: strin
5366
};
5467

5568
loadPost();
56-
}, [id, isAuthenticated]);
69+
}, [postId, isAuthenticated]);
5770

5871
const handleCommentVote = async (commentId: number, weight: number, e: React.MouseEvent) => {
5972
e.stopPropagation();
@@ -98,7 +111,7 @@ export default function PostDetailClient({ id, slug }: { id: string; slug: strin
98111
[commentId]: currentPoints + pointsDelta
99112
}));
100113

101-
await gateway.votePostComment(parseInt(id), commentId, newWeight);
114+
await gateway.votePostComment(postId, commentId, newWeight);
102115
} catch (err) {
103116
console.error('Failed to vote on comment:', err);
104117
}
@@ -112,12 +125,12 @@ export default function PostDetailClient({ id, slug }: { id: string; slug: strin
112125
}
113126

114127
try {
115-
await gateway.createPostComment(parseInt(id), newComment, replyToId || undefined);
128+
await gateway.createPostComment(postId, newComment, replyToId || undefined);
116129
setNewComment('');
117130
setReplyToId(null);
118131

119132
// Reload post to get updated comments
120-
const response = await gateway.getPost(parseInt(id));
133+
const response = await gateway.getPost(postId);
121134
setComments(response.comments || []);
122135
} catch (err) {
123136
console.error('Failed to create comment:', err);
@@ -128,12 +141,12 @@ export default function PostDetailClient({ id, slug }: { id: string; slug: strin
128141
if (!editContent.trim()) return;
129142

130143
try {
131-
await gateway.updatePostComment(commentId, parseInt(id), editContent);
144+
await gateway.updatePostComment(commentId, postId, editContent);
132145
setEditingCommentId(null);
133146
setEditContent('');
134147

135148
// Reload comments
136-
const response = await gateway.getPost(parseInt(id));
149+
const response = await gateway.getPost(postId);
137150
setComments(response.comments || []);
138151
} catch (err) {
139152
console.error('Failed to update comment:', err);
@@ -146,10 +159,10 @@ export default function PostDetailClient({ id, slug }: { id: string; slug: strin
146159
}
147160

148161
try {
149-
await gateway.deletePostComment(commentId, parseInt(id));
162+
await gateway.deletePostComment(commentId, postId);
150163

151164
// Reload comments
152-
const response = await gateway.getPost(parseInt(id));
165+
const response = await gateway.getPost(postId);
153166
setComments(response.comments || []);
154167
} catch (err) {
155168
console.error('Failed to delete comment:', err);
@@ -388,7 +401,7 @@ export default function PostDetailClient({ id, slug }: { id: string; slug: strin
388401
)}
389402
</h1>
390403
{canEditPost(post) && (
391-
<Link href={routes.postEdit(parseInt(id), slug)}>
404+
<Link href={routes.postEdit(postId, slug)}>
392405
<PrimaryButton className="ml-4">
393406
Edit
394407
</PrimaryButton>
Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,14 @@
11
import PostDetailClient from './PostDetailClient';
2-
import postsData from '@/data/posts.json';
32

4-
// Generate static pages for all posts from static data
3+
// For static export, generate a single placeholder page.
4+
// The ASP.NET Core backend routes all /posts/{id}/{slug} requests
5+
// to this placeholder, and the client loads the actual post by ID.
56
export async function generateStaticParams() {
6-
try {
7-
// Read from static JSON data generated at build time
8-
const posts = postsData.results || [];
9-
10-
console.log(`Generating ${posts.length} post pages from static data (generated: ${postsData.generated})`);
11-
12-
// Generate params for all posts
13-
return posts.map((post: any) => ({
14-
id: post.id.toString(),
15-
slug: post.slug,
16-
}));
17-
} catch (error) {
18-
console.error('Failed to load posts from static data:', error);
19-
// Fallback to placeholder if data is unavailable
20-
return [{ id: '0', slug: '_placeholder' }];
21-
}
7+
// Return a placeholder - the actual routing happens client-side
8+
return [{ id: '0', slug: '_placeholder' }];
229
}
2310

24-
export default async function PostDetailPage({
25-
params,
26-
}: {
27-
params: Promise<{ id: string; slug: string }>;
28-
}) {
29-
const { id, slug } = await params;
30-
31-
return <PostDetailClient id={id} slug={slug} />;
11+
export default function PostDetailPage() {
12+
return <PostDetailClient />;
3213
}
3314

TechStacks.Client/src/app/stacks/[slug]/TechStackDetailClient.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

33
import { useEffect, useState } from 'react';
44
import Link from 'next/link';
5+
import { usePathname } from 'next/navigation';
56
import routes from '@/lib/utils/routes';
67
import * as gateway from '@/lib/api/gateway';
78
import { useAuth, PrimaryButton } from '@servicestack/react';
89
import { useAppStore } from '@/lib/stores/useAppStore';
910
import { FavoriteButton } from '@/components/ui/FavoriteButton';
1011

11-
export default function TechStackDetailClient({ slug }: { slug: string }) {
12+
export default function TechStackDetailClient() {
13+
const pathname = usePathname();
14+
const segments = pathname.split('/').filter(Boolean);
15+
const slug = segments[1] ?? '';
1216
const [stack, setStack] = useState<any>(null);
1317
const [loading, setLoading] = useState(true);
1418
const { isAuthenticated } = useAuth();
Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,13 @@
11
import TechStackDetailClient from './TechStackDetailClient';
2-
import stacksData from '@/data/stacks.json';
32

4-
// Generate static pages for all tech stacks from static data
3+
// For static export, generate a single placeholder page.
4+
// The ASP.NET Core backend routes all /stacks/{slug} requests
5+
// to this placeholder, and the client loads the actual stack by slug.
56
export async function generateStaticParams() {
6-
try {
7-
// Read from static JSON data generated at build time
8-
const stacks = stacksData.results || [];
9-
10-
console.log(`Generating ${stacks.length} tech stack pages from static data (generated: ${stacksData.generated})`);
11-
12-
// Generate params for all tech stacks
13-
return stacks.map((stack: any) => ({
14-
slug: stack.slug,
15-
}));
16-
} catch (error) {
17-
console.error('Failed to load tech stacks from static data:', error);
18-
// Fallback to placeholder if data is unavailable
19-
return [{ slug: '_placeholder' }];
20-
}
7+
// Return a placeholder - the actual routing happens client-side
8+
return [{ slug: '_placeholder' }];
219
}
2210

23-
export default async function TechStackDetailPage({
24-
params,
25-
}: {
26-
params: Promise<{ slug: string }>;
27-
}) {
28-
const { slug } = await params;
29-
30-
return <TechStackDetailClient slug={slug} />;
11+
export default function TechStackDetailPage() {
12+
return <TechStackDetailClient />;
3113
}

TechStacks.Client/src/app/tech/[slug]/TechnologyDetailClient.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
import { useEffect, useState } from 'react';
44
import Link from 'next/link';
5+
import { usePathname } from 'next/navigation';
56
import routes from '@/lib/utils/routes';
67
import * as gateway from '@/lib/api/gateway';
78
import { useAuth, PrimaryButton } from '@servicestack/react';
89
import { FavoriteButton } from '@/components/ui/FavoriteButton';
910

10-
export default function TechnologyDetailClient({ slug }: { slug: string }) {
11+
export default function TechnologyDetailClient() {
12+
const pathname = usePathname();
13+
const segments = pathname.split('/').filter(Boolean);
14+
const slug = segments[1] ?? '';
1115
const [tech, setTech] = useState<any>(null);
1216
const [loading, setLoading] = useState(true);
1317
const { isAuthenticated } = useAuth();
Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,13 @@
11
import TechnologyDetailClient from './TechnologyDetailClient';
2-
import techData from '@/data/tech.json';
32

4-
// Generate static pages for all technologies from static data
3+
// For static export, generate a single placeholder page.
4+
// The ASP.NET Core backend routes all /tech/{slug} requests
5+
// to this placeholder, and the client loads the actual technology by slug.
56
export async function generateStaticParams() {
6-
try {
7-
// Read from static JSON data generated at build time
8-
const technologies = techData.results || [];
9-
10-
console.log(`Generating ${technologies.length} technology pages from static data (generated: ${techData.generated})`);
11-
12-
// Generate params for all technologies
13-
return technologies.map((tech: any) => ({
14-
slug: tech.slug,
15-
}));
16-
} catch (error) {
17-
console.error('Failed to load technologies from static data:', error);
18-
// Fallback to placeholder if data is unavailable
19-
return [{ slug: '_placeholder' }];
20-
}
7+
// Return a placeholder - the actual routing happens client-side
8+
return [{ slug: '_placeholder' }];
219
}
2210

23-
export default async function TechnologyDetailPage({
24-
params,
25-
}: {
26-
params: Promise<{ slug: string }>;
27-
}) {
28-
const { slug } = await params;
29-
30-
return <TechnologyDetailClient slug={slug} />;
11+
export default function TechnologyDetailPage() {
12+
return <TechnologyDetailClient />;
3113
}

TechStacks/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
// placeholder page that loads the post client-side from the API.
129129
var fallbackRoutes = new Dictionary<string, string>
130130
{
131-
["/posts/{id:long}/{slug}"] = "posts/0/_placeholder/index.html",
131+
["/posts/{id:long}/{slug}"] = "posts/0/_placeholder.html",
132132
["/tech/{slug}"] = "tech/_placeholder.html",
133133
["/stacks/{slug}"] = "stacks/_placeholder.html",
134134
};

0 commit comments

Comments
 (0)