You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular 19 SSR中带动态参数路由的404重定向问题

Angular 19 SSR下博客404重定向问题与SEO优化方案

问题背景

我有一个blog/:slug路由,博客文章数据通过resolver获取。当GET请求返回404状态时,用户应被重定向到/404页面。但目前存在两个核心问题:

  • 页面闪烁问题:当resolver返回RedirectCommand时,服务器会先在blog/:slug路径下渲染404页面并返回,之后再重定向到/404路径,再次渲染404页面,导致页面闪烁。该问题仅在直接修改URL路由时出现,应用内路由跳转则正常。
  • SEO不友好问题:/404页面本身的状态码是404,但通过RedirectCommand重定向时状态码变为200,爬虫会识别为软404,影响SEO。

需要实现:不先渲染博客文章页面就直接重定向到/404页面,同时保证爬虫能识别到正确的404状态码。

环境与代码

依赖版本(package.json)

"dependencies": {
    "@angular/animations": "19.1.3",
    "@angular/common": "19.1.3",
    "@angular/compiler": "19.1.3",
    "@angular/core": "19.1.3",
    "@angular/forms": "19.1.3",
    "@angular/platform-browser": "19.1.3",
    "@angular/platform-browser-dynamic": "19.1.3",
    "@angular/platform-server": "19.1.3",
    "@angular/router": "19.1.3",
    "@angular/ssr": "19.1.4",
    "express": "4.18.2",
    "rxjs": "~7.8.0",
    "tslib": "^2.3.0",
    "zone.js": "~0.15.0"
 },
"devDependencies": {
    "@angular/build": "19.1.4",
    "@angular/cli": "19.1.4",
    "@angular/compiler-cli": "19.1.3",
    "@types/express": "4.17.17",
    "@types/node": "18.18.0",
    "typescript": "~5.5.2"
}

server.ts

const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');

const app = express();
const angularApp = new AngularNodeAppEngine();

app.use(
  express.static(browserDistFolder, {
    maxAge: '1y',
    index: false,
    redirect: false,
  }),
);

app.use('/**', (req, res, next) => {
  angularApp
    .handle(req)
    .then((response) =>
      response ? writeResponseToNodeResponse(response, res) : next(),
    )
    .catch(next);
});

if (isMainModule(import.meta.url)) {
  const port = process.env['PORT'] || 4000;
  app.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

export const reqHandler = createNodeRequestHandler(app);

app.config.ts

export const appConfig: ApplicationConfig = {
  providers: [
    provideAnimations(),
    provideHttpClient(withFetch(), withInterceptors([ authInterceptor ])),
    provideRouter(routes, withInMemoryScrolling({ scrollPositionRestoration: 'enabled' }), withComponentInputBinding(), withRouterConfig({ onSameUrlNavigation: 'reload' })),
    provideClientHydration(withIncrementalHydration(), withHttpTransferCacheOptions({ includeRequestsWithAuthHeaders: true, includeHeaders: ['Authorization-Refresh', 'Authorization-Access'] })),
  ],
};

app.config.server.ts

const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering(),
    provideServerRoutesConfig(serverRoutes)
  ]
};

blog.resolver.ts

export const blogPostResolver: ResolveFn<BlogPost> = async (route: ActivatedRouteSnapshot) => {
  const router = inject(Router);
  const userService = inject(UserService);
  const requestsBlogService = inject(RequestsBlogService);

  const slug = route.paramMap.get('slug')!;

  const request = userService.isUserLogged()
    ? requestsBlogService.getBlogPostAsAdmin(slug)
    : requestsBlogService.getBlogPost(slug);

  const response = await lastValueFrom(request);

  switch(response.status) {
    case 200:
      return response.data;

    default:
      return new RedirectCommand(router.parseUrl('/404'));
  }
};

app.routes.ts

export const routes: Routes = [
   {
    path: 'blog/:slug',
    loadComponent: () => import('./components/blog/post/single-view/blog-post-single-view.component').then(m => m.BlogPostSingleViewComponent),
    title: 'Article | Apexara',
    resolve: { resolvedData: blogPostResolver },
    runGuardsAndResolvers: 'paramsChange',
    data: { pageName: 'Article' },
  },
  {
    path: '404',
    loadComponent: () => import('./components/not-found-404/not-found-404.component').then(m => m.NotFound404Component),
    title: 'Page Not Found | Apexara',
    data: { pageName: 'Not Found' },
  },
  {
    path: '**',
    redirectTo: '404',
  },
]

app.routes.server.ts

export const serverRoutes: ServerRoute[] = [
  {
    path: 'blog/:slug',
    renderMode: RenderMode.Server,
  },
  {
    path: '404',
    renderMode: RenderMode.Server,
    status: 404,
    headers: {
      'Cache-Control': 'no-cache',
    },
  },
];

组件中使用resolvedData

this.subscriptions.push(
  this.route.data.subscribe(({ resolvedData }) => {
    this.blogPost = resolvedData;

    this.init();
  }),
);

解决方案

1. 解决页面闪烁:替换重定向为抛出NotFoundError

问题根源是RedirectCommand会触发当前路由的服务器渲染后再发送重定向指令。要避免这个问题,直接在resolver中抛出NotFoundError,让Angular SSR直接渲染404页面并返回正确状态码,无需重定向。

修改blog.resolver.ts:

import { NotFoundError } from '@angular/router';

export const blogPostResolver: ResolveFn<BlogPost> = async (route: ActivatedRouteSnapshot) => {
  const userService = inject(UserService);
  const requestsBlogService = inject(RequestsBlogService);

  const slug = route.paramMap.get('slug')!;

  const request = userService.isUserLogged()
    ? requestsBlogService.getBlogPostAsAdmin(slug)
    : requestsBlogService.getBlogPost(slug);

  const response = await lastValueFrom(request);

  if (response.status === 200) {
    return response.data;
  } else {
    // 抛出NotFoundError,触发Angular渲染404路由
    throw new NotFoundError('Blog post not found');
  }
};

2. 确保SEO友好:配置路由错误处理器

在路由配置中添加errorHandler,让Angular捕获NotFoundError时直接渲染/404页面,并保留404状态码。

修改app.routes.ts及app.config.ts中的路由配置:

import { Routes, withRouterConfig } from '@angular/router';
import { NotFoundError } from '@angular/router';

// 新增错误处理函数
const errorHandler = (error: unknown) => {
  if (error instanceof NotFoundError) {
    return {
      component: () => import('./components/not-found-404/not-found-404.component').then(m => m.NotFound404Component),
      title: 'Page Not Found | Apexara',
      status: 404,
    };
  }
  throw error;
};

// 更新provideRouter配置
export const appConfig: ApplicationConfig = {
  providers: [
    // ...其他providers
    provideRouter(routes, 
      withInMemoryScrolling({ scrollPositionRestoration: 'enabled' }), 
      withComponentInputBinding(), 
      withRouterConfig({ onSameUrlNavigation: 'reload', errorHandler }) // 注入错误处理器
    ),
    // ...其他providers
  ],
};

3. 验证效果

  • 直接访问不存在的blog/xxx时,服务器会直接返回404状态码和404页面,不会先渲染博客页面再重定向,彻底解决页面闪烁问题。
  • 爬虫抓取时会识别到正确的404状态码,避免软404影响SEO。
  • 应用内路由跳转时,Angular会正常处理错误并导航到404页面,保持原有交互逻辑。

内容的提问来源于stack exchange,提问作者user12288241

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 15:10:52