PR #10826 · audit item 4 · 23 Sep 2026

Five pages no longer need an NgModule

Each of these pages had one component, one module that declared it, and one routing module with a single empty-path route. That's two files of ceremony per page. The PR deletes them and lets the router load the component directly.

10module files deleted
−148net lines (142 added, 290 removed)
12.30 MBinitial bundle, unchanged

The pattern

Here is comparison-page before and after. The other pages follow the same shape.

Before, three files

// comparison-page.module.ts
@NgModule({
  declarations: [ComparisonPageComponent],
  imports: [
    CommonModule,
    ComparisonPageRoutingModule,
    MarketingPageSharedModule
  ],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})

// comparison-page-routing.module.ts
const routes = [
  { path: '', component: ComparisonPageComponent }
];

// component
@Component({
  selector: 'app-comparison-page',
  standalone: false
})

After, one file

// component
@Component({
  selector: 'app-comparison-page',
  imports: [
    CommonModule,
    MarketingPageSharedModule
  ]
})

// route
{
  path: 'topfan-vs-patreon',
  loadComponent: () =>
    import('.../comparison-page.component')
      .then((m) => m.ComparisonPageComponent),
  data: { comparisonSlug: 'topfan-vs-patreon' }
}

Dropping CUSTOM_ELEMENTS_SCHEMA matters more than it looks. That schema tells the compiler to accept any unknown tag, so a missing import just renders an empty element at runtime. Without it, a missing import fails the build. The build passed, which means every tag in these five templates now resolves to a real component.

What each page imports now

PageRoutesComponent imports
comparison-page 7 generated slugs CommonModule, MarketingPageSharedModule
marketing-page 14 generated slugs CommonModule, MarketingPageSharedModule
sub-page-view white-label root matcher, PROFILE_SUB_PAGE CommonModule, SharedModule, MarketingPageSharedModule
link-in-bio-fan-preview /bio/:userName CommonModule, SharedModule, four preview components
creators-home /creator, /creators CommonModule, NgIvsModule, SharedModule, MarketingPageSharedModule, FaqComponent, ButtonComponent, BookDemoFormComponent

SharedModule is still there on three of them. Splitting it is audit item 10. These components import it whole for now, the same way their old modules did.

The one that kept a routes file

link-in-bio-fan-preview had two child routes, not one. :userName renders the preview. A wildcard sends anything else to the not-found page. loadComponent can only load one component, so those two routes moved into link-in-bio-fan-preview.routes.ts, a plain exported Routes array. The app router still uses loadChildren for it, but it now loads that array instead of an NgModule.

- loadChildren: () => import('./.../link-in-bio-fan-preview.module').then(m => m.LinkInBioFanPreviewModule)
+ loadChildren: () => import('./.../link-in-bio-fan-preview.routes').then((m) => m.LINK_IN_BIO_FAN_PREVIEW_ROUTES)

Generated routes

The marketing and comparison routes don't live in app-routing.module.ts. scripts/prefetch-marketing-pages.mjs fetches published slugs from the API and writes marketing-page-routes.generated.ts and comparison-page-routes.generated.ts. If I'd only edited the generated files, the next npm run build-prod would regenerate them with imports of modules that no longer exist, and the build would break.

So the PR changes the template in the script and the two generated files together. generate-static-routes.mjs and generate-sitemap.mjs also read those files, but they only regex out the slug arrays, so they don't care how the routes load.

The surprise in creators-home

Behavior change /creators/login used to render the login page

CreatorsHomeModule imported AuthModule and SetupProfileModule. Neither exported anything the template used. But both import their own routing modules, and RouterModule.forChild registers routes through a multi-provider. When the router lazy-loaded CreatorsHomeModule, it collected every route in that module's injector. So the real route table under /creators looked like this:

''  CreatorsHomeComponent          // first in the list, so /creators still worked
''  AuthLandingComponent           // from AuthRoutingModule
    'login'          LoginComponent
    'signup'         SignUpComponent
    'verify-by-otp'  VerifyByOtpComponent
    ...
''  SetupLandingComponent          // from SetupProfileRoutingModule
    'setup-profile'    SetupProfileComponent
    'content-creator'  ContentCreatorSetupComponent
    'confirm-creator'  ConfirmYourCreatorComponent

The same list was under /creator. I'm fairly sure nobody meant this. Nothing in the code navigates there. The signup button goes to auth/creators-signup from the root, and Application.confirmCreator is an absolute /edit-preference/... path. prerender.routes.txt and the sitemap don't list any /creators/* URL either.

A standalone component's imports don't feed the router, so these routes are gone now. /creators/login falls through to the profile sub-page route and renders app-sub-page-view. If someone bookmarked one of those URLs, that's what they'll see.

Things I checked that didn't change

Same Route data lookup. The marketing and comparison components read marketingSlug and comparisonSlug by walking up route.parent until they find it. Before, the data sat on the parent of the component's route. Now it sits on the component's own route. The loop finds it one step earlier.

Same Case-insensitive URLs. withCaseInsensitivePaths turns each path into a matcher. A route without a children array counts as a leaf and has to match the whole URL. loadChildren routes never had a children array either, so /Athletes matches and /athletes/extra doesn't, exactly as before.

Same Modal service. An earlier commit on this branch fixed a bug where two BsModalService instances fought over one dialog. I checked ngx-bootstrap 21's ModalModule. It provides nothing, so the service still comes from the root injector. Dropping ModalModule from creators-home can't create a second instance.

Same Initial bundle. I built the base branch and this branch with the QA config. Both report 12.30 MB initial. The routes still use dynamic import(), so none of these pages moved into the first load.

How it was tested

URLStatusRendered
/creators200app-creators-home
/creator200app-creators-home
/athletes200app-marketing-page
/topfan-vs-patreon200app-comparison-page
/bio/prateek200app-link-in-bio-fan-preview
/bio404app-not-found
/creators/login200app-sub-page-view

Not tested

Affected areas and what to verify

Everything below is public and works without logging in. Test each page two ways. Load the URL directly, which renders through SSR and then hydrates. Then reach it by clicking a link from another page, which makes the browser fetch the lazy chunk. The second path is the one my curl test never touched.

AreaRiskWhy
Creators home High Most imports changed. Four modals, a video player, and query-param actions.
Link in bio Medium New routes file. Five kinds of product preview, each from a different import.
Sub-pages Medium Two routes, one of them a custom matcher that only fires on white-label domains.
Marketing pages Low Same imports as before. The route shape changed in a generated file.
Comparison pages Low Same as marketing pages.
Prefetch script Low Runs before every QA, staging, and prod build. It rewrites two route files.

High Creators home, /creators and /creator

The old module imported FormsModule, NgSelectModule, ModalModule, AccordionModule, and NgMultiSelectDropDownModule. The template uses none of them, so the component doesn't import them. Modals opened through BsModalService don't need ModalModule in the opener, but clicking each one is the only real proof.

Medium Link in bio, /bio/:userName

Use a creator whose link-in-bio has at least one of each product type. Each type renders through a different component, and a missing import shows up as a blank tile.

Medium Sub-pages, /:id/:subPageUrl and white-label roots

Low Marketing pages, 14 slugs

Low Comparison pages, 7 slugs

Low Prefetch script

On every page

What's next

The remaining feature modules are bigger. creators has 3 components, post-detail and setup-profile have 6 each, and the list climbs from there. Each one will hit SharedModule. I think the SharedModule split (item 10) should start before the big ones, or every converted component will import 650 lines of declarations it mostly doesn't use.