From ee3fde630f50ed1d3eaa0a49f095a05066ff3f98 Mon Sep 17 00:00:00 2001 From: Francisco de la Vega Date: Fri, 24 Apr 2026 12:23:03 +0200 Subject: [PATCH 1/2] Improve blog features --- src/app/app-routing.module.ts | 3 +- .../blog-entry-detail.component.html | 73 +++-- .../blog-entry-detail.component.spec.ts | 121 +++++-- .../blog-entry-detail.component.ts | 202 +++++++++++- .../pages/dome-blog/dome-blog.component.html | 68 +++- .../dome-blog/dome-blog.component.spec.ts | 85 +++-- .../pages/dome-blog/dome-blog.component.ts | 145 ++++++++- .../entry-form/entry-form.component.html | 93 +++++- .../entry-form/entry-form.component.spec.ts | 176 ++++++++-- .../entry-form/entry-form.component.ts | 304 +++++++++++++++++- .../dome-blog-service.service.spec.ts | 13 + src/app/services/dome-blog-service.service.ts | 10 +- 12 files changed, 1159 insertions(+), 134 deletions(-) diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index e81c2c53..2a66560d 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -124,7 +124,7 @@ const routes: Routes = [ component: DomeBlogComponent }, { - path: 'blog/:id', + path: 'blog/:slugOrId', component: BlogEntryDetailComponent }, { @@ -158,4 +158,3 @@ const routes: Routes = [ exports: [RouterModule] }) export class AppRoutingModule { } - diff --git a/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.html b/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.html index 4deb7f3d..4f1bdfe4 100644 --- a/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.html +++ b/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.html @@ -1,23 +1,5 @@
-
-
-
-

- {{entry.title}} -

-

- Created by - - {{entry.author}} - - on {{entry.date | date:'EEEE, dd/MM/yy, HH:mm'}}. -

