-
Notifications
You must be signed in to change notification settings - Fork 27k
Closed
Labels
area: commonIssues related to APIs in the @angular/common packageIssues related to APIs in the @angular/common packagearea: serverIssues related to server-side renderingIssues related to server-side renderingcommon: image directiveneeds reproductionThis issue needs a reproduction in order for the team to investigate furtherThis issue needs a reproduction in order for the team to investigate further
Milestone
Description
Which @angular/* package(s) are the source of the bug?
Don't known / other
Is this a regression?
No
Description
Introduction
I am using Firestore to store data for a CMS.
The data is stored in a field called blocks.
The CMS is divided into two components: Block and Inline.
Block Component
block.component.ts
import { ChangeDetectionStrategy, Component, input, ViewEncapsulation } from "@angular/core";
import { NgClass } from "@angular/common";
import { InlineComponent } from "../inline/inline.component";
import { JoinArrayPipe } from "../../pipes/join-array/join-array.pipe";
import { BlockFlowPipe } from "../../pipes/block-flow/block-flow.pipe";
import { BlockFlowRootPipe } from "../../pipes/block-flow-root/block-flow-root.pipe";
import { SectionPipe } from "../../pipes/section/section.pipe";
import { ArticlePipe } from "../../pipes/article/article.pipe";
import { AsidePipe } from "../../pipes/aside/aside.pipe";
import { MeshPipe } from "../../pipes/mesh/mesh.pipe";
import { HeadingPipe } from "../../pipes/heading/heading.pipe";
import { ParagraphPipe } from "../../pipes/paragraph/paragraph.pipe";
import { AddressPipe } from "../../pipes/address/address.pipe";
import { ListPipe } from "../../pipes/list/list.pipe";
import { MenuPipe } from "../../pipes/menu/menu.pipe";
import { DescriptionPipe } from "../../pipes/description/description.pipe";
import { SeparatorPipe } from "../../pipes/separator/separator.pipe";
import { Block } from "../../types/block/block";
@Component({
selector: "[contentBlock]",
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
imports: [
NgClass,
InlineComponent,
JoinArrayPipe,
BlockFlowPipe,
BlockFlowRootPipe,
SectionPipe,
ArticlePipe,
AsidePipe,
MeshPipe,
HeadingPipe,
ParagraphPipe,
AddressPipe,
ListPipe,
MenuPipe,
DescriptionPipe,
SeparatorPipe,
],
templateUrl: "./block.component.html",
})
export class BlockComponent {
readonly blocks = input.required<Block[]>({ alias: "contentBlock" });
}
block.component.html
@for (block of blocks(); track $index) {
@switch (block["@type"]) {
@case ("block-flow") {
@let blockFlow = block | blockFlow;
<div
class="block-flow"
[ngClass]="blockFlow.ngClass"
[attr.id]="blockFlow.id"
[attr.lang]="blockFlow.lang"
[attr.title]="blockFlow.title"
[contentBlock]="blockFlow.blocks"
></div>
}
@case ("block-flow-root") {
@let blockFlowRoot = block | blockFlowRoot;
<div
class="block-flow"
[ngClass]="blockFlowRoot.ngClass"
[attr.id]="blockFlowRoot.id"
[attr.lang]="blockFlowRoot.lang"
[attr.title]="blockFlowRoot.title"
[contentBlock]="blockFlowRoot.blocks"
></div>
}
@case ("section") {
@let section = block | section;
<section
class="section"
[ngClass]="section.ngClass"
[attr.id]="section.id"
[attr.lang]="section.lang"
[attr.title]="section.title"
[contentBlock]="section.blocks"
></section>
}
@case ("article") {
@let article = block | article;
<article
class="article"
[ngClass]="article.ngClass"
[attr.id]="article.id"
[attr.lang]="article.lang"
[attr.title]="article.title"
[contentBlock]="article.blocks"
></article>
}
@case ("aside") {
@let aside = block | aside;
<aside
class="aside"
[ngClass]="aside.ngClass"
[attr.id]="aside.id"
[attr.lang]="aside.lang"
[attr.title]="aside.title"
[contentBlock]="aside.blocks"
></aside>
}
@case ("mesh") {
@let mesh = block | mesh;
<div class="mesh" [ngClass]="mesh.ngClass" [attr.id]="mesh.id" [attr.lang]="mesh.lang" [attr.title]="mesh.title">
@for (meshItem of mesh.meshItems; track $index) {
<div
class="mesh-item"
[ngClass]="meshItem.ngClass"
[attr.id]="meshItem.id"
[attr.lang]="meshItem.lang"
[attr.title]="meshItem.title"
[contentBlock]="meshItem.blocks"
></div>
}
</div>
}
@case ("heading") {
@let heading = block | heading;
@switch (heading.level) {
@case (1) {
<h1
class="heading"
[ngClass]="['x-small:size:medium', 'small:size:large', 'medium:size:x-large'] | joinArray: heading.ngClass"
[attr.id]="heading.id"
[attr.lang]="heading.lang"
[attr.title]="heading.title"
[contentInline]="heading.inlines"
></h1>
}
@case (2) {
<h2
class="heading"
[ngClass]="['x-small:size:small', 'small:size:medium', 'medium:size:large'] | joinArray: heading.ngClass"
[attr.id]="heading.id"
[attr.lang]="heading.lang"
[attr.title]="heading.title"
[contentInline]="heading.inlines"
></h2>
}
@case (3) {
<h3
class="heading"
[ngClass]="['x-small:size:x-small', 'small:size:small', 'medium:size:medium'] | joinArray: heading.ngClass"
[attr.id]="heading.id"
[attr.lang]="heading.lang"
[attr.title]="heading.title"
[contentInline]="heading.inlines"
></h3>
}
}
}
@case ("paragraph") {
@let paragraph = block | paragraph;
<p
class="paragraph"
[ngClass]="['x-small:size:medium', 'small:size:large', 'medium:size:x-large'] | joinArray: paragraph.ngClass"
[attr.id]="paragraph.id"
[attr.lang]="paragraph.lang"
[attr.title]="paragraph.title"
[contentInline]="paragraph.inlines"
></p>
}
@case ("address") {
@let address = block | address;
<p
class="address"
[ngClass]="['x-small:size:medium', 'small:size:large', 'medium:size:x-large'] | joinArray: address.ngClass"
[attr.id]="address.id"
[attr.lang]="address.lang"
[attr.title]="address.title"
[contentInline]="address.inlines"
></p>
}
@case ("list") {
@let list = block | list;
@switch (list.type) {
@case ("ordered") {
<ol
class="list"
[ngClass]="
['x-small:size:medium', 'small:size:large', 'medium:size:x-large', 'x-small:type:ordered']
| joinArray: list.ngClass
"
[attr.id]="list.id"
[attr.lang]="list.lang"
[attr.title]="list.title"
>
@for (listItem of list.listItems; track $index) {
<li
class="list-item"
[ngClass]="listItem.ngClass"
[attr.id]="listItem.id"
[attr.lang]="listItem.lang"
[attr.title]="listItem.title"
[contentInline]="listItem.inlines"
></li>
}
</ol>
}
@case ("unordered") {
<ul
class="list"
[ngClass]="
['x-small:size:medium', 'small:size:large', 'medium:size:x-large', 'x-small:type:unordered']
| joinArray: list.ngClass
"
[attr.id]="list.id"
[attr.lang]="list.lang"
[attr.title]="list.title"
>
@for (listItem of list.listItems; track $index) {
<li
class="list-item"
[ngClass]="listItem.ngClass"
[attr.id]="listItem.id"
[attr.lang]="listItem.lang"
[attr.title]="listItem.title"
[contentInline]="listItem.inlines"
></li>
}
</ul>
}
}
}
@case ("menu") {
@let menu = block | menu;
<ul
class="menu"
[ngClass]="['x-small:size:medium', 'small:size:large', 'medium:size:x-large'] | joinArray: menu.ngClass"
[attr.id]="menu.id"
[attr.lang]="menu.lang"
[attr.title]="menu.title"
>
@for (menuItem of menu.menuItems; track $index) {
<li
class="menu-item"
[ngClass]="menuItem.ngClass"
[attr.id]="menuItem.id"
[attr.lang]="menuItem.lang"
[attr.title]="menuItem.title"
[contentInline]="menuItem.inlines"
></li>
}
</ul>
}
@case ("description") {
@let description = block | description;
<dl
class="description"
[ngClass]="['x-small:size:medium', 'small:size:large', 'medium:size:x-large'] | joinArray: description.ngClass"
[attr.id]="description.id"
[attr.lang]="description.lang"
[attr.title]="description.title"
>
@for (descriptionItem of description.descriptionItems; track $index) {
@switch (descriptionItem.type) {
@case ("term") {
<dt
class="description-term"
[ngClass]="descriptionItem.ngClass"
[attr.id]="descriptionItem.id"
[attr.lang]="descriptionItem.lang"
[attr.title]="descriptionItem.title"
[contentInline]="descriptionItem.inlines"
></dt>
}
@case ("detail") {
<dd
class="description-detail"
[ngClass]="descriptionItem.ngClass"
[attr.id]="descriptionItem.id"
[attr.lang]="descriptionItem.lang"
[attr.title]="descriptionItem.title"
[contentInline]="descriptionItem.inlines"
></dd>
}
}
}
</dl>
}
@case ("separator") {
@let separator = block | separator;
<hr class="separator" [ngClass]="separator.ngClass" />
}
}
}
<ng-content />
Inline Component
inline.component.ts
import { ChangeDetectionStrategy, Component, input, ViewEncapsulation } from "@angular/core";
import { NgClass, NgOptimizedImage } from "@angular/common";
import { RouterLink } from "@angular/router";
import { JoinArrayPipe } from "../../pipes/join-array/join-array.pipe";
import { InlineFlowPipe } from "../../pipes/inline-flow/inline-flow.pipe";
import { InlineFlowRootPipe } from "../../pipes/inline-flow-root/inline-flow-root.pipe";
import { HiddenPipe } from "../../pipes/hidden/hidden.pipe";
import { StrongPipe } from "../../pipes/strong/strong.pipe";
import { EmphasisPipe } from "../../pipes/emphasis/emphasis.pipe";
import { AbbreviationPipe } from "../../pipes/abbreviation/abbreviation.pipe";
import { TimePipe } from "../../pipes/time/time.pipe";
import { SubscriptPipe } from "../../pipes/subscript/subscript.pipe";
import { SuperscriptPipe } from "../../pipes/superscript/superscript.pipe";
import { LinkPipe } from "../../pipes/link/link.pipe";
import { AnchorPipe } from "../../pipes/anchor/anchor.pipe";
import { ButtonPipe } from "../../pipes/button/button.pipe";
import { ImagePipe } from "../../pipes/image/image.pipe";
import { LogoPipe } from "../../pipes/logo/logo.pipe";
import { IconPipe } from "../../pipes/icon/icon.pipe";
import { LinebreakPipe } from "../../pipes/linebreak/linebreak.pipe";
import { Inline } from "../../types/inline/inline";
@Component({
selector: "[contentInline]",
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
imports: [
NgClass,
NgOptimizedImage,
RouterLink,
JoinArrayPipe,
InlineFlowPipe,
InlineFlowRootPipe,
HiddenPipe,
StrongPipe,
EmphasisPipe,
AbbreviationPipe,
TimePipe,
SubscriptPipe,
SuperscriptPipe,
LinkPipe,
AnchorPipe,
ButtonPipe,
ImagePipe,
LogoPipe,
IconPipe,
LinebreakPipe,
],
templateUrl: "./inline.component.html",
})
export class InlineComponent {
readonly inlines = input.required<Inline[]>({ alias: "contentInline" });
}
inline.component.html
@for (inline of inlines(); track $index) {
@switch (inline["@type"]) {
@case ("inline-flow") {
@let inlineFlow = inline | inlineFlow;
<span
class="inline-flow"
[ngClass]="inlineFlow.ngClass"
[attr.id]="inlineFlow.id"
[attr.lang]="inlineFlow.lang"
[attr.title]="inlineFlow.title"
[contentInline]="inlineFlow.inlines ?? []"
>{{ inlineFlow.text }}</span
>
}
@case ("inline-flow-root") {
@let inlineFlowRoot = inline | inlineFlowRoot;
<span
class="inline-flow-root"
[ngClass]="inlineFlowRoot.ngClass"
[attr.id]="inlineFlowRoot.id"
[attr.lang]="inlineFlowRoot.lang"
[attr.title]="inlineFlowRoot.title"
[contentInline]="inlineFlowRoot.inlines ?? []"
>{{ inlineFlowRoot.text }}</span
>
}
@case ("hidden") {
@let hidden = inline | hidden;
<span
class="hidden"
[ngClass]="hidden.ngClass"
[attr.id]="hidden.id"
[attr.lang]="hidden.lang"
[attr.title]="hidden.title"
[contentInline]="hidden.inlines ?? []"
>{{ hidden.text }}</span
>
}
@case ("strong") {
@let strong = inline | strong;
<strong
class="strong"
[ngClass]="strong.ngClass"
[attr.id]="strong.id"
[attr.lang]="strong.lang"
[attr.title]="strong.title"
[contentInline]="strong.inlines ?? []"
>{{ strong.text }}</strong
>
}
@case ("emphasis") {
@let emphasis = inline | emphasis;
<em
class="emphasis"
[ngClass]="emphasis.ngClass"
[attr.id]="emphasis.id"
[attr.lang]="emphasis.lang"
[attr.title]="emphasis.title"
[contentInline]="emphasis.inlines ?? []"
>{{ emphasis.text }}</em
>
}
@case ("abbreviation") {
@let abbreviation = inline | abbreviation;
<abbr
class="abbreviation"
[ngClass]="abbreviation.ngClass"
[attr.id]="abbreviation.id"
[attr.lang]="abbreviation.lang"
[attr.title]="abbreviation.title"
[contentInline]="abbreviation.inlines ?? []"
>{{ abbreviation.text }}</abbr
>
}
@case ("time") {
@let time = inline | time;
<time
class="time"
[ngClass]="time.ngClass"
[attr.dateTime]="time.dateTime"
[attr.id]="time.id"
[attr.lang]="time.lang"
[attr.title]="time.title"
[contentInline]="time.inlines ?? []"
>{{ time.text }}</time
>
}
@case ("subscript") {
@let subscript = inline | subscript;
<sub
class="subscript"
[ngClass]="subscript.ngClass"
[attr.id]="subscript.id"
[attr.lang]="subscript.lang"
[attr.title]="subscript.title"
[contentInline]="subscript.inlines ?? []"
>{{ subscript.text }}</sub
>
}
@case ("superscript") {
@let superscript = inline | superscript;
<sup
class="superscript"
[ngClass]="superscript.ngClass"
[attr.id]="superscript.id"
[attr.lang]="superscript.lang"
[attr.title]="superscript.title"
[contentInline]="superscript.inlines ?? []"
>{{ superscript.text }}</sup
>
}
@case ("link") {
@let link = inline | link;
@switch (link.type) {
@case ("router") {
<a
class="link"
[ngClass]="['x-small:color:primary'] | joinArray: link.ngClass"
[routerLink]="link.routerLink"
[fragment]="link.fragment"
[queryParams]="link.queryParams"
[queryParamsHandling]="link.queryParamsHandling"
[attr.id]="link.id"
[attr.lang]="link.lang"
[attr.title]="link.title"
[contentInline]="link.inlines ?? []"
>{{ link.text }}</a
>
}
@case ("href") {
<a
class="link"
[ngClass]="['x-small:color:primary'] | joinArray: link.ngClass"
[attr.href]="link.href"
[attr.target]="link.target"
[attr.id]="link.id"
[attr.lang]="link.lang"
[attr.title]="link.title"
[contentInline]="link.inlines ?? []"
>{{ link.text }}</a
>
}
}
}
@case ("anchor") {
@let anchor = inline | anchor;
@switch (anchor.type) {
@case ("router") {
<a
class="anchor"
[ngClass]="['x-small:color:primary'] | joinArray: anchor.ngClass"
[routerLink]="anchor.routerLink"
[fragment]="anchor.fragment"
[queryParams]="anchor.queryParams"
[queryParamsHandling]="anchor.queryParamsHandling"
[attr.id]="anchor.id"
[attr.lang]="anchor.lang"
[attr.title]="anchor.title"
[contentInline]="anchor.inlines ?? []"
>{{ anchor.text }}</a
>
}
@case ("href") {
<a
class="anchor"
[ngClass]="['x-small:color:primary'] | joinArray: anchor.ngClass"
[attr.href]="anchor.href"
[attr.target]="anchor.target"
[attr.id]="anchor.id"
[attr.lang]="anchor.lang"
[attr.title]="anchor.title"
[contentInline]="anchor.inlines ?? []"
>{{ anchor.text }}</a
>
}
}
}
@case ("button") {
@let button = inline | button;
@switch (button.type) {
@case ("router") {
<a
class="button"
[ngClass]="['x-small:color:2-inverse', 'x-small:background-color:primary'] | joinArray: button.ngClass"
[routerLink]="button.routerLink"
[fragment]="button.fragment"
[queryParams]="button.queryParams"
[queryParamsHandling]="button.queryParamsHandling"
[attr.id]="button.id"
[attr.lang]="button.lang"
[attr.title]="button.title"
[contentInline]="button.inlines ?? []"
>{{ button.text }}</a
>
}
@case ("href") {
<a
class="button"
[ngClass]="['x-small:color:2-inverse', 'x-small:background-color:primary'] | joinArray: button.ngClass"
[attr.href]="button.href"
[attr.target]="button.target"
[attr.id]="button.id"
[attr.lang]="button.lang"
[attr.title]="button.title"
[contentInline]="button.inlines ?? []"
>{{ button.text }}</a
>
}
}
}
@case ("image") {
@let image = inline | image;
<img
class="image"
[ngClass]="image.ngClass"
[ngSrc]="image.ngSrc"
[width]="image.width"
[height]="image.height"
[priority]="image.priority"
[placeholder]="image.placeholder"
[attr.alt]="image.alt"
[attr.id]="image.id"
[attr.lang]="image.lang"
[attr.title]="image.title"
/>
}
@case ("logo") {
@let logo = inline | logo;
<svg
class="logo"
[ngClass]="logo.ngClass"
[attr.viewBox]="logo.viewBox"
[attr.xmlns]="'http://www.w3.org/2000/svg'"
[attr.aria-hidden]="true"
>
@for (logoItem of logo.logoItems; track $index) {
<path [attr.d]="logoItem.d" [attr.fill]="logoItem.fill" />
}
</svg>
}
@case ("icon") {
@let icon = inline | icon;
<svg
class="icon"
[ngClass]="icon.ngClass"
[attr.viewBox]="icon.viewBox"
[attr.xmlns]="'http://www.w3.org/2000/svg'"
[attr.aria-hidden]="true"
>
@for (iconItem of icon.iconItems; track $index) {
<path [attr.d]="iconItem.d" [attr.fill]="iconItem.fill" />
}
</svg>
}
@case ("linebreak") {
@let linebreak = inline | linebreak;
<br class="linebreak" [ngClass]="linebreak.ngClass" />
}
}
}
<ng-content />
Generating the Content
When the blocks are retrieved from Firestore, they are simply loaded as follows (example):
<article class="article" [contentBlock]="document.blocks"></article>
The Issue
After a number of iterations, SSR stops rendering the HTML, and instead, it outputs a <!--container--> instead, which is simply used for hydration.
What Should Happen
SSR should wait until all HTML is rendered and avoid outputing <!--container--> as this is not good for SEO.
Please provide a link to a minimal reproduction of the bug
No response
Please provide the exception or error you saw
Please provide the environment you discovered this bug in (run ng version)
Angular CLI: 19.2.7
Node: 22.14.0
Package Manager: npm 10.9.2
OS: win32 x64
Angular: 19.2.6
... common, compiler, compiler-cli, core, platform-browser
... platform-browser-dynamic, platform-server, router
Package Version
---------------------------------------------------------
@angular-devkit/architect 0.1902.7
@angular-devkit/build-angular 19.2.7
@angular-devkit/core 19.2.7
@angular-devkit/schematics 19.2.7
@angular/cli 19.2.7
@angular/fire 19.1.0
@angular/ssr 19.2.7
@schematics/angular 19.2.7
rxjs 7.8.2
typescript 5.8.3
Anything else?
No response
Metadata
Metadata
Assignees
Labels
area: commonIssues related to APIs in the @angular/common packageIssues related to APIs in the @angular/common packagearea: serverIssues related to server-side renderingIssues related to server-side renderingcommon: image directiveneeds reproductionThis issue needs a reproduction in order for the team to investigate furtherThis issue needs a reproduction in order for the team to investigate further