attempt implementing prerendering

This commit is contained in:
Phillip Thelen
2025-02-04 17:43:30 +01:00
parent d46fa98390
commit e10b7deb4b
11 changed files with 3907 additions and 77 deletions
+36
View File
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Habitica - Gamify Your Life</title>
<meta name="description" content="Habitica is a free habit and productivity app that treats your real life like a game. Habitica can help you achieve your goals to become healthy and happy.">
<meta name="keywords" content="Habits,Goals,Todo,Gamification,Health,Fitness,School,Work">
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700,700i|Roboto:400,400i,700,700i" rel="stylesheet">
<link rel="shortcut icon" sizes="48x48" href="/static/icons/favicon.ico">
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
<link rel="mask-icon" href="/static/icons/favicon.ico">
<meta property="og:image" content="/static/emails/images/meta-image.png" />
<script type="module" src="/src/main-static.js"></script>
</head>
<body>
<div id="loading-screen">
<svg width="80" height="80" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg">
<path
xmlns="http://www.w3.org/2000/svg"
fill-rule="evenodd"
clip-rule="evenodd"
d="M79.05 72.15c-.8-1.766-2.643-2.62-3.845-1.766-1.201.855-2.867.985-4.448.602-1.584-.385-1.885-4.01-1.543-8.195.342-4.184.909-5.795 1.267-7.314.404-1.524 2.191-1.404 2.405-.209.215 1.196 1.454 1.196 3.266-.979 1.811-2.175 1.543-8.52-.546-13.684-2.088-5.163.817-4.661 1.66-4.149.844.513 1.362-.255 1.156-3.2-.204-2.945-2.916-5.247-5.096-6.657-2.184-1.41-4.842-2.967-4.78-6.745.063-3.777 5.2-3.658 5.897-3.596.697.063 2.037-.233 1.264-4.157-.773-3.924-3.575-4.673-5.332-4.567-1.758.106-2.943 1.071-5.427.133-2.484-.938-4.136-.572-6.45-.057-2.313.515-5.343 1.94-9.112 2.959-1.989.545-2.661.683-4.828.718-1.33.02-1.885 1.633-.106 3.61 1.408 1.608 4.597 2.036 6.515 1.768 1.236-.174 1.521.645 1.407 1.85a20.023 20.023 0 0 0-.024 4.488c.198 1.5.45 4.051-.258 5.713-.35.817-1.361 1.693-2.449 1.633-1.413-.084-2.555-1.75-3.537-3.726-2.06-4.152-4.48-5.033-13.509-8.835-8.12-3.417-12.516-8.749-15.24-12.185-2.421-3.042-4.846-1.89-4.626.855.179 2.128 1.48 9.008 4.781 13.141 4.058 6.314 10.32 9.177 17.534 9.739 1.885.149 3.065.52 3.225 1.383.236 1.835-1.557 3.11-4.898 2.722-3.341-.39-4.768.22-4.103 2.121 2.123 4.477 7.021 4.672 9.058 4.857.686.122 3.114 0 4.41.355 1.51.418 1.836 2.514-.353 3.648-3.892 1.903-5.59 3.479-7.561 7.075-1.486 2.826-2.77 7.555-1.435 14.365 1.283 6.62-8.342 6.83-12.497 5.89-1.793-.377-3.675-3.778.716-6.625 3.553-2.305 4.269-3.724 4.111-6.642-.184-3.4-2.058-3.644-2.053-6.598v-7.05c0-.602-.488-1.088-1.087-1.088h-3.334a1.087 1.087 0 0 1-1.087-1.087v-4.25c0-.602-.488-1.087-1.088-1.087h-3.317a1.087 1.087 0 0 1-1.087-1.088v-3.81c0-.602-.489-1.087-1.088-1.087h-4.04a1.087 1.087 0 0 1-1.089-1.088V26.25c0-.602-.488-1.088-1.087-1.088H1.088C.485 25.161 0 25.65 0 26.25v4.26c0 .602.488 1.087 1.088 1.087h4.049c.602 0 1.087.489 1.087 1.088v15.192c0 .602.489 1.087 1.088 1.087h4.277c.602 0 1.088.489 1.088 1.088v4.968c0 .602.488 1.087 1.087 1.087h6.005c1.836-.13 2.156 2.335 2.137 3.214-.04 2.007-2.308 2.652-3.382 3.487-2.861 2.21-5.077 4.459-3.78 8.781l.032.09c2.362 5.017 8.855 4.499 12.956 4.499h25.817c1.459 0 2.959.339 2.614-1.362-.342-1.7-1.063-4.024-3.162-4.024-2.1 0-1.758 1.166-3.81.57-2.054-.597-2.057-3.371 1.027-8.198 3.19-4.122 8.652-3.81 11.952-.895 3.301 2.915 2.325 7.978 1.633 10.885-.396 2.048.545 3.06 1.67 3.032H78.58c2.015-.035 1.62-1.391.464-4.035h.008z"
fill="#fff"
>
</path>
</svg>
</div>
<div id="app"></div>
<script type="text/javascript" src="//cloudfront.loggly.com/js/loggly.tracker-latest.min.js" async></script>
<!-- Translations -->
</body>
</html>
+64
View File
@@ -0,0 +1,64 @@
import fs from 'fs';
import path from 'path';
import { approvedLanguages } from '../common/script/libs/i18n';
const localePath = path.join(__dirname, '../common/locales/');
const translations = {};
const momentLangs = {};
function loadFile (file) {
const contents = fs.readFileSync(file);
return JSON.parse(contents);
}
function _loadTranslations (locale) {
const files = fs.readdirSync(path.join(localePath, locale));
translations[locale] = {};
files.forEach(file => {
if (path.extname(file) !== '.json') return;
const t = loadFile(path.join(localePath, locale, file))
translations[locale] = {
...translations[locale],
...t,
}
});
}
// First fetch English strings so we can merge them with missing strings in other languages
_loadTranslations('en');
// Then load all other languages
approvedLanguages.forEach(file => {
if (file === 'en' || fs.statSync(path.join(localePath, file)).isDirectory() === false) return;
_loadTranslations(file);
// Strip empty strings, then merge missing strings from english
translations[file] = {
...translations.en,
...translations[file],
}
});
export default function localePlugin() {
const virtualModuleId = 'virtual:translations'
const resolvedVirtualModuleId = '\0' + virtualModuleId
return {
name: 'locale', // required, will show up in warnings and errors
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
return `export default ${JSON.stringify(translations)}`;
}
},
}
}
+3590 -9
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -40,6 +40,7 @@
"uuid": "^9.0.1",
"validator": "^13.9.0",
"vite": "^6.0.0",
"vite-plugin-prerender": "^1.0.8",
"vue": "^2.7.10",
"vue-fragment": "^1.6.0",
"vue-mugen-scroll": "^0.2.6",
@@ -49,6 +50,8 @@
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"@prerenderer/renderer-puppeteer": "^1.2.4",
"@prerenderer/rollup-plugin": "^0.3.12",
"@vitest/browser": "^3.0.5",
"babel-plugin-lodash": "^3.3.4",
"inspectpack": "^4.7.1",
+84
View File
@@ -0,0 +1,84 @@
import Vue from 'vue';
import axios from 'axios';
import {
ModalPlugin,
DropdownPlugin,
PopoverPlugin,
FormPlugin,
FormInputPlugin,
FormRadioPlugin,
TooltipPlugin,
NavbarPlugin,
CollapsePlugin,
} from 'bootstrap-vue';
import Fragment from 'vue-fragment';
import AppComponent from './app';
import {
setup as setupAnalytics,
} from '@/libs/analytics';
import { setUpLogging } from '@/libs/logging';
import router from './router/index';
import getStore from './store';
import StoreModule from './libs/store';
import './filters/registerGlobals';
import i18n from './libs/i18n';
import translations from 'virtual:translations';
let urlParams = new URLSearchParams(window.location.search);
let language = urlParams.get('lang');
window['habitica-i18n'] = {
language,
strings: translations[language],
};
const IS_PRODUCTION = import.meta.env.NODE_ENV === 'production'; // eslint-disable-line no-process-env
// Configure Vue global options, see https://vuejs.org/v2/api/#Global-Config
Vue.config.performance = !IS_PRODUCTION;
Vue.config.productionTip = IS_PRODUCTION;
// window['habitica-i18n] is injected by the server
Vue.use(i18n, { i18nData: window && window['habitica-i18n'] });
Vue.use(StoreModule);
Vue.use(ModalPlugin);
Vue.use(DropdownPlugin);
Vue.use(PopoverPlugin);
Vue.use(FormPlugin);
Vue.use(FormInputPlugin);
Vue.use(FormRadioPlugin);
Vue.use(TooltipPlugin);
Vue.use(NavbarPlugin);
Vue.use(CollapsePlugin);
Vue.use(Fragment.Plugin);
setUpLogging();
setupAnalytics(); // just create queues for analytics, no scripts loaded at this time
const store = getStore();
if (import.meta.env.TIME_TRAVEL_ENABLED === 'true') {
(async () => {
const sinon = await import('sinon');
if (axios.defaults.headers.common['x-api-user']) {
const response = await axios.get('/api/v4/debug/time-travel-time');
const time = new Date(response.data.data.time);
Vue.config.clock = sinon.useFakeTimers({
now: time,
shouldAdvanceTime: true,
});
}
})();
}
const vueInstance = new Vue({
el: '#app',
router,
store,
render: h => h(AppComponent),
});
export default vueInstance;
window.externalLink = url => {
vueInstance.$root.$emit('habitica:external-link', url);
};
-5
View File
@@ -27,12 +27,7 @@ const IS_PRODUCTION = import.meta.env.NODE_ENV === 'production'; // eslint-disab
// Configure Vue global options, see https://vuejs.org/v2/api/#Global-Config
// Enable perf timeline measuring for Vue components in Chrome Dev Tools
// Note: this has been disabled because it caused some perf issues
// if rendering becomes too slow in dev mode, we should turn it off
// See https://github.com/vuejs/vue/issues/5174
Vue.config.performance = !IS_PRODUCTION;
// Disable annoying reminder abour production build in dev mode
Vue.config.productionTip = IS_PRODUCTION;
// window['habitica-i18n] is injected by the server
+8
View File
@@ -0,0 +1,8 @@
import translations from 'virtual:translations';
let urlParams = new URLSearchParams(window.location.search);
let language = urlParams.get('lang');
window['habitica-i18n'] = {
language,
strings: translations[language],
};
+48 -2
View File
@@ -3,13 +3,16 @@ import path from 'path';
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue2'
import { fileURLToPath } from 'node:url'
import prerender from '@prerenderer/rollup-plugin'
import setupNconf from '../server/libs/setupNconf';
import localePlugin from './localePlugin';
import { approvedLanguages } from '../common/script/libs/i18n';
const configFile = path.join(path.resolve(__dirname, '../../config.json'));
setupNconf(configFile, nconf);
const DEV_BASE_URL = nconf.get('BASE_URL');
const envVars = [
'AMAZON_PAYMENTS_SELLER_ID',
'AMAZON_PAYMENTS_CLIENT_ID',
@@ -41,6 +44,14 @@ if (nconf.get('BASE_URL').indexOf('//habitica.com') !== -1) {
envObject['import.meta.env.TIME_TRAVEL_ENABLED'] = `false`;
}
const base_pages = ['/static/home', '/static/faq'];
const localed_pages = [];
approvedLanguages.forEach((lang) => {
base_pages.forEach((page) => {
localed_pages.push(`${page}?lang=${lang}`);
});
});
// https://vitejs.dev/config/
export default defineConfig({
define: envObject,
@@ -52,7 +63,36 @@ export default defineConfig({
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
plugins: [
vue()
localePlugin(),
vue(),
prerender({
routes: localed_pages,
renderer: '@prerenderer/renderer-puppeteer',
indexPath: 'index-static.html',
entryPath: 'index-static.html',
postProcess (renderedRoute) {
// Replace all http with https urls and localhost to your site url
renderedRoute.html = renderedRoute.html.replace(
/http:/ig,
'https:',
).replace(
/(https:\/\/)?(localhost|127\.0\.0\.1):\d*/ig,
(process.env.BASE_URL || ''),
);
const language = renderedRoute.originalRoute.split('?')[1].split('=')[1];
renderedRoute.outputPath = path.join(renderedRoute.route, `index.${language}.html`);
if (renderedRoute.outputPath.indexOf('/') === 0) {
renderedRoute.outputPath = renderedRoute.outputPath.slice(1);
}
console.log(`Prerendered ${renderedRoute.outputPath} - ${renderedRoute.html.length} bytes - ${renderedRoute.route}`);
},
rendererOptions: {
inject: {
prerendered: true,
},
}
}),
],
optimizeDeps: {
include: ['moment-recur']
@@ -60,6 +100,12 @@ export default defineConfig({
build: {
commonjsOptions: {
include: [/moment-recur/, /node_modules/]
},
rollupOptions: {
input: {
main: path.resolve(__dirname, 'index.html'),
'index-static.html': path.resolve(__dirname, 'index-static.html'),
}
}
},
base: '/',
+37
View File
@@ -0,0 +1,37 @@
export const approvedLanguages = [
'bg', 'cs', 'da', 'de', 'en', 'en_GB',
'es', 'es_419', 'fr', 'he', 'hu', 'id', 'it',
'ja', 'nl', 'pl', 'pt', 'pt_BR', 'ro', 'ru', 'sk',
'sr', 'sv', 'tr', 'uk', 'zh', 'zh_TW',
];
// A map of languages that have different versions and the relative versions
export const multipleVersionsLanguages = {
es: {
'es-419': 'es_419',
'es-mx': 'es_419',
'es-gt': 'es_419',
'es-cr': 'es_419',
'es-pa': 'es_419',
'es-do': 'es_419',
'es-ve': 'es_419',
'es-co': 'es_419',
'es-pe': 'es_419',
'es-ar': 'es_419',
'es-ec': 'es_419',
'es-cl': 'es_419',
'es-uy': 'es_419',
'es-py': 'es_419',
'es-bo': 'es_419',
'es-sv': 'es_419',
'es-hn': 'es_419',
'es-ni': 'es_419',
'es-pr': 'es_419',
},
zh: {
'zh-tw': 'zh_TW',
},
pt: {
'pt-br': 'pt_BR',
},
};
+14 -3
View File
@@ -2,12 +2,15 @@ import { serveClient } from '../../libs/client';
const api = {};
// All requests to /new_app (except /new_app/static) should serve the new client in development
// if (IS_PROD && IS_NEW_CLIENT_ENABLED) {
const ROOT = `${__dirname}/../../../client/dist/static`;
function serveStaticPage (expressRes, pageName, langCode) {
return expressRes.sendFile(`./${pageName}/index.${langCode}.html`, { root: ROOT });
}
// All the routes (except for the api and payments routes) serve the new client side
// The code that does it can be found in /middlewares/notFound.js
api.getNewClient = {
api.getClient = {
method: 'GET',
url: '/',
noLanguage: true,
@@ -16,4 +19,12 @@ api.getNewClient = {
},
};
api.getStaticHomme = {
method: 'GET',
url: '/static/home',
async handler (req, res) {
return serveStaticPage(res, 'home', req.language);
},
};
export default api;
+23 -58
View File
@@ -2,33 +2,16 @@ import fs from 'fs';
import path from 'path';
import _ from 'lodash';
import shared from '../../common';
import {
approvedLanguages as capprovedLanguages,
multipleVersionsLanguages as cmultipleVersionsLanguages,
} from '../../common/script/libs/i18n';
export const localePath = path.join(__dirname, '../../common/locales/');
export const BROWSER_SCRIPT_CACHE_PATH = path.join(__dirname, '/../../../i18n_cache/');
// Store translations
export const translations = {};
// Store MomentJS localization files
export const momentLangs = {};
// Handle differences in language codes between MomentJS and /locales
const momentLangsMapping = {
en: 'en-gb',
en_GB: 'en-gb', // eslint-disable-line camelcase
no: 'nn',
zh: 'zh-cn',
zh_TW: 'zh-tw', // eslint-disable-line camelcase
es_419: 'es', // eslint-disable-line camelcase
pt_BR: 'pt-br', // eslint-disable-line camelcase
};
export const approvedLanguages = [
'bg', 'cs', 'da', 'de', 'en', 'en_GB',
'es', 'es_419', 'fr', 'he', 'hu', 'id', 'it',
'ja', 'nl', 'pl', 'pt', 'pt_BR', 'ro', 'ru', 'sk',
'sr', 'sv', 'tr', 'uk', 'zh', 'zh_TW',
];
function _loadTranslations (locale) {
const files = fs.readdirSync(path.join(localePath, locale));
@@ -46,7 +29,7 @@ function _loadTranslations (locale) {
_loadTranslations('en');
// Then load all other languages
approvedLanguages.forEach(file => {
capprovedLanguages.forEach(file => {
if (file === 'en' || fs.statSync(path.join(localePath, file)).isDirectory() === false) return;
_loadTranslations(file);
@@ -55,8 +38,16 @@ approvedLanguages.forEach(file => {
_.defaults(translations[file], translations.en);
});
// Add translations to shared
shared.i18n.translations = translations;
// Handle differences in language codes between MomentJS and /locales
const momentLangsMapping = {
en: 'en-gb',
en_GB: 'en-gb', // eslint-disable-line camelcase
no: 'nn',
zh: 'zh-cn',
zh_TW: 'zh-tw', // eslint-disable-line camelcase
es_419: 'es', // eslint-disable-line camelcase
pt_BR: 'pt-br', // eslint-disable-line camelcase
};
export const langCodes = Object.keys(translations);
@@ -65,6 +56,11 @@ export const availableLanguages = langCodes.map(langCode => ({
name: translations[langCode].languageName,
}));
// Remove en_GB from langCodes checked by browser to avoid it being
// used in place of plain original 'en'
// (it's an optional language that can be enabled only in setting)
export const defaultLangCodes = _.without(langCodes, 'en_GB');
langCodes.forEach(code => {
const lang = _.find(availableLanguages, { code });
@@ -82,41 +78,10 @@ langCodes.forEach(code => {
}
});
// Remove en_GB from langCodes checked by browser to avoid it being
// used in place of plain original 'en'
// (it's an optional language that can be enabled only in setting)
export const defaultLangCodes = _.without(langCodes, 'en_GB');
// Add translations to shared
shared.i18n.translations = translations;
// A map of languages that have different versions and the relative versions
export const multipleVersionsLanguages = {
es: {
'es-419': 'es_419',
'es-mx': 'es_419',
'es-gt': 'es_419',
'es-cr': 'es_419',
'es-pa': 'es_419',
'es-do': 'es_419',
'es-ve': 'es_419',
'es-co': 'es_419',
'es-pe': 'es_419',
'es-ar': 'es_419',
'es-ec': 'es_419',
'es-cl': 'es_419',
'es-uy': 'es_419',
'es-py': 'es_419',
'es-bo': 'es_419',
'es-sv': 'es_419',
'es-hn': 'es_419',
'es-ni': 'es_419',
'es-pr': 'es_419',
},
zh: {
'zh-tw': 'zh_TW',
},
pt: {
'pt-br': 'pt_BR',
},
};
export const multipleVersionsLanguages = cmultipleVersionsLanguages;
export function geti18nBrowserScript (languageCode) {
const language = _.find(availableLanguages, { code: languageCode });