293 lines
8.6 KiB
TypeScript
Executable File
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();
|
|
}
|
|
}
|