-
-
-
+ + diff --git a/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.spec.ts b/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.spec.ts index b0c7a884..34990a48 100644 --- a/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.spec.ts +++ b/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.spec.ts @@ -1,28 +1,109 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { TranslateModule } from '@ngx-translate/core'; -import { RouterTestingModule } from '@angular/router/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { MarkdownModule } from 'ngx-markdown'; - import { BlogEntryDetailComponent } from './blog-entry-detail.component'; +import { convertToParamMap } from '@angular/router'; describe('BlogEntryDetailComponent', () => { - let component: BlogEntryDetailComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - imports: [BlogEntryDetailComponent, HttpClientTestingModule, RouterTestingModule, TranslateModule.forRoot(), MarkdownModule.forRoot()] - }) - .compileComponents(); - - fixture = TestBed.createComponent(BlogEntryDetailComponent); - component = fixture.componentInstance; - }); + const buildComponent = (slugOrId = 'entry-slug', serviceOverrides?: Partial) => { + const route = { + snapshot: { + paramMap: convertToParamMap({ slugOrId }) + } + } as any; + const router = { navigate: jasmine.createSpy('navigate') } as any; + const domeBlogService = { + getBlogEntries: jasmine.createSpy('getBlogEntries').and.resolveTo([ + { _id: 'entry-1', slug: 'entry-slug' } + ]), + getBlogEntryById: jasmine.createSpy('getBlogEntryById').and.resolveTo({ + _id: 'entry-1', + title: 'Entry title', + metaDescription: 'Meta description', + content: 'Body content' + }), + deleteBlogEntry: jasmine.createSpy('deleteBlogEntry').and.resolveTo({ ok: true }), + ...serviceOverrides + } as any; + + const localStorageService = { + getObject: jasmine.createSpy('getObject').and.returnValue({}) + } as any; + const titleService = { setTitle: jasmine.createSpy('setTitle') } as any; + const metaService = { updateTag: jasmine.createSpy('updateTag') } as any; + + const component = new BlogEntryDetailComponent( + route, + router, + domeBlogService, + localStorageService, + titleService, + metaService + ); + + return { component, router, domeBlogService, titleService, metaService }; + }; it('should create', () => { + const { component } = buildComponent(); expect(component).toBeTruthy(); }); + + it('should resolve slug to entry and apply seo metadata on init', async () => { + const { component, domeBlogService, titleService, metaService } = buildComponent('entry-slug'); + + await component.ngOnInit(); + + expect(domeBlogService.getBlogEntries).toHaveBeenCalled(); + expect(domeBlogService.getBlogEntryById).toHaveBeenCalledWith('entry-1'); + expect(titleService.setTitle).toHaveBeenCalledWith('Entry title'); + expect(metaService.updateTag).toHaveBeenCalledWith({ name: 'description', content: 'Meta description' }); + }); + + it('should return featured image from string or object', () => { + const { component } = buildComponent(); + component.entry = { featuredImage: ' https://cdn/test.png ' }; + expect(component.getFeaturedImage()).toBe('https://cdn/test.png'); + + component.entry = { featuredImage: { url: ' https://cdn/obj.png ' } }; + expect(component.getFeaturedImage()).toBe('https://cdn/obj.png'); + }); + + it('should normalize tags from array and csv', () => { + const { component } = buildComponent(); + component.entry = { tags: [' ai ', '', 'news'] }; + expect(component.getEntryTags()).toEqual(['ai', 'news']); + + component.entry = { tags: 'one, two, , three' }; + expect(component.getEntryTags()).toEqual(['one', 'two', 'three']); + }); + + it('should open delete confirmation when entry id exists', () => { + const { component } = buildComponent(); + component.entry = { _id: 'entry-2', title: 'Delete me' }; + + component.openDeleteDialog(); + + expect(component.showDeleteConfirm).toBeTrue(); + expect(component.deleteConfirmMessage).toContain('Delete me'); + }); + + it('should delete entry and navigate back', async () => { + const { component, domeBlogService, router } = buildComponent(); + component.entry = { _id: 'entry-5', title: 'Post' }; + + await component.confirmDeleteEntry(); + + expect(domeBlogService.deleteBlogEntry).toHaveBeenCalledWith('entry-5'); + expect(router.navigate).toHaveBeenCalledWith(['/blog']); + expect(component.deleting).toBeFalse(); + }); + + it('should fallback to id lookup for object ids', async () => { + const objectId = '507f1f77bcf86cd799439011'; + const { component, domeBlogService } = buildComponent(objectId); + domeBlogService.getBlogEntryById.and.resolveTo({ _id: objectId, title: 'By id' }); + + const result = await component.getEntryBySlugOrId(objectId); + + expect(domeBlogService.getBlogEntryById).toHaveBeenCalledWith(objectId); + expect(result._id).toBe(objectId); + }); }); diff --git a/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.ts b/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.ts index ed494205..095edd3c 100644 --- a/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.ts +++ b/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.ts @@ -3,11 +3,16 @@ import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; import { DomeBlogServiceService } from "src/app/services/dome-blog-service.service" import { MarkdownComponent } from "ngx-markdown"; +import { LocalStorageService } from "src/app/services/local-storage.service"; +import { LoginInfo } from "src/app/models/interfaces"; +import * as moment from 'moment'; +import { ConfirmDialogComponent } from "src/app/shared/confirm-dialog/confirm-dialog.component"; +import { Meta, Title } from "@angular/platform-browser"; @Component({ selector: 'app-blog-entry-detail', standalone: true, - imports: [CommonModule, MarkdownComponent], + imports: [CommonModule, MarkdownComponent, ConfirmDialogComponent], templateUrl: './blog-entry-detail.component.html', styleUrl: './blog-entry-detail.component.css' }) @@ -15,18 +20,207 @@ export class BlogEntryDetailComponent implements OnInit { constructor( private route: ActivatedRoute, private router: Router, - private domeBlogService: DomeBlogServiceService + private domeBlogService: DomeBlogServiceService, + private localStorage: LocalStorageService, + private titleService: Title, + private metaService: Meta ) { } entry:any={}; blogId:any=''; + partyId:any=''; + checkAdmin:boolean=false; + deleting:boolean=false; + showDeleteConfirm = false; + deleteConfirmTitle = 'Delete entry'; + deleteConfirmMessage = ''; + deleteConfirmButtonText = 'Delete'; + deleteConfirmButtonClass = 'px-4 py-2 text-sm font-medium text-white bg-red-700 border border-transparent rounded-md hover:bg-red-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500'; async ngOnInit(): Promise { - this.blogId = this.route.snapshot.paramMap.get('id')!; - this.entry=await this.domeBlogService.getBlogEntryById(this.blogId); + this.initPartyInfo(); + this.blogId = this.route.snapshot.paramMap.get('slugOrId') || this.route.snapshot.paramMap.get('id')!; + this.entry = await this.getEntryBySlugOrId(this.blogId); + this.applySeoMetadata(); } goBack(){ this.router.navigate(['/blog']); } + + goToUpdate() { + const entryId = this.entry?._id || (this.isObjectId(this.blogId) ? this.blogId : null); + if (!entryId) { + return; + } + + this.router.navigate(['/blog-entry/', entryId]); + } + + canManageEntry(): boolean { + return this.checkAdmin; + } + + openDeleteDialog() { + const entryId = this.entry?._id || (this.isObjectId(this.blogId) ? this.blogId : null); + if (!entryId || this.deleting) { + return; + } + + this.deleteConfirmMessage = `Are you sure you want to delete "${this.entry?.title || 'this post'}"? This action cannot be undone.`; + this.showDeleteConfirm = true; + } + + closeDeleteDialog() { + this.showDeleteConfirm = false; + } + + async confirmDeleteEntry() { + const entryId = this.entry?._id || (this.isObjectId(this.blogId) ? this.blogId : null); + if (!entryId) { + this.closeDeleteDialog(); + return; + } + + this.closeDeleteDialog(); + this.deleting = true; + try { + await this.domeBlogService.deleteBlogEntry(entryId); + this.goBack(); + } catch (error) { + console.error('There was an error while deleting the entry!', error); + } finally { + this.deleting = false; + } + } + + async getEntryBySlugOrId(slugOrId: string) { + if (!slugOrId) { + return {}; + } + + if (this.isObjectId(slugOrId)) { + try { + return await this.domeBlogService.getBlogEntryById(slugOrId); + } catch (error) { + } + } + + try { + const entries = await this.domeBlogService.getBlogEntries(); + const matchedEntry = entries.find((entry: any) => entry.slug === slugOrId); + if (matchedEntry?._id) { + try { + return await this.domeBlogService.getBlogEntryById(matchedEntry._id); + } catch (error) { + return matchedEntry; + } + } + + if (matchedEntry) { + return matchedEntry; + } + } catch (error) { + } + + try { + return await this.domeBlogService.getBlogEntryById(slugOrId); + } catch (error) { + return {}; + } + } + + isObjectId(value: string): boolean { + return /^[a-f\d]{24}$/i.test(value); + } + + initPartyInfo(){ + let aux = this.localStorage.getObject('login_items') as LoginInfo; + if(JSON.stringify(aux) != '{}' && (((aux.expire - moment().unix())-4) > 0)) { + if(aux.logged_as==aux.id){ + this.partyId = aux.partyId; + } else { + let loggedOrg = aux.organizations.find((element: { id: any; }) => element.id == aux.logged_as) + this.partyId = loggedOrg.partyId; + } + this.checkAdmin=aux.roles.some(role => + role.name === 'admin' + ); + } + } + + private applySeoMetadata() { + const title = (this.entry?.title || '').toString().trim(); + const metaDescription = this.getEntryMetaDescription(); + + if (title) { + this.titleService.setTitle(title); + } + + if (metaDescription) { + this.metaService.updateTag({ name: 'description', content: metaDescription }); + } + } + + private getEntryMetaDescription(): string { + const explicitMeta = (this.entry?.metaDescription || '').toString().trim(); + if (explicitMeta) { + return this.truncateText(explicitMeta, 160); + } + + const excerpt = (this.entry?.excerpt || '').toString().trim(); + if (excerpt) { + return this.truncateText(excerpt, 160); + } + + const plainContent = this.stripMarkdown((this.entry?.content || '').toString()); + return this.truncateText(plainContent, 160); + } + + getFeaturedImage(): string | null { + if (typeof this.entry?.featuredImage === 'string' && this.entry.featuredImage.trim().length > 0) { + return this.entry.featuredImage.trim(); + } + + if (typeof this.entry?.featuredImage?.url === 'string' && this.entry.featuredImage.url.trim().length > 0) { + return this.entry.featuredImage.url.trim(); + } + + return null; + } + + getEntryTags(): string[] { + const rawTags = this.entry?.tags; + if (Array.isArray(rawTags)) { + return rawTags + .map((tag) => (tag ?? '').toString().trim()) + .filter((tag) => tag.length > 0); + } + + if (typeof rawTags === 'string') { + return rawTags + .split(',') + .map((tag) => tag.trim()) + .filter((tag) => tag.length > 0); + } + + return []; + } + + private stripMarkdown(content: string): string { + return content + .replace(/!\[[^\]]*]\([^)]*\)/g, ' ') + .replace(/\[[^\]]*]\([^)]*\)/g, ' ') + .replace(/[`*_>#-]/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + } + + private truncateText(text: string, maxLength: number): string { + if (!text || text.length <= maxLength) { + return text; + } + + return `${text.slice(0, maxLength).trim()}...`; + } } diff --git a/src/app/pages/dome-blog/dome-blog.component.html b/src/app/pages/dome-blog/dome-blog.component.html index 7ce753b3..363c50e7 100644 --- a/src/app/pages/dome-blog/dome-blog.component.html +++ b/src/app/pages/dome-blog/dome-blog.component.html @@ -29,12 +29,21 @@

