UNPKG

@paperbits/core

Version:
275 lines (229 loc) 11.4 kB
import * as Utils from "@paperbits/common/utils"; import parallel from "await-parallel-limit"; import { IPublisher, HtmlPage, HtmlPagePublisher, SitemapBuilder } from "@paperbits/common/publishing"; import { normalizePermalink } from "@paperbits/common/permalinks/utils"; import { maxParallelPublisingTasks } from "@paperbits/common/constants"; import { IBlobStorage, Query } from "@paperbits/common/persistence"; import { IPageService, PageContract } from "@paperbits/common/pages"; import { ISiteService, SiteSettingsContract } from "@paperbits/common/sites"; import { Logger } from "@paperbits/common/logging"; import { ILocaleService, LocaleModel } from "@paperbits/common/localization"; import { IMediaService } from "@paperbits/common/media"; import { StyleCompiler, StyleManager } from "@paperbits/common/styles"; import { StyleBuilder } from "@paperbits/styles"; import { OpenGraphType } from "@paperbits/common/publishing/openGraph"; import { MimeTypes, RegExps } from "@paperbits/common"; import { SourceLink } from "@paperbits/common/publishing/sourceLink"; import { ISettingsProvider } from "@paperbits/common/configuration"; import { SearchIndexBuilder } from "@paperbits/common/search"; const globalStylesheetPermalink = `/styles/styles.css`; const rootHtmlPageFilePath = "/index.html"; export class PagePublisher implements IPublisher { private localStyleBuilder: StyleBuilder; constructor( protected readonly pageService: IPageService, protected readonly siteService: ISiteService, protected readonly mediaService: IMediaService, protected readonly outputBlobStorage: IBlobStorage, protected readonly htmlPagePublisher: HtmlPagePublisher, protected readonly styleCompiler: StyleCompiler, protected readonly localeService: ILocaleService, protected readonly sitemapBuilder: SitemapBuilder, protected readonly searchIndexBuilder: SearchIndexBuilder, protected readonly settingsProvider: ISettingsProvider, protected readonly logger: Logger ) { this.localStyleBuilder = new StyleBuilder(this.outputBlobStorage); } public async renderPage(page: HtmlPage): Promise<string> { try { const htmlContent = await this.htmlPagePublisher.renderHtml(page); return htmlContent; } catch (error) { throw new Error(`Unable to render page "${page.title}": ${error.stack || error.message}`); } } private async getFaviconPermalink(faviconSourceKey: string): Promise<string> { try { const media = await this.mediaService.getMediaByKey(faviconSourceKey); return media?.permalink; } catch (error) { this.logger.trackEvent("Publishing", { message: "Could not retrieve favicon." }); } } private getIndexableContent(html: string): string { return html; // const regex = /<main.*>([\s\S]*)<\/main>/g; // const match = regex.exec(html); // if (!match || match.length < 1) { // return null; // } // const mainContent = match[1]; // return mainContent; } private async renderAndUpload(settings: SiteSettingsContract, faviconPermalink: string, page: PageContract, globalStylesLink: SourceLink, locale?: LocaleModel): Promise<void> { if (!page.permalink) { this.logger.trackEvent("Publishing", { message: `Skipping page "${page.title}" with no permalink specified.` }); return; } let permalink = normalizePermalink(page.permalink); const isPermalinkValid = RegExps.permalink.test(permalink); if (!isPermalinkValid) { this.logger.trackEvent("Publishing", { message: `Skipping page "${page.title}" with invalid permalink: "${permalink}".` }); return; } this.logger.trackEvent("Publishing", { message: `Publishing page ${page.title}${locale ? ` (${locale.code})` : ""}...` }); try { const siteAuthor = settings?.author; const siteTitle = settings?.title; const siteDescription = settings?.description; const siteKeywords = settings?.keywords; const siteHostname = settings?.hostname; const localePrefix = locale ? `/${locale.code}` : ""; const pagePermalink = `${localePrefix}${page.permalink}`; const pageContent = await this.pageService.getPageContent(page.key, locale?.code); const pageUrl = siteHostname ? `https://${siteHostname}${pagePermalink}` : pagePermalink; const styleManager = new StyleManager(); const htmlPage: HtmlPage = { title: [page.title, siteTitle].join(" - "), description: page.description || siteDescription, keywords: page.keywords || siteKeywords, permalink: pagePermalink, url: pageUrl, siteHostName: siteHostname, faviconPermalink: faviconPermalink, locale: locale, styleReferences: [globalStylesLink], author: siteAuthor, socialShareData: page.socialShareData, openGraph: { type: page.permalink === "/" ? OpenGraphType.website : OpenGraphType.article, title: page.title || siteTitle, description: page.description || siteDescription, siteName: siteTitle }, bindingContext: { contentItemKey: page.key, styleManager: styleManager, navigationPath: pagePermalink, locale: locale?.code, template: { page: { value: pageContent, } } } }; if (page.jsonLd) { let structuredData: any; try { structuredData = JSON.parse(page.jsonLd); htmlPage.linkedData = structuredData; } catch (error) { this.logger.trackEvent("Publishing", { message: "Unable to parse page linked data." }); } } const htmlContent = await this.renderPage(htmlPage); this.sitemapBuilder.appendPermalink(pagePermalink); this.searchIndexBuilder.appendHtml(pagePermalink, htmlPage.title, htmlPage.description, this.getIndexableContent(htmlContent)); let permalink = normalizePermalink(pagePermalink); const isPermalinkValid = RegExps.permalink.test(permalink); if (!isPermalinkValid) { this.logger.trackEvent("Publishing", { message: `The permalink is invalid: "${permalink}". Skipping publishing.` }); return; } permalink = `${permalink}${rootHtmlPageFilePath}`; const uploadPath = permalink; const contentBytes = Utils.stringToUnit8Array(htmlContent); await this.outputBlobStorage.uploadBlob(uploadPath, contentBytes, MimeTypes.textHtml); } catch (error) { throw new Error(`Unable to publish page "${page.title}": ${error.stack || error.message}`); } } private async publishNonLocalized(siteSettings: SiteSettingsContract, faviconPermalink: string, globalStylesLink: SourceLink): Promise<void> { const query: Query<PageContract> = Query.from<PageContract>(); let pagesOfResults = await this.pageService.search(query); do { const tasks = []; const pages = pagesOfResults.value; for (const page of pages) { tasks.push(() => this.renderAndUpload(siteSettings, faviconPermalink, page, globalStylesLink)); } await parallel(tasks, maxParallelPublisingTasks); if (pagesOfResults.takeNext) { pagesOfResults = await pagesOfResults.takeNext(); } else { pagesOfResults = null; } } while (pagesOfResults); } private async publishLocalized(siteSettings: SiteSettingsContract, faviconPermalink: string, globalStylesLink: SourceLink, locales: LocaleModel[]): Promise<void> { const defaultLocale = await this.localeService.getDefaultLocaleCode(); for (const locale of locales) { const requestedLocale = locale.code === defaultLocale ? null : locale; const query: Query<PageContract> = Query.from<PageContract>(); let pagesOfResults = await this.pageService.search(query, requestedLocale?.code); do { const tasks = []; const pages = pagesOfResults.value; for (const page of pages) { tasks.push(() => this.renderAndUpload(siteSettings, faviconPermalink, page, globalStylesLink, requestedLocale)); } await parallel(tasks, maxParallelPublisingTasks); if (pagesOfResults.takeNext) { pagesOfResults = await pagesOfResults.takeNext(); } else { pagesOfResults = null; } } while (pagesOfResults); } } public async publish(): Promise<void> { const locales = await this.localeService.getLocales(); const staticAssetSuffix = await this.settingsProvider.getSetting<string>("staticAssetSuffix"); const localizationEnabled = locales.length > 0; const globalStyleSheet = await this.styleCompiler.getStyleSheet(); const stylesheetPermalink = Utils.appendSuffixToFileName(globalStylesheetPermalink, staticAssetSuffix); // Building global styles const signature = await this.localStyleBuilder.buildStyle(stylesheetPermalink, globalStyleSheet); const globalStylesLink: SourceLink = { src: stylesheetPermalink, integrity: signature }; try { const settings = await this.siteService.getSettings<any>(); const siteSettings: SiteSettingsContract = settings.site; const faviconPermalink = siteSettings?.faviconSourceKey ? await this.getFaviconPermalink(siteSettings.faviconSourceKey) : null; if (localizationEnabled) { await this.publishLocalized(siteSettings, faviconPermalink, globalStylesLink, locales); } else { await this.publishNonLocalized(siteSettings, faviconPermalink, globalStylesLink); } } catch (error) { throw new Error(`Unable to complete pages publishing. ${error.stack || error.message}`); } } }