@paperbits/core
Version:
Paperbits core components.
275 lines (229 loc) • 11.4 kB
text/typescript
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}`);
}
}
}