{{ entry.title }}

- @if(entry.partyId == partyId){ - + @if(canManageEntry(entry)){ +
+ + +
}

@@ -42,18 +51,43 @@

{{entry.author}} on {{entry.date | date:'EEEE, dd/MM/yy, HH:mm'}}.

- - - + @if(getEntryTags(entry); as tags){ + @if(tags.length > 0){ +
+ @for(tag of tags; track tag){ + + {{ tag }} + + } +
+ } + } +
+ @if(getFeaturedImage(entry); as featuredImage){ + + } +
+

+ {{ getEntryExcerpt(entry) }} +

+ +
+
}
+ + diff --git a/src/app/pages/dome-blog/dome-blog.component.spec.ts b/src/app/pages/dome-blog/dome-blog.component.spec.ts index ed40116c..7658d5e8 100644 --- a/src/app/pages/dome-blog/dome-blog.component.spec.ts +++ b/src/app/pages/dome-blog/dome-blog.component.spec.ts @@ -1,28 +1,73 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { TranslateModule } from '@ngx-translate/core'; -import { RouterTestingModule } from '@angular/router/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; - import { DomeBlogComponent } from './dome-blog.component'; +import { Subject } from 'rxjs'; describe('DomeBlogComponent', () => { - let component: DomeBlogComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - imports: [DomeBlogComponent, HttpClientTestingModule, RouterTestingModule, TranslateModule.forRoot()] - }) - .compileComponents(); - - fixture = TestBed.createComponent(DomeBlogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + const buildComponent = (entries: any[] = []) => { + const messages$ = new Subject(); + const router = { + navigate: jasmine.createSpy('navigate') + } as any; + const eventMessage = { messages$ } as any; + const localStorageService = { + getObject: jasmine.createSpy('getObject').and.returnValue({}) + } as any; + const domeBlogService = { + getBlogEntries: jasmine.createSpy('getBlogEntries').and.resolveTo(entries), + deleteBlogEntry: jasmine.createSpy('deleteBlogEntry').and.resolveTo({ ok: true }) + } as any; + + const component = new DomeBlogComponent(router, eventMessage, localStorageService, domeBlogService); + return { component, router, domeBlogService, messages$ }; + }; it('should create', () => { + const { component } = buildComponent(); expect(component).toBeTruthy(); }); + + it('should use slug for route id when available', () => { + const { component } = buildComponent(); + expect(component.getEntryRouteId({ _id: '1', slug: 'my-post' })).toBe('my-post'); + expect(component.getEntryRouteId({ _id: '1' })).toBe('1'); + }); + + it('should normalize tags from both array and csv string', () => { + const { component } = buildComponent(); + expect(component.getEntryTags({ tags: [' ai ', '', 'news'] })).toEqual(['ai', 'news']); + expect(component.getEntryTags({ tags: 'alpha, beta , , gamma' })).toEqual(['alpha', 'beta', 'gamma']); + }); + + it('should prioritize excerpt and metaDescription before markdown fallback', () => { + const { component } = buildComponent(); + expect(component.getEntryExcerpt({ excerpt: ' Short excerpt ' })).toBe('Short excerpt'); + expect(component.getEntryExcerpt({ excerpt: ' ', metaDescription: ' Meta text ' })).toBe('Meta text'); + expect(component.getEntryExcerpt({ content: '# Title **bold** [link](https://x.com)' })).toContain('Title'); + }); + + it('should open delete modal with selected entry', () => { + const { component } = buildComponent(); + component.openDeleteDialog({ _id: 'entry-1', title: 'Post A' }); + + expect(component.showDeleteConfirm).toBeTrue(); + expect(component.pendingDeleteEntry?._id).toBe('entry-1'); + expect(component.deleteConfirmMessage).toContain('Post A'); + }); + + it('should delete selected entry and refresh list', async () => { + const { component, domeBlogService } = buildComponent(); + spyOn(component, 'loadEntries').and.resolveTo(); + component.pendingDeleteEntry = { _id: 'entry-2', title: 'Post B' }; + + await component.confirmDeleteEntry(); + + expect(domeBlogService.deleteBlogEntry).toHaveBeenCalledWith('entry-2'); + expect(component.loadEntries).toHaveBeenCalled(); + expect(component.deletingEntryId).toBeNull(); + }); + + it('should navigate to details with computed route id', () => { + const { component, router } = buildComponent(); + component.goToDetails({ _id: 'entry-3', slug: 'custom-slug' }); + expect(router.navigate).toHaveBeenCalledWith(['/blog/', 'custom-slug']); + }); }); diff --git a/src/app/pages/dome-blog/dome-blog.component.ts b/src/app/pages/dome-blog/dome-blog.component.ts index 3cc71ac7..47c763ef 100644 --- a/src/app/pages/dome-blog/dome-blog.component.ts +++ b/src/app/pages/dome-blog/dome-blog.component.ts @@ -6,14 +6,13 @@ import {LocalStorageService} from "src/app/services/local-storage.service"; import { DomeBlogServiceService } from "src/app/services/dome-blog-service.service" import { LoginInfo } from 'src/app/models/interfaces'; import * as moment from 'moment'; -import { lastValueFrom, Subject } from 'rxjs'; -import { MarkdownComponent } from "ngx-markdown"; -import { takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { ConfirmDialogComponent } from "src/app/shared/confirm-dialog/confirm-dialog.component"; @Component({ selector: 'app-dome-blog', standalone: true, - imports: [CommonModule, MarkdownComponent], + imports: [CommonModule, ConfirmDialogComponent], templateUrl: './dome-blog.component.html', styleUrl: './dome-blog.component.css' }) @@ -34,13 +33,19 @@ export class DomeBlogComponent implements OnInit, OnDestroy { partyId:any=''; checkAdmin:boolean=false; private destroy$ = new Subject(); + deletingEntryId: string | null = null; + showDeleteConfirm = false; + pendingDeleteEntry: any = null; + deleteConfirmTitle = 'Delete entry'; + deleteConfirmMessage = ''; + deleteConfirmButtonText = 'Delete'; + deleteConfirmButtonClass = 'px-4 py-2 text-sm font-medium text-white bg-red-700 border border-transparent rounded-md hover:bg-red-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500'; entries:any[]=[ ] async ngOnInit(): Promise { this.initPartyInfo(); - let entries = await this.domeBlogService.getBlogEntries(); - this.entries=entries; + await this.loadEntries(); } ngOnDestroy(){ @@ -66,8 +71,8 @@ export class DomeBlogComponent implements OnInit, OnDestroy { } - goToDetails(id:any) { - this.router.navigate(['/blog/', id]); + goToDetails(entry:any) { + this.router.navigate(['/blog/', this.getEntryRouteId(entry)]); } goToCreate(){ @@ -78,11 +83,123 @@ export class DomeBlogComponent implements OnInit, OnDestroy { this.router.navigate(['/blog-entry/', id]); } - hasLongWord(str: string | undefined, threshold = 20) { - if(str){ - return str.split(/\s+/).some(word => word.length > threshold); - } else { - return false - } + canManageEntry(entry: any): boolean { + return this.checkAdmin; + } + + isDeletingEntry(entry: any): boolean { + return this.deletingEntryId === entry?._id; + } + + openDeleteDialog(entry: any) { + if (!entry?._id || this.isDeletingEntry(entry)) { + return; + } + + this.pendingDeleteEntry = entry; + this.deleteConfirmMessage = `Are you sure you want to delete "${entry.title}"? This action cannot be undone.`; + this.showDeleteConfirm = true; + } + + closeDeleteDialog() { + this.showDeleteConfirm = false; + this.pendingDeleteEntry = null; + } + + async confirmDeleteEntry() { + if (!this.pendingDeleteEntry?._id) { + this.closeDeleteDialog(); + return; + } + + this.deletingEntryId = this.pendingDeleteEntry._id; + this.closeDeleteDialog(); + try { + await this.domeBlogService.deleteBlogEntry(this.deletingEntryId); + await this.loadEntries(); + } catch (error) { + console.error('There was an error while deleting the entry!', error); + } finally { + this.deletingEntryId = null; + } + } + + async loadEntries() { + try { + let entries = await this.domeBlogService.getBlogEntries(); + this.entries = Array.isArray(entries) ? entries : []; + } catch (error) { + this.entries = []; + } + } + + getEntryRouteId(entry:any): string { + if (entry?.slug && typeof entry.slug === 'string' && entry.slug.trim().length > 0) { + return entry.slug.trim(); + } + + return entry?._id; + } + + getFeaturedImage(entry: any): string | null { + if (typeof entry?.featuredImage === 'string' && entry.featuredImage.trim().length > 0) { + return entry.featuredImage.trim(); + } + + if (typeof entry?.featuredImage?.url === 'string' && entry.featuredImage.url.trim().length > 0) { + return entry.featuredImage.url.trim(); + } + + return null; + } + + getEntryTags(entry: any): string[] { + const rawTags = entry?.tags; + if (Array.isArray(rawTags)) { + return rawTags + .map((tag) => (tag ?? '').toString().trim()) + .filter((tag) => tag.length > 0); + } + + if (typeof rawTags === 'string') { + return rawTags + .split(',') + .map((tag) => tag.trim()) + .filter((tag) => tag.length > 0); + } + + return []; + } + + getEntryExcerpt(entry: any): string { + const explicitExcerpt = (entry?.excerpt || '').toString().trim(); + if (explicitExcerpt) { + return explicitExcerpt; + } + + const metaDescription = (entry?.metaDescription || '').toString().trim(); + if (metaDescription) { + return metaDescription; + } + + const plainTextContent = this.stripMarkdown((entry?.content || '').toString()); + return this.truncateText(plainTextContent, 260); + } + + private stripMarkdown(content: string): string { + return content + .replace(/!\[[^\]]*]\([^)]*\)/g, ' ') + .replace(/\[[^\]]*]\([^)]*\)/g, ' ') + .replace(/[`*_>#-]/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + } + + private truncateText(text: string, maxLength: number): string { + if (!text || text.length <= maxLength) { + return text; + } + + return `${text.slice(0, maxLength).trim()}...`; } } diff --git a/src/app/pages/dome-blog/entry-form/entry-form.component.html b/src/app/pages/dome-blog/entry-form/entry-form.component.html index 32025eb1..58ed4528 100644 --- a/src/app/pages/dome-blog/entry-form/entry-form.component.html +++ b/src/app/pages/dome-blog/entry-form/entry-form.component.html @@ -1,17 +1,6 @@
-
-
-
-

- Create a new entry -

-
-
-