UNPKG

15.3 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const _ = require("lodash");
4const cookie = require("cookie");
5const fs = require("fs");
6const moment = require("moment");
7const OS = require("os");
8const request = require("request");
9const Debug = require("debug");
10const url_parser = require("url");
11const seo_1 = require("./seo");
12const common_1 = require("./common");
13const debug = Debug("bablic:seo");
14const BABLIC_ROOT = "https://www.bablic.com";
15function escapeRegex(str) {
16 return str.replace(/([.?+^$[\]\\(){}|-])/g, "\\$1");
17}
18const Defaults = {
19 siteId: null,
20 rootUrl: null,
21 locale: null,
22 subDir: false,
23 subDirBase: "",
24 subDirOptional: false,
25 onReady: null,
26 seo: {
27 useCache: true,
28 defaultCache: [],
29 test: false,
30 cacheDir: OS.tmpdir() + "/bpCache",
31 },
32 folders: null,
33};
34const BackwardCompOptions = {
35 siteId: ["site_id"],
36 rootUrl: ["root_url"],
37 subDir: ["subdir", "sub_dir"],
38 subDirBase: ["subdir_base"],
39 subDirOptional: ["subdir_optional"],
40};
41exports.BackwardCompSEOOptions = {
42 useCache: ["use_cache"],
43 defaultCache: ["default_cache"],
44};
45class BablicSDK {
46 constructor(options) {
47 this.meta = null;
48 this.lastModified = null;
49 this.snippet = "";
50 this.keywordsByLocale = null;
51 this.reverseKeywordByLocale = null;
52 this.handle = (req, res, next) => this.handler(req, res, next);
53 let generalOptions = options;
54 for (let key in BackwardCompOptions) {
55 if (!options[key]) {
56 BackwardCompOptions[key].forEach((alt) => {
57 if (generalOptions[alt]) {
58 generalOptions[key] = generalOptions[alt];
59 }
60 });
61 }
62 }
63 if (options.seo) {
64 for (let key in exports.BackwardCompSEOOptions) {
65 if (!options.seo[key]) {
66 exports.BackwardCompSEOOptions[key].forEach((alt) => {
67 if (generalOptions.seo[alt]) {
68 generalOptions.seo[key] = generalOptions.seo[alt];
69 }
70 });
71 }
72 }
73 }
74 if (!options.siteId) {
75 throw new Error("Middleware requires and site_id");
76 }
77 this.options = _.defaultsDeep(options, Defaults);
78 this.seoHandler = new seo_1.SeoMiddleware(this.options.siteId, this.options.seo, { subDir: this.options.subDir, subDirBase: this.options.subDirBase, subDirOptional: this.options.subDirOptional });
79 this.seoMiddleware = this.seoHandler.middleware();
80 if (this.options.meta) {
81 this.meta = this.options.meta;
82 this.processKeywords(this.options.keywords);
83 }
84 if (this.options.snippet) {
85 this.snippet = this.options.snippet;
86 }
87 this.lastModified = this.options.lastModified;
88 if (this.meta && this.snippet) {
89 if (this.options.onReady) {
90 this.options.onReady();
91 }
92 return;
93 }
94 this.loadSiteMeta(() => {
95 if (this.options.onReady) {
96 this.options.onReady();
97 }
98 });
99 }
100 getSiteMeta(cbk) {
101 debug("getting from bablic");
102 request({
103 method: "GET",
104 url: `${BABLIC_ROOT}/api/v1/site/${this.options.siteId}?channel_id=node`,
105 }, (error, response, body) => {
106 if (error) {
107 return cbk(error);
108 }
109 if (!body) {
110 return cbk(new Error("empty response"));
111 }
112 try {
113 let data;
114 if (typeof (body) === "string") {
115 data = JSON.parse(body);
116 }
117 else {
118 data = body;
119 }
120 debug("data:", data);
121 this.saveSiteMeta(data);
122 cbk();
123 }
124 catch (e) {
125 debug(e);
126 }
127 });
128 }
129 saveSiteMeta(data) {
130 let { snippet, meta, lastModified } = data;
131 this.snippet = snippet;
132 this.meta = meta;
133 this.lastModified = lastModified;
134 this.processKeywords(data.keywords);
135 this.LOCALE_REGEX = null;
136 data.id = this.options.siteId;
137 fs.writeFile(this.snippetUrl(), JSON.stringify(data), (error) => {
138 if (error) {
139 console.error("Error saving snippet to cache", error);
140 }
141 });
142 }
143 snippetUrl() {
144 return `${OS.tmpdir()}/snippet.${this.options.siteId}`;
145 }
146 getLocale(req) {
147 if (req.headers["bablic-locale"]) {
148 return req.headers["bablic-locale"];
149 }
150 let auto = this.meta.autoDetect;
151 let defaultLocale = this.meta.default;
152 let customUrls = this.meta.customUrls;
153 let localeKeys = this.meta.localeKeys.slice();
154 localeKeys.push(this.meta.original);
155 let localeDetection = this.meta.localeDetection;
156 if (this.options.subDir) {
157 localeDetection = "subdir";
158 }
159 return common_1.getLocaleByURL(url_parser.parse(getCurrentUrl(req)), localeDetection, customUrls, detectLocaleFromCookie(req, this.meta), defaultLocale, auto ? detectLocaleFromHeader(req) : "", false, this.options.locale, this.options.subDirBase, this.options.folders, localeKeys);
160 }
161 loadSiteMeta(cbk) {
162 debug("loading meta from file");
163 fs.readFile(this.snippetUrl(), (error, data) => {
164 if (error) {
165 debug("no local file, getting from server");
166 return this.getSiteMeta(cbk);
167 }
168 debug("reading from temp file");
169 try {
170 let object = JSON.parse(data.toString("utf8"));
171 if (object.id != this.options.siteId || object.error) {
172 debug("not of this site id");
173 return this.getSiteMeta(cbk);
174 }
175 this.meta = object.meta;
176 this.snippet = object.snippet;
177 this.lastModified = object.lastModified;
178 this.processKeywords(object.keywords);
179 cbk();
180 }
181 catch (e) {
182 debug(e);
183 return this.getSiteMeta(cbk);
184 }
185 debug("checking snippet time");
186 fs.stat(this.snippetUrl(), (e, stats) => {
187 if (e) {
188 return cbk();
189 }
190 let last_modified = moment(stats.mtime.getTime());
191 if (last_modified > moment().subtract(4, "hours")) {
192 return debug("snippet cache is good");
193 }
194 debug("refresh snippet");
195 this.getSiteMeta(() => debug("refreshed snippet"));
196 });
197 });
198 }
199 handleBablicCallback(req, res) {
200 this.getSiteMeta(() => debug("site snippet refreshed"));
201 res.end("OK");
202 }
203 getLink(locale, url) {
204 let parsed = url_parser.parse(url);
205 return common_1.getLink(locale, parsed, this.meta, {
206 subDir: this.options.subDir,
207 subDirBase: this.options.subDirBase,
208 subDirOptional: this.options.subDirOptional,
209 folders: this.options.folders,
210 returnFull: true,
211 });
212 }
213 altTags(url, locale) {
214 let locales = this.meta.localeKeys || [];
215 let tags = _(locales)
216 .concat([this.meta.original])
217 .without(locale)
218 .map((l) => `<link rel="alternate" href="${this.getLink(l, url)}" hreflang="${l == this.meta.original ? "x-default" : l}">`)
219 .valueOf();
220 return tags.join("");
221 }
222 purgeCache() {
223 if (!this.seoHandler)
224 return Promise.resolve();
225 return this.seoHandler.purgeCache();
226 }
227 generateOriginalPath(url, locale) {
228 let urlParts = url.split("?");
229 let pathname = urlParts[0];
230 let pathParts = pathname.split('.');
231 let ext = pathParts.length > 1 ? '.' + pathParts[pathParts.length - 1] : '';
232 let pathNoExt = pathParts.length > 1 ? pathParts.slice(0, pathParts.length - 1).join('.') : pathname;
233 let reversed = this.reverseKeywordByLocale[locale];
234 let original = pathNoExt.split("/").map((p) => reversed[p] || p).join("/");
235 if (original != pathNoExt) {
236 urlParts[0] = original + ext;
237 return urlParts.join("?");
238 }
239 else {
240 return null;
241 }
242 }
243 generateTranslatedPath(url, locale) {
244 let urlParts = url.split("?");
245 let pathname = urlParts[0];
246 let pathParts = pathname.split('.');
247 let ext = pathParts.length > 1 ? '.' + pathParts[pathParts.length - 1] : '';
248 let pathNoExt = pathParts.length > 1 ? pathParts.slice(0, pathParts.length - 1).join('.') : pathname;
249 let proper = this.keywordsByLocale[locale];
250 let translated = pathNoExt.split("/").map((p) => proper[p] || p).join("/");
251 if (translated != pathNoExt) {
252 urlParts[0] = translated + ext;
253 return urlParts.join("?");
254 }
255 else {
256 return null;
257 }
258 }
259 handler(_req, _res, next) {
260 const req = _req;
261 const res = _res;
262 if (!req.originalUrl) {
263 req.originalUrl = req.url;
264 }
265 if ((req.originalUrl == "/_bablicCallback" && req.method == "POST") || req.headers["x-bablic-refresh"]) {
266 debug("Redirecting to Bablic callback");
267 return this.handleBablicCallback(req, res);
268 }
269 res.setHeader("x-bablic-id", this.options.siteId);
270 if (!this.LOCALE_REGEX && this.options.subDir && this.meta && this.meta.localeKeys) {
271 this.LOCALE_REGEX = RegExp("^(?:" + escapeRegex(this.options.subDirBase) + ")?\\/(" + this.meta.localeKeys.join("|") + ")\\b");
272 }
273 if (!this.meta) {
274 debug("not loaded yet", req.originalUrl);
275 req.bablic = {
276 locale: "",
277 };
278 extendResponseLocals(res, {
279 bablic: {
280 locale: "",
281 snippet: "",
282 snippetBottom: "<!-- Bablic Footer OFF -->",
283 snippetTop: "<!-- Bablic Head OFF -->",
284 },
285 });
286 return next();
287 }
288 let locale = req.forceLocale || this.options.forceLocale || this.getLocale(req);
289 req.bablic = {
290 locale,
291 proxied: false,
292 };
293 let _snippet = this.snippet;
294 if (this.meta.original == locale) {
295 _snippet = _snippet.replace("<script", "<script async");
296 }
297 if (this.options.subDir && this.LOCALE_REGEX) {
298 req.url = req.url.replace(this.LOCALE_REGEX, "");
299 req.originalUrl = req.originalUrl.replace(this.LOCALE_REGEX, "");
300 _snippet = `<script>var bablic=bablic||{};bablic.localeURL="subdir";bablic.subDirBase="${this.options.subDirBase}";bablic.subDirOptional=${!!this.options.subDirOptional};</script>` + _snippet;
301 }
302 if (this.reverseKeywordByLocale && this.reverseKeywordByLocale[locale]) {
303 let original = this.generateOriginalPath(req.url, locale);
304 // build original URL, so server will return proper content
305 if (original) {
306 req.url = original;
307 req.originalUrl = this.generateOriginalPath(req.originalUrl, locale) || req.originalUrl;
308 }
309 else {
310 // check to see if there is a translated URL, if so, it should be redirected to it
311 let translated = this.generateTranslatedPath(req.originalUrl, locale);
312 if (translated) {
313 res.writeHead(301, { location: translated });
314 return res.end();
315 }
316 }
317 }
318 let fullUrl = req.originalUrl;
319 if (this.options.rootUrl) {
320 const rootParsed = url_parser.parse(this.options.rootUrl);
321 fullUrl = rootParsed.protocol + '//' + rootParsed.hostname + req.originalUrl;
322 }
323 extendResponseLocals(res, {
324 bablic: {
325 locale,
326 snippet: _snippet,
327 snippetBottom: "",
328 snippetTop: "<!-- start Bablic Head -->" + this.altTags(fullUrl, locale) + _snippet + "<!-- start Bablic Head -->",
329 },
330 });
331 if (!this.seoMiddleware) {
332 return next();
333 }
334 if (locale == this.meta.original) {
335 debug("ignored same language", req.url);
336 return next();
337 }
338 return this.seoMiddleware(this.meta, this.lastModified, this.keywordsByLocale, this.reverseKeywordByLocale, req, res, next);
339 }
340 processKeywords(keywords) {
341 if (!keywords) {
342 return;
343 }
344 this.keywordsByLocale = {};
345 this.reverseKeywordByLocale = {};
346 this.meta.localeKeys.forEach((locale) => {
347 let proper = {};
348 let reverse = {};
349 for (let keyword in keywords) {
350 if (!keywords[keyword][locale]) {
351 continue;
352 }
353 proper[keyword] = keywords[keyword][locale];
354 reverse[keywords[keyword][locale]] = keyword;
355 }
356 this.keywordsByLocale[locale] = proper;
357 this.reverseKeywordByLocale[locale] = reverse;
358 });
359 }
360}
361exports.BablicSDK = BablicSDK;
362function extendResponseLocals(res, context) {
363 if (typeof (res.locals) == "function") {
364 res.locals(context);
365 }
366 else if (res.locals) {
367 _.extend(res.locals, context);
368 }
369 else {
370 res.locals = context;
371 }
372}
373function detectLocaleFromHeader(req) {
374 let header = req.headers["accept-language"];
375 if (!header) {
376 return "";
377 }
378 let langs = header.split(",");
379 if (langs.length > 0) {
380 return langs[0].replace("-", "_");
381 }
382 return "";
383}
384function detectLocaleFromCookie(req, meta) {
385 let cookieHeader = req.headers.cookie;
386 if (!cookieHeader) {
387 return "";
388 }
389 if (!meta.localeKeys) {
390 return "";
391 }
392 let cookies = req.cookies || cookie.parse(cookieHeader);
393 if (!cookies) {
394 return "";
395 }
396 let bablicCookie = cookies.bab_locale;
397 if (!bablicCookie) {
398 return "";
399 }
400 let index = meta.localeKeys.indexOf(bablicCookie);
401 if (index > -1) {
402 return bablicCookie;
403 }
404 let partialFound = _.find(meta.localeKeys, (l) => l[0] == bablicCookie[0] && l[1] == bablicCookie[1]);
405 return partialFound || "";
406}
407function getCurrentUrl(req) {
408 return `http://${req.headers.host}${req.originalUrl}`;
409}