2

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();
  }),
);

2 Answers 2

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;
  }
};
Sign up to request clarification or add additional context in comments.

1 Comment

Doesn't work. It behaves the same way as with my code.
0

Unfortunately Angular web application can provide only response status 200. The reason is pretty simple: application downloaded before generated page upon client's request.

I found such solution for me - implement minor update of server.ts:

  1. In dist/<your project name>/ find file prerendered-routes.json

  2. Transform it to array with routes like

const routes = ['/', '/about', '/contact'];

My website is small so I can use array, for big websites you can use database for checking if page exists.

  1. Next update is for response sending to client in server.ts
  server.get('*', (req, res, next) => {
    const { protocol, originalUrl, baseUrl, headers } = req;

    commonEngine
      .render({
        bootstrap,
        documentFilePath: indexHtml,
        url: `${protocol}://${headers.host}${originalUrl}`,
        publicPath: browserDistFolder,
        providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
      })

//start of update is here

      .then((html) => {
        if (!routes.includes(originalUrl)) {
          res.status(404);
        }
        res.send(html);
      })

//end of update is here

      .catch((err) => next(err));
  });

As the result: unknown requested page will be provided with 404 response status.

I will be happy if it can help you.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.