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.
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
| Page | Routes | Component 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
ng build --configuration=qa --prerender=falsepasses with no errors.- I started the SSR server from that build and requested each route with curl.
| URL | Status | Rendered |
|---|---|---|
/creators | 200 | app-creators-home |
/creator | 200 | app-creators-home |
/athletes | 200 | app-marketing-page |
/topfan-vs-patreon | 200 | app-comparison-page |
/bio/prateek | 200 | app-link-in-bio-fan-preview |
/bio | 404 | app-not-found |
/creators/login | 200 | app-sub-page-view |
Not tested
- Nobody has clicked through these pages in a browser yet. The next section lists what to click. SSR proves the components render. It doesn't prove the video player, the book-demo modal, or the link-in-bio carousels behave after hydration.
- No white-label project was built. They share
src/app, so the risk is low, but the repo rules ask for it. npm run build-devfails on the base branch too, becausesrc/environments/environment.dev.tsdoesn't exist. That's unrelated to this PR, and someone should fix it.
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.
| Area | Risk | Why |
|---|---|---|
| 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.
- "Apply now" opens the creator requirements modal. Check all three copies, in the marketing section, the button row, and the bottom of the page.
- "Request demo" goes to the request-demo page.
- "Watch video" opens the video player modal and plays.
- On the non-marketing layout, the IVS player at the top loads and plays.
- The book-demo form modal opens, submits, and shows the thank-you modal. The thank-you "Close" button closes it.
?watch_demo_video=true,?apply_now=true, and?watch_sizzle_video=trueeach open their modal on load, then disappear from the URL.?key=<creator username>shows the referee card with the creator's photo and name. Clicking the photo opens their profile.- The FAQ accordion expands. The testimonial cards scroll and expand on hover.
- On a white-label domain,
/creatorsstill redirects to the white-label home. - Try
/creators/login. It should now render the sub-page view, not the login page. That's expected. Flag it if anyone depends on the old URL.
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.
- Banner, profile photo, name, tick icon, and bio text all show. Clicking the name or photo opens the profile.
- Social icons show and open the right links in a new tab.
- A specific store item renders its card and opens on click.
- A portfolio item renders. Clicking an individual item opens it with the player.
- A feed content item renders and opens its modal.
- A general action item renders the monetize carousel.
- A welcome video item renders its preview.
- External links open in a new tab.
- The footer says "Powered by" normally and "Get your own platform" when the bio has a referral link.
- Check a mobile width. The layout switches to the mobile product list.
/bio/<unknown user>goes to not-found./bio/a/balso goes to not-found, through the wildcard in the new routes file.
Medium Sub-pages, /:id/:subPageUrl and white-label roots
- On the main site, open a creator's published sub-page at
/<username>/<sub-page>. The loader shows, then the page renders with the cover page header and footer. - Add
?preview=trueto an unpublished sub-page while logged in as its owner. It renders the draft. - On a white-label domain, open
/<sub-page>with a single segment. This goes throughwhiteLabelRootSubPageMatcher, which returns nothing on the main site, so you can only test it on a white-label build. - On a white-label domain,
/<username>/<sub-page>redirects to/<sub-page>and keeps the query string. This check reads theidparam throughroute.pathFromRoot, and that param now sits on the component's own route. - On a white-label domain, a single-segment slug that isn't a sub-page but is a username goes to that profile. Anything else goes to not-found.
Low Marketing pages, 14 slugs
- Open a few, for example
/home,/athletes, and/music-artists. Sections render, and the animated counters and scroll-in effects run. - Mixed case still works, for example
/Athletes. - Reach one from another page without a full reload, for example by following an in-app link, and check it renders.
- On a white-label domain, they redirect to the white-label home. In the installed PWA, logged out, they redirect to the referrer login.
Low Comparison pages, 7 slugs
- Open
/topfan-vs-patreonand one more. The comparison tables and hero render with the competitor logo. - Click through from the header's comparison list. That's the client-side lazy-load path.
Low Prefetch script
- Run
npm run prefetch-pages-qa. The two generated route files should still sayloadComponentand import the.componentfiles. The only diff should be the timestamp and any slugs the API added or removed. - The next QA deploy builds cleanly. That run is the first time the script output meets the build.
On every page
- Open the console. There should be no
NG0500-series hydration errors and no "is not a known element" errors. - View the page source. The SSR HTML should contain the page content, not an empty shell.
- Build at least one white-label project and repeat the creators, marketing, and sub-page checks there.
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.