feat(content-management): expose SEO fields in Static Pages editor
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
ContentPage.seo (title/description/keywords/canonical/ogTitle/ogDescription/ ogImage) was already modeled and serialized to bootstrap JSON but had no editable UI. Added updateSeo() to the component and a translated SEO fieldset using app-form-field/app-input. Added seoSection/seoTitle/seoDescription/ seoKeywords/seoCanonical/seoOgTitle/seoOgDescription/seoOgImage translation keys to en/ru/hy and the Translations type. Verified in browser at /edit/static-pages: SEO section renders with translated labels for all three mock pages.
This commit is contained in:
@@ -71,6 +71,31 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<h4 class="page-card__subheading">{{ 'builder.seoSection' | translate }}</h4>
|
||||||
|
<div class="editor-grid two">
|
||||||
|
<app-form-field [label]="'builder.seoTitle' | translate">
|
||||||
|
<app-input [ngModel]="page.seo?.title || ''" (ngModelChange)="updateSeo(page.id, { title: $event })" />
|
||||||
|
</app-form-field>
|
||||||
|
<app-form-field [label]="'builder.seoDescription' | translate">
|
||||||
|
<app-input [ngModel]="page.seo?.description || ''" (ngModelChange)="updateSeo(page.id, { description: $event })" />
|
||||||
|
</app-form-field>
|
||||||
|
<app-form-field [label]="'builder.seoKeywords' | translate">
|
||||||
|
<app-input [ngModel]="page.seo?.keywords || ''" (ngModelChange)="updateSeo(page.id, { keywords: $event })" />
|
||||||
|
</app-form-field>
|
||||||
|
<app-form-field [label]="'builder.seoCanonical' | translate">
|
||||||
|
<app-input [ngModel]="page.seo?.canonical || ''" (ngModelChange)="updateSeo(page.id, { canonical: $event })" />
|
||||||
|
</app-form-field>
|
||||||
|
<app-form-field [label]="'builder.seoOgTitle' | translate">
|
||||||
|
<app-input [ngModel]="page.seo?.ogTitle || ''" (ngModelChange)="updateSeo(page.id, { ogTitle: $event })" />
|
||||||
|
</app-form-field>
|
||||||
|
<app-form-field [label]="'builder.seoOgDescription' | translate">
|
||||||
|
<app-input [ngModel]="page.seo?.ogDescription || ''" (ngModelChange)="updateSeo(page.id, { ogDescription: $event })" />
|
||||||
|
</app-form-field>
|
||||||
|
<app-form-field [label]="'builder.seoOgImage' | translate">
|
||||||
|
<app-input [ngModel]="page.seo?.ogImage || ''" (ngModelChange)="updateSeo(page.id, { ogImage: $event })" />
|
||||||
|
</app-form-field>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</app-card>
|
</app-card>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,15 @@
|
|||||||
color: var(--text-primary, #1e3c38);
|
color: var(--text-primary, #1e3c38);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-card__subheading {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-secondary, #5f6e6a);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
.toggle-grid {
|
.toggle-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/c
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ProjectEditorFacade } from '../../project-editor/facade/project-editor.facade';
|
import { ProjectEditorFacade } from '../../project-editor/facade/project-editor.facade';
|
||||||
import { ContentManagementFacade } from '../facade/content-management.facade';
|
import { ContentManagementFacade } from '../facade/content-management.facade';
|
||||||
import { ContentPage } from '../models/content-page.model';
|
import { ContentPage, ContentPageSeoConfig } from '../models/content-page.model';
|
||||||
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
||||||
import { MarketplaceHtmlEditorComponent } from '../../project-editor/components/html-editor/marketplace-html-editor.component';
|
import { MarketplaceHtmlEditorComponent } from '../../project-editor/components/html-editor/marketplace-html-editor.component';
|
||||||
import { ButtonComponent } from '../../../shared/ui/button/button.component';
|
import { ButtonComponent } from '../../../shared/ui/button/button.component';
|
||||||
@@ -81,6 +81,16 @@ export class StaticPagesEditorComponent {
|
|||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSeo(id: string, patch: Partial<ContentPageSeoConfig>): void {
|
||||||
|
this.persist(this.pages().map(page => page.id !== id ? page : ({
|
||||||
|
...page,
|
||||||
|
seo: {
|
||||||
|
...(page.seo ?? {}),
|
||||||
|
...patch,
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
move(id: string, direction: -1 | 1): void {
|
move(id: string, direction: -1 | 1): void {
|
||||||
const pages = [...this.pages()].sort((a, b) => a.order - b.order);
|
const pages = [...this.pages()].sort((a, b) => a.order - b.order);
|
||||||
const index = pages.findIndex(page => page.id === id);
|
const index = pages.findIndex(page => page.id === id);
|
||||||
|
|||||||
@@ -465,6 +465,14 @@ export const en: Translations = {
|
|||||||
orderLabel: 'Order',
|
orderLabel: 'Order',
|
||||||
translationTitle: 'Translation Title',
|
translationTitle: 'Translation Title',
|
||||||
htmlPreview: 'HTML',
|
htmlPreview: 'HTML',
|
||||||
|
seoSection: 'SEO',
|
||||||
|
seoTitle: 'SEO Title',
|
||||||
|
seoDescription: 'SEO Description',
|
||||||
|
seoKeywords: 'Keywords',
|
||||||
|
seoCanonical: 'Canonical URL',
|
||||||
|
seoOgTitle: 'OG Title',
|
||||||
|
seoOgDescription: 'OG Description',
|
||||||
|
seoOgImage: 'OG Image',
|
||||||
visible: 'Visible',
|
visible: 'Visible',
|
||||||
layoutLabel: 'Layout',
|
layoutLabel: 'Layout',
|
||||||
columns: 'Columns',
|
columns: 'Columns',
|
||||||
|
|||||||
@@ -465,6 +465,14 @@ export const hy: Translations = {
|
|||||||
orderLabel: 'Հերթականություն',
|
orderLabel: 'Հերթականություն',
|
||||||
translationTitle: 'Թարգմանության վերնագիր',
|
translationTitle: 'Թարգմանության վերնագիր',
|
||||||
htmlPreview: 'HTML',
|
htmlPreview: 'HTML',
|
||||||
|
seoSection: 'SEO',
|
||||||
|
seoTitle: 'SEO վերնագիր',
|
||||||
|
seoDescription: 'SEO նկարագրություն',
|
||||||
|
seoKeywords: 'Հիմնաբառեր',
|
||||||
|
seoCanonical: 'Canonical URL',
|
||||||
|
seoOgTitle: 'OG վերնագիր',
|
||||||
|
seoOgDescription: 'OG նկարագրություն',
|
||||||
|
seoOgImage: 'OG պատկեր',
|
||||||
visible: 'Տեսանելի',
|
visible: 'Տեսանելի',
|
||||||
layoutLabel: 'Layout',
|
layoutLabel: 'Layout',
|
||||||
columns: 'Սյուներ',
|
columns: 'Սյուներ',
|
||||||
|
|||||||
@@ -465,6 +465,14 @@ export const ru: Translations = {
|
|||||||
orderLabel: 'Порядок',
|
orderLabel: 'Порядок',
|
||||||
translationTitle: 'Заголовок перевода',
|
translationTitle: 'Заголовок перевода',
|
||||||
htmlPreview: 'HTML',
|
htmlPreview: 'HTML',
|
||||||
|
seoSection: 'SEO',
|
||||||
|
seoTitle: 'SEO заголовок',
|
||||||
|
seoDescription: 'SEO описание',
|
||||||
|
seoKeywords: 'Ключевые слова',
|
||||||
|
seoCanonical: 'Canonical URL',
|
||||||
|
seoOgTitle: 'OG заголовок',
|
||||||
|
seoOgDescription: 'OG описание',
|
||||||
|
seoOgImage: 'OG изображение',
|
||||||
visible: 'Видимость',
|
visible: 'Видимость',
|
||||||
layoutLabel: 'Layout',
|
layoutLabel: 'Layout',
|
||||||
columns: 'Колонки',
|
columns: 'Колонки',
|
||||||
|
|||||||
@@ -463,6 +463,14 @@ export interface Translations {
|
|||||||
orderLabel: string;
|
orderLabel: string;
|
||||||
translationTitle: string;
|
translationTitle: string;
|
||||||
htmlPreview: string;
|
htmlPreview: string;
|
||||||
|
seoSection: string;
|
||||||
|
seoTitle: string;
|
||||||
|
seoDescription: string;
|
||||||
|
seoKeywords: string;
|
||||||
|
seoCanonical: string;
|
||||||
|
seoOgTitle: string;
|
||||||
|
seoOgDescription: string;
|
||||||
|
seoOgImage: string;
|
||||||
visible: string;
|
visible: string;
|
||||||
layoutLabel: string;
|
layoutLabel: string;
|
||||||
columns: string;
|
columns: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user