UNPKG

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