Standalone routes: the 15 one-component pages

This is the first PR from the what's left plan. Fifteen public pages now load their component straight from the app routes, with no NgModule in between. I also deleted two modules that nothing loaded. The server renders the same HTML for all 20 URLs I checked.

topfan/TopFansLite-WebFrontend#10830 base hotfix/release-0.0.23/21-code-refactors 42 files, +154 −3016

What changed

Each of these modules declared one component and had one inline route with an empty path. The component now lists its own imports, and the app route points at it with loadComponent. The 15 *.module.ts files are gone.

PageURLsComponent imports
about/aboutCommonModule, SharedModule, RouterLink
pricing/pricingCommonModule, SharedModule, RouterLink
privacy-policy/privacy-policySharedModule, RouterLink
terms-condition/terms-conditions, /terms-of-serviceSharedModule, RouterLink
contact-us/contact-usCommonModule, SharedModule, NgSelectModule
contact-v1/request-demo, /schedule-a-demoCommonModule, SharedModule, DynamicFormBuilderModule
help-faq/help-faqCommonModule, SharedModule, FaqComponent, RouterLink
calendar-connect/calendar-connectCommonModule, SharedModule
shopify-connect/shopify-connectCommonModule, SharedModule
imported-opt-out/imported/opt-outCommonModule, SharedModule
unsubscribe-newsletter/unsubscribe/newsletterCommonModule, SharedModule
unsubscribe-instruction/unsubscribe-instructionCommonModule, SharedModule
not-found/not-found, the wildcard, /bio/**CommonModule, SharedModule
shortend-url/share/:shortend-urlnone
access-user-account/access-usernone

The guards on each app route didn't move. /about and /pricing keep the white-label and PWA guards, and the contact, demo, and FAQ routes keep the PWA guard. link-in-bio-fan-preview.routes.ts now loads the standalone not-found component for its wildcard.

The breadcrumbs needed RouterLink

Five templates use routerLink for the "Home" breadcrumb or a "contact us" link. They worked because each module imported RouterModule.forChild(routes), and that re-exports the router directives. SharedModule doesn't export RouterModule.

So with the module gone, the component has to import RouterLink itself. Without it, the build still passes. Angular treats routerLink="/" on an <a> as a plain HTML attribute, and the link renders with no href. about, pricing, privacy-policy, terms-condition, and help-faq import it now. The server output has href="/" on the breadcrumb for all five, same as the base build.

Route titles and descriptions moved to a constants file

Six modules put a title and description in their child route's data. That data now sits on the app route. The strings live in src/app/constants/public-page-route-data.ts, so the routing file has no inline text.

{
  path: ROUTE_PATHS.ABOUT,
  loadComponent: () =>
    import('./modules/public/about/about.component').then((m) => m.AboutComponent),
  canActivate: marketingSurfaceGuards,
  data: PUBLIC_PAGE_ROUTE_DATA.data.ABOUT
}

I only noticed this while moving it, but nothing reads that data. CommonService.getTitle and getDescription walk the route tree for it, but neither has a caller. Each page sets its real title and meta description through SeoService. I kept the data anyway because the plan said to. It costs 752 bytes in the initial bundle, since the strings moved out of lazy chunks into main. Dropping the data and those two methods would be a small follow-up.

terms-condition is different. It used the route's title property, and Angular's default title strategy does apply that to document.title. Both terms routes keep it.

Imports I dropped

  • CarouselModule from help-faq. The template has no carousel.
  • NgSelectModule from contact-v1. The form comes from the dynamic form builder, which imports its own select.
  • AlertModalComponent from imported-opt-out. The class uses it as the type of a @ViewChild, but the template never renders one.
  • CUSTOM_ELEMENTS_SCHEMA from pricing, calendar-connect, and shopify-connect. Without the schema, an unknown tag fails the build, and the build passed.
  • SharedModule from shortend-url and access-user-account. Both have an empty template, and every service they inject is providedIn: 'root'.

Two dead modules deleted

modules/public/advertise and modules/public/comparison had no route pointing at them and no importer. They're gone, 9 files and 2,564 lines. The live /advertise page is advertisement-home, and the live comparison pages are comparison-page. Neither shares code with the deleted folders.

How it was tested

I built the base commit in a separate worktree and this branch in the main checkout, both with ng build --configuration=qa --prerender=false. Both builds passed. ESLint on the 18 changed TypeScript files reports no errors. The 4 warnings it prints are on lines this change doesn't touch.

Initial bundleBaseThis branch
Reported total12.30 MB12.30 MB
Exact raw bytes12,295,37912,296,131, +752

Then I started the SSR server from each build and requested the same 20 URLs. I stripped out the hashed chunk names and Angular's per-component style IDs, then diffed the full HTML. The rendered markup was identical on every URL. The only other difference was the same style ID inside the hydration data.

URLBaseThis branch
/about, /pricing, /privacy-policy200same HTML
/terms-conditions, /terms-of-service200same HTML
/contact-us, /request-demo, /schedule-a-demo200same HTML
/help-faq200same HTML
/calendar-connect, /shopify-connect200same HTML
/imported/opt-out, /unsubscribe/newsletter, /unsubscribe-instruction200same HTML
/share/abc123, /access-user200same HTML
/not-found, /this-page-does-not-exist-xyz/a/b404same HTML
/bio/someuser/extra/segment404same HTML
/about/extra-segment200, falls through to the sub-page viewsame HTML

Last, I opened the branch build in a browser. /pricing hydrated, and clicking its "Home" breadcrumb navigated to / without a reload. From there I navigated in the app to each converted URL, which loads the new lazy chunks in the browser. Every page rendered, and the console logged no errors.

Two things looked wrong at first and turned out to be the same on the base build:

  • Going from /calendar-connect to /shopify-connect bounces back. shopify-connect calls location.back() when the URL has no code or shop params.
  • document.title goes stale after in-app navigation. The terms title "TopFan - Terms & Conditions" stays after you leave the terms page. The route title sets it, and the next page never replaces it on the client.

Affected areas

Every page here is public, so you can test them without logging in. The callback pages need real query params from an email, an OAuth flow, or an admin link.

AreaRiskWhy
Not-found and the wildcard Medium Every unknown URL on the site lands here, and the SSR response has to stay a 404.
Contact and demo forms Medium They have reCAPTCHA, a phone input, and a select. contact-v1 lost an import. I didn't submit either form.
OAuth and email callbacks Medium calendar-connect, shopify-connect, imported-opt-out, unsubscribe-newsletter, shortend-url, and access-user all act on query params. I only loaded them without params.
About, pricing, privacy, terms, help Low Static content. The breadcrumb links were the one real risk, and they render with href.

Verification checklist

Test each page two ways. Load the URL directly, which renders on the server and then hydrates. Then reach it from another page, so the browser fetches the lazy chunk. Checks are saved in this browser.

0 of 0 checked

Medium Not-found

Medium Contact and demo forms

Medium Callback pages

Low Static pages

On every page

Not tested

  • No form submissions and no callback with real params. Those need a QA account and real emails or OAuth flows.
  • No white-label build. angular.json only has the TopFans project right now, since white-label projects are generated.
  • No prerender run. The pages are the same, but npm run prerender-qa would confirm it.

What's next

PR 2 in the plan covers fulfillment-order, creators, setup-profile, and forum. Nothing imports those four, and they don't import any routed module, so no extra URLs disappear. Forum is the one to click through, because it calls NgCircleProgressModule.forRoot and provides BsModalService at the module level.