server side rendering - How would I go about redirecting to 404 on routes with dynamic params in Angular 19 SSR? - Stack Overflo

admin2025-04-15  3

I have a route like blog/:slug. The blog post data is fetched through a resolver. When I receive status 404 from the GET request the user should get redirected to the /404 page. The problem is that when the resolver returns a RedirectCommand the server renders the 404 page and returns it on the blog/:slug path and then it redirects to the /404 path it renders the 404 page html again and the page flickers. This problem appears only when I route by changing the url. It works fine when I route through the app. Is it possible to redirect to the /404 page without initially rendering the blog post page? The page status of the /404 page is 404, but it is 200 when redirect by RedirectCommand. Is there an SEO friendly workaround/way to show the crawler page is 404 without it being a soft 404?

I am using Angular 19

package.json dependencies

"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-viewponent').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-404ponent').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',
    },
  },
];

usage of resolvedData:

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

    this.init();
  }),
);

I have a route like blog/:slug. The blog post data is fetched through a resolver. When I receive status 404 from the GET request the user should get redirected to the /404 page. The problem is that when the resolver returns a RedirectCommand the server renders the 404 page and returns it on the blog/:slug path and then it redirects to the /404 path it renders the 404 page html again and the page flickers. This problem appears only when I route by changing the url. It works fine when I route through the app. Is it possible to redirect to the /404 page without initially rendering the blog post page? The page status of the /404 page is 404, but it is 200 when redirect by RedirectCommand. Is there an SEO friendly workaround/way to show the crawler page is 404 without it being a soft 404?

I am using Angular 19

package.json dependencies

"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',
    },
  },
];

usage of resolvedData:

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

    this.init();
  }),
);
Share Improve this question asked Feb 4 at 9:40 user12288241user12288241 111 bronze badge
Add a comment  | 

1 Answer 1

Reset to default 0

try this:

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);

  try {
    const response = await lastValueFrom(request);

    if (response.status === 200) {
      return response.data;
    }

    throw new Error('Not found');
  } catch (error) {
    router.navigate(['/404'], { skipLocationChange: false });
    return EMPTY;
  }
};
转载请注明原文地址:http://www.anycun.com/QandA/1744729941a86811.html