What changed
Each module's routes now live in a plain Routes array, and the app route loads that array. Creators has a single route, so its app route uses loadComponent instead. Every component the module declared lists its own imports. The guards and route data on the app routes didn't move.
| Module | URLs | Converted | Loaded from |
|---|---|---|---|
fulfillment-order | /order-status/... | 3 | FULFILLMENT_ORDER_ROUTES |
creators | /explore-creators | 3 | CreatorContainerComponent |
setup-profile | /content/... | 6 | SETUP_PROFILE_ROUTES |
forum | /forum/... | 8 | FORUM_ROUTES |
resources | /resources/... | 9 | RESOURCES_ROUTES |
promotional | /, /home/..., /search-result/... | 4 | PROMOTIONAL_ROUTES |
post-detail | /post/... | 6 | POST_DETAIL_ROUTES |
messages | /messages | 6 | MESSAGES_ROUTES |
insights | /insights/... | 15 | INSIGHTS_ROUTES |
auth | /auth/... | 13 | AUTH_ROUTES |
Three pieces outside these folders became standalone too, because the converted components needed them. CreaterProfileInfoComponent from edit-preference renders on step 2 of creator sign-up. FilterModalComponent from src/app/components and ArticleDescriptionPipe from the shared pipes are both used by insights. The table counts 73, and these three make 76. EditPreferenceModule now imports the profile-info component instead of declaring it, so /edit-preference/become-a-creator/creator-profile-info still routes to it.
The commits follow the plan's grouping:
- 29ae2c77e2. fulfillment-order, creators, setup-profile, and forum.
- 1448a67334. resources and promotional.
- ba3dc6383c. post-detail, messages, and insights.
- 6c3effddc9. auth.
The leaked routes are gone
A lazy NgModule that imports a routed NgModule picks up that module's routes as its own children. A standalone component's imports don't touch the router. So every leak listed in the plan went away with this branch:
- /auth lost 49 edit-preference route entries. Auth only needed one component from that module, and it now imports that component directly.
- /resources lost about 80. It imported WalletModule and DashboardModule and used nothing from either. Two templates did use
SearchBarV2Component, which they got through WalletModule's exports. They import it directly now. - PostDetailModule's three routes left five importers. Those were messages, insights, dashboard, monetize, and portfolio. The module had an empty
exportslist, so the three NgModules that stay gained nothing from it. I removed the import from them.
I searched for anything that links to those URLs. Post links in the app build absolute /post/... paths. The relativeTo navigations only change query params, and the relative routerLinks are one segment long. Nothing I found points at a leaked URL.
Most leaked URLs sit behind authGuard, so I couldn't request them logged out. /auth is open to logged-out users, which made it the one place to see the change. On base, /auth/privacy and /auth/edit-content match a route under /auth. On this branch they fall through to the app's /:username/:page route and render the sub-page view, the same thing /about/extra-segment does.
Providers moved onto the routes
A service in a lazy module's providers gets one instance per module, shared by every page under it. A route's providers gives the same scope, so that's where these went:
{
path: '',
component: ForumContainerComponent,
providers: [BsModalService],
children: [ ... ]
}
- Forum keeps
BsModalServiceon its root route. - Resources keeps
ResourcesServiceon its root route. The service is alsoprovidedIn: 'root', but the module gave resources its own copy, which holds the unread counts for announcements and tips. The route keeps that separate copy. - Messages keeps
AudioRecordingServiceon its route, next to thecanDeactivateguard it already had. ReelsMobileViewComponentinjectsContentLengthHandlerPipeas a service, so the component provides it itself.
I dropped two things. Forum called NgCircleProgressModule.forRoot({}), but AppModule imports SharedModule, which makes the same call, so the options already exist at the root. The DatePipe providers went too, because the root injector already has one. To check nothing else fell off, I listed every token the converted classes inject that isn't providedIn: 'root'. Each one is either provided at the root through AppModule or provided by the component itself.
What the custom-elements schema was hiding
Six of the old modules set CUSTOM_ELEMENTS_SCHEMA. It tells the compiler to accept any unknown tag or property. With the schema gone, the build failed in four templates. None of these bindings or tags ever did anything:
insights.component.htmlbound[showConfirmationBtn]and[cancelBtnLabel]onapp-send-email-modal, which has neither input.insight-fan-management.component.htmlbound[isContinueDisabled]onapp-import-confirm-modal.import-confirm-modal.component.htmlbound[failedImports]onapp-failed-imports-modal.login.component.htmlhad an<ng-adsense>tag. No installed package defines it, so the browser rendered an empty unknown element. Its parent is a column flex container with no gap, so removing it moves nothing.
I deleted all four. With the schema gone, the next binding or tag that doesn't match anything fails the build instead of doing nothing quietly.
Six dead promotional components deleted
PromotionalModule declared six components that no template or route used. They were direct-to-fans, trusted-brands, user-profile, explore-topfan-pro, feature-display-grid, and featured-categories. Converting them would have been wasted work, and user-profile had a real problem. Its selector is app-user-profile, the same as a shared component's. That was harmless inside an NgModule, but it would clash in a standalone import list. featured-categories.constants.ts stays, because the header imports it.
One leftover is a ::ng-deep app-explore-topfan-pro rule in promotional.component.scss. It matches nothing now. I left it for a cleanup PR.
Auth route strings moved to constants
The auth routes had inline SEO text in their data. It now lives in authConstants.ts under LOGIN_PAGE.SEO_DESCRIPTION, SIGN_UP_PAGE, and FORGOT_PASSWORD_PAGE. The sign-up description was a template literal with a line break and 14 spaces in the middle. I joined it into one line.
Like the public pages in the last PR, nothing reads this data. LoginComponent only reads loginAudience. Each page sets its real title and description itself. On both builds, /auth/login ends up with the title "Login Page" and the description "Login in your TopFan.com Account or Reset your Password". The strings cost about 540 bytes in the initial bundle, since main already imports authConstants.ts.
How it was tested
Every template resolves the same things
A missing import often compiles. Drop RouterLink and routerLink="/" turns into a plain attribute, and the link loses its href. So a passing build proves very little about 76 hand-written import lists.
I compiled base and branch with ngc in full AOT mode. The output lists, for every component, the directives, components, and pipes the compiler matched in its template. A script pulled that list out for each component on both sides and diffed them. 655 components match exactly. The only differences are the six deleted promotional components, plus FailedImportsModal, whose import path has a double slash on one side. It's the same class. To make sure the check works, I removed RouterLink from one component. It compiled, and the diff flagged it.
Builds and lint
I built base in a separate worktree and the branch in the main checkout, both with ng build --configuration=qa --prerender=false. Both passed. ESLint on the 91 changed TypeScript files went from 99 errors and 26 warnings to 23 errors and 26 warnings. All 76 errors that disappeared were prefer-standalone. The 49 that remain are the same ones base has, under the same rules.
Bundle size
I rebuilt both sides with --stats-json and walked the static imports from main to find the initial files.
| Initial load | Base | This branch |
|---|---|---|
| JavaScript files | 18 | 15 |
| JavaScript bytes | 10,472,004 | 10,481,271, +9,267 |
main bytes | 287,523 | 287,458 |
| CSS bytes | 2,134,905 | 2,134,905 |
| All browser JavaScript | 23,523,534 | 23,521,040, −2,494 |
The initial growth is 0.09%, and I checked where it comes from. No module entered or left the initial load. The same source files are in it on both sides. esbuild regrouped the initial chunks, merging two of them into one 7.2 MB file, and the minifier picked different names across 605 files. About 540 bytes of the growth is the auth strings. The rest is that renaming.
The lazy side is where it pays off. This table shows the JavaScript a first visit to each route fetches on top of the initial load. It's raw bytes before compression. Some of those files may already be cached from another page.
| Route | Base | This branch | Change |
|---|---|---|---|
/auth | 4,526,115 | 189,025 | −4,337,090 |
/resources | 4,236,243 | 85,459 | −4,150,784 |
/insights | 2,513,570 | 1,995,786 | −517,784 |
/messages | 2,464,166 | 2,037,576 | −426,590 |
/ and /home | 1,127,457 | 1,123,962 | −3,495 |
/post | 425,635 | 425,444 | −191 |
/forum | 131,906 | 131,634 | −272 |
/content | 109,781 | 109,602 | −179 |
/explore-creators | 39,147 | 38,615 | −532 |
/order-status | 16,062 | 15,578 | −484 |
Auth and resources no longer drag in edit-preference, wallet, and dashboard. Messages and insights no longer pull in everything PostDetailModule imported. Insights still loads WalletModule's code, because InsightsEarningComponent renders app-transaction-history. It imports the module for that one component, which brings the code but not the routes.
Server-rendered HTML
I started the SSR server from each build and requested the same 28 URLs on both. I replaced the chunk hashes and Angular's style IDs with placeholders, then diffed the full HTML.
| URL | Status | Result |
|---|---|---|
/home, /search-result/a/b, /creators, /post/someuser/image/123 | 200 | same HTML |
/, /post/forum/someuser/123 | 200 | same markup, new component IDs |
/order-status/confirmed, /order-status/dispute | 200 | same markup, new component IDs |
Nine /auth/... URLs | 200 | same HTML |
/forum, /forum/list, /messages, /insights, /resources, /content, /explore-creators | 200 | same HTML |
/setup-profile, /promotional | 404 | same HTML |
/auth/privacy, /auth/edit-content | 200 | sub-page view instead of the auth shell |
On every URL the preload list and the integrity hash on main differ, since the chunks changed. The "new component IDs" rows need a word. Angular hashes each component's metadata into an ID, and the standalone flag is part of that hash. PromotionalComponent and the order-status components changed that flag, so their IDs changed in the hydration data. The server and the browser run the same build, so they still agree.
This check has a blind spot. The auth pages and the guarded pages come back as the app shell with an empty outlet on both builds. The diff says they match, but it says nothing about their templates. The dependency diff and the browser cover those.
In the browser
I opened the branch build and loaded /, /auth/login, /auth/signup, step 1 of /auth/creators-signup, and /order-status/confirmed. Each rendered the same components as base, and the console had no Angular errors. Two failures showed up on both builds. PUT /api/orders returns 400 when there's no order in the URL, and the reCAPTCHA endpoint returns 404 on localhost. Order status also says "from undefined undefined" on both, for the same missing-params reason.
Affected areas
Most of these pages need a logged-in account, and I didn't have one in this session. So the risk tags lean on how much each page does and how little of it I could click.
| Area | Risk | Why |
|---|---|---|
| Auth | High | Every login, sign-up, OTP, password reset, and social callback goes through it. SSR renders only the shell here, and I loaded three pages without submitting anything. |
| Home and category pages | Medium | The busiest URLs on the site. I deleted six components from this module. SSR markup matches. |
| Insights | Medium | 15 declarations, four removed bindings, the filter modal, and the fan import flow. Never opened. |
| Messages | Medium | The audio recorder and the leave-page guard moved onto the route. Never opened. |
| Forum and resources | Medium | Their module-level services now sit on the root route. Resources lost two module imports. Never opened. |
| Post detail | Medium | Dashboard, monetize, and portfolio dropped the module import. The mobile reel view provides its pipe itself now. |
| Order status, content setup, explore creators | Low | Small, self-contained, and nothing else imported them. |
| Leaked URLs | Low | They stop resolving, but nothing in the app links to them. |
Verification checklist
Use a QA creator account. Load each page directly once, then reach it from another page so the browser fetches the lazy chunk. Keep the console open the whole time. Checks are saved in this browser.
High Auth
Medium Home and category pages
Medium Insights
Medium Messages
Medium Forum and resources
Medium Post detail
Low Small modules
Leaked URLs and every page
Not tested
- Anything behind login. I compiled and diffed messages, insights, forum, resources, content, explore creators, and the post pages, but never opened them.
- Form submissions. I didn't log in, sign up, reset a password, or finish a social login.
- The leaked URLs behind
authGuard. I only confirmed the change on/auth. - A phone-sized viewport, which is the only way to reach the mobile reel view.
- A white-label build or a prerender run.
What's next
The branch is ready to split into the four PRs the plan suggested, one per commit, each based on the one before. Auth should go last and get the full checklist above.
After that, the large modules are left. Those are edit-preference, profile, monetize, dashboard, and wallet, at 40 to 75 components each. The plan says to split SharedModule first, and this branch backs that up. 66 of the 76 import lists here include SharedModule, and it brings all 147 of its declarations whether a page uses them or not.