arti24/packages/websites/WebSite.ts

293 lines
8.6 KiB
TypeScript
Executable File

import { Express, Request } from 'express';
import { WebPage } from './WebPage.js';
import { Paths } from '../paths/Paths.js';
import fs from 'fs';
import { isProduction } from '../common-configuration.js';
import { PicturesFileComponent } from '../utils/PictureFileComponent.js';
import { Language } from '../utils/Tools.js';
import { DomainDetails } from './../Domain.js';
import { Routes } from './../paths/Routes.js';
import { schemaGraph } from './../schema-org/SchemaGraph.js';
import { SitemapItemLoose, SitemapStream, streamToPromise } from 'sitemap';
import { Readable } from 'stream';
import formatXml from 'xml-formatter';
import {
IIdThing,
ToIdThingWebPageWrapper,
ToThingWebSiteWrapper,
} from '../schema-org/Graph.js';
import { GraphOrg } from './FormPage.js';
import { Picture } from './../pictures/Picture.js';
// Cache dla często używanych danych
interface PageCache {
routes: Map<string, WebPage>;
noCacheRoutes: string[];
sitemap?: Promise<string>;
robotsTxt?: string;
}
export abstract class WebSite extends GraphOrg {
getAllUrls(): string[] {
return Array.from(this.cache.routes.keys());
}
protected pagesMap = new Map<string, WebPage>();
protected readonly domainUrl: string;
private cache: PageCache;
constructor(
public pictures: PicturesFileComponent,
protected paths: Paths,
private readonly logo: Picture | undefined,
) {
super();
this.domainUrl = DomainDetails.getSingleton().domainUrl;
this.cache = this.createEmptyCache();
}
getPages() {
return this.pagesMap.values();
}
private createEmptyCache(): PageCache {
return {
routes: new Map(),
noCacheRoutes: ['/sw.js'],
};
}
getSchemaOrgId(): string {
return `${this.domainUrl}/#website`;
}
getId(): string {
return `${this.domainUrl}/#website`;
}
initPages(pages: WebPage[]): void {
schemaGraph.add(new ToThingWebSiteWrapper(this));
const newCache = this.createEmptyCache();
for (const page of pages) {
this.pagesMap.set(page.getId(), page);
const plRoute = page.getRoute(Language.pl);
// Dodaj główne trasy
newCache.routes.set(plRoute, page);
const enRoute = page.getRoute(Language.en);
newCache.routes.set(enRoute, page);
// Dodaj przekierowania
// Dodaj trasy bez cache jeśli potrzeba
if (!page.cached) {
newCache.noCacheRoutes.push(page.getRoute(Language.pl));
newCache.noCacheRoutes.push(page.getRoute(Language.en));
}
// Dodaj do schemaGraph
schemaGraph.add({
toThing: (language: Language): IIdThing[] => {
return new ToIdThingWebPageWrapper(page).asSchemaOrg(
language,
);
},
});
}
this.cache = newCache;
}
getNoCacheRoutes(): string[] {
return [...this.cache.noCacheRoutes];
}
getRenderRoutes(): IterableIterator<[string, WebPage]> {
return this.cache.routes.entries();
}
getBasePath(isCommon: boolean): string {
return this.paths.getViewsDir(isCommon);
}
getKeys(): IterableIterator<string> {
return this.cache.routes.keys();
}
has(uri: string): boolean {
return this.cache.routes.has(uri);
}
getPageName(req: Request): string | null {
const path = req.path;
const webPage = this.getByRoute(path);
return webPage?.getId() ?? null;
}
getByRoute(route: string): WebPage | undefined {
const normalizedRoute = this.normalizeRoute(route);
return this.cache.routes.get(normalizedRoute);
}
getByKey(pageKey: string): WebPage | undefined {
return this.pagesMap.get(pageKey);
}
private normalizeRoute(route: string): string {
return route.length > 1 && route.endsWith('/')
? route.slice(0, -1)
: route;
}
getSitemap(): Promise<string> {
if (!this.cache.sitemap) {
this.cache.sitemap = this.generateSitemap();
}
return this.cache.sitemap;
}
private async generateSitemap(): Promise<string> {
const lastmod = new Date().toISOString().split('T')[0];
const entries: SitemapItemLoose[] = [];
this.addWebPagesSitemapItem(entries, lastmod);
return this.createSitemap(entries);
}
protected async createSitemap(
entries: SitemapItemLoose[],
): Promise<string> {
const sitemapStream = new SitemapStream({
hostname: this.domainUrl,
});
const xml = await streamToPromise(
Readable.from(entries).pipe(sitemapStream),
);
return xml.toString();
}
getUrls(webPage: WebPage, language: Language) {
const canonical = webPage.getUrl(language);
return {
canonical,
alternate_pl: webPage.getUrl(Language.pl),
alternate_en: webPage.getUrl(Language.en),
alternate_default: webPage.getUrl(Language.pl),
};
}
private addWebPagesSitemapItem(
entries: SitemapItemLoose[],
lastmod: string,
): void {
const webPages = Array.from(this.pagesMap.values());
for (const language of [Language.pl, Language.en]) {
for (const webPage of webPages) {
const alternates = [
{ lang: Language.pl, url: webPage.getUrl(Language.pl) },
{ lang: Language.en, url: webPage.getUrl(Language.en) },
];
const pictures = webPage.getDisplayedPictures();
const images = pictures.map((picture) => ({
url: picture.getUrl(),
title: picture.getSiteTitle(language),
caption: picture.getLocalisedDescription(language).caption,
geoLocation: Picture.geoSitemapLocation(language),
}));
entries.push({
url: webPage.getUrl(language),
lastmod,
changefreq: webPage.getFrequency(),
priority: webPage.getPriority(),
links: alternates,
img: images,
});
}
}
}
getRobotsTxt(): string {
if (!this.cache.robotsTxt) {
this.cache.robotsTxt = this.generateRobotsTxt();
}
return this.cache.robotsTxt;
}
protected generateRobotsTxt(): string {
return `User-agent: *
Disallow: /*?
Sitemap: ${this.domainUrl}${Routes.Google.SITEMAP}
`;
}
configureSiteAndRobots(app: Express): void {
// Obsługa robots.txt
app.get(Routes.Google.ROBOTS, (_, res) => {
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Cache-Control', 'public, max-age=3600');
res.send(this.getRobotsTxt());
});
// Obsługa sitemap
app.get(
[Routes.Google.SITEMAP, Routes.Google.SITEMAP_XML],
async (_, res) => {
try {
const siteMapXml = await this.getSitemap();
res.setHeader('Content-Type', 'application/xml');
res.setHeader('Cache-Control', 'public, max-age=3600');
if (!isProduction) {
this.saveSitemapToFile(siteMapXml);
}
res.send(siteMapXml);
} catch (error) {
console.error('Error generating sitemap:', error);
res.status(500).send('Error generating sitemap');
}
},
);
}
private saveSitemapToFile(xmlContent: string): void {
try {
const filePath = `${this.paths.getDomainCacheDir()}/sitemap.xml`;
const formattedXml = formatXml(xmlContent, {
indentation: ' ',
collapseContent: true,
lineSeparator: '\n',
});
fs.writeFileSync(filePath, formattedXml, 'utf8');
} catch (error) {
console.error('Error saving sitemap file:', error);
}
}
getName(): string {
return "t('webSite.name')";
}
getDescription(): string {
return "t('webSite.description')";
}
getLogo(): Picture | undefined {
return this.logo;
}
abstract getHomePage(): WebPage;
abstract getAboutMePage(): WebPage;
getWelcomePage(): WebPage {
return this.getHomePage();
}